简体   繁体   English

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

[英]INotifyPropertyChanged And derived properties on different objects

Recently I inherited a pretty big project developed in C# and WPF.最近我继承了一个用 C# 和 WPF 开发的相当大的项目。 It uses bindings along with the INotifyPropertyChanged interface to propagate changes to/from the View.它使用绑定和INotifyPropertyChanged接口将更改传播到/从视图传播。

A little preface: In different classes I have properties that depend on other properties in the same class (think for example the property TaxCode that depends on properties like Name and Lastname ).一点前言:在不同的类中,我的属性依赖于同一类中的其他属性(例如,属性TaxCode依赖于NameLastname等属性)。 With the help of some code I found here on SO (can't find again the answer though) I created the abstract class ObservableObject and the attribute DependsOn .借助我在 SO 上找到的一些代码(虽然找不到答案),我创建了抽象类ObservableObject和属性DependsOn The source is the following:来源如下:

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;
        }
    }
}

These classes allow me to:这些课程允许我:

  1. Use just this.SetValue(ref this.name, value) inside the setter of my properties.在我的属性的 setter 中只使用this.SetValue(ref this.name, value)
  2. Use the attribute DependsOn(nameof(Name), nameof(LastName)) on the property TaxCode在属性DependsOn(nameof(Name), nameof(LastName))使用属性DependsOn(nameof(Name), nameof(LastName))

This way TaxCode only has a getter property that combines FirstName , LastName (and other properties) and returns the corresponding code.这样TaxCode只有一个 getter 属性,它结合了FirstNameLastName (和其他属性)并返回相应的代码。 Even with binding this property is up to date thanks to this dependency system.由于这个依赖系统,即使绑定了这个属性也是最新的。

So, as long as TaxCode has dependencies on properties that are in the same class, everything works correctly.因此,只要TaxCode依赖于同一类中的属性,一切都可以正常工作。 However I'm in the need to have properties that have one or more dependencies on their child object .但是,我需要拥有对其子对象具有一个或多个依赖关系的属性 For example (I'll just use json to make the hierarchy more simple):例如(我将只使用 json 使层次结构更简单):

{
  Name,
  LastName,
  TaxCode,
  Wellness,
  House:
  {
    Value
  },
  Car:
  {
    Value
  }
}

So, the Property Wellness of person sould be implemented like this:因此,人的财产健康应该像这样实现:

[DependsOn(nameof(House.Value), nameof(Car.Value))]
public double Wellness { get =>(this.House.Value + this.Car.Value);}

The first problem is that "House.Value" and "Car.Value" are not valid parameters for nameof in that context.第一个问题是“House.Value”和“Car.Value”在该上下文中不是nameof有效参数。 The second is that with my actual code I can raise properties that are only in the same object so no properties of childs, nor properties that are application wide (I have for example a property that represents if the units of measurement are expressed in metric/imperial and the change of it affects how values are shown).第二个是,使用我的实际代码,我可以提升仅在同一对象中的属性,因此没有孩子的属性,也没有应用程序范围的属性(例如,我有一个属性表示测量单位是否以公制/英制及其更改会影响值的显示方式)。

Now a solution I could use could be to insert a dictionary of events in my ObservableObject with the key being the name of the property and make the parent register a callback.现在我可以使用的一个解决方案是在我的ObservableObject插入一个事件字典,键是属性的名称,并使父级注册回调。 This way when the property of a child changes the event is fired with the code to notify that a property in the parent has changed.这样,当子项的属性发生更改时,将使用代码触发事件以通知父项中的属性已更改。 This approach however forces me to register the callbacks everytime a new child is instantiated.然而,这种方法迫使我在每次实例化新子项时注册回调。 It is certainly not much, but I liked the idea of just specifying dependencies and let my base class do the work for me.这当然不多,但我喜欢只指定依赖项并让我的基类为我完成工作的想法。

So, long story short, what I'm trying to achieve is to have a system that can notify dependent property changes even if the properties involved are its childs or are unrelated to that specific object.所以,长话短说,我想要实现的是拥有一个可以通知依赖属性更改的系统,即使所涉及的属性是其子项或与该特定对象无关。 Since the codebase is quite big I'd like not to just throw away the existing ObservableObject + DependsOn approach, and I'm looking for a more elegant way than just place callbacks all over my code.由于代码库非常大,我不想只是丢弃现有的ObservableObject + DependsOn 方法,而且我正在寻找一种更优雅的方法,而不仅仅是在我的代码中放置回调。

