Détecter les changements d’une propriété de dépendance dans WinRT

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

Aujourd’hui j’aimerais partager une astuce que j’ai utilisée en développant ma première application Windows Store. Je suis complètement nouveau sur cette technologie et c’est mon premier billet à ce sujet, donc j’espère que je ne vais pas trop me ridiculiser…

Il est souvent utile d’être notifié quand la valeur d’une propriété de dépendance change ; beaucoup de contrôles exposent des évènements à cet effet, mais ce n’est pas toujours le cas. Par exemple, récemment j’essayais de détecter les changements de la propriété Content d’un ContentControl. En WPF, j’aurais utilisé la classe DependencyPropertyDescriptor, mais elle n’est pas disponible dans WinRT.

Heureusement, il y a un mécanisme qui existe sur toutes les plateformes XAML, et qui peut résoudre ce problème: le binding. La solution est donc simplement de créer une classe avec un propriété “bidon” qui est liée à la propriété qu’on souhaite observer, et d’appeler un handler quand la valeur de cette propriété bidon change. Pour rendre ça un peu plus propre et masquer l’implémentation réelle, j’ai emballé ça sous forme d’une méthode d’extension qui renvoie un IDisposable:

    public static class DependencyObjectExtensions
    {
        public static IDisposable WatchProperty(this DependencyObject target,
                                                string propertyPath,
                                                DependencyPropertyChangedEventHandler handler)
        {
            return new DependencyPropertyWatcher(target, propertyPath, handler);
        }

        class DependencyPropertyWatcher : DependencyObject, IDisposable
        {
            private DependencyPropertyChangedEventHandler _handler;

            public DependencyPropertyWatcher(DependencyObject target,
                                             string propertyPath,
                                             DependencyPropertyChangedEventHandler handler)
            {
                if (target == null) throw new ArgumentNullException("target");
                if (propertyPath == null) throw new ArgumentNullException("propertyPath");
                if (handler == null) throw new ArgumentNullException("handler");

                _handler = handler;

                var binding = new Binding
                {
                    Source = target,
                    Path = new PropertyPath(propertyPath),
                    Mode = BindingMode.OneWay
                };
                BindingOperations.SetBinding(this, ValueProperty, binding);
            }

            private static readonly DependencyProperty ValueProperty =
                DependencyProperty.Register(
                    "Value",
                    typeof(object),
                    typeof(DependencyPropertyWatcher),
                    new PropertyMetadata(null, ValuePropertyChanged));

            private static void ValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                var watcher = d as DependencyPropertyWatcher;
                if (watcher == null)
                    return;

                watcher.OnValueChanged(e);
            }

            private void OnValueChanged(DependencyPropertyChangedEventArgs e)
            {
                var handler = _handler;
                if (handler != null)
                    handler(this, e);
            }

            public void Dispose()
            {
                _handler = null;
                // There is no ClearBinding method, so set a dummy binding instead
                BindingOperations.SetBinding(this, ValueProperty, new Binding());
            }
        }
    }

On peut l’utiliser comme ceci:

// Abonnement
watcher = myControl.WatchProperty("Content", myControl_ContentChanged);

// Désabonnement
watcher.Dispose();

J’espère que vous trouverez cela utile!

Utiliser les informations de l’appelant de C# 5 quand on cible une version plus ancienne du .NET Framework

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

Les attributs d’informations de l’appelant (caller info attributes) sont une des nouveautés de C# 5. Ce sont des attributs qui s’appliquent aux paramètres optionnels d’une méthode, et qui permettent de passer implicitement à cette méthode des informations sur l’appelant. Je ne suis pas sûr que cette description soit très claire, voilà donc un exemple pour bien comprendre :

        static void Log(
            string message,
            [CallerMemberName] string memberName = null,
            [CallerFilePath] string filePath = null,
            [CallerLineNumber] int lineNumber = 0)
        {
            Console.WriteLine(
                "[{0:g} - {1} - {2} - line {3}] {4}",
                DateTime.UtcNow,
                memberName,
                filePath,
                lineNumber,
                message);
        }

La méthode ci-dessous prend plusieurs paramètres destinés à passer des informations sur l’appelant : nom du membre (méthode, propriété…) appelant, chemin du fichier source et numéro de ligne. Les attributs Caller* font que le compilateur va automatiquement passer les valeurs appropriées, vous n’avez donc pas besoin de les spécifier vous-même :

        static void Foo()
        {
            Log("Hello world");
            // Equivalent à:
            // Log("Hello world", "Foo", @"C:\x\y\z\Program.cs", 18);
        }

Cela est bien sûr particulièrement utile pour les méthodes de log…

Remarquez que les attributs Caller* attributes sont définis dans le .NET Framework 4.5. Maintenant, supposons que l’on utilise Visual Studio 2012 pour cibler une version plus ancienne du framework (par exemple 4.0) : les attributs d’informations de l’appelant n’existent pas en 4.5, on ne peut donc pas les utiliser… Mais attendez ! Et si on pouvait faire croire au compilateur que ces attributs sont bien présents ? Définissons nos propres attributs, en prenant soin de les placer dans le namespace où le compilateur s’attend à les trouver :

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    public class CallerMemberNameAttribute : Attribute
    {
    }

    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    public class CallerFilePathAttribute : Attribute
    {
    }

    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    public class CallerLineNumberAttribute : Attribute
    {
    }
}

Si on compile et qu’on exécute le programme, on voit que nos attributs personnalisés on bien été pris en compte par le compilateur. Il n’est donc pas nécessaire qu’ils se trouvent dans mscorlib.dll comme les “vrais” attributs d’informations de l’appelant, il suffit qu’ils soient dans le bon namespace, et le compilateur les accepte. Cela nous permet d’utiliser cette fonctionnalité quand on cible .NET 4.0, 3.5 ou même 2.0 !

Remarquez qu’une astuce similaire permet la création de méthodes d’extension quand on cible .NET 2.0 : il suffit de créer une classe ExtensionAttribute dans le namespace System.Runtime.CompilerServices. C’est d’ailleurs ce qui permet à LinqBridge de fonctionner.

Posted in Astuces. Tags: , . 3 Comments »

Les autres nouveautés de Visual Studio 2012

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

Visual Studio 2012 RC est sorti il y a 10 jours, et bien que je n’ai pas encore eu beaucoup de temps pour jouer avec, j’en suis assez satisfait pour l’instant. Beaucoup de choses ont déjà été dites sur le design, ainsi que sur les nouvelles fonctionnalités les plus importantes, mais il y a aussi beaucoup de petites améliorations moins remarquables qui vont nous faciliter la vie. Comme je n’ai pas vu grand chose d’écrit à ce sujet, je me suis dit qu’il serait utile de faire une petite liste de ce que j’ai remarqué jusqu’ici.

Amélioration de Modifier et Continuer: meilleure expérience de débogage avec les méthodes anonymes

Modifier et continuer est une fonctionnalité très utile qui est disponible dans Visual Studio depuis longtemps. C’est génial quand vous devez corriger du code en plein milieu d’une session de débogage pas-à-pas, sans avoir à redémarrer toute l’application. Cette fonctionnalité a toujours eu une limitation : elle n’était pas utilisable dans une méthode contenant une méthode anonyme ou une expression lambda, comme indiqué par ce message d’erreur :

Modifying a ’method’ which contains a lambda expression will prevent the debug session from continuing while Edit and Continue is enabled.

Avant .NET 3.5, ce n’était pas vraiment un problème car les méthodes anonymes n’étaient pas très courantes, mais depuis l’arrivée de Linq et la généralisation des expressions lambda, cette limitation était devenue de plus en plus gênante.

Eh bien, bonne nouvelle : Visual Studio 2012 règle ça ! Vous pouvez maintenant utiliser Modifier et Continuer dans une méthode qui contient des expressions lambda ou des requêtes Linq. Notez que la limitation n’a pas complètement disparu : il n’est toujours pas possible de modifier une instruction qui contient une méthode anonyme. Notez le message légèrement différent :

Modifying a statement which contains a lambda expression will prevent the debug session from continuing while Edit and Continue is enabled.

Mais vous pouvez modifier tout le reste du corps de la méthode, ce qui est une grosse amélioration. Notez que la modification d’une méthode qui contient des types anonymes est également supportée, tant que vous ne modifiez pas le type anonyme lui-même.

Optimisation de l’auto-complétion pour les gestionnaires d’évènements.

