Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Why I Love Silverlight DataBinding

0.00/5 (No votes)
2 Dec 2009 1  
Solving a common UI problem with a ViewModel.

Introduction

The MVVM design pattern (a.k.a. ViewModel) is discussed a lot. That is not a sign of strength. Mature, established design patterns are not discussed that much. They are simply studied and used.

If anything, the many discussions reflect a need for a better understanding. Many articles and blog posts discuss MVVM on the conceptual level. Quite a few bloggers think that additional frameworks are needed, and focus on the framework rather than the design pattern itself. Some provide an actual example of a ViewModel, but the problem addressed is something that could be simply implemented without a ViewModel. Very few actually describe (like this one) a non-trivial problem that is then elegantly solved with a ViewModel.

This article describes a common UI problem and shows how it can be solved with MVVM.

Disclaimer: Unlike other patterns, MVVM has not been described clearly (e.g., on Wikipedia, that does a pretty good job defining other patterns), so there is a (small) chance that this might not actually be an example of MVVM. The problem and solution are interesting nonetheless.

The problem

In user interfaces, it often happens that several items on the screen need to be synchronized. A simple example is a Save button that indicates whether there is anything to save; disabling the button gives the user feedback that a save operation was completed successfully. Another example is updating a record shown in a DataGrid, in a DataForm. The DataGrid shows the original values, and the DataForm shows the updated, but not yet saved ones. This is the example discussed here.

We require:

  1. The DataForm displays the item selected in the DataGrid.
  2. The DataGrid is read-only, and displays persisted values; the DataForm displays the current (potentially changed, not saved) values.
  3. Both the DataGrid and the DataForm indicate with a special background color whether an item still needs to be saved.
  4. There is a summary line that indicates how many unsaved records there are.
  5. There is a Save button that saves a (single) selected record.
  6. There is a Reset button that resets a (single) selected record.
  7. Both the Save and the Reset buttons are enabled if and only if the selected record is not persisted yet.
  8. The content shown (person records) can be filtered, both by Gender and by a record's Change State.

Note that the state the data is in is reflected in various ways that all need to be in sync. The user is able to edit individual records in parallel, and can undo or save individual changes. At any time, the application gives a clear feedback on what the state of the data is. We could implement this with a lot of event handling. But Silverlight databinding supports a more elegant solution: the ViewModel solution.

The Visual Studio solution

In Visual Studio, we have a standard Silverlight WCF service that sends a list of Person objects to the client upon request. The Person class is defined in the Model/Person.cs file in the ASP.NET web application project. Upon creating the service reference, Visual Studio creates equivalent Person objects in the ViewModelDemo.DemoServiceReference namespace. We rename this namespace:

using Model = ViewModelDemo.DemoServiceReference;

so that the class of our Person objects on the client can be called Model.Person. On the client, we also have a ViewModel.Person class that wraps the Model.Person class:

public class Person : INotifyPropertyChanged
{
    private Model.Person _modelPerson = null;

