[英]How to update the derived properties using INotifyPropertyChanged
[英]INotifyPropertyChanged And derived properties on different objects
最近我繼承了一個用 C# 和 WPF 開發的相當大的項目。 它使用綁定和INotifyPropertyChanged
接口將更改傳播到/從視圖傳播。
一點前言:在不同的類中,我的屬性依賴於同一類中的其他屬性(例如,屬性TaxCode
依賴於Name
和Lastname
等屬性)。 借助我在 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))
使用屬性DependsOn(nameof(Name), nameof(LastName))
這樣TaxCode
只有一個 getter 屬性,它結合了FirstName
、 LastName
(和其他屬性)並返回相應的代碼。 由於這個依賴系統,即使綁定了這個屬性也是最新的。
因此,只要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 方法,而且我正在尋找一種更優雅的方法,而不僅僅是在我的代碼中放置回調。
當然,如果我的方法是錯誤的/我所擁有的代碼無法實現我想要的,請隨時提出更好的方法。
帶有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原則(這一切都是為了盡量減少代碼的更改)並使類無法測試。 它引入了與其他類的緊密耦合並降低了類的內聚力。 你遲早會在調試問題時遇到麻煩。
我認為,微軟在設計 WPF 數據綁定引擎時也面臨着同樣的問題。 您正在以某種方式嘗試重新發明它 - 您正在尋找一個PropertyPath
因為它目前正在 XAML 綁定中使用。 為了支持這一點,微軟創建了整個依賴屬性概念和一個全面的數據綁定引擎,用於解析屬性路徑、傳輸數據值並觀察數據變化。 我不認為你真的想要那么復雜的東西。
相反,我的建議是:
對於同一類中的屬性依賴項,請像您當前所做的那樣使用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
屬性和MyChildClass
類的MyProperty
屬性。
如您所見,這看起來並不那么好。 此外,整個概念取決於屬性設置器執行的事件處理程序注冊。 如果您碰巧直接設置字段值(例如child = new MyChildClass()
),那么這一切都將不起作用。 我建議你不要使用這種方法。
我認為,您使用 DependendOn 的方式不適用於更大的項目和更復雜的關系。 (1 到 n,n 到 m,……)
您應該使用 am 觀察者模式。 例如:您可以有一個集中的地方,所有 ViewModels (ObservableObjects) 都可以在其中自行注冊並開始監聽更改事件。 您可以使用發件人信息引發更改的事件,每個 ViewModel 都會獲取所有事件,並可以決定單個事件是否有趣。
如果您的應用程序可以打開多個獨立的窗口/視圖,您甚至可以開始限定偵聽器的范圍,因此獨立的窗口/視圖是分開的,並且只獲取它們自己范圍內的事件。
如果您在虛擬化列表/網格中顯示了很長的項目列表,您可以檢查該項目現在是否真的顯示了任何 UI,如果沒有停止監聽或在這種情況下不關心事件。
並且您可以稍微延遲引發某些事件(例如那些會觸發非常大的 UI 更改的事件),並清除先前事件的隊列,如果在延遲內使用不同的參數再次引發相同的事件。
我認為所有這些的示例代碼對於這個線程來說太多了......如果你真的需要一些建議的代碼,告訴我......
您可以讓事件在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}");
}
}
現在你可以依靠孫子了。
模型.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);
}
}
主窗口.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>
主窗口.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 。 一個廣泛的主題,簡而言之,您將在抽象的幫助下構建對象樹,從而允許您在運行時交換實現。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.