Visual Studio a une fonctionnalité d’auto-complétion très utile qui permet de générer un gestionnaire d’évènement automatiquement quand vous tapez += et Tab après un nom d’évènement :

image_thumb9

Mais la partie new EventHandler(…) est redondante, car depuis C# 2, un groupe de méthode est implicitement convertible en un delegate de type compatible. Visual Studio 2012 corrige cela et génère la forme courte :

image3_thumb

OK, c’est un tout petit changement, mais l’ancien comportement m’agaçait beaucoup… J’avais d’ailleurs suggéré cette amélioration sur Connect, donc je suis content de voir qu’elle a été implémentée.

Amélioration de Rechercher/Remplacer

Le dialogue de recherche de Visual Studio n’avait reçu aucune amélioration depuis une éternité (probablement depuis VS2003 ou VS2005), il était donc temps que ça change… Dans VS2012, il a été remplacé par la fonction Quick Find de l’extension Productivity Power Tools. Cela se présente sous forme d’un petit panneau dans le coin en haut à droite de l’éditeur, beaucoup plus discret que l’ancien dialogue :

image_thumb8

Ce panneau offre les fonctionnalités suivantes :

  • recherche incrémentale (les correspondances sont surlignées au fur et à mesure que vous tapez votre recherche)
  • accès rapide aux options telles que Respecter la casse, Mot entier ou Expression régulière, via la liste déroulante du champ de recherche
  • support des expressions régulières .NET (l’ancien dialogue de recherche utilisait un autre type de regex, pas totalement compatible avec la syntaxe des regex .NET)

Si, pour une raison ou une autre, vous voulez utiliser le dialogue de recherche complet, il est toujours là : un élément du menu déroulant du champ de recherche permet d’y accéder.

Lancement rapide

Où est cette commande déjà ? Dans le menu Déboguer ou dans le menu Projet ? Je n’arrive pas à m’en souvenir, et je ne connais pas le raccourci clavier…

Cette réflexion vous est familière ? A moi, oui… Visual Studio a tellement de fonctionnalités qu’il est parfois difficile de trouver ce que vous cherchez. C’est pourquoi la fonctionnalité de Lancement rapide est une nouveauté appréciable; elle se présente sous forme d’une boite de recherche dans le coin en haut à droite de l’EDI, et vous permet de simplement taper ce que vous voulez faire :

image_thumb7

Encore une fois, cette fonctionnalité vient de l’extension Productivity Power Tools, et a été incluse dans Visual Studio lui-même. Elle a aussi reçu une place plus en vue dans l’EDI (dans VS2010 elle était seulement accessible via le raccourci Ctrl+Q).

Un explorateur de solution plus malin

Jusqu’à maintenant, l’explorateur de solution affichait seulement les fichiers qui se trouvaient dans votre projet, mais rien sur le contenu des fichiers. Si vous vouliez voir les classes et membres, il fallait basculer sur la vue des classes. Visual Studio 2012 change cela : vous pouvez maintenant voir ce qui est déclaré dans le fichier, en dépliant simplement le nœud correspondant dans l’explorateur de solution :

image_thumb6

Cette fonctionnalité était présente dans Eclipse depuis longtemps, il était donc temps que Visual Studio rattrape ce retard Clignement d'œil.

Quelques autres fonctionnalités intéressantes du nouvel explorateur de solution :

  • Scope to This : permet de voir seulement l’arborescence à partir d’un nœud spécifique (par exemple si vous voulez voir seulement le contenu d’un dossier). La nouvelle arborescence peut être ouverte dans une vue séparée.
  • Pending changes filter : permet d’afficher uniquement les fichiers modifiés (si le contrôle de source est actif pour le projet)
  • Open files filter : permet de n’afficher que les fichiers actuellement ouverts
  • Sync With Active Document : permet de localiser rapidement le fichier courant dans l’explorateur de solution (très utile pour les grosses solutions)

Une fois de plus, c’est une fonctionnalité qui vient de Productivity Power Tools (elle s’appelait Solution Navigator), avec quelques améliorations.

MENUS EN LETTRES CAPITALES

Les titres des menus de VS2012 sont EN LETTRES CAPITALES, et ce n’est pas vraiment le changement le plus populaire… il y a eu une quantité impressionnante de retours négatifs à ce sujet! Franchement, je suis un peu surpris que les gens attachent autant d’importance à un détail si insignifiant, alors qu’il y a tant d’autres choses plus importantes à discuter. Personnellement, ça m’a un peu troublé quand j’ai vu les premières captures d’écran, mais quand j’ai pu mettre la main sur la RC, j’ai finalement trouvé que ça ne me gênait pas du tout. Je suppose que la plupart des gens finiront par s’y habituer…

Bref, la raison pour laquelle je mentionne ça, c’est simplement pour dire que si vraiment vous n’aimez pas les menus EN LETTRES CAPITALES, vous pouvez les remettre en casse normale avec une simple manipulation du registre. Ouvrez regedit, trouvez la clé HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\11.0\General, créez une valeur DWORD nommée SuppressUppercaseConversion, et mettez sa valeur à 1.

[WPF] Utiliser Linq pour filtrer, trier et grouper les données dans une CollectionView

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

WPF offre un mécanisme assez simple pour la mise en forme de collections de données, via l’interface ICollectionView et ses propriétés Filter, SortDescriptions et GroupDescriptions :

// Collection à laquelle la vue est liée
public ObservableCollection People { get; private set; }
...

// Vue par défaut de la collection People
ICollectionView view = CollectionViewSource.GetDefaultView(People);

// Uniquement les adultes
view.Filter = o => ((Person)o).Age >= 18;

// Tri par nom et prénom
view.SortDescriptions.Add(new SortDescription("LastName", ListSortDirection.Ascending));
view.SortDescriptions.Add(new SortDescription("FirstName", ListSortDirection.Ascending));

// Groupement par pays
view.GroupDescriptions.Add(new PropertyGroupDescription("Country"));

Bien que cette technique ne soit pas très difficile à mettre en œuvre, elle présente certains inconvénients :

  • La syntaxe un peu lourde et pas très naturelle : le fait que le paramètre du filtre soit un object alors qu’on sait que les éléments sont de type Person réduit la lisibilité, et l’ajout des tris et descriptions comporte beaucoup de répétitions
  • Le fait de spécifier les noms des propriétés sous forme de chaine introduit des risques d’erreur, puisqu’ils ne sont pas vérifiés par le compilateur.

Depuis quelques années, on a pris l’habitude d’utiliser Linq pour faire ce genre de choses… il serait donc pratique de pouvoir le faire aussi pour définir le filtre, le tri et le groupement d’une ICollectionView.

Voyons donc quelle syntaxe on pourrait utiliser pour faire ça avec Linq… quelque chose comme ça, par exemple ?

People.Where(p => p.Age >= 18)
      .OrderBy(p => p.LastName)
      .ThenBy(p => p.FirstName)
      .GroupBy(p => p.Country);

Ou encore, en utilisant la syntaxe de requête Linq :

from p in People
where p.Age >= 18
orderby p.LastName, p.FirstName
group p by p.Country;

Bon, évidemment ça ne suffit pas : ce code ne fait rien d’autre que créer une requête Linq sur la collection, il ne modifie pas la CollectionView… mais avec un tout petit peu de travail supplémentaire on peut obtenir le résultat voulu :

var query =
    from p in People.ShapeView()
    where p.Age >= 18
    orderby p.LastName, p.FirstName
    group p by p.Country;

query.Apply();

La méthode ShapeView renvoie un wrapper qui encapsule la vue par défaut de la collection, et expose des méthodes Where, OrderBy et GroupBy avec les signatures appropriées pour définir la mise en forme. Créer la requête n’a pas d’effet direct, c’est la méthode Apply qui permet d’appliquer les changements à la vue : en effet, il vaut mieux tous les appliquer en même temps à l’aide de ICollectionView.DeferRefresh, pour ne provoquer un rafraichissement de la vue à chaque nouvelle clause de la requête. Lors de l’appel à Apply, on observe que la vue est bien mise à jour pour refléter les clauses de la requête.

Cette solution permet de conserver le typage fort pour le filtrage, le tri et le groupement, avec pour bénéfice immédiat la vérification par le compilateur. C’est également plus concis et plus lisible que le code d’origine… Attention quand même à une chose : certaines requêtes qui seront correctes du point de vue de C# ne seront en fait pas applicables à une CollectionView. Par exemple, si vous essayez de grouper par la première lettre du nom (p.LastName.Substring(0, 1)), la méthode GroupBy échouera, car seules les propriétés sont supportées par PropertyGroupDescription.

