Récursion terminale en C#

Très mauvaisMauvaisMoyenBonExcellent (2 votes) 
Loading ... Loading ...

Quel que soit le langage de programmation utilisé, certains traitements s’implémentent naturellement sous forme d’un algorithme récursif (même si ce n’est pas toujours la solution la plus optimale). Le problème de l’approche récursive, c’est qu’elle consomme potentiellement beaucoup d’espace sur la pile : à partir d’un certain niveau de “profondeur” de la récursion, l’espace alloué pour la pile d’exécution du thread est épuisé, et on obtient une erreur de type “débordement de la pile” (StackOverflowException en .NET).

(more…)

[Entity Framework] Utiliser Include avec des expressions lambda

Très mauvaisMauvaisMoyenBonExcellent (5 votes) 
Loading ... Loading ...

Je travaille en ce moment sur un projet qui utilise Entity Framework 4. Bien que le lazy loading soit activé, j’utilise généralement la méthode ObjectQuery.Include pour charger les entités associées en une seule fois, de façon à éviter des appels supplémentaires à la base de données lors de l’accès à ces entités :

var query =
    from ord in db.Orders.Include("OrderDetails")
    where ord.Date >= DateTime.Today
    select ord;

Ou encore, pour inclure aussi le produit :

var query =
    from ord in db.Orders.Include("OrderDetails.Product")
    where ord.Date >= DateTime.Today
    select ord;

Il y a quelque chose qui m’ennuie avec cette méthode Include : le fait de devoir spécifier le chemin de la propriété sous forme de chaine de caractères. En effet cette approche présente deux inconvénients majeurs :

  • Elle comporte un risque d’erreur important : on a vite fait de faire une faute de frappe dans le chemin de la propriété, et puisque c’est une chaine de caractères, le compilateur ne remarque rien. On a donc une erreur à l’exécution alors que ça aurait pu être vérifié dès la compilation.
  • On ne profite plus de l’assistance de l’IDE : pas d’intellisense ni de refactoring. Si on renomme une propriété du modèle, le refactoring automatique ne prend pas en compte le contenu des chaines de caractères. Il faut donc aller modifier manuellement les appels à Include qui font référence à cette propriété, avec le risque non négligeable d’en oublier au passage…

Il serait donc plus pratique d’utiliser une expression lambda pour spécifier le chemin de la propriété à inclure. Le principe est connu, et fréquemment utilisé pour éviter de passer une chaine quand on veut spécifier le nom d’une propriété.

Le cas “de base”, dans lequel on ne charge qu’une propriété directement liée à la source, est assez simple à gérer, et on trouve des implémentations un peu partout sur le net. Il suffit d’utiliser une méthode qui extrait le nom de la propriété à partir de l’expression :

    public static class ObjectQueryExtensions
    {
        public static ObjectQuery<T> Include<T>(this ObjectQuery<T> query, Expression<Func<T, object>> selector)
        {
            string propertyName = GetPropertyName(selector);
            return query.Include(propertyName);
        }

        private static string GetPropertyName<T>(Expression<Func<T, object>> expression)
        {
            MemberExpression memberExpr = expression.Body as MemberExpression;
            if (memberExpr == null)
                throw new ArgumentException("Expression body must be a member expression");
            return memberExpr.Member.Name;
        }
    }

En utilisant cette méthode d’extension, on peut réécrire le code du premier exemple de la façon suivante :

var query =
    from ord in db.Orders.Include(o => o.OrderDetails)
    where ord.Date >= DateTime.Today
    select ord;

Ce code fonctionne, mais seulement pour les cas simples… dans le deuxième exemple, on veut aussi inclure la propriété OrderDetail.Product, et le code ci-dessus ne permet pas de gérer ce cas. En effet, l’expression qu’il faudrait écrire pour inclure la propriété Product serait du type o.OrderDetails.Select(od => od.Product), or la méthode GetPropertyName ne sait gérer que les propriétés, pas les appels de méthode…

Pour obtenir le chemin complet de la propriété à inclure, il faut parcourir tout l’arbre d’expression pour en extraire les propriétés. Bien que cela puisse paraitre assez complexe, il existe une classe qui peut nous y aider : ExpressionVisitor. Cette classe, introduite en .NET 4.0, implémente le design pattern Visiteur pour parcourir tous les noeuds de l’arbre. L’implémentation de base ne fait rien de particulier, elle se contente de visiter chaque noeud. Tout ce que nous avons à faire, c’est en hériter pour spécialiser certaines méthodes de façon à extraire les propriétés utilisées dans l’expression. On va donc redéfinir les méthodes suivantes :

  • VisitMember : c’est la méthode appelée pour visiter l’accès à une propriété ou à un champ
  • VisitMethodCall : la méthode appelée pour visiter les appels de méthode. Bien que ce cas ne nous intéresse pas directement a priori, on doit modifier son comportement dans le cas des opérateurs Linq : l’implémentation par défaut visite les paramètres dans l’ordre normal, mais pour les méthodes d’extension comme Select ou SelectMany, on doit visiter le premier paramètre (le paramètre this) en dernier, de façon à conserver l’ordre voulu pour le chemin de la propriété

Voici donc la nouvelle implémentation de la méthode d’extension Include :

    public static class ObjectQueryExtensions
    {
        public static ObjectQuery<T> Include<T>(this ObjectQuery<T> query, Expression<Func<T, object>> selector)
        {
            string path = new PropertyPathVisitor().GetPropertyPath(expression);
            return query.Include(path);
        }

        class PropertyPathVisitor : ExpressionVisitor
        {
            private Stack<string> _stack;

            public string GetPropertyPath(Expression expression)
            {
                _stack = new Stack<string>();
                Visit(expression);
                return _stack
                    .Aggregate(
                        new StringBuilder(),
                        (sb, name) =>
                            (sb.Length > 0 ? sb.Append(".") : sb).Append(name))
                    .ToString();
            }

            protected override Expression VisitMember(MemberExpression expression)
            {
                if (_stack != null)
                    _stack.Push(expression.Member.Name);
                return base.VisitMember(expression);
            }

            protected override Expression VisitMethodCall(MethodCallExpression expression)
            {
                if (IsLinqOperator(expression.Method))
                {
                    for (int i = 1; i < expression.Arguments.Count; i++)
                    {
                        Visit(expression.Arguments[i]);
                    }
                    Visit(expression.Arguments[0]);
                    return expression;
                }
                return base.VisitMethodCall(expression);
            }

            private static bool IsLinqOperator(MethodInfo method)
            {
                if (method.DeclaringType != typeof(Queryable) && method.DeclaringType != typeof(Enumerable))
                    return false;
                return Attribute.GetCustomAttribute(method, typeof(ExtensionAttribute)) != null;
            }
        }
    }

J’ai déjà parlé plus haut de la méthode VisitMethodCall, je ne reviens donc pas dessus. L’implémentation de VisitMember est très simple : on se contente d’ajouter le nom de la propriété sur une pile. Au fait, pourquoi une pile ? Parce que la visite de l’expression ne se déroule pas dans l’ordre auquel on pense intuitivement. Par exemple, dans une expression du type o.OrderDetails.Select(od => od.Product), le premier noeud examiné n’est pas o, mais l’appel à Select, car ce qui précède (o.OrderDetails) est en fait un paramètre de la méthode statique Select… Pour obtenir les propriétés dans l’ordre voulu, on les place donc sur une pile de façon à les relire ensuite dans l’ordre inverse.

La méthode GetPropertyPath est elle aussi assez facile à comprendre : elle initialise la pile, visite l’expression, et reconstitue le chemin à partir de la pile.

On peut donc maintenant réécrire le code du deuxième exemple de la façon suivante :

var query =
    from ord in db.Orders.Include(o => OrderDetails.Select(od => od.Product))
    where ord.Date >= DateTime.Today
    select ord;

Cette méthode fonctionne aussi pour des cas plus complexes. Ajoutons un peu de piment à notre exemple : une ou plusieurs remises peuvent être appliquées à chaque article commandé, et chaque remise est associée à une campagne promotionnelle. Si on veut inclure les remises et les campagnes associées dans les résultats de la requête, on peut écrire quelque chose comme ça :

var query =
    from ord in db.Orders.Include(o => OrderDetails.Select(od => od.Discounts.Select(d => d.Campaign)))
    where ord.Date >= DateTime.Today
    select ord;

Le résultat est le même que si on avait passé à Include le chemin “OrderDetails.Discounts.Campaign”. Comme les Select imbriqués réduisent la lisibilité du code, on peut écrire l’expression un peu différemment :

var query =
    from ord in db.Orders.Include(o => o.OrderDetails
                                        .SelectMany(od => od.Discounts)
                                        .Select(d => d.Campaign))
    where ord.Date >= DateTime.Today
    select ord;

Pour finir, deux remarques sur cette solution :

  • Une méthode d’extension similaire est inclue dans le Entity Framework Feature CTP4 (voir cet article pour plus de détails). Il est donc probable qu’elle finisse par être intégrée dans le Framework (peut-être dans un service pack pour .NET 4.0 ?)
  • Bien que cette solution cible Entity Framework 4.0, il est a priori possible de l’adapter à EF 3.5. La classe ExpressionVisitor n’est pas disponible en 3.5, mais le LINQKit de Joseph Albahari en fournit une implémentation. Je n’ai pas essayé, mais ça devrait fonctionner de la même façon…

