Implementing INotifyPropertyChanged - does a better way exist?

前端 未结 30 2474
感情败类
感情败类 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:50

    I came up with this base class to implement the observable pattern, pretty much does what you need ("automatically" implementing the set and get). I spent line an hour on this as prototype, so it doesn't have many unit tests, but proves the concept. Note it uses the Dictionary<string, ObservablePropertyContext> to remove the need for private fields.

      public class ObservableByTracking<T> : IObservable<T>
      {
        private readonly Dictionary<string, ObservablePropertyContext> _expando;
        private bool _isDirty;
    
        public ObservableByTracking()
        {
          _expando = new Dictionary<string, ObservablePropertyContext>();
    
          var properties = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList();
          foreach (var property in properties)
          {
            var valueContext = new ObservablePropertyContext(property.Name, property.PropertyType)
            {
              Value = GetDefault(property.PropertyType)
            };
    
            _expando[BuildKey(valueContext)] = valueContext;
          }
        }
    
        protected void SetValue<T>(Expression<Func<T>> expression, T value)
        {
          var keyContext = GetKeyContext(expression);
          var key = BuildKey(keyContext.PropertyName, keyContext.PropertyType);
    
          if (!_expando.ContainsKey(key))
          {
            throw new Exception($"Object doesn't contain {keyContext.PropertyName} property.");
          }
    
          var originalValue = (T)_expando[key].Value;
          if (EqualityComparer<T>.Default.Equals(originalValue, value))
          {
            return;
          }
    
          _expando[key].Value = value;
          _isDirty = true;
        }
    
        protected T GetValue<T>(Expression<Func<T>> expression)
        {
          var keyContext = GetKeyContext(expression);
          var key = BuildKey(keyContext.PropertyName, keyContext.PropertyType);
    
          if (!_expando.ContainsKey(key))
          {
            throw new Exception($"Object doesn't contain {keyContext.PropertyName} property.");
          }
    
          var value = _expando[key].Value;
          return (T)value;
        }
    
        private KeyContext GetKeyContext<T>(Expression<Func<T>> expression)
        {
          var castedExpression = expression.Body as MemberExpression;
          if (castedExpression == null)
          {
            throw new Exception($"Invalid expression.");
          }
    
          var parameterName = castedExpression.Member.Name;
    
          var propertyInfo = castedExpression.Member as PropertyInfo;
          if (propertyInfo == null)
          {
            throw new Exception($"Invalid expression.");
          }
    
          return new KeyContext {PropertyType = propertyInfo.PropertyType, PropertyName = parameterName};
        }
    
        private static string BuildKey(ObservablePropertyContext observablePropertyContext)
        {
          return $"{observablePropertyContext.Type.Name}.{observablePropertyContext.Name}";
        }
    
        private static string BuildKey(string parameterName, Type type)
        {
          return $"{type.Name}.{parameterName}";
        }
    
        private static object GetDefault(Type type)
        {
          if (type.IsValueType)
          {
            return Activator.CreateInstance(type);
          }
          return null;
        }
    
        public bool IsDirty()
        {
          return _isDirty;
        }
    
        public void SetPristine()
        {
          _isDirty = false;
        }
    
        private class KeyContext
        {
          public string PropertyName { get; set; }
          public Type PropertyType { get; set; }
        }
      }
    
      public interface IObservable<T>
      {
        bool IsDirty();
        void SetPristine();
      }
    

    Here's the usage

    public class ObservableByTrackingTestClass : ObservableByTracking<ObservableByTrackingTestClass>
      {
        public ObservableByTrackingTestClass()
        {
          StringList = new List<string>();
          StringIList = new List<string>();
          NestedCollection = new List<ObservableByTrackingTestClass>();
        }
    
        public IEnumerable<string> StringList
        {
          get { return GetValue(() => StringList); }
          set { SetValue(() => StringIList, value); }
        }
    
        public IList<string> StringIList
        {
          get { return GetValue(() => StringIList); }
          set { SetValue(() => StringIList, value); }
        }
    
        public int IntProperty
        {
          get { return GetValue(() => IntProperty); }
          set { SetValue(() => IntProperty, value); }
        }
    
        public ObservableByTrackingTestClass NestedChild
        {
          get { return GetValue(() => NestedChild); }
          set { SetValue(() => NestedChild, value); }
        }
    
        public IList<ObservableByTrackingTestClass> NestedCollection
        {
          get { return GetValue(() => NestedCollection); }
          set { SetValue(() => NestedCollection, value); }
        }
    
        public string StringProperty
        {
          get { return GetValue(() => StringProperty); }
          set { SetValue(() => StringProperty, value); }
        }
      }
    
    0 讨论(0)
  • 2020-11-21 05:51

    Here is a Unity3D or non-CallerMemberName version of NotifyPropertyChanged

    public abstract class Bindable : MonoBehaviour, INotifyPropertyChanged
    {
        private readonly Dictionary<string, object> _properties = new Dictionary<string, object>();
        private static readonly StackTrace stackTrace = new StackTrace();
        public event PropertyChangedEventHandler PropertyChanged;
    
        /// <summary>
        ///     Resolves a Property's name from a Lambda Expression passed in.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="property"></param>
        /// <returns></returns>
        internal string GetPropertyName<T>(Expression<Func<T>> property)
        {
            var expression = (MemberExpression) property.Body;
            var propertyName = expression.Member.Name;
    
            Debug.AssertFormat(propertyName != null, "Bindable Property shouldn't be null!");
            return propertyName;
        }
    
        #region Notification Handlers
    
        /// <summary>
        ///     Notify's all other objects listening that a value has changed for nominated propertyName
        /// </summary>
        /// <param name="propertyName"></param>
        internal void NotifyOfPropertyChange(string propertyName)
        {
            OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
        }
    
        /// <summary>
        ///     Notifies subscribers of the property change.
        /// </summary>
        /// <typeparam name="TProperty">The type of the property.</typeparam>
        /// <param name="property">The property expression.</param>
        internal void NotifyOfPropertyChange<TProperty>(Expression<Func<TProperty>> property)
        {
            var propertyName = GetPropertyName(property);
            NotifyOfPropertyChange(propertyName);
        }
    
        /// <summary>
        ///     Raises the <see cref="PropertyChanged" /> event directly.
        /// </summary>
        /// <param name="e">The <see cref="PropertyChangedEventArgs" /> instance containing the event data.</param>
        internal void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            var handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, e);
            }
        }
    
        #endregion
    
        #region Getters
    
        /// <summary>
        ///     Gets the value of a property
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name"></param>
        /// <returns></returns>
        internal T Get<T>(Expression<Func<T>> property)
        {
            var propertyName = GetPropertyName(property);
            return Get<T>(GetPropertyName(property));
        }
    
        /// <summary>
        ///     Gets the value of a property automatically based on its caller.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        internal T Get<T>()
        {
            var name = stackTrace.GetFrame(1).GetMethod().Name.Substring(4); // strips the set_ from name;
            return Get<T>(name);
        }
    
        /// <summary>
        ///     Gets the name of a property based on a string.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name"></param>
        /// <returns></returns>
        internal T Get<T>(string name)
        {
            object value = null;
            if (_properties.TryGetValue(name, out value))
                return value == null ? default(T) : (T) value;
            return default(T);
        }
    
        #endregion
    
        #region Setters
    
        /// <summary>
        ///     Sets the value of a property whilst automatically looking up its caller name.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value"></param>
        internal void Set<T>(T value)
        {
            var propertyName = stackTrace.GetFrame(1).GetMethod().Name.Substring(4); // strips the set_ from name;
            Set(value, propertyName);
        }
    
        /// <summary>
        ///     Sets the value of a property
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value"></param>
        /// <param name="name"></param>
        internal void Set<T>(T value, string propertyName)
        {
            Debug.Assert(propertyName != null, "name != null");
            if (Equals(value, Get<T>(propertyName)))
                return;
            _properties[propertyName] = value;
            NotifyOfPropertyChange(propertyName);
        }
    
        /// <summary>
        ///     Sets the value of a property based off an Expression (()=>FieldName)
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value"></param>
        /// <param name="property"></param>
        internal void Set<T>(T value, Expression<Func<T>> property)
        {
            var propertyName = GetPropertyName(property);
    
            Debug.Assert(propertyName != null, "name != null");
    
            if (Equals(value, Get<T>(propertyName)))
                return;
            _properties[propertyName] = value;
            NotifyOfPropertyChange(propertyName);
        }
    
        #endregion
    }
    

    This code enables you to write property backing fields like this:

      public string Text
        {
            get { return Get<string>(); }
            set { Set(value); }
        }
    

    Furthermore, in resharper if you create a pattern/search snippet you can then also automate you're workflow by converting simple prop fields into the above backing.

    Search Pattern:

    public $type$ $fname$ { get; set; }
    

    Replace Pattern:

    public $type$ $fname$
    {
        get { return Get<$type$>(); }
        set { Set(value); }
    }
    
    0 讨论(0)
  • 2020-11-21 05:52

    I think people should pay a little more attention to performance; it really does impact the UI when there are a lot of objects to be bound (think of a grid with 10,000+ rows), or if the object's value changes frequently (real-time monitoring app).

    I took various implementation found here and elsewhere and did a comparison; check it out perfomance comparison of INotifyPropertyChanged implementations.


    Here is a peek at the result Implemenation vs Runtime

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

    Let me introduce my own approach called Yappi. It belongs to Runtime proxy|derived class generators, adding new functionality to an existing object or type, like Caste Project's Dynamic Proxy.

    It allows to implement INotifyPropertyChanged once in base class, and then declare derived classes in following style, still supporting INotifyPropertyChanged for new properties:

    public class Animal:Concept
    {
        protected Animal(){}
        public virtual string Name { get; set; }
        public virtual int Age { get; set; }
    }
    

    Complexity of derived class or proxy construction can be hidden behind the following line:

    var animal = Concept.Create<Animal>.New();
    

    And all INotifyPropertyChanged implementation work can be done like this:

    public class Concept:INotifyPropertyChanged
    {
        //Hide constructor
        protected Concept(){}
    
        public static class Create<TConcept> where TConcept:Concept
        {
            //Construct derived Type calling PropertyProxy.ConstructType
            public static readonly Type Type = PropertyProxy.ConstructType<TConcept, Implementation<TConcept>>(new Type[0], true);
            //Create constructing delegate calling Constructor.Compile
            public static Func<TConcept> New = Constructor.Compile<Func<TConcept>>(Type);
        }
    
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        protected void OnPropertyChanged(PropertyChangedEventArgs eventArgs)
        {
            var caller = PropertyChanged;
            if(caller!=null)
            {
                caller(this, eventArgs);
            }
        }
    
        //define implementation
        public class Implementation<TConcept> : DefaultImplementation<TConcept> where TConcept:Concept
        {
            public override Func<TBaseType, TResult> OverrideGetter<TBaseType, TDeclaringType, TConstructedType, TResult>(PropertyInfo property)
            {
                return PropertyImplementation<TBaseType, TDeclaringType>.GetGetter<TResult>(property.Name);
            }
            /// <summary>
            /// Overriding property setter implementation.
            /// </summary>
            /// <typeparam name="TBaseType">Base type for implementation. TBaseType must be TConcept, and inherits all its constraints. Also TBaseType is TDeclaringType.</typeparam>
            /// <typeparam name="TDeclaringType">Type, declaring property.</typeparam>
            /// <typeparam name="TConstructedType">Constructed type. TConstructedType is TDeclaringType and TBaseType.</typeparam>
            /// <typeparam name="TResult">Type of property.</typeparam>
            /// <param name="property">PropertyInfo of property.</param>
            /// <returns>Delegate, corresponding to property setter implementation.</returns>
            public override Action<TBaseType, TResult> OverrideSetter<TBaseType, TDeclaringType, TConstructedType, TResult>(PropertyInfo property)
            {
                //This code called once for each declared property on derived type's initialization.
                //EventArgs instance is shared between all events for each concrete property.
                var eventArgs = new PropertyChangedEventArgs(property.Name);
                //get delegates for base calls.
                Action<TBaseType, TResult> setter = PropertyImplementation<TBaseType, TDeclaringType>.GetSetter<TResult>(property.Name);
                Func<TBaseType, TResult> getter = PropertyImplementation<TBaseType, TDeclaringType>.GetGetter<TResult>(property.Name);
    
                var comparer = EqualityComparer<TResult>.Default;
    
                return (pthis, value) =>
                {//This code executes each time property setter is called.
                    if (comparer.Equals(value, getter(pthis))) return;
                    //base. call
                    setter(pthis, value);
                    //Directly accessing Concept's protected method.
                    pthis.OnPropertyChanged(eventArgs);
                };
            }
        }
    }
    

    It is fully safe for refactoring, uses no reflection after type construction and fast enough.

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

    All these answer are very nice.

    My solution is using the code snippets to do the job.

    This uses the simplest call to PropertyChanged event.

    Save this snippet and use it as you use 'fullprop' snippet.

    the location can be found at 'Tools\Code Snippet Manager...' menu at Visual Studio.

    <?xml version="1.0" encoding="utf-8" ?>
    <CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
        <CodeSnippet Format="1.0.0">
            <Header>
                <Title>inotifypropfull</Title>
                <Shortcut>inotifypropfull</Shortcut>
                <HelpUrl>http://ofirzeitoun.wordpress.com/</HelpUrl>
                <Description>Code snippet for property and backing field with notification</Description>
                <Author>Ofir Zeitoun</Author>
                <SnippetTypes>
                    <SnippetType>Expansion</SnippetType>
                </SnippetTypes>
            </Header>
            <Snippet>
                <Declarations>
                    <Literal>
                        <ID>type</ID>
                        <ToolTip>Property type</ToolTip>
                        <Default>int</Default>
                    </Literal>
                    <Literal>
                        <ID>property</ID>
                        <ToolTip>Property name</ToolTip>
                        <Default>MyProperty</Default>
                    </Literal>
                    <Literal>
                        <ID>field</ID>
                        <ToolTip>The variable backing this property</ToolTip>
                        <Default>myVar</Default>
                    </Literal>
                </Declarations>
                <Code Language="csharp">
                    <![CDATA[private $type$ $field$;
    
        public $type$ $property$
        {
            get { return $field$;}
            set { 
                $field$ = value;
                var temp = PropertyChanged;
                if (temp != null)
                {
                    temp(this, new PropertyChangedEventArgs("$property$"));
                }
            }
        }
        $end$]]>
                </Code>
            </Snippet>
        </CodeSnippet>
    </CodeSnippets>
    

    You can modify the call as you like (to use the above solutions)

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

    I keep this around as a snippet. C# 6 adds some nice syntax for invoking the handler.

    // INotifyPropertyChanged
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    private void Set<T>(ref T property, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(property, value) == false)
        {
            property = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    
    0 讨论(0)
提交回复
热议问题