WinForms, DataBinding et Mises à Jour depuis plusieurs Threads

This post is available in english.

Télécharger ici le code source de cet article.

Lorsque l’on essaye d’appliquer le modèle MVC aux WinForms, on peut se faciliter la vie en utilisant l’interface INotifyPropertyChanged pour faire du DataBinding entre le contrôleur et le formulaire.

On peut donc écrire un contrôleur qui ressemble à ceci :

    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    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"));
}
}
}

Le formulaire est defini comme ceci :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    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();
}
}

Le formulaire va donc mettre à jour le label “labelStatus” lorsque la propriété “Status” du contrôleur change.

Tout ce code est executé dans la thread principale, celle où la pompe à messages du formulaire principal est placée.

 

Une touche d’asynchronisme

Imaginons maintenant que le contrôleur va effectuer un certain nombre d’opérations en asynchrone, par l’intermédiaire d’un timer par exemple.

Modifions le contrôleur en ajoutant ceci :

1
2
3
4
5
6
7
8
9
10
11
        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
);
}

En modifiant le contrôleur de cette manière, on va faire en sorte que la propriété Status soit mise à jour régulièrement.

Le modèle de fonctionnement de System.Threading.Timer fait en sorte que la méthode ChangeStatus est appelée depuis une thread différente de celle qui a créé le formulaire. Ainsi, lorsque le code s’exécute, la mise à jour du label se fait arrêter avec l’exception suivante :

   Cross-thread operation not valid: Control 'labelStatus' accessed from a thread other than the thread it was created on.

La solution est assez simple, il faut faire en sorte que la mise à jour se fasse sur la Thread principale en utilisant la méthode Control.Invoke().

Cela dit dans notre contexte, c'est l'engin de DataBinding qui s'attache à l'événement PropertyChanged. Il faut donc faire en sorte d’appeler l’événement PropertyChanged en le “décorant” d’un appel à la méthode Control.Invoke().

On pourrait modifier le contrôleur pour appeler l’évènement sur la Thread principale :

1
2
3
4
5
6
7
8
    set 
{
_status = value;

// Notify that the property has changed
   Action action = () => PropertyChanged(this, new PropertyChangedEventArgs("Status"));
   _form.Invoke(action);
}

Mais cela impliquerait d’ajouter du code dépendant des WinForms dans le contrôleur, ce qui n’est pas acceptable. Comme on veut être capable de placer ce contrôleur dans un test unitaire, appeler la méthode Control.Invoke() poserait problème, puisqu’il faudrait une instance de formulaire, que l’on aurait pas dans ce contexte.

 

La délégation par interface

L’idée est donc déléguer à la vue (donc au formulaire) la responsabilité de placer l’appel de l’évènement sur la thread principale. On peut le faire par l’intermédiaire d’une interface passée en paramètre du constructeur du contrôleur. Une interface comme ceci :

1
2
3
4
    public interface ISynchronousCall
{
void Invoke(Action a);
}

On fait en sorte que le formulaire l'implémente :

1
2
3
4
5
    void ISynchronousCall.Invoke(Action action)
{
// Call the provided action on the UI Thread using Control.Invoke()
Invoke(action);
}

Puis on va lever l’évènement comme ceci :

1
2
3
    _synchronousInvoker.Invoke(
() => PropertyChanged(this, new PropertyChangedEventArgs("Status"))
);

Mais comme tout bon programmeur efficace (entendez paresseux), on veut éviter d'écrire une interface.

 

La délégation par lambda

On va donc tenter d’utiliser les lambda pour appeler la méthode Control.Invoke(). Pour faire cela, on va modifier le constructeur du contrôleur, et plutôt que de prendre une interface en paramètre, on va écrire :

1
2
3
4
5
    public MyController(Action<Action> synchronousInvoker)
{
_synchronousInvoker = synchronousInvoker;
...
}

En clair, on donne au constructeur une action qui a pour responsabilité d’appeler une action qui lui est passée en paramètre.

Cela permet de construire le contrôleur de cette manière :

1
    _controller = new MyController(a => Invoke(a));