Of course If my approach is wrong / what I want cannot be achieved with the code I have, please DO feel free to suggest better ways.当然,如果我的方法是错误的/我所拥有的代码无法实现我想要的,请随时提出更好的方法。

The original solution with a DependsOnAttribute is a nice idea, but the implementation has a couple of performance and multithreading issues.带有DependsOnAttribute的原始解决方案是一个不错的主意,但该实现存在一些性能和多线程问题。 Anyway, it doesn't introduce any surprising dependencies to your class.无论如何,它不会为您的类引入任何令人惊讶的依赖项。

class MyItem : ObservableObject
{
    public int Value { get; }

    [DependsOn(nameof(Value))]
    public int DependentValue { get; }
}

Having this, you can use your MyItem anywhere - in your app, in unit tests, in a class library you might be willing to create later.有了这个,你可以在任何地方使用你的MyItem - 在你的应用程序中,在单元测试中,在你以后可能愿意创建的类库中。

Now, consider such a 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; }
}

This class has two "surprising" dependencies now:这个类现在有两个“令人惊讶”的依赖项:

  • MyDependentItem now needs to know a particular property of the IMySubItem type (whereas originally, it only exposes an instance of that type, without knowing its details). MyDependentItem现在需要知道IMySubItem类型的特定属性(而最初,它只公开该类型的实例,而不知道其详细信息)。 When you change the IMySubItem properties somehow, you are forced to change the MyDependentItem class too.当您以某种方式更改IMySubItem属性时,您也被迫更改MyDependentItem类。

  • Additionally, MyDependentItem needs a reference to a global object (represented as a singleton here).此外, MyDependentItem需要对全局对象的引用(此处表示为单例)。

All this breaks the SOLID principles (it's all about to minimize changes in code) and makes the class not testable.所有这些都违反了SOLID原则(这一切都是为了尽量减少代码的更改)并使类无法测试。 It introduces a tight coupling to other classes and lowers the class' cohesion.它引入了与其他类的紧密耦合并降低了类的内聚力。 You will have troubles debugging the issues with that, sooner or later.你迟早会在调试问题时遇到麻烦。

I think, Microsoft faced same issues when they designed the WPF Data Binding Engine.我认为,微软在设计 WPF 数据绑定引擎时也面临着同样的问题。 You're somehow trying to reinvent it - you're looking for a PropertyPath as it is currently being used in XAML bindings.您正在以某种方式尝试重新发明它 - 您正在寻找一个PropertyPath因为它目前正在 XAML 绑定中使用。 To support this, Microsoft created the whole dependency property concept and a comprehensive Data Binding Engine that resolves the property paths, transfers the data values and observes the data changes.为了支持这一点,微软创建了整个依赖属性概念和一个全面的数据绑定引擎,用于解析属性路径、传输数据值并观察数据变化。 I don't think you really want something of that complexity.我不认为你真的想要那么复杂的东西。

Instead, my suggestions would be:相反,我的建议是:

  • For the property dependencies in the same class, use the DependsOnAttribute as you're currently doing.对于同一类中的属性依赖项,请像您当前所做的那样使用DependsOnAttribute I would slightly refactor the implementation to boost the performance and to ensure the thread safety.我会稍微重构实现以提高性能并确保线程安全。

  • For a dependency to an external object, use the Dependency Inversion Principle of SOLID ;对于外部对象的依赖,使用SOLID的依赖倒置原则; implement it as dependency injection in constructors.在构造函数中将其实现为依赖注入。 For your measurement units example, I would even separate the data and the presentation aspects, eg by using a view-model that has a dependency to some ICultureSpecificDisplay (your measurement units).对于您的测量单位示例,我什至会将数据和表示方面分开,例如通过使用依赖于某些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); }
    • For a dependency in the composition structure of your object, just do it manually.对于对象组合结构中的依赖项,只需手动执行即可。 How many such dependent properties do you have?你有多少这样的依赖属性? A couple in a class?一对情侣? Does it make sense to invent a comprehensive framework instead of additional 2-3 lines of code?发明一个全面的框架而不是额外的 2-3 行代码有意义吗?

