Automatic notification from other properties in C# ViewModel (MVVM)
I would like to link up my post from yesterday, in which I described how to implement a ViewModel with INotifyPropertyChanged interface in WPF context in a compiler-safe manner.
.NET in combination with C# offers a very elegant solution with custom attributes to link properties, for example.
Today I will show how to implement a custom attribute with reflection, which ensures that a property marked with this attribute triggers a PropertyChanged event when the value of another property changes.
In this context, I would like to refer again to my GitHub repository from last time.
Attribute class
With an attribute you are able to store constant meta information for a .NET entity, like a class or a method, at compiler time.
What we want is to store the information for a property which listens for the changes of another property, that triggers PropertyChanged
event.
For this we need the name of the source property as information:
using System;
// ...
[AttributeUsage(AttributeTargets.Property,
AllowMultiple = true,
Inherited = false)]
public class ReceiveNotificationFromAttribute : Attribute
{
#region Constructors (1)
/// <summary>
/// Initializes a new instance of the <see cref="ReceiveNotificationFromAttribute" /> class.
/// </summary>
/// <param name="senderName">
/// The value for the <see cref="ReceiveNotificationFromAttribute.SenderName" /> property.
/// </param>
public ReceiveNotificationFromAttribute(string senderName)
{
SenderName = senderName;
}
#endregion Constructors
#region Properties (1)
/// <summary>
/// Gets or sets the name of sender / sending member of the notification.
/// </summary>
public string SenderName
{
get;
set;
}
#endregion Properties
}
Later you can use the attribute the following way:
// ...
class MyViewModel : INotifyPropertyChanged
{
// ...
public string? Name
{
get { return this.Get(); }
set { this.Set(value); }
}
[ReceiveNotificationFrom("Name")]
public string? UpperName
{
get
{
if (this.Name == null)
{
return null;
}
return this.Name.ToUpper();
}
}
// ...
}
Our goal: Trigger PropertyChanged
event for UpperName
everytime Name
changes.
In the ViewModel we can start with adding helper methods RaisePropertyChanged
and HandleReceiveNotificationFromAttributes
:
// ...
class MyViewModel : INotifyPropertyChanged
{
// ...
protected void Set<TProperty>(TProperty newValue, [CallerMemberName] string propertyName = "")
{
_properties[propertyName] = newValue;
// first raise notification for "this" property
// named in `propertyName`
RaisePropertyChanged(propertyName);
// now for all that uses `ReceiveNotificationFrom`
// and are linked with "this"
HandleReceiveNotificationFromAttributes(propertyName);
}
protected RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
protected void HandleReceiveNotificationFromAttributes(string propertyName)
{
// ..
}
// ...
}
With reflection we start to detect all properties in the MyViewModel
class:
// ...
protected void HandleReceiveNotificationFromAttributes(string propertyName)
{
// get all public and non-public instance properties
var allProperties = GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
// ...
}
// ...
}
Next we filter all properties which using / containing ReceiveNotificationFrom
attributes:
// ...
using System.Linq;
// ...
protected void HandleReceiveNotificationFromAttributes(string propertyName)
{
// ...
var allPropertiesWithReceiveNotificationFromAttributes =
allProperties.Select((property) =>
{
// we collect attributes
// and property together
// in a new anonymous object
return new
{
Attributes = property.GetCustomAttributes(typeof(ReceiveNotificationFromAttribute), false)
.Cast<ReceiveNotificationFromAttribute>()
.ToArray(),
Property = property,
};
})
.Where((x) =>
{
return x.Count > 0;
})
.ToArray();
// ...
}
// ...
}
Now we filter only the property with ReceiveNotificationFrom
that have the same value in their Name
property as the current value in propertyName
:
// ...
protected void HandleReceiveNotificationFromAttributes(string propertyName)
{
// ...
var allMatchingProperties =
allPropertiesWithReceiveNotificationFromAttributes.Where((x) =>
{
// ensure to have only 1 or 0
// attributes, otherwise `SingleOrDefault()`
// will throw an error
var attrib = x.Attributes.SingleOrDefault(y => y.SenderName == propertyName);
return attrib != null;
})
.ToArray();
// ...
}
// ...
}
Finally, after we got all required information, we can start sending the notifications:
// ...
protected void HandleReceiveNotificationFromAttributes(string propertyName)
{
// ...
// also ensure not to have duplicates
var propertiesToNotifyByName =
allMatchingProperties.Select(x => x.Property.Name)
.Distinct(StringComparer.Ordinal)
.ToArray();
foreach (var propertyName in propertiesToNotifyByName)
{
RaisePropertyChanged(propertyName);
}
}
// ...
}
Conclusion
Reflection is a very powerful tool for runtime operation. Especially in the WPF, WCF and ASP.NET contextes it is often used.
Keep in mind that reflection is relative slow and may produce runtime errors, because with it you usually leave the world of strong typing of C# & Co.
Have fun while trying it out! 🎉