Tag Archives: markup extension

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

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 😉

[WPF] Markup extensions et templates

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] Une markup extension qui met à jour sa cible

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.

[WPF] Binding asynchrone sur une propriété du ViewModel

Mise à jour : Comme l’a très justement indiqué Jérémy en commentaire, la propriété IsAsync du Binding permet de faire à peu près la même chose beaucoup plus simplement… Bien que ma méthode puisse servir pour certains besoins spécifiques, dans la plupart des cas la propriété IsAsync est probablement le meilleur choix ! Je laisse le billet malgré tout, ne serait-ce que pour la classe SwitchBinding qui me semble assez utile…

J’ai eu récemment besoin, dans une application basée sur le pattern MVVM, d’afficher une propriété dont la valeur était assez longue à obtenir (récupérer par une requête HTTP). Au départ, j’ai simplement implémenté la propriété en suivant le principe du lazy loading : le binding sur cette propriété provoquait donc l’obtention de la valeur par une requête HTTP. Le résultat, prévisible, était un gel de l’interface pendant la récupération de la valeur. La solution classique pour ce genre de problème est de récupérer la valeur sur un autre thread, et d’affecter le résultat au contrôle qui doit l’afficher… sauf qu’en MVVM on n’a pas accès à ce contrôle. Une autre approche, plus adaptée, est d’affecter la valeur à la propriété du ViewModel, ce qui déclenche l’évènement PropertyChanged et rafraichit la vue.

J’ai essayé pas mal de choses avant d’arriver à une solution avec le moins possible de code de “plomberie”, je vais donc vous la faire partager. Voilà le code de la propriété :

        private bool _retrievingValue = false;

        private object _value;
        public object Value
        {
            get
            {
                if (_value == null && !_retrievingValue)
                {
                    _retrievingValue = true;
                    ThreadPool.QueueUserWorkItem(
                        (state) =>
                        {
                            this.Value = _model.RetrieveValue(); // Very long operation...
                            _retrievingValue = false;
                        });
                }
                return _value;
            }
            set
            {
                _value = value;
                OnPropertyChanged("Value");
            }
        }

Ce code est assez simple à comprendre, mais mérite quand même quelques commentaires :

  • Le premier binding sur cette propriété récupère d’abord une valeur nulle, mais déclenche aussi la récupération asynchrone de la valeur. Notez le flag _retrievingValue qui évite de lancer plusieurs fois la récupération
  • Quand la récupération de la valeur est terminée, la propriété est mise à jour, et l’évènement PropertyChanged met à jour le binding
  • Un détail intéressant est que la propriété est mise à jour directement dans le thread de travail. Puisque cette mise à jour provoque une modification de la vue, on aurait pu s’attendre à une InvalidOperationException, car on ne peut pas modifier la vue à partir d’un autre thread… mais en fait, le mécanisme de binding de WPF est lui-même asynchrone, ce qui masque la complexité de l’appel cross-thread. Il est donc inutile de recourir à un Dispatcher.Invoke ou autre pirouette, ce qui simplifie bien la vie des développeurs que nous sommes…
  • Attention : ce système de binding asynchrone fonctionne bien pour affecter une valeur à une propriété du ViewModel, mais ne permet pas, par exemple, de modifier les éléments d’une ObservableCollection. Si vous essayez, à partir d’un autre thread, d’ajouter ou enlever des éléments à une collection sur laquelle la vue est bindée, cela provoquera une NotSupportedException :

    Ce type de CollectionView ne prend pas en charge les modifications de son SourceCollection à partir d’un thread différent du thread du Dispatcher.

    Pour modifier des collections de façon asynchrone, il faudra donc se débrouiller autrement… cela fera l’objet d’un prochain billet si je trouve une solution satisfaisante (peut-être à base d’AsyncOperation…). Si vous avez une idée là-dessus, n’hésitez pas à la poster en commentaire !

    Fermons cette parenthèse et revenons à notre propriété Value. La méthode décrite plus haut fonctionne bien et nécessite assez peu de code, mais elle a un inconvénient : pendant la récupération de la valeur, l’utilisateur ne voit rien… On aimerait pouvoir afficher quelque chose qui indique que l’application travaille. Pour ça, on peut introduire une propriété IsValueReady qui indiquera si la valeur est prête. Côté XAML, on pourra utiliser un Trigger sur cette propriété pour modifier l’affichage.

            private bool _retrievingValue = false;
    
            private object _value;
            public object Value
            {
                get
                {
                    if (!_isValueReady && !_retrievingValue)
                    {
                        _retrievingValue = true;
                        ThreadPool.QueueUserWorkItem(
                            (state) =>
                            {
                                this.Value = _model.RetrieveValue(); // Very long operation...
                                this.IsValueReady = true;
                                _retrievingValue = false;
                            });
                    }
                    return _value;
                }
                set
                {
                    _value = value;
                    OnPropertyChanged("Value");
                }
            }
    
            private bool _isValueReady = false;
            public bool IsValueReady
            {
                get { return _isValueReady; }
                private set
                {
                    _isValueReady = value;
                    OnPropertyChanged("IsValueReady");
                }
            }
    

    Ça commence à faire un code un peu plus conséquent pour une simple propriété, mais ce code est toujours le même… les seules choses qui changent sont le nom de la propriété, son type, et le code qui récupère la valeur. Si on a beaucoup de propriétés de ce genre à créer, on pourrait donc facilement écrire un code snippet qui génèrerait le plus gros du code.

    Avec Blend, il est probablement assez simple de créer un trigger pour prendre en compte la propriété IsValueReady dans la vue… mais je ne me suis toujours pas mis à Blend, je code directement la vue en XAML, et je trouve les Triggers beaucoup trop lourds à écrire… J’ai donc utilisé une autre solution que je trouve beaucoup plus simple et plus lisible, à base de markup extension (oui, j’aime bien les markup extensions…). Ca donne le XAML suivant :

        <Grid>
            <TextBlock Text="{Binding Value, FallbackValue=Blabla}"
                       Visibility="{my:SwitchBinding IsValueReady, Visible, Hidden}"/>
            <ProgressBar IsIndeterminate="True" Width="150" Height="30"
                         Visibility="{my:SwitchBinding IsValueReady, Hidden, Visible}"/>
        </Grid>
    

    Ce code masque le TextBlock et affiche la ProgressBar tant que la valeur n’est pas prête. Quand la récupération de la valeur est terminée, la ProgressBar disparait et le TextBlock redevient visible…

    SwitchBinding est une markup extension qui hérite de Binding et renvoie une valeur ou une autre selon que la propriété bindée vaut true ou false. Je ne m’étendrai pas sur le fonctionnement de cette extension, car ce n’est pas le sujet de ce billet, mais voici tout de même son code :

        public class SwitchBindingExtension : Binding
        {
            public SwitchBindingExtension()
            {
                Initialize();
            }
    
            public SwitchBindingExtension(string path)
                : base(path)
            {
                Initialize();
            }
    
            public SwitchBindingExtension(string path, object valueIfTrue, object valueIfFalse)
                : base(path)
            {
                Initialize();
                this.ValueIfTrue = valueIfTrue;
                this.ValueIfFalse = valueIfFalse;
            }
    
            private void Initialize()
            {
                this.ValueIfTrue = Binding.DoNothing;
                this.ValueIfFalse = Binding.DoNothing;
                this.Converter = new SwitchConverter(this);
            }
    
            [ConstructorArgument("valueIfTrue")]
            public object ValueIfTrue { get; set; }
    
            [ConstructorArgument("valueIfFalse")]
            public object ValueIfFalse { get; set; }
    
            private class SwitchConverter : IValueConverter
            {
                public SwitchConverter(SwitchBindingExtension switchExtension)
                {
                    _switch = switchExtension;
                }
    
                private SwitchBindingExtension _switch;
    
                #region IValueConverter Members
    
                public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
                {
                    try
                    {
                        bool b = System.Convert.ToBoolean(value);
                        return b ? _switch.ValueIfTrue : _switch.ValueIfFalse;
                    }
                    catch
                    {
                        return DependencyProperty.UnsetValue;
                    }
                }
    
                public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
                {
                    return Binding.DoNothing;
                }
    
                #endregion
            }
    
        }
    

    Cette markup extension est en fait l’équivalent XAML de l’opérateur conditionnel de C# (condition ? valeurSiVrai : valeurSiFaux), et peut être utilisée pour toutes sortes de valeurs.

    Une autre option pour réaliser le comportement voulu aurait été de créer des propriétés qui renvoient une valeur de Visibility selon la valeur de IsValueReady, mais ça fait encore 2 propriétés de plus à créer, ce qui alourdit pas mal le ViewModel.

    Voilà, c’est tout pour aujourd’hui… N’hésitez pas à me faire part de vos commentaires ou suggestions 😉

