[C#/WPF] BindingList
On va s’écarter un peu de WP7 pour un article qui s’inscrit dans la lignée du “Après 8 ans de .NET, j’en découvre encore”.
Au programme, un problème assez frustrant que j’ai pu rencontrer à plusieurs reprise, dans des applications WPF. Commençons par poser les bases : nous avons une liste d’éléments, avec une propriété numérique (ici, “Count”). Nous voulons afficher ces éléments ainsi que la somme de leur “Count”, et permettre à l’utilisateur de les modifier individuellement (ce qui doit bien entendu mettre à jour la somme).
Pas de grande surprise au niveau du XAML (notez toutefois l’utilisation du fort pratique StringFormat pour le binding) :
1: <Window x:Class="WpfApplication1.MainWindow"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: Title="MainWindow" Height="900" Width="500">
5: <StackPanel>
6: <TextBlock Text="{Binding Path=Total, StringFormat=Total: \{0\}}" />
7:
8: <ItemsControl ItemsSource="{Binding Path=Items}">
9: <ItemsControl.ItemTemplate>
10: <DataTemplate>
11: <TextBox Text="{Binding Path=Count}" />
12: </DataTemplate>
13: </ItemsControl.ItemTemplate>
14: </ItemsControl>
15: </StackPanel>
16: </Window>
Et la déclaration de la classe “Item” :
1: public class Item : INotifyPropertyChanged
2: {
3: private int count;
4:
5: public event PropertyChangedEventHandler PropertyChanged;
6:
7: public int Count
8: {
9: get
10: {
11: return this.count;
12: }
13:
14: set
15: {
16: this.count = value;
17: this.NotifyPropertyChanged("Count");
18: }
19: }
20:
21: protected void NotifyPropertyChanged(string propertyName)
22: {
23: var eventHandler = this.PropertyChanged;
24:
25: if (eventHandler != null)
26: {
27: eventHandler(this, new PropertyChangedEventArgs(propertyName));
28: }
29: }
30: }
Dans le ViewModel, nous stockons les objets dans une ObservableCollection, afin que la vue soit automatiquement notifiée quand un item est ajouté ou retiré :
1: public class ViewModel : INotifyPropertyChanged
2: {
3: public ViewModel()
4: {
5: this.Items = new ObservableCollection<Item>();
6:
7: this.Items.Add(new Item { Count = 1 });
8: this.Items.Add(new Item { Count = 2 });
9: this.Items.Add(new Item { Count = 3 });
10: this.Items.Add(new Item { Count = 4 });
11:
12: this.ComputeSum();
13: }
14:
15: public event PropertyChangedEventHandler PropertyChanged;
16:
17: public ObservableCollection<Item> Items { get; set; }
18:
19: public int Total { get; protected set; }
20:
21: protected void ComputeSum()
22: {
23: this.Total = this.Items.Sum(i => i.Count);
24: this.NotifyPropertyChanged("Total");
25: }
26:
27: protected void NotifyPropertyChanged(string propertyName)
28: {
29: var eventHandler = this.PropertyChanged;
30:
31: if (eventHandler != null)
32: {
33: eventHandler(this, new PropertyChangedEventArgs(propertyName));
34: }
35: }
36: }
A l’exécution, nous avons bien notre liste d’items qui s’affiche, ainsi que le total. Maintenant, comment faire pour qu’il se mette à jour ?
C’est là que la frustration commence. En effet, ObservableCollection permet d’être notifié lorsqu’un élément est ajouté ou supprimé, mais pas lorsqu’une des propriétés des éléments est modifiée, même si ceux-ci implémentent INotifyPropertChanged. Damned, comment ont-ils pu oublier un besoin aussi élémentaire ?
Du coup, comment faire ? Soit créer une nouvelle collection héritée d’ObservableCollection pour combler ce manque, soit faire toute la tambouille d’abonnement aux évènements “PropertyChanged” directement dans le ViewModel. Dans ce cas-ci, par souci de simplicité, optons pour la seconde solution :
1: public class ViewModel : INotifyPropertyChanged
2: {
3: public ViewModel()
4: {
5: this.Items = new ObservableCollection<Item>();
6:
7: this.Items.CollectionChanged += this.Items_CollectionChanged;
8:
9: this.Items.Add(new Item { Count = 1 });
10: this.Items.Add(new Item { Count = 2 });
11: this.Items.Add(new Item { Count = 3 });
12: this.Items.Add(new Item { Count = 4 });
13:
14: this.ComputeSum();
15: }
16:
17: public event PropertyChangedEventHandler PropertyChanged;
18:
19: public ObservableCollection<Item> Items { get; set; }
20:
21: public int Total { get; protected set; }
22:
23: protected void ComputeSum()
24: {
25: this.Total = this.Items.Sum(i => i.Count);
26: this.NotifyPropertyChanged("Total");
27: }
28:
29: protected void NotifyPropertyChanged(string propertyName)
30: {
31: var eventHandler = this.PropertyChanged;
32:
33: if (eventHandler != null)
34: {
35: eventHandler(this, new PropertyChangedEventArgs(propertyName));
36: }
37: }
38:
39: private void Items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
40: {
41: if (e.OldItems != null)
42: {
43: foreach (INotifyPropertyChanged oldItem in e.OldItems)
44: {
45: oldItem.PropertyChanged -= this.ItemPropertyChanged;
46: }
47: }
48:
49: if (e.NewItems != null)
50: {
51: foreach (INotifyPropertyChanged newItem in e.NewItems)
52: {
53: newItem.PropertyChanged += this.ItemPropertyChanged;
54: }
55: }
56:
57: this.ComputeSum();
58: }
59:
60: private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e)
61: {
62: if (e.PropertyName == "Count")
63: {
64: this.ComputeSum();
65: }
66: }
67: }
Ca marche, mais laisse quand même sur sa faim : ce code manque clairement d’élégance.
Mais pas plus tard qu’aujourd’hui, je suis tombé, complètement par hasard, sur une discussion sur StackOverflow décrivant la classe “BindingList”. Il s’agit d’une ObservableCollection en plus puissant, puisqu’elle permet entre autres d’être notifié quand une propriété d’un des éléments est modifiée !
Adaptons donc notre code pour l’utiliser :
1: public class ViewModel : INotifyPropertyChanged
2: {
3: public ViewModel()
4: {
5: this.Items = new BindingList<Item>();
6:
7: this.Items.Add(new Item { Count = 1 });
8: this.Items.Add(new Item { Count = 2 });
9: this.Items.Add(new Item { Count = 3 });
10: this.Items.Add(new Item { Count = 4 });
11:
12: this.ComputeSum();
13:
14: this.Items.ListChanged += this.Items_ListChanged;
15: }
16:
17: public event PropertyChangedEventHandler PropertyChanged;
18:
19: public BindingList<Item> Items { get; set; }
20:
21: public int Total { get; protected set; }
22:
23: protected void ComputeSum()
24: {
25: this.Total = this.Items.Sum(i => i.Count);
26: this.NotifyPropertyChanged("Total");
27: }
28:
29: protected void NotifyPropertyChanged(string propertyName)
30: {
31: var eventHandler = this.PropertyChanged;
32:
33: if (eventHandler != null)
34: {
35: eventHandler(this, new PropertyChangedEventArgs(propertyName));
36: }
37: }
38:
39: private void Items_ListChanged(object sender, ListChangedEventArgs e)
40: {
41: if (e.ListChangedType != ListChangedType.ItemChanged
42: || (e.ListChangedType == ListChangedType.ItemChanged && e.PropertyDescriptor.Name == "Count"))
43: {
44: this.ComputeSum();
45: }
46: }
47: }
Ca marche, et c’est autrement plus classe ;o)
Après vérification, cette classe existe depuis .NET 2.0, rien de neuf donc… Et pourtant !
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 :