My approach on nested properties that bind to XAML

Posted on

Problem

Here is my simple model.

public class Product
{
    public String Name { get; set; }
    public int Price { get; set; }
    public String Info { get; set; }
}

When I use this in viewmodel as following:

    public MainViewModel()
    {
        Item = new Product() { Name = "Coffee", Price = 4, Info = "Coffe is hot" };
    }

    protected Product _item;
    public Product Item
    {
        get { return _item; }
        set
        {
            _item = value;
            NotifyPropertyChanged("Item");
        }
    }

In my XAML, I am binding to the nested properties.

<StackPanel>
    <TextBox Text="{Binding Item.Name}"/>
    <TextBox Text="{Binding Item.Price}"/>
    <TextBox Text="{Binding Item.Info}"/>
    <Button Content="Change Item" Click="Change_Item_Clicked"/>
</StackPanel>

I change only nested property in the click event:

    private void Change_Item_Clicked(object sender, RoutedEventArgs e)
    {
        MainViewModel vm = DataContext as MainViewModel;

        vm.Item.Name = "Donut";
    }

This obviously doesn’t update the UI since the parent property implements INotifyPropertyChanged but not the nested one.

So I came up with the following solution and it does then update the UI fine.

I am essentially creating viewmodel version of the Product class as below that implments INotifyPropertyChanged.

   public class ProductVM : Notifier
    {
        protected Product _p;
        public ProductVM(Product p)
        {
            _p = p;
        }

        public String Name 
        { 
            get { return _p.Name; }
            set
            {
                _p.Name = value;
                NotifyPropertyChanged("Name");
            }
        }

        public int Price
        {
            get { return _p.Price; }
            set
            {
                _p.Price = value;
                NotifyPropertyChanged("Price");
            }
        }

        public String Info
        {
            get { return _p.Info; }
            set
            {
                _p.Info = value;
                NotifyPropertyChanged("Info");
            }
        }
    }

Now the viewmodel use its version of the Product class as defined above that has ability to notify on changes.

    class MainViewModel : Notifier
    {
        public MainViewModel()
        {
            Item = new ProductVM( new Product() { Name = "Coffee", Price = 4, Info = "Coffe is hot" });
        }

        protected ProductVM _item;
        public ProductVM Item
        {
            get { return _item; }
            set
            {
                _item = value;
                NotifyPropertyChanged("Item");
            }
        }
    }

This works great but what do you guys think of this approach? If we are going to bind to a property (nested or top level), wouldn’t it be best for all these properties to implment the INotifyPropertyChanged?

What naming conversion can I use to name ProductVM better? Is there a less cody way to achieve this?

Solution

To me it looks fine. If you can or will not modify the model objects (Product) by letting them implement INotifyPropertyChanged then this approach is the price to pay for using XAML. In larger projects, it can be a little annoying to seemingly write the same thing twice, but you on the other hand have a true separation of concerns, and your solution is an implementation of the MVVM-pattern.
To me ProductVM is an OK name, I always name my view objects that way.


Instead of using string literals in the call to NotifyPropertyChanged you can use nameof(<property name>):

               NotifyPropertyChanged(nameof(Name));

this will make it easier to maintain, if the property name changes.


   Another approach is to define `NotifyPropertyChanged` as:

  private void NotifyPropertyChanged([CallerMemberName] string name = "")
  {
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
  }

then you don’t need to provide the property name in the call:

    public String Name 
    { 
        get { return _p.Name; }
        set
        {
            _p.Name = value;
            NotifyPropertyChanged();
        }
    }

CallerMemberName requires a reference to System.Runtime.CompilerServices.

Leave a Reply

Your email address will not be published. Required fields are marked *