c# - onpropertychanged - Implementando INotifyPropertyChanged-¿existe una mejor manera?
property change wpf (30)
A partir de .Net 4.5 hay finalmente una manera fácil de hacer esto.
.Net 4.5 introduce un nuevo atributo de información del llamante.
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 también un comparador a la función.
EqualityComparer<T>.Default.Equals
También vea la información de la persona que llama (C # y Visual Basic)
Microsoft debería haber implementado algo rápido para INotifyPropertyChanged
, como en las propiedades automáticas, simplemente especifique {get; set; notify;}
{get; set; notify;}
{get; set; notify;}
Creo que tiene mucho sentido hacerlo. ¿O hay alguna complicación para hacerlo?
¿Podemos nosotros mismos implementar algo como ''notificar'' en nuestras propiedades? ¿Existe una solución elegante para implementar INotifyPropertyChanged
en su clase o la única forma de hacerlo es elevar el evento PropertyChanged
en cada propiedad?
Si no, ¿podemos escribir algo para generar automáticamente el fragmento de código para generar el evento PropertyChanged
?
Aquí hay una versión de NotifyPropertyChanged de Unity3D o que no es CallerMemberName
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
}
Este código le permite escribir campos de respaldo de propiedad como este:
public string Text
{
get { return Get<string>(); }
set { Set(value); }
}
Además, en resharper, si creas un fragmento de patrón / búsqueda, también puedes automatizar tu flujo de trabajo al convertir campos de apoyo simples en el respaldo anterior.
Patrón de búsqueda:
public $type$ $fname$ { get; set; }
Reemplazar patrón:
public $type$ $fname$
{
get { return Get<$type$>(); }
set { Set(value); }
}
Basado en la respuesta de Thomas que fue adaptada de una respuesta de Marc, he convertido el código de cambio de propiedad reflejante en una clase base:
public abstract class PropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
protected void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
{
if (selectorExpression == null)
throw new ArgumentNullException("selectorExpression");
var me = selectorExpression.Body as MemberExpression;
// Nullable properties can be nested inside of a convert function
if (me == null)
{
var ue = selectorExpression.Body as UnaryExpression;
if (ue != null)
me = ue.Operand as MemberExpression;
}
if (me == null)
throw new ArgumentException("The body must be a member expression");
OnPropertyChanged(me.Member.Name);
}
protected void SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression, params Expression<Func<object>>[] additonal)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return;
field = value;
OnPropertyChanged(selectorExpression);
foreach (var item in additonal)
OnPropertyChanged(item);
}
}
El uso es el mismo que la respuesta de Thomas, excepto que puede pasar propiedades adicionales para notificar. Esto fue necesario para manejar columnas calculadas que deben actualizarse en una cuadrícula.
private int _quantity;
private int _price;
public int Quantity
{
get { return _quantity; }
set { SetField(ref _quantity, value, () => Quantity, () => Total); }
}
public int Price
{
get { return _price; }
set { SetField(ref _price, value, () => Price, () => Total); }
}
public int Total { get { return _price * _quantity; } }
Tengo esto conduciendo una colección de artículos almacenados en un BindingList expuesto a través de un DataGridView. Se ha eliminado la necesidad de que yo haga llamadas de actualización manual () a la cuadrícula.
Creé un método de extensión en mi biblioteca base para reutilizarlo:
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;
}
}
Esto funciona con .Net 4.5 debido a CallerMemberNameAttribute . Si desea usarlo con una versión anterior de .Net, debe cambiar la declaración del método de: ...,[CallerMemberName] string propertyName = "", ...
to ...,string propertyName, ...
Uso:
public class Dog : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
string _name;
public string Name
{
get { return _name; }
set
{
this.SetPropertyAndNotify(PropertyChanged, ref _name, value);
}
}
}
Creo que la gente debería prestar un poco más de atención al rendimiento, realmente afecta la interfaz de usuario cuando hay muchos objetos para vincular (piense en una cuadrícula con más de 10,000 filas) o si el valor del objeto cambia con frecuencia (aplicación de monitoreo en tiempo real) .
Tomé varias implementaciones encontradas aquí y en otros lugares e hice una comparación, verifíquelas en la comparación de rendimiento de las implementaciones de INotifyPropertyChanged .
Aquí hay un vistazo al resultado.
Introduzco una clase de Bindable en mi blog en http://timoch.com/blog/2013/08/annoyed-with-inotifypropertychange/ Bindable utiliza un diccionario como bolsa de propiedades. Es bastante fácil agregar las sobrecargas necesarias para que una subclase administre su propio campo de respaldo utilizando parámetros de referencia.
- Sin cuerda magica
- Sin reflejo
- Se puede mejorar para suprimir la búsqueda de diccionario predeterminada
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); }
}
}
Mantengo esto como un fragmento. C # 6 agrega una buena sintaxis para invocar el controlador.
// 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));
}
}
Está escrito en alemán, pero puede descargar ViewModelBase.cs. Todos los comentarios en el archivo cs están escritos en inglés.
Con esta clase ViewModelBase es posible implementar propiedades vinculables similares a las conocidas propiedades de dependencia:
public string SomeProperty
{
get { return GetValue( () => SomeProperty ); }
set { SetValue( () => SomeProperty, value ); }
}
Permítanme presentarles mi propio enfoque llamado Yappi . Pertenece a los generadores de clases derivadas de Runtime Proxy, que agregan nueva funcionalidad a un objeto o tipo existente, como el Proxy dinámico de Caste Project.
Permite implementar INotifyPropertyChanged una vez en la clase base, y luego declarar las clases derivadas en el siguiente estilo, aún siendo compatible con INotifyPropertyChanged para nuevas propiedades:
public class Animal:Concept
{
protected Animal(){}
public virtual string Name { get; set; }
public virtual int Age { get; set; }
}
La complejidad de la construcción derivada de clase o proxy se puede ocultar detrás de la siguiente línea:
var animal = Concept.Create<Animal>.New();
Y todo el trabajo de implementación de INotifyPropertyChanged se puede hacer así:
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);
};
}
}
}
Es totalmente seguro para refactorizar, no utiliza la reflexión después de la construcción de tipo y lo suficientemente rápido.
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 convertirlo en una expresión lambda:
private string name;
public string Name
{
get { return name; }
set { SetField(ref name, value, () => Name); }
}
Solo agrega 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 este blog actualizado URL
Sí, ciertamente existe mejor manera. Aquí está:
El tutorial paso a paso se redujo por mí, basado en este útil artículo .
- Crear nuevo proyecto
- Instalar el paquete del núcleo del castillo en el proyecto
Instalar el paquete Castle.Core
- Instalar solo bibliotecas de mvvm light
Install-Package MvvmLightLibs
- Agrega 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 los 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 así:
DataContext = ProxyCreator.MakeINotifyPropertyChanged<MainViewModel>();
- Disfrutar.
¡¡¡Atención!!! Todas las propiedades delimitadas se deben decorar con la palabra clave virtual porque se usaron por proxy de castillo para anular.
Si está utilizando dinámica en .NET 4.5, no necesita preocuparse por INotifyPropertyChanged
.
dynamic obj = new ExpandoObject();
obj.Name = "John";
Si el nombre está vinculado a algún control, simplemente funciona bien.
Sin usar algo como postsharp, la versión mínima que uso usa 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 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 clase base si lo desea. El retorno de bool
de SetField
te dice si fue un no-op, en caso de que quieras 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 lo cual el compilador agregará automáticamente el "Name"
.
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);
}
También está Fody que tiene un complemento PropertyChanged , que te permite escribir esto:
[ImplementPropertyChanged]
public class Person
{
public string GivenNames { get; set; }
public string FamilyName { get; set; }
}
... y en el momento de la compilación inyecta propiedades modificadas notificaciones.
Todas estas respuestas son muy bonitas.
Mi solución es usar los fragmentos de código para hacer el trabajo.
Esto utiliza la llamada más simple al evento PropertyChanged.
Guarde este fragmento de código y utilícelo cuando utilice el fragmento de código ''fullprop''.
la ubicación se puede encontrar en el menú ''Herramientas / Code Snippet Manager ...'' en 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>
Puede modificar la llamada como desee (para utilizar las soluciones anteriores)
Todavía no he tenido la oportunidad de probar esto, pero la próxima vez que esté configurando un proyecto con un gran requisito para INotifyPropertyChanged, pretendo escribir un atributo Postsharp 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 debe activar más de un OnPropertyChanged (si, por ejemplo, tuviera una propiedad FullName en la clase anterior)
Actualmente estoy usando una plantilla personalizada en Resharper, pero incluso con eso estoy harto de que todas mis propiedades sean tan largas.
Ah, una rápida búsqueda en Google (que debería haber hecho antes de escribir esto) muestra que al menos una persona ha hecho algo como esto antes here . No exactamente lo que tenía en mente, pero lo suficientemente cerca para demostrar que la teoría es buena.
Un enfoque muy similar a AOP es inyectar el material 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:
Utilizar esta
using System;
using System.ComponentModel;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
public static class ObservableFactory
{
public static T Create<T>(T target)
{
if (!typeof(T).IsInterface)
throw new ArgumentException("Target should be an interface", "target");
var proxy = new Observable<T>(target);
return (T)proxy.GetTransparentProxy();
}
}
internal class Observable<T> : RealProxy, INotifyPropertyChanged, INotifyPropertyChanging
{
private readonly T target;
internal Observable(T target)
: base(ImplementINotify(typeof(T)))
{
this.target = target;
}
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
if (methodCall != null)
{
return HandleMethodCall(methodCall);
}
return null;
}
public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;
IMessage HandleMethodCall(IMethodCallMessage methodCall)
{
var isPropertySetterCall = methodCall.MethodName.StartsWith("set_");
var propertyName = isPropertySetterCall ? methodCall.MethodName.Substring(4) : null;
if (isPropertySetterCall)
{
OnPropertyChanging(propertyName);
}
try
{
object methodCalltarget = target;
if (methodCall.MethodName == "add_PropertyChanged" || methodCall.MethodName == "remove_PropertyChanged"||
methodCall.MethodName == "add_PropertyChanging" || methodCall.MethodName == "remove_PropertyChanging")
{
methodCalltarget = this;
}
var result = methodCall.MethodBase.Invoke(methodCalltarget, methodCall.InArgs);
if (isPropertySetterCall)
{
OnPropertyChanged(methodCall.MethodName.Substring(4));
}
return new ReturnMessage(result, null, 0, methodCall.LogicalCallContext, methodCall);
}
catch (TargetInvocationException invocationException)
{
var exception = invocationException.InnerException;
return new ReturnMessage(exception, methodCall);
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanging(string propertyName)
{
var handler = PropertyChanging;
if (handler != null) handler(this, new PropertyChangingEventArgs(propertyName));
}
public static Type ImplementINotify(Type objectType)
{
var tempAssemblyName = new AssemblyName(Guid.NewGuid().ToString());
var dynamicAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
tempAssemblyName, AssemblyBuilderAccess.RunAndCollect);
var moduleBuilder = dynamicAssembly.DefineDynamicModule(
tempAssemblyName.Name,
tempAssemblyName + ".dll");
var typeBuilder = moduleBuilder.DefineType(
objectType.FullName, TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract);
typeBuilder.AddInterfaceImplementation(objectType);
typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanged));
typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanging));
var newType = typeBuilder.CreateType();
return newType;
}
}
}
Otra idea...
public class ViewModelBase : INotifyPropertyChanged
{
private Dictionary<string, object> _propertyStore = new Dictionary<string, object>();
protected virtual void SetValue<T>(T value, [CallerMemberName] string propertyName="") {
_propertyStore[propertyName] = value;
OnPropertyChanged(propertyName);
}
protected virtual T GetValue<T>([CallerMemberName] string propertyName = "")
{
object ret;
if (_propertyStore.TryGetValue(propertyName, out ret))
{
return (T)ret;
}
else
{
return default(T);
}
}
//Usage
//public string SomeProperty {
// get { return GetValue<string>(); }
// set { SetValue(value); }
//}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var temp = PropertyChanged;
if (temp != null)
temp.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Otra solución combinada es usar StackFrame:
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void Set<T>(ref T field, T value)
{
MethodBase method = new StackFrame(1).GetMethod();
field = value;
Raise(method.Name.Substring(4));
}
protected void Raise(string propertyName)
{
var temp = PropertyChanged;
if (temp != null)
{
temp(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Uso:
public class TempVM : BaseViewModel
{
private int _intP;
public int IntP
{
get { return _intP; }
set { Set<int>(ref _intP, value); }
}
}
Otras cosas que puede considerar al implementar este tipo de propiedades es el hecho de que INotifyPropertyChang * ed * ing utiliza clases de argumento de evento.
Si tiene una gran cantidad de propiedades que se están configurando, entonces el número de instancias de la clase de argumento de evento puede ser enorme, debe considerar el almacenamiento en caché ya que son una de las áreas en las que puede ocurrir una explosión de cadena.
Eche un vistazo a esta implementación y explicación de por qué fue concebida.
Resolví de esta manera (es un poco laborioso, pero seguramente es más rápido en tiempo de ejecución).
En VB (lo siento, pero creo que no es difícil traducirlo en C #), hago esta sustitución con RE:
(?<Attr><(.*ComponentModel/.)Bindable/(True/)>)( |/r/n)*(?<Def>(Public|Private|Friend|Protected) .*Property )(?<Name>[^ ]*) As (?<Type>.*?)[ |/r/n](?![ |/r/n]*Get)
con:
Private _${Name} As ${Type}/r/n${Attr}/r/n${Def}${Name} As ${Type}/r/nGet/r/nReturn _${Name}/r/nEnd Get/r/nSet (Value As ${Type})/r/nIf _${Name} <> Value Then /r/n_${Name} = Value/r/nRaiseEvent PropertyChanged(Me, New ComponentModel.PropertyChangedEventArgs("${Name}"))/r/nEnd If/r/nEnd Set/r/nEnd Property/r/n
Esta transofrm todo el código de esta manera:
<Bindable(True)>
Protected Friend Property StartDate As DateTime?
En
Private _StartDate As DateTime?
<Bindable(True)>
Protected Friend Property StartDate As DateTime?
Get
Return _StartDate
End Get
Set(Value As DateTime?)
If _StartDate <> Value Then
_StartDate = Value
RaiseEvent PropertyChange(Me, New ComponentModel.PropertyChangedEventArgs("StartDate"))
End If
End Set
End Property
Y si quiero tener un código más legible, puedo ser lo contrario haciendo la siguiente sustitución:
Private _(?<Name>.*) As (?<Type>.*)[/r/n ]*(?<Attr><(.*ComponentModel/.)Bindable/(True/)>)[/r/n ]*(?<Def>(Public|Private|Friend|Protected) .*Property )/k<Name> As /k<Type>[/r/n ]*Get[/r/n ]*Return _/k<Name>[/r/n ]*End Get[/r/n ]*Set/(Value As /k<Type>/)[/r/n ]*If _/k<Name> <> Value Then[/r/n ]*_/k<Name> = Value[/r/n ]*RaiseEvent PropertyChanged/(Me, New (.*ComponentModel/.)PropertyChangedEventArgs/("/k<Name>"/)/)[/r/n ]*End If[/r/n ]*End Set[/r/n ]*End Property
Con
${Attr} ${Def} ${Name} As ${Type}
Tiro para reemplazar el código IL del método establecido, pero no puedo escribir un montón de código compilado en IL ... ¡Si un día lo escribo, te lo diré!
Una idea utilizando la reflexión:
class ViewModelBase : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
bool Notify<T>(MethodBase mb, ref T oldValue, T newValue) {
// Get Name of Property
string name = mb.Name.Substring(4);
// Detect Change
bool changed = EqualityComparer<T>.Default.Equals(oldValue, newValue);
// Return if no change
if (!changed) return false;
// Update value
oldValue = newValue;
// Raise Event
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(name));
}//if
// Notify caller of change
return true;
}//method
string name;
public string Name {
get { return name; }
set {
Notify(MethodInfo.GetCurrentMethod(), ref this.name, value);
}
}//method
}//class
Uso el siguiente método de extensión (usando C # 6.0) para hacer que la implementación de INPC sea lo más fácil posible:
public static bool ChangeProperty<T>(this PropertyChangedEventHandler propertyChanged, ref T field, T value, object sender,
IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null)
{
if (comparer == null)
comparer = EqualityComparer<T>.Default;
if (comparer.Equals(field, value))
{
return false;
}
else
{
field = value;
propertyChanged?.Invoke(sender, new PropertyChangedEventArgs(propertyName));
return true;
}
}
La implementación de INPC se reduce a (puede implementar esto cada vez o crear una clase base):
public class INPCBaseClass: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool changeProperty<T>(ref T field, T value,
IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null)
{
return PropertyChanged.ChangeProperty(ref field, value, this, comparer, propertyName);
}
}
Luego escribe tus propiedades de esta manera:
private string testProperty;
public string TestProperty
{
get { return testProperty; }
set { changeProperty(ref testProperty, value); }
}
NOTA: Puede omitir la [CallerMemberName]
declaración en el método de extensión, si lo desea, pero quería mantenerla flexible.
Si tiene propiedades sin un campo de respaldo, puede sobrecargar changeProperty
:
protected bool changeProperty<T>(T property, Action<T> set, T value,
IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null)
{
bool ret = changeProperty(ref property, value, comparer, propertyName);
if (ret)
set(property);
return ret;
}
Un ejemplo de uso sería:
public string MyTestProperty
{
get { return base.TestProperty; }
set { changeProperty(base.TestProperty, (x) => { base.TestProperty = x; }, value); }
}
=> here mi solución con las siguientes características
public ResourceStatus Status
{
get { return _status; }
set
{
_status = value;
Notify(Npcea.Status,Npcea.Comments);
}
}
- no refelcción
- notación corta
- ninguna cadena mágica en tu código de negocio
- Reutilización de PropertyChangedEventArgs en toda la aplicación
- Posibilidad de notificar múltiples propiedades en una declaración
Acabo de encontrar ActiveSharp - Automatic INotifyPropertyChanged , todavía tengo que usarlo, pero se ve bien.
Para citar desde su sitio web ...
Enviar notificaciones de cambio de propiedad sin especificar el nombre de la propiedad como una cadena.
En su lugar, escribe propiedades como esta:
public int Foo
{
get { return _foo; }
set { SetValue(ref _foo, value); } // <-- no property name here
}
Tenga en cuenta que no es necesario incluir el nombre de la propiedad como una cadena. ActiveSharp lo determina de manera confiable y correcta por sí mismo. Funciona en función del hecho de que la implementación de su propiedad pasa el campo de respaldo (_foo) por ref. (ActiveSharp usa esa llamada "por referencia" para identificar qué campo de respaldo se pasó, y desde el campo identifica la propiedad).
He escrito un artículo que ayuda con esto ( https://msdn.microsoft.com/magazine/mt736453 ). Puede usar el paquete SolSoft.DataBinding NuGet. Entonces puedes escribir código como este:
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)
}
Beneficios:
- la clase base es opcional
- no hay reflexión sobre cada ''valor establecido''
- pueden tener propiedades que dependen de otras propiedades, y todas generan automáticamente los eventos apropiados (el artículo tiene un ejemplo de esto)
Me doy cuenta de que esta pregunta ya tiene un millón de respuestas, pero ninguna de ellas me pareció adecuada. Mi problema es que no quiero ningún éxito de rendimiento y estoy dispuesto a soportar un poco de verbosidad solo por esa razón. Tampoco me importan demasiado las propiedades automáticas, lo que me llevó a la siguiente solución:
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()
{
}
}
En otras palabras, la solución anterior es conveniente si no te importa hacer esto:
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
- Sin reflejo
- Solo notifica si valor antiguo! = Valor nuevo
- Notificar múltiples propiedades a la vez
Contras
- Sin propiedades automáticas (aunque puede agregar soporte para ambas)
- Algo de verbosidad
- Boxeo (pequeño éxito de rendimiento?)
Ay, todavía es mejor que hacer esto,
set
{
if (!someProperty.Equals(value))
{
someProperty = value;
OnPropertyChanged("SomeProperty");
OnPropertyChanged("AnotherProperty");
}
}
Por cada propiedad individual, que se convierte en una pesadilla con la verbosidad adicional ;-(
Tenga en cuenta que no pretendo que esta solución sea mejor en términos de rendimiento en comparación con las demás, solo que es una solución viable para aquellos a quienes no les gustan las otras soluciones presentadas.
Se me ocurrió esta clase base para implementar el patrón observable, hace prácticamente lo que necesitas ( "automáticamente" implementando el conjunto y obteniendo). Pasé una hora en línea como prototipo, por lo que no tiene muchas pruebas unitarias, pero prueba el concepto. Tenga en cuenta que utiliza Dictionary<string, ObservablePropertyContext>
para eliminar la necesidad de campos privados.
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();
}
Aquí está el uso
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); }
}
}
Sugiero usar la propiedad reactiva. Este es el método más corto, excepto Fody.
public class Data : INotifyPropertyChanged
{
// boiler-plate
...
// props
private string name;
public string Name
{
get { return name; }
set { SetField(ref name, value, "Name"); }
}
}
en lugar
public class Data
{
// Don''t need boiler-plate and INotifyPropertyChanged
// props
public ReactiveProperty<string> Name { get; } = new ReactiveProperty<string>();
}
( DOCS )