WPF: INotifyPropertyChanged И производные свойства для разных объектов - PullRequest
0 голосов
/ 31 января 2019

Недавно я унаследовал довольно большой проект, разработанный на 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. Использоватьатрибут DependsOn(nameof(Name), nameof(LastName)) в свойстве TaxCode

Таким образом, TaxCode имеет только свойство-получатель, которое объединяет FirstName, LastName (и другие свойства) и возвращает соответствующий код.Даже с привязкой это свойство является актуальным благодаря этой системе зависимостей.

Таким образом, пока TaxCode имеет зависимости от свойств, которые находятся в одном классе, все работает правильно.Однако мне нужно иметь свойства, которые имеют одну или несколько зависимостей от их дочернего объекта .Например (я просто использую json, чтобы сделать иерархию более простой):

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

Итак, свойство Wellness человека может быть реализовано так:

[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 голосов
/ 05 февраля 2019

Вы можете позволить событиям всплывать в иерархии 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 .Широкая тема, короче говоря, вы должны построить дерево объектов с помощью абстракций, что позволит вам менять реализации во время выполнения.

0 голосов
/ 06 февраля 2019

Оригинальное решение с 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 требуется ссылка на глобальный объект (представленный в видеSingleton здесь).

Все это нарушает принципы 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()), то все это не будет работать.Я бы посоветовал вам не использовать этот подход.

0 голосов
/ 05 февраля 2019

Я думаю, способ, которым вы пользуетесь с DependendOn, не работает для более крупных проектов и более сложных отношений.(От 1 до n, от n до m,…)

Вы должны использовать шаблон наблюдателя.Например: у вас может быть централизованное место, где все ViewModels (ObservableObjects) регистрируют себя и начинают слушать изменения событий.Вы можете вызвать измененные события с помощью информации об отправителе, и каждая ViewModel получает все события и может решить, является ли интересным одно событие.

Если ваше приложение может открывать несколько независимых окон / представлений, вы даже можете начать охватыватьслушатели, поэтому независимые окна / представления разделены и получают только события своей области видимости.

Если у вас есть длинные списки элементов, которые отображаются в виртуализированном списке / сетке, вы можете проверить, действительно ли элемент отображает какие-либоПользовательский интерфейс прямо сейчас, и если не прекратить слушать или просто не заботиться о событиях в этом случае.

И вы можете вызывать определенные события (например, те события, которые вызовут действительно большое изменение пользовательского интерфейса) с небольшой задержкой,и очистить очередь предыдущих событий, если одно и то же событие будет вызвано снова с другими параметрами в пределах задержки.

Я думаю, что пример кода для всего этого будет слишком для этого потока ... Если вам действительно нужен код дляодно из предложений, скажи мне ...

...