WinForms, DataBinding and Updates from multiple Threads
Cet article est disponible en francais.
When one is trying to use the MVC model on the WinForms, it is possible to use the INotifyPropertyChanged interface to allow DataBinding between the controler and form.
It is then possible to write a controller like this :
[code:c#]
public class MyController : INotifyPropertyChanged
{
// Register a default handler to avoid having to test for null
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public void ChangeStatus()
{
Status = DateTime.Now.ToString();
}
private string _status;
public string Status
{
get { return _status; }
set
{
_status = value;
// Notify that the property has changed
PropertyChanged(this, new PropertyChangedEventArgs("Status"));
}
}
}
[/code]
The form is defined like this :
[code:c#]
public partial class MyForm : Form
{
private MyController _controller = new MyController();
public MyForm()
{
InitializeComponent();
// Make a link between labelStatus.Text and _controller.Status
labelStatus.DataBindings.Add("Text", _controller, "Status");
}
private void buttonChangeStatus_Click(object sender, EventArgs e)
{
_controller.ChangeStatus();
}
}
[/code]
The form will update the “labelStatus” when the “Status” property of controller changes.
All of this code is executed in the main thread, where the message pump of the main form is located.
A touch of asynchronism
Let’s imagine now that the controller is going to perform some operations asynchronously, using a timer for instance.
We update the controller by adding this :
[code:c#]
private System.Threading.Timer _timer;
public MyController()
{
_timer = new Timer(
d => ChangeStatus(),
null,
TimeSpan.FromSeconds(1), // Start in one second
TimeSpan.FromSeconds(1) // Every second
);
}
[/code]
By altering the controller this way, the “Status” property is going to be updated regularly
The operation model of the System.Threading.Timer implies that the ChangeStatus method is called from a different thread than the thread that created the main form. Thus, when the code is executed, the update of the label is halted by the following exception :
Cross-thread operation not valid: Control 'labelStatus' accessed from a thread other than the thread it was created on.
The solution is quite simple, the update of the UI must be performed on the main thread using Control.Invoke().
That said, in our example, it’s the DataBinding engine that hooks on the PropertyChanged event. We must make sure that the PropertyChanged event is called “decorated” by a call to Control.Invoke().
We could update the controller to invoke the event on the main Thread:
[code:c#]
set
{
_status = value;
// Notify that the property has changed
Action action = () => PropertyChanged(this, new PropertyChangedEventArgs("Status"));
_form.Invoke(action);
}
[/code]
But that would require the addition of WinForms depend code in the controller, which is not acceptable. Since we want to put the controller in a Unit Test, calling the Control.Invoke() method would be problematic, as we would need a Form instance that we would not have in this context.
Delegation by Interface
The idea is to delegate to the view (here the form) the responsibility of placing the call to the event on the main thread. We can do so by using an interface passed as a parameter of the controller’s constructor. It could be an interface like this one :
[code:c#]
public interface ISynchronousCall
{
void Invoke(Action a);
}
[/code]
The form would implement it:
[code:c#]
void ISynchronousCall.Invoke(Action action)
{
// Call the provided action on the UI Thread using Control.Invoke()
Invoke(action);
}
[/code]
We would then raise the event like this :
[code:c#]
_synchronousInvoker.Invoke(
() => PropertyChanged(this, new PropertyChangedEventArgs("Status"))
);
[/code]
But like every efficient programmer (read lazy), we want to avoid writing an interface.
Delegation by Lambda
We will try to use lambda functions to call the method Control.Invoke() method. For this, we will update the constructor of the controller, and instead of taking an interface as a parameter, we will use :
[code:c#]
public MyController(Action<Action> synchronousInvoker)
{
_synchronousInvoker = synchronousInvoker;
...
}
[/code]
To clarify, we give to the constructor an action that has the responsibility to call an action that is passed to it by parameter.
It allows to build the controller like this :
[code:c#]
_controller = new MyController(a => Invoke(a));
[/code]
Here, no need to implement an interface, just pass a small lambda that invokes an actions on the main thread. And it is used like this :
[code:c#]
_synchronousInvoker(
() => PropertyChanged(this, new PropertyChangedEventArgs("Status"))
);
[/code]
This means that the lambda specified as a parameter will be called on the UI Thread, in the proper context to update the associated label.
The controller is still isolated from the view, but adopts anyway the behavior of the view when updating “databound” properties.
If we would have wanted to use the controller in a unit test, it would have been constructed this way :
[code:c#]
_controller = new MyController(a => a());
[/code]
The passed lambda would only need to call the action directly.
Bonus: Easier writing of the notification code
A drawback of using INotifyPropertyChanged is that it is required to write the name of the property as string. This is a problem for many reasons, mainly when using refactoring or obfuscation tools.
C# 3.0 brings expression trees, a pretty interesting feature that can be used in this context. The idea is to use the expression trees to make an hypothetical “memberof” that would get the MemberInfo of a property, much like typeof gets the System.Type of a type.
Here is a small helper method that raises events :
[code:c#]
private void InvokePropertyChanged<T>(Expression<Func<T>> expr)
{
var body = expr.Body as MemberExpression;
if (body != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(body.Member.Name));
}
}
[/code]
A method that can be used like this :
[code:c#]
_synchronousInvoker(
() => InvokePropertyChanged(() => Status)
);
[/code]
The “Status” property is used as a property in the code, not as a string. It is then easier to rename it with a refactoring tool without breaking the code logic.
Note that the lambda () => Status is never called. It is only analyzed by the InvokePropertyChanged method as being able to provide the name of a property.
The Whole Controller
[code:c#]
public class MyController : INotifyPropertyChanged
{
// Register a default handler to avoid having to test for null
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private System.Threading.Timer _timer;
private readonly Action<Action> _synchronousInvoker;
public MyController(Action<Action> synchronousInvoker)
{
_synchronousInvoker = synchronousInvoker
_timer = new Timer(
d => Status = DateTime.Now.ToString(),
null,
1000, // Start in one second
1000 // Every second
);
}
public void ChangeStatus()
{
Status = DateTime.Now.ToString();
}
private string _status;
public string Status
{
get { return _status; }
set
{
_status = value;
// Notify that the property has changed
_synchronousInvoker(
() => InvokePropertyChanged(() => Status)
);
}
}
/// <summary>
/// Raise the PropertyChanged event for the property “get” specified in the expression
/// </summary>
/// <typeparam name="T">The type of the property</typeparam>
/// <param name="expr">The expression to get the property from</param>
private void InvokePropertyChanged<T>(Expression<Func<T>> expr)
{
var body = expr.Body as MemberExpression;
if (body != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(body.Member.Name));
}
}
}
[/code]