Ok so here is the first post on databinding. This will be a couple of posts where I discuss databinding and also the separation of concerns regarding form behaviour and presentation at the hand of the MVVM pattern.
The philisophy behind the MVVM pattern is that the (M)odel carries the data being presented, the (V)iew(M)odel controls the (V)iew and controls access to the (M)odel. The (V)iew is purely the presentation of the model and VM state. There are several schools of thought as to how this is supposed to work. Some ppl say that the View should be abstracted by an interface and injected into the VM and the VM controls the View through this interface. However, I don't agree with this. This creates an 'outward' dependency that I don't like. My opinion is that the View must be a passive observer (presenter) of the ViewModel and Model wrt presentation of info and then 'push' user interaction to the VM through loosely coupled mechanisms.
These posts will loosely follow the implementation I did for my own MVVM framework for Winforms. If you doubt the benefit of this pattern, I hope that these posts will change your mind.
In the code I am using 'older' C# syntax as this is supported way back to Compact Framework 1. I use the MVVM framework on WinCE devices as well.
The source is here:
https://dl.dropboxusercontent.com/u/47608139/MyBB/WinformsDataBinding1.zip
To define some terms:
Behaviour is how the form/view behaves as the user interacts with it. Behaviour forms part of the User Experience (UX).
Presentation is how the data and interactive controls are visually presented. The Presentation also forms part of the UX.
Ideally the VM should be completely agnostic of what the presentation looks like. This will become evident in this post.
Databinding is a very powerful mechanism. Not only can you use it to bind user entry, but also the control the behaviour of controls and presentation on the form. Databinding works by means of the INotifyPropertyChanged interface. You can databind to an object that implements this interface. For the purposes of this demo, I have created a base class called ObservableObject that implements this interface.
Code:
using System.ComponentModel;
namespace WinformsDataBinding1
{
public class ObservableObject:
INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler == null)
return;
handler(this,new PropertyChangedEventArgs(propertyName));
}
}
}
Take note of the above event dispatcher. This is how you have to to it and not as below
Code:
if (PropertyChanged == null)
return;
PropertyChanged(this,new PropertyChangedEventArgs(propertyName));
The above can create race conditions in multi-threaded environments. But that is a topic in itself.
We will derive our ViewModel from the ObservableObject purely to reuse the event and event dispatcher.
We define our ViewModel as
Code:
using System;
namespace WinformsDataBinding1
{
public class ViewModel:
ObservableObject
{
public DateTime TheDay
{
get { return theDay; }
set
{
if (theDay == value)
return;
theDay = value;
OnPropertyChanged("TheDay");
TheDayAfter = TheDay.AddDays(1);
TheDayAfterIsEnabled = TheDayAfter.Year < 2018;
}
}
public DateTime TheDayAfter
{
get { return theDayAfter; }
private set
{
if (theDayAfter == value)
return;
theDayAfter = value;
OnPropertyChanged("TheDayAfter");
}
}
public bool TheDayAfterIsEnabled
{
get { return theDayAfterIsEnabled; }
private set
{
if (theDayAfterIsEnabled == value)
return;
theDayAfterIsEnabled = value;
OnPropertyChanged("TheDayAfterIsEnabled");
}
}
public bool TheDayAfterIsThisYear
{
get { return TheDayAfter.Year == DateTime.Today.Year; }
}
public bool TheDayAfterIsNextYear
{
get { return TheDayAfter.Year == DateTime.Today.Year + 1; }
}
public string Task
{
get { return task; }
set
{
if (task == value)
return;
task = value;
OnPropertyChanged("Task");
}
}
public bool CanDoTheTask
{
get { return string.IsNullOrEmpty(task) == false; }
}
public void InitialView()
{
TheDay = DateTime.Today;
}
private DateTime theDay;
private DateTime theDayAfter;
private bool theDayAfterIsEnabled = true;
}
}
So the 'UX' behind the form is as follows:
- The user can select a date in edtTheDay.
- edtTheDayAfter displays the date of the next day
- edtTheDayAfter is enabled if the day after is < 2018
- lblNextYear is visibile only if the selected date is in the following year
- btnHappy is enabled only if the selected date is in the following year
- pnlThisYear is green if the selected date is in this year and red otherwise.
- btnDoIt is active only when edtTask is not empty
So this seems like a strange UX but it illustrates the power of databinding and how easy this can be achieved with very little (and simple) code.
This ViewModel has the following properties:
TheDay is a property that is user-editable
Code:
public DateTime TheDay
{
get { return theDay; }
set
{
if (theDay == value)
return;
theDay = value;
OnPropertyChanged("TheDay");
TheDayAfter = TheDay.AddDays(1);
TheDayAfterIsEnabled = TheDayAfter.Year < 2018;
}
}
This is why it has a public getter and setter. The getter is executed when databinding needs the field value for display in the control. The setter is executed when the user changes the value in the control (more about this later). Take note of the setter pattern. Firstly it checks if the value has changed. If not, return. When the new value is diff from the current, update it and raise the event to signal change.
Once this is done, the ViewModel can update other values in the Model as well e.g. TheDayAfter.
TheDayAfter property is never set by the user and its setter is private. It is only set by the VM based on the desired rules.
Code:
public DateTime TheDayAfter
{
get { return theDayAfter; }
private set
{
if (theDayAfter == value)
return;
theDayAfter = value;
OnPropertyChanged("TheDayAfter");
}
}
The setter for TheDayAfter also checks for changes and raises the change event. This is why the setter for TheDay sets the property (executes setter) instead of setting the backing field.
TheDayAfterIsEnabled is also set based on TheDay and the UX rules we have set.
The above 2 properties illustrate one approach to modifying the Model. When these properties are set, they raise the change event.
The next 2 properties follow a different approach. Their values are purely based on the value of TheDay. This means that to display these values,, databinding only needs the getters. The getters return the 'calculated' value of the property based on TheDay.
Code:
public bool TheDayAfterIsThisYear
{
get { return TheDayAfter.Year == DateTime.Today.Year; }
}
public bool TheDayAfterIsNextYear
{
get { return TheDayAfter.Year == DateTime.Today.Year + 1; }
}
The difference in approach is actually quite interesting. In fact all the readonly properties could have followed either the first or the second approach. What I have found is that when you have controls bound to the VM propeties, whenever one property changed event is raised, ALL the bound properties are read. This removes the need to raise an event for change in each read-only property. However I have read somewhere that this was bound to change in that only the property that changed would be read. But somehow that does not seem to be the case yet. By implication, all readonly/dependent properties only then need getters (approach 2). But make sure that your getters are lightweight as they are executed many times. Use lazy loading etc if needed. So when MS implements the reading of only the property that changed, then you need to use approach 1. In C#5 there are mechanisms that mark properties dependent on others that makes databinding a bit more intelligent.
Code:
public string Task
{
get { return task; }
set
{
if (task == value)
return;
task = value;
OnPropertyChanged("Task");
}
}
public bool CanDoTheTask
{
get { return string.IsNullOrEmpty(task) == false; }
}
The CanDoTheTask property is only true when the Task user-editable property is not empty. That is al you need to do!!!
So now we have implemented all our properties that will drive behaviour and present the Model to the View. You can see that the VM knows nothing about the View/presentation. In fact, it does not even import the System.Windows.Forms namespace.
Fuuuuuuuuu... the site threw away huge edits.....!!!!