Custom button BackgroundColor not updating

GabrielBunselmeyerGabrielBunselmeyer Member ✭✭
edited August 2018 in Xamarin.Forms

Hey there!

I'm currently facing a problem trying to update the color of a CustomButton I've made when it's clicked. Have been searching the forums/internet in general for a solution, but no luck yet. Being new to Xamarin Forms, I'm quite unsure of the way I've been doing things, and assume I'm missing something to make it work.

Anyway: I need a different button for each item of a table in my database. To achieve this dynamic creation, I've resorted to creating the custombuttons in code-behind instead of Xaml. Only the color of the clicked button has to change, so to bind the BackgroundColorProperty I'd need a different property for each one in my ViewModel; and, as the number of buttons is dynamic, I don't see a way of doing that.

So I created a command (using Prism) that receives the clicked button as a parameter (which isn't exactly right as far as MVVM goes, I guess) to change just its BackgroundColor. Thing is, it doesn't work if the BackgroundColor has already been set. If it hasn't it works fine, though until the user clicks the button its background is transparent (hasn't been set).

I've only tested with Android so far, but I assume it's a problem with the custom renderer? As in the CustomRenderer might not be getting the event(?) to redraw the background. But if this is the case, why would it work if the color isn't set yet?

Might be obvious, but I'm still quite new to Xamarin Forms, and I fear my limited experience won't be enough to solve this, as I've been trying for longer than I'd like to admit already.

Custom button code:

   public class CustomResidueButton : Button
                    {

                        public static readonly BindableProperty CustomTextProperty =
                            BindableProperty.Create("CustomText", typeof(string), typeof(CustomResidueButton), string.Empty);

                        public string CustomText
                        {
                            get { return (string)GetValue(CustomTextProperty); }
                            set { SetValue(CustomTextProperty, value); }
                        }


                        public static readonly BindableProperty CustomImageProperty =
                            BindableProperty.Create("CustomImage", typeof(string), typeof(CustomResidueButton), string.Empty);

                        public string CustomImage
                        {
                            get { return (string)GetValue(CustomImageProperty); }
                            set { SetValue(CustomImageProperty, value); }
                        }

                        public static readonly BindableProperty ResidueIdProperty =
                            BindableProperty.Create("ResidueId", typeof(string), typeof(CustomResidueButton), string.Empty);

                        public string ResidueId
                        {
                            get { return (string)GetValue(ResidueIdProperty); }
                            set { SetValue(ResidueIdProperty, value); }
                        }

                    }

Custom buttons creation in the main-page's code behind, called on the constructor:

private void CreateAndLoadResidueButtons(Grid filterGrid)
            {
                if (APIUtil.ResidueList != null)
                {
                    int row = 1;
                    int column = 0;

                    foreach (Residue residue in APIUtil.ResidueList)
                    {
                        CustomResidueButton btn = new CustomResidueButton
                        {
                            //Style = residueBtnStyle,
                            CustomText = residue.residueName,
                            CustomImage = "tire",
                            ResidueId = residue.residueId.ToString(),

                            Command = ((MainMapViewModel)BindingContext).ResidueSelectedCommand,
                        };

                        Thickness margin = new Thickness();

                        if (row == 3)
                            margin.Bottom = 6;
                        if (column == 0)
                            margin.Left = 6;
                        if (column == 2)
                            margin.Right = 6;

                        btn.Margin = margin;

                        btn.CommandParameter = btn;

                        filterGrid.Children.Add(btn, column, row);

                        if (column == 2)
                        {
                            column = 0;
                            row++;
                        }
                        else
                            column++;
                    }
                }
            }

Command for when a button is clicked:

    public DelegateCommand<object> ResidueSelectedCommand { get; set; }
            private void CreateCommands()
            {
                ResidueSelectedCommand = new DelegateCommand<object>(ResidueSelectedExecute);
            }

            private void ResidueSelectedExecute(object selectedButton)
            {
                if (CanClick)
                {
                    CanClick = false;

                    MessagingCenter.Send(new MessageService(), "ChangedResidueType");

                    CustomResidueButton btn = selectedButton as CustomResidueButton;
                    int resId = int.Parse(btn.ResidueId);

                    if (SelectedResidueIdList.Any(i => i == resId))
                    {
                        SelectedResidueIdList.Remove(resId);
                        btn.BackgroundColor = Color.Pink;

                    } else {
                        SelectedResidueIdList.Add(resId);
                        btn.BackgroundColor = Color.Yellow;
                    }

                    if (SelectedResidueIdList.Count > 0)
                        LoadPins(APIUtil.GetSitesOfResidueList(SelectedResidueIdList));
                    else
                        LoadPins(APIUtil.GetAllSites());

                    CanClick = true;
                }
            }