[WPF] Une grille simplifiée utilisant des attributs XAML

Très mauvaisMauvaisMoyenBonExcellent (3 votes) 
Loading ... Loading ...

Le composant Grid est l’un des contrôles les plus utilisés en WPF. Il permet de disposer facilement des éléments selon des lignes et des colonnes. Malheureusement le code pour l’utiliser, bien que simple à écrire, est relativement lourd :

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="5"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="60" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <Label Content="Name" Grid.Row="0" Grid.Column="0" />
    <TextBox Text="Hello world" Grid.Row="0" Grid.Column="1"/>
    <Rectangle Fill="Black" Grid.Row="1" Grid.ColumnSpan="2"/>
    <Label Content="Image" Grid.Row="2" Grid.Column="0" />
    <Image Source="Resources/Desert.jpg" Grid.Row="2" Grid.Column="1" />
</Grid>

Dans cet exemple, plus de la moitié du code est constitué de la définition de la grille ! Bien que cette syntaxe offre une certaine souplesse et permette de contrôler assez finement la disposition, dans la plupart des cas on a seulement besoin de définir la hauteur des lignes ou la largeur des colonnes… il serait donc beaucoup plus simple de pouvoir déclarer la grille de cette façon :

<Grid Rows="Auto,5,*" Columns="60,*">
    ...
</Grid>

La suite de cet article démontre comment atteindre précisément ce résultat, en créant une classe SimpleGrid héritée de Grid.

Pour commencer, notre classe aura besoin de deux nouvelles propriétés : Rows et Columns. Ces propriétés définissent respectivement les hauteurs et largeurs des lignes et des colonnes. Ces dimensions ne sont pas de simples nombres : en effet, des valeurs comme "*", "2*" ou "Auto" sont des dimensions valides. Il existe en WPF un type dédié pour représenter ces dimensions : la structure GridLength. Nos deux propriétés seront donc des collections de GridLength. Voilà donc la signature de la classe SimpleGrid :

public class SimpleGrid : Grid
{
    public IList<GridLength> Rows { get; set; }
    public IList<GridLength> Columns { get; set; }
}

Puisque ce sont ces propriétés qui vont contrôler les lignes et colonnes de la grille, il faut qu’elles modifient les RowDefinitions et ColumnDefinitions de la classe de base. Voilà donc comment les implémenter pour obtenir le résultat voulu :

        private IList<GridLength> _rows;
        public IList<GridLength> Rows
        {
            get { return _rows; }
            set
            {
                _rows = value;
                RowDefinitions.Clear();
                if (_rows == null)
                    return;
                foreach (var length in _rows)
                {
                    RowDefinitions.Add(new RowDefinition { Height = length });
                }
            }
        }

        private IList<GridLength> _columns;
        public IList<GridLength> Columns
        {
            get { return _columns; }
            set
            {
                _columns = value;
                ColumnDefinitions.Clear();
                if (_columns == null)
                    return;
                foreach (var length in _columns)
                {
                    ColumnDefinitions.Add(new ColumnDefinition { Width = length });
                }
            }
        }

Notre classe SimpleGrid est d’ores et déjà utilisable… à partir du code C#, ce qui ne nous aide pas beaucoup quand il s’agit de simplifier le code XAML. Il nous faut donc trouver un moyen de déclarer dans un attribut les valeurs de ces propriétés, ce qui n’est pas évident dans la mesure où ce sont des collections…

En XAML, tous les attributs sont écrits sous forme de chaines de caractères. Pour convertir ces chaines en valeurs du type voulu, WPF fait appel à des convertisseurs, qui sont des classes héritées de TypeConverter, associées à chaque type qui supporte les conversions de et vers d’autres types. Par exemple, le type GridLength a pour convertisseur le type GridLengthConverter, qui permet de convertir des nombres ou des chaines de caractères en GridLength, et inversement. Le mécanisme de conversion est décrit dans cet article sur MSDN.

Nous allons donc devoir créer un convertisseur et l’associer au type de nos propriétés. Comme nous n’avons pas la main sur le type IList<T>, nous allons d’abord créer un type spécifique GridLengthCollection qu’on utilisera à la place de IList<GridLength>, et lui associer un convertisseur GridLengthCollectionConverter :

    [TypeConverter(typeof(GridLengthCollectionConverter))]
    public class GridLengthCollection : ReadOnlyCollection<GridLength>
    {
        public GridLengthCollection(IList<GridLength> lengths)
            : base(lengths)
        {
        }
    }

Pourquoi une collection en lecture seule ? Tout bêtement parce que permettre d’ajouter ou de supprimer des lignes ou des colonnes compliquerait l’implémentation, et n’apporterait rien pour l’objectif qui nous intéresse, à savoir simplifier la déclaration de la grille en XAML. Donc, restons dans la simplicité… Pour éviter de réinventer la roue, on hérite de la classe ReadOnlyCollection<T>, qui correspond parfaitement à notre besoin.

Notez aussi l’utilisation de l’attribut TypeConverter : il sert à indiquer le convertisseur associé à un type. Il nous reste donc simplement à implémenter ce convertisseur :

    public class GridLengthCollectionConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            if (sourceType == typeof(string))
                return true;
            return base.CanConvertFrom(context, sourceType);
        }

        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            if (destinationType == typeof(string))
                return true;
            return base.CanConvertTo(context, destinationType);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
        {
            string s = value as string;
            if (s != null)
                return ParseString(s, culture);
            return base.ConvertFrom(context, culture, value);
        }

        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            if (destinationType == typeof(string) && value is GridLengthCollection)
                return ToString((GridLengthCollection)value, culture);
            return base.ConvertTo(context, culture, value, destinationType);
        }

        private string ToString(GridLengthCollection value, CultureInfo culture)
        {
            var converter = new GridLengthConverter();
            return string.Join(",", value.Select(v => converter.ConvertToString(v)));
        }

        private GridLengthCollection ParseString(string s, CultureInfo culture)
        {
            var converter = new GridLengthConverter();
            var lengths = s.Split(',').Select(p => (GridLength)converter.ConvertFromString(p.Trim()));
            return new GridLengthCollection(lengths.ToArray());
        }
    }

Ce convertisseur est capable de convertir une GridLengthCollection de et vers une chaine de caractères, dans laquelle les dimensions sont séparées par des virgules. Notez l’utilisation du convertisseur GridLengthConverter : puisqu’il existe déjà un convertisseur pour les éléments de notre collection, autant s’en servir…

Toutes les pièces du puzzle étant en place, il ne nous reste plus qu’à utiliser notre nouvelle grille simplifiée :

<my:SimpleGrid Rows="Auto,5,*" Columns="60,*">
    <Label Content="Name" Grid.Row="0" Grid.Column="0" />
    <TextBox Text="Hello world" Grid.Row="0" Grid.Column="1"/>
    <Rectangle Fill="Black" Grid.Row="1" Grid.ColumnSpan="2"/>
    <Label Content="Image" Grid.Row="2" Grid.Column="0" />
    <Image Source="Resources/Desert.jpg" Grid.Row="2" Grid.Column="1" />
</my:SimpleGrid>

On obtient donc un résultat beaucoup plus concis et lisible qu’en utilisant une Grid normale, l’objectif est donc atteint :)

On pourrait bien sûr envisager des améliorations, par exemple déclarer les propriétés Rows et Columns comme des DependencyProperty afin de permettre le binding, ou encore gérer l’ajout et la suppression de colonnes. Cependant, cette grille s’adresse à des scénarios simples où la grille est définie une fois pour toutes et n’est pas modifiée à l’exécution (ce qui correspond a priori au cas d’utilisation le plus fréquent), il semble donc plus judicieux de la garder la plus simple possible. Pour des besoins plus spécifiques, par exemple si l’on veut utiliser les propriétés MinWidth, MaxWidth ou encore SharedSizeGroup, il faudra donc revenir à la Grid standard.

Pour référence, voici le code final de la classe SimpleGrid :

    public class SimpleGrid : Grid
    {
        private GridLengthCollection _rows;
        public GridLengthCollection Rows
        {
            get { return _rows; }
            set
            {
                _rows = value;
                RowDefinitions.Clear();
                if (_rows == null)
                    return;
                foreach (var length in _rows)
                {
                    RowDefinitions.Add(new RowDefinition { Height = length });
                }
            }
        }

        private GridLengthCollection _columns;
        public GridLengthCollection Columns
        {
            get { return _columns; }
            set
            {
                _columns = value;
                if (_columns == null)
                    return;
                ColumnDefinitions.Clear();
                foreach (var length in _columns)
                {
                    ColumnDefinitions.Add(new ColumnDefinition { Width = length });
                }
            }
        }
    }
Posted in Code sample, WPF. Tags: , , . 1 Comment »

