Implementing INotifyPropertyChanged - does a better way exist?

前端 未结 30 2473
感情败类
感情败类 2020-11-21 05:23

Microsoft should have implemented something snappy for INotifyPropertyChanged, like in the automatic properties, just specify {get; set; notify;} I

相关标签:
30条回答
  • 2020-11-21 05:40

    I realize this question already has a gazillion answers, but none of them felt quite right for me. My issue is I don't want any performance hits and am willing to put up with a little verbosity for that reason alone. I also don't care too much for auto properties either, which led me to the following solution:

    public abstract class AbstractObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
    
        public void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    
        protected virtual bool SetValue<TKind>(ref TKind Source, TKind NewValue, params string[] Notify)
        {
            //Set value if the new value is different from the old
            if (!Source.Equals(NewValue))
            {
                Source = NewValue;
    
                //Notify all applicable properties
                foreach (var i in Notify)
                    OnPropertyChanged(i);
    
                return true;
            }
    
            return false;
        }
    
        public AbstractObject()
        {
        }
    }
    

    In other words, the above solution is convenient if you don't mind doing this:

    public class SomeObject : AbstractObject
    {
        public string AnotherProperty
        {
            get
            {
                return someProperty ? "Car" : "Plane";
            }
        }
    
        bool someProperty = false;
        public bool SomeProperty
        {
            get
            {
                return someProperty;
            }
            set
            {
                SetValue(ref someProperty, value, "SomeProperty", "AnotherProperty");
            }
        }
    
        public SomeObject() : base()
        {
        }
    }
    

    Pros

    • No reflection
    • Only notifies if old value != new value
    • Notify multiple properties at once

    Cons

    • No auto properties (you can add support for both, though!)
    • Some verbosity
    • Boxing (small performance hit?)

    Alas, it is still better than doing this,

    set
    {
        if (!someProperty.Equals(value))
        {
            someProperty = value;
            OnPropertyChanged("SomeProperty");
            OnPropertyChanged("AnotherProperty");
        }
    }
    

    For every single property, which becomes a nightmare with the additional verbosity ;-(

    Note, I do not claim this solution is better performance-wise compared to the others, just that it is a viable solution for those who don't like the other solutions presented.

    0 讨论(0)
  • 2020-11-21 05:41

    I created an Extension Method in my base Library for reuse:

    public static class INotifyPropertyChangedExtensions
    {
        public static bool SetPropertyAndNotify<T>(this INotifyPropertyChanged sender,
                   PropertyChangedEventHandler handler, ref T field, T value, 
                   [CallerMemberName] string propertyName = "",
                   EqualityComparer<T> equalityComparer = null)
        {
            bool rtn = false;
            var eqComp = equalityComparer ?? EqualityComparer<T>.Default;
            if (!eqComp.Equals(field,value))
            {
                field = value;
                rtn = true;
                if (handler != null)
                {
                    var args = new PropertyChangedEventArgs(propertyName);
                    handler(sender, args);
                }
            }
            return rtn;
        }
    }
    

    This works with .Net 4.5 because of CallerMemberNameAttribute. If you want to use it with an earlier .Net version you have to change the method declaration from: ...,[CallerMemberName] string propertyName = "", ... to ...,string propertyName, ...

    Usage:

    public class Dog : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        string _name;
    
        public string Name
        {
            get { return _name; }
            set
            {
                this.SetPropertyAndNotify(PropertyChanged, ref _name, value);
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-21 05:43

    Without using something like postsharp, the minimal version I use uses something like:

    public class Data : INotifyPropertyChanged
    {
        // boiler-plate
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
        protected bool SetField<T>(ref T field, T value, string propertyName)
        {
            if (EqualityComparer<T>.Default.Equals(field, value)) return false;
            field = value;
            OnPropertyChanged(propertyName);
            return true;
        }
    
        // props
        private string name;
        public string Name
        {
            get { return name; }
            set { SetField(ref name, value, "Name"); }
        }
    }
    

    Each property is then just something like:

    private string name;
    public string Name
    {
        get { return name; }
        set { SetField(ref name, value, "Name"); }
    }
    

    which isn't huge; it can also be used as a base-class if you want. The bool return from SetField tells you if it was a no-op, in case you want to apply other logic.


    or even easier with C# 5:

    protected bool SetField<T>(ref T field, T value,
        [CallerMemberName] string propertyName = null)
    {...}
    

    which can be called like this:

    set { SetField(ref name, value); }
    

    with which the compiler will add the "Name" automatically.


    C# 6.0 makes the implementation easier:

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    

    ...and now with C#7:

    protected void OnPropertyChanged(string propertyName)
       => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    
    protected bool SetField<T>(ref T field, T value,[CallerMemberName] string propertyName =  null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
    
    private string name;
    public string Name
    {
        get => name;
        set => SetField(ref name, value);
    }
    

    And, with C# 8 and Nullable reference types, it would look like this:

    public event PropertyChangedEventHandler? PropertyChanged;
    
    protected void OnPropertyChanged(string? propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    
    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
    
    private string name;
    public string Name
    {
        get => name;
        set => SetField(ref name, value);
    }
    
    0 讨论(0)
  • 2020-11-21 05:44

    Yes, better way certainly exists. Here it is:

    Step by step tutorial shrank by me, based on this useful article.

    • Create new project
    • Install castle core package into the project

    Install-Package Castle.Core

    • Install mvvm light libraries only

    Install-Package MvvmLightLibs

    • Add two classes in project:

    NotifierInterceptor

    public class NotifierInterceptor : IInterceptor
        {
            private PropertyChangedEventHandler handler;
            public static Dictionary<String, PropertyChangedEventArgs> _cache =
              new Dictionary<string, PropertyChangedEventArgs>();
    
            public void Intercept(IInvocation invocation)
            {
                switch (invocation.Method.Name)
                {
                    case "add_PropertyChanged":
                        handler = (PropertyChangedEventHandler)
                                  Delegate.Combine(handler, (Delegate)invocation.Arguments[0]);
                        invocation.ReturnValue = handler;
                        break;
                    case "remove_PropertyChanged":
                        handler = (PropertyChangedEventHandler)
                                  Delegate.Remove(handler, (Delegate)invocation.Arguments[0]);
                        invocation.ReturnValue = handler;
                        break;
                    default:
                        if (invocation.Method.Name.StartsWith("set_"))
                        {
                            invocation.Proceed();
                            if (handler != null)
                            {
                                var arg = retrievePropertyChangedArg(invocation.Method.Name);
                                handler(invocation.Proxy, arg);
                            }
                        }
                        else invocation.Proceed();
                        break;
                }
            }
    
            private static PropertyChangedEventArgs retrievePropertyChangedArg(String methodName)
            {
                PropertyChangedEventArgs arg = null;
                _cache.TryGetValue(methodName, out arg);
                if (arg == null)
                {
                    arg = new PropertyChangedEventArgs(methodName.Substring(4));
                    _cache.Add(methodName, arg);
                }
                return arg;
            }
        }
    

    ProxyCreator

    public class ProxyCreator
    {
        public static T MakeINotifyPropertyChanged<T>() where T : class, new()
        {
            var proxyGen = new ProxyGenerator();
            var proxy = proxyGen.CreateClassProxy(
              typeof(T),
              new[] { typeof(INotifyPropertyChanged) },
              ProxyGenerationOptions.Default,
              new NotifierInterceptor()
              );
            return proxy as T;
        }
    }
    
    • Create your view model, for example:

    -

     public class MainViewModel
        {
            public virtual string MainTextBox { get; set; }
    
            public RelayCommand TestActionCommand
            {
                get { return new RelayCommand(TestAction); }
            }
    
            public void TestAction()
            {
                Trace.WriteLine(MainTextBox);
            }
        }
    
    • Put bindings into xaml:

      <TextBox Text="{Binding MainTextBox}" ></TextBox>
      <Button Command="{Binding TestActionCommand}" >Test</Button>
      
    • Put line of code in code-behind file MainWindow.xaml.cs like this:

    DataContext = ProxyCreator.MakeINotifyPropertyChanged<MainViewModel>();

    • Enjoy.

    enter image description here

    Attention!!! All bounded properties should be decorated with keyword virtual because they used by castle proxy for overriding.

    0 讨论(0)
  • 2020-11-21 05:46

    Look here : http://dotnet-forum.de/blogs/thearchitect/archive/2012/11/01/die-optimale-implementierung-des-inotifypropertychanged-interfaces.aspx

    It's written in German, but you can download the ViewModelBase.cs. All the comments in the cs-File are written in English.

    With this ViewModelBase-Class it is possible to implement bindable properties similar to the well known Dependency Properties :

    public string SomeProperty
    {
        get { return GetValue( () => SomeProperty ); }
        set { SetValue( () => SomeProperty, value ); }
    }
    
    0 讨论(0)
  • 2020-11-21 05:46

    I have written an article that helps with this (https://msdn.microsoft.com/magazine/mt736453). You can use the SolSoft.DataBinding NuGet package. Then you can write code like this:

    public class TestViewModel : IRaisePropertyChanged
    {
      public TestViewModel()
      {
        this.m_nameProperty = new NotifyProperty<string>(this, nameof(Name), null);
      }
    
      private readonly NotifyProperty<string> m_nameProperty;
      public string Name
      {
        get
        {
          return m_nameProperty.Value;
        }
        set
        {
          m_nameProperty.SetValue(value);
        }
      }
    
      // Plus implement IRaisePropertyChanged (or extend BaseViewModel)
    }
    

    Benefits:

    1. base class is optional
    2. no reflection on every 'set value'
    3. can have properties that depend on other properties, and they all automatically raise the appropriate events (article has an example of this)
    0 讨论(0)
提交回复
热议问题