簡體   English   中英

INotifyPropertyChanged 和不同對象上的派生屬性

[英]INotifyPropertyChanged And derived properties on different objects

最近我繼承了一個用 C# 和 WPF 開發的相當大的項目。 它使用綁定和INotifyPropertyChanged接口將更改傳播到/從視圖傳播。

一點前言:在不同的類中,我的屬性依賴於同一類中的其他屬性(例如,屬性TaxCode依賴於NameLastname等屬性)。 借助我在 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;
        }
    }
}

這些課程允許我:

  1. 在我的屬性的 setter 中只使用this.SetValue(ref this.name, value)
  2. 在屬性DependsOn(nameof(Name), nameof(LastName))使用屬性DependsOn(nameof(Name), nameof(LastName))

這樣TaxCode只有一個 getter 屬性,它結合了FirstNameLastName (和其他屬性)並返回相應的代碼。 由於這個依賴系統,即使綁定了這個屬性也是最新的。

因此,只要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); }
    • 對於對象組合結構中的依賴項,只需手動執行即可。 你有多少這樣的依賴屬性? 一對情侶? 發明一個全面的框架而不是額外的 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需要訂閱子事件:

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM