Utiliser la molette de la souris en Silverlight avec le Windowless activé sous Firefox et Chrome

En Silverlight, la molette de la souris fonctionne très bien sous tous les principaux navigateurs (Internet Explorer, Firefox, Chrome, etc.) lorsque le Windowless n’est pas activé. Lorsque ce dernier est activé, la tâche se complique puisque les navigateurs qui se basent sur NAPI (Firefox et Chrome) ne permettent plus à Silverlight de gérer la molette de la souris. Cet article permet grâce au DOM de récupérer l'événement lié à la molette auprès du navigateur.

Cet article se base sur le code publié sur Compiled Experience auquel j’ai ajouté quelques modifications pour qu’il prenne en compte les éléments dérivant de ItemsControl.

Pour faciliter l’intégration de ce comportement dans votre application, nous allons créer un “Behavior” à ajouter à votre Listbox, Combobox, etc.

Le code se divise en 2 parties :

  • Le code “helper” permettant d’aller chercher les informations liées à l’utilisation de la molette auprès du DOM.
  • Le Behavior en lui même faisant appel au “helper” et qui sera ajouter aux contrôles en XAML.

Le code “Helper”

public class MouseWheelEventArgs : EventArgs
{
    public Point Location { get; private set; }
    public double Delta { get; private set; }
    public bool Handled { get; set; }
    public MouseWheelEventArgs(double delta)
        : this(delta, new Point())
    {
    }
    public MouseWheelEventArgs(double delta, Point location)
    {
        Delta = delta;
        Location = location;
    }
}

public class MouseWheelHelper
{
    private static Worker MouseWheelWorker;
    private bool isMouseOver;

    public MouseWheelHelper(UIElement element)
    {
        if (MouseWheelWorker == null)
            MouseWheelWorker = new Worker();
    
        MouseWheelWorker.Moved += HandleMouseWheel;
    
        element.MouseEnter += HandleMouseEnter;
        element.MouseLeave += HandleMouseLeave;
        element.MouseMove += HandleMouseMove;
    }
    
    public event EventHandler<MouseWheelEventArgs> Moved;
    
    private void HandleMouseWheel(object sender, MouseWheelEventArgs args)
    {
        if (isMouseOver)
            Moved(this, args);
    }
    
    private void HandleMouseEnter(object sender, EventArgs e)
    {
        isMouseOver = true;
    }
    
    private void HandleMouseLeave(object sender, EventArgs e)
    {
        isMouseOver = false;
    }
    
    private void HandleMouseMove(object sender, EventArgs e)
    {
        isMouseOver = true;
    }
    
    private class Worker
    {
        public Worker()
        {
            if (!HtmlPage.IsEnabled)
                return;
            HtmlPage.Window.AttachEvent("DOMMouseScroll", HandleMouseWheel);
            HtmlPage.Window.AttachEvent("onmousewheel", HandleMouseWheel);
            HtmlPage.Document.AttachEvent("onmousewheel", HandleMouseWheel);
        }
    
        public event EventHandler<MouseWheelEventArgs> Moved;
    
        private void HandleMouseWheel(object sender, HtmlEventArgs args)
        {
            double delta = 0;
            var eventObj = args.EventObject;
    
            if (eventObj.GetProperty("wheelDelta") != null)
            {
                delta = ((double)eventObj.GetProperty("wheelDelta")) / 120;
                if (HtmlPage.Window.GetProperty("opera") != null)
                    delta = -delta;
            }
            else if (eventObj.GetProperty("detail") != null)
            {
                delta = -((double)eventObj.GetProperty("detail")) / 3;
    
                if (HtmlPage.BrowserInformation.UserAgent.IndexOf("Macintosh") 
                        != -1)
                    delta = delta * 3;
            }
    
            if (delta == 0 || Moved == null)
                return;
    
            var wheelArgs = new MouseWheelEventArgs(delta);
            Moved(this, wheelArgs);
    
            if (wheelArgs.Handled)
                args.PreventDefault();
        }
    }
}
  • La classe MouseWheelEventArgspermet de remonter les informations du défilement de la molette de la classe Worker à la classe MouseWheelHelper.
  • La classe MouseWheelHelper est la classe qui sera appelée par le “Behavior".
  • La classe **Worker **quant à elle va chercher dans le DOM les informations du défilement de la molette en fonction du navigateur utilisé par le client.

Pour récupérer ces informations on s’abonne aux événements JavaScript DOMMouseScroll pour Firefox et onmousewheel pour Chrome et les autres. Lorsque la molette est utilisée, les informations sont transmises du JavaScript vers le Silverlight.

Le code suivant représente le “Behavior” qui sera appliqué aux contrôles en XAML. Dans l’article de base, ce “Behavior” ne s’appliquait qu'à un ScrollViewer, ce qui rend l’utilisation de la molette sur un TreeView impossible. En appliquant ce “Behavior” sur n’importe quel UIElement, on permet ce comportement.

En revanche, pour bénéficier des propriétés VerticalOffset, HorizontalOffset et des méthodes ScrollToVerticalOffset et ScrollToHorizontalOffset, il nous faut un objet de type ScrollViewer. Heureusement, pour les contrôles dérivant de ItemsControl (TreeView, ListBox, etc.) le toolkit Silverlight fournit la méthode d’extension GetScrollHost() permettant de récupérer leurs objets ScrollViewer. N’oubliez pas d’ajouter une référence vers la DLL System.Windows.Controls.Toolkit à votre projet.

public class MouseWheelScrollBehavior : Behavior<UIElement>
{
    private MouseWheelHelper helper;

    protected override void OnAttached()
    {
        base.OnAttached();
    
        helper = new MouseWheelHelper(AssociatedObject);
        helper.Moved += OnMouseWheelMoved;
    }
    
    private void OnMouseWheelMoved(object sender, MouseWheelEventArgs e)
    {
        ScrollViewer sv;
        double verticalOffset;
        double horizontalOffset;
    
        if (AssociatedObject is ItemsControl)
        {
            ItemsControl tr = AssociatedObject as ItemsControl;
            sv = tr.GetScrollHost();
        }
        else
        {
            sv = AssociatedObject as ScrollViewer;
        }
    
        if (sv != null)
        {
            verticalOffset = sv.VerticalOffset;
            horizontalOffset = sv.VerticalOffset;
            sv.ScrollToVerticalOffset(sv.VerticalOffset + (e.Delta * -10));
            sv.ScrollToHorizontalOffset(sv.HorizontalOffset + (e.Delta * -10));
        }
    }
    
    protected override void OnDetaching()
    {
        base.OnDetaching();
    
        helper.Moved -= OnMouseWheelMoved;
    }
}

Une fois le “Behavior” créé, il faut l’appliquer à chaque ListBox, TreeView, ComboBox, etc… de l’application comme ceci :

<ListBox>
  <i:Interaction.Behaviors>
    <behaviors:MouseWheelScrollBehavior/>
  </i:Interaction.Behaviors>
  <!-- Code -->
</ListBox>

i représente System.Windows.Interactivity (n’oubliez pas d’ajouter la référence au projet) et behavior représente le namespace qui contient le “Behavior” créé ci-dessus.

Pour éviter cette tâche fastidieuse et sujette à l’erreur, la meilleure solution est de créer un style par défaut pour ces contrôles :

<Style TargetType="ListBox">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="ListBox">
        <ScrollViewer>
          <i:Interaction.Behaviors>
            <behaviors:MouseWheelScrollBehavior/>
          </i:Interaction.Behaviors>
          <ItemsPresenter/>
        </ScrollViewer>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

Ainsi toutes les ListBox de l’application auront ce comportement.


Voir également