nogood
nogood

Reputation: 1320

MVVM how to get notify event for a nested class object

Hi I know that there a posts about this topic, but I could not solve my problems with them.

I want to understand and learn a simple way to get a ViewModelBase that I can subcribe to in my View so that a UI Refresh is forced.

I have written an windows console example. The structure is Class Customer(string Name, MyAddress Address) where MyAddress is a Class(string StreetName). In Main I have a list of customers. Now I want to get a message every time there is a change in the list or in the property of the customer including a change of the streetname. I cant get that to work. If I change the name of the customer it works but not for the 'nest' address. If I change StreetName I dont get a notify Event. I don't know how to subcribe to the ViewModelBase for all the customers in the list. The Console Progam can be copy/paste in VisulaStudio and runs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace CS_MVVM_NotifyFromNestedClass
{
    class Program
    {
        public class ViewModelBase : INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler PropertyChanged;

            protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }

            protected void SetValue<T>(ref T backingFiled, T value, [CallerMemberName] string propertyName = null)
            {
                if (EqualityComparer<T>.Default.Equals(backingFiled, value)) return;
                backingFiled = value;
                OnPropertyChanged(propertyName);
            }
        }

        public class Customer : ViewModelBase
        {
            private string _name;
            public string Name
            {
                get => _name;
                set { SetValue(ref _name, value); }
            }
            public MyAddress Address { get; set; }
            public Customer(string name, MyAddress address)
            {
                Name = name;
                Address = address;
            }
        }
        public class MyAddress : ViewModelBase
        {
            private string _street;
            public string Street
            {
                get => _street;
                set { SetValue(ref _street, value); }
            }

            public MyAddress(string street)
            {
                Street = street;
            }
        }

        public static BindingList<Customer> MyCustomers = new BindingList<Customer>
            {
                new Customer("John", new MyAddress("JoStreet")),
                new Customer("Susi", new MyAddress("SeaStreet")),
            };
        static void Main(string[] args)
        {
            //BindingList Event 
            MyCustomers.ListChanged += OnBindingListChanged;

            // 1) Change Name  <-- THIS FIRES THE 'OnBindingListChanged' EVENT
            MyCustomers[0].Name = "Rick";
            // 2) Change Street  <-- THIS DOESN'T FIRE A CHANGE-EVENT
            MyCustomers[0].Address.Street = "Rockyroad";
            //I dont know how to hook up the 'property change event' from ViewModelBase for all obj. of MyCustomer-List
            //MyCustomers[0].Address.PropertyChanged += OnSingleObjPropChanged;  // <--doesn't work

            Console.ReadLine();
        }
        private static void OnBindingListChanged(object sender, ListChangedEventArgs e)
        {
            Console.WriteLine("1) BindingList was changed");
            foreach (var c in MyCustomers)
            {
                Console.WriteLine($"{c.Name}  {c.Address.Street}");
            }
        }
        private static void OnSingleObjPropChanged(object sender, PropertyChangedEventArgs e)
        {
            //Never reached --> how to 'hook'
            Console.WriteLine("2) Property of List Item was changed");
            foreach (var c in MyCustomers)
            {
                Console.WriteLine($"{c.Name}  {c.Address.Street}");
            }
        }
    }
}

First Edit: inner BindingList in the CustomerClass plus the ViewModelBase @Karoshee

I did leave the MyAdresse thing out to simplify. I added a BindingList 'MyTelNrs' to my CustomerClass and subcribt to the ListChanged Event. I didn't change the ViewModelBase from the execpted answere. I do get a notification in my UI, but I don't know if I'am doing it in a save/right way. Just to let the following readers know ... (maybe someone that is better then me answeres, if the below way is 'okay')

    public class Customer: ViewModelBase
    {
        private string _name;
        public string Name
        {
            get => _name;
            set => SetValue(ref _name, value);
        }

        public BindingList<string> MyTelNrs = new();

        private void OnLstChanged(object sender, ListChangedEventArgs e)
        {
            OnPropertyChanged(nameof(MyTelNrs));
        }

        public Customer(string name, BindingList<string> myTelNrs)
        {
            Name = name;
            MyTelNrs = myTelNrs;

            MyTelNrs.ListChanged += OnLstChanged;
        }
    }

Upvotes: 0

Views: 607

Answers (1)

Karoshee
Karoshee

Reputation: 308

First of all need to make Address property a notify property:

            public MyAddress Address
            {
                get => _address;
                set
                {
                    SetValue(ref _address, value);
                }
            }

Than you need to add some additional logic into ViewModelBase, something like this:

public class ViewModelBase : INotifyPropertyChanged, IDisposable
{
    /// <summary>
    /// All child property values and names, that subscribed to PropertyChanged
    /// </summary>
    protected Dictionary<ViewModelBase, string> nestedProperties 
        = new Dictionary<ViewModelBase, string>();

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected void SetValue<T>(ref T backingFiled, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(backingFiled, value)) return;
        if (backingFiled is ViewModelBase viewModel)
        {   // if old value is ViewModel, than we assume that it was subscribed,
            // so - unsubscribe it
            viewModel.PropertyChanged -= ChildViewModelChanged;
            nestedProperties.Remove(viewModel);
        }
        if (value is ViewModelBase valueViewModel)
        {
            // if new value is ViewModel, than we must subscribe it on PropertyChanged 
            // and add it into subscribe dictionary
            valueViewModel.PropertyChanged += ChildViewModelChanged;
            nestedProperties.Add(valueViewModel, propertyName);
        }
        backingFiled = value;
        OnPropertyChanged(propertyName);
    }

    private void ChildViewModelChanged(object? sender, PropertyChangedEventArgs e)
    {
        // this is child property name,
        // need to get parent property name from dictionary
        string propertyName = e.PropertyName;
        if (sender is ViewModelBase viewModel)
        {
            propertyName = nestedProperties[viewModel];
        }
        // Rise parent PropertyChanged with parent property name
        OnPropertyChanged(propertyName);
    }

    public void Dispose()
    {   // need to make sure that we unsubscibed
        foreach (ViewModelBase viewModel in nestedProperties.Keys)
        {
            viewModel.PropertyChanged -= ChildViewModelChanged;
            viewModel.Dispose();
        }
    }
}

As I know this does not contradict the MVVM, the only issue with subscribing/unsubscribing child property changed.

Updated:

I added few changes and comments in code below.

The key thing here, that you need to subscribe to PropertyChanged of child properties that inherited from ViewModelBase.

But subscribing is a half way throught: you need to make sure that you unsubscribe it, when objects does not need anymore, so it's has to be stored in nestedProperties.

Also we need to replace child property name from ChildViewModelChanged with parent property name to rise PropertyChange event on parent object. For that goal I saved property name with property value than subscribed on ChildViewModelChanged, this is why I use Dictionary type in nestedProperties

Also important thing to unsubscribe all ProperyChanged, when object no longer needed. I added IDisposable interface and Dispose method to do that thing. Dispose method also needs to be rised (with using or manualy), in your case perhaps will be better to make own BindingList with IDisposable, that rise Dispose on all items.

Upvotes: 1

Related Questions