posté @ Wednesday, December 31, 2008 12:54 PM

Nativement, Silverlight ne prend en compte qu'un nombre limité d'évènements souris :

  • MouseMove / Enter/ Leave
  • MouseLeftButtonDown / Up

Or, dans beaucoup d'applications métiers, certains écrans font apparaître des scrollbars (vue complexe, ListBox, ComboBox, DataGrid...). A la vue de ces scrollbars, l'utilisateur lambda a très souvent le réflexe d'utiliser sa souris pour faire défiler les éléments de haut en bas. Hors Silverlight ne supporte pas ces évènements et les Controles ScrollViewer, DataGrid etc. ne supportent donc pas nativement la molette.

Si vous avez déjà joué avec le Scrolling "programmatique", vous avez peut-être remarqué que beaucoup de contrôles faisant apparaître une ScrollBar n'utilisent pas de ScrollViewer, mais ont leur propre logique de Scrolling (bien souvent non exposée publiquement - c'est le cas notamment du DataGrid) pour diverses raisons (Virtualisation des éléments...) et il ne semble donc pas possible d'avoir une solution "miracle" qui marche dans tous les cas.

L'astuce présentée ici s'appuie sur les fonctionnalités d'automatisation d'interface (voir namespace System.Windows.UIAutomation dans la doc MSDN). En effet, l'API d'automatisation de Microsoft propose un moyen d'exposer des fonctionnalités de Scrolling aux lecteurs / contrôleurs d'écrans et framework de test. Chaque contrôle ayant une logique de scrolling peut donc l'exposer via son AutomationPeer : c'est ce que font tous les contrôles de ce type de Silverlight core, Silverlight Toolkit, ainsi que ceux des auteurs de CustomControls consciencieux. Voici par exemple un extrait de code d'une SuggestBox supportant l'automation :

public class SuggestBox : Control
    {
        private class SuggestBoxAutomationPeer : FrameworkElementAutomationPeer
        {
            SuggestBox _automated;
            public SuggestBoxAutomationPeer(SuggestBox automated)
                : base(automated)
            {
                _automated = automated;
            }

            public override object GetPattern(PatternInterface patternInterface)
            {
                if (patternInterface == PatternInterface.Scroll)
                {
                    if (_automated.IsPopupOpen && _automated._childListBox != null)
                        return FrameworkElementAutomationPeer.CreatePeerForElement(_automated._childListBox).GetPattern(patternInterface);
                }
                return base.GetPattern(patternInterface);
            }
        }
        protected override System.Windows.Automation.Peers.AutomationPeer OnCreateAutomationPeer()
        {
            return new SuggestBoxAutomationPeer(this);
        }
    }
 

Pour exploiter celà, la première chose à faire, est d'implémenter notre scrolling programmatique a un endroit centralisé, qui sera appelable par JavaScript. Dans cet exemple, nous prendrons l'objet Application lui-même. Nous aurons besoin simplement d'une méthode marquée [ScriptableMember] et d'enregistrer l'objet application comme un ScriptableObject:

IEnumerable<UIElement> SelfAndAncestors(UIElement source)
        {
            var current = source;
            do
            {
                yield return current;
                current = VisualTreeHelper.GetParent(current) as UIElement;
            }
            while (current != null);
        }

        [ScriptableMember]
        public void OnMouseWheel(double delta)
        {          

            IEnumerable<UIElement> candidates;
            
            candidates = VisualTreeHelper.FindElementsInHostCoordinates(_currentMousePosition, RootVisual);
            // Si un element a le focus, on gere les candidats 
            // en placant en premier l'élément ayant le focus ainsi que ses parents
            var focused = FocusManager.GetFocusedElement() as UIElement;
            if (focused != null)
                candidates = SelfAndAncestors(focused)
                    .Concat(candidates);
            var scrollProvider = (from elem in candidates
                                  let automationPeer = FrameworkElementAutomationPeer.CreatePeerForElement(elem)
                                  where automationPeer != null
                                  let scroller = automationPeer.GetPattern(PatternInterface.Scroll) as IScrollProvider
                                  where scroller != null
                                  select scroller).FirstOrDefault();

            if (scrollProvider != null && scrollProvider.VerticallyScrollable)
            {

                if (delta > 0)
                    scrollProvider.Scroll(System.Windows.Automation.ScrollAmount.NoAmount,
                        System.Windows.Automation.ScrollAmount.SmallDecrement);
                else
                    scrollProvider.Scroll(System.Windows.Automation.ScrollAmount.NoAmount,
                        System.Windows.Automation.ScrollAmount.SmallIncrement);
            }
            
        }
