Pregunta Implementando INotifyPropertyChanged - ¿existe una mejor manera?


Microsoft debería haber implementado algo ágil para INotifyPropertyChanged, como en las propiedades automáticas, solo especifique {get; set; notify;} Creo que tiene mucho sentido hacerlo. ¿O hay complicaciones para hacerlo?

¿Podemos nosotros mismos implementar algo como 'notificar' en nuestras propiedades? ¿Hay una solución elegante para implementar INotifyPropertyChanged en su clase o la única manera de hacerlo es elevando el PropertyChanged evento en cada propiedad.

Si no, podemos escribir algo para generar automáticamente la pieza de código para aumentar PropertyChanged  ¿evento?


555


origen


Respuestas:


Sin usar algo como postsharp, la versión mínima que uso utiliza algo como:

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

Cada propiedad es, entonces, algo así como:

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

que no es enorme; también puede usarse como una clase base si lo desea. los bool regresar de SetField le dice si fue un no-op, en caso de que quiera aplicar otra lógica.


o incluso más fácil con C # 5:

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

que se puede llamar así:

set { SetField(ref name, value); }

con el cual el compilador agregará "Name" automáticamente.


C # 6.0 facilita la implementación:

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

... y ahora con C # 7:

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

513



A partir de .Net 4.5 finalmente hay una manera fácil de hacer esto.

.Net 4.5 presenta un nuevo atributo de información de llamada.

private void OnPropertyChanged<T>([CallerMemberName]string caller = null) {
     // make sure only to call this if the value actually changes

     var handler = PropertyChanged;
     if (handler != null) {
        handler(this, new PropertyChangedEventArgs(caller));
     }
}

Probablemente sea una buena idea agregar un comparador a la función también.

EqualityComparer<T>.Default.Equals

Más ejemplos aquí y aquí

Ver también Información de llamada (C # y Visual Basic)


188



Realmente me gusta la solución de Marc, pero creo que se puede mejorar ligeramente para evitar el uso de una "cadena mágica" (que no admite la refactorización). En lugar de usar el nombre de la propiedad como una cadena, es fácil hacer que sea una expresión lambda:

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

Simplemente agregue los siguientes métodos al código de Marc, hará el truco:

protected virtual void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
{
    if (selectorExpression == null)
        throw new ArgumentNullException("selectorExpression");
    MemberExpression body = selectorExpression.Body as MemberExpression;
    if (body == null)
        throw new ArgumentException("The body must be a member expression");
    OnPropertyChanged(body.Member.Name);
}

protected bool SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression)
{
    if (EqualityComparer<T>.Default.Equals(field, value)) return false;
    field = value;
    OnPropertyChanged(selectorExpression);
    return true;
}

Por cierto, esto fue inspirado por esta publicación en el blog  URL actualizada


160



También hay Fody que tiene un PropertyChanged complemento, que te permite escribir esto:

[ImplementPropertyChanged]
public class Person 
{        
    public string GivenNames { get; set; }
    public string FamilyName { get; set; }
}

... y en tiempo de compilación inyecta notificaciones de propiedad cambiadas.


102



Creo que la gente debería prestar más atención al rendimiento, realmente impacta en la UI cuando hay muchos objetos para enlazar (piense en una cuadrícula con más de 10.000 filas) o si el valor del objeto cambia frecuentemente (aplicación de monitoreo en tiempo real) .

Tomé varias implementaciones encontradas aquí y en otras partes e hice una comparación, compruébalo comparación de rendimiento de implementaciones INotifyPropertyChanged.


Aquí hay un vistazo al resultado Implemenation vs Runtime


60



Presento una clase de Bindable en mi blog en http://timoch.com/blog/2013/08/annoyed-with-inotifypropertychange/ Bindable usa un diccionario como bolsa de propiedades. Es bastante fácil agregar las sobrecargas necesarias para que una subclase administre su propio campo de respaldo usando los parámetros de ref.

  • Sin cadena mágica
  • Sin reflejo
  • Se puede mejorar para suprimir la búsqueda del diccionario por defecto

El código:

public class Bindable : INotifyPropertyChanged {
    private Dictionary<string, object> _properties = new Dictionary<string, object>();

