I think anyone, who at least once developed a WPF application, has also met the INotifyPropertyChanged interface. It is the key to build a program based on MVVM pattern, it helps to refresh binding and keep UI and code behind sync. Just like its name says, you receive a notification after a property's value has changed. But what if you need a notification before it changes? Let's assume we want to track user's changes and rollback if it is needed. In this case the INotifyPropertyChanging interface has to be implemented. Unfortunately, however this interface exists in .NET, nothing supports it. It means, you don't get any update or notify during property changing. But it doesn't have to mean, that you can't implement it yourself. So, let's see:
First, INotifyPropertyChanged.
I don't really like to operate with 'magic' strings, furthermore you don't get any compile error, but runtime error in case of typo, which is quite irritating. So, make it private and create another one with property selector using Expression.
Alright. So far so good. But it would be better, if we hadn't write in every property setter a value compare, a backing field setter and a raise. We can use a SetAndRaisePropertyChanged method instead. It has three parameters: a property selector expression, a backing field reference and the value itself. If the value really changed, we set it and raise a property change.
In this moment, it wouldn't be necessary to explicitly extract the property name, but it make sense later. So, we have a base class, which provides a SetAndRaisePropertyChanged method. With this method you can implement a bindable property like this:
Ok, after this little detour let's start to implement the INotifyPropertyChanging interface. You can add to your base class or not, it's up to you, has no effect after all. Our goal is, to receive a notification before a property actually changes. Create an empty generic virtual method, it can be overwritten in the inherited class to handle property changing. As the method signature shows, you get the property name, the old value and the new value as well.
If not, you can simply remove the event handler part from the method. To make it work, add this handler method to the SetAndRaisePropertyChanged, right before backing field setter line. As result the new content of your method looks like this:
And that's it. All you need to do is inherit your class from this base class, use SetAndRaisePropertyChanged instead of explicitly comparing and setting value and overwrite OnPropertyChanging method. Like I mentioned before, you can't prevent value changes. The problem is that if property is bound to a visual element (e.g TextBox), the user already made changes, you receive any notification first in code behind after that. So, if you want to prevent it, you actually need to set it back to it's old value, but sometimes it is so late. Other issue can be a complex control, like a TabControl. If you try to prevent tab changes you might face with some inconsistency. The same page is visible, but an other tab header is selected.
First, INotifyPropertyChanged.
protected void RaisePropertyChanged(string propertyName)
{
var handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
I don't really like to operate with 'magic' strings, furthermore you don't get any compile error, but runtime error in case of typo, which is quite irritating. So, make it private and create another one with property selector using Expression.
protected void RaisePropertyChanged<TProperty>(Expression<Func<TProperty>> propertySelector)
{
var property = this.ExtractPropertyName(propertySelector);
this.RaisePropertyChanged(property);
}
private string ExtractPropertyName<TProperty>(Expression<Func<TProperty>> propertySelector)
{
MemberExpression me = propertySelector.Body as MemberExpression;
if (me == null)
{
UnaryExpression ue = propertySelector.Body as UnaryExpression;
if (ue != null)
{
me = ue.Operand as MemberExpression;
}
}
if ((me != null) && !string.IsNullOrEmpty(me.Member.Name))
{
return me.Member.Name;
}
throw new ArgumentException("Property could not be found.");
}
Alright. So far so good. But it would be better, if we hadn't write in every property setter a value compare, a backing field setter and a raise. We can use a SetAndRaisePropertyChanged method instead. It has three parameters: a property selector expression, a backing field reference and the value itself. If the value really changed, we set it and raise a property change.
protected bool SetAndRaisePropertyChanged<TProperty>(
Expression<Func<TProperty>> propertySelector,
ref TProperty backingField, TProperty value)
{
var result = backingField == null || backingField.Equals(value) == false;
if (result)
{
var propertyName = this.ExtractPropertyName(propertySelector);
backingField = value;
this.RaisePropertyChanged(propertyName);
}
return result;
}
In this moment, it wouldn't be necessary to explicitly extract the property name, but it make sense later. So, we have a base class, which provides a SetAndRaisePropertyChanged method. With this method you can implement a bindable property like this:
private string _myProperty;
public string MyProperty
{
get { return _myProperty; }
set { this.SetAndRaisePropertyChanged(() => this.MyProperty, ref _myProperty, value); }
}
Ok, after this little detour let's start to implement the INotifyPropertyChanging interface. You can add to your base class or not, it's up to you, has no effect after all. Our goal is, to receive a notification before a property actually changes. Create an empty generic virtual method, it can be overwritten in the inherited class to handle property changing. As the method signature shows, you get the property name, the old value and the new value as well.
protected virtual void OnPropertyChanging<TProperty>(
string propertyName, TProperty oldValue, TProperty newValue)
{ }
You cannot cancel or prevent the change, at least not at this point. I assume the base class also implements the INotifyPropertyChanging interface and you have a PropertyChanging event, too. In this case:private void OnPropertyChangingHandler<TProperty>(
string propertyName, TProperty oldValue, TProperty newValue)
{
var hander = this.PropertyChanging;
if (hander != null)
{
hander(this, new PropertyChangingEventArgs(propertyName));
}
// call virtual method after event
this.OnPropertyChanging(propertyName, oldValue, newValue);
}
If not, you can simply remove the event handler part from the method. To make it work, add this handler method to the SetAndRaisePropertyChanged, right before backing field setter line. As result the new content of your method looks like this:
protected bool SetAndRaisePropertyChanged<TProperty>(
Expression<Func<TProperty>> propertySelector,
ref TProperty backingField, TProperty value)
{
var result = backingField == null || backingField.Equals(value) == false;
if (result)
{
var propertyName = this.ExtractPropertyName(propertySelector);
// call changing method
this.OnPropertyChangingHandler(propertyName, backingField, value);
backingField = value;
this.RaisePropertyChanged(propertyName);
}
return result;
}
And that's it. All you need to do is inherit your class from this base class, use SetAndRaisePropertyChanged instead of explicitly comparing and setting value and overwrite OnPropertyChanging method. Like I mentioned before, you can't prevent value changes. The problem is that if property is bound to a visual element (e.g TextBox), the user already made changes, you receive any notification first in code behind after that. So, if you want to prevent it, you actually need to set it back to it's old value, but sometimes it is so late. Other issue can be a complex control, like a TabControl. If you try to prevent tab changes you might face with some inconsistency. The same page is visible, but an other tab header is selected.
No comments:
Post a Comment