.net - troficos - redes alimenticias
Cómo subir evento PropertyChanged sin usar el nombre de cadena (8)
Sería bueno tener la capacidad de generar el evento ''PropertyChanged'' sin especificar explícitamente el nombre de la propiedad modificada. Me gustaría hacer algo como esto:
public string MyString
{
get { return _myString; }
set
{
ChangePropertyAndNotify<string>(val=>_myString=val, value);
}
}
private void ChangePropertyAndNotify<T>(Action<T> setter, T value)
{
setter(value);
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(setter.Method.Name));
}
}
En este caso, el nombre recibido es un nombre del método lambda: "<set_MyString> b__0".
- ¿Puedo estar seguro de que el recorte "<set_" y "> b__0" siempre proporcionará el nombre de propiedad correcto?
- ¿Hay alguna otra notificación sobre la propiedad cambiada (de la propiedad en sí)?
Gracias.
En el siguiente ejemplo, debe pasar 3 valores (campo de respaldo, nuevo valor, propiedad como lambda) pero no hay cadenas mágicas y el evento de propiedad cambiada solo se genera cuando realmente no es igual.
class Sample : INotifyPropertyChanged
{
private string _name;
public string Name
{
get { return _name; }
set { this.SetProperty(ref _name, value, () => this.Name); }
}
protected void SetProperty<T>(ref T backingField, T newValue, Expression<Func<T>> propertyExpression)
{
if (backingField == null && newValue == null)
{
return;
}
if (backingField == null || !backingField.Equals(newValue))
{
backingField = newValue;
this.OnPropertyChanged(propertyExpression);
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyExpression.GetPropertyName()));
}
}
}
Y el siguiente código contiene métodos de extensión para obtener un nombre de propiedad de una expresión lambda.
public static class Extensions
{
public static string GetPropertyName<TProperty>(this Expression<Func<TProperty>> propertyExpression)
{
return propertyExpression.Body.GetMemberExpression().GetPropertyName();
}
public static string GetPropertyName(this MemberExpression memberExpression)
{
if (memberExpression == null)
{
return null;
}
if (memberExpression.Member.MemberType != MemberTypes.Property)
{
return null;
}
var child = memberExpression.Member.Name;
var parent = GetPropertyName(memberExpression.Expression.GetMemberExpression());
if (parent == null)
{
return child;
}
else
{
return parent + "." + child;
}
}
public static MemberExpression GetMemberExpression(this Expression expression)
{
var memberExpression = expression as MemberExpression;
if (memberExpression != null)
{
return memberExpression;
}
var unaryExpression = expression as UnaryExpression;
if (unaryExpression != null)
{
memberExpression = (MemberExpression)unaryExpression.Operand;
if (memberExpression != null)
{
return memberExpression;
}
}
return null;
}
public static void ShouldEqual<T>(this T actual, T expected, string name)
{
if (!Object.Equals(actual, expected))
{
throw new Exception(String.Format("{0}: Expected <{1}> Actual <{2}>.", name, expected, actual));
}
}
}
Finalmente un código de prueba:
class q3191536
{
public static void Test()
{
var sample = new Sample();
var propertyChanged = 0;
sample.PropertyChanged +=
new PropertyChangedEventHandler((sender, e) =>
{
if (e.PropertyName == "Name")
{
propertyChanged += 1;
}
}
);
sample.Name = "Budda";
sample.Name.ShouldEqual("Budda", "sample.Name");
propertyChanged.ShouldEqual(1, "propertyChanged");
sample.Name = "Tim";
sample.Name.ShouldEqual("Tim", sample.Name);
propertyChanged.ShouldEqual(2, "propertyChanged");
sample.Name = "Tim";
sample.Name.ShouldEqual("Tim", sample.Name);
propertyChanged.ShouldEqual(2, "propertyChanged");
}
}
Esta es la forma en que lo encontré para hacerlo:
public abstract class ViewModel<T> : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public void RaisePropertyChanged(Expression<Func<T, object>> expression)
{
var propertyName = GetPropertyFromExpression(expression);
this.OnPropertyChanged(propertyName);
}
public string GetPropertyFromExpression(System.Linq.Expressions.Expression expression)
{
if (expression == null)
throw new ArgumentException("Getting property name form expression is not supported for this type.");
var lamda = expression as LambdaExpression;
if (lamda == null)
throw new NotSupportedException("Getting property name form expression is not supported for this type.");
var mbe = lamda.Body as MemberExpression;
if (mbe != null)
return mbe.Member.Name;
var unary = lamda.Body as UnaryExpression;
if (unary != null)
{
var member = unary.Operand as MemberExpression;
if (member != null)
return member.Member.Name;
}
throw new NotSupportedException("Getting property name form expression is not supported for this type.");
}
}
Estoy usando el método de extensión
public static class ExpressionExtensions {
public static string PropertyName<TProperty>(this Expression<Func<TProperty>> projection) {
var memberExpression = (MemberExpression)projection.Body;
return memberExpression.Member.Name;
}
}
en combinación con el siguiente método. El método se define en la clase que implementa la interfaz INotifyPropertyChanged (normalmente una clase base de la cual se derivan mis otras clases).
protected void OnPropertyChanged<TProperty>(Expression<Func<TProperty>> projection) {
var e = new PropertyChangedEventArgs(projection.PropertyName());
OnPropertyChanged(e);
}
Entonces puedo subir el PropertyChanged-Event de la siguiente manera
private double _rate;
public double Rate {
get {
return _rate;
}
set {
if (_rate != value) {
_rate = value;
OnPropertyChanged(() => Rate );
}
}
}
Con este enfoque, es fácil cambiar el nombre de las Propiedades (en Visual Studio), ya que garantiza que la llamada PropertyChanged correspondiente también se actualice.
Hay varios enfoques para hacer esto sin usar un nombre de propiedad.
Lo mejor es simplemente leer los blogs.
http://justinangel.net/AutomagicallyImplementingINotifyPropertyChanged
Las soluciones ya publicadas tienen una combinación de dos problemas:
1) Algunos requieren que crees una clase base y heredes de ella. Este es un gran problema que puede lanzar una llave en la cadena de herencia de tus clases y hacerte comenzar a rediseñar tu dominio solo para permitir un desarrollo "extra" como este.
2) Si bien las soluciones existentes le permiten designar a qué propiedad disparar el evento cambiado mediante una expresión lambda, aún registran y distribuyen una representación de cadena del nombre de la propiedad porque se basan en la clase PropertyChangedEventArgs existente. Por lo tanto, cualquier código que realmente use su evento PropertyChanged tiene que hacer una comparación de cadenas que de nuevo interrumpe cualquier refactorización automática que necesite hacer en el futuro sin mencionar que su soporte de tiempo de compilación está fuera de la ventana, que es uno de los principales puntos que permite expresiones lambda en lugar de cadenas en primer lugar.
Esta es mi versión de genéricos que sigue el mismo patrón de evento / delegado iniciado por MS, lo que significa que no se necesitan clases base ni métodos de extensión.
public class PropertyChangedEventArgs<TObject> : EventArgs
{
private readonly MemberInfo _property;
public PropertyChangedEventArgs(Expression<Func<TObject, object>> expression)
{
_property = GetPropertyMember(expression);
}
private MemberInfo GetPropertyMember(LambdaExpression p)
{
MemberExpression memberExpression;
if (p.Body is UnaryExpression)
{
UnaryExpression ue = (UnaryExpression)p.Body;
memberExpression = (MemberExpression)ue.Operand;
}
else
{
memberExpression = (MemberExpression)p.Body;
}
return (PropertyInfo)(memberExpression).Member;
}
public virtual bool HasChanged(Expression<Func<TObject, object>> expression)
{
if (GetPropertyMember(expression) == Property)
return true;
return false;
}
public virtual MemberInfo Property
{
get
{
return _property;
}
}
}
public delegate void PropertyChangedEventHandler<TObject>(object sender, PropertyChangedEventArgs<TObject> e);
public interface INotifyPropertyChanged<TObject>
{
event PropertyChangedEventHandler<TObject> PropertyChanged;
}
Ahora puedes usarlo en una clase como esta:
public class PagedProduct : INotifyPropertyChanged<PagedProduct>
{
IPager _pager;
public event PropertyChangedEventHandler<PagedProduct> PropertyChanged = delegate { };
public PagedProduct() { }
public IPager Pager
{
get { return _pager; }
set
{
if (value != _pager)
{
_pager = value;
// let everyone know this property has changed.
PropertyChanged(this, new PropertyChangedEventArgs<PagedProduct>(a => a.Pager));
}
}
}
}
¡Y finalmente puedes escuchar los eventos en ese objeto y determinar qué propiedad cambió usando una expresión lambda también!
void SomeMethod()
{
PagedProduct pagedProducts = new PagedProduct();
pagedProducts.PropertyChanged += pagedProducts_PropertyChanged;
}
void pagedProducts_PropertyChanged(object sender, PropertyChangedEventArgs<PagedProduct> e)
{
// lambda expression is used to determine if the property we are interested in has changed. no strings here
if (e.HasChanged(a => a.Pager))
{
// do something mind blowing like ordering pizza with a coupon
}
}
Utilizo un método de extensión simple para obtener el nombre de la propiedad para evitar problemas con las cadenas mágicas. También mantiene la legibilidad del código, es decir, es explícito lo que está sucediendo.
El método de extensión es simplemente el siguiente:
public static string GetPropertyName(this MethodBase methodBase)
{
return methodBase.Name.Substring(4);
}
Con esto, significa que los conjuntos de propiedades son resistentes frente a los cambios de nombre y se parecen a los siguientes:
private string _name;
public string Name
{
get { return _name; }
set
{
name = value;
RaisePropertyChanged(MethodBase.GetCurrentMethod().GetPropertyName());
}
}
Aquí he escrito más sobre este método de extensión y he publicado un fragmento de código coincidente aquí .
Se agregó C # 6 Respuesta
En C # 6 (y cualquier versión de VB viene con Visual Studio 2015) tenemos el nameof
operador que hace las cosas más fáciles que nunca. En mi respuesta original a continuación, utilizo una función C # 5 (atributos de información de llamadas) para manejar el caso común de notificaciones de "cambio automático". El operador nameof
se puede usar en todos los casos, y es especialmente útil en el escenario de notificación "related-property-changed".
Para simplificar, creo que mantendré el enfoque de atributo de información de llamante para notificaciones comunes autocambiadas. Menos tipeo significa menos posibilidades de errores tipográficos y de copiar / pegar errores inducidos ... el compilador aquí asegura que elijas un tipo / miembro / variable válido, pero no garantiza que elijas el correcto. Es simple usar el nuevo nameof
operador para las notificaciones de cambio de propiedad relacionada. El ejemplo siguiente muestra un comportamiento clave de los atributos de información de la persona que llama ... el atributo no tiene efecto en un parámetro si la persona que llama especifica el parámetro (es decir, la información de la persona que llama se proporciona para el valor del parámetro solo cuando el parámetro se omite llamador).
También vale la pena observar que el nameof
operador también puede ser utilizado por los manejadores de evento PropertyChanged. Ahora puede comparar el valor PropertyName
en el evento (que es una string
) con una propiedad particular utilizando el operador nameof
, eliminando más cadenas mágicas.
Información de referencia para el nameof
aquí: https://msdn.microsoft.com/en-us/library/dn986596.aspx
Ejemplo:
public class Program
{
void Main()
{
var dm = new DataModel();
dm.PropertyChanged += propertyChangedHandler;
}
void propertyChangedHandler(object sender, PropertyChangedEventArgs args)
{
if (args.PropertyName == nameof(DataModel.NumberSquared))
{
//do something spectacular
}
}
}
public class DataModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
}
public class DataModel : DataModelBase
{
//a simple property
string _something;
public string Something
{
get { return _something; }
set { _something = value; OnPropertyChanged(); }
}
//a property with another related property
int _number;
public int Number
{
get { return _number; }
set
{
_number = value;
OnPropertyChanged();
OnPropertyChanged(nameof(this.NumberSquared));
}
}
//a related property
public int NumberSquared { get { return Number * Number; } }
}
Original C # 5 respuesta
Desde C # 5, es mejor utilizar los atributos de información de la persona que llama, esto se resuelve en tiempo de compilación, no es necesario reflexionar.
Implemento esto en una clase base, las clases derivadas solo llaman al método OnPropertyChanged
desde sus OnPropertyChanged
de propiedades. Si alguna propiedad cambia implícitamente otro valor, también puedo usar la versión "Explícita" del método en el organizador de propiedades, que ya no es "seguro", pero es una situación rara que acabo de aceptar.
Alternativamente, podría usar este método para notificaciones de auto cambio y usar la respuesta dada por @Jehof para notificaciones de cambio de propiedad relacionadas ... esto tendría la ventaja de no tener cadenas mágicas, con la ejecución más rápida para el caso común de notificaciones de auto cambio.
Esta última sugerencia se implementa a continuación (¡creo que comenzaré a usarla!)
public class DataModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
OnPropertyChangedExplicit(propertyName);
}
protected void OnPropertyChanged<TProperty>(Expression<Func<TProperty>> projection)
{
var memberExpression = (MemberExpression)projection.Body;
OnPropertyChangedExplicit(memberExpression.Member.Name);
}
void OnPropertyChangedExplicit(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
}
public class DataModel : DataModelBase
{
//a simple property
string _something;
public string Something
{
get { return _something; }
set { _something = value; OnPropertyChanged(); }
}
//a property with another related property
int _number;
public int Number
{
get { return _number; }
set
{
_number = value;
OnPropertyChanged();
OnPropertyChanged(() => NumberSquared);
}
}
//a related property
public int NumberSquared { get { return Number * Number; } }
}
Actualización : el código original no es compatible con Windows Phone, ya que depende de LambdaExpression.Compile () para obtener el objeto fuente del evento. Aquí está el método de extensión actualizado (con las pruebas de parámetros eliminadas también):
public static void Raise<T>(this PropertyChangedEventHandler handler, Expression<Func<T>> propertyExpression)
{
if (handler != null)
{
var body = propertyExpression.Body as MemberExpression;
var expression = body.Expression as ConstantExpression;
handler(expression.Value, new PropertyChangedEventArgs(body.Member.Name));
}
}
El uso se mantiene como a continuación.
Puede obtener el nombre de propiedad utilizando la reflexión en una función lambda que llama al getter de propiedad. tenga en cuenta que en realidad no tiene que invocar ese lambda, solo lo necesita para la reflexión:
public static class INotifyPropertyChangedHelper
{
public static void Raise<T>(this PropertyChangedEventHandler handler, Expression<Func<T>> propertyExpression)
{
if (handler != null)
{
var body = propertyExpression.Body as MemberExpression;
if (body == null)
throw new ArgumentException("''propertyExpression'' should be a member expression");
var expression = body.Expression as ConstantExpression;
if (expression == null)
throw new ArgumentException("''propertyExpression'' body should be a constant expression");
object target = Expression.Lambda(expression).Compile().DynamicInvoke();
var e = new PropertyChangedEventArgs(body.Member.Name);
handler(target, e);
}
}
public static void Raise<T>(this PropertyChangedEventHandler handler, params Expression<Func<T>>[] propertyExpressions)
{
foreach (var propertyExpression in propertyExpressions)
{
handler.Raise<T>(propertyExpression);
}
}
}
A continuación, le mostramos cómo puede usar ese ayudante en su clase para plantear el evento para una o varias propiedades:
PropertyChanged.Raise(() => this.Now);
PropertyChanged.Raise(() => this.Age, () => this.Weight);
Tenga en cuenta que este asistente también es no operativo en caso de que PropertyChanged
sea null
.