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

Кой е най-добрият начин в c# за уведомяване на свойството, променено в полето на елемент без 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 текст. Правя webRequest за получаване на нов екземпляр на MyClass и го запазвам на _item. _item.Field не може да уведоми за промяна на свойството, защото Field е само get, който анализира 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