Coercing ViewModel Values with INotifyPropertyChanged
08 May 2011Perhaps one of the most ambivalent things about putting code on GitHub is that it’s more or less an open project. It’s great that people (including yourself) can continue to work on it, but it seems to lack closure so that you can move on with your life.
So one of the things that I’ve been missing in my BindableBase
class is property coercion, a la dependency properties. It’s a pretty smart idea; you can keep values in a valid state without pushing change notification. Unfortunately, there are some problems that crop up pretty quickly in INotifyPropertyChanged
based view models.
Consider a Foo property that refuses to set a value less than zero.
private int _foo; public int Foo { get { return _foo; } set { if(_foo >= 0) SetProperty(ref _foo, value, "Foo"); } }
That looks like pretty good coercion, but the problem is that your binding and your property are now out of sync. That is, the binding told your object to set a value and assumed it did; you gave it no notification to the contrary. So while your object retains the old value, the bound TextBox
will blissfully report -23.
A more brute force option would raise PropertyChanged
in the event of this kind of coercion, but that breaks the code contract for INotifyPropertyChanged
in that nothing changed. So how do we get around this problem?
If you take a look at the invocation list of your PropertyChanged
event with some bound variables, you’ll notice that there’s a PropertyChangedEventManager
hooked up.
Considering that the other item in the list is my default delegate, this object must be responsible for communicating my events to the binding system
Of course, the next thing to do is fire up Reflector and take a look at what’s there.
public class PropertyChangedEventManager : WeakEventManager { // Fields private WeakEventManager.ListenerList _proposedAllListenersList; private static readonly string AllListenersKey; // Methods static PropertyChangedEventManager(); private PropertyChangedEventManager(); public static void AddListener(INotifyPropertyChanged source, IWeakEventListener listener, string propertyName); private void OnPropertyChanged(object sender, PropertyChangedEventArgs args); private void PrivateAddListener(INotifyPropertyChanged source, IWeakEventListener listener, string propertyName); private void PrivateRemoveListener(INotifyPropertyChanged source, IWeakEventListener listener, string propertyName); protected override bool Purge(object source, object data, bool purgeAll); public static void RemoveListener(INotifyPropertyChanged source, IWeakEventListener listener, string propertyName); protected override void StartListening(object source); protected override void StopListening(object source); // Properties private static PropertyChangedEventManager CurrentManager { get; } }
Basically, AddListener
access the private CurrentManager
singleton and subscribes the given listener to OnPropertyChanged
. Fortunately for us, there’s a flaw in th the implementation of OnPropertyChanged
. What it does is it gets the list of listeners based on the sender and raises their event with the given sender and args. The problem here is that it doesn’t verify that the object raising the event is actually the sender! That is to say, we should be able to send a fake PropertyChanged event through another object acting as a surrogate. All we need to do is add that object to the PropertyChangedEvenManager’s list and start impersonating.
To that end, I added this private class to BindableBase
to lazily add the INotifyPropertyChanged
proxy object to the PropertyChangedEventManager
. Since we’re not actually interested in the events, I made a stub implementation of IWeakEventListener
that returns false constantly to indicate it’s not handling the event. Finally, I hold onto both of these references to keep them from being garbage collected.
private class PropertyChangedEventManagerProxy { // We need to hold on to these refs to keep it from getting GC'd private readonly NotifyPropertyChangedProxy _notifyPropertyChangedProxy; private readonly IWeakEventListener _weakEventListener; private PropertyChangedEventManagerProxy() { _notifyPropertyChangedProxy = new NotifyPropertyChangedProxy(); _weakEventListener = new WeakListenerStub(); PropertyChangedEventManager.AddListener(_notifyPropertyChangedProxy, _weakEventListener, string.Empty); } public void RaisePropertyChanged(object sender, string propertyName) { _notifyPropertyChangedProxy.Raise(sender, new PropertyChangedEventArgs(propertyName)); } private static PropertyChangedEventManagerProxy _instance; public static PropertyChangedEventManagerProxy Instance { get { return _instance ?? (_instance = new PropertyChangedEventManagerProxy()); } } private class NotifyPropertyChangedProxy : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged = delegate { }; public void Raise(object sender, PropertyChangedEventArgs e) { PropertyChanged(sender, e); } } private class WeakListenerStub : IWeakEventListener { public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { return false; } } }
The only thing left to do is add the coerce value function as an optional parameter on SetProperty
and hook it up:
protected void SetProperty<T>( ref T backingStore, T value, string propertyName, Action onChanged = null, Action<T> onChanging = null, Func<T, T> coerceValue = null) { VerifyCallerIsProperty(propertyName); var effectiveValue = coerceValue != null ? coerceValue(value) : value; if (EqualityComparer<T>.Default.Equals(backingStore, effectiveValue)) { // If we coerced this value and the coerced value is not equal to the original, we need to // send a fake PropertyChanged event to notify WPF that this value isn't what it thinks it is. if (coerceValue != null && !EqualityComparer<T>.Default.Equals(value, effectiveValue)) PropertyChangedEventManagerProxy.Instance.RaisePropertyChanged(this, propertyName); return; } if (onChanging != null) onChanging(effectiveValue); OnPropertyChanging(propertyName); backingStore = effectiveValue; if (onChanged != null) onChanged(); OnPropertyChanged(propertyName); }
And there you have it. All the benefits of dependency property coercion, and the same short, sweet SetProperty
syntax.
As before, everything is on GitHub so you can take a look at the whole thing and fork it if you see something to be improved.