DataBinding avancé
Par
Matthieu MEZIL
Tutoriel présentant des notions avancés de DataBinding.
I. Introduction
I-A. But du tutoriel
I-B. Pré-requis
II. Utilisation du PropertyDescriptor
III. Utilisation du RealProxy
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 rajouter la classe WorkerFr qui rajoute la propriété SecuNumber.
Jusque là, pas très difficile. Maintenant, on va crée 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 avez 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 rajoutez "virtuellement" la propriété Age à la classe Worker en la rajoutant 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é par la classe Worker) et de
CustomSourceView<Worker, WorkerFr>. Si vous êtes un maître des design pattern, 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
{
[...]
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éé 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'appelle 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; }
} |
Imaginons maintenant qu'on ait une couche Data (c'est sûrement le cas). J'ai fait le choix d'avoir
des objets métiers 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étiers 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 soucis 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 rajoute un évènement
PersistantPropertyChanged qui prend en paramètre un PersistantPropertyChangedEventArgs. L'attribut
PersistantAttribute permet de spécifier quelle est la table et la colonne du DataSet utilisées pour
stocker notre propriété persistantes.
[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 donné 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, 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);
}
throw returnMessage.Exception;
} |
Vous pouvez télécharger les sources
ici.


Ce document est issu de
http://www.developpez.com et reste la propriété exclusive de son auteur.
La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.