    /// <summary>
    /// Gets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="name"></param>
    /// <returns></returns>
    protected T Get<T>([CallerMemberName] string name = null) {
        Debug.Assert(name != null, "name != null");
        object value = null;
        if (_properties.TryGetValue(name, out value))
            return value == null ? default(T) : (T)value;
        return default(T);
    }

    /// <summary>
    /// Sets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <param name="name"></param>
    /// <remarks>Use this overload when implicitly naming the property</remarks>
    protected void Set<T>(T value, [CallerMemberName] string name = null) {
        Debug.Assert(name != null, "name != null");
        if (Equals(value, Get<T>(name)))
            return;
        _properties[name] = value;
        OnPropertyChanged(name);
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Se puede usar así:

public class Contact : Bindable {
    public string FirstName {
        get { return Get<string>(); }
        set { Set(value); }
    }
}

31



Todavía no he tenido la oportunidad de probar esto, pero la próxima vez que estoy configurando un proyecto con un gran requisito para INotifyPropertyChanged, tengo la intención de escribir un Postsharp atributo que inyectará el código en tiempo de compilación. Algo como:

[NotifiesChange]
public string FirstName { get; set; }

Se convertirá:

private string _firstName;

public string FirstName
{
   get { return _firstname; }
   set
   {
      if (_firstname != value)
      {
          _firstname = value;
          OnPropertyChanged("FirstName")
      }
   }
}

No estoy seguro de si esto funcionará en la práctica y necesito sentarme y probarlo, pero no veo por qué no. Es posible que deba hacer que acepte algunos parámetros para situaciones en las que se necesita activar más de un OnPropertyChanged (si, por ejemplo, tengo una propiedad FullName en la clase anterior)

Actualmente estoy usando una plantilla personalizada en Resharper, pero incluso con eso me estoy hartando de que todas mis propiedades sean tan largas.


Ah, una búsqueda rápida en Google (que debería haber hecho antes de escribir esto) muestra que al menos una persona ha hecho algo como esto antes aquí. No es exactamente lo que tenía en mente, pero lo suficientemente cerca como para mostrar que la teoría es buena.


15



Sí, mejor manera existe ciertamente. Aquí está:

Tutorial paso a paso se redujo por mí, basado en esto artículo útil.

  • Crear nuevo proyecto
  • Instalar el paquete de Castle Core en el proyecto

Install-Package Castle.Core

  • Instale mvvm light libraries solamente

Install-Package MvvmLightLibs

  • Agregue dos clases en el proyecto:

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;
    }
}
  • Crea tu modelo de vista, por ejemplo:

-

 public class MainViewModel
    {
        public virtual string MainTextBox { get; set; }

        public RelayCommand TestActionCommand
        {
            get { return new RelayCommand(TestAction); }
        }

        public void TestAction()
        {
            Trace.WriteLine(MainTextBox);
        }
    }
  • Poner enlaces en xaml:

    <TextBox Text="{Binding MainTextBox}" ></TextBox>
    <Button Command="{Binding TestActionCommand}" >Test</Button>
    
  • Coloque la línea de código en el archivo de código subyacente MainWindow.xaml.cs de la siguiente manera:

DataContext = ProxyCreator.MakeINotifyPropertyChanged<MainViewModel>();

  • Disfrutar.

enter image description here

¡¡¡Atención!!! Todas las propiedades limitadas deben estar decoradas con palabra clave virtual porque usaron el proxy del castillo para sobrescribir.


9



Un enfoque muy similar a AOP es inyectar el elemento INotifyPropertyChanged en un objeto ya instanciado sobre la marcha. Puedes hacer esto con algo como Castle DynamicProxy. Aquí hay un artículo que explica la técnica:

Agregar INotifyPropertyChanged a un objeto existente


6



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

Está escrito en alemán, pero puedes descargar ViewModelBase.cs. Todos los comentarios en el archivo cs están escritos en inglés.

Con esta ViewModelBase-Class es posible implementar propiedades enlazables similares a las bien conocidas Propiedades de dependencia:

public string SomeProperty
{
    get { return GetValue( () => SomeProperty ); }
    set { SetValue( () => SomeProperty, value ); }
}

5