If you've used the INotifyPropertyChanged interface in WPF you might have wished that there was some way to raise the property changed events in a strongly typed way, rather than using property strings. Traditionally, a property changed notification would be raised something like this:
public class Person: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _name;
public string Name {
get
{
return _name;
}
set
{
if (_name == value) return;
_name = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Name"));
}
}
}
}
The problem with this is that the name of the property ("Name") is hard coded as a string, which means that if you rename the property you have to remember to rename the string and if you forget you won't know until runtime. Likewise, a type-o ("Nam") will also not be reported until runtime. It would be so much better if these notifications could be strongly typed and raised using expressions.
For example, why can't I raise the notification like this?
this.Notify(x => x.Name);
Well, I've written a few extension methods to allow exactly that:
public static class NotifyPropertyChangedExtensions
{
// Raise the property change notification for the property identified by the
// received expression.
public static void Notify<T, TReturn>(this T notifier, Expression<Func<T, TReturn>> propertyExpression)
where T : INotifyPropertyChanged
{
var args = new PropertyChangedEventArgs(propertyExpression.ToPropertyName());
notifier.RaiseEvent("PropertyChanged", args);
}
// Find the property info for the property identified by the received expression.
public static PropertyInfo ToPropertyInfo<TSource, TProperty>(this Expression<Func<TSource, TProperty>> propertyLambda)
{
Type type = typeof(TSource);
MemberExpression member = propertyLambda.Body as MemberExpression;
if (member == null)
throw new ArgumentException(string.Format("Expression '{0}' refers to a method, not a property.", propertyLambda));
PropertyInfo propInfo = member.Member as PropertyInfo;
if (propInfo == null)
throw new ArgumentException(string.Format("Expression '{0}' refers to a field, not a property.", propertyLambda));
if (type != propInfo.ReflectedType &&
!type.IsSubclassOf(propInfo.ReflectedType))
throw new ArgumentException(string.Format("Expression '{0}' refers to a property that is not from type {1}.", propertyLambda, type));
return propInfo;
}
// Find the name of the property identified by the received expression.
public static string ToPropertyName<TSource, TProperty>(this Expression<Func<TSource, TProperty>> propertyLambda)
{
return propertyLambda.ToPropertyInfo().Name;
}
// Raises an event via reflection.
// More details in:
// http://blog.peppermint-it.com/2013/04/raising-net-events-using-reflection.html
public static void RaiseEvent<T>(this object instance, string eventName, T eventArgs)
where T : EventArgs
{
Type type = instance.GetType();
FieldInfo field = FindEventInfo(type, eventName);
if (field == null) throw new ArgumentException("Cannot find event named: " + eventName + " on type: " + type.Name, "eventName");
object handler = field.GetValue(instance);
if (handler != null)
{
Type invokeType = handler.GetType();
MethodInfo invoke = invokeType.GetMethod("Invoke", new[] { typeof(object), eventArgs.GetType() });
invoke.Invoke(handler, new[] { instance, eventArgs });
}
}
// Supports RaiseEvent.
// More details in:
// http://blog.peppermint-it.com/2013/04/raising-net-events-using-reflection.html
private static FieldInfo FindEventInfo(Type type, string eventName)
{
var field = type.GetField(eventName, BindingFlags.NonPublic | BindingFlags.Instance);
if (field != null) return field;
if (type == typeof(object)) return null;
return FindEventInfo(type.BaseType, eventName);
}
}
Now that's all fine and dandy, but what about when handling the change event. Once again, typically you get code like this:
Person person = new Person();
person.PropertyChanged += (sender, args) =>
{
if (args.PropertyName == "Name")
{
// do stuff.
}
};
Once again, hard coded strings reduce compile time safety and once again an extension method comes in handy:
public static class NotifyPropertyChangedExtensions
{
// Check whether or not the property name in the received event args corresponds to the
// property name identified by the received property expression.
public static bool IsProperty<T, TReturn>(this PropertyChangedEventArgs args, Expression<Func<T, TReturn>> propertyExpression)
{
return args.PropertyName == propertyExpression.ToPropertyName();
}
// Raise the property change notification for the property identified by the
// received expression.
public static void Notify<T, TReturn>(this T notifier, Expression<Func<T, TReturn>> propertyExpression)
where T : INotifyPropertyChanged
{
var args = new PropertyChangedEventArgs(propertyExpression.ToPropertyName());
notifier.RaiseEvent("PropertyChanged", args);
}
// Find the property info for the property identified by the received expression.
public static PropertyInfo ToPropertyInfo<TSource, TProperty>(this Expression<Func<TSource, TProperty>> propertyLambda)
{
Type type = typeof(TSource);
MemberExpression member = propertyLambda.Body as MemberExpression;
if (member == null)
throw new ArgumentException(string.Format("Expression '{0}' refers to a method, not a property.", propertyLambda));
PropertyInfo propInfo = member.Member as PropertyInfo;
if (propInfo == null)
throw new ArgumentException(string.Format("Expression '{0}' refers to a field, not a property.", propertyLambda));
if (type != propInfo.ReflectedType &&
!type.IsSubclassOf(propInfo.ReflectedType))
throw new ArgumentException(string.Format("Expression '{0}' refers to a property that is not from type {1}.", propertyLambda, type));
return propInfo;
}
// Find the name of the property identified by the received expression.
public static string ToPropertyName<TSource, TProperty>(this Expression<Func<TSource, TProperty>> propertyLambda)
{
return propertyLambda.ToPropertyInfo().Name;
}
// Raises an event via reflection.
// More details in:
// http://blog.peppermint-it.com/2013/04/raising-net-events-using-reflection.html
public static void RaiseEvent<T>(this object instance, string eventName, T eventArgs)
where T : EventArgs
{
Type type = instance.GetType();
FieldInfo field = FindEventInfo(type, eventName);
if (field == null) throw new ArgumentException("Cannot find event named: " + eventName + " on type: " + type.Name, "eventName");
object handler = field.GetValue(instance);
if (handler != null)
{
Type invokeType = handler.GetType();
MethodInfo invoke = invokeType.GetMethod("Invoke", new[] { typeof(object), eventArgs.GetType() });
invoke.Invoke(handler, new[] { instance, eventArgs });
}
}
// Supports RaiseEvent.
// More details in:
// http://blog.peppermint-it.com/2013/04/raising-net-events-using-reflection.html
private static FieldInfo FindEventInfo(Type type, string eventName)
{
var field = type.GetField(eventName, BindingFlags.NonPublic | BindingFlags.Instance);
if (field != null) return field;
if (type == typeof(object)) return null;
return FindEventInfo(type.BaseType, eventName);
}
}
Now the previous code can be changed to:
Person person = new Person();
person.PropertyChanged += (sender, args) =>
{
if (args.IsProperty(x => x.Name))
{
// do stuff.
}
};
Compile time balance is once again restored!