If I still didn't convince you - well, you can of course extend your DependsOnAttribute to store not only property names but also the types where those properties are declared.如果我仍然没有说服您 - 好吧,您当然可以扩展您的DependsOnAttribute以不仅存储属性名称,还存储声明这些属性的类型。 Your ObservableObject needs to be updated too.您的ObservableObject需要更新。

Let's take a look.让我们来看看。 This is an extended attribute that also can hold the type reference.这是一个扩展属性,也可以保存类型引用。 Note that it can be applied multiple times now.请注意,它现在可以多次应用。

[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; }
}

The ObservableObject needs to subscribe to the children events: 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));
        }
    }
}

You can use that code like this:您可以像这样使用该代码:

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;
}

The Sum property depends on the Val property of the same class and on the MyProperty property of the MyChildClass class. Sum属性取决于同一类的Val属性和MyChildClass类的MyProperty属性。

As you see, this doesn't look that great.如您所见,这看起来并不那么好。 Furthermore, the whole concept depends on the event handler registration performed by the property setters.此外,整个概念取决于属性设置器执行的事件处理程序注册。 If you happen to set the field value directly (eg child = new MyChildClass() ), then it all won't work.如果您碰巧直接设置字段值(例如child = new MyChildClass() ),那么这一切都将不起作用。 I would suggest you not to use this approach.我建议你不要使用这种方法。

I think, the way, you are using with DependendOn is not working for bigger projects and more complicated relations.我认为,您使用 DependendOn 的方式不适用于更大的项目和更复杂的关系。 (1 to n, n to m, …) (1 到 n,n 到 m,……)

You should use am observer pattern.您应该使用 am 观察者模式。 Eg: You can have a centralized place where all ViewModels (ObservableObjects) register them self and start listening to change events.例如:您可以有一个集中的地方,所有 ViewModels (ObservableObjects) 都可以在其中自行注册并开始监听更改事件。 You can raise the changed events with sender information, and every ViewModel gets all events and can decide if a single event is interesting.您可以使用发件人信息引发更改的事件,每个 ViewModel 都会获取所有事件,并可以决定单个事件是否有趣。

If your application can open multiple independent windows / views, you can even start to scope the listeners, so independent windows / views are separated and get only events of their own scope.如果您的应用程序可以打开多个独立的窗口/视图,您甚至可以开始限定侦听器的范围,因此独立的窗口/视图是分开的,并且只获取它们自己范围内的事件。

If you have long lists of items which are shown in a virtualized list / grid you can check if the item is really displaying any UI right now and if not stop listening or just don't care about events in this case.如果您在虚拟化列表/网格中显示了很长的项目列表,您可以检查该项目现在是否真的显示了任何 UI,如果没有停止监听或在这种情况下不关心事件。

And you can raise certain events (eg those events which would trigger a really big UI change) with a little delay, and clear the queue of previous events, if the same event is raised again with different parameters within the delay.并且您可以稍微延迟引发某些事件(例如那些会触发非常大的 UI 更改的事件),并清除先前事件的队列,如果在延迟内使用不同的参数再次引发相同的事件。

I think sample code for all this would be to much for this thread… If you really need some code for one of the suggestions, tell me…我认为所有这些的示例代码对于这个线程来说太多了......如果你真的需要一些建议的代码,告诉我......

You can let the events bubble up through the ObservableObject hierarchy.您可以让事件在ObservableObject层次结构中冒泡。 As suggested, the base class could handle the hook up.正如所建议的,基类可以处理连接。

[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}");
    }
}

Now you can depend on a grandchild.现在你可以依靠孙子了。

Models.cs模型.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主窗口.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主窗口.xaml.cs

using System.Windows;

namespace WpfApp
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.DataContext = 
                new TaxPayer(
                    new House(
                        new Safe()));
        }
    }
}

For project wide dependencies the advised approach is to use Dependency Injection .对于项目范围的依赖项,建议的方法是使用Dependency Injection A broad topic, in short you'd build the object tree with the help of abstractions, allowing you to swap implementations at runtime.一个广泛的主题,简而言之,您将在抽象的帮助下构建对象树,从而允许您在运行时交换实现。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM