INotifyPropertyChanged 和派生属性不同 objects

INotifyPropertyChanged And derived properties on different objects

最近接手了一个用C#和WPF开发的比较大的项目。 它使用绑定以及 INotifyPropertyChanged 接口来传播更改 to/from 视图。

一点前言: 在不同的 classes 中,我的属性依赖于同一个 class 中的其他属性 (例如考虑 属性 TaxCode取决于 NameLastname 等属性)。 借助我在 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 允许我:

  1. 在我的属性 setter 中仅使用 this.SetValue(ref this.name, value)
  2. 在 属性 税码
  3. 上使用属性 DependsOn(nameof(Name), nameof(LastName))

这样 TaxCode 只有 getter 属性 结合了 FirstNameLastName(和其他属性)和 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()),那么这一切都行不通。我建议你不要使用这种方法。