当字段依赖于另一个字段时通知 属性 更改的最佳方式

Best way to notify property change when field is depending on another

在没有 setget 取决于其他字段的情况下,在 c# 中通知 属性 项目字段更改的最佳方法是什么?

例如:

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
}

设置 Item 时为 "Field" 增加 PropertyChanged 的​​最佳方法是什么?我想在设置 Item 时调用 OnPropertyChanged("Field"); 但如果我有很多字段,代码很快就会变得丑陋且无法维护。

编辑:

我想知道是否有一个 function/method/attribute 是这样工作的:

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

=>当Item改变时,所有依赖字段都会通知属性改变。

它存在吗?

一种方法是多次调用 OnPropertyChanged

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

但是,这不是很容易维护。另一种选择是将 setter 添加到您的 get-only 属性 并从另一个 属性:

进行设置
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");
    }
}

没有内置机制可以使用属性来指示属性之间的这种关系,但是可以创建一个助手 class 来为您做到这一点。

我在这里做了一个非常基本的例子:

[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 功能变得更简单,但我将把所有这些留作 reader 的练习。它可能也不能很好地处理继承。

要使用这个 class:

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

据我所知,没有内置的方法。我通常这样做:

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 某处找到的这种方法,而不是 class 中的硬编码名称(如果 属性 的名称更改,它会中断)。

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 处重命名 属性,而不是搜索硬编码的字符串值。

我也对一种内置的依赖关系很好奇,所以我把最喜欢的放在那里:)

即使在此解决方案中,事件仍然从 setter 传播(因此不完全是问题所在),它提供了一种很好的、​​更易于管理的方式来表示依赖关系。有人可能会觉得有用。

解决方案是创建用于触发 INotifyPropertyChanged 事件的自定义包装器。我们可以定义以下方法,而不是手动调用 OnPropertyChanged(最好在我们稍后将重用的基础 class 内):

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

这 class 为我们提供了一种设置给定字段值的方法,而无需提供调用方的名称。

我们还必须定义一个 class 来定义依赖属性(此 class 的实例从 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,如下所示:

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