DataBinding avancé

Matthieu MEZIL

Tutoriel présentant des notions avancées de DataBinding.

N'hésitez pas à commenter cet article ! Commentez

- Titre : DataBinding avancé

- Auteur : Matthieu MEZIL

- Parution : 26 février 2007

- Mise à jour : 25 janvier 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Matthieu MEZIL. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- 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 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.