Ici, pas d'interface à implémenter, juste une petite lambda qui fait l'invocation d'une Action sur la Thread GUI. Et on l’utilise comme ceci :

1
2
3
    _synchronousInvoker(
() => PropertyChanged(this, new PropertyChangedEventArgs("Status"))
);

Cela veut donc dire que la lambda spécifiée en paramètre sera appelée par la Thread UI, donc dans le bon contexte pour mettre à jour le label associé.

Le contrôleur est effectivement toujours isolé de la vue, mais adopte malgré tout le comportement de la vue pour les changement de propriétés.

Si l’on voulait utiliser le contrôleur dans un cadre de test unitaire, on le construirait de cette manière :

1
    _controller = new MyController(a => a());

La lambda spécifiée ne se contenterait alors que d'appeler directement l'action en paramètre.

 

Bonus: Faciliter l’écriture du code de notification

Un désavantage de passer INotifyPropertyChanged est qu’il est nécessaire d’écrire le nom de la propriété sous forme de chaine de caractères. C’est assez gênant pour plusieurs raisons, notamment lors de l’utilisation d’outils de refactoring ou d’obfuscation.

C# 3.0 apporte les arbres d’expressions, une fonctionnalité intéressante qui peut être utilisée dans ce contexte. L’idée est d’utiliser les arbres d’expressions pour faire une sorte d’hypothétique “memberof” qui permettrait d’obtenir le MemberInfo d’une propriété, de la même manière que typeof permet d’obtenir le System.Type d’un type.

Voici une petite méthode d’aide à la levée d’évènements :

1
2
3
4
5
6
7
8
9
    private void InvokePropertyChanged<T>(Expression<Func<T>> expr)
{
var body = expr.Body as MemberExpression;

if (body != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(body.Member.Name));
}
}

Méthode que l'on va utiliser comme ceci :

1
2
3
    _synchronousInvoker(
() => InvokePropertyChanged(() => Status)
);

Ainsi la propriété “Status” est utilisée en tant que propriété dans le code, et non en tant que chaine de caractère. Il devient alors facile de la renommer via un outil de refactoring sans casser la logique du code.

Il faut noter que la lambda () => Status n’est jamais exécutée. Elle est simplement analysée par la méthode InvokePropertyChanged comme étant capable de fournir le nom d’une propriété.

 

Le contrôleur en un seul morceau

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    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));
}
}
}

Publié samedi 2 janvier 2010 19:07 par jay
Classé sous , ,
Attachment(s): TestMVC_src.zip
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Commentaires

# re: WinForms, DataBinding et Mises à Jour depuis plusieurs Threads @ dimanche 3 janvier 2010 12:05

Intéressant, trés intéressant, merci.

Graveen


Les 10 derniers blogs postés

- [RIA Services] Include et DomainDataSource par Blog Technique d'Audrey PETIT le il y a 4 heures et 27 minutes

- ZUNE : Version ZUNE Software V 4.2 et la socialisation par Blog Technique de Romelard Fabrice le il y a 5 heures et 52 minutes

- Pratique de Silverlight par Eric Ambrosi par Blog de Frédéric Queudret le il y a 7 heures et 59 minutes

- Apprendre à développer pour les mobiles avec la nouvelle génération .NET par Perspective le il y a 9 heures et 15 minutes

- ZUNE : Nouvelle version du ZUNE Software – V 4.2 par Blog Technique de Romelard Fabrice le il y a 9 heures et 40 minutes

- Nouveau système d'aide pour Visual Studio 2010 : pour ceux qui n'apprécient pas trop l'absence d'index... par CoqBlog le 03-20-2010, 20:05

- L'interface naturelle de Windows Phone 7 Series par Perspective le 03-20-2010, 18:49

- Comment mapper une vue SQL sur une collection de complex type? par Matthieu MEZIL le 03-19-2010, 21:05

- SQL Server : Query Notification ou comment être notifié de modifications de données côté application (SqlDependency) par SQL Server vu par Christian Robert le 03-19-2010, 15:06

- [WF4] Un Binding Activity/ActivityDesigner qui passe mal? par Blog de Jérémy Jeanson le 03-19-2010, 13:42