c# - 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;
}
}
解决方案
首先需要使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。BindingList
IDisposable
推荐阅读
- java - 为什么数组的长度没有更新,导致索引越界错误?
- gremlin - 如何从顶点标签中删除属性约束
- c++ - 加法程序在意外点退出
- python - 为什么此代码中“pxssh.pxssh()”中没有指定端口?
- php - Elementor Wordpress 插件将单词“array”随机添加到我的自定义小部件中
- django - django可以集成zerorpc吗?
- python-3.x - Bokeh DataCube 小部件的 SumAggregator 如何汇总超过 1 个字段
- reactjs - 为什么我在位置 4 处得到 JSON 中的 Unexpected token F
- c - 如何修复 C 中类型不匹配和尾随空格的错误
- python - 将秒转换为天、小时、分钟和秒