最近,我继承了一个用C#和WPF开发的相当大的项目。 它使用绑定以及INotifyPropertyChanged接口将更改传播到视图或从视图传播。
一些前言: 在不同的类中,我有一些属性,它们依赖于同一类中的其他属性 (例如,属性“ TaxCode”依赖于诸如“名称”和“姓氏”之类的属性)。 借助于一些我在SO上找到的代码(尽管找不到答案),我创建了抽象类“ 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 stackoverflow 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;
}
}
}
这些课程使我能够:
this.SetValue(ref this.name, value)
。DependsOn(nameof(Name), nameof(LastName))
这种方式,TaxCode仅具有将FirstName,LastName(和其他属性)组合在一起的getter属性,并返回相应的代码。即使有了绑定,由于有了依赖系统,该属性仍是最新的。
因此,只要TaxCode对同一类中的属性具有依赖性,则所有内容都将正常运行。但是,我需要具有对其子对象具有一个或多个依赖项的属性。例如(我将仅使用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的有效参数。 第二个是,通过我的实际代码,我可以引发仅在同一个对象中的属性,因此没有子级的属性,也没有应用范围内的属性(例如,我有一个属性来表示单位是否度量单位以公制/英制表示,其变化会影响值的显示方式。
现在,我可以使用的解决方案是在ObservableObject中插入事件字典,其键为属性名称,并使父级注册回调。这样,当子代的属性发生更改时,事件将通过代码触发,以通知父代的属性已更改。但是,这种方法迫使我每次实例化一个新孩子时都注册回调。肯定不多,但是我喜欢仅指定依赖项并让我的基类为我完成工作的想法。
因此,长话短说,我想要达到的目标是拥有一个能够通知相关属性更改的系统,即使所涉及的属性是其子级或与该特定对象无关。由于代码库很大,所以我不想抛弃现有的ObservableObject + DependsOn方法,而且我正在寻找一种比将回调函数放在整个代码中更优雅的方法。
当然,如果我的方法是错误的/用我所拥有的代码无法实现我想要的东西,请随时提出更好的方法。我不是WPF专家,我正在努力学习尽可能多的东西。
谢谢。
答案 0 :(得分:2)
使用DependsOnAttribute
的原始解决方案是一个不错的主意,但是该实现存在一些性能和多线程问题。无论如何,它不会给您的类带来任何令人惊讶的依赖。
class MyItem : ObservableObject
{
public int Value { get; }
[DependsOn(nameof(Value))]
public int DependentValue { get; }
}
有了这个,您可以在任何地方使用MyItem
-在您的应用程序,单元测试中,或者您以后可能愿意创建的类库中。
现在,考虑一个这样的课程:
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; }
}
该类现在有两个“令人惊讶”的依赖项:
MyDependentItem
现在需要知道IMySubItem
类型的特定属性(而最初,它仅公开该类型的实例,而并不知道其详细信息)。当您以某种方式更改IMySubItem
属性时,也不得不更改MyDependentItem
类。
此外,MyDependentItem
需要引用一个全局对象(在这里表示为单例)。
所有这些都破坏了 SOLID 原则(这是为了最大程度地减少代码更改),并使该类不可测试。它引入了与其他班级的紧密联系,并降低了班级的凝聚力。迟早会调试与此相关的问题。
我认为,Microsoft在设计WPF数据绑定引擎时面临同样的问题。您正在以某种方式尝试重新发明它-您正在寻找PropertyPath
,因为它目前正在XAML绑定中使用。为此,Microsoft创建了整个依赖项属性概念和一个全面的数据绑定引擎,该引擎可解析属性路径,传输数据值并观察数据更改。我不认为您真的想要这种复杂的东西。
相反,我的建议是:
对于同一类中的属性依赖项,请像现在一样使用DependsOnAttribute
。我会稍微重构实现以提高性能并确保线程安全。
要获得对外部对象的依赖关系,请使用 SOLID 的依赖关系反转原理;在构造函数中将其实现为依赖项注入。对于您的度量单位示例,我什至将数据和表示方面分开,例如通过使用与某些ICultureSpecificDisplay
(您的度量单位)相关的视图模型。
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);
}
如果我仍然不能说服您-那么,您当然可以扩展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
需要订阅子事件:
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
属性取决于同一类的Val
属性和MyProperty
类的MyChildClass
属性。
如您所见,这看起来并不好。此外,整个概念取决于属性设置程序执行的事件处理程序注册。如果您碰巧直接设置了字段值(例如child = new MyChildClass()
),那么所有这些都将无效。我建议您不要使用这种方法。
答案 1 :(得分:1)
我认为,与DependendOn一起使用的方式不适用于较大的项目和更复杂的关系。 (1到n,n到m,...)
您应该使用观察者模式。例如:您可以在一个集中的地方,所有ViewModel(ObservableObjects)自行注册并开始侦听更改事件。您可以使用发件人信息引发更改的事件,每个ViewModel都可以获取所有事件,并可以确定单个事件是否有趣。
如果您的应用程序可以打开多个独立的窗口/视图,您甚至可以开始调整侦听器的作用域,因此独立的窗口/视图将被分离,并且仅获得其自身作用域的事件。
如果您有较长的项目列表显示在虚拟化的列表/网格中,则可以检查该项目现在是否真的在显示任何用户界面,如果不停止监听或者在这种情况下根本不关心事件,< / p>
并且您可以稍微延迟地引发某些事件(例如,那些会触发非常大的UI更改的事件),并清除先前事件的队列,如果相同的事件通过延迟内的不同参数再次引发。 / p>
我认为所有这些示例代码对于该线程来说都是很多……如果您真的需要一些建议代码,请告诉我……
答案 2 :(得分:1)
您可以让事件在ObservableObject
层次结构中冒泡。如建议的那样,基类可以处理该挂钩。
[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。简而言之,您将在抽象的帮助下构建对象树,从而允许您在运行时交换实现。