Notez que le wrapper n’écrase pas les propriétés courantes de la CollectionView si vous ne spécifiez pas la clause Linq correspondante, il est donc possible de modifier une vue existante sans devoir tout spécifier à nouveau. Si nécessaire, des méthodes ClearFilter, ClearSort et ClearGrouping permettent de réinitialiser le filtre, le tri et le regroupement :

// Suppression du regroupement et ajout d'un tri :
People.ShapeView()
      .ClearGrouping()
      .OrderBy(p => p.LastName);
      .Apply();

Notez que comme pour une requête Linq “normale”, on peut au choix utiliser la syntaxe de requête ou appeler directement les méthodes, puisqu’il s’agit simplement d’une transformation syntaxique effectuée par le compilateur.

Pour finir, voici le code complet du wrapper et les méthodes d’extension associées :

    public static class CollectionViewShaper
    {
        public static CollectionViewShaper<TSource> ShapeView<TSource>(this IEnumerable<TSource> source)
        {
            var view = CollectionViewSource.GetDefaultView(source);
            return new CollectionViewShaper<TSource>(view);
        }

        public static CollectionViewShaper<TSource> Shape<TSource>(this ICollectionView view)
        {
            return new CollectionViewShaper<TSource>(view);
        }
    }

    public class CollectionViewShaper<TSource>
    {
        private readonly ICollectionView _view;
        private Predicate<object> _filter;
        private readonly List<SortDescription> _sortDescriptions = new List<SortDescription>();
        private readonly List<GroupDescription> _groupDescriptions = new List<GroupDescription>();

        public CollectionViewShaper(ICollectionView view)
        {
            if (view == null)
                throw new ArgumentNullException("view");
            _view = view;
            _filter = view.Filter;
            _sortDescriptions = view.SortDescriptions.ToList();
            _groupDescriptions = view.GroupDescriptions.ToList();
        }

        public void Apply()
        {
            using (_view.DeferRefresh())
            {
                _view.Filter = _filter;
                _view.SortDescriptions.Clear();
                foreach (var s in _sortDescriptions)
                {
                    _view.SortDescriptions.Add(s);
                }
                _view.GroupDescriptions.Clear();
                foreach (var g in _groupDescriptions)
                {
                    _view.GroupDescriptions.Add(g);
                }
            }
        }

        public CollectionViewShaper<TSource> ClearGrouping()
        {
            _groupDescriptions.Clear();
            return this;
        }

        public CollectionViewShaper<TSource> ClearSort()
        {
            _sortDescriptions.Clear();
            return this;
        }

        public CollectionViewShaper<TSource> ClearFilter()
        {
            _filter = null;
            return this;
        }

        public CollectionViewShaper<TSource> ClearAll()
        {
            _filter = null;
            _sortDescriptions.Clear();
            _groupDescriptions.Clear();
            return this;
        }

        public CollectionViewShaper<TSource> Where(Func<TSource, bool> predicate)
        {
            _filter = o => predicate((TSource)o);
            return this;
        }

        public CollectionViewShaper<TSource> OrderBy<TKey>(Expression<Func<TSource, TKey>> keySelector)
        {
            return OrderBy(keySelector, true, ListSortDirection.Ascending);
        }

        public CollectionViewShaper<TSource> OrderByDescending<TKey>(Expression<Func<TSource, TKey>> keySelector)
        {
            return OrderBy(keySelector, true, ListSortDirection.Descending);
        }

        public CollectionViewShaper<TSource> ThenBy<TKey>(Expression<Func<TSource, TKey>> keySelector)
        {
            return OrderBy(keySelector, false, ListSortDirection.Ascending);
        }

        public CollectionViewShaper<TSource> ThenByDescending<TKey>(Expression<Func<TSource, TKey>> keySelector)
        {
            return OrderBy(keySelector, false, ListSortDirection.Descending);
        }

        private CollectionViewShaper<TSource> OrderBy<TKey>(Expression<Func<TSource, TKey>> keySelector, bool clear, ListSortDirection direction)
        {
            string path = GetPropertyPath(keySelector.Body);
            if (clear)
                _sortDescriptions.Clear();
            _sortDescriptions.Add(new SortDescription(path, direction));
            return this;
        }

        public CollectionViewShaper<TSource> GroupBy<TKey>(Expression<Func<TSource, TKey>> keySelector)
        {
            string path = GetPropertyPath(keySelector.Body);
            _groupDescriptions.Add(new PropertyGroupDescription(path));
            return this;
        }

        private static string GetPropertyPath(Expression expression)
        {
            var names = new Stack<string>();
            var expr = expression;
            while (expr != null && !(expr is ParameterExpression) && !(expr is ConstantExpression))
            {
                var memberExpr = expr as MemberExpression;
                if (memberExpr == null)
                    throw new ArgumentException("The selector body must contain only property or field access expressions");
                names.Push(memberExpr.Member.Name);
                expr = memberExpr.Expression;
            }
            return String.Join(".", names.ToArray());
        }
    }
Posted in WPF. Tags: , , . 1 Comment »

[WPF] Créer des styles paramétrables à l’aide des propriétés attachées

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

Je voudrais aujourd’hui partager avec vous une petite astuce que j’utilise souvent depuis quelques mois. Supposons que pour améliorer l’apparence de votre application, vous ayez créé des styles personnalisés pour les contrôles standards :

Bon, je ne suis pas designer, hein… mais ça fera parfaitement l’affaire pour illustrer mon propos ;) . Ces styles sont très simples, ce sont les styles par défaut des CheckBox et RadioButton dans lesquels j’ai seulement modifié les templates pour remplacer les BulletChrome par ces superbes marques bleues. Voilà le code :

        <Style x:Key="{x:Type CheckBox}" TargetType="{x:Type CheckBox}">
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
            <Setter Property="Background" Value="{StaticResource CheckBoxFillNormal}"/>
            <Setter Property="BorderBrush" Value="{StaticResource CheckBoxStroke}"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="FocusVisualStyle" Value="{StaticResource EmptyCheckBoxFocusVisual}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type CheckBox}">
                        <BulletDecorator Background="Transparent"
                                         SnapsToDevicePixels="true">
                            <BulletDecorator.Bullet>
                                <Border BorderBrush="{TemplateBinding BorderBrush}"
                                        Background="{TemplateBinding Background}"
                                        BorderThickness="1"
                                        Width="11" Height="11" Margin="0,1,0,0">
                                    <Grid>
                                        <Path Name="TickMark"
                                              Fill="Blue"
                                              Data="M0,4 5,9 9,0 4,5"
                                              Visibility="Hidden" />
                                        <Rectangle Name="IndeterminateMark"
                                                   Fill="Blue"
                                                   Width="7" Height="7"
                                                   HorizontalAlignment="Center"
                                                   VerticalAlignment="Center"
                                                   Visibility="Hidden" />
                                    </Grid>
                                </Border>
                            </BulletDecorator.Bullet>
                            <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                              Margin="{TemplateBinding Padding}"
                                              RecognizesAccessKey="True"
                                              SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </BulletDecorator>
                        <ControlTemplate.Triggers>
                            <Trigger Property="HasContent" Value="true">
                                <Setter Property="FocusVisualStyle" Value="{StaticResource CheckRadioFocusVisual}"/>
                                <Setter Property="Padding" Value="4,0,0,0"/>
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                            <Trigger Property="IsChecked" Value="True">
                                <Setter TargetName="TickMark" Property="Visibility" Value="Visible" />
                            </Trigger>
                            <Trigger Property="IsChecked" Value="{x:Null}">
                                <Setter TargetName="IndeterminateMark" Property="Visibility" Value="Visible" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style x:Key="{x:Type RadioButton}" TargetType="{x:Type RadioButton}">
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
            <Setter Property="Background" Value="#F4F4F4"/>
            <Setter Property="BorderBrush" Value="{StaticResource CheckBoxStroke}"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type RadioButton}">
                        <BulletDecorator Background="Transparent">
                            <BulletDecorator.Bullet>
                                <Grid VerticalAlignment="Center" Margin="0,1,0,0">
                                    <Ellipse Width="11" Height="11"
                                             Stroke="{TemplateBinding BorderBrush}"
                                             StrokeThickness="1"
                                             Fill="{TemplateBinding Background}" />
                                    <Ellipse Name="TickMark"
                                             Width="7" Height="7"
                                             Fill="Blue"
                                             Visibility="Hidden" />
                                    <Ellipse Name="IndeterminateMark"
                                             Width="3" Height="3"
                                             Fill="Blue"
                                             Visibility="Hidden" />
                                </Grid>
                            </BulletDecorator.Bullet>
                            <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                              Margin="{TemplateBinding Padding}"
                                              RecognizesAccessKey="True"
                                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </BulletDecorator>
                        <ControlTemplate.Triggers>
                            <Trigger Property="HasContent" Value="true">
                                <Setter Property="FocusVisualStyle" Value="{StaticResource CheckRadioFocusVisual}"/>
                                <Setter Property="Padding" Value="4,0,0,0"/>
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                            <Trigger Property="IsChecked" Value="True">
                                <Setter TargetName="TickMark" Property="Visibility" Value="Visible" />
                            </Trigger>
                            <Trigger Property="IsChecked" Value="{x:Null}">
                                <Setter TargetName="IndeterminateMark" Property="Visibility" Value="Visible" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

