Binding between custom Usercontrol and ViewModel not working?

kkopkkop PLMember
edited September 2016 in Xamarin.Forms

Hi!

I've created a custom usercontrol in my application and I try to bind to its property (BindableProperty) to ViewModel but it doesn't work for me. Am I doing something wrong?

This is the usercontrol. It is just custom stepper for the purpose of test project: "decrease" and "increase" buttons and quantity label between them.
Please notice that binding inside the usercontrol works perfect (for example Commands bindings) . What I am trying to do is bind QuantityProperty to ViewModel

namespace UsercontrolBindingTest.Usercontrols
{
    using System.Windows.Input;
    using Xamarin.Forms;

    public class CustomQuantityStepper : ContentView
    {
        public static readonly BindableProperty QuantityProperty =
            BindableProperty.Create(nameof(Quantity), typeof(int), typeof(CustomQuantityStepper), 0, BindingMode.TwoWay);

        public int Quantity
        {
            get
            {
                return (int)base.GetValue(QuantityProperty);
            }

            set
            {
                base.SetValue(QuantityProperty, value);
                this.OnPropertyChanged(nameof(this.Quantity));
            }
        }

        public ICommand DecreaseQuantityCommand { get; private set; }

        public ICommand IncreaseQuantityCommand { get; private set; }

        public CustomQuantityStepper()
        {
            this.BindingContext = this;

            this.DecreaseQuantityCommand = new Command(() => this.Quantity--);
            this.IncreaseQuantityCommand = new Command(() => this.Quantity++);

            this.DrawControl();
        }

        private void DrawControl()
        {
            var quantityEntry = new Entry();
            quantityEntry.SetBinding(Entry.TextProperty, new Binding("Quantity", BindingMode.TwoWay));
            quantityEntry.WidthRequest = 50;
            quantityEntry.HorizontalTextAlignment = TextAlignment.Center;

            var increaseQuantityButton = new Button { Text = "+" };
            increaseQuantityButton.SetBinding(Button.CommandProperty, "IncreaseQuantityCommand");

            var decreaseQuantityButton = new Button { Text = "-" }; 
            decreaseQuantityButton.SetBinding(Button.CommandProperty, "DecreaseQuantityCommand");

            var ui = new StackLayout()
            {
                Orientation = StackOrientation.Horizontal,
                Children =
                {
                    decreaseQuantityButton,
                    quantityEntry,
                    increaseQuantityButton
                }
            };

            this.Content = ui;
        }
    }
}

The view with proof that binding between View and VM is working:

namespace UsercontrolBindingTest
{
    using Usercontrols;
    using Xamarin.Forms;

    public class App : Application
    {
        public App()
        {
            MainPage = new ContentPage
            {
                BindingContext = new MainPageVM(),
                Content = new StackLayout
                {
                    VerticalOptions = LayoutOptions.Center,
                    HorizontalOptions = LayoutOptions.Center,
                    Children =
                    {
                        this.GetLabel("Title"),
                        this.GetCustomStepper(),
                        this.GetLabel("SelectedQuantity")
                    }
                }
            };
        }

        private Label GetLabel(string boundedPropertyName)
        {
            var ret = new Label();
            ret.HorizontalOptions = LayoutOptions.CenterAndExpand;
            ret.SetBinding(Label.TextProperty, new Binding(boundedPropertyName));

            return ret;
        }

        private CustomQuantityStepper GetCustomStepper()
        {
            var ret = new CustomQuantityStepper();
            var dataContext = this.BindingContext as MainPageVM;

            ret.SetBinding(CustomQuantityStepper.QuantityProperty, new Binding("SelectedQuantity", BindingMode.TwoWay));

            return ret;
        }
    }
}

And simple ViewModel:

namespace UsercontrolBindingTest
{
    using System.ComponentModel;

    internal class MainPageVM : INotifyPropertyChanged
    {
        private int _selectedQuantity;

        public int SelectedQuantity
        {
            get
            {
                return this._selectedQuantity;
            }

            set
            {
                this._selectedQuantity = value;
                this.NotifyPropertyChanged(nameof(this.SelectedQuantity));
            }
        }

        public string Title { get; set; } = "ViewModel is bound";

        public MainPageVM()
        {
            this.SelectedQuantity = 0;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged(string propName)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
        }
    }
}

I've checked plenty of different topis and blog posts but I wasn't able to find solutions for my issue. Hope for help here...

EDIT: Attached sample project here: https://1drv.ms/u/s!Apu16I9kXJFtl28YBjkfwDztT9j0

