Лучший способ уведомить об изменении свойства, когда поле зависит от другого

Как лучше всего в С# уведомить об изменении свойства в поле элемента без set, но get зависит от других полей?

Например :

public class Example : INotifyPropertyChanged
{
    private MyClass _item;
    public event PropertyChangedEventHandler PropertyChanged;

    public MyClass Item
    {
        get
        {
            return _item;
        }
        protected set
        {
            _item = value;
            OnPropertyChanged("Item");
        }
    }

    public object Field
    {
        get
        {
            return _item.Field;
        }
    }
#if !C#6
    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
#else
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        // => can be called in a set like this: 
        // public MyClass Item { set { _item = value; OnPropertyChanged();} }
        // OnPropertyChanged will be raised for "Item"
    }
#endif
}

Как лучше всего поднять PropertyChanged для "Field" при установке Item? Я хотел вызвать OnPropertyChanged("Field"); при установке Item, но если бы у меня было много полей, код быстро стал бы уродливым и непригодным для сопровождения.

Редактировать:

Интересно, есть ли функция/метод/атрибут, работающий следующим образом:

[DependOn(Item)]
public object Field
{
    get
    {
        return _item.Field;
    }
}

=> Когда Item изменится, все зависимые поля уведомят об изменении свойства.

Он существует?


person A.Pissicat    schedule 11.08.2016    source источник
comment
вы можете реализовать INotify для MyClass   -  person Mong Zhu    schedule 11.08.2016
comment
Field в MyClass не меняется, что экземпляр _item заменяется другим. В моем случае MyClass содержит текст xml. Я делаю веб-запрос, получаю новый экземпляр MyClass и сохраняю его на _item. _item.Field не может уведомить об изменении свойства, потому что Field - это только получатель, который анализирует файл xml.   -  person A.Pissicat    schedule 11.08.2016


Ответы (3)


Один из способов — просто вызвать OnPropertyChanged несколько раз:

public MyClass Item
{
    get
    {
        return _item;
    }
    protected set
    {
        _item = value;
        OnPropertyChanged("Item");
        OnPropertyChanged("Field");
    }
}

Однако это не очень ремонтопригодно. Другой вариант — добавить установщик к свойству только для получения и установить его из другого свойства:

public MyClass Item
{
    get
    {
        return _item;
    }
    protected set
    {
        _item = value;
        OnPropertyChanged("Item");
        Field = _item.Field;
    }
}

public object Field
{
    get
    {
        return _field;
    }
    private set
    {
        _field = value;
        OnPropertyChanged("Field");
    }
}

Не существует встроенного механизма использования атрибутов для указания этой связи между свойствами, однако можно было бы создать вспомогательный класс, который мог бы сделать это за вас.

Я сделал действительно простой пример того, как это может выглядеть здесь:

[AttributeUsage( AttributeTargets.Property )]
public class DepondsOnAttribute : Attribute
{
    public DepondsOnAttribute( string name )
    {
        Name = name;
    }

    public string Name { get; }
}

public class PropertyChangedNotifier<T> : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public PropertyChangedNotifier( T owner )
    {
        mOwner = owner;
    }

    public void OnPropertyChanged( string propertyName )
    {
        var handler = PropertyChanged;
        if( handler != null ) handler( mOwner, new PropertyChangedEventArgs( propertyName ) );

        List<string> dependents;
        if( smPropertyDependencies.TryGetValue( propertyName, out dependents ) )
        {
            foreach( var dependent in dependents ) OnPropertyChanged( dependent );
        }
    }

    static PropertyChangedNotifier()
    {
        foreach( var property in typeof( T ).GetProperties() )
        {
            var dependsOn = property.GetCustomAttributes( true )
                                    .OfType<DepondsOnAttribute>()
                                    .Select( attribute => attribute.Name );

            foreach( var dependency in dependsOn )
            {
                List<string> list;
                if( !smPropertyDependencies.TryGetValue( dependency, out list ) )
                {
                    list = new List<string>();
                    smPropertyDependencies.Add( dependency, list );
                }

                if (property.Name == dependency)
                    throw new ApplicationException(String.Format("Property {0} of {1} cannot depends of itself", dependency, typeof(T).ToString()));

                list.Add( property.Name );
            }
        }
    }

    private static readonly Dictionary<string, List<string>> smPropertyDependencies = new Dictionary<string, List<string>>();

    private readonly T mOwner;
}

Это не очень надежно (например, вы можете создать циклическую зависимость между свойствами, и измененное свойство застрянет в ситуации бесконечной рекурсии). Его также можно упростить, используя некоторые возможности .NET 4.5 и C#6, но я оставлю все это в качестве упражнения для читателя. Вероятно, он также не очень хорошо справляется с наследованием.

Чтобы использовать этот класс:

public class Example : INotifyPropertyChanged
{
    private MyClass _item;
    private PropertyChangedNotifier<Example> _notifier;

    public Example()
    {
        _notifier = new PropertyChangedNotifier<Example>( this );
    }

    public event PropertyChangedEventHandler PropertyChanged
    {
        add { _notifier.PropertyChanged += value; }
        remove { _notifier.PropertyChanged -= value; }
    }

    public MyClass Item
    {
        get
        {
            return _item;
        }
        protected set
        {
            _item = value;
            OnPropertyChanged("Item");
        }
    }