Vous avez donc maintenant de magnifiques contrôles qui vont faire de l’application un grand succès, le management est content, tout va pour le mieux dans le meilleur des mondes… Et là, vous réalisez que dans un autre écran de l’application, les contrôles doivent avoir le même style, mais en vert !

La première solution qui vient à l’esprit est de dupliquer le style en mettant du vert à la place du bleu. Mais comme vous êtes un bon développeur soucieux des bonnes pratiques, vous savez que la duplication de code, c’est mal : si vous devez un jour modifier le style de la CheckBox bleue, il faudra aussi modifier celui de la verte… et peut-être aussi la rouge, la noire, etc. Bref, ça deviendrait vite ingérable. Il faut donc refactoriser, mais comment ? Il faudrait pouvoir passer la couleur en paramètre du style, mais un style n’est pas une méthode à laquelle on peut passer des paramètres…

Il faudrait donc avoir une propriété supplémentaire pour indiquer la couleur des “ticks”, et se binder sur cette propriété dans le template. Une approche possible est de créer des contrôles personnalisés hérités de CheckBox et RadioButton, avec une propriété supplémentaire TickBrush… mais personnellement je n’aime pas beaucoup cette approche : je préfère éviter de créer de nouveaux contrôles quand on peut s’en sortir avec les contrôles standard.

En fait, il y a une solution plus simple : il suffit de créer une classe, qu’on appelle par exemple ThemeProperties, et de déclarer dedans une propriété attachée de type Brush:

    public static class ThemeProperties
    {
        public static Brush GetTickBrush(DependencyObject obj)
        {
            return (Brush)obj.GetValue(TickBrushProperty);
        }

        public static void SetTickBrush(DependencyObject obj, Brush value)
        {
            obj.SetValue(TickBrushProperty, value);
        }

        public static readonly DependencyProperty TickBrushProperty =
            DependencyProperty.RegisterAttached(
                "TickBrush",
                typeof(Brush),
                typeof(ThemeProperties),
                new FrameworkPropertyMetadata(Brushes.Black));
    }

On modifie un peu nos templates pour remplacer la couleur en dur par un binding sur cette propriété :

                                ...

                                <!-- CheckBox -->
                                        <Path Name="TickMark"
                                              Fill="{TemplateBinding my:ThemeProperties.TickBrush}"
                                              Data="M0,4 5,9 9,0 4,5"
                                              Visibility="Hidden" />
                                        <Rectangle Name="IndeterminateMark"
                                                   Fill="{TemplateBinding my:ThemeProperties.TickBrush}"
                                                   Width="7" Height="7"
                                                   HorizontalAlignment="Center"
                                                   VerticalAlignment="Center"
                                                   Visibility="Hidden" />

                                ...

                                <!-- RadioButton -->
                                    <Ellipse Name="TickMark"
                                             Width="7" Height="7"
                                             Fill="{TemplateBinding my:ThemeProperties.TickBrush}"
                                             Visibility="Hidden" />
                                    <Ellipse Name="IndeterminateMark"
                                             Width="3" Height="3"
                                             Fill="{TemplateBinding my:ThemeProperties.TickBrush}"
                                             Visibility="Hidden" />

Et quand on utilise les contrôles, on précise la couleur qu’on veut pour le tick :

<CheckBox Content="Checked" IsChecked="True" my:ThemeProperties.TickBrush="Blue" />

On peut donc maintenant avoir des contrôles qui partagent le même style, mais en changeant la couleur d’un élément du template :

On a donc effectivement rendu les styles paramétrables ! Il reste cependant un petit souci : étant donné que tous les contrôles d’un même éran utilisent tous la même couleur, il n’est pas très pratique de devoir la répéter sur chaque contrôle. L’idéal serait de pouvoir indiquer à la racine de la vue la couleur à utiliser pour tous les contrôles… et justement, les dependency properties (et donc les propriétés attachées) offrent une fonctionnalité qui permet de faire exactement ça : l’héritage de valeur. Il suffit d’indiquer le flag Inherits lors de la déclaration de la propriété TickBrush :

        public static readonly DependencyProperty TickBrushProperty =
            DependencyProperty.RegisterAttached(
                "TickBrush",
                typeof(Brush),
                typeof(ThemeProperties),
                new FrameworkPropertyMetadata(Brushes.Black, FrameworkPropertyMetadataOptions.Inherits));

Avec cette modification, la propriété devient “ambiante” : il suffit d’indiquer sa valeur sur un contrôle parent (par exemple la racine de la vue) pour que tous les descendants prennent en compte cette valeur. On peut donc très facilement faire des écrans avec des contrôles qui partagent le même style, mais en appliquant des couleurs différentes.

Le concept peut bien sûr être étendu à d’autres cas : en fait, dès qu’un élément du template doit varier selon un critère arbitraire, cette technique peut s’appliquer. Cela évite bien souvent de devoir dupliquer le template pour ne changer qu’un petit détail.

[WPF 4.5] Abonnement à un évènement à l’aide d’une markup extension

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

Voilà un certain temps que je n’avais plus parlé des markup extensions… J’y reviens à l’occasion de la sortie de Visual Studio 11 Developer Preview, qui introduit un certain nombre de nouveautés dans WPF. La nouveauté dont je vais parler n’est sans doute pas la plus spectaculaire, mais elle vient combler un manque des versions précédentes : le support des markup extensions pour les évènements.

Jusqu’ici, il était possible d’utiliser une markup extension en XAML pour affecter une valeur à une propriété, mais on ne pouvait pas faire la même chose pour s’abonner à un évènement. Dans WPF 4.5, c’est désormais possible. Voilà donc un petit exemple de ce que cela permet de faire…

Quand on utilise le pattern MVVM, on associe souvent des commandes du ViewModel à des contrôles de la vue, via le mécanisme de binding. Cette approche fonctionne généralement assez bien, mais elle présente certains inconvénients :

  • cela introduit beaucoup de code de “plomberie” dans le ViewModel
  • tous les contrôles n’ont pas une propriété Command (en fait, la plupart ne l’ont pas), et quand cette propriété existe, elle ne correspond qu’à un seul évènement du contrôle (par exemple le clic sur un bouton). Il n’y a pas de moyen vraiment simple de relier les autres évènements à des commandes du ViewModel.

Il serait plus pratique de pouvoir lier directement l’évènement à une méthode du ViewModel de la façon suivante:

        <Button Content="Click me"
                Click="{my:EventBinding OnClick}" />

Avec la méthode OnClick définie dans le ViewModel:

        public void OnClick(object sender, EventArgs e)
        {
            MessageBox.Show("Hello world!");
        }