    public Person(Model.Person modelPerson)
    {
        _modelPerson = modelPerson;
        // editable properties:
        _firstName = _modelPerson.FirstName;
        _lastName = _modelPerson.LastName;
        _dob = _modelPerson.DateOfBirth;
        _gender = _modelPerson.Gender;
    }

We could say that the Model.Person object is kept in the ViewModel.Person object to remember the original values of its properties. This way, we can easily reset the values and check whether they are changed. After a successful Save operation, the corresponding Person objects have the same property values again. For each property of Model.Person, we have two properties for ViewModel.Person. They are used for binding to the DataForm and the DataGrid, respectively (Req. 2).

private string _firstName;
[Display(Name="First Name")]
public string FirstName
{
     get
     {
         return _firstName;
     }
     set
     {
         if (_firstName != value)
         {
             _firstName = value;     // validation: out of scope
             NotifyPropertyChanged("FirstName");
             IsDirty = (_firstName != _modelPerson.FirstName);
          }
     }
}
        
[Display(AutoGenerateField = false)]
public string FirstNameModel
{
    get
    {
        return _modelPerson.FirstName;
    }
    private set
    {
        if (_modelPerson.FirstName != value)
        {
            _modelPerson.FirstName = value;
            NotifyPropertyChanged("FirstNameModel");
        }
    }
}

There is an IsDirty property for the ViewModel.Person that helps implement Req. 7.

private bool _isDirty;
[Display(AutoGenerateField = false)]
public bool IsDirty
{
    get
    {
        return _isDirty;
    }
    private set
    {
        _isDirty = value;
        NotifyPropertyChanged("IsDirty");
        NotifyPropertyChanged("BackgroundColorString");
    }
}

Before we finish Req. 7, let's look at the property BackgroundColorString:

[Display(AutoGenerateField=false)]
public string BackgroundColorString
{
    get
    {
        if (_isDirty)
        {
            return "#FFCE5B";  // Colors.Orange.ToString();
        }
        return null;
    }
}

Indeed, this covers Req. 3 because the the Background property of the DataForm and (the Panel in each of the cells in) the DataGrid rows observe this BackgroundColorString property - whenever it changes, they update their background. Back to Req. 7, the two buttons are defined in XAML, like so:

<Button x:Name="SaveButton" 
       IsEnabled="{Binding SelectedItem.IsDirty, ElementName=TheGrid}" 
       VerticalAlignment="Top" Margin="5">
    <StackPanel Orientation="Horizontal" Margin="5">
        <TextBlock Text="Save" VerticalAlignment="Center" Margin="5,0" />
        <Image Source="arrow-forward_32.png" />
    </StackPanel>
</Button>

We use element binding to synchronize the buttons to the selected item in the DataGrid, and bind the IsEnabled property of the buttons to the IsDirty property of the currently selected Person record. This concludes Req. 7, so three down, five to go. Element binding is also used to synchronize the DataGrid and the DataForm (Req. 1); the definition of the DataForm contains:

CurrentItem="{Binding SelectedItem, ElementName=TheGrid, Mode=OneWay}"

Wow. Element Binding is my friend! The summary line, positioned right below the DataGrid (Req. 4), is a simple TextBlock that binds to (or observes) a SummaryText property. Now, this property is not a property of a ViewModel.Person, but of the Application Cache.

Caching

Why use a cache? Well, why not? In the old days, with HTML and JavaScript (remember?), the possibility of caching on the client did not even occur to me, probably because data and markup were inseparably mixed. But, now that we have the Silverlight technology, it's obvious we want to cache data on the client to minimize the load on the server and to optimize application performance. So, we set the main page's DataContext to an Application Cache (a Singleton, even though we have only a single page). The cache has a property People that the DataGrid, the Pager, and the DataForm bind to:

private PagedCollectionView _people;
public PagedCollectionView People
{
    get
    {
        if (_people == null)
        {
            _people = new PagedCollectionView(new Collection<viewmodel.person>());
            // to avoid calling service more than once
            // (if there is more than one control bound to People)

            DemoServiceClient proxy = new DemoServiceClient();
            proxy.GetPeopleCompleted += 
              new EventHandler<getpeoplecompletedeventargs>(proxy_GetPeopleCompleted);
            proxy.GetPeopleAsync();
        }
        return _people;
    }
    set
    {
        _people = value;
        NotifyPropertyChanged("People");
    }
}
        
private void proxy_GetPeopleCompleted(object sender, GetPeopleCompletedEventArgs e)
{
    if (e.Error != null)
    {
        MessageBox.Show(e.Error.Message);  // bare bones
        return;
    }

    Collection<viewmodel.person> people = new Collection<viewmodel.person>();
    foreach (Model.Person modelPerson in e.Result)
    {
        ViewModel.Person person = new ViewModel.Person(modelPerson);
        people.Add(person);
        person.PropertyChanged += new PropertyChangedEventHandler(person_PropertyChanged);
    }
    People = new PagedCollectionView(people);
    ApplyFilters();
}
        
private void person_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    switch (e.PropertyName)
    {
        case "IsDirty":
            NotifyPropertyChanged("ChangeSummary");
            break;
    }
}

