WPF:INotifyPropertyChanged和不同对象上的派生属性

时间:2019-01-31 17:39:19

标签: c# wpf data-binding parent-child inotifypropertychanged

最近,我继承了一个用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;
        }
    }
}

这些课程使我能够:

  1. 在属性的设置器中仅使用this.SetValue(ref this.name, value)
  2. 在属性TaxCode上使用属性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专家,我正在努力学习尽可能多的东西。

谢谢。

3 个答案:

答案 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);
     }
    
    • 对于对象的合成结构中的依赖项,只需手动进行即可。您有多少个此类依存属性?一对班上的一对?发明一个全面的框架而不是额外的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属性和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。简而言之,您将在抽象的帮助下构建对象树,从而允许您在运行时交换实现。