Eh bien cela est désormais possible ! Voilà donc une petite preuve de concept… La classe EventBindingExtension présentée ci-dessous obtient d’abord le DataContext du contrôle, puis recherche la méthode spécifiée dans le DataContext, et renvoie un delegate pour cette méthode:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Markup;

    public class EventBindingExtension : MarkupExtension
    {
        public EventBindingExtension() { }

        public EventBindingExtension(string eventHandlerName)
        {
            this.EventHandlerName = eventHandlerName;
        }

        [ConstructorArgument("eventHandlerName")]
        public string EventHandlerName { get; set; }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            if (string.IsNullOrEmpty(EventHandlerName))
                throw new ArgumentException("The EventHandlerName property is not set", "EventHandlerName");

            var target = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));

            EventInfo eventInfo = target.TargetProperty as EventInfo;
            if (eventInfo == null)
                throw new InvalidOperationException("The target property must be an event");

            object dataContext = GetDataContext(target.TargetObject);
            if (dataContext == null)
                throw new InvalidOperationException("No DataContext found");

            var handler = GetHandler(dataContext, eventInfo, EventHandlerName);
            if (handler == null)
                throw new ArgumentException("No valid event handler was found", "EventHandlerName");

            return handler;
        }

        #region Helper methods

        static object GetHandler(object dataContext, EventInfo eventInfo, string eventHandlerName)
        {
            Type dcType = dataContext.GetType();

            var method = dcType.GetMethod(
                eventHandlerName,
                GetParameterTypes(eventInfo));
            if (method != null)
            {
                if (method.IsStatic)
                    return Delegate.CreateDelegate(eventInfo.EventHandlerType, method);
                else
                    return Delegate.CreateDelegate(eventInfo.EventHandlerType, dataContext, method);
            }

            return null;
        }

        static Type[] GetParameterTypes(EventInfo eventInfo)
        {
            var invokeMethod = eventInfo.EventHandlerType.GetMethod("Invoke");
            return invokeMethod.GetParameters().Select(p => p.ParameterType).ToArray();
        }

        static object GetDataContext(object target)
        {
            var depObj = target as DependencyObject;
            if (depObj == null)
                return null;

            return depObj.GetValue(FrameworkElement.DataContextProperty)
                ?? depObj.GetValue(FrameworkContentElement.DataContextProperty);
        }

        #endregion
    }

Cette classe est utilisable comme dans l’exemple présenté plus haut.

En l’état, cette markup extension présente une limitation un peu gênante : le DataContext doit être défini avant l’appel à ProvideValue, sinon il n’est pas possible de trouver la méthode qui gère l’évènement. Une solution pourrait être de s’abonner à l’évènement DataContextChanged pour s’abonner à l’évènement plus tard, mais en attendant il faut quand même renvoyer une valeur… et si on renvoie null, on obtient une exception car on ne peut pas s’abonner à un évènement avec un handler null. Il faudrait donc renvoyer un handler “bidon” généré dynamiquement en fonction de la signature de l’évènement. Voilà qui complique un peu les choses… mais ça reste faisable.

Voici une deuxième version qui implémente cette amélioration :

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Windows;
using System.Windows.Markup;

    public class EventBindingExtension : MarkupExtension
    {
        private EventInfo _eventInfo;

        public EventBindingExtension() { }

        public EventBindingExtension(string eventHandlerName)
        {
            this.EventHandlerName = eventHandlerName;
        }

        [ConstructorArgument("eventHandlerName")]
        public string EventHandlerName { get; set; }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            if (string.IsNullOrEmpty(EventHandlerName))
                throw new ArgumentException("The EventHandlerName property is not set", "EventHandlerName");

            var target = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));

            var targetObj = target.TargetObject as DependencyObject;
            if (targetObj == null)
                throw new InvalidOperationException("The target object must be a DependencyObject");

            _eventInfo = target.TargetProperty as EventInfo;
            if (_eventInfo == null)
                throw new InvalidOperationException("The target property must be an event");

            object dataContext = GetDataContext(targetObj);
            if (dataContext == null)
            {
                SubscribeToDataContextChanged(targetObj);
                return GetDummyHandler(_eventInfo.EventHandlerType);
            }

            var handler = GetHandler(dataContext, _eventInfo, EventHandlerName);
            if (handler == null)
            {
                Trace.TraceError(
                    "EventBinding: no suitable method named '{0}' found in type '{1}' to handle event '{2'}",
                    EventHandlerName,
                    dataContext.GetType(),
                    _eventInfo);
                return GetDummyHandler(_eventInfo.EventHandlerType);
            }

            return handler;

        }

        #region Helper methods

        static Delegate GetHandler(object dataContext, EventInfo eventInfo, string eventHandlerName)
        {
            Type dcType = dataContext.GetType();

            var method = dcType.GetMethod(
                eventHandlerName,
                GetParameterTypes(eventInfo.EventHandlerType));
            if (method != null)
            {
                if (method.IsStatic)
                    return Delegate.CreateDelegate(eventInfo.EventHandlerType, method);
                else
                    return Delegate.CreateDelegate(eventInfo.EventHandlerType, dataContext, method);
            }

            return null;
        }

        static Type[] GetParameterTypes(Type delegateType)
        {
            var invokeMethod = delegateType.GetMethod("Invoke");
            return invokeMethod.GetParameters().Select(p => p.ParameterType).ToArray();
        }

        static object GetDataContext(DependencyObject target)
        {
            return target.GetValue(FrameworkElement.DataContextProperty)
                ?? target.GetValue(FrameworkContentElement.DataContextProperty);
        }

        static readonly Dictionary<Type, Delegate> _dummyHandlers = new Dictionary<Type, Delegate>();

        static Delegate GetDummyHandler(Type eventHandlerType)
        {
            Delegate handler;
            if (!_dummyHandlers.TryGetValue(eventHandlerType, out handler))
            {
                handler = CreateDummyHandler(eventHandlerType);
                _dummyHandlers[eventHandlerType] = handler;
            }
            return handler;
        }

        static Delegate CreateDummyHandler(Type eventHandlerType)
        {
            var parameterTypes = GetParameterTypes(eventHandlerType);
            var returnType = eventHandlerType.GetMethod("Invoke").ReturnType;
            var dm = new DynamicMethod("DummyHandler", returnType, parameterTypes);
            var il = dm.GetILGenerator();
            if (returnType != typeof(void))
            {
                if (returnType.IsValueType)
                {
                    var local = il.DeclareLocal(returnType);
                    il.Emit(OpCodes.Ldloca_S, local);
                    il.Emit(OpCodes.Initobj, returnType);
                    il.Emit(OpCodes.Ldloc_0);
                }
                else
                {
                    il.Emit(OpCodes.Ldnull);
                }
            }
            il.Emit(OpCodes.Ret);
            return dm.CreateDelegate(eventHandlerType);
        }

        private void SubscribeToDataContextChanged(DependencyObject targetObj)
        {
            DependencyPropertyDescriptor
                .FromProperty(FrameworkElement.DataContextProperty, targetObj.GetType())
                .AddValueChanged(targetObj, TargetObject_DataContextChanged);
        }

        private void UnsubscribeFromDataContextChanged(DependencyObject targetObj)
        {
            DependencyPropertyDescriptor
                .FromProperty(FrameworkElement.DataContextProperty, targetObj.GetType())
                .RemoveValueChanged(targetObj, TargetObject_DataContextChanged);
        }

        private void TargetObject_DataContextChanged(object sender, EventArgs e)
        {
            DependencyObject targetObj = sender as DependencyObject;
            if (targetObj == null)
                return;

            object dataContext = GetDataContext(targetObj);
            if (dataContext == null)
                return;

            var handler = GetHandler(dataContext, _eventInfo, EventHandlerName);
            if (handler != null)
            {
                _eventInfo.AddEventHandler(targetObj, handler);
            }
            UnsubscribeFromDataContextChanged(targetObj);
        }

        #endregion
    }

Voilà donc un exemple du genre de choses qu’on peut faire grâce à cette nouvelle fonctionnalité de WPF. On pourrait aussi imaginer un système de “behavior” similaire à ce qu’on peut faire avec des propriétés attachées, par exemple pour réaliser une action standard lorsqu’un évènement se produit. Les possiblités sont sans doute nombreuses, je vous laisse le soin de les trouver ;)

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…)

[WPF] Afficher une image GIF animée

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

WPF est une technologie géniale, mais parfois on a l’impression qu’il lui manque certaines fonctionnalités assez basiques… Un exemple souvent cité est l’absence de support pour les images GIF animées. En fait, le format GIF proprement dit est supporté, mais le contrôle Image n’affiche que la première image de l’animation.

