【问题标题】:Best way to notify property change when field is depending on another当字段依赖于另一个字段时通知属性更改的最佳方式
【发布时间】:2016-12-18 04:36:02
【问题描述】:

在没有set 但get 取决于其他字段的情况下,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");,但如果我有很多字段,代码很快就会变得丑陋且无法维护。

编辑:

我想知道是否有这样的函数/方法/属性:

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

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

它存在吗?

【问题讨论】:

  • 你可以为MyClass实现INotify
  • Field in MyClass 不会改变,实例 _item 被另一个替换。在我的例子中,MyClass 包含一个 xml 文本。我通过 webRequest 获取MyClass 的新实例并将其保存在_item 上。 _item.Field 无法通知属性更改,因为Field 只是解析xml文件的get。

标签: c# inotifypropertychanged


【解决方案1】:

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

解决方案是创建一个自定义包装器来触发 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;
    }
}

【讨论】:

    【解决方案2】:

    一种方法是多次调用OnPropertyChanged:

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

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

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

    【讨论】:

    • 这就是我实际所做的。问题是我的课程还没有修复,我必须创建字段并删除字段进行测试。这是一个很长的路,可能会产生误导。如果字段Item“忽略”依赖它的字段,我可以只关注新闻字段。更易于维护,可以避免错误
    • 我开发了一个类似的属性(没有看到您的编辑 ^^),但您的 PropertyChangedNotifier 看起来比我的代码更好。我会用你的例子,谢谢。如果dependent == propertyName,我将添加测试
    • 为了完整起见,在您的 PropertyChangedNotifier 中,扩展属性依赖于多个其他属性的示例用法(堆叠 [DependsOn( "Item" )])。
    • 编辑,堆叠不起作用,给出关于重复属性的编译器错误...您忘记添加 AllowMultiple = true,就像这样 [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
    • 属性解其实不错。但是如果Item 在另一个班级呢?那么你不能使用这个解决方案。我已经在另一个问题中回答了这个问题,其中的解决方案是相同的,即使该属性在另一个类中:stackoverflow.com/questions/43653750/…
    【解决方案3】:

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

    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(() =&gt; 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 的所有位置重命名该属性,而不是搜索硬编码的字符串值。

    我也很好奇一种内置的方式来做依赖,所以我把一个最喜欢的放在里面:)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-11-30
      • 2019-02-16
      • 2013-12-26
      • 1970-01-01
      • 2019-03-11
      • 1970-01-01
      相关资源
      最近更新 更多