[WPF] Utiliser les InputBindings avec le pattern MVVM

Si vous développez des applications WPF en suivant le design pattern Model-View-ViewModel, vous vous êtes peut-être déjà trouvé confronté au problème suivant : comment, en XAML, lier un raccourci clavier ou une action de la souris à une commande du ViewModel ? Idéalement, on aimerait pouvoir faire comme ça :

    <UserControl.InputBindings>
        <KeyBinding Modifiers="Control" Key="E" Command="{Binding EditCommand}"/>
    </UserControl.InputBindings>

Malheureusement, ce code ne fonctionne pas, pour deux raisons :

  1. La propriété Command n’est pas une DependencyProperty, on ne peut donc pas faire de binding dessus
  2. Les InputBindings ne font pas partie de l’arbre logique ou visuel du contrôle, ils n’héritent donc pas du DataContext

Une solution, bien sûr, serait de passer par le code-behind pour créer les InputBindings, mais en général, dans une application développée selon le pattern MVVM, on préfère éviter d’écrire du code-behind. J’ai longuement cherché des solutions alternatives pour pouvoir le faire en XAML, mais la plupart sont relativement complexes et peu intuitives. J’ai donc finalement créé une markup extension qui permet de se binder à une commande du DataContext, à n’importe quel endroit du code XAML, même si l’élément n’hérite pas du DataContext.

Cette extension s’utilise comme un simple binding :

    <UserControl.InputBindings>
        <KeyBinding Modifiers="Control" Key="E" Command="{input:CommandBinding EditCommand}"/>
    </UserControl.InputBindings>

(Le namespace XML input étant mappé sur le namespace CLR où est déclarée la markup extension)

Pour réaliser cette extension, j’avoue que j’ai un peu triché… j’ai fouillé avec Reflector le code des classes de WPF, afin de trouver des champs privés qui permettraient de récupérer le DataContext de l’élément racine. J’accède ensuite à ces champs par réflexion.

Voici le code :

using System;
using System.Reflection;
using System.Windows;
using System.Windows.Input;
using System.Windows.Markup;

namespace MVVMLib.Input
{
    [MarkupExtensionReturnType(typeof(ICommand))]
    public class CommandBindingExtension : MarkupExtension
    {
        public CommandBindingExtension()
        {
        }

        public CommandBindingExtension(string commandName)
        {
            this.CommandName = commandName;
        }

        [ConstructorArgument("commandName")]
        public string CommandName { get; set; }

        private object targetObject;
        private object targetProperty;

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            IProvideValueTarget provideValueTarget = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            if (provideValueTarget != null)
            {
                targetObject = provideValueTarget.TargetObject;
                targetProperty = provideValueTarget.TargetProperty;
            }

            if (!string.IsNullOrEmpty(CommandName))
            {
                // The serviceProvider is actually a ProvideValueServiceProvider, which has a private field "_context" of type ParserContext
                ParserContext parserContext = GetPrivateFieldValue<ParserContext>(serviceProvider, "_context");
                if (parserContext != null)
                {
                    // A ParserContext has a private field "_rootElement", which returns the root element of the XAML file
                    FrameworkElement rootElement = GetPrivateFieldValue<FrameworkElement>(parserContext, "_rootElement");
                    if (rootElement != null)
                    {
                        // Now we can retrieve the DataContext
                        object dataContext = rootElement.DataContext;

                        // The DataContext may not be set yet when the FrameworkElement is first created, and it may change afterwards,
                        // so we handle the DataContextChanged event to update the Command when needed
                        if (!dataContextChangeHandlerSet)
                        {
                            rootElement.DataContextChanged += new DependencyPropertyChangedEventHandler(rootElement_DataContextChanged);
                            dataContextChangeHandlerSet = true;
                        }

                        if (dataContext != null)
                        {
                            ICommand command = GetCommand(dataContext, CommandName);
                            if (command != null)
                                return command;
                        }
                    }
                }
            }

            // The Command property of an InputBinding cannot be null, so we return a dummy extension instead
            return DummyCommand.Instance;
        }