De nombreuses solutions à ce problème ont été proposées sur les forums et blogs techniques, généralement des variantes autour des approches suivantes :

  • Utiliser le contrôle MediaElement : malheureusement ce contrôle ne supporte que les URI de type file:// ou http://, et non le schéma d’URI pack:// utilisé pour les ressources WPF ; l’image ne peut donc pas être inclue dans les ressources, elle doit être dans un fichier à part. De plus, la transparence n’est pas supportée, si bien que le résultat final est assez laid
  • Utiliser le contrôle PictureBox de Windows Forms, via un WindowsFormsHost : personnellement j’ai horreur d’utiliser des contrôles Windows Forms en WPF, ça me donne l’impression de faire quelque chose de mal :P
  • Créer un contrôle dérivé de Image qui gère l’animation. Pour l’implémentation, certaines solutions tirent partie de la classe ImageAnimator de System.Drawing (GDI), d’autres utilisent une animation WPF pour changer de frame. C’est une approche assez “propre”, mais qui oblige à utiliser un contrôle spécifique pour les GIF. De plus la solution utilisant ImageAnimator se révèle assez peu fluide.

Comme vous l’aurez deviné, aucune de ces solutions ne me satisfait vraiment… De plus, aucune ne gère proprement la durée de chaque frame, et suppose simplement que toutes les frames durent 100ms (c’est presque toujours le cas, mais le presque fait toute la différence…). Je n’ai donc gardé que les meilleures idées dans les approches ci-dessus pour créer ma propre solution. Les objectifs que je souhaitais atteindre sont les suivants :

  • Ne pas dépendre de Windows Forms ou de GDI
  • Afficher l’image animée dans un contrôle Image standard
  • Pouvoir utiliser le même code XAML pour une image fixe ou animée
  • Supporter la transparence
  • Tenir compte de la durée réelle de chaque frame de l’image

Pour arriver à ce résultat, je suis parti d’une idée simple, voire évidente : pour animer l’image, il suffit d’appliquer une animation à la propriété Source du contrôle Image. Or WPF fournit tous les outils nécessaires pour réaliser ce type d’animation ; en l’occurrence la classe ObjectAnimationUsingKeyFrames répond parfaitement au besoin : on peut spécifier à quel instant exact affecter une valeur donnée à la propriété, ce qui permet de tenir compte de la durée des frames.

Le problème suivant est d’extraire les différentes frames de l’image : heureusement ce scénario est prévu dans WPF, et la classe BitmapDecoder fournit une propriété Frames qui sert à ça. Donc, pas de difficulté majeure à ce niveau…

Enfin, dernier obstacle : extraire la durée de chaque frame. C’est finalement la partie qui m’a demandé le plus de recherche… J’ai d’abord cru qu’il faudrait lire manuellement le fichier pour trouver cette information, en décodant directement les données binaires. Mais la solution est finalement assez simple, et tire partie de la classe BitmapMetadata. La seule difficulté a été de localiser le “chemin” de la métadonnée qui contient cette information, mais après quelques tâtonnements, la voilà : /grctlext/Delay.

La solution finale est implémentée sous forme d’une propriété attachée AnimatedSource applicable au contrôle Image, qui s’utilise en lieu et place de Source :

<Image Stretch="None" my:ImageBehavior.AnimatedSource="/Images/animation.gif" />

On peut également affecter une image fixe à cette propriété, elle s’affichera normalement ; on peut donc utiliser cette propriété sans se soucier de savoir si l’image à afficher sera fixe ou animée.

Au final, tous les objectifs fixés au départ sont donc atteints, et il y a même une cerise sur le gâteau : cette solution fonctionne également dans le designer (du moins dans Visual Studio 2010), on voit donc directement l’animation quand on affecte la propriété AnimatedSource :)

Sans plus attendre, voilà le code complet :

    public static class ImageBehavior
    {
        #region AnimatedSource

        [AttachedPropertyBrowsableForType(typeof(Image))]
        public static ImageSource GetAnimatedSource(Image obj)
        {
            return (ImageSource)obj.GetValue(AnimatedSourceProperty);
        }

        public static void SetAnimatedSource(Image obj, ImageSource value)
        {
            obj.SetValue(AnimatedSourceProperty, value);
        }

        public static readonly DependencyProperty AnimatedSourceProperty =
            DependencyProperty.RegisterAttached(
              "AnimatedSource",
              typeof(ImageSource),
              typeof(ImageBehavior),
              new UIPropertyMetadata(
                null,
                AnimatedSourceChanged));

        private static void AnimatedSourceChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            Image imageControl = o as Image;
            if (imageControl == null)
                return;

            var oldValue = e.OldValue as ImageSource;
            var newValue = e.NewValue as ImageSource;
            if (oldValue != null)
            {
                imageControl.BeginAnimation(Image.SourceProperty, null);
            }
            if (newValue != null)
            {
                imageControl.DoWhenLoaded(InitAnimationOrImage);
            }
        }

        private static void InitAnimationOrImage(Image imageControl)
        {
            BitmapSource source = GetAnimatedSource(imageControl) as BitmapSource;
            if (source != null)
            {
                var decoder = GetDecoder(source) as GifBitmapDecoder;
                if (decoder != null && decoder.Frames.Count > 1)
                {
                    var animation = new ObjectAnimationUsingKeyFrames();
                    var totalDuration = TimeSpan.Zero;
                    BitmapSource prevFrame = null;
                    FrameInfo prevInfo = null;
                    foreach (var rawFrame in decoder.Frames)
                    {
                        var info = GetFrameInfo(rawFrame);
                        var frame = MakeFrame(
                            source,
                            rawFrame, info,
                            prevFrame, prevInfo);

                        var keyFrame = new DiscreteObjectKeyFrame(frame, totalDuration);
                        animation.KeyFrames.Add(keyFrame);

                        totalDuration += info.Delay;
                        prevFrame = frame;
                        prevInfo = info;
                    }
                    animation.Duration = totalDuration;
                    animation.RepeatBehavior = RepeatBehavior.Forever;
                    if (animation.KeyFrames.Count > 0)
                        imageControl.Source = (ImageSource)animation.KeyFrames[0].Value;
                    else
                        imageControl.Source = decoder.Frames[0];
                    imageControl.BeginAnimation(Image.SourceProperty, animation);
                    return;
                }
            }
            imageControl.Source = source;
            return;
        }

        private static BitmapDecoder GetDecoder(BitmapSource image)
        {
            BitmapDecoder decoder = null;
            var frame = image as BitmapFrame;
            if (frame != null)
                decoder = frame.Decoder;

            if (decoder == null)
            {
                var bmp = image as BitmapImage;
                if (bmp != null)
                {
                    if (bmp.StreamSource != null)
                    {
                        decoder = BitmapDecoder.Create(bmp.StreamSource, bmp.CreateOptions, bmp.CacheOption);
                    }
                    else if (bmp.UriSource != null)
                    {
                        Uri uri = bmp.UriSource;
                        if (bmp.BaseUri != null && !uri.IsAbsoluteUri)
                            uri = new Uri(bmp.BaseUri, uri);
                        decoder = BitmapDecoder.Create(uri, bmp.CreateOptions, bmp.CacheOption);
                    }
                }
            }

            return decoder;
        }

        private static BitmapSource MakeFrame(
            BitmapSource fullImage,
            BitmapSource rawFrame, FrameInfo frameInfo,
            BitmapSource previousFrame, FrameInfo previousFrameInfo)
        {
            DrawingVisual visual = new DrawingVisual();
            using (var context = visual.RenderOpen())
            {
                if (previousFrameInfo != null && previousFrame != null &&
                    previousFrameInfo.DisposalMethod == FrameDisposalMethod.Combine)
                {
                    var fullRect = new Rect(0, 0, fullImage.PixelWidth, fullImage.PixelHeight);
                    context.DrawImage(previousFrame, fullRect);
                }

                context.DrawImage(rawFrame, frameInfo.Rect);
            }
            var bitmap = new RenderTargetBitmap(
                fullImage.PixelWidth, fullImage.PixelHeight,
                fullImage.DpiX, fullImage.DpiY,
                PixelFormats.Pbgra32);
            bitmap.Render(visual);
            return bitmap;
        }

        private class FrameInfo
        {
            public TimeSpan Delay { get; set; }
            public FrameDisposalMethod DisposalMethod { get; set; }
            public double Width { get; set; }
            public double Height { get; set; }
            public double Left { get; set; }
            public double Top { get; set; }

            public Rect Rect
            {
                get { return new Rect(Left, Top, Width, Height); }
            }
        }

        private enum FrameDisposalMethod
        {
            Replace = 0,
            Combine = 1,
            RestoreBackground = 2,
            RestorePrevious = 3
        }

        private static FrameInfo GetFrameInfo(BitmapFrame frame)
        {
            var frameInfo = new FrameInfo
            {
                Delay = TimeSpan.FromMilliseconds(100),
                DisposalMethod = FrameDisposalMethod.Replace,
                Width = frame.PixelWidth,
                Height = frame.PixelHeight,
                Left = 0,
                Top = 0
            };

            BitmapMetadata metadata;
            try
            {
                metadata = frame.Metadata as BitmapMetadata;
                if (metadata != null)
                {
                    const string delayQuery = "/grctlext/Delay";
                    const string disposalQuery = "/grctlext/Disposal";
                    const string widthQuery = "/imgdesc/Width";
                    const string heightQuery = "/imgdesc/Height";
                    const string leftQuery = "/imgdesc/Left";
                    const string topQuery = "/imgdesc/Top";

                    var delay = metadata.GetQueryOrNull<ushort>(delayQuery);
                    if (delay.HasValue)
                        frameInfo.Delay = TimeSpan.FromMilliseconds(10 * delay.Value);

                    var disposal = metadata.GetQueryOrNull<byte>(disposalQuery);
                    if (disposal.HasValue)
                        frameInfo.DisposalMethod = (FrameDisposalMethod) disposal.Value;

                    var width = metadata.GetQueryOrNull<ushort>(widthQuery);
                    if (width.HasValue)
                        frameInfo.Width = width.Value;

                    var height = metadata.GetQueryOrNull<ushort>(heightQuery);
                    if (height.HasValue)
                        frameInfo.Height = height.Value;

                    var left = metadata.GetQueryOrNull<ushort>(leftQuery);
                    if (left.HasValue)
                        frameInfo.Left = left.Value;

                    var top = metadata.GetQueryOrNull<ushort>(topQuery);
                    if (top.HasValue)
                        frameInfo.Top = top.Value;
                }
            }
            catch (NotSupportedException)
            {
            }

            return frameInfo;
        }

        private static T? GetQueryOrNull<T>(this BitmapMetadata metadata, string query)
            where T : struct
        {
            if (metadata.ContainsQuery(query))
            {
                object value = metadata.GetQuery(query);
                if (value != null)
                    return (T) value;
            }
            return null;
        }

        #endregion
    }