Did you see that? The Appication Cache observes each ViewModel.Person in the People collection, and whenever their IsDirty property is changed, any UI Element observing the ChangeSummary property of the Application Cache gets notified. And, you guessed it, the summary line observes this property of the Application Cache:

public string ChangeSummary
{
    get
    {
        int count = 0;
        foreach (ViewModel.Person person in _people.SourceCollection)
        {
            if (person.IsDirty)
            {
                count += 1;
            }
        }
        if (count == 1)
        {
            return String.Format("There is one person with unsaved changes.", count);
        }
        return String.Format("There are {0} persons with unsaved changes.", count);
    }
}

If you run the application with a breakpoint on the getter of the People property, you see that it is hit several times. The very first time, a call to the server is made and _people is given a non-null value. The latter is done because there are several UI Elements bound to the People property, and we want the server call to be made only once. Once the server has returned the People collection, the setter of the People property is called, triggering all these UI Elements to call the getter once again to obtain the PagedCollectionView. Neat.

Req. 6 is simple to implement in the code-behind (Quelle Horreur!) of the page:

private void ResetButton_Click(object sender, RoutedEventArgs e)
{
    ViewModel.Person currentPerson = TheGrid.SelectedItem as ViewModel.Person;
    if (currentPerson != null)
    {
        currentPerson.Reset();
    }
}

which calls a simple method of ViewModel.Person:

public void Reset()
{
    FirstName = _modelPerson.FirstName;
    LastName = _modelPerson.LastName;
    DateOfBirth = _modelPerson.DateOfBirth;
    Gender = _modelPerson.Gender;
}

Req. 5 requires a little bit more code since we need to call the server. We make sure this call is successful before we actually update the UI. The latter, again, is done implicitly, by setting properties that raise PropertyChanged events:

public void Save()
{
    DemoServiceClient proxy = new DemoServiceClient();
    Model.Person tmpPerson = new Model.Person()
    {
        PersonId = _modelPerson.PersonId,
        FirstName = _firstName,
        LastName = _lastName,
        DateOfBirth = _dateOfBirth,
        Gender = _gender
    };
    proxy.SavePersonCompleted += 
      new EventHandler<savepersoncompletedeventargs>(SavePersonCompleted);
    proxy.SavePersonAsync(tmpPerson);
}

private void SavePersonCompleted(object sender, SavePersonCompletedEventArgs e)
{
    if (e.Error != null)
    {
        MessageBox.Show(e.Error.Message, 
          "Server Error", MessageBoxButton.OK);
        return;
    }
    Model.Person saved = e.Result;
    if (saved.PersonId == this.PersonId)
    {
        FirstNameModel = _firstName;
        LastNameModel = _lastName;
        DateOfBirthModel = _dateOfBirth;
        GenderModel = _gender;
        IsDirty = false;
    }
}

This leaves Req. 8. We could do this using the SelectionChanged of the ComboBoxes (not shown in the picture above). But, now that we have a new hammer, we could see whether we can nail this requirement with it as well. Yes, we can! We can TwoWay bind the CurrentItem property of each ComboBox to a property of the Application Cache. In this way, we can detect that a filter selection has changed and set the People's (PagedCollectionView's) filter. However, this is somewhat of a stretch.

Points of interest

Did you notice:

  • The only event used to synchronize UI elements is PropertyChanged.
  • Data is retrieved only once, and only on demand. Rich Internet Applications can minimize the load on servers (and hence may be more performant).
  • Next to MVVM, the solution uses the Observer and Singleton patterns.

You might also have noticed that validation is not addressed and that (quite inconsistently) we use AutoGenerateColumns="false" for the DataGrid and AutoGenerateFields="true" for the DataForm, so we need to suppress the display of the properties that give access to the Model.Person - [Display(AutoGenerateField = false)].

Before running the example application

  1. Set ViewModelDemo.Web as the StartUp Project.
  2. Set ViewModelDemoTestPage.aspx as the Start Page.
  3. Add a Service Reference to the Service References folder in the Silverlight project. Discover, and call the namespace DemoServiceReference.
  4. Build.

History

  • Dec. 2, 2009 - First version.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here