And finally, the Android custom renderer:

class ResidueButtonRenderer : ViewRenderer<CustomResidueButton, Android.Views.View>
        {
            private readonly Context _context;

            public ResidueButtonRenderer(Context context) : base(context)
            {
                _context = context;
            }

            private TextView _residueNameView;
            private ImageView _residueIconView;

            protected override void OnElementChanged(ElementChangedEventArgs<CustomResidueButton> e)
            {
                base.OnElementChanged(e);

                //e.NewElement.CommandParameter = Element.CommandParameter;

                if (Control == null)
                {
                    // Element é o CustomResidueButton
                    // inflate no layout do botão
                    var inflater = _context.GetSystemService(Context.LayoutInflaterService) as LayoutInflater;
                    var rootLayout = inflater.Inflate(Resource.Layout.ResidueButton, null, false);

                    _residueNameView = rootLayout.FindViewById(Resource.Id.ResidueName) as TextView;
                    _residueNameView.Text = Element.CustomText;

                    // usa a string do CustomResidueButton de nome pra pegar imagem do drawable
                    int imageId = Context.Resources.GetIdentifier(Element.CustomImage, "drawable", Context.PackageName);
                    _residueIconView = rootLayout.FindViewById(Resource.Id.ResidueIcon) as ImageView;
                    _residueIconView.SetImageResource(imageId);

                    SetBackground(rootLayout);
                    SetNativeControl(rootLayout);

                    // executa os Commands, o conteudo de Execute() é o CommandParameter
                    rootLayout.Click += (s, a) => Element.Command?.Execute(Element.CommandParameter);
                }
            }

            private void SetBackground(Android.Views.View rootLayout)
            {
                var backgroundColor = Element.BackgroundColor.ToAndroid();

                var enabledBackground = new GradientDrawable(GradientDrawable.Orientation.LeftRight, new int[] { backgroundColor, backgroundColor });
                var stateList = new StateListDrawable();
                var rippleItem = new RippleDrawable(ColorStateList.ValueOf(Android.Graphics.Color.White), enabledBackground, null);

                stateList.AddState(new[] { Android.Resource.Attribute.StateEnabled }, rippleItem);

                rootLayout.Background = stateList;
            }

        }

At this point any kind of help would be appreciated, even if it's just telling me I'm doing things completely wrong. :tongue:
Thanks in advance!

Best Answers

  • Accepted Answer

    Finally got a fix for this.

    Basically, I didn't fully understand how Android inflaters worked, so changing

    var rootLayout = Inflater.Inflate(Resource.Layout.ResidueButton, null);
    

    to

    var rootLayout = Inflater.Inflate(Resource.Layout.ResidueButton, this);
    

    did the trick. Yes, that simple. It's proving a bit complicated working with custom renderers without previous experience in native Android/iOs. Final code for the OnElementPropertyChanged was:

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                if (e.PropertyName == CustomResidueButton.BackgroundColorProperty.PropertyName)
                {
                    CustomResidueButton senderBtn = sender as CustomResidueButton;
    
                    if (senderBtn != null)
                    {
                        var rootLayout = Inflater.Inflate(Resource.Layout.ResidueButton, this);
    
                        LinearLayout layout = rootLayout.FindViewById<LinearLayout>(Resource.Id.residueBtnLayout);
                        layout.SetBackgroundColor(senderBtn.BackgroundColor.ToAndroid());
                    }
                } else
                {
                    base.OnElementPropertyChanged(sender, e);
                }
            }
    

    Thanks John anyway for the quick responses, fortunally was able to get it working on my own.