[C#] Une implémentation du pattern WeakEvent

Très mauvaisMauvaisMoyenBonExcellent (3 votes) 
Loading ... Loading ...

Comme vous le savez peut-être, la mauvaise utilisation des évènements est l’une des principales causes de fuites mémoires dans une application .NET : en effet, un évènement garde des références aux objets qui y sont abonnés (via le delegate), ce qui empêche le garbage collector de collecter ces objets quand ils ne sont plus utilisés. Le problème est particulièrement vrai pour un évènement statique, puisque les références sont conservées pendant toute l’exécution de l’application. Si on crée de nombreux objets qui s’abonnent à un évènement statique et qu’on ne les désabonne pas, ils restent indéfiniment en mémoire, même si on n’en a plus besoin depuis longtemps, ce qui peut finir par saturer la mémoire.

La solution “évidente” au problème est bien sûr de désabonner les objets qui ne sont plus utilisés. Malheureusement, il n’y a pas toujours de moyen simple de savoir à quel moment on peut désabonner un objet. Une autre approche est d’implémenter le pattern WeakEvent, qui permet de ne garder qu’une référence faible vers les objets abonnés à l’évènement, de façon à ne pas empêcher le garbage collector de les collecter. Microsoft inclut dans WPF des éléments pour implémenter le pattern WeakEvent, et explique comment créer ses propres évènements selon ce pattern, à l’aide de la classe WeakEventManager et de l’interface IWeakEventListener. Cependant, cette technique est assez lourde à mettre en œuvre, aussi bien pour exposer un tel évènement (il faut créer une nouvelle classe dédiée) que pour s’abonner à l’évènement (implémentation de IWeakEventListener).

J’ai donc réfléchi à une autre solution, permettant d’implémenter plus facilement le pattern WeakEvent. Ma première idée était d’utiliser une liste de WeakReference pour stocker la liste des delegates abonnés à l’évènement. Malheureusement, lorsqu’on s’abonne à un évènement, on écrit généralement quelque chose comme ça :

myObject.MyEvent += new EventHandler(myObject_MyEvent);

On crée donc un delegate, mais on ne garde aucune référence dessus. Puisque l’évènement ne référence ce delegate que via une WeakReference, rien n’empêche le garbage collector de le collecter… et c’est effectivement ce qui arrive. Au bout d’un temps variable (pas plus de quelques secondes d’après mes observations), le delegate est collecté et n’est donc plus appelé quand l’évènement est déclenché.

Plutôt que de conserver une référence faible vers le delegate lui même, une meilleure solution serait de faire une référence faible sur l’objet qui implémente la méthode (Delegate.Target). J’ai donc créé une classe WeakDelegate<TDelegate> pour gérer cela :

    public class WeakDelegate<TDelegate> : IEquatable<TDelegate>
    {
        private WeakReference _targetReference;
        private MethodInfo _method;

        public WeakDelegate(Delegate realDelegate)
        {
            if (realDelegate.Target != null)
                _targetReference = new WeakReference(realDelegate.Target);
            else
                _targetReference = null;
            _method = realDelegate.Method;
        }

        public TDelegate GetDelegate()
        {
            return (TDelegate)(object)GetDelegateInternal();
        }

        private Delegate GetDelegateInternal()
        {
            if (_targetReference != null)
            {
                return Delegate.CreateDelegate(typeof(TDelegate), _targetReference.Target, _method);
            }
            else
            {
                return Delegate.CreateDelegate(typeof(TDelegate), _method);
            }
        }

        public bool IsAlive
        {
            get { return _targetReference == null || _targetReference.IsAlive; }
        }

        #region IEquatable<TDelegate> Members

        public bool Equals(TDelegate other)
        {
            Delegate d = (Delegate)(object)other;
            return d != null
                && d.Target == _targetReference.Target
                && d.Method.Equals(_method);
        }

        #endregion

        internal void Invoke(params object[] args)
        {
            Delegate handler = (Delegate)(object)GetDelegateInternal();
            handler.DynamicInvoke(args);
        }
    }

Il ne reste plus qu’à gérer une liste de WeakDelegate<TDelegate>, ce que fait la classe WeakEvent<TDelegate> :

    public class WeakEvent<TEventHandler>
    {
        private List<WeakDelegate<TEventHandler>> _handlers;

        public WeakEvent()
        {
            _handlers = new List<WeakDelegate<TEventHandler>>();
        }

        public virtual void AddHandler(TEventHandler handler)
        {
            Delegate d = (Delegate)(object)handler;
            _handlers.Add(new WeakDelegate<TEventHandler>(d));
        }

        public virtual void RemoveHandler(TEventHandler handler)
        {
            // also remove "dead" (garbage collected) handlers
            _handlers.RemoveAll(wd => !wd.IsAlive || wd.Equals(handler));
        }

        public virtual void Raise(object sender, EventArgs e)
        {
            var handlers = _handlers.ToArray();
            foreach (var weakDelegate in handlers)
            {
                if (weakDelegate.IsAlive)
                {
                    weakDelegate.Invoke(sender, e);
                }
                else
                {
                    _handlers.Remove(weakDelegate);
                }
            }
        }

        protected List<WeakDelegate<TEventHandler>> Handlers
        {
            get { return _handlers; }
        }
    }

Cette classe gère automatiquement la suppression des handlers “morts” (collectés), et fournit une méthode Raise pour faciliter le déclenchement de l’évènement. Elle peut s’utiliser de la façon suivante :

        private WeakEvent<EventHandler> _myEvent = new WeakEvent<EventHandler>();
        public event EventHandler MyEvent
        {
            add { _myEvent.AddHandler(value); }
            remove { _myEvent.RemoveHandler(value); }
        }

        protected virtual void OnMyEvent()
        {
            _myEvent.Raise(this, EventArgs.Empty);
        }

C’est un peu plus long à écrire qu’un évènement “classique”, mais ce n’est finalement pas grand chose par rapport aux avantages que ça apporte… D’ailleurs, on peut facilement créer un “code snippet” pour Visual Studio, qui permet de créer un “évènement faible” en un rien de temps, avec seulement 3 informations à renseigner :

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <CodeSnippet Format="1.0.0">
    <Header>
      <Title>wevt</Title>
      <Shortcut>wevt</Shortcut>
      <Description>Code snippet for a weak event</Description>
      <Author>Thomas Levesque</Author>
      <SnippetTypes>
        <SnippetType>Expansion</SnippetType>
      </SnippetTypes>
    </Header>
    <Snippet>
      <Declarations>
        <Literal>
          <ID>type</ID>
          <ToolTip>Event type</ToolTip>
          <Default>EventHandler</Default>
        </Literal>
        <Literal>
          <ID>event</ID>
          <ToolTip>Event name</ToolTip>
          <Default>MyEvent</Default>
        </Literal>
        <Literal>
          <ID>field</ID>
          <ToolTip>Name of the field holding the registered handlers</ToolTip>
          <Default>_myEvent</Default>
        </Literal>
      </Declarations>
      <Code Language="csharp">
        <![CDATA[private WeakEvent<$type$> $field$ = new WeakEvent<EventHandler>();
        public event $type$ $event$
        {
            add { $field$.AddHandler(value); }
            remove { $field$.RemoveHandler(value); }
        }

        protected virtual void On$event$()
        {
            $field$.Raise(this, EventArgs.Empty);
        }
	$end$]]>
      </Code>
    </Snippet>
  </CodeSnippet>
</CodeSnippets>

Ce qui donne dans Visual Studio le résultat suivant :

Code snippet pour implémenter un WeakEvent

Automatiser la vérification des null avec les expressions Linq

Très mauvaisMauvaisMoyenBonExcellent (Pas encore de note) 
Loading ... Loading ...

Le problème

Je suis sûr qu’il vous est déjà arrivé d’écrire ce genre de code :

X x = GetX();
string name = "Default";
if (xx != null && xx.Foo != null && xx.Foo.Bar != null && xx.Foo.Bar.Baz != null)
{
    name = xx.Foo.Bar.Baz.Name;
}

On veut juste obtenir name = xx.Foo.Bar.Baz.Name, mais on est obligé de tester chaque objet intermédiaire pour vérifier qu’il n’est pas nul, ce qui peut vite s’avérer pénible si la propriété voulue est profondément enfouie dans un graphe d’objets…

Une solution

Linq offre une fonctionnalité qui permet (entre autres) de régler ce problème : les expressions. Il est possible, à partir d’une expression lambda, d’obtenir son arbre syntaxique (ou AST : Abstract Syntax Tree), et de faire toutes sortes de manipulations sur cet arbre. On peut également générer dynamiquement un arbre syntaxique, et le compiler pour obtenir un delegate qu’on pourra ensuite exécuter.

Mais quel rapport avec le problème qui nous intéresse ? Eh bien tout simplement, nous allons pouvoir utiliser les expressions Linq pour analyser l’arbre syntaxique correspondant à l’accès à la propriété xx.Foo.Bar.Baz.Name, et réécrire cet arbre de façon à y ajouter des tests de nullité pour chaque objet intermédiaire.

Nous allons donc créer une méthode d’extension NullSafeEval, qui prendra en premier paramètre une expression lambda définissant comment accéder à la propriété voulue, et en deuxième paramètre la valeur par défaut à renvoyer si un objet nul est rencontré en cours de route.

Cette méthode va transformer l’expression xx.Foo.Bar.Baz.Name en ceci :

    (xx == null)
    ? defaultValue
    : (xx.Foo == null)
      ? defaultValue
      : (xx.Foo.Bar == null)
        ? defaultValue
        : (xx.Foo.Bar.Baz == null)
          ? defaultValue
          : xx.Foo.Bar.Baz.Name;

Voici l’implémentation de la méthode NullSafeEval :

        public static TResult NullSafeEval<TSource, TResult>(this TSource source, Expression<Func<TSource, TResult>> expression, TResult defaultValue)
        {
            var safeExp = Expression.Lambda<Func<TSource, TResult>>(
                NullSafeEvalWrapper(expression.Body, Expression.Constant(defaultValue)),
                expression.Parameters[0]);

            var safeDelegate = safeExp.Compile();
            return safeDelegate(source);
        }

        private static Expression NullSafeEvalWrapper(Expression expr, Expression defaultValue)
        {
            Expression obj;
            Expression safe = expr;

            while (!IsNullSafe(expr, out obj))
            {
                var isNull = Expression.Equal(obj, Expression.Constant(null));

                safe =
                    Expression.Condition
                    (
                        isNull,
                        defaultValue,
                        safe
                    );

                expr = obj;
            }
            return safe;
        }

        private static bool IsNullSafe(Expression expr, out Expression nullableObject)
        {
            nullableObject = null;

            if (expr is MemberExpression || expr is MethodCallExpression)
            {
                Expression obj;
                MemberExpression memberExpr = expr as MemberExpression;
                MethodCallExpression callExpr = expr as MethodCallExpression;

                if (memberExpr != null)
                {
                    // Static fields don't require an instance
                    FieldInfo field = memberExpr.Member as FieldInfo;
                    if (field != null && field.IsStatic)
                        return true;

                    // Static properties don't require an instance
                    PropertyInfo property = memberExpr.Member as PropertyInfo;
                    if (property != null)
                    {
                        MethodInfo getter = property.GetGetMethod();
                        if (getter != null && getter.IsStatic)
                            return true;
                    }
                    obj = memberExpr.Expression;
                }
                else
                {
                    // Static methods don't require an instance
                    if (callExpr.Method.IsStatic)
                        return true;

                    obj = callExpr.Object;
                }

                // Value types can't be null
                if (obj.Type.IsValueType)
                    return true;

                // Instance member access or instance method call is not safe
                nullableObject = obj;
                return false;
            }
            return true;
        }

En résumé, ce code remonte l’arbre de l’expression lambda, en encadrant chaque appel à une propriété ou méthode d’instance par une expression conditionnelle (condition ? valeur si vrai : valeur si faux).

Et voilà comment on utilise cette méthode :

string name = xx.NullSafeEval(x => x.Foo.Bar.Baz.Name, "Default");

C’est tout de même plus clair et plus concis que notre code initial :)