Et voici la méthode d’extension DoWhenLoaded utilisée dans le code ci-dessus :

public static void DoWhenLoaded<T>(this T element, Action<T> action)
    where T : FrameworkElement
{
    if (element.IsLoaded)
    {
        action(element);
    }
    else
    {
        RoutedEventHandler handler = null;
        handler = (sender, e) =>
        {
            element.Loaded -= handler;
            action(element);
        };
        element.Loaded += handler;
    }
}

Cette classe sera inclue dans la prochaine version de la librairie Dvp.NET, dont j’avais déjà parlé il y quelque temps.

Mise à jour : le code qui récupère la durée d’une frame ne fonctionne que sous Windows Seven, et sous Windows Vista si la Platform Update est installée (non testé). La durée par défaut (100ms) sera utilisée sur les autres versions de Windows. Je mettrai à jour l’article si je trouve une solution qui fonctionne sur tous les systèmes (je sais que je pourrais utiliser System.Drawing.Bitmap, mais je préfèrerais éviter…)

Mise à jour 2 : comme Klaus l’a signalé dans un commentaire sur la version anglaise de mon blog, la classe ImageBehavior ne gérait pas certains attributs importants des frames : la méthode de destruction (est-ce qu’une frame doit simplement remplacer la frame précédente, ou être combinée avec elle), et la position des frames (Left/Top/Width/Height). J’ai mis à jour le code pour gérer ces attributs correctement. Merci Klaus !

Mise à jour 3 : encore un petit bug corrigé, la récupération du décodeur à partir d’une URI relative ne fonctionnait pas. Merci à l’anonyme qui l’a signalé!

Mise à jour 4 : plutôt que de continuer à poster les améliorations sur ce billet, j’ai finalement créé un projet sur CodePlex où cette classe sera maintenue. Vous pouvez aussi l’installer avec NuGet, l’id du package est WpfAnimatedGif. Merci à Diego Mijelshon pour la suggestion!

Posted in WPF. Tags: , , . 31 Comments »

[WPF] Comment faire un binding dans les cas où on n’hérite pas du DataContext

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

La propriété DataContext de WPF est extrêmement pratique, car elle est automatiquement héritée par tous les enfants de l’élément où elle est définie ; il n’est donc pas nécessaire de la redéfinir pour chaque élément qu’on veut lier aux données. Cependant, il arrive que le DataContext ne soit pas accessible pour certains éléments : c’est le cas des éléments qui n’appartiennent pas à l’arbre visuel ni à l’arbre logique. Il devient alors très difficile de définir une propriété ce ces éléments par un binding…

Prenons un exemple simple : on veut afficher une liste de produits dans un DataGrid. Dans la grille, on veut pouvoir afficher où masquer la colonne du prix, en fonction d’une propriété ShowPrice exposée par le ViewModel. L’approche évidente consiste à binder la propriété Visibility de la colonne à la propriété ShowPrice :

<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                    Visibility="{Binding ShowPrice,
                        Converter={StaticResource visibilityConverter}}"/>

Malheureusement, changer la valeur de la propriété ShowPrice n’a aucun effet, et la colonne reste toujours affichée… pourquoi ? Si on regarde la fenêtre de sortie de Visual Studio, on remarque la ligne suivante :

System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=ShowPrice; DataItem=null; target element is ‘DataGridTextColumn’ (HashCode=32685253); target property is ‘Visibility’ (type ‘Visibility’)

Derrière cet obscur charabia se cache une explication toute simple : WPF ne sait pas quel FrameworkElement utiliser pour récupérer le DataContext, car la colonne n’appartient pas à l’arbre visuel ni à l’arbre logique du DataGrid.

On peut toujours essayer de “triturer” le binding pour obtenir le résultat voulu, par exemple en essayant de binder par rapport au DataGrid lui-même :

<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                    Visibility="{Binding DataContext.ShowPrice,
                        Converter={StaticResource visibilityConverter},
                        RelativeSource={RelativeSource FindAncestor, AncestorType=DataGrid}}"/>

Ou encore, en ajoutant une CheckBox bindée sur ShowPrice et en essayant de binder la visibilité de la colonne sur la propriété IsChecked, en spécifiant le nom de l’élément :

<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                    Visibility="{Binding IsChecked,
                        Converter={StaticResource visibilityConverter},
                        ElementName=chkShowPrice}"/>

Mais rien à faire, on obtient toujours le même résultat…

A ce stade, il semble que la seule approche qui pourrait marcher est de passer par le code-behind, ce qu’on préfère généralement éviter quand on suit le pattern MVVM… mais ce serait dommage d’abandonner aussi vite ;)

La solution est en fait assez simple, et se base sur la classe Freezable. La vocation première de cette classe est de définir des objets qui ont un état modifiable et un état non modifiable. Mais en l’occurrence, la caractéristique qui nous intéresse est qu’un objet qui hérite de Freezable peut hériter du DataContext, bien qu’il ne s’agisse pas d’un élément visuel. Je ne connais pas le mécanisme exact qui permet d’obtenir ce comportement, mais toujours est-il que cela va nous permettre d’arriver au résultat voulu…

