Implement compiler-safe ViewModel in C#
A few years ago, I worked at two companies that developed their applications using WPF (Windows Presentation Foundation).
One core concept is the so-called MVVM, which stands for Model-View-ViewModel pattern.
In this approach, an object is bound to a XAML view, which informs the view through an event when its data changes.
In one of my GitHub repositories, I have explored this topic and developed a concept that allows for implementing it using reflection, without relying on unsafe patterns such as “magic strings”.
Background
Objects can be bound as DataContext to XAML views, and their data can be accessed through property paths:
<Window x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyApp"
Title="My app" Height="480" Width="640">
<Window.DataContext>
<local:WindowVM />
</Window.DataContext>
<TextBox Text="{Binding Path=UserEmail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</Window>
In this example, the Window will be connected to a new instance of the class WindowVM
as data context.
Later the TextBox, which is part of the window, will bind its own Text property with the UserEmail
property of this context object.
Implement ViewModel class
The implementation of the WindowVM
class is quite simple:
namespace MyApp;
public class WindowVM
{
public string UserEmail
{
get;
set;
}
}
Now we have a class whose instance can be easily created in the Window
view.
BUT: The problem is that it only works once during the initial reading. The reason is that there is no way to find out in WindowVM
when the value of a property changes.
For this purpose, there is the INotifyPropertyChanged interface, which only has the PropertyChanged event.
The XAML view notices that its DataContext implements this interface, and connects to this event.
An implementation in WindowVM
would look like this:
using System.ComponentModel;
namespace MyApp;
public class WindowVM : INotifyPropertyChanged
{
// ...
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The UserEmail
property should be adjusted as follows:
// ...
public class WindowVM : INotifyPropertyChanged
{
private string _userEmail = "";
public string UserEmail
{
get
{
return this._userEmail;
}
set
{
this._userEmail = value;
RaisePropertyChanged("UserEmail");
}
}
// ...
}
This would already work.
However, the problem now is that this code is error-prone: Since we are using a “magic string” when calling RaisePropertyChanged
, it can easily be overlooked during refactoring.
Here, C# lambda expressions come into play: These are not actual functions, but rather single-line compiler expressions that the compiler directly returns as evaluated objects, allowing their information to be evaluated at runtime.
To do this, we need to rewrite the RaisePropertyChanged
method as follows:
// ...
using System.Linq.Expressions;
public class WindowVM : INotifyPropertyChanged
{
// ...
protected void RaisePropertyChanged(Expression<Func<TProperty>> expr)
{
var memberExpr = (MemberExpression)expr.Body;
var propertyInfo = (PropertyInfo)memberExpression.Member;
var propertyName = propertyInfo.Name;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Finally, we need to adjust the UserEmail
property as follows:
// ...
public class WindowVM : INotifyPropertyChanged
{
// ...
public string UserEmail
{
get
{
return this._userEmail;
}
set
{
this._userEmail = value;
RaisePropertyChanged(() => this.UserEmail);
}
}
// ...
}
Through () => this.UserEmail
, we now have a compiler-safe expression that, for example, outputs errors if we have made a typo while refactoring.
Simplifications (part 1)
To make the WindowVM
class available for additional properties, you could use a dictionary and implement generic getter and setter methods:
// ...
using System.Collections.Concurrent;
using System.Collections.Generic;
public class WindowVM : INotifyPropertyChanged
{
// use a thread-safe dictionary
private readonly IDictionary<string, object> _properties =
new ConcurrentDictionary<string, object>();
// ...
// helper method to get property name from expression
private string _GetPropertyName(Expression<Func<TProperty>> expr)
{
var memberExpr = (MemberExpression)expr.Body;
var propertyInfo = (PropertyInfo)memberExpression.Member;
var propertyName = propertyInfo.Name;
return propertyName;
}
// get property value by expression
protected TProperty Get<TProperty>(Expression<Func<TProperty>> expr)
{
var propertyName = _GetPropertyName(expr);
return (TProperty)_properties[propertyName];
}
// set property value by expression
// and raise PropertyChanged event
protected void Set<TProperty>(Expression<Func<TProperty>> expr, TProperty newValue)
{
var propertyName = _GetPropertyName(expr);
_properties[propertyName] = newValue;
// we do not need the `RaisePropertyChanged()`
// for this example anymore
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
In UserEmail
this finally could look like this:
// ...
public class WindowVM : INotifyPropertyChanged
{
// ...
public string UserEmail
{
get
{
return this.Get(() => this.UserEmail);
}
set
{
this.Set(() => this.UserEmail, value);
}
}
// ...
}
Simplifications (part 2)
This pattern greatly simplifies working with INotifyPropertyChanged
in conjunction with ViewModel classes.
However, Microsoft had implemented an even simpler pattern in C# with the CallerMemberName attribute.
In this case, only an optional string argument is passed to a method, to which the above mentioned attribute is applied.
The compiler looks at which member (in this case: which property) the method was called from and submits the member name.
Our code would have to be adjusted accordingly:
// ...
using System.Runtime.CompilerServices;
public class WindowVM : INotifyPropertyChanged
{
// ...
public string UserEmail
{
get { return this.Get(); }
set { this.Set(value); }
}
// ...
// get property value by expression
protected TProperty Get<TProperty>([CallerMemberName] string propertyName = "")
{
// ...
}
protected void Set<TProperty>(TProperty newValue, [CallerMemberName] string propertyName = "")
{
// ...
}
}
With this, properties can now be easily implemented that make use of the PropertyChanged
event, as the code is usually the same.
Conclusion
As you can see, using ViewModel in the WPF context with the right patterns can make life much easier.
Lambda expressions and attributes are both powerful tools for this purpose.
Have fun while trying it out! 🎉