Notez que l’implémentation proposée gère non seulement les accès aux propriétés, mais également les appels de méthode, on pourrait donc avoir quelque chose comme ça :

string name = xx.NullSafeEval(x => x.Foo.GetBar(42).Baz.Name, "Default");

Les indexeurs ne sont pas encore gérés, mais pourraient être ajoutés sans grande difficulté ; je vous laisse le soin de le faire si vous en avez l’usage ;)

Limitations

Même si cette solution peut sembler très intéressante au premier abord, lisez la suite avant de vous précipiter pour intégrer ce code dans des programmes réels…

  • Tout d’abord, le code proposé est avant tout un “proof of concept” et n’a pas subi de tests approfondis, sa fiabilité peut donc laisser à désirer.
  • Ensuite, il ne faut pas perdre de vue que la génération dynamique de code à partir d’une expression est très pénalisante pour les performances…

    Une piste possible pour limiter ce problème serait de mettre en cache les delegates obtenus pour chaque expression, de façon à ne pas les regénérer inutilement. Malheureusement, il n’y a pas (à ma connaissance) de moyen simple de comparer deux expressions Linq, ce qui complique sensiblement l’implémentation de ce cache…

  • D’autre part, vous aurez peut-être remarqué que les propriétés et méthodes intermédiaires de l’expression sont évaluées plusieurs fois ; non seulement cela peut avoir un impact non négligeable sur les performances, mais surtout, cela peut causer des effets de bord aux conséquences difficilement prévisibles…

    Une solution possible serait de réécrire l’expression de la façon suivante :

    Foo foo = null;
    Bar bar = null;
    Baz baz = null;
    var name =
        (x == null)
        ? defaultValue
        : ((foo = x.Foo) == null)
          ? defaultValue
          : ((bar = foo.Bar) == null)
            ? defaultValue
            : ((baz = bar.Baz) == null)
              ? defaultValue
              : baz.Name;
    

    Malheureusement, ce n’est pas possible en .NET 3.5 : en effet, cette version ne supporte que des expressions simples, il n’est donc pas possible de déclarer des variables ou de leur affecter une valeur, ni d’écrire plusieurs instructions distinctes. En revanche, en .NET 4.0, le support des expressions Linq a été grandement amélioré, et il est possible de générer ce type de code. J’ai commencé à transformer le code de NullSafeEval pour arriver à ce résultat, mais ça s’avère beaucoup plus complexe que prévu… je publierai la nouvelle méthode si j’arrive à quelque chose de concluant ;)

Au final, je ne recommande donc pas d’utiliser cette technique dans du code réel, en tous cas en l’état actuel. Cela donne cependant un aperçu intéressant des possibilités des expressions Linq. Sachez qu’elle sont également utilisées, entre autres :

  • Pour la génération de requêtes SQL dans des ORM comme Linq to SQL ou Entity Framework
  • Pour construire dynamiquement des prédicats complexes, comme dans la classe PredicateBuilder de Joseph Albahari
  • Pour implementer la “réflexion statique” dont on a beaucoup parlé sur les blogs depuis quelques temps

[C# 4.0] Implémenter un objet dynamique personnalisé

Très mauvaisMauvaisMoyenBonExcellent (Pas encore de note) 
Loading ... Loading ...

Comme vous le savez sans doute déjà si vous vous intéressez à l’actualité de .NET, la future version 4.0 de C#, actuellement en beta, introduit un nouveau type appelé dynamic. Celui ci permet d’accéder à des propriétés ou méthodes d’un objet qui ne sont pas connus statiquement (à la compilation). Ils seront résolus dynamiquement à l’exécution grâce au DLR (Dynamic Language Runtime), qui est l’une des grandes nouveautés de .NET 4.0. Cela permet notamment de faciliter la manipulation d’objets COM, ou de tout autre objet dont on ne connait pas statiquement le type. Pour plus d’informations sur le type dynamic, je vous invite à consulter la documentation MSDN.

En jouant un peu avec la beta de Visual Studio 2010, je me suis rendu compte qu’on pouvait faire des choses très intéressantes avec ce type dynamic… En effet, il est possible de créer ses propres objets dynamiques, en contrôlant comment sont évalués les appels aux membres de l’objet. Il faut pour cela implémenter l’interface IDynamicMetaObjectProvider. Cette interface semble simple à première vue, dans la mesure où elle ne définit qu’un seul membre : la méthode GetMetaObject. Là où ça se complique un peu, c’est pour implémenter cette méthode… Il faut construire un DynamicMetaObject à partir d’une Expression, et j’avoue que je me suis tout d’abord découragé en voyant la complexité de la tâche.

Heureusement, il y a une technique beaucoup plus simple pour implémenter ses propres objets dynamiques : il suffit d’hériter de la classe DynamicObject, qui fournit une implementation de base de IDynamicMetaObjectProvider. Il suffit ensuite de redéfinir les méthodes qui nous intéressent pour obtenir le comportement souhaité.

Pour le premier exemple, on va s’inspirer du langage Javascript, dans lequel il est possible d’ajouter des membres (propriétés ou méthodes) à un objet existant, de la façon suivante :

var x = new Object();
x.Message = "Hello world !";
x.ShowMessage = function()
{
  alert(this.Message);
};
x.ShowMessage();

Ce code crée un objet, lui ajoute une propriété Message en définissant sa valeur, et ajoute également une méthode ShowMessage qui affiche le message.

Dans les précédentes versions de C#, il était impossible de réaliser une telle chose : en effet C# est un langage à typage statique, ce qui implique que l’accès aux membres d’un objet est résolu à la compilation, et non à l’exécution. La classe Object n’ayant pas de propriété Message ou de méthode ShowMessage, on ne peut pas écrire x.Message ou x.ShowMessage(). Et c’est là qu’entre en jeu le type dynamic

Nous allons donc créer un objet dynamique qui permet d’écrire en C# un code similaire au code Javascript ci-dessus. Pour cela, on va stocker dans un Dictionary<string, object> les valeurs des propriétés définies dynamiquement pour l’objet. La clé du fonctionnement de cette classe est de redéfinir les méthodes TryGetMember et TrySetMember. Ces méthodes implementent la logique permettant de lire ou d’écrire un membre de l’objet dynamique. Pour fixer les idées, voici le code, je le commenterai plus loin.

public class MyDynamicObject : DynamicObject
{
    private Dictionary<string, object> _properties = new Dictionary<string, object>();

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        return _properties.TryGetValue(binder.Name, out result);
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        _properties[binder.Name] = value;
        return true;
    }
}

Détaillons maintenant le code ci-dessus… La méthode TryGetMember tente d’obtenir la propriété demandée à partir du dictionnaire. Notez que le nom de la propriété est accessible via la propriété Name du paramètre binder. Si la propriété existe, sa valeur est renvoyée via le paramètre de sortie result et la méthode renvoie true. Dans le cas contraire, la méthode renvoie false, ce qui cause une exception de type RuntimeBinderException à l’endroit où la propriété est accédée en lecture. Cette erreur indique simplement l’échec de la résolution dynamique de la propriété.