L’idée est de créer une classe, que j’ai appelée BindingProxy, qui hérite de Freezable et dans laquelle on va déclarer une dependency property Data :

    public class BindingProxy : Freezable
    {
        #region Overrides of Freezable

        protected override Freezable CreateInstanceCore()
        {
            return new BindingProxy();
        }

        #endregion

        public object Data
        {
            get { return (object)GetValue(DataProperty); }
            set { SetValue(DataProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Data.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DataProperty =
            DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
    }

On va ensuite déclarer une instance de cette classe dans les ressources du DataGrid, et binder la propriété Data sur le DataContext courant :

<DataGrid.Resources>
    <local:BindingProxy x:Key="proxy" Data="{Binding}" />
</DataGrid.Resources>

Il suffit ensuite de spécifier que la source de notre binding est cet objet BindingProxy, facilement accessible puisqu’il est déclaré comme ressource :

<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                    Visibility="{Binding Data.ShowPrice,
                        Converter={StaticResource visibilityConverter},
                        Source={StaticResource proxy}}"/>

Remarquez qu’on a aussi préfixé le chemin du binding par “Data”, puisque le chemin est maintenant relatif à l’objet BindingProxy.

Le binding fonctionne maintenant comme prévu, moyennant une solution relativement simple à mettre en oeuvre…

[C# 5] Programmation asynchrone avec C# 5

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

Depuis quelque temps, les spéculations allaient bon train sur les fonctionnalités de la future version 5 du langage C#… Très peu d’informations officielles avaient filtré à ce sujet, la seule chose plus ou moins certaine était l’introduction du concept de “compilateur en temps que service”, qui permettrait de tirer parti du compilateur à partir du code. A part ça, silence radio de la part de Microsoft…

Lors de la PDC jeudi dernier, un coin du voile a enfin été levé, mais pas du tout sur ce qu’on attendait ! Anders Hejlsberg, le créateur de C#, a bien consacré quelques minutes à la notion de “compiler as a service”, mais l’essentiel de sa présentation portait sur quelque chose de complètement différent : la programmation asynchrone en C#.

Il est bien sûr déjà possible d’effectuer des traitements asynchrones en C#, mais c’est généralement assez pénible et peu intuitif… On est souvent obligé de passer par des callbacks pour indiquer ce qui doit être exécuté à la fin du traitement asynchrone, et on se retrouve rapidement avec un code difficile à relire et à comprendre, et donc à maintenir. Pour une démonstration de ce problème, je vous invite à lire l’excellent article d’Eric Lippert à ce sujet, il explique ça beaucoup mieux que moi…

Cet article (et la série qu’il conclut) était en fait un prélude à l’annonce faite à la PDC : C# 5 intègrera une nouvelle syntaxe permettant d’écrire du code asynchrone de façon beaucoup plus naturelle, avec l’introduction de deux nouveaux mots-clés : async et await. Le code à écrire pour réaliser un traitement asynchrone sera quasiment identique à celui d’un traitement synchrone : toute la complexité sera masquée par cette nouvelle fonctionnalité du langage.

Puisqu’un exemple vaut mieux qu’un long discours, je vais reprendre l’exemple utilisé par Anders Hejlsberg pendant sa présentation, en le simplifiant un peu. Supposons qu’on veuille rechercher des titres de films par leur année de sortie. Pour simplifier, on utilisera le service OData de Netflix. Le code suivant effectue la recherche de façon synchrone, en récupérant les résultats 10 par 10 :

        private void btnSearch_Click(object sender, RoutedEventArgs e)
        {
            int year;
            if (!int.TryParse(txtYear.Text, out year))
            {
                MessageBox.Show("L'année saisie est incorrecte");
                return;
            }
            SearchMovies(year);
        }

        private void SearchMovies(int year)
        {
            var netflixUri = new Uri("http://odata.netflix.com/Catalog/");
            var catalog = new Netflix.NetflixCatalog(netflixUri);
            lstTitles.Items.Clear();
            int count = 0;
            int pageSize = 10;
            while (true)
            {
                var movies = SearchMoviesBatch(catalog, year, count, pageSize);
                if (movies.Length == 0)
                    break;
                foreach (var title in movies)
                {
                    lstTitles.Items.Add(title.Name);
                }
                count += movies.Length;
            }
        }

        private Title[] SearchMoviesBatch(NetflixCatalog catalog, int year, int count, int pageSize)
        {
            var query = from title in catalog.Titles
                            where title.ReleaseYear == year
                            orderby title.Name
                            select title;
            return query.Skip(count).Take(pageSize).ToArray();
        }

Ce code a le mérite d’être assez simple, mais il suffit de l’exécuter pour se rendre compte qu’il y a un problème : la récupération des résultats peut prendre un certain temps, pendant lequel l’interface reste figée. Il faut donc effectuer la recherche de façon asynchrone, pour que l’interface reste réactive. Voici une approche possible, avec la version actuelle de C# :

        private void SearchMoviesAsync(int year)
        {
            lstTitles.Items.Clear();
            Thread t = new Thread(() =>
            {
                var netflixUri = new Uri("http://odata.netflix.com/Catalog/");
                var catalog = new Netflix.NetflixCatalog(netflixUri);
                int count = 0;
                int pageSize = 10;
                while (true)
                {
                    var movies = SearchMoviesBatch(catalog, year, count, pageSize);
                    if (movies.Length == 0)
                        break;
                    foreach (var title in movies)
                    {
                        Dispatcher.Invoke(new Action(() => lstTitles.Items.Add(title.Name)));
                    }
                    count += movies.Length;
                }
            });
            t.Start();
        }

(Les deux autres méthodes sont inchangées)

On voit que le code commence déjà à être moins clair, à cause de l’expression lambda passée au constructeur du thread, et de l’utilisation de Dispatcher.Invoke pour mettre à jour l’interface graphique. Imaginez un peu ce que ça donnerait dans un scénario plus complexe, avec plusieurs tâches asynchrones interdépendantes (comme dans l’article d’Eric Lippert mentionné plus haut).

Avec la nouvelle syntaxe introduite par C# 5, voici comment on pourrait écrire ce code :

        private async void SearchMoviesAsync(int year)
        {
            var netflixUri = new Uri("http://odata.netflix.com/Catalog/");
            var catalog = new Netflix.NetflixCatalog(netflixUri);
            lstTitles.Items.Clear();
            int count = 0;
            int pageSize = 10;
            while (true)
            {
                var movies = await SearchMoviesBatchAsync(catalog, year, count, pageSize);
                if (movies.Length == 0)
                    break;
                foreach (var title in movies)
                {
                    lstTitles.Items.Add(title.Name);
                }
                count += movies.Length;
            }
        }

        private async Task<Title[]> SearchMoviesBatchAsync(NetflixCatalog catalog, int year, int count, int pageSize)
        {
            var query = from title in catalog.Titles
                        where title.ReleaseYear == year
                        orderby title.Name
                        select title;
            return await query.Skip(count).Take(pageSize).ToArrayAsync();
        }

Remarquez que les deux méthodes ont un nouveau modificateur async, qui indique qu’elles s’exécutent de façon asynchrone. Lors de l’appel à une autre méthode asynchrone, l’appel est précédé du mot-clé await. Lorsque la méthode SearchMoviesAsync est appelée, elle commence à s’exécuter normalement, jusqu’au mot-clé await. A partir de là, deux scénarios sont possibles

  • soit l’appel à SearchMoviesBatchAsync se termine de façon synchrone, auquel cas l’exécution continue normalement
  • soit il s’exécute de façon asynchrone, dans ce cas le contrôle est rendu à la méthode qui appelle SearchMoviesAsync (en l’occurrence btnSearch_Click). Quand l’appel à SearchMoviesBatchAsync se termine, l’exécution de SearchMoviesAsync reprend là où elle en était (de ce point de vue, await fonctionne un peu comme yield return)

Un peu comme pour les blocs itérateurs, le compilateur réécrit le code de la méthode en créant un delegate avec le code qui suit l’appel asynchrone, et appelle ce delegate quand la tâche asynchrone se termine. Remarquez d’ailleurs que ce delegate est appelé sur le même thread, celui du dispatcher en l’occurrence : on n’a donc pas besoin de Dispatcher.Invoke pour mettre à jour l’interface graphique.

En pratique, tout ce système se base sur la classe Task introduite dans .NET 4. Remarquez d’ailleurs que le type de retour de la méthode SearchMoviesBatchAsync est Task<Title[]>. Pourtant, quand on appelle cette méthode à partir de SearchMoviesAsync, on récupère bien un objet de type Title[], et non Task<Title[]>. C’est l’autre effet du mot-clé await : il récupère le résultat d’une tâche une fois qu’elle est terminée.

Encore une chose : j’ai utilisé dans le code une méthode d’extension ToArrayAsync, voici son code :

        public static Task<T[]> ToArrayAsync<T>(this IQueryable<T> source)
        {
            return TaskEx.Run(() => source.ToArray());
        }

Voilà pour l’introduction à cette future nouvelle fonctionnalité de C#. J’espère que c’était à peu près compréhensible et que je n’ai pas dit trop de bêtises… tout n’est pas encore complètement clair dans ma têteClignement d'œil. Pour en savoir plus, voici quelques liens utiles :

Pour les adeptes de VB.NET, sachez que cette fonctionnalité sera aussi inclue dans la prochaine version de Visual Basic, ainsi que les itérateurs, qui n’existaient qu’en C# jusqu’à maintenant.

Posted in C# 5.0. Tags: , , . 6 Comments »