WPF Model Data Binding - Part I
The Data Binding concept, first introduced with Windows Forms, provides a way for decoupling the View from the Data and for eliminating the boilerplate, mass code of marshaling the data to and from the view. WPF extends this mechanism by adding some great features, especially when dealing with Data Templates, which provides an easy way for reflecting the data with view. In WPF, Data Binding is everywhere!
Data binding is a great mechanism, but there are some situations that data-binding not exactly fits. In Part I of this post I will talk about such situation, and I will provide some kind of pattern that I developed for workaround this problem. In Part II, I will upgrade my solution with other techniques, and I will show how to handle multi-threading scenarios with data binding.
You can download the code from here.
Let say that you are writing a fascinated WPF application and you have at least one window or dialog, binded to data. The markup-snippet bellow demonstrates how to bind a model object, Person, to a simple window, MainWindow.
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Tomer.Patterns.ModelBinding.MainWindow"
Title="Model Binding Pattern"
Width="451"
Height="238"
WindowStartupLocation="CenterScreen"
Closing="MainWindow_Closing"
Style="{StaticResource {x:Type Window}}">
<Grid>
<Label Content="First Name" ... />
<Label Content="Last Name" ... />
<Label Content="Blog" ... />
<TextBox Text="{Binding FirstName}" ... /> <TextBox Text="{Binding LastName}" ... /> <TextBox Text="{Binding Blog}" ... />
</Grid>
</Window>
As you can see, this is a simple scenario for binding the view (UI) with the model (Data).
The questions you should ask are:
"What if I change the data in this form, click OK, and then the validation/controller/service/other rejects these changes?"
"Now that the binded, Person instance was altered, how can I rollback?"
A simple solution is to clone the Person instance before binding it to the view. This solution is not clean, it is optional and not mandatory, and it demands each object to be cloneable, which is not always possible (for example, WCF data-contract-types).
One way to work around this problem is to provide a generic base class for your model types, with the following characteristics:
- Property notification changes
- In-memory state persistency
- Commit and Rollback mechanism
- Dirty flag
- Thread safe
The code-snippet bellow demonstrates a Skelton of such class.
using System;
using System.ComponentModel;
using System.Diagnostics;
namespace Tomer.Patterns.ModelBinding
{ public abstract class DataEntity<TData> : IDisposable where TData : new()
{ private bool _isDirty = false;
private bool _isDisposed = false;
protected DataEntity() : this (new TData()) { } protected DataEntity(TData data)
{ this._data = data;
}
public event PropertyChangedEventHandler PropertyChanged;
public bool IsDirty
{ get { return _isDirty; } private set { ... } }
/// <summary>
/// Commit changes of this instance and reset the dirty flag
/// </summary>
public void Commit()
{ // Commit changes only if in dirty state
if (IsDirty) { ... } }
/// <summary>
/// Rollback changes of this instance and reset the dirty flag
/// </summary>
public void Rollback()
{ // Rollback changes only if in dirty state
if (IsDirty) { ... } }
public TData Data
{ get { return _data; } }
/// <summary>
/// Derived classes must call this instance method to notify property change
/// </summary>
/// <param name="propertyName">The exact name of the property that was changed</param>
protected void NotifyPropertyChanged(string propertyName)
{ ...
}
~DataEntity() { Dispose(false); }
public void Dispose() { ... }
/// <summary>
/// Serialize current state into memory
/// </summary>
private void SerializeState() { ... } /// <summary>
/// Deserialize previous state from memory
/// </summary>
private void DeserializeState() { ... } }
}
This generic base type provides a kind of wrapper around the data object (it is best match for data type that came from Web Service/WCF). It wraps the raw-data-type by holding a reference to it, and provides properties accordantly.
This base class tracks derive-types changes by providing a method for notifying property changes. Whenever a property changes, a dirty-flag is turned on. To rollback changes, this type uses .NET binary serialization for holding the state in-memory. Calling the Rollback method simply deserialize previous state. Calling the Commit method simply serialize current state.
The code-snippet bellow shows how to create a data-entity based on the generic base class.
using System;
using System.Collections.Generic;
using System.Text;
using Tomer.Patterns.ModelBinding;
namespace Tomer.Patterns.UI
{ /// <summary>
/// This type represents a raw data, for example a WCF data contract
/// </summary>
[Serializable]
public class PersonData
{ #region Fields
private string _firstName;
private string _lastName;
private string _blog;
#endregion
#region Properties
public string FirstName
{ get { return _firstName; } set { _firstName = value; } }
public string LastName
{ get { return _lastName; } set { _lastName = value; } }
public string Blog
{ get { return _blog; } set { _blog = value; } }
#endregion
}
/// <summary>
/// This type represents a data entity which supports data
/// binding notifications, commit, rollback and other operations
/// <see cref="Tomer.Patterns.ModelBinding.DataEntity<TData>"/>
/// </summary>
public class Person : DataEntity<PersonData>
{ #region Initializers
public Person() { } public Person(PersonData data) : base (data) { } #endregion
#region Properties
public string FirstName
{ get { return Data.FirstName; } set
{ Data.FirstName = value;
NotifyPropertyChanged("FirstName"); }
}
public string LastName
{ get { return Data.LastName; } set
{ Data.LastName = value;
NotifyPropertyChanged("LastName"); }
}
public string Blog
{ get { return Data.Blog; } set
{ Data.Blog = value;
NotifyPropertyChanged("Blog"); }
}
#endregion
}
}
Now lets see how to bind the Person entity with the WPF view:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.ComponentModel;
using Tomer.Patterns.UI;
namespace Tomer.Patterns.ModelBinding
{ public partial class MainWindow : Window
{ public MainWindow()
{ InitializeComponent();
PersonData data = new PersonData();
data.FirstName = "Tomer";
data.LastName = "Shamam";
data.Blog = "http://blogs.microsoft.co.il/blogs/tomershamam";
Person person = new Person(data);
this.DataContext = person;
}
void buttonCommit_Click(object sender, RoutedEventArgs e)
{ Person person = DataContext as Person;
person.Commit();
}
void buttonRollback_Click(object sender, RoutedEventArgs e)
{ Person person = DataContext as Person;
person.Rollback();
}
}
}
In Part II of this post, I will provide other options for handling state, and show how to handle multi-threaded scenarios.
You can download the code from here.