La méthode TrySetMember effectue l’opération inverse : elle définit la valeur d’une propriété. Si le membre auquel on veut accéder n’existe pas, il est ajouté au dictionnaire, la méthode renvoie donc toujours true.

Voyons maintenant comment utiliser cet objet :

dynamic x = new MyDynamicObject();
x.Message = "Hello world !";
Console.WriteLine(x.Message);

Ce code fonctionne sans problème et affiche “Hello world !” dans la console… sympa, non ?

Et les méthodes dans tout ça ? Et bien, je pourrais vous dire qu’il faut redéfinir la méthode TryInvokeMember, qui sert à gérer les appels dynamiques de méthodes… Mais en fait, ce n’est même pas nécessaire ! Notre implémentation gère déjà cette fonctionnalité : il suffit d’affecter un delegate à une propriété de l’objet. Ce ne sera donc pas réellement une méthode membre de l’objet, mais plutôt une propriété qui renvoie un Delegate ; mais puisque le code pour invoquer ce delegate est identique à celui de l’appel d’une méthode, on s’en contentera pour l’instant ;) . Voilà un exemple d’ajout d’une méthode à l’objet :

dynamic x = new MyDynamicObject();
x.Message = "Hello world !";
x.ShowMessage = new Action(
    () =>
    {
        Console.WriteLine(x.Message);
    });
x.ShowMessage();

On a donc, à peu de choses près, un code identique au code Javascript qu’on voulait imiter. Et tout ça avec une classe de moins de 10 lignes de code (sans compter les accolades)…

Cette petite classe peut être assez pratique comme objet à tout faire, par exemple pour regrouper des informations sans avoir à créer une classe spécifique. En cela elle est similaire à un type anonyme (déjà présent en C# 3), mais avec l’avantage qu’on peut utiliser l’objet comme valeur de retour d’une méthode, ce qui n’était pas possible avec un type anonyme.

Il est bien sûr possible de créer des objets dynamiques plus utiles que celui-ci… par exemple, voilà un petit wrapper pour un DataRow, pour faciliter l’accès aux champs :

public class DynamicDataRow : DynamicObject
{
    private DataRow _dataRow;

    public DynamicDataRow(DataRow dataRow)
    {
        if (dataRow == null)
            throw new ArgumentNullException("dataRow");
        this._dataRow = dataRow;
    }

    public DataRow DataRow
    {
        get { return _dataRow; }
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        result = null;
        if (_dataRow.Table.Columns.Contains(binder.Name))
        {
            result = _dataRow[binder.Name];
            return true;
        }
        return false;
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        if (_dataRow.Table.Columns.Contains(binder.Name))
        {
            _dataRow[binder.Name] = value;
            return true;
        }
        return false;
    }
}

Ajoutons une petite méthode d’extension pour rendre plus naturelle l’utilisation de ce wrapper :

public static class DynamicDataRowExtensions
{
    public static dynamic AsDynamic(this DataRow dataRow)
    {
        return new DynamicDataRow(dataRow);
    }
}

On peut maintenant écrire un code de ce genre :

DataTable table = new DataTable();
table.Columns.Add("FirstName", typeof(string));
table.Columns.Add("LastName", typeof(string));
table.Columns.Add("DateOfBirth", typeof(DateTime));

dynamic row = table.NewRow().AsDynamic();
row.FirstName = "John";
row.LastName = "Doe";
row.DateOfBirth = new DateTime(1981, 9, 12);
table.Rows.Add(row.DataRow);

// Add more rows...
// ...

var bornInThe20thCentury = from r in table.AsEnumerable()
                           let dr = r.AsDynamic()
                           where dr.DateOfBirth.Year > 1900
                           && dr.DateOfBirth.Year <= 2000
                           select new { dr.LastName, dr.FirstName };

foreach (var item in bornInThe20thCentury)
{
    Console.WriteLine("{0} {1}", item.FirstName, item.LastName);
}

Voilà, maintenant que vous connaissez le principe, vous pouvez donner libre cours à votre imagination :)

Mise à jour : Aussitôt après la publication de cet article, voilà que je tombe sur la classe ExpandoObject, qui fait exactement la même chose que la classe MyDynamicObject ci-dessus… il semblerait donc que j’ai encore réinventé la roue ! Cela dit, il est toujours intéressant de voir le fonctionnement d’un objet dynamique, ne serait-ce qu’à des fins didactiques ;) . Pour plus d’infos sur ExpandoObject, voir ce billet sur le blog C# FAQ

[WPF] Markup extensions et templates

Très mauvaisMauvaisMoyenBonExcellent (Pas encore de note) 
Loading ... Loading ...

Note : Ce billet est la suite de celui sur une markup extension qui met à jour sa cible, et réutilise le même code de départ.

Vous avez peut-être remarqué que l’utilisation d’une markup extension personnalisée dans un template donnait parfois des résultats inattendus… Nous allons voir dans ce billet comment faire une markup extension qui se comporte correctement dans un template.

Illustration du problème

Reprenons l’exemple du précédent billet : une markup extension qui renvoie l’état de la connectivité réseau, et met à jour la propriété cible quand le réseau est connecté ou déconnecté :

<CheckBox IsChecked="{my:NetworkAvailable}" Content="Network is available" />

Mettons maintenant la même CheckBox dans un ControlTemplate :

<ControlTemplate x:Key="test">
  <CheckBox IsChecked="{my:NetworkAvailable}" Content="Network is available" />
</ControlTemplate>

Et créons un contrôle qui utilise ce template :

<Control Template="{StaticResource test}" />

Si on se déconnecte du réseau, on remarque que la CheckBox n’est pas automatiquement mise à jour par la NetworkAvailableExtension, alors que ça fonctionnait bien quand on l’utilisait hors du template…

Explication et solution

La markup expression est évaluée quand elle est rencontrée par le parser XAML : en l’occurrence, lors du parsing du template. Or, à cet instant le contrôle CheckBox n’est pas encore créé, la méthode ProvideValue ne peut donc pas y accéder… Quand une markup extension est évaluée dans un template, le TargetObject est en fait un objet de type System.Windows.SharedDp, qui est une classe interne de WPF.

Pour que la markup extension puisse accéder à sa cible, il faut qu’elle soit évaluée lorsque le template est appliqué : on doit donc retarder son évaluation. Pour y arriver, il suffit en fait de renvoyer l’extension elle-même comme valeur de retour de ProvideValue : de cette façon, elle sera de nouveau évaluée lors de la création du contrôle cible.

Pour savoir si l’extension est appelée pour le template ou pour un contrôle “réel”, il suffit de tester si le type du TargetObject est System.Windows.SharedDp. Le code de la méthode ProvideValue devient donc :

        public sealed override object ProvideValue(IServiceProvider serviceProvider)
        {
            IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            if (target != null)
            {
                if (target.TargetObject.GetType().FullName == "System.Windows.SharedDp")
                    return this;
                _targetObject = target.TargetObject;
                _targetProperty = target.TargetProperty;
            }

            return ProvideValueInternal(serviceProvider);
        }

Voilà, c’est réparé, la CheckBox se met à nouveau à jour en cas de changement de connectivité réseau :)

Encore un os

Mais ne crions pas victoire trop vite, on n’est pas encore tout à fait au bout de nos peines… Que se passe-t-il si on souhaite maintenant utiliser notre ControlTemplate sur plusieurs contrôles ?

<Control Template="{StaticResource test}" />
<Control Template="{StaticResource test}" />

Résultat : la seconde checkbox se met à jour, mais pas la première…

La raison est simple : il y a deux contrôles CheckBox, mais une seule instance de NetworkAvailableExtension, partagée entre toutes les instances du template. Or NetworkAvailableExtension ne peut référencer qu’un seul objet cible, c’est donc le dernier pour lequel ProvideValue a été appelée qui est conservé…

Il suffit donc de gérer non pas un objet cible, mais une collection d’objets cibles, qui seront tous mis à jour dans la méthode UpdateValue. Voilà le code final de la classe de base UpdatableMarkupExtension :

    public abstract class UpdatableMarkupExtension : MarkupExtension
    {
        private List<object> _targetObjects = new List<object>();
        private object _targetProperty;

        protected IEnumerable<object> TargetObjects
        {
            get { return _targetObjects; }
        }

        protected object TargetProperty
        {
            get { return _targetProperty; }
        }

        public sealed override object ProvideValue(IServiceProvider serviceProvider)
        {
            // Retrieve target information
            IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;

            if (target != null && target.TargetObject != null)
            {
                // In a template the TargetObject is a SharedDp (internal WPF class)
                // In that case, the markup extension itself is returned to be re-evaluated later
                if (target.TargetObject.GetType().FullName == "System.Windows.SharedDp")
                    return this;

                // Save target information for later updates
                _targetObjects.Add(target.TargetObject);
                _targetProperty = target.TargetProperty;
            }

            // Delegate the work to the derived class
            return ProvideValueInternal(serviceProvider);
        }

        protected virtual void UpdateValue(object value)
        {
            if (_targetObjects.Count > 0)
            {
                // Update the target property of each target object
                foreach (var target in _targetObjects)
                {
                    if (_targetProperty is DependencyProperty)
                    {
                        DependencyObject obj = target as DependencyObject;
                        DependencyProperty prop = _targetProperty as DependencyProperty;

                        Action updateAction = () => obj.SetValue(prop, value);

                        // Check whether the target object can be accessed from the
                        // current thread, and use Dispatcher.Invoke if it can't

                        if (obj.CheckAccess())
                            updateAction();
                        else
                            obj.Dispatcher.Invoke(updateAction);
                    }
                    else // _targetProperty is PropertyInfo
                    {
                        PropertyInfo prop = _targetProperty as PropertyInfo;
                        prop.SetValue(target, value, null);
                    }
                }
            }
        }

        protected abstract object ProvideValueInternal(IServiceProvider serviceProvider);
    }

La classe UpdatableMarkupExtension est donc maintenant pleinement opérationnelle… jusqu’à preuve du contraire ;) . Cette classe constitue une bonne base pour toute markup extension devant mettre à jour sa cible, sans avoir à se préoccuper des aspects “bas niveau” du suivi des objets cibles et de leur mise à jour.

[WPF] Tri automatique d’un GridView : suite

Très mauvaisMauvaisMoyenBonExcellent (4 votes) 
Loading ... Loading ...

Il y a quelques mois, j’avais publié un billet où j’expliquais comment trier automatiquement un GridView lors du clic sur un en-tête de colonne. J’avais laissé un point ouvert : l’ajout d’un symbole dans l’en-tête de colonne pour indiquer visuellement la direction du tri. C’est maintenant chose faite !

Exemple GridViewSort avec symbole de tri

Exemple GridViewSort avec symbole de tri

Pour arriver à ce résultat, j’ai utilisé un Adorner : c’est un composant qui permet de dessiner “par-dessus” un élément graphique existant, sur une couche de dessin indépendante.

La nouvelle version de la classe GridViewSort peut s’utiliser de la même façon qu’avant, la grille affiche alors des symboles de tri par défaut. Ils ne sont pas particulièrement “jolis”, donc si vous vous sentez une âme d’artiste, vous pouvez fournir vos propres images de la façon suivante :

        <ListView ItemsSource="{Binding Persons}"
                  IsSynchronizedWithCurrentItem="True"
                  util:GridViewSort.AutoSort="True"
                  util:GridViewSort.SortGlyphAscending="/Images/up.png"
                  util:GridViewSort.SortGlyphDescending="/Images/down.png">

Il est aussi possible de désactiver les symboles de tri, en mettant à false la propriété attachée ShowSortGlyph :

        <ListView ItemsSource="{Binding Persons}"
                  IsSynchronizedWithCurrentItem="True"
                  util:GridViewSort.AutoSort="True"
                  util:GridViewSort.ShowSortGlyph="False">

Notez qu’en l’état actuel, la gestion du symbole de tri ne fonctionne qu’en mode de tri automatique (AutoSort = true). Le cas d’un tri personnalisé avec la propriété Command n’est pas encore géré.

Voici le code complet de la nouvelle classe GridViewSort (un peu plus dense que le précédent…) :

using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Documents;

namespace Wpf.Util
{
    public class GridViewSort
    {
        #region Public attached properties

        public static ICommand GetCommand(DependencyObject obj)
        {
            return (ICommand)obj.GetValue(CommandProperty);
        }

        public static void SetCommand(DependencyObject obj, ICommand value)
        {
            obj.SetValue(CommandProperty, value);
        }

        // Using a DependencyProperty as the backing store for Command.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CommandProperty =
            DependencyProperty.RegisterAttached(
                "Command",
                typeof(ICommand),
                typeof(GridViewSort),
                new UIPropertyMetadata(
                    null,
                    (o, e) =>
                    {
                        ItemsControl listView = o as ItemsControl;
                        if (listView != null)
                        {
                            if (!GetAutoSort(listView)) // Don't change click handler if AutoSort enabled
                            {
                                if (e.OldValue != null && e.NewValue == null)
                                {
                                    listView.RemoveHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(ColumnHeader_Click));
                                }
                                if (e.OldValue == null && e.NewValue != null)
                                {
                                    listView.AddHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(ColumnHeader_Click));
                                }
                            }
                        }
                    }
                )
            );

        public static bool GetAutoSort(DependencyObject obj)
        {
            return (bool)obj.GetValue(AutoSortProperty);
        }

        public static void SetAutoSort(DependencyObject obj, bool value)
        {
            obj.SetValue(AutoSortProperty, value);
        }

        // Using a DependencyProperty as the backing store for AutoSort.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AutoSortProperty =
            DependencyProperty.RegisterAttached(
                "AutoSort",
                typeof(bool),
                typeof(GridViewSort),
                new UIPropertyMetadata(
                    false,
                    (o, e) =>
                    {
                        ListView listView = o as ListView;
                        if (listView != null)
                        {
                            if (GetCommand(listView) == null) // Don't change click handler if a command is set
                            {
                                bool oldValue = (bool)e.OldValue;
                                bool newValue = (bool)e.NewValue;
                                if (oldValue && !newValue)
                                {
                                    listView.RemoveHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(ColumnHeader_Click));
                                }
                                if (!oldValue && newValue)
                                {
                                    listView.AddHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(ColumnHeader_Click));
                                }
                            }
                        }
                    }
                )
            );

        public static string GetPropertyName(DependencyObject obj)
        {
            return (string)obj.GetValue(PropertyNameProperty);
        }

        public static void SetPropertyName(DependencyObject obj, string value)
        {
            obj.SetValue(PropertyNameProperty, value);
        }

        // Using a DependencyProperty as the backing store for PropertyName.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PropertyNameProperty =
            DependencyProperty.RegisterAttached(
                "PropertyName",
                typeof(string),
                typeof(GridViewSort),
                new UIPropertyMetadata(null)
            );

        public static bool GetShowSortGlyph(DependencyObject obj)
        {
            return (bool)obj.GetValue(ShowSortGlyphProperty);
        }

        public static void SetShowSortGlyph(DependencyObject obj, bool value)
        {
            obj.SetValue(ShowSortGlyphProperty, value);
        }

        // Using a DependencyProperty as the backing store for ShowSortGlyph.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ShowSortGlyphProperty =
            DependencyProperty.RegisterAttached("ShowSortGlyph", typeof(bool), typeof(GridViewSort), new UIPropertyMetadata(true));

        public static ImageSource GetSortGlyphAscending(DependencyObject obj)
        {
            return (ImageSource)obj.GetValue(SortGlyphAscendingProperty);
        }

        public static void SetSortGlyphAscending(DependencyObject obj, ImageSource value)
        {
            obj.SetValue(SortGlyphAscendingProperty, value);
        }

        // Using a DependencyProperty as the backing store for SortGlyphAscending.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SortGlyphAscendingProperty =
            DependencyProperty.RegisterAttached("SortGlyphAscending", typeof(ImageSource), typeof(GridViewSort), new UIPropertyMetadata(null));

        public static ImageSource GetSortGlyphDescending(DependencyObject obj)
        {
            return (ImageSource)obj.GetValue(SortGlyphDescendingProperty);
        }

        public static void SetSortGlyphDescending(DependencyObject obj, ImageSource value)
        {
            obj.SetValue(SortGlyphDescendingProperty, value);
        }

        // Using a DependencyProperty as the backing store for SortGlyphDescending.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SortGlyphDescendingProperty =
            DependencyProperty.RegisterAttached("SortGlyphDescending", typeof(ImageSource), typeof(GridViewSort), new UIPropertyMetadata(null));

        #endregion

        #region Private attached properties

        private static GridViewColumnHeader GetSortedColumnHeader(DependencyObject obj)
        {
            return (GridViewColumnHeader)obj.GetValue(SortedColumnHeaderProperty);
        }

        private static void SetSortedColumnHeader(DependencyObject obj, GridViewColumnHeader value)
        {
            obj.SetValue(SortedColumnHeaderProperty, value);
        }

        // Using a DependencyProperty as the backing store for SortedColumn.  This enables animation, styling, binding, etc...
        private static readonly DependencyProperty SortedColumnHeaderProperty =
            DependencyProperty.RegisterAttached("SortedColumnHeader", typeof(GridViewColumnHeader), typeof(GridViewSort), new UIPropertyMetadata(null));

        #endregion

        #region Column header click event handler

        private static void ColumnHeader_Click(object sender, RoutedEventArgs e)
        {
            GridViewColumnHeader headerClicked = e.OriginalSource as GridViewColumnHeader;
            if (headerClicked != null && headerClicked.Column != null)
            {
                string propertyName = GetPropertyName(headerClicked.Column);
                if (!string.IsNullOrEmpty(propertyName))
                {
                    ListView listView = GetAncestor<ListView>(headerClicked);
                    if (listView != null)
                    {
                        ICommand command = GetCommand(listView);
                        if (command != null)
                        {
                            if (command.CanExecute(propertyName))
                            {
                                command.Execute(propertyName);
                            }
                        }
                        else if (GetAutoSort(listView))
                        {
                            ApplySort(listView.Items, propertyName, listView, headerClicked);
                        }
                    }
                }
            }
        }

        #endregion

        #region Helper methods

        public static T GetAncestor<T>(DependencyObject reference) where T : DependencyObject
        {
            DependencyObject parent = VisualTreeHelper.GetParent(reference);
            while (!(parent is T))
            {
                parent = VisualTreeHelper.GetParent(parent);
            }
            if (parent != null)
                return (T)parent;
            else
                return null;
        }

        public static void ApplySort(ICollectionView view, string propertyName, ListView listView, GridViewColumnHeader sortedColumnHeader)
        {
            ListSortDirection direction = ListSortDirection.Ascending;
            if (view.SortDescriptions.Count > 0)
            {
                SortDescription currentSort = view.SortDescriptions[0];
                if (currentSort.PropertyName == propertyName)
                {
                    if (currentSort.Direction == ListSortDirection.Ascending)
                        direction = ListSortDirection.Descending;
                    else
                        direction = ListSortDirection.Ascending;
                }
                view.SortDescriptions.Clear();

                GridViewColumnHeader currentSortedColumnHeader = GetSortedColumnHeader(listView);
                if (currentSortedColumnHeader != null)
                {
                    RemoveSortGlyph(currentSortedColumnHeader);
                }
            }
            if (!string.IsNullOrEmpty(propertyName))
            {
                view.SortDescriptions.Add(new SortDescription(propertyName, direction));
                if (GetShowSortGlyph(listView))
                    AddSortGlyph(
                        sortedColumnHeader,
                        direction,
                        direction == ListSortDirection.Ascending ? GetSortGlyphAscending(listView) : GetSortGlyphDescending(listView));
                SetSortedColumnHeader(listView, sortedColumnHeader);
            }
        }

        private static void AddSortGlyph(GridViewColumnHeader columnHeader, ListSortDirection direction, ImageSource sortGlyph)
        {
            AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(columnHeader);
            adornerLayer.Add(
                new SortGlyphAdorner(
                    columnHeader,
                    direction,
                    sortGlyph
                    ));
        }

        private static void RemoveSortGlyph(GridViewColumnHeader columnHeader)
        {
            AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(columnHeader);
            Adorner[] adorners = adornerLayer.GetAdorners(columnHeader);
            if (adorners != null)
            {
                foreach (Adorner adorner in adorners)
                {
                    if (adorner is SortGlyphAdorner)
                        adornerLayer.Remove(adorner);
                }
            }
        }

        #endregion

        #region SortGlyphAdorner nested class

        private class SortGlyphAdorner : Adorner
        {
            private GridViewColumnHeader _columnHeader;
            private ListSortDirection _direction;
            private ImageSource _sortGlyph;

            public SortGlyphAdorner(GridViewColumnHeader columnHeader, ListSortDirection direction, ImageSource sortGlyph)
                : base(columnHeader)
            {
                _columnHeader = columnHeader;
                _direction = direction;
                _sortGlyph = sortGlyph;
            }

            private Geometry GetDefaultGlyph()
            {
                double x1 = _columnHeader.ActualWidth - 13;
                double x2 = x1 + 10;
                double x3 = x1 + 5;
                double y1 = _columnHeader.ActualHeight / 2 - 3;
                double y2 = y1 + 5;

                if (_direction == ListSortDirection.Ascending)
                {
                    double tmp = y1;
                    y1 = y2;
                    y2 = tmp;
                }

                PathSegmentCollection pathSegmentCollection = new PathSegmentCollection();
                pathSegmentCollection.Add(new LineSegment(new Point(x2, y1), true));
                pathSegmentCollection.Add(new LineSegment(new Point(x3, y2), true));

                PathFigure pathFigure = new PathFigure(
                    new Point(x1, y1),
                    pathSegmentCollection,
                    true);

                PathFigureCollection pathFigureCollection = new PathFigureCollection();
                pathFigureCollection.Add(pathFigure);

                PathGeometry pathGeometry = new PathGeometry(pathFigureCollection);
                return pathGeometry;
            }

            protected override void OnRender(DrawingContext drawingContext)
            {
                base.OnRender(drawingContext);

                if (_sortGlyph != null)
                {
                    double x = _columnHeader.ActualWidth - 13;
                    double y = _columnHeader.ActualHeight / 2 - 5;
                    Rect rect = new Rect(x, y, 10, 10);
                    drawingContext.DrawImage(_sortGlyph, rect);
                }
                else
                {
                    drawingContext.DrawGeometry(Brushes.LightGray, new Pen(Brushes.Gray, 1.0), GetDefaultGlyph());
                }
            }
        }

        #endregion
    }
}