    [DependsOn( "Item" )]
    public object Field
    {
        get
        {
            return _item.Field;
        }
    }
    protected void OnPropertyChanged(string propertyName)
    {
        _notifier.OnPropertyChanged( propertyName );
    }
}
person Kyle    schedule 11.08.2016
comment
Это то, что я на самом деле делаю. Проблема в том, что мой класс еще не исправлен, мне нужно создать поля и удалить поля для проверки. Это долгий путь, который может ввести в заблуждение. Если поле Item игнорирует поля, которые от него зависят, я могу сосредоточиться только на полях новостей. Более удобен в сопровождении и позволяет избежать ошибок - person A.Pissicat; 11.08.2016
comment
Я разработал аналогичный атрибут (не видел вашего редактирования ^^), но ваш PropertyChangedNotifier выглядит лучше, чем мой код. Воспользуюсь вашим примером, спасибо. Я просто добавлю тест, если dependent == propertyName - person A.Pissicat; 11.08.2016
comment
Для полноты картины в PropertyChangedNotifier разверните пример использования, где свойство зависит от нескольких других свойств (наложение [DependsOn(Item)]). - person tomosius; 24.03.2017
comment
Редактировать, стекирование не работает, выдает ошибку компилятора о повторяющихся атрибутах... Вы забыли добавить AllowMultiple = true, вот так [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] - person tomosius; 25.03.2017
comment
Решение с атрибутами на самом деле хорошее. Но что, если бы Item учился в другом классе? Тогда вы не можете использовать это решение. Я ответил на этот вопрос в другом вопросе, где решение такое же, даже если свойство было в другом классе: stackoverflow.com/questions/43653750/ - person Jogge; 28.04.2017

Насколько я знаю, встроенного метода для этого нет. Я обычно делаю так:

public class Foo : INotifyPropertyChanged
{
    private Bar _bar1;

    public Bar Item
    {
        get { return _bar1; }
        set 
        { 
             SetField(ref _bar1, value); 
             ItemChanged();
        }
    }

    public string MyString
    {
        get { return _bar1.Item; }
    }

    private void ItemChanged()
    {
        OnPropertyChanged("MyString");
    }
}

public class Bar
{
    public string Item { get; set; }
}

Таким образом, у вас нет логики уведомления внутри свойства. На мой взгляд, так удобнее в сопровождении, и понятно, что делает метод.

Кроме того, я предпочитаю использовать этот метод, который я нашел где-то на SO, вместо жестко запрограммированного имени в классе (если имя свойства меняется, оно ломается).

OnPropertyChanged("MyString"); становится OnPropertyChanged(GetPropertyName(() => MyString));

где GetPropertyName это:

public static string GetPropertyName<T>(Expression<Func<T>> propertyLambda)
{
    if (propertyLambda == null) throw new ArgumentNullException("propertyLambda");

    var me = propertyLambda.Body as MemberExpression;

    if (me == null)
    {
        throw new ArgumentException("You must pass a lambda of the form: '() => Class.Property' or '() => object.Property'");
    }

    return me.Member.Name;
}

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

Мне также любопытен встроенный способ сделать зависимость, поэтому я добавляю туда избранное :)

person Etienne Faucher    schedule 11.08.2016

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

Решение состоит в том, чтобы создать пользовательскую оболочку для запуска событий INotifyPropertyChanged. Вместо того, чтобы вызывать OnPropertyChanged вручную, мы можем определить следующие методы (желательно внутри базового класса, который мы будем использовать позже):

public abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    internal void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected ViewModelPropertyChange SetPropertyValue<T>(ref T property, T value, [CallerMemberName] string propertyName = null)
    {
        property = value;
        OnPropertyChanged(propertyName);

        return new ViewModelPropertyChange(this);
    }
}

Этот класс предоставляет нам способ установки значения данного поля без необходимости указывать имя стороны, от которой исходит вызов.

Мы также должны определить класс, который позволит использовать для определения зависимых свойств (экземпляр этого класса возвращается из метода SetPropertyValue).

public class ViewModelPropertyChange
{
    private readonly ViewModelBase _viewModel;

    public ViewModelPropertyChange(ViewModelBase viewModel)
    {
        _viewModel = viewModel;
    }

    public ViewModelPropertyChange WithDependent(string name)
    {
        _viewModel.OnPropertyChanged(name);

        return this;
    }
}

Он просто хранит ссылку на объект, который изменяется, и позволяет распространить событие на следующие свойства.

При этом мы можем создать класс, производный от ViewModelBase, следующим образом:

class OurViewModel : ViewModelBase
{
    private int _partOne;
    public int PartOne
    {
        get => _partOne;
        set => SetPropertyValue(ref _partOne, value)
            .WithDependent(nameof(Total));
    }

    private int _partTwo;
    public int PartTwo
    {
        get => _partTwo;
        set => SetPropertyValue(ref _partTwo, value)
            .WithDependent(nameof(Total))
            .WithDependent(nameof(PartTwoPlus2));
    }

    public int Total {
        get => PartOne + PartTwo;
    }

    public int PartTwoPlus2 {
        get => PartTwo + 2;
    }
}
person Tomasz    schedule 21.03.2018