Answers

  • Can't seem to fix the code tags for whatever reason either.

  • Tried adding OnElementPropertyChanged to the custom renderer, but still no luck. It gets called fine, yet if the backgroundcolor is set during the button's creation it still won't update to a new one.

  • JohnHardmanJohnHardman GBUniversity mod

    @GabrielBunselmeyer said:
    Can't seem to fix the code tags for whatever reason either.

    Put three back ticks (with nothing in between) on the line before each code block, and three back ticks (also with nothing in between) on the line after each code block. On my keyboard the back tick is on the key to the left of the "1" above the Qwerty keys. In case your keyboard is different there is one back tick at the end of this sentence that you can copy and paste `

  • @JohnHardman said:

    @GabrielBunselmeyer said:
    Can't seem to fix the code tags for whatever reason either.

    Put three back ticks (with nothing in between) on the line before each code block, and three back ticks (also with nothing in between) on the line after each code block. On my keyboard the back tick is on the key to the left of the "1" above the Qwerty keys. In case your keyboard is different there is one back tick at the end of this sentence that you can copy and paste `

    Fixed it, thanks! One less issue with the post, haha.

  • JohnHardmanJohnHardman GBUniversity mod

    @GabrielBunselmeyer - I'm in the midst of debugging something right now, so haven't taken a good look at your post. However, one thing you might need is an override of OnElementPropertyChanged in your custom renderer, watching for changes to the BackgroundColorProperty.

  • GabrielBunselmeyerGabrielBunselmeyer Member ✭✭
    edited August 2018

    @JohnHardman said:
    @GabrielBunselmeyer - I'm in the midst of debugging something right now, so haven't taken a good look at your post. However, one thing you might need is an override of OnElementPropertyChanged in your custom renderer, watching for changes to the BackgroundColorProperty.

    Tried implementing it yesterday like so:

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                base.OnElementPropertyChanged(sender, e);
    
                if (e.PropertyName == CustomResidueButton.BackgroundColorProperty.PropertyName)
                {
                    var inflater = _context.GetSystemService(Context.LayoutInflaterService) as LayoutInflater;
                    var rootLayout = inflater.Inflate(Resource.Layout.ResidueButton, null);
    
                    CustomResidueButton btn = (CustomResidueButton)sender;
                    if (btn != null)
                    {
                        var enabledBackground = new GradientDrawable(GradientDrawable.Orientation.LeftRight, new int[] { btn.BackgroundColor.ToAndroid(), btn.BackgroundColor.ToAndroid() });
                        var stateList = new StateListDrawable();
                        var rippleItem = new RippleDrawable(ColorStateList.ValueOf(Android.Graphics.Color.White), enabledBackground, null);
    
                        stateList.AddState(new[] { Android.Resource.Attribute.StateEnabled }, rippleItem);
    
                        rootLayout.Background = stateList;
                    }
                } 
            }
    

    It gets called fine, but doesn't solve the problem. The overridden OnElementPropertyChanged happens, and yet the color doesn't change. What am I missing here?

    It seems the color passed to the OnElementPropertyChanged is always the same one if it's set, which would imply (guessing here) I'm not getting the right button element when calling .BackgroundColor. And yet, if the color isn't set before the click, the same code works to change it. I'm honestly quite confused at what I'm doing wrong.

    By set I mean the BackgroundColor line below:

    CustomResidueButton btn = new CustomResidueButton
                        {
                            //Style = residueBtnStyle,
                            CustomText = residue.residueName,
                            CustomImage = "tire",
                            ResidueId = residue.residueId.ToString(),
                            BackgroundColor = Color.Blue,
    
                            Command = ((MainMapViewModel)BindingContext).ResidueSelectedCommand
                        };
    
  • Ok so it seems the code in my Custom Renderer is wrong; the background color isn't set even using .SetBackgroundColor. Might be working with the wrong view somehow; will take a look on inflaters and whatnot.

  • GabrielBunselmeyerGabrielBunselmeyer Member ✭✭
    Accepted Answer

    Finally got a fix for this.

    Basically, I didn't fully understand how Android inflaters worked, so changing

    var rootLayout = Inflater.Inflate(Resource.Layout.ResidueButton, null);
    

    to

    var rootLayout = Inflater.Inflate(Resource.Layout.ResidueButton, this);
    

    did the trick. Yes, that simple. It's proving a bit complicated working with custom renderers without previous experience in native Android/iOs. Final code for the OnElementPropertyChanged was:

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                if (e.PropertyName == CustomResidueButton.BackgroundColorProperty.PropertyName)
                {
                    CustomResidueButton senderBtn = sender as CustomResidueButton;
    
                    if (senderBtn != null)
                    {
                        var rootLayout = Inflater.Inflate(Resource.Layout.ResidueButton, this);
    
                        LinearLayout layout = rootLayout.FindViewById<LinearLayout>(Resource.Id.residueBtnLayout);
                        layout.SetBackgroundColor(senderBtn.BackgroundColor.ToAndroid());
                    }
                } else
                {
                    base.OnElementPropertyChanged(sender, e);
                }
            }
    

    Thanks John anyway for the quick responses, fortunally was able to get it working on my own.

  • @JohnHardman said:
    @GabrielBunselmeyer - Glad it's working and that it was the need for OnElementPropertyChanged as per earlier post.

    One thing you might want to consider is whether you should call base.OnElementPropertyChanged(sender, e); before your handling of BackgroundColorProperty. I suspect you're safe not to in this circumstance (although haven't checked the XF source code to make sure), but as a general pattern I tend to call the base class before then doing my own customisation in my custom renderers.

    Oh, you're definitely right. Changed it for testing purposes and forgot to put it back.
    I appreciate the support!

Sign In or Register to comment.