Le paramêtre delta aura pour valeur le delta de l'évènement JavaScript MouseWheel intercepté. 

La partie la plus importante de la méthode est la requête Linq qui parcoure la liste des candidats (liste d'UIElement produient par VisualTreeHelper.FindElementsInHostCoordinates et par le FocusManager. Ce que l'on recherche, c'est le premier UIElement ayant un automationPeer supportant le scrolling. Une foit qu'on l'a trouvé, on manipule simplement le scrollProvider pour lui indiquer ce que l'on souhaite faire. Cette méthode fonctionne aussi bien avec les ScrollViewer, DataGrid, ListBox, SuggestBox etc, bien que leur logique de scrolling soit différente (et souvent inaccessible publiquement).

Il ne reste plus qu'à brancher ce code sur un évènement JavaScript.

D'abord, nous enregistrons l'objet application dans le dictionnaire d'objet Scriptable dans la méthode Application_Startup():

HtmlPage.RegisterScriptableObject("SilverlightApp", this);

Ensuite dans la page HTML hôte de notre application, nous effectuons les tâches suivantes :

  • Modification du tag object:
<div id="silverlightControlHost">
        <object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%">
            ...
<param name="onload" value="onSilverlightLoad" /> <a href="http://go.microsoft.com/fwlink/?LinkID=124807" style="text-decoration: none;"> <img src="http://go.microsoft.com/fwlink/?LinkId=108181" alt="Get Microsoft Silverlight" style="border-style: none"/> </a> </object> <iframe style='visibility:hidden;height:0;width:0;border:0px'></iframe> </div>

Ce qu'il faut rajouter, c'est le tag <param name="onload" />.

  • Implémenter la fonction onSilverlightLoad:
var slApp;
        /** This is high-level function.
        * It must react to delta being more/less than zero.
        */
        function handle(delta) {
            slApp.OnMouseWheel(delta);
        }

        /** Event handler for mouse wheel event.
        */
        function wheel(event) {
            var delta = 0;
            if (!event) /* For IE. */
                event = window.event;
            if (event.wheelDelta) { /* IE/Opera. */
                delta = event.wheelDelta / 120;
                /** In Opera 9, delta differs in sign as compared to IE.
                */
                if (window.opera)
                    delta = -delta;
            } else if (event.detail) { /** Mozilla case. */
                /** In Mozilla, sign of delta is different than in IE.
                * Also, delta is multiple of 3.
                */
                delta = -event.detail / 3;
            }
            /** If delta is nonzero, handle it.
            * Basically, delta is now positive if wheel was scrolled up,
            * and negative, if wheel was scrolled down.
            */
            if (delta)
                handle(delta);
            /** Prevent default actions caused by mouse wheel.
            * That might be ugly, but we handle scrolls somehow
            * anyway, so don't bother here..
            */
            if (event.preventDefault)
                event.preventDefault();
            event.returnValue = false;
        }



        function onSilverlightLoad(sender, args) {

            var slCtl = sender.getHost();
            slApp = slCtl.Content.SilverlightApp;
            /** Initialization code. 
            * If you use your own event management code, change it as required.
            */
            if (window.addEventListener)
            /** DOMMouseScroll is for mozilla. */
                window.addEventListener('DOMMouseScroll', wheel, false);
            /** IE/Opera. */
            window.onmousewheel = document.onmousewheel = wheel;

        }
Et voilà, that's done :).

Commentaires :

# re: [Silverlight 2] Molette de la souris, solution non intrusive
Ecrit par Geoffrey DANIEL le 1/7/2009 9:35 PM
Nice !
J'avais pas pense a passer par du JS...

Ecrire un commentaire :

Titre :*
Nom *
Email
Url
Commentaire : *  


Please add 1 and 2 and type the answer here: