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

后端 未结 3 1366
粉色の甜心
粉色の甜心 2021-01-31 06:16

What is the best way in c# to notify property changed on an item\'s field without set but get depends on other fields ?

For example :



        
相关标签:
3条回答
  • 2021-01-31 07:09

    As far as I know, there is not built in method for that. I usually do like this:

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

    You don't have the notifying logic inside of the property this way. It is more maintainable in my opinion this way and it is clear what the method does.

    Also, I prefer to use this method I found somewhere on SO instead of the hardcoded name in the class (if the name of the property changes, it breaks).

    OnPropertyChanged("MyString"); becomes OnPropertyChanged(GetPropertyName(() => MyString));

    where GetPropertyName is:

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

    After this, every time I change the property as the name, I will have to rename the property everywhere I have GetPropertyName instead of searching for the hardcoded string values.

    I'm also curious about a built-in way to do the dependency, so I'm putting a favorite in there :)

    0 讨论(0)
  • 2021-01-31 07:17

    One way is to just call OnPropertyChanged multiple times:

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

    This isn't very maintainable, however. Another option is to add a setter to your get-only property and set it from the other property:

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

    There is no built-in mechanism for using attributes to indicate this relationship between properties, however it would be possible to create a helper class that could do it for you.

    I've made a really basic example of what that might look like here:

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

    This isn't terribly robust (for example you could create a circular dependency between properties and the property changed would get stuck in an infinite recursion situation). It can also be made simpler using some .NET 4.5 and C#6 features, but I'll leave all that as an exercise for the reader. It probably also doesn't handle inheritance very well.

    To use this 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 );
        }
    }
    
    0 讨论(0)
  • Even though in this solution the event is still propagated from the setter (so not exactly what the question is about), it provides a nice, more manageable way for representing dependencies. Someone might find it useful.

    The solution is to create a custom wrapper for triggering INotifyPropertyChanged events. Instead of calling OnPropertyChanged manually we can define following mathods (preferably inside a base class that we will reuse later):

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

    This class provides us with a way of setting a value of a given field without a need for providing the name of a proparty the call comes from.

    We also have to define a class that will enable use to define dependent properties (instance of this class is returned from SetPropertyValue mathod).

    public class ViewModelPropertyChange
    {
        private readonly ViewModelBase _viewModel;
    
        public ViewModelPropertyChange(ViewModelBase viewModel)
        {
            _viewModel = viewModel;
        }
    
        public ViewModelPropertyChange WithDependent(string name)
        {
            _viewModel.OnPropertyChanged(name);
    
            return this;
        }
    }
    

    It simply stores a reference to an object that is being changed and makes it possible to propagate an event to next properties.

    With this we can create a class derived from ViewModelBase like this:

    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;
        }
    }
    
    0 讨论(0)
提交回复
热议问题