Good-bye OnPropertyChanged. Hello BindableProperty
One of the most annoying things with XAML binding in WPF and Windows App Store apps is the need to raise a property changed notification to the user interface. Typically, you would abstract the actual property changed notification in to a base class like this:
public class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void SetProperty<T>(ref T oldValue, T newValue, [CallerMemberName] string property = "")
{
if (object.Equals(oldValue, newValue))
{
return;
}
oldValue = newValue;
this.OnPropertyChanged(property);
}
protected virtual void OnPropertyChanged(string property)
{
var eventHandler = this.PropertyChanged;
if (eventHandler != null)
{
eventHandler(this, new PropertyChangedEventArgs(property));
}
}
}
This at least prevents you from having to implement INotifyPropertyChanged
on every one of your models. The issue however is that you still need to raise the event. In order to do so, you would invoke the base class' OnPropertyChanged
method.
public class User : BindableBase
{
private string firstName;
private string lastName;
public string FirstName
{
get
{
return this.firstName;
}
set
{
this.SetProperty(ref this.firstName, value);
}
}
public string LastName
{
get
{
return this.lastName;
}
set
{
this.SetProperty(ref this.lastName, value);
}
}
}
The boiler plate code here gets really monotonous when you have a project with more than a handful of models. Luckily, Roslyn now has Initializer support for Auto-Properties..
With Auto-Property Initializers, we can build a wrapper around the actual property and bind to the value our wrapper holds. To demonstrate, we will create a simple wrapper. There will be a generic and a non-generic flavor. The non-generic provides a static method for constructing a generic wrapper. We will call this wrapper BindableProperty.
Generic Property Wrapper
public class BindableProperty<T> : BindableBase
{
private T value;
public BindableProperty(T value, [CallerMemberName] string propertyName = "")
{
this.value = value;
this.Name = propertyName;
}
public BindableProperty([CallerMemberName] string propertyName = "")
{
this.value = default(T);
this.Name = propertyName;
}
public T Value
{
get
{
return this.value;
}
set
{
this.SetProperty(ref this.value, value);
}
}
public string Name { get; private set; }
public static BindableProperty<T> Prepare(T value, [CallerMemberName] string propertyName = "")
{
return new BindableProperty<T>(value, propertyName);
}
public static BindableProperty<T> Prepare([CallerMemberName] string propertyName = "")
{
return new BindableProperty<T>(default(T), propertyName);
}
}
Non-generic static class for coonstruction
public static class BindableProperty
{
public static BindableProperty<T> Prepare<T>([CallerMemberName] string propertyName = "")
{
return new BindableProperty<T>(default(T), propertyName);
}
}
You can see the BindableProperty
With our new BindableProperty
public class User
{
public BindableProperty<string> FirstName { get; set; } = BindableProperty.Prepare<string>();
public BindableProperty<string> LastName { get; set; } = BindableProperty.Prepare<string>();
}
We now have properties in our model that can be data-bound to and push change notifications back to the UI. We could even provide an initial value for the properties if we wanted to.
public class User
{
public BindableProperty<string> FirstName { get; set; } = BindableProperty.Prepare<string>(string.Empty);
public BindableProperty<string> LastName { get; set; } = BindableProperty.Prepare<string>("Stevenson");
}
Now we can create a view model real-quick for our view.
public class NewUserViewModel : ICommand
{
public User NewUser { get; set; } = new User();
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public async void Execute(object parameter)
{
if (string.IsNullOrEmpty(this.NewUser.FirstName.Value) ||
string.IsNullOrEmpty(this.NewUser.LastName.Value))
{
var dlg = new MessageDialog("User information is invalid");
await dlg.ShowAsync();
}
}
}
In order to execute properly, the FirstName and LastName properties need to have their Value property checked. If the Value property is null, we show a dialog. Having to check the Value property is an extra step that you wouldn't normally need to take, but one extra 'dot' is a lot less than having to write all the OnPropertyChanged boiler plate code.
Now for our view, we just wire up a quick simple user entry form. The TextBoxes are bound to the Value property in each of the User's BindableProperty properties.
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="First name" />
<TextBox Text="{Binding Path=NewUser.FirstName.Value}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Last name" />
<TextBox Text="{Binding Path=NewUser.LastName.Valuey}" />
</StackPanel>
<Button Command="{Binding }"
Content="Create." />
</StackPanel>
This is going to make your models a lot cleaner and speed up the amount of time it takes to create property changed based objects.