        private ICommand GetCommand(object dataContext, string commandName)
        {
            PropertyInfo prop = dataContext.GetType().GetProperty(commandName);
            if (prop != null)
            {
                ICommand command = prop.GetValue(dataContext, null) as ICommand;
                if (command != null)
                    return command;
            }
            return null;
        }

        private void AssignCommand(ICommand command)
        {
            if (targetObject != null && targetProperty != null)
            {
                if (targetProperty is DependencyProperty)
                {
                    DependencyObject depObj = targetObject as DependencyObject;
                    DependencyProperty depProp = targetProperty as DependencyProperty;
                    depObj.SetValue(depProp, command);
                }
                else
                {
                    PropertyInfo prop = targetProperty as PropertyInfo;
                    prop.SetValue(targetObject, command, null);
                }
            }
        }

        private bool dataContextChangeHandlerSet = false;
        private void rootElement_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            FrameworkElement rootElement = sender as FrameworkElement;
            if (rootElement != null)
            {
                object dataContext = rootElement.DataContext;
                if (dataContext != null)
                {
                    ICommand command = GetCommand(dataContext, CommandName);
                    if (command != null)
                    {
                        AssignCommand(command);
                    }
                }
            }
        }

        private T GetPrivateFieldValue<T>(object target, string fieldName)
        {
            FieldInfo field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
            if (field != null)
            {
                return (T)field.GetValue(target);
            }
            return default(T);
        }

        // A dummy command that does nothing...
        private class DummyCommand : ICommand
        {

            #region Singleton pattern

            private DummyCommand()
            {
            }

            private static DummyCommand _instance = null;
            public static DummyCommand Instance
            {
                get
                {
                    if (_instance == null)
                    {
                        _instance = new DummyCommand();
                    }
                    return _instance;
                }
            }

            #endregion

            #region ICommand Members

            public bool CanExecute(object parameter)
            {
                return false;
            }

            public event EventHandler CanExecuteChanged;

            public void Execute(object parameter)
            {
            }

            #endregion
        }
    }

Cette solution a cependant une limitation : elle ne fonctionne que pour le DataContext de la racine du XAML. On ne peut donc pas l’utiliser, par exemple, pour définir des InputBindings sur un contrôle dont on redéfinit aussi le DataContext, car la markup extension renverra le DataContext de l’élément racine.

[WPF] Binding sur les paramètres d’application à l’aide d’une Markup extension

Voilà, c’est fait, j’ai créé mon blog sur .NET… j’ai mis le temps, mais j’ai fini par y venir 😉

Je me présente rapidement : Thomas Levesque, 27 ans, ingénieur de formation. Je suis passionné depuis toujours par l’informatique, et plus particulièrement par la technologie .NET, que je suis de très près depuis ses débuts. Comme je suis du genre curieux, je passe pas mal de temps à fouiner dans les docs MSDN et sur le net pour m’auto-former sur les dernières nouveautés du framework. Aujourd’hui, je travaille comme développeur C# pour une PME en région parisienne.

Mais assez parlé de moi, venons-en au sujet qui nous intéresse : le binding sur les paramètres d’application en WPF.

Pour l’utilisateur d’une application, il est plus confortable de ne pas avoir à redéfinir sans arrêt ses préférences (dimensions de la fenêtre, activation de telle ou telle option…) : d’où l’utilité des paramètres d’application, introduits dans la version 2.0 du framework. Pour le développeur, c’est un peu pénible… même avec la classe Settings définie par Visual Studio, il reste encore pas mal de code à écrire pour lire et appliquer les paramètres au démarrage de l’appli, puis les enregistrer avant de quitter.

Dans Windows Forms, on pouvait définir des bindings entre les propriétés des contrôles et les paramètres d’application, mais ça restait peu pratique, et finalement assez peu utilisé (du moins c’est l’impression que j’en ai…).

Avec WPF, ça devient beaucoup plus intéressant… bien qu’il n’y ait pas de documentation “officielle” sur cette pratique, il est tout à fait possible de définir dans le code XAML des bindings sur les paramètres d’application. Par exemple, pour binder les dimensions de la fenêtre sur des paramètres, la méthode qui est présentée sur de nombreux blogs est la suivante :

