MVVM 如何获取嵌套 class 对象的通知事件

MVVM how to get notify event for a nested class object

你好,我知道有关于这个主题的帖子,但我无法解决我的问题。

我想了解并学习一种简单的方法来获取我可以在我的视图中订阅的 ViewModelBase,以便强制 UI 刷新。

我写了一个 windows 控制台示例。结构是 Class Customer(string Name, MyAddress Address),其中 MyAddress 是 Class(string StreetName)。在 Main 中,我有一个客户列表。现在,每当列表或客户的 属性 发生变化(包括街道名称的变化)时,我都想收到一条消息。 我无法让它工作。如果我更改客户的姓名,它会起作用,但不适用于 'nest' 地址。如果我更改 StreetName,我不会收到通知事件。我不知道如何为列表中的所有客户订阅 ViewModelBase。 控制台程序可以在 VisulaStudio 中 copy/paste 并运行:

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}");
            }
        }
    }
}

第一次编辑:CustomerClass 中的内部 BindingList 加上 ViewModelBase @Karoshee

为了简化,我确实省略了 MyAdresse。我向我的 CustomerClass 添加了一个 BindingList 'MyTelNrs' 并订阅了 ListChanged 事件。我没有从执行的回答中更改 ViewModelBase。我确实在 UI 中收到通知,但我不知道我是否以 save/right 方式进行通知。只是为了让下面的读者知道......(也许有人比我更好的答案,如果下面的方式是'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;
        }
    }

首先需要做Address属性一个通知属性:

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

您需要向 ViewModelBase 添加一些额外的逻辑,例如:

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();
        }
    }
}

据我所知,这与 MVVM 并不矛盾,subscribing/unsubscribing 子 属性 的唯一问题已更改。

更新:

我在下面的代码中添加了一些更改和注释。

这里的关键是,您需要订阅从 ViewModelBase 继承的子属性的 PropertyChanged。

但是订阅只进行了一半:当对象不再需要时,您需要确保取消订阅,因此必须将其存储在 nestedProperties.

我们还需要用父 属性 名称替换 ChildViewModelChanged 中的子 属性 名称,以在父对象上引发 PropertyChange 事件。为了这个目标,我用 属性 值保存了 属性 名称,而不是在 ChildViewModelChanged 上订阅,这就是我在 nestedProperties

中使用字典类型的原因

当不再需要对象时,取消订阅所有 ProperyChanged 也很重要。我添加了 IDisposable 接口和 Dispose 方法来做那件事。 Dispose 方法也需要提升(使用 using 或手动),在您的情况下,使用 IDisposable 制作自己的 BindingList 可能会更好,这会提升所有项目的 Dispose。