III. Les interfaces INotifyPropertyChanged et INotifyCollectionChanged▲
Observons la définition de cette interface et des types associés :
namespace
System.
ComponentModel
{
public
interface
INotifyPropertyChanged
{
event
PropertyChangedEventHandler PropertyChanged;
}
public
delegate
void
PropertyChangedEventHandler
(
object
sender,
PropertyChangedEventArgs e);
public
class
PropertyChangedEventArgs :
EventArgs
{
public
virtual
string
PropertyName {
get
;
}
public
PropertyChangedEventArgs
(
string
propertyName);
}
}
On voit que cette interface est assez simple à implémenter : ce n'est qu'un évènement. Afin d'avoir plus de confort, il est préférable de créer une méthode de visibilité private ou protected afin de lever l'évènement. On peut ainsi créer une petite application similaire à la précédente.
<Window
x
:
Class
=
"BindingTutorial.WpfApplication.MainWindow"
xmlns
=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns
:
x
=
"http://schemas.microsoft.com/winfx/2006/xaml"
Title
=
"MainWindow"
Height
=
"350"
Width
=
"525"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition />
</Grid.RowDefinitions>
<TextBox
Text
=
"{Binding Name}"
/>
<Button Grid.
Row
=
"1"
Content
=
"Click Me"
Click
=
"ChangeName"
/>
<ListBox Grid.
Row
=
"2"
Height
=
"100"
ItemsSource
=
"{Binding FavoriteMeals}"
/>
<Button Grid.
Row
=
"3"
Content
=
"Click Me"
Click
=
"AddFavoriteMeal"
/>
</Grid>
</Window>
using
System;
using
System.
Collections.
Generic;
using
System.
ComponentModel;
using
System.
Diagnostics;
using
System.
Windows;
namespace
BindingTutorial.
WpfApplication
{
public
partial
class
MainWindow
{
private
readonly
Person _myPerson;
public
MainWindow
(
)
{
InitializeComponent
(
);
_myPerson =
new
Person {
Name =
"Bobby"
,
FavoriteMeals =
new
List<
string
>
{
"Pepperoni Pizza"
}
};
DataContext =
_myPerson;
}
private
void
ChangeName
(
object
sender,
RoutedEventArgs e)
{
_myPerson.
Name =
_myPerson.
Name +
_myPerson.
Name;
Trace.
WriteLine
(
"Changing name"
);
}
private
void
AddFavoriteMeal
(
object
sender,
RoutedEventArgs e)
{
_myPerson.
FavoriteMeals.
Add
(
"Simple Pizza"
);
Trace.
WriteLine
(
"Adding a favorite meal"
);
}
}
public
class
Person :
INotifyPropertyChanged
{
private
string
_name;
private
List<
string
>
_favoriteMeals;
public
String Name
{
get
{
return
_name;
}
set
{
_name =
value
;
RaisePropertyChanged
(
"Name"
);
}
}
public
List<
String>
FavoriteMeals
{
get
{
return
_favoriteMeals;
}
set
{
_favoriteMeals =
value
;
RaisePropertyChanged
(
"FavoriteMeals"
);
}
}
private
void
RaisePropertyChanged
(
String property)
{
if
(
PropertyChanged !=
null
)
PropertyChanged
(
this
,
new
PropertyChangedEventArgs
(
property));
}
public
event
PropertyChangedEventHandler PropertyChanged;
}
}
Au lieu de définir notre fenêtre comme contexte, on utilise cette fois un objet métier. On s'aperçoit donc que l'on implémente l'interface INotifyPropertyChanged et qu'on a implémenté une méthode RaisePropertyChanged pour lever l'évènement. Cette méthode est utilisée à chaque fois qu'on appelle le setter d'une propriété. Avec cette notification, l'interface graphique est capable de se rafraichir et ainsi le Binding de la TextBox reflète ce qu'il y a derrière, dans l'objet métier, y compris si le changement n'est pas initié par une saisie.
Cependant, l'ajout d'un item à notre collection ne fonctionne toujours pas, implémenter l'interface INotifyCollectionChanged est nécessaire. Observons-la de plus près :
namespace
System.
Collections.
Specialized
{
public
interface
INotifyCollectionChanged
{
event
NotifyCollectionChangedEventHandler CollectionChanged;
}
public
delegate
void
NotifyCollectionChangedEventHandler
(
object
sender,
NotifyCollectionChangedEventArgs e);
public
class
NotifyCollectionChangedEventArgs :
EventArgs
{
public
NotifyCollectionChangedEventArgs
(
NotifyCollectionChangedAction action);
public
NotifyCollectionChangedEventArgs
(
NotifyCollectionChangedAction action,
object
changedItem);
public
NotifyCollectionChangedEventArgs
(
NotifyCollectionChangedAction action,
object
changedItem,
int
index);
public
NotifyCollectionChangedEventArgs
(
NotifyCollectionChangedAction action,
IList changedItems);
public
NotifyCollectionChangedEventArgs
(
NotifyCollectionChangedAction action,
IList changedItems,
int
startingIndex);
public
NotifyCollectionChangedEventArgs
(
NotifyCollectionChangedAction action,
object
newItem,
object
oldItem);
public
NotifyCollectionChangedEventArgs
(
NotifyCollectionChangedAction action,
object
newItem,
object
oldItem,
int
index);
public
NotifyCollectionChangedEventArgs
(
NotifyCollectionChangedAction action,
IList newItems,
IList oldItems);
public
NotifyCollectionChangedEventArgs
(
NotifyCollectionChangedAction action,
IList newItems,
IList oldItems,
int
startingIndex);
public
NotifyCollectionChangedEventArgs
(
NotifyCollectionChangedAction action,
object
changedItem,
int
index,
int
oldIndex);
public
NotifyCollectionChangedEventArgs
(
NotifyCollectionChangedAction action,
IList changedItems,
int
index,
int
oldIndex);
public
NotifyCollectionChangedAction Action {
get
;
}
public
IList NewItems {
get
;
}
public
IList OldItems {
get
;
}
public
int
NewStartingIndex {
get
;
}
public
int
OldStartingIndex {
get
;
}
}
public
enum
NotifyCollectionChangedAction
{
Add,
Remove,
Replace,
Move,
Reset,
}
}
On s'aperçoit que bien que l'interface soit simple à implémenter, l'argument à passer pour l'évènement est un peu plus complexe à créer. En effet, cet évènement ne se contente pas de dire que la collection a changé, mais il détaille en quoi elle a changé : qui a disparu, qui est apparu, qui s'est déplacé ou a été remplacé. En bref, c'est une logique assez complexe à implémenter. De plus, il est rarement une bonne idée de vouloir implémenter sa propre collection ! Les collections du framework sont suffisamment puissantes pour la plupart des besoins. C'est d'ailleurs une collection fournie avec le framework qui va nous intéresser : ObservableCollection<T>. Ce type implémente INotifyCollectionChanged mais aussi IEnumerable, IEnumerable<T>, ICollection, ICollection<T>, IList, IList<T> et INotifyPropertyChanged.
Si l'on remplace notre List<String> par une ObservableCollection<String>, on a bien une mise à jour de la ListBox lors du clic sur le second bouton !
public
class
Person :
INotifyPropertyChanged
{
private
string
_name;
private
ObservableCollection<
string
>
_favoriteMeals;
public
String Name
{
get
{
return
_name;
}
set
{
_name =
value
;
RaisePropertyChanged
(
"Name"
);
}
}
public
ObservableCollection<
String>
FavoriteMeals
{
get
{
return
_favoriteMeals;
}
set
{
_favoriteMeals =
value
;
RaisePropertyChanged
(
"FavoriteMeals"
);
}
}
private
void
RaisePropertyChanged
(
String property)
{
if
(
PropertyChanged !=
null
)
PropertyChanged
(
this
,
new
PropertyChangedEventArgs
(
property));
}
public
event
PropertyChangedEventHandler PropertyChanged;
}
IV. Les DependencyProperties▲
Si on se penche d'un peu plus près sur les classes du framework, prenons par exemple un Slider, on s'aperçoit qu'elles n'implémentent pas INotifyPropertyChanged. Pourtant, lorsque l'on fait l'essai suivant, on s'aperçoit que changer la valeur du Slider change le contenu de la TextBox (et inversement).
<Window
x
:
Class
=
"BindingTutorial.WpfApplication.MainWindow"
xmlns
=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns
:
x
=
"http://schemas.microsoft.com/winfx/2006/xaml"
Title
=
"MainWindow"
Height
=
"350"
Width
=
"525"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height
=
"Auto"
/>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBox
x
:
Name
=
"myTextBox"
Text
=
"{Binding ElementName=mySlider, Path=Value, Mode=TwoWay}"
/>
<Slider
x
:
Name
=
"mySlider"
Grid.
Column
=
"1"
/>
</Grid>
</Window>
Ces classes n'implémentant pas INotifyPropertyChanged, elles ont donc besoin d'un autre mécanisme de notification. Cet autre mécanisme vient de l'héritage par les contrôles de la classe DependencyObject. Cependant, bien que faire hériter les objets métier (ou ViewModels en MVVM) de cette classe soit possible, c'est rarement confortable ! Cette classe est plutôt héritée lorsque l'on développe des éléments graphiques.
Voici un exemple de déclaration de DependencyProperty
public
class
MyObject :
DependencyObject
{
public
static
readonly
DependencyProperty MyPropProperty =
DependencyProperty.
Register
(
"MyProp"
,
typeof
(
String),
typeof
(
MyObject),
new
PropertyMetadata
(
default
(
String)));
public
String MyProp
{
get
{
return
(
String)GetValue
(
MyPropProperty);
}
set
{
SetValue
(
MyPropProperty,
value
);
Trace.
WriteLine
(
String.
Format
(
"Set {0}"
,
value
));
}
}
}
On peut voir que la DependencyProperty est un champ statique de la classe et que la propriété associée s'en sert comme paramètre des fonctions statiques GetValue et SetValue de DependencyObject.
Il est également possible de définir des AttachedDependencyProperties, ce sont des DependencyProperties que l'on peut attacher à d'autres DependencyObjects. C'est utile pour ajouter un champ « bindable » à un contrôle du framework par exemple. Plus d'info dans la doc MSDNMSDN Library