Voilà, j’espère que ça vous sera utile :) .

[WPF] Une markup extension qui met à jour sa cible

Très mauvaisMauvaisMoyenBonExcellent (Pas encore de note) 
Loading ... Loading ...

Si vous avez lu mes précédents billets sur le sujet, vous savez que je suis un grand fan des markup extensions… Cependant, celles-ci ont une limitation qui peut s’avérer assez gênante : elles ne sont évaluées qu’une seule fois. Il serait pourtant utile de pouvoir les réévaluer pour mettre à jour la propriété cible, comme pour un binding… Cela peut être utile dans différents cas, notamment :

  • si la valeur de la markup extension peut changer en réponse à un évènement
  • si l’état de l’objet cible quand la markup extension est évaluée ne permet pas encore de déterminer la valeur à renvoyer, et qu’il faut différer l’évaluation (par exemple si l’on a besoin du DataContext de l’objet, et que celui-ci n’est pas encore défini lors de l’évaluation)

Voyons donc comment on peut obtenir le comportement voulu…

La méthode ProvideValue d’une markup extension prend un paramètre de type IServiceProvider, qui fournit, entre autres, un service IProvideValueTarget. Cette interface expose des propriétés TargetObject et TargetProperty, qui permettent d’obtenir l’objet et la propriété cibles de la markup extension. Il est donc possible, si l’on sauvegarde cette information, de mettre à jour la propriété concernée alors que la markup extension a déjà été évaluée.

On va donc créer un classe abstraite UpdatableMarkupExtension, qui sauvegarde l’objet et la propriété cible et fournit une méthode pour mettre à jour la valeur :

    public abstract class UpdatableMarkupExtension : MarkupExtension
    {
        private object _targetObject;
        private object _targetProperty;

        protected object TargetObject
        {
            get { return _targetObject; }
        }

        protected object TargetProperty
        {
            get { return _targetProperty; }
        }

        public sealed override object ProvideValue(IServiceProvider serviceProvider)
        {
            IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            if (target != null)
            {
                _targetObject = target.TargetObject;
                _targetProperty = target.TargetProperty;
            }

            return ProvideValueInternal(serviceProvider);
        }

        protected void UpdateValue(object value)
        {
            if (_targetObject != null)
            {
                if (_targetProperty is DependencyProperty)
                {
                    DependencyObject obj = _targetObject as DependencyObject;
                    DependencyProperty prop = _targetProperty as DependencyProperty;

                    Action updateAction = () =>  obj.SetValue(prop, value);

                    // Check whether the target object can be accessed from the
                    // current thread, and use Dispatcher.Invoke if it can't

                    if (obj.CheckAccess())
                        updateAction();
                    else
                        obj.Dispatcher.Invoke(updateAction);
                }
                else // _targetProperty is PropertyInfo
                {
                    PropertyInfo prop = _targetProperty as PropertyInfo;
                    prop.SetValue(_targetObject, value, null);
                }
            }
        }

        protected abstract object ProvideValueInternal(IServiceProvider serviceProvider);
    }

Comme il est indispensable de sauvegarder l’objet et la propriété cibles, on marque la méthode ProvideValue comme sealed pour qu’elle ne puisse pas être redéfinie, et on définit à la place une méthode abstraite ProvideValueInternal pour que les classes dérivées puissent fournir leur implémentation.

La méthode UpdateValue gère la mise à jour de la propriété cible, qui peut être soit une propriété de dépendance (DependencyProperty), soit une propriété CLR classique (PropertyInfo). Dans le cas d’une DependencyProperty, l’objet cible hérite de DependencyObject, et donc de DispatcherObject : il faut donc s’assurer qu’on n’accède à cet objet qu’à partir du thread qui le possède, à l’aide des méthodes CheckAccess et Invoke.

Voyons maintenant comment utiliser cette classe, au travers d’un exemple simple. Supposons qu’on souhaite réaliser une markup extension qui indique si le réseau est disponible, et qui s’utiliserait de la façon suivante :

<CheckBox IsChecked="{my:NetworkAvailable}" Content="Network is available" />

On souhaite que la checkbox se mette à jour si l’état de la connexion change (cable branché ou débranché, Wifi hors de portée…). Il faut donc gérer l’évènement NetworkChange.NetworkAvailabilityChanged, et mettre à jour la propriété IsChecked en conséquence. Notre extension va donc utiliser les fonctionnalités implémentées dans la classe UpdatableMarkupExtension :

    public class NetworkAvailableExtension : UpdatableMarkupExtension
    {
        public NetworkAvailableExtension()
        {
            NetworkChange.NetworkAvailabilityChanged += new NetworkAvailabilityChangedEventHandler(NetworkChange_NetworkAvailabilityChanged);
        }

        protected override object ProvideValueInternal(IServiceProvider serviceProvider)
        {
            return NetworkInterface.GetIsNetworkAvailable();
        }

        private void NetworkChange_NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
        {
            UpdateValue(e.IsAvailable);
        }
    }

