I. Introduction▲
I-A. But du tutoriel▲
Le but de ce tutoriel est de présenter les possibilités « avancées » du DataBinding telles que l'ajout virtuel de propriétés à une classe.
Nous en profiterons pour présenter l'intérêt de la classe RealProxy.
I-B. Prérequis▲
Pour suivre aisément ce tutoriel, il est conseillé d'avoir déjà utilisé :
- le DataBinding « simple » ;
- les classes génériques
II. Utilisation du PropertyDescriptor▲
Imaginons que vous vouliez travailler sur un objet Worker ayant trois propriétés : FirstName, LastName et BirthDay.
On va maintenant ajouter la classe WorkerFr qui rajoute la propriété SecuNumber.
Jusque-là, pas très difficile. Maintenant, on va créer une classe WorkerList et WorkerFrList avec WorkerFrList qui hérite de WorkerList qui elle-même hérite de BindingList<Worker>.
Et là, c'est le drame. En effet, si on a une Form qui a une DataSource de type WorkerFrList, on ne peut pas se binder sur la propriété SecuNumber. Normal, à aucun moment on ne précise que WorkerFrList est une liste de WorkerFr. On peut dériver les méthodes afin de créer des WorkerFr et non des Worker mais ça ne suffit pas.
Rendre WorkerList générique :
public
class
WorkerList<
WorkerType>
: BindingList<
WorkerType>
{
}
public
class
WorkerFrList : WorkerList<
WorkerFr>
{
}
Cependant, dans une approche de qualité, cette solution n'est pas satisfaisante. En effet, on veut interdire la possibilité de créer directement une WorkerList<WorkerFr> sans passer par WorkerFrList.
Passer par une factory
On peut passer par une factory. Les classes WokerList<WorkerType> et WorkerFrList sont déclarées internal dans un projet Business et on a une classe public qui va instancier pour nous la bonne instance en fonction d'un type passé en paramètre (Worker ou WorkerFr). OK ça marche. Le problème est le suivant : la factory va retourner une interface (IWorkerList). Si on veut utiliser des méthodes spécifiques à WorkerFrList, on ne peut pas le faire ! Une autre solution consiste à déclarer les classes WorkerList et WorkerfrList public mais avec un constructeur internal et avoir une factory par classe. Cette solution est parfaitement viable, mais, vous l'aurez compris, ce n'est pas celle-là que je souhaite exposer ici.
Utiliser un custom PropertyDescriptor
Dans ce cas, WorkerList n'est pas générique.
J'ai vu dans le webcast de Mitsu sur le DataBinding avancé l'utilisation d'un CustomPropertyDescriptor. On va utiliser la réflexion pour pouvoir rajouter automatiquement toutes les propriétés spécifiques à une classe dérivée.
Nous allons définir une classe CustomSource. Cette classe va nous permettre de rajouter "virtuellement" des propriétés sur nos objets. Cette classe hérite de Component pour pouvoir être utilisée depuis le designer. Elle implémente l'interface IListSource. Celle-ci permet de déléguer l'implémentation de IList. Pour connaître le type dérivé et le type de base (WorkerFr, Worker), CustomSourceView sera générique et on lui passera les deux types.
public
class
CustomSource<
BaseType, InheritedType>
: Component, IListSource
{
[...
]
protected
virtual CustomSourceViewBase CreateCustomSourceView
(
)
{
return
new
CustomSourceView<
BaseType, InheritedType>(
this, DataSource, DataMember);
}
[...
]
}
On va définir une classe CustomSourceView qui va faire office de relais entre les contrôles et la source de données. C'est cette classe qui va implémenter IList pour CustomSource On définit une classe de base abstraite : CustomSourceViewBase. On remarquera le Design Pattern Patron sur la méthode GetItemProperties.
public
abstract class
CustomSourceViewBase : IList, ITypedList
{
[...
]
public
PropertyDescriptorCollection GetItemProperties
(
PropertyDescriptor[] listAccessors)
{
if
(
SourceProps !=
null
)
return
SourceProps;
if
(
DataSource ==
null
)
SourceProps =
PropertyDescriptorCollection.Empty
;
return
GetItemPropertiesCore
(
listAccessors);
}
protected
abstract PropertyDescriptorCollection GetItemPropertiesCore
(
PropertyDescriptor[] listAccessors);
[...
]
}
On va faire également une classe de base pour nos dérivés de PropertyDescriptor.
internal abstract class
CustomPropertyDescriptor : PropertyDescriptor
{
[...
]
}
On va ensuite dériver CustomSourceViewBase pour pouvoir se binder sur les propriétés de WorkerFr avec une DataSource de type WorkerFrList. On va également dériver CustomPropertyDescriptor dans une classe appartenant à CustomSourceView.
public
class
CustomSourceView<
BaseType, InheritedType>
: CustomSourceViewBase
{
[...
]
protected
override PropertyDescriptorCollection GetItemPropertiesCore
(
PropertyDescriptor[] listAccessors)
{
object
list =
ListBindingHelper.GetList
(
DataSource);
if
((
list is
ITypedList) &&
! string
.IsNullOrEmpty
(
DataMember))
SourceProps =
ListBindingHelper.GetListItemProperties
(
list, DataMember, listAccessors);
else
SourceProps =
ListBindingHelper.GetListItemProperties
(
list, listAccessors);
PropertyInfo[] inheritedProps =
typeof
(
InheritedType).GetProperties
(
);
PropertyInfo[] baseProps =
typeof
(
BaseType).GetProperties
(
);
List<
PropertyInfo>
newProps =
new
List<
PropertyInfo>(
);
foreach (
PropertyInfo inheritedProp in
inheritedProps)
{
bool found =
false
;
foreach (
PropertyInfo baseProp in
baseProps)
if
(
baseProp.Name
==
inheritedProp.Name
)
{
found =
true
;
break;
}
if
(
!found)
newProps.Add
(
inheritedProp);
}
int
indexProp =
sourceProps.Count
;
PropertyDescriptor[] props =
new
PropertyDescriptor[indexProp +
newProps.Count
];
for
(
int
i =
0
; i <
indexProp; i++
)
props[i] =
sourceProps[i];
foreach (
PropertyInfo newProp in
newProps)
props[indexProp++
] =
new
CustomTypePropertyDescriptor
(
owner, GetType
(
), newProp.Name
, newProp.PropertyType
);
SourceProps =
new
PropertyDescriptorCollection
(
props);
return
SourceProps;
}
[...
]
private
class
CustomTypePropertyDescriptor : CustomPropertyDescriptor
{
[...
]
public
override object
GetValue
(
object
component)
{
if
(
!(
component is
InheritedType))
return
null
;
InheritedType t =
(
InheritedType)component;
if
(
t ==
null
)
return
null
;
return
typeof
(
InheritedType).GetProperty
(
Name).GetValue
(
t, null
);
}
public
override void SetValue
(
object
component, object
value)
{
if
(
!(
component is
InheritedType))
return
;
InheritedType t =
(
InheritedType)component;
if
(
t ==
null
)
return
;
typeof
(
InheritedType).GetProperty
(
Name).SetValue
(
t, value, null
);
}
[...
]
}
}
Puis on rajoute une classe WorkerFrCustomSource qui ne fait qu'hériter de WorkerFrCustomSource :
public
class
WorkerFrCustomSource : CustomSource<
Worker, WorkerFr>
{
}
Il suffit maintenant d'aller dans ma Form, de poser dessus un component de type WorkerFrCustomSource, puis un contrôle BindingSource et un TextBox et on peut maintenant se binder à la propriété SecuNumber :
workerFrCustomSource.DataSource
=
workerFrList;
workerFrBindingSource.DataSource
=
workerFrCustomSource;
secuNumberTextBox.DataBindings.Add
(
"Text"
, workerFrBindingSource, "SecuNumber"
);
Imaginez que vous ayez une propriété BirthDay sur la classe Worker. Vous voulez rajouter la propriété Age (readonly). En revanche, vous ne voulez (ou ne pouvez) pas modifier la classe Worker. Vous pourriez en hériter, mais le C# ne permettant pas l'héritage multiple, vous serez obligé de dupliquer la propriété dans votre dérivé de WorkerFr.
Vous pouvez rajouter "virtuellement" la propriété Age à la classe Worker en y ajoutant un PropertyDescriptor dans la méthode GetItemProperties. Pour cela, vous devez implémenter une classe AgePropertyDescriptor qui hérite de PropertyDescriptor. Cependant, on a maintenant un problème : comment rajouter cette propriété à WorkerFrCustomSource ? Il faudrait avoir un CustomSourceView qui hérite à la fois d'un CustomSourceView dans lequel on rajouterait la propriété âge (utilisée par la classe Worker) et de CustomSourceView<Worker, WorkerFr>. Si vous êtes un maître des design patterns, pas de problème, sinon, petit scarabée, tu vas découvrir le pattern Decorator.
On va de nouveau dériver la classe CustomSourceViewBase. Cette nouvelle classe va permettre de rajouter "virtuellement" la propriété Age :
public
class
WorkerCustomSourceViewDecorator : CustomSourceViewBase
{
[...
]
public
override PropertyDescriptorCollection GetItemProperties
(
PropertyDescriptor[] listAccessors)
{
if
(
SourceProps !=
null
)
return
SourceProps;
PropertyDescriptorCollection basePropertiesDescriptorCollection =
BaseTypedList.GetItemProperties
(
listAccessors);
int
nbBaseProps =
basePropertiesDescriptorCollection.Count
;
PropertyDescriptor[] props =
new
PropertyDescriptor[nbBaseProps +
1
];
for
(
int
indexProp =
0
; indexProp <
nbBaseProps; indexProp++
)
props[indexProp] =
basePropertiesDescriptorCollection[indexProp];
props[nbBaseProps] =
new
AgeWorkerCustomTypePropertyDescriptor
(
BaseTypedList.GetType
(
));
SourceProps =
new
PropertyDescriptorCollection
(
props);
return
SourceProps;
}
[...
]
private
class
AgeWorkerCustomTypePropertyDescriptor : CustomPropertyDescriptor
{
[...
]
///
<
comment>
///
<
exception cref=
"PropertyDescriptorException"
>
PropertyDescriptorException </
exception>
///
</
comment>
public
override object
GetValue
(
object
component)
{
Worker worker =
component as
Worker;
if
(
worker ==
null
)
throw
new
PropertyDescriptorException
(
);
int
yearsOld =
DateTime.Now.Year
-
worker.BirthDay.Year
;
if
(
DateTime.Now.Month
<
worker.BirthDay.Month
)
yearsOld-
-
;
else
if
((
DateTime.Now.Month
==
worker.BirthDay.Month
) &&
(
DateTime.Now.Day
<
worker.BirthDay.Day
))
yearsOld-
-
;
return
yearsOld;
}
[...
]
}
}
public
class
PropertyDescriptorException : ApplicationException
{
[...
]
}
public
class
WorkerFrCustomSourceDecorator : CustomSource<
Worker, WorkerFr>
{
protected
override CustomSourceViewBase CreateCustomSourceView
(
)
{
return
new
WorkerCustomSourceViewDecorator
(
base.CreateCustomSourceView
(
));
}
}
III. Utilisation du RealProxy▲
C'est super, ça marche. Tout n'est cependant pas terminé. Tout d'abord, imaginons que par code, on modifie la valeur d'une propriété "bindée" du patient actuellement affiché. La modification n'est pas visible sur la Form tant que celle-ci n'a pas été rafraichie. Avec le Framework 2.0, Microsoft a simplifié la façon de faire cela. Il suffit d'implémenter l'interface INotifyPropertyChanged. Cette interface n'a qu'un évènement PropertyChanged.
Le problème est qu'il va falloir, sur tous les sets de mes propriétés mettre le code suivant :
public
string
FisrtName
{
get
{ return
fisrtName; }
set
{
if
(
fisrtName !=
value)
{
fisrtName =
value;
OnPropertyChanged
(
"FirstName"
);
}
}
}
protected
void OnPropertyChanged
(
string
propertyName)
{
if
(
PropertyChanged !=
null
)
PropertyChanged
(
this, new
PropertyChangedEventArgs
(
propertyName));
}
Autant dire tout de suite que ça risque d'être un peu pénible et qu'on n'est pas non plus à l'abri d'un mauvais copier/coller. On a alors deux possibilités :
- se créer un snippet qui va faire le travail pour nous ;
- utiliser un RealProxy.
Le snippet a l'avantage d'être plus simple et surtout de permettre de garder du code facilement compréhensible par un développeur débutant.
Le RealProxy a l'avantage de factoriser le code répétitif (ce qui facilitera la maintenance si on veut rajouter une action supplémentaire sur le set).
Le snippet
[...
]
<
Snippet>
<
Declarations>
<
Literal>
<
ID>
type
</
ID>
<
ToolTip>
Property
type
</
ToolTip>
<
Default
>
int
</
Default
>
</
Literal>
<
Literal>
<
ID>
property
</
ID>
<
ToolTip>
Property
name</
ToolTip>
<
Default
>
MyProperty</
Default
>
</
Literal>
<
Literal>
<
ID>
field</
ID>
<
ToolTip>
The variable backing this property
</
ToolTip>
<
Default
>
myVar</
Default
>
</
Literal>
</
Declarations>
<
Code Language=
"csharp"
><
![CDATA[private
$type
$ $field$;
public
$type
$ $property
$
{
get
{ return
$field$;}
set
{
if
(
$field$ !=
value)
{
$field$ =
value;
OnPropertyChanged
(
"$property$"
);
}
}
}
$end
$]] >
[...
]
Le RealProxy La classe RealProxy permet de contrôler l'appel des méthodes d'une classe tout en faisant croire qu'on est une instance de cette classe alors qu'en réalité, on est une instance de __TransparentProxy (classe interne au Framework).
internal class
WorkerProxy<
WorkerType>
: RealProxy where
WorkerType : Worker, new
(
)
{
[...
]
public
static
WorkerType Create
(
)
{
return
CreateProxy
(
new
WorkerType
(
));
}
public
static
WorkerType CreateProxy
(
WorkerType worker)
{
return
(
WorkerType)(
new
WorkerProxy<
WorkerType>(
worker).GetTransparentProxy
(
));
}
public
override IMessage Invoke
(
IMessage msg)
{
IMethodCallMessage methodCallMessage =
(
IMethodCallMessage)msg;
IMethodReturnMessage returnMessage =
null
;
if
(
methodCallMessage.MethodName.StartsWith
(
"set_"
))
{
string
propertyName =
methodCallMessage.MethodName.Remove
(
0
, 4
);
PropertyInfo propertyInfo =
typeof
(
WorkerType).GetProperty
(
propertyName);
object
newValue =
methodCallMessage.Args
[0
];
if
(
propertyInfo !=
null
)
{
object
oldValue =
propertyInfo.GetValue
(
Worker, null
);
if
((
oldValue ==
newValue) || ((
oldValue !=
null
) &&
(
oldValue.Equals
(
newValue))))
return
new
ReturnMessage
(
null
, null
, 0
, methodCallMessage.LogicalCallContext
, methodCallMessage);
Worker worker =
Worker as
Worker;
returnMessage =
RemotingServices.ExecuteMessage
(
worker, methodCallMessage);
worker.OnPropertyChanged
(
propertyName);
}
}
if
(
returnMessage ==
null
)
returnMessage =
RemotingServices.ExecuteMessage
(
Worker, methodCallMessage);
return
returnMessage;
}
[...
]
}
Avec notre RealProxy, la propriété FirstName se résume à ça (le code généré par un Encapsuate Field) :
public
string
FisrtName
{
get
{ return
fisrtName; }
set
{ fisrtName =
value; }
}
Attention avec l'utilisation, il y a un petit bogue avec la classe __TransparentProxy quand on l'utilise comme clé d'un dictionnaire. J'ai fait remonter le bogue à Microsoft. Pour plus d'info sur ce bogue et comment le contourner :
http://blogs.microsoft.fr/mitsufu/archive/2006/02/17/20204.aspx#52360
http://blogs.microsoft.fr/mitsufu/archive/2006/02/17/20204.aspx#52361
http://blogs.microsoft.fr/mitsufu/archive/2006/02/17/20204.aspx#52362
Imaginons maintenant qu'on ait une couche Data (c'est sûrement le cas). J'ai fait le choix d'avoir des objets métier indépendants, c'est-à-dire qu'ils peuvent vivre de façon autonome. Pour cela, on ne respecte pas tout à fait le modèle MVC. En effet, les contrôles sont bindés avec la couche Métier et non avec la couche Data. Les données sont dupliquées (un exemplaire dans la couche Métier et un autre dans la couche Data). Ainsi nos objets métier sont plus indépendants et il n'y aura que la méthode Fill (qui va remplir la liste), AcceptChanges (qui va écrire dans notre BD) et RejectChanges (qui va annuler les modifications dans le DataSet) des listes à changer si on veut supprimer la couche Data.
Le problème, c'est que le Binding n'intervient qu'entre un contrôle et, dans notre cas, un objet métier. Par conséquent, la partie Data n'est pas mise à jour. Comme on travaille en ADO .NET 2.0, on a un DataSet et on travaille en mode déconnecté. On peut donc mettre le DataSet à jour en temps réel sans souci de performances. Pour cela, on va écrire des interfaces IWorker et IWorkerFr dans la couche Data. Ces interfaces ne font qu'exposer les propriétés persistantes en base et rajoutent un évènement PersistantPropertyChanged qui prend en paramètre un PersistantPropertyChangedEventArgs. L'attribut PersistantAttribute permet de spécifier quelles sont la table et la colonne du DataSet utilisées pour stocker notre propriété persistante.
[DebuggerDisplay
(
"{TableName,nq}.{ColumnName,nq}"
)]
public
class
PersistantAttribute : Attribute
{
[...
]
}
public
interface
IWorker
{
[Persistant
(
"Worker"
, "LastName"
)]
string
LastName { get
; set
;}
[Persistant
(
"Worker"
, "FirstName"
)]
string
FirstName { get
; set
;}
event
PersistantPropertyEventHandler PersistantPropertyChanged;
}
public
interface
IWorkerFr : IWorker
{
[Persistant
(
"WorkerFr"
, "SecuNumber"
)]
string
SecuNumber { get
; set
;}
}
public
delegate
void PersistantPropertyEventHandler
(
object
sender, PersistantPropertyEventArgs e);
[DebuggerDisplay
(
"{TableName,nq}.{ColumnName,nq}={Value}"
)]
public
class
PersistantPropertyEventArgs : EventArgs
{
[...
]
}
public
class
PersistantAttributeException : ApplicationException, ISerializable
{
[...
]
}
Il nous suffit alors de nous brancher sur l'événement PersistantPropertyChanged dans la couche Data et de déclencher l'évènement quand on fait un set sur une propriété persistante. Là aussi, on va utiliser le RealProxy. Si vous aviez utilisé un snippet, vous seriez contraint de modifier le code dans toutes vos propriétés (de l'interface).
Dans la classe RealProxy, on va rajouter une propriété static qui va nous donner la liste des interfaces implémentées par notre WorkerType.
private
static
List<
Type
>
IWorkerInterfaceTypes
{
get
{
if
(
iWorkerInterfaceTypes ==
null
)
{
iWorkerInterfaceTypes =
new
List<
Type
>(
);
Type
[] interfaceTypes =
typeof
(
WorkerType).GetInterfaces
(
);
foreach (
Type
interfaceType in
interfaceTypes)
{
Type
[] subInterfaceTypes =
interfaceType.GetInterfaces
(
);
foreach (
Type
subInterfaceType in
subInterfaceTypes)
if
(
subInterfaceType ==
typeof
(
IWorker))
{
iWorkerInterfaceTypes.Add
(
interfaceType);
break;
}
}
}
return
iWorkerInterfaceTypes;
}
}
Ensuite, après avoir lancé la méthode OnPropertyChanged dans la méthode Invoke du Proxy, on lance l'équivalent pour PersistantPropertyChanged.
foreach (
Type
interfaceType in
IWorkerInterfaceTypes)
{
PropertyInfo[] interfacePropertiesInfo =
interfaceType.GetProperties
(
);
foreach (
PropertyInfo interfacePropertyInfo in
interfacePropertiesInfo)
if
(
propertyInfo.Name
==
interfacePropertyInfo.Name
)
{
object
[] propertyAttributes =
interfacePropertyInfo.GetCustomAttributes
(
typeof
(
PersistantAttribute), true
);
switch (
propertyAttributes.Length
)
{
case
0
:
break;
case
1
:
PersistantAttribute persistantAttribute =
(
PersistantAttribute)(
propertyAttributes[0
]);
worker.OnPersistantPropertyChanged
(
persistantAttribute.TableName
, persistantAttribute.ColumnName
, newValue);
break;
default
:
throw
new
PersistantAttributeException
(
);
}
}
}
Le dernier point consiste à créer une classe dans la couche Data qui possède le DataSet, et est capable de remplir une WorkerList (on passera par une interface définie dans la couche Data pour ne pas avoir de références croisées) et s'abonne à l'évènement PersistantPropertyChanged afin de mettre à jour la DataRow dans le DataSet.
Un autre point où le RealProxy peut se révéler particulièrement intéressant, c'est pour la validation de la valeur saisie (par une expression régulière par exemple). C'est ce que je fais sur la vérification du numéro de plaque d'immatriculation dans mon exemple. En effet, avec le DataBinding, on ne peut par récupérer une exception générée dans le set de la propriété (du moins, je ne sais pas comment faire). En passant par notre RealProxy, on va pouvoir faire remonter l'information :
if
(
returnMessage.Exception
!=
null
)
{
if
(
returnMessage.Exception
is
PropertyException)
{
BusinessObject.OnPropertyException
(
BusinessObjectProxy, propertyName, (
PropertyException)(
returnMessage.Exception
));
return
new
ReturnMessage
(
null
, null
, 0
, methodCallMessage.LogicalCallContext
, methodCallMessage); //
sinon, on
ne fait rien
}
throw
returnMessage.Exception
;
}
Vous remarquerez dans mon code les attributs DebuggerBrowsable ou DebuggerDisplay. Si vous ne maîtrisez pas les attributs de type Debugger, je vous conseille les Webcasts de Mitch :
http://www.microsoft.com/france/vision/db/msdn/P00235
http://www.microsoft.com/france/vision/db/msdn/P00236
http://www.microsoft.com/france/events/event.aspx?EventID=118769430
http://www.microsoft.com/france/events/event.aspx?EventID=118769431
http://www.microsoft.com/france/events/event.aspx?EventID=118769432
http://www.microsoft.com/france/events/event.aspx?EventID=118769434
Vous pouvez télécharger les sources ici.