Answers

  • FriedSlothFriedSloth DEMember ✭✭

    Try this:
    add a change handler to your property like:

        public static readonly BindableProperty QuantityProperty =
            BindableProperty.Create(nameof(Quantity), typeof(int), typeof(CustomQuantityStepper), 0, BindingMode.TwoWay,        propertyChanged: QuantityChanged);
    
        private static void QuantityChanged(BindableObject bindable, object oldValue, object newValue)
        {
              CustomQuantityStepper thisView = (CustomQuantityStepper)bindable;
          thisView .OnPropertyChanged(nameof(thisView.Quantity));
        }
    
  • FriedSlothFriedSloth DEMember ✭✭
    edited September 2016

    Try this:
    add a change handler to your property like:

        public static readonly BindableProperty QuantityProperty =
            BindableProperty.Create(nameof(Quantity), typeof(int), typeof(CustomQuantityStepper), 0, BindingMode.TwoWay,        propertyChanged: QuantityChanged);
    
        private static void QuantityChanged(BindableObject bindable, object oldValue, object newValue)
        {
              CustomQuantityStepper thisView = (CustomQuantityStepper)bindable;
          thisView .OnPropertyChanged(nameof(thisView.Quantity));
        }
    

    edit: And if that does not work, change the value of the entry in the QuantityChanged eg thisView.quantityEntry.Text = (string)newValue;

  • kkopkkop PLMember

    Well I've just tried both of your solutions and unfortunately neither of them worked :(

  • kkopkkop PLMember

    Sorry for double post but I managed to resolve it.

    My mistake was setting the BoundedContext of CustomQuantityStepper to "this".
    After that I had to set proper source for bindings inside my usercontrol, fe:

    quantityEntry.SetBinding(Entry.TextProperty, new Binding("Quantity", BindingMode.TwoWay, source: this));
    
  • alextnalextn USMember ✭✭
    edited May 2017

    I had an issue with my ViewModel that did not want to work with RaisePropertyChanged(() => my_property); from MvxViewModel and worked well with OnPropertyChanged(nameof(my_property)); from ContentView

    so I used your solution:

        private void NotifyPropertyChanged(string propName)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
        }
    

    and it's now working very well, thanks !

  • alextnalextn USMember ✭✭
    edited May 2017

    .

  • ajaxerajaxer AUMember ✭✭

    My solution is a bit more XAML involved and may help others.

    <ContentPage
        xmlns="http://xamarin.com/schemas/2014/forms"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
        x:Class="MyApp.MyHostPage"
        xmlns:uc="clr-namespace:MyApp.ViewControls"
        prism:ViewModelLocator.AutowireViewModel="True">
        <StackLayout VerticalOptions="FillAndExpand">
        ..
           <uc:MyCustomControl ViewName="XXX" />
        </StackLayout>
    </ContentPage>
    

    Prism will automagically connect the viewmodel to the control as they are registered in App.xaml.cs.

    MyCustomControl looks like this

    <ContentView
        x:Class="MyApp.ViewControls.MyCustomControl"
        xmlns="http://xamarin.com/schemas/2014/forms"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
        prism:ViewModelLocator.AutowireViewModel="True">
        <StackLayout>
            <ActivityIndicator IsRunning="{Binding sSearchLabelVisible}" Color="Red" />
            <Label IsVisible="{Binding IsSearchLabelVisible}" Text="Searching. Please wait...." />
        </StackLayout>
    </ContentView>
    

    So now the challenge was to have the property named ViewName be set by the HostPage and for the custom control
    receive that into the custom control viewmodel.

    The code-behind file for the HostPage did not need anything in it.
    However the child control, MyCustomControl needed to have a bindable property defined.
    It does not appear possible for this to be valid anywhere else than the code-behind file.
    The property ViewName is a simple string and only when this is defined here in the code behind is the xaml declaration that refers to it is valid.

    So my file MyCustomControl.xaml.cs has this in it

    public partial class MyCustomControl: ContentView
        {
            public MyCustomControl()
            {
                InitializeComponent();
            }
    
    public static readonly BindableProperty ViewNameProperty = BindableProperty.Create(
                                                propertyName: "ViewName",
                                                returnType: typeof(string),
                                                declaringType: typeof(ContentView),
                                                defaultValue: string.Empty,
                                                defaultBindingMode: BindingMode.TwoWay,
                                                propertyChanged: ViewNameChanged
                                                );
        public string ViewName
        {
            get { return (string)GetValue(ViewNameProperty); }
            set { SetValue(ViewNameProperty, value);}
        }
    
        private static void ViewNameChanged(BindableObject bindable, object oldValue, object newValue)
        {
            MyCustomControl thisView = (MyCustomControl)bindable;
            thisView.ViewName = (string)newValue;
            ((MyCustomControlViewModel)thisView.BindingContext).ViewName = (string)newValue;
        }
    }
    

    Setting the local thisView.ViewName value is not really necessary but it was useful for debugging.
    At this stage however, the viewmodel still does not know about the value of ViewName. (We are still only in codebehind).

    After much trial and error I discovered that this model is accessible via the BindingContext.
    And so I arrived at the line ((MyCustomControlViewModel)thisView.BindingContext).ViewName = (string)newValue;

    The viewmodel is defined thus (using Fody for automatic INotifyPropertyChanged handling):

    [AddINotifyPropertyChangedInterface]
    public class MyCustomControlViewModel : BaseViewModel
    {
        public string ViewName { get; set; }
    
        public MyCustomControlViewModel()
        {
            this.PropertyChanged += UpdateViewName;
        }
        private void UpdateViewName(object sender, PropertyChangedEventArgs e)
        {
            // can do stuff here finally
        }
    }
    

    The one problem remaining to me is back at the xaml level.
    Currently I set the value of the custom control property as absolute.
    <uc:MyCustomControl ViewName="XXX" />
    I would prefer to be able to bind this to a HostPage property value but so far I have had no luck.

    I tried <uc:MyCustomControl ViewName="{Binding Source=x:Reference HostPage, Path=ViewName}" />
    and <uc:MyCustomControl ViewName="{Binding HostPage.ViewName}" />
    but nothing works, the value is always null in the code behind.

  • ajaxerajaxer AUMember ✭✭

    An update on this last problem. I finally worked out how to bind the property values of the HostPage successfully.
    The key was to understand that the HostPage requires a bindable property also to expose it to the child control.
    This is the code in the codebehind of the HostPage.
    public HostPage(){ InitializeComponent(); this.ViewName = ((HostPageViewModel)this.BindingContext).ViewName; }
    This code takes the value set in the HostPage viewmodel (binding context of the HostPage Control) in the constructor so that the accessor value was set. In my case it is readonly so I dont need to setup a changeproperty method.
    Then in the HostPage xaml I can reference the value and pass it down to the CustomControl.

    <uc:MyCustomControl ViewName="{Binding Source={x:Reference HostPage}, Path=ViewName}" />

Sign In or Register to comment.