INotifyPropertyChanged 和派生属性不同 objects
INotifyPropertyChanged And derived properties on different objects
最近接手了一个用C#和WPF开发的比较大的项目。
它使用绑定以及 INotifyPropertyChanged
接口来传播更改 to/from 视图。
一点前言:
在不同的 classes 中,我的属性依赖于同一个 class 中的其他属性 (例如考虑 属性 TaxCode
取决于 Name
和 Lastname
等属性)。
借助我在 SO 上找到的一些代码(尽管无法再次找到答案),我创建了抽象 class ObservableObject
和属性 DependsOn
。
来源如下:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace TestNameSpace
{
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public sealed class DependsOn : Attribute
{
public DependsOn(params string[] properties)
{
this.Properties = properties;
}
public string[] Properties { get; private set; }
}
[Serializable]
public abstract class ObservableObject : INotifyPropertyChanged
{
private static Dictionary<Type, Dictionary<string, string[]>> dependentPropertiesOfTypes = new Dictionary<Type, Dictionary<string, string[]>>();
[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
private readonly bool hasDependentProperties;
public ObservableObject()
{
DependsOn attr;
Type type = this.GetType();
if (!dependentPropertiesOfTypes.ContainsKey(type))
{
foreach (PropertyInfo pInfo in type.GetProperties())
{
attr = pInfo.GetCustomAttribute<DependsOn>(false);
if (attr != null)
{
if (!dependentPropertiesOfTypes.ContainsKey(type))
{
dependentPropertiesOfTypes[type] = new Dictionary<string, string[]>();
}
dependentPropertiesOfTypes[type][pInfo.Name] = attr.Properties;
}
}
}
if (dependentPropertiesOfTypes.ContainsKey(type))
{
hasDependentProperties = true;
}
}
public virtual void OnPropertyChanged(string propertyName)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
if (this.hasDependentProperties)
{
//check for any computed properties that depend on this property
IEnumerable<string> computedPropNames = dependentPropertiesOfTypes[this.GetType()].Where(kvp => kvp.Value.Contains(propertyName)).Select(kvp => kvp.Key);
if (computedPropNames != null && !computedPropNames.Any())
{
return;
}
//raise property changed for every computed property that is dependant on the property we did just set
foreach (string computedPropName in computedPropNames)
{
//to avoid Whosebug as a result of infinite recursion if a property depends on itself!
if (computedPropName == propertyName)
{
throw new InvalidOperationException("A property can't depend on itself");
}
this.OnPropertyChanged(computedPropName);
}
}
}
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
return this.SetField<T>(ref field, value, false, propertyName);
}
protected bool SetField<T>(ref T field, T value, bool forceUpdate, [CallerMemberName] string propertyName = null)
{
bool valueChanged = !EqualityComparer<T>.Default.Equals(field, value);
if (valueChanged || forceUpdate)
{
field = value;
this.OnPropertyChanged(propertyName);
}
return valueChanged;
}
}
}
这些 classes 允许我:
- 在我的属性 setter 中仅使用
this.SetValue(ref this.name, value)
。
- 在 属性 税码
上使用属性 DependsOn(nameof(Name), nameof(LastName))
这样 TaxCode
只有 getter 属性 结合了 FirstName
、LastName
(和其他属性)和 returns相应的代码。由于这个依赖系统,即使绑定了这个 属性 也是最新的。
因此,只要 TaxCode
依赖于同一 class 中的属性,一切都可以正常工作。然而,我需要拥有一个或多个依赖于 其 child object 的属性。例如(我将只使用 json 使层次结构更简单):
{
Name,
LastName,
TaxCode,
Wellness,
House:
{
Value
},
Car:
{
Value
}
}
所以,属性 人的健康应该这样实现:
[DependsOn(nameof(House.Value), nameof(Car.Value))]
public double Wellness { get =>(this.House.Value + this.Car.Value);}
第一个问题是“House.Value”和“Car.Value”在该上下文中不是 nameof
的有效参数。
第二个是,使用我的实际代码,我可以提高仅在相同 object 中的属性,因此没有 child 的属性,也没有 应用程序范围内的属性 (例如,我有一个 属性 表示测量单位是否用 metric/imperial 表示,它的变化会影响值的显示方式)。
现在我可以使用的一个解决方案是在我的 ObservableObject
中插入一个事件字典,键是 属性 的名称,并使 parent 注册一个回调.这样,当 child 的 属性 发生变化时,将触发事件并使用代码通知 parent 中的 属性 已发生变化。然而,这种方法迫使我在每次实例化新的 child 时注册回调。它当然不多,但我喜欢只指定依赖项并让我的基础 class 为我完成工作的想法。
所以,长话短说,我想要实现的是有一个系统可以通知相关的 属性 更改,即使所涉及的属性是它的 child 或与它无关具体 object。由于代码库非常大,我不想放弃现有的 ObservableObject
+ DependsOn 方法,我正在寻找一种比在我的代码中放置回调更优雅的方法。
当然,如果我的方法是错误的/我所拥有的代码无法实现我想要的,请随时提出更好的方法。
我认为,您使用 DependendOn 的方式不适用于更大的项目和更复杂的关系。 (1 到 n,n 到 m,...)
你应该使用观察者模式。例如:你可以有一个集中的地方,所有的 ViewModels (ObservableObjects) 都可以自行注册并开始监听变化事件。您可以使用发送者信息引发更改的事件,每个 ViewModel 都会获取所有事件并可以决定单个事件是否有趣。
如果您的应用程序可以打开多个独立的 windows / 视图,您甚至可以开始为侦听器设置范围,因此独立的 windows / 视图是分开的,并且只获取它们自己范围内的事件。
如果您有一长串显示在虚拟化列表/网格中的项目,您可以检查该项目现在是否真的显示任何 UI,如果没有,请停止收听或根本不关心本例中的事件。
并且您可以稍微延迟引发某些事件(例如那些会触发非常大的 UI 变化的事件),并清除先前事件的队列,如果同一事件再次引发不同延迟内的参数。
我认为所有这些的示例代码对于这个线程来说已经太多了……如果您真的需要其中一个建议的一些代码,请告诉我……
您可以让事件在 ObservableObject
层次结构中向上冒泡。正如建议的那样,底座 class 可以处理连接。
[Serializable]
public abstract class ObservableObject : INotifyPropertyChanged
{
// ...
// Code left out for brevity
// ...
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
return this.SetField<T>(ref field, value, false, propertyName);
}
protected bool SetField<T>(ref T field, T value, bool forceUpdate, [CallerMemberName] string propertyName = null)
{
bool valueChanged = !EqualityComparer<T>.Default.Equals(field, value);
if (valueChanged || forceUpdate)
{
RemovePropertyEventHandler(field as ObservableObject);
AddPropertyEventHandler(value as ObservableObject);
field = value;
this.OnPropertyChanged(propertyName);
}
return valueChanged;
}
protected void AddPropertyEventHandler(ObservableObject observable)
{
if (observable != null)
{
observable.PropertyChanged += ObservablePropertyChanged;
}
}
protected void RemovePropertyEventHandler(ObservableObject observable)
{
if (observable != null)
{
observable.PropertyChanged -= ObservablePropertyChanged;
}
}
private void ObservablePropertyChanged(object sender, PropertyChangedEventArgs e)
{
this.OnPropertyChanged($"{sender.GetType().Name}.{e.PropertyName}");
}
}
现在可以靠孙子了
Models.cs
public class TaxPayer : ObservableObject
{
public TaxPayer(House house)
{
House = house;
}
[DependsOn("House.Safe.Value")]
public string TaxCode => House.Safe.Value;
private House house;
public House House
{
get => house;
set => SetField(ref house, value);
}
}
public class House : ObservableObject
{
public House(Safe safe)
{
Safe = safe;
}
private Safe safe;
public Safe Safe
{
get => safe;
set => SetField(ref safe, value);
}
}
public class Safe : ObservableObject
{
private string val;
public string Value
{
get => val;
set => SetField(ref val, value);
}
}
MainWindow.xaml
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid VerticalAlignment="Center" HorizontalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0">Safe Content:</Label>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding House.Safe.Value, UpdateSourceTrigger=PropertyChanged}" />
<Label Grid.Row="1" Grid.Column="0">Tax Code:</Label>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding TaxCode, Mode=OneWay}" IsEnabled="False" />
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace WpfApp
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext =
new TaxPayer(
new House(
new Safe()));
}
}
}
对于项目范围的依赖项,建议的方法是使用 Dependency Injection。一个广泛的主题,简而言之,您将在抽象的帮助下构建对象树,允许您在运行时交换实现。
带有 DependsOnAttribute
的原始解决方案是个好主意,但实现存在一些性能和多线程问题。无论如何,它不会对您的 class.
引入任何令人惊讶的依赖性
class MyItem : ObservableObject
{
public int Value { get; }
[DependsOn(nameof(Value))]
public int DependentValue { get; }
}
有了这个,您可以在任何地方使用您的 MyItem
- 在您的应用程序中,在单元测试中,在您以后可能愿意创建的 class 库中。
现在,考虑这样一个 class:
class MyDependentItem : ObservableObject
{
public IMySubItem SubItem { get; } // where IMySubItem offers some NestedItem property
[DependsOn(/* some reference to this.SubItem.NestedItem.Value*/)]
public int DependentValue { get; }
[DependsOn(/* some reference to GlobalSingleton.Instance.Value*/)]
public int OtherValue { get; }
}
这个 class 现在有两个 "surprising" 依赖项:
MyDependentItem
现在需要知道 IMySubItem
类型的特定 属性 (而最初,它只公开该类型的一个实例,而不知道它的细节)。当您以某种方式更改 IMySubItem
属性时,您也被迫更改 MyDependentItem
class。
此外,MyDependentItem
需要引用全局 object(此处表示为单例)。
所有这些都违反了 SOLID 原则(所有这些都是为了尽量减少代码更改)并使 class 不可测试。它引入了与其他 classes 的紧密耦合并降低了 class' 的凝聚力。你迟早会在调试问题时遇到麻烦。
我认为,Microsoft 在设计 WPF 数据绑定引擎时也遇到了同样的问题。您正在以某种方式尝试重新发明它 - 您正在寻找 PropertyPath
,因为它目前正在 XAML 绑定中使用。为支持这一点,Microsoft 创建了整体依赖性 属性 概念和一个综合数据绑定引擎,用于解析 属性 路径、传输数据值并观察数据变化。我不认为你真的想要那么复杂的东西。
相反,我的建议是:
对于同一个 class 中的 属性 依赖项,请像您当前所做的那样使用 DependsOnAttribute
。我会稍微重构实现以提高性能并确保线程安全。
对于外部object的依赖,使用SOLID的依赖倒置原则;在构造函数中将其实现为依赖注入。对于您的测量单位示例,我什至会将数据和表示方面分开,例如通过使用依赖于某些 ICultureSpecificDisplay
(您的测量单位)的 view-model。
class MyItem
{
public double Wellness { get; }
}
class MyItemViewModel : INotifyPropertyChanged
{
public MyItemViewModel(MyItem item, ICultureSpecificDisplay display)
{
this.item = item;
this.display = display;
}
// TODO: implement INotifyPropertyChanged support
public string Wellness => display.GetStringWithMeasurementUnits(item.Wellness);
}
- 对于object的组合结构中的依赖项,只需手动执行即可。你有多少这样的依赖属性? class 中的情侣?发明一个全面的框架而不是额外的 2-3 行代码是否有意义?
如果我仍然不能说服您 - 那么,您当然可以扩展 DependsOnAttribute
以不仅存储 属性 名称,还存储声明这些属性的类型。您的 ObservableObject
也需要更新。
一起来看看吧。
这是一个扩展属性,也可以保存类型引用。请注意,它现在可以多次应用。
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
class DependsOnAttribute : Attribute
{
public DependsOnAttribute(params string[] properties)
{
Properties = properties;
}
public DependsOnAttribute(Type type, params string[] properties)
: this(properties)
{
Type = type;
}
public string[] Properties { get; }
// We now also can store the type of the PropertyChanged event source
public Type Type { get; }
}
ObservableObject
需要订阅children个事件:
abstract class ObservableObject : INotifyPropertyChanged
{
// We're using a ConcurrentDictionary<K,V> to ensure the thread safety.
// The C# 7 tuples are lightweight and fast.
private static readonly ConcurrentDictionary<(Type, string), string> dependencies =
new ConcurrentDictionary<(Type, string), string>();
// Here we store already processed types and also a flag
// whether a type has at least one dependency
private static readonly ConcurrentDictionary<Type, bool> registeredTypes =
new ConcurrentDictionary<Type, bool>();
protected ObservableObject()
{
Type thisType = GetType();
if (registeredTypes.ContainsKey(thisType))
{
return;
}
var properties = thisType.GetProperties()
.SelectMany(propInfo => propInfo.GetCustomAttributes<DependsOn>()
.SelectMany(attribute => attribute.Properties
.Select(propName =>
(SourceType: attribute.Type,
SourceProperty: propName,
TargetProperty: propInfo.Name))));
bool atLeastOneDependency = false;
foreach (var property in properties)
{
// If the type in the attribute was not set,
// we assume that the property comes from this type.
Type sourceType = property.SourceType ?? thisType;
// The dictionary keys are the event source type
// *and* the property name, combined into a tuple
dependencies[(sourceType, property.SourceProperty)] =
property.TargetProperty;
atLeastOneDependency = true;
}
// There's a race condition here: a different thread
// could surpass the check at the beginning of the constructor
// and process the same data one more time.
// But this doesn't really hurt: it's the same type,
// the concurrent dictionary will handle the multithreaded access,
// and, finally, you have to instantiate two objects of the same
// type on different threads at the same time
// - how often does it happen?
registeredTypes[thisType] = atLeastOneDependency;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var e = new PropertyChangedEventArgs(propertyName);
PropertyChanged?.Invoke(this, e);
if (registeredTypes[GetType()])
{
// Only check dependent properties if there is at least one dependency.
// Need to call this for our own properties,
// because there can be dependencies inside the class.
RaisePropertyChangedForDependentProperties(this, e);
}
}
protected bool SetField<T>(
ref T field,
T value,
[CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}
if (registeredTypes[GetType()])
{
if (field is INotifyPropertyChanged oldValue)
{
// We need to remove the old subscription to avoid memory leaks.
oldValue.PropertyChanged -= RaisePropertyChangedForDependentProperties;
}
// If a type has some property dependencies,
// we hook-up events to get informed about the changes in the child objects.
if (value is INotifyPropertyChanged newValue)
{
newValue.PropertyChanged += RaisePropertyChangedForDependentProperties;
}
}
field = value;
OnPropertyChanged(propertyName);
return true;
}
private void RaisePropertyChangedForDependentProperties(
object sender,
PropertyChangedEventArgs e)
{
// We look whether there is a dependency for the pair
// "Type.PropertyName" and raise the event for the dependent property.
if (dependencies.TryGetValue(
(sender.GetType(), e.PropertyName),
out var dependentProperty))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(dependentProperty));
}
}
}
您可以像这样使用该代码:
class MyClass : ObservableObject
{
private int val;
public int Val
{
get => val;
set => SetField(ref val, value);
}
// MyChildClass must implement INotifyPropertyChanged
private MyChildClass child;
public MyChildClass Child
{
get => child;
set => SetField(ref child, value);
}
[DependsOn(typeof(MyChildClass), nameof(MyChildClass.MyProperty))]
[DependsOn(nameof(Val))]
public int Sum => Child.MyProperty + Val;
}
Sum
属性 依赖于相同 class 的 Val
属性 和 MyProperty
属性 MyChildClass
class.
如您所见,这看起来不太好。此外,整个概念取决于 属性 setter 执行的事件处理程序注册。如果你碰巧直接设置字段值(例如child = new MyChildClass()
),那么这一切都行不通。我建议你不要使用这种方法。
最近接手了一个用C#和WPF开发的比较大的项目。
它使用绑定以及 INotifyPropertyChanged
接口来传播更改 to/from 视图。
一点前言:
在不同的 classes 中,我的属性依赖于同一个 class 中的其他属性 (例如考虑 属性 TaxCode
取决于 Name
和 Lastname
等属性)。
借助我在 SO 上找到的一些代码(尽管无法再次找到答案),我创建了抽象 class ObservableObject
和属性 DependsOn
。
来源如下:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace TestNameSpace
{
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public sealed class DependsOn : Attribute
{
public DependsOn(params string[] properties)
{
this.Properties = properties;
}
public string[] Properties { get; private set; }
}
[Serializable]
public abstract class ObservableObject : INotifyPropertyChanged
{
private static Dictionary<Type, Dictionary<string, string[]>> dependentPropertiesOfTypes = new Dictionary<Type, Dictionary<string, string[]>>();
[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
private readonly bool hasDependentProperties;
public ObservableObject()
{
DependsOn attr;
Type type = this.GetType();
if (!dependentPropertiesOfTypes.ContainsKey(type))
{
foreach (PropertyInfo pInfo in type.GetProperties())
{
attr = pInfo.GetCustomAttribute<DependsOn>(false);
if (attr != null)
{
if (!dependentPropertiesOfTypes.ContainsKey(type))
{
dependentPropertiesOfTypes[type] = new Dictionary<string, string[]>();
}
dependentPropertiesOfTypes[type][pInfo.Name] = attr.Properties;
}
}
}
if (dependentPropertiesOfTypes.ContainsKey(type))
{
hasDependentProperties = true;
}
}
public virtual void OnPropertyChanged(string propertyName)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
if (this.hasDependentProperties)
{
//check for any computed properties that depend on this property
IEnumerable<string> computedPropNames = dependentPropertiesOfTypes[this.GetType()].Where(kvp => kvp.Value.Contains(propertyName)).Select(kvp => kvp.Key);
if (computedPropNames != null && !computedPropNames.Any())
{
return;
}
//raise property changed for every computed property that is dependant on the property we did just set
foreach (string computedPropName in computedPropNames)
{
//to avoid Whosebug as a result of infinite recursion if a property depends on itself!
if (computedPropName == propertyName)
{
throw new InvalidOperationException("A property can't depend on itself");
}
this.OnPropertyChanged(computedPropName);
}
}
}
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
return this.SetField<T>(ref field, value, false, propertyName);
}
protected bool SetField<T>(ref T field, T value, bool forceUpdate, [CallerMemberName] string propertyName = null)
{
bool valueChanged = !EqualityComparer<T>.Default.Equals(field, value);
if (valueChanged || forceUpdate)
{
field = value;
this.OnPropertyChanged(propertyName);
}
return valueChanged;
}
}
}
这些 classes 允许我:
- 在我的属性 setter 中仅使用
this.SetValue(ref this.name, value)
。 - 在 属性 税码 上使用属性
DependsOn(nameof(Name), nameof(LastName))
这样 TaxCode
只有 getter 属性 结合了 FirstName
、LastName
(和其他属性)和 returns相应的代码。由于这个依赖系统,即使绑定了这个 属性 也是最新的。
因此,只要 TaxCode
依赖于同一 class 中的属性,一切都可以正常工作。然而,我需要拥有一个或多个依赖于 其 child object 的属性。例如(我将只使用 json 使层次结构更简单):
{
Name,
LastName,
TaxCode,
Wellness,
House:
{
Value
},
Car:
{
Value
}
}
所以,属性 人的健康应该这样实现:
[DependsOn(nameof(House.Value), nameof(Car.Value))]
public double Wellness { get =>(this.House.Value + this.Car.Value);}
第一个问题是“House.Value”和“Car.Value”在该上下文中不是 nameof
的有效参数。
第二个是,使用我的实际代码,我可以提高仅在相同 object 中的属性,因此没有 child 的属性,也没有 应用程序范围内的属性 (例如,我有一个 属性 表示测量单位是否用 metric/imperial 表示,它的变化会影响值的显示方式)。
现在我可以使用的一个解决方案是在我的 ObservableObject
中插入一个事件字典,键是 属性 的名称,并使 parent 注册一个回调.这样,当 child 的 属性 发生变化时,将触发事件并使用代码通知 parent 中的 属性 已发生变化。然而,这种方法迫使我在每次实例化新的 child 时注册回调。它当然不多,但我喜欢只指定依赖项并让我的基础 class 为我完成工作的想法。
所以,长话短说,我想要实现的是有一个系统可以通知相关的 属性 更改,即使所涉及的属性是它的 child 或与它无关具体 object。由于代码库非常大,我不想放弃现有的 ObservableObject
+ DependsOn 方法,我正在寻找一种比在我的代码中放置回调更优雅的方法。
当然,如果我的方法是错误的/我所拥有的代码无法实现我想要的,请随时提出更好的方法。
我认为,您使用 DependendOn 的方式不适用于更大的项目和更复杂的关系。 (1 到 n,n 到 m,...)
你应该使用观察者模式。例如:你可以有一个集中的地方,所有的 ViewModels (ObservableObjects) 都可以自行注册并开始监听变化事件。您可以使用发送者信息引发更改的事件,每个 ViewModel 都会获取所有事件并可以决定单个事件是否有趣。
如果您的应用程序可以打开多个独立的 windows / 视图,您甚至可以开始为侦听器设置范围,因此独立的 windows / 视图是分开的,并且只获取它们自己范围内的事件。
如果您有一长串显示在虚拟化列表/网格中的项目,您可以检查该项目现在是否真的显示任何 UI,如果没有,请停止收听或根本不关心本例中的事件。
并且您可以稍微延迟引发某些事件(例如那些会触发非常大的 UI 变化的事件),并清除先前事件的队列,如果同一事件再次引发不同延迟内的参数。
我认为所有这些的示例代码对于这个线程来说已经太多了……如果您真的需要其中一个建议的一些代码,请告诉我……
您可以让事件在 ObservableObject
层次结构中向上冒泡。正如建议的那样,底座 class 可以处理连接。
[Serializable]
public abstract class ObservableObject : INotifyPropertyChanged
{
// ...
// Code left out for brevity
// ...
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
return this.SetField<T>(ref field, value, false, propertyName);
}
protected bool SetField<T>(ref T field, T value, bool forceUpdate, [CallerMemberName] string propertyName = null)
{
bool valueChanged = !EqualityComparer<T>.Default.Equals(field, value);
if (valueChanged || forceUpdate)
{
RemovePropertyEventHandler(field as ObservableObject);
AddPropertyEventHandler(value as ObservableObject);
field = value;
this.OnPropertyChanged(propertyName);
}
return valueChanged;
}
protected void AddPropertyEventHandler(ObservableObject observable)
{
if (observable != null)
{
observable.PropertyChanged += ObservablePropertyChanged;
}
}
protected void RemovePropertyEventHandler(ObservableObject observable)
{
if (observable != null)
{
observable.PropertyChanged -= ObservablePropertyChanged;
}
}
private void ObservablePropertyChanged(object sender, PropertyChangedEventArgs e)
{
this.OnPropertyChanged($"{sender.GetType().Name}.{e.PropertyName}");
}
}
现在可以靠孙子了
Models.cs
public class TaxPayer : ObservableObject
{
public TaxPayer(House house)
{
House = house;
}
[DependsOn("House.Safe.Value")]
public string TaxCode => House.Safe.Value;
private House house;
public House House
{
get => house;
set => SetField(ref house, value);
}
}
public class House : ObservableObject
{
public House(Safe safe)
{
Safe = safe;
}
private Safe safe;
public Safe Safe
{
get => safe;
set => SetField(ref safe, value);
}
}
public class Safe : ObservableObject
{
private string val;
public string Value
{
get => val;
set => SetField(ref val, value);
}
}
MainWindow.xaml
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid VerticalAlignment="Center" HorizontalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0">Safe Content:</Label>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding House.Safe.Value, UpdateSourceTrigger=PropertyChanged}" />
<Label Grid.Row="1" Grid.Column="0">Tax Code:</Label>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding TaxCode, Mode=OneWay}" IsEnabled="False" />
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace WpfApp
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext =
new TaxPayer(
new House(
new Safe()));
}
}
}
对于项目范围的依赖项,建议的方法是使用 Dependency Injection。一个广泛的主题,简而言之,您将在抽象的帮助下构建对象树,允许您在运行时交换实现。
带有 DependsOnAttribute
的原始解决方案是个好主意,但实现存在一些性能和多线程问题。无论如何,它不会对您的 class.
class MyItem : ObservableObject
{
public int Value { get; }
[DependsOn(nameof(Value))]
public int DependentValue { get; }
}
有了这个,您可以在任何地方使用您的 MyItem
- 在您的应用程序中,在单元测试中,在您以后可能愿意创建的 class 库中。
现在,考虑这样一个 class:
class MyDependentItem : ObservableObject
{
public IMySubItem SubItem { get; } // where IMySubItem offers some NestedItem property
[DependsOn(/* some reference to this.SubItem.NestedItem.Value*/)]
public int DependentValue { get; }
[DependsOn(/* some reference to GlobalSingleton.Instance.Value*/)]
public int OtherValue { get; }
}
这个 class 现在有两个 "surprising" 依赖项:
MyDependentItem
现在需要知道IMySubItem
类型的特定 属性 (而最初,它只公开该类型的一个实例,而不知道它的细节)。当您以某种方式更改IMySubItem
属性时,您也被迫更改MyDependentItem
class。此外,
MyDependentItem
需要引用全局 object(此处表示为单例)。
所有这些都违反了 SOLID 原则(所有这些都是为了尽量减少代码更改)并使 class 不可测试。它引入了与其他 classes 的紧密耦合并降低了 class' 的凝聚力。你迟早会在调试问题时遇到麻烦。
我认为,Microsoft 在设计 WPF 数据绑定引擎时也遇到了同样的问题。您正在以某种方式尝试重新发明它 - 您正在寻找 PropertyPath
,因为它目前正在 XAML 绑定中使用。为支持这一点,Microsoft 创建了整体依赖性 属性 概念和一个综合数据绑定引擎,用于解析 属性 路径、传输数据值并观察数据变化。我不认为你真的想要那么复杂的东西。
相反,我的建议是:
对于同一个 class 中的 属性 依赖项,请像您当前所做的那样使用
DependsOnAttribute
。我会稍微重构实现以提高性能并确保线程安全。对于外部object的依赖,使用SOLID的依赖倒置原则;在构造函数中将其实现为依赖注入。对于您的测量单位示例,我什至会将数据和表示方面分开,例如通过使用依赖于某些
ICultureSpecificDisplay
(您的测量单位)的 view-model。class MyItem { public double Wellness { get; } } class MyItemViewModel : INotifyPropertyChanged { public MyItemViewModel(MyItem item, ICultureSpecificDisplay display) { this.item = item; this.display = display; } // TODO: implement INotifyPropertyChanged support public string Wellness => display.GetStringWithMeasurementUnits(item.Wellness); }
- 对于object的组合结构中的依赖项,只需手动执行即可。你有多少这样的依赖属性? class 中的情侣?发明一个全面的框架而不是额外的 2-3 行代码是否有意义?
如果我仍然不能说服您 - 那么,您当然可以扩展 DependsOnAttribute
以不仅存储 属性 名称,还存储声明这些属性的类型。您的 ObservableObject
也需要更新。
一起来看看吧。 这是一个扩展属性,也可以保存类型引用。请注意,它现在可以多次应用。
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
class DependsOnAttribute : Attribute
{
public DependsOnAttribute(params string[] properties)
{
Properties = properties;
}
public DependsOnAttribute(Type type, params string[] properties)
: this(properties)
{
Type = type;
}
public string[] Properties { get; }
// We now also can store the type of the PropertyChanged event source
public Type Type { get; }
}
ObservableObject
需要订阅children个事件:
abstract class ObservableObject : INotifyPropertyChanged
{
// We're using a ConcurrentDictionary<K,V> to ensure the thread safety.
// The C# 7 tuples are lightweight and fast.
private static readonly ConcurrentDictionary<(Type, string), string> dependencies =
new ConcurrentDictionary<(Type, string), string>();
// Here we store already processed types and also a flag
// whether a type has at least one dependency
private static readonly ConcurrentDictionary<Type, bool> registeredTypes =
new ConcurrentDictionary<Type, bool>();
protected ObservableObject()
{
Type thisType = GetType();
if (registeredTypes.ContainsKey(thisType))
{
return;
}
var properties = thisType.GetProperties()
.SelectMany(propInfo => propInfo.GetCustomAttributes<DependsOn>()
.SelectMany(attribute => attribute.Properties
.Select(propName =>
(SourceType: attribute.Type,
SourceProperty: propName,
TargetProperty: propInfo.Name))));
bool atLeastOneDependency = false;
foreach (var property in properties)
{
// If the type in the attribute was not set,
// we assume that the property comes from this type.
Type sourceType = property.SourceType ?? thisType;
// The dictionary keys are the event source type
// *and* the property name, combined into a tuple
dependencies[(sourceType, property.SourceProperty)] =
property.TargetProperty;
atLeastOneDependency = true;
}
// There's a race condition here: a different thread
// could surpass the check at the beginning of the constructor
// and process the same data one more time.
// But this doesn't really hurt: it's the same type,
// the concurrent dictionary will handle the multithreaded access,
// and, finally, you have to instantiate two objects of the same
// type on different threads at the same time
// - how often does it happen?
registeredTypes[thisType] = atLeastOneDependency;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var e = new PropertyChangedEventArgs(propertyName);
PropertyChanged?.Invoke(this, e);
if (registeredTypes[GetType()])
{
// Only check dependent properties if there is at least one dependency.
// Need to call this for our own properties,
// because there can be dependencies inside the class.
RaisePropertyChangedForDependentProperties(this, e);
}
}
protected bool SetField<T>(
ref T field,
T value,
[CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}
if (registeredTypes[GetType()])
{
if (field is INotifyPropertyChanged oldValue)
{
// We need to remove the old subscription to avoid memory leaks.
oldValue.PropertyChanged -= RaisePropertyChangedForDependentProperties;
}
// If a type has some property dependencies,
// we hook-up events to get informed about the changes in the child objects.
if (value is INotifyPropertyChanged newValue)
{
newValue.PropertyChanged += RaisePropertyChangedForDependentProperties;
}
}
field = value;
OnPropertyChanged(propertyName);
return true;
}
private void RaisePropertyChangedForDependentProperties(
object sender,
PropertyChangedEventArgs e)
{
// We look whether there is a dependency for the pair
// "Type.PropertyName" and raise the event for the dependent property.
if (dependencies.TryGetValue(
(sender.GetType(), e.PropertyName),
out var dependentProperty))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(dependentProperty));
}
}
}
您可以像这样使用该代码:
class MyClass : ObservableObject
{
private int val;
public int Val
{
get => val;
set => SetField(ref val, value);
}
// MyChildClass must implement INotifyPropertyChanged
private MyChildClass child;
public MyChildClass Child
{
get => child;
set => SetField(ref child, value);
}
[DependsOn(typeof(MyChildClass), nameof(MyChildClass.MyProperty))]
[DependsOn(nameof(Val))]
public int Sum => Child.MyProperty + Val;
}
Sum
属性 依赖于相同 class 的 Val
属性 和 MyProperty
属性 MyChildClass
class.
如您所见,这看起来不太好。此外,整个概念取决于 属性 setter 执行的事件处理程序注册。如果你碰巧直接设置字段值(例如child = new MyChildClass()
),那么这一切都行不通。我建议你不要使用这种方法。