Notez qu’on s’abonne à l’évènement NetworkAvailabilityChanged dans le constructeur de la classe. Si on voulait s’abonner à un évènement de l’objet cible, il faudrait plutôt le faire dans la méthode ProvideValueInternal, pour avoir accès à l’objet cible.

On voit donc qu’il est très simple d’implémenter une markup extension capable de mettre à jour sa cible a posteriori, après la première évaluation. Cela permet d’obtenir un fonctionnement similaire à celui du binding, mais sans être limité aux propriétés de dépendance. J’ai notamment utilisé cette possibilité pour réaliser un système de localisation qui permet de changer la langue “à la volée”, sans redémarrer le programme.

Mise à jour : En l’état actuel, cette markup extension ne supporte pas l’utilisation dans un template. Pour une explication et une solution à ce problème, lisez ce billet.

[C#] Relation parent/enfant et sérialisation XML

Très mauvaisMauvaisMoyenBonExcellent (Pas encore de note) 
Loading ... Loading ...

Me revoilà avec un peu de retard, j’ai un peu manqué de temps libre ces dernières semaines… Voilà donc un petit post pour présenter une idée qui m’est venue récemment. Pour une fois, il ne sera pas question de WPF, c’est de conception C# qu’il s’agit !

Le problème

Il est assez courant, dans un programme, d’avoir un objet parent qui possède une collection d’enfants ayant une référence vers leur parent. C’est le cas, par exemple, des contrôles Windows Forms, qui possèdent une collection de contrôles enfants (Controls), et une propriété qui indique le contrôle parent (Parent).

Ce type de structure est assez simple à réaliser, cela nécessite juste un peu de code pour maintenir la cohérence de la relation. Là où ça se complique un peu, c’est quand on veut sérialiser l’objet parent en XML… Prenons un exemple simple (purement théorique bien sûr) :

    public class Parent
    {
        public Parent()
        {
            this.Children = new List<Child>();
        }

        public string Name { get; set; }

        public List<Child> Children { get; set; }

        public void AddChild(Child child)
        {
            child.ParentObject = this;
            this.Children.Add(child);
        }

        public void RemoveChild(Child child)
        {
            this.Children.Remove(child);
            child.ParentObject = null;
        }
    }
    public class Child
    {
        public string Name { get; set; }

        public Parent ParentObject { get; set; }
    }

Créons une instance de Parent avec quelques enfants, et essayons de la sérialiser en XML :

            Parent p = new Parent { Name = "The parent" };
            p.AddChild(new Child { Name = "First child" });
            p.AddChild(new Child { Name = "Second child" });

            string xml;
            XmlSerializer xs = new XmlSerializer(typeof(Parent));
            using (StringWriter wr = new StringWriter())
            {
                xs.Serialize(wr, p);
                xml = wr.ToString();
            }

            Console.WriteLine(xml);

Quand on sérialise l’objet Parent, il se produit une InvalidOperationException due à une référence circulaire : en effet, le parent référence les enfants, qui référencent le parent, qui référence les enfants… et ainsi de suite. La première solution qui vient à l’esprit est de ne pas sérialiser la propriété Child.ParentObject : il suffit pour cela de lui appliquer l’attribut XmlIgnore. La sérialisation se passe alors sans problème, mais quand on désérialise, mauvaise surprise : la propriété ParentObject n’est pas renseignée, et pour cause, puisqu’elle n’a pas été sauvegardée…

On pourrait opter pour une solution simple : après la désérialisation : parcourir la liste des enfants pour affecter manuellement la propriété ParentObject. Mais ce n’est vraiment pas très élégant… et comme j’aime bien écrire du beau code, j’ai donc cherché autre chose ;)

La solution

La solution qui m’est venue à l’esprit consiste en une collection générique spécialisée ChildItemCollection<P,T>, et une interface IChildItem<P> que les enfants doivent implémenter.

L’interface IChildItem<P> définit simplement une propriété Parent, de type P :

    /// <summary>
    /// Defines the contract for an object that has a parent object
    /// </summary>
    /// <typeparam name="P">Type of the parent object</typeparam>
    public interface IChildItem<P> where P : class
    {
        P Parent { get; set; }
    }

La collection ChildItemCollection<P,T> implémente IList<T> en délégant l’implémentation à une List<T> ou à la collection passée en paramètre du constructeur, et gère le maintien de la relation parent/enfant :

    /// <summary>
    /// Collection of child items. This collection automatically set the
    /// Parent property of the child items when they are added or removed
    /// </summary>
    /// <typeparam name="P">Type of the parent object</typeparam>
    /// <typeparam name="T">Type of the child items</typeparam>
    public class ChildItemCollection<P, T> : IList<T>
        where P : class
        where T : IChildItem<P>
    {
        private P _parent;
        private IList<T> _collection;

        public ChildItemCollection(P parent)
        {
            this._parent = parent;
            this._collection = new List<T>();
        }

        public ChildItemCollection(P parent, IList<T> collection)
        {
            this._parent = parent;
            this._collection = collection;
        }

        #region IList<T> Members

        public int IndexOf(T item)
        {
            return _collection.IndexOf(item);
        }

        public void Insert(int index, T item)
        {
            if (item != null)
                item.Parent = _parent;
            _collection.Insert(index, item);
        }

        public void RemoveAt(int index)
        {
            T oldItem = _collection[index];
            _collection.RemoveAt(index);
            if (oldItem != null)
                oldItem.Parent = null;
        }

        public T this[int index]
        {
            get
            {
                return _collection[index];
            }
            set
            {
                T oldItem = _collection[index];
                if (value != null)
                    value.Parent = _parent;
                _collection[index] = value;
                if (oldItem != null)
                    oldItem.Parent = null;
            }
        }

        #endregion

        #region ICollection<T> Members

        public void Add(T item)
        {
            if (item != null)
                item.Parent = _parent;
            _collection.Add(item);
        }

        public void Clear()
        {
            foreach (T item in _collection)
            {
                if (item != null)
                    item.Parent = null;
            }
            _collection.Clear();
        }

        public bool Contains(T item)
        {
            return _collection.Contains(item);
        }

        public void CopyTo(T[] array, int arrayIndex)
        {
            _collection.CopyTo(array, arrayIndex);
        }

        public int Count
        {
            get { return _collection.Count; }
        }

        public bool IsReadOnly
        {
            get { return _collection.IsReadOnly; }
        }

        public bool Remove(T item)
        {
            bool b = _collection.Remove(item);
            if (item != null)
                item.Parent = null;
            return b;
        }

        #endregion

        #region IEnumerable<T> Members

        public IEnumerator<T> GetEnumerator()
        {
            return _collection.GetEnumerator();
        }

        #endregion

        #region IEnumerable Members

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return (_collection as System.Collections.IEnumerable).GetEnumerator();
        }

        #endregion
    }

Voyons donc comment utiliser cette solution dans notre exemple précédent… Nous allons d’abord modifier la classe Child pour qu’elle implémente l’interface IChildItem<Parent> :

    public class Child : IChildItem<Parent>
    {
        public string Name { get; set; }

        [XmlIgnore]
        public Parent ParentObject { get; internal set; }

        #region IChildItem<Parent> Members

        Parent IChildItem<Parent>.Parent
        {
            get
            {
                return this.ParentObject;
            }
            set
            {
                this.ParentObject = value;
            }
        }

        #endregion
    }

Vous remarquerez qu’on implémente l’interface IChildItem<Parent> de façon explicite : l’intérêt est de “masquer” la propriété Parent, qui ne sera accessible que si on manipule l’objet Child via une variable de type IChildItem<Parent>. On définit aussi l’accesseur set de la propriété ParentObject comme étant internal, de façon à empêcher sa modification à partir d’un autre assembly.

Dans la classe Parent, il suffit de remplacer la List<Child> par une ChildItemCollection<Parent, Child>. On supprime au passage les méthodes AddChild et RemoveChild, qui ne sont plus nécessaires puisque la classe ChildItemCollection<P,T> se charge de renseigner la propriété Parent.

    public class Parent
    {
        public Parent()
        {
            this.Children = new ChildItemCollection<Parent, Child>(this);
        }

        public string Name { get; set; }

        public ChildItemCollection<Parent, Child> Children { get; private set; }
    }

Notez qu’on passe au constructeur de ChildItemCollection<Parent, Child> une référence vers l’objet courant : c’est de cette façon que la collection sait quel doit être le parent de ses éléments.

Le code utilisé précédemment pour sérialiser un Parent fonctionne maintenant correctement. Lors de la désérialisation, la propriété Child.ParentObject n’est pas affectée quand le Child est désérialisé (puisqu’elle a l’attribut XmlIgnore), mais lors de l’ajout à la collection Parent.Children.

Cette solution permet donc de conserver la relation parent/enfant lors de la sérialisation XML, sans recourir à des bricolages peu élégants… Notez cependant une restriction : si la propriété ParentObject est modifiée autrement que par la classe ChildItemCollection<P,T>, la cohérence de la relation parent/enfant est rompue. Il est cependant assez simple d’ajouter à l’accesseur set la logique pour maintenir la cohérence, je ne l’ai pas fait dans cet exemple par souci de clarté et de simplicité.