首页 > 解决方案 > MVVM如何获取嵌套类对象的通知事件

问题描述

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

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

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

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 中收到通知,但我不知道我是否以保存/正确的方式进行操作。只是为了让以下读者知道......(如果下面的方式是“好的”,也许有人比我更好回答)

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

标签: c#mvvminotifypropertychanged

解决方案


首先需要使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 并不矛盾,订阅/取消订阅子属性的唯一问题发生了变化。

更新:

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

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

但是订阅已经完成了一半:您需要确保取消订阅,当对象不再需要时,它必须存储在nestedProperties.

此外,我们需要将子属性名称替换ChildViewModelChanged为父属性名称,以在父对象上引发 PropertyChange 事件。为了这个目标,我用属性值保存了属性名称而不是 subscribed on ChildViewModelChanged,这就是我使用 Dictionary 类型的原因nestedProperties

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


推荐阅读