<Window x:Class="WpfApplication1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:p="clr-namespace:WpfApplication1.Properties"
        Title="Window1"
        Height="{Binding Source={x:Static p:Settings.Default}, Path=Height, Mode=TwoWay}"
        Width="{Binding Source={x:Static p:Settings.Default}, Path=Width, Mode=TwoWay}"
        Left="{Binding Source={x:Static p:Settings.Default}, Path=Left, Mode=TwoWay}"
        Top="{Binding Source={x:Static p:Settings.Default}, Path=Top, Mode=TwoWay}">

(Dans cet exemple, Height, Width, Top et Left sont des paramètres de l’application)

Cette méthode a l’avantage de fonctionner, mais franchement, est-ce que vous vous voyez écrire ça des dizaines de fois, pour chaque paramètre de l’application ? C’est long à écrire, peu intuitif, répétitif, et ça rend le code globalement peu lisible…

Loin de moi l’idée de dénigrer ceux qui ont eu cette idée, bien sûr… mais il est très facile d’améliorer cette méthode, en créant notre propre « markup extension ». Nous allons donc définir une classe qui va hériter de Binding, et servir spécifiquement à lier des propriétés à des paramètres de l’application.

Une « markup extension » (qu’on pourrait traduire approximativement par « balise d’extension ») est un objet qu’on peut utiliser dans le code XAML pour récupérer une valeur. On en utilise en permanence en WPF : Binding, StaticResource, DynamicResource sont des exemples de markup extensions.

On peut facilement définir sa propre markup extension en créant une classe dérivée de MarkupExtension, qui doit implémenter une méthode ProvideValue. En l’occurrence, l’essentiel de ce qu’on veut faire est déjà implémenté dans la classe Binding (qui hérite indirectement de MarkupExtension). Nous allons donc hériter directement de Binding, et simplement initialiser les propriétés qui nous intéressent pour se binder sur les paramètres :

using System.Windows.Data;

namespace WpfApplication1
{
    public class SettingBindingExtension : Binding
    {
        public SettingBindingExtension()
        {
            Initialize();
        }

        public SettingBindingExtension(string path)
            :base(path)
        {
            Initialize();
        }

        private void Initialize()
        {
            this.Source = WpfApplication1.Properties.Settings.Default;
            this.Mode = BindingMode.TwoWay;
        }
    }
}

Notez le suffixe « Extension » à la fin du nom de la classe : par convention, la plupart des markup extensions ont ce suffixe (Binding est une exception qui confirme la règle…). Ce suffixe pourra être omis quand on utilisera la classe en XAML (un peu comme les attributs, dont on omet le suffixe « Attribute » ).

Dans cette classe, on a défini 2 constructeurs, qui correspondent à ceux de Binding. On ne redéfinit pas la méthode ProvideValue, car celle de la classe Binding nous convient parfaitement (et d’ailleurs elle est marquée comme finale (sealed), si bien qu’on ne pourrait pas la redéfinir…). Le code « intéressant », si j’ose dire, est dans la méthode Initialize. On y définit la propriété Source comme étant les paramètres de notre appli, de façon à ce que le Path indiqué pour le binding renvoie le paramètre qui nous intéresse, et Mode = TwoWay, de façon à ce que les paramètres soient automatiquement mis à jour à partir de l’interface. L’intérêt étant de ne pas avoir à redéfinir ces propriétés à chaque fois…

Pour utiliser cette classe, c’est tout simple ! Reprenons l’exemple précédent, en remplaçant les Binding par notre extension SettingBinding :

<Window x:Class="WpfApplication1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:WpfApplication1"
        Title="Window1"
        Height="{my:SettingBinding Height}"
        Width="{my:SettingBinding Width}"
        Left="{my:SettingBinding Left}"
        Top="{my:SettingBinding Top}">

C’est tout de même plus lisible et plus facile à utiliser…

Ah, et bien sûr, pour que ça marche, on n’oublie pas d’enregistrer les paramètres dans l’évènement Exit de l’application…

        private void Application_Exit(object sender, ExitEventArgs e)
        {
            WpfApplication1.Properties.Settings.Default.Save();
        }

Et voilà, les dimensions de la fenêtre seront enregistrées et restaurées à chaque lancement de l’application, sans qu’on ait rien de plus à coder !

Télécharger les sources

Mise à jour : Si vous voulez en savoir plus sur les markup extensions, je vous invite à lire le tutoriel que j’ai écrit suite à ce billet : Les markup extensions en WPF