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

- Merci par Blog de Jérémy Jeanson le 10-01-2019, 20:47

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01