【问题标题】:MVVM how to get notify event for a nested class objectMVVM如何获取嵌套类对象的通知事件
【发布时间】:2021-07-01 05:27:47
【问题描述】:

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

我想了解并学习一种简单的方法来获取可以在我的视图中订阅的 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;
        }
    }

【问题讨论】:

  • 我建议您将ViewModelBase 重命名为ObservableObject 或类似名称。
  • 好的,这不是问题,但这对这里有什么帮助?
  • 这是一个评论 不是一个答案

标签: c# mvvm inotifypropertychanged


【解决方案1】:

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

更新:

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

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

但是订阅已经完成了一半:您需要确保在不再需要对象时取消订阅,因此必须将其存储在nestedProperties。

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

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

【讨论】:

  • 谢谢。它有效(对于第一次快速尝试:) ...我必须说我确实可以估计代码在做什么,但我无法逐行理解它:(你自己想出了这个,还是您可以发布一个链接,以获取有关此方法的更多信息吗?无论如何,非常感谢您抽出宝贵时间。希望我能在 Blazor 应用程序中使用它。这几天/几夜都很艰难
  • 欢迎,我更新了一些解释的答案,希望这会有所帮助。
  • 您好,请问您是否可以添加最后一张?我还想在 Customer 类中使用嵌套列表。作为一个例子,我想到了来自 Customer (List) 的 TelephoneNrs。我应该使用什么类型的列表(ObservableList/BindingList)以及如何从这些列表中获取通知事件(如何在 List TelephoneNr 是客户类的属性的情况下更改 ViewModelBase)。我应该在一开始就问这个问题,但我确实忘记了。
  • BindingList 应该可以很好地用于内部收藏,只需要确保您订阅了ListChanged,并从中提升PropertyChanged。对于简单的类型,例如 int 或 string 在列表中,它可能有点棘手。例如,您可以创建自己的泛型类Notifier&lt;T&gt;,其中 T 是您的类型,在此类中您已通知该类型的属性。
  • IDisposable 在我们的例子中的目的是帮助GarbageCollector 完成它的工作:释放内存。简而言之:订阅了事件的不必要对象比假设的寿命更长,因为 GC 认为该对象仍在使用,因此在最坏的情况下会导致内存泄漏。如果您想更深入地了解 GarbageCollector 的工作原理,特别是订阅事件。老实说:我已经在反射上添加了IDisposable,以防万一。我也有强烈的感觉,GC 可以改变一些东西,我也应该刷新我的意识 ^^
猜你喜欢
  • 2019-03-21
  • 1970-01-01
  • 1970-01-01
  • 2020-07-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-04-27
  • 1970-01-01
相关资源
最近更新 更多