Accueil > jQuery, Silverlight > Silverlight Toast notification in & out of browser (via jQuery plugin)

Silverlight Toast notification in & out of browser (via jQuery plugin)

Nous avons vu dans une première partie comment fonctionnent les notifications toast out of browser, dans une deuxième partie comment faire un plugin jquery qui nous permet d’avoir le même comportement dans le browser pour une page web, il est temps maintenant de donner à nos applications silverlight utilisant les notifications toast de fonctionner elles aussi dans le browser.

L’objet NotificationWindow de Silverlight ne peut pas être utilisé dans le browser et y déclenche une exception. Pour afficher des toasts dans le browser, nous allons donc utiliser le plugin jQuery que nous avons codé dans un précédent article. Pour bien faire, il faudrait que la notification OOB et la notification dans le browser aient le même rendu visuel et que l’utilisation dans ou hors du browser soit complètement encapsulée pour être le moins intrusif possible dans l’application principale. L’encapsulation sera réalisée par une Factory qui renverra le bon type concret de toast en fonction de l’exécution courante.

Pour la visualisation dans le browser, plusieurs solutions s’offrent à nous mais elles ont toutes un point commun: on part d’un FrameworkElement défini dans notre application SL et on veut l’afficher dans le browser. Nous allons donc matérialiser cela par une interface IToastConverter. Les classes qui implémentent cette interface auront pour rôle de convertir le FrameworkElement représentant la notification en entité affichable dans un navigateur. L’idéal serait de convertir le XAML directement en HTML mais cela dépasse le cadre de cet article. Nous allons pour l’instant tout simplement utiliser la technique vue dans mon précédent billet: Render Silverlight control to image: WriteableBitmap. On obtiendra avec cette technique une image représentant notre contenu XAML qu’on pourra facilement afficher dans le navigateur en l’intégrant dans une balise image toute simple. L’utilisation de l’interface nous permettra plus tard d’étendre ce mécanisme en codant d’autres implémentations de IToastConverter.

On obtient le diagramme suivant:

La classe ToastFactory va instancier la bonne implémentation de Toast suivant le mode d’exécution actuel. Cette classe conserve également un cache des toasts déjà configurés côté html à des fins d’optimisation. Dans le cas d’une application s’exécutant OOB, on utilisera simplement l’objet NotificationWindow inclus dans Silverlight comme dans le premier article de la série. Dans le cas in browser, nous nous appuyons sur notre plugin jQuery mais qui dit plugin jQuery dit jQuery… Comment être sûr que la page dans laquelle le plugin est instancié a chargé la librairie jQuery? Pour utiliser le plugin il faudra également le déclarer dans la page. C’est le rôle de la classe BrowserToastCreator.
Elle contient 2 méthodes: LoadJquery et LoadJqueryToastPlugin. Ces 2 méthodes agissent de la même manière: elles récupèrent le contenu d’un fichier javascript inclus dans la DLL comme ressource embarquée et l’écrit dans la page en utilisant HtmlPage.Window.Eval(script). Une petite particularité pour jQuery, on vérifie qu’il n’a pas déjà été inclus dans la page en ajoutant if(typeof($)==’undefined’) avant l’exécution du script. La méthode ressemble à ça:

        private static void LoadJquery()
        {
            if (!jQueryLoaded)
            {
                using (System.IO.Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ToastMaker.Script.jquery-1.5.min.js"))
                {
                    using (System.IO.StreamReader reader = new System.IO.StreamReader(stream))
                    {
                        string script = "if(typeof($)=='undefined'){";
                        script += reader.ReadToEnd()+"}";
                        HtmlPage.Window.Eval(script);
                        jQueryLoaded = true;
                    }
                }
            }
        }

A ce stade, on a donc chargé dans la page la librairie jQuery et notre plugin de notification.

Passons maintenant à l’implémentation concrète du toast in browser:

Le toast doit avant tout être initialisé, ça sera le travail de la méthode Initialize(HtmlElement container) de l’interface IToastConverter. Le point commun de toutes les implémentations de converters est que la notification sera contenue dans une div et que cette div sera la cible du plugin de notification. Le constructeur de la classe InBrowserToast va donc créer et injecter cette div dans la page, appeler la méthode Initialize de son IToastConverter, et appeler le plugin pour déclarer la div comme une cible de notification. Pour faciliter cette tâche, nous avons ajouté une fonction dans le fichier javascript du plugin qui nous permettra de lancer un callback à notre objet silverlight quand le constructeur du plugin aura terminé son exécution.

        public IToastConverter Converter { get; protected set; }
 
        private readonly object _lock = new object();
        private bool divOK;
 
        internal InBrowserToast(string id, IToastConverter converter):base(id)
        {
            Converter = converter;
            HtmlPage.RegisterScriptableObject(Id, this);
            HtmlElement div = HtmlPage.Document.CreateElement("div");
            div.Id = Id;
            div.SetAttribute("style", "background-color:black;");
            Converter.Initialize(div);
            HtmlPage.Document.Body.AppendChild(div);
            lock (_lock)
            {
                HtmlPage.Window.Invoke("CreateToast", new string[] { HtmlPage.Plugin.Id, Id });
                if(!divOK)
                    Monitor.Wait(_lock);
            }
        }
 
        [ScriptableMember]
        public void DivOKCallback()
        {
            lock (_lock)
            {
                divOK = true;
                Monitor.Pulse(_lock);
            }
        }

On utilise ici Monitor.Wait au moment d’appeler la fonction javascript "CreateToast". Cela permet de mettre en pause l’exécution du code silverlight jusqu’à ce que l’objet de lock soit débloqué par la méthode Monitor.Pulse. On est donc sûrs en sortant du constructeur que la div a été injectée dans la page, que le converter est initialisé, et que le plugin de notification a été appelé.

Le code de la fonction CreateToast est le suivant:

function CreateToast(pluginId, ToastId) {
    $("#" + ToastId).toast();
    var control = document.getElementById(pluginId);
    eval("control.content." + ToastId + ".DivOKCallback()");
}

La ligne eval("control.content." + ToastId + ".DivOKCallback()") permet d’appeler la fonction de callback dans le code c#.

La fonction Initialize du converter que nous utilisons comme exeple est la suivante:

        private string IdImg;
 
        public void Initialize(HtmlElement container)
        {
            HtmlElement img = HtmlPage.Document.CreateElement("img");
            IdImg = container.Id + "_img";
            img.Id = IdImg;
            container.AppendChild(img);
        }

Elle se contente d’ajouter à la div container une balise img qui acceuillera l’image de notre contenu de notification.

Il ne nous reste plus à ce stade qu’à afficher la notification. Ici tout se passe dans le converter et le code de la classe InBrowserToast se contente d’initialiser les différentes valeurs des paramètres et à les répercuter sur le javascript. Une fois tous les paramètres répercutés, elle déclenche l’affichage du toast en appelant la fonction showToast() du plugin jQuery.

        public override void Show()
        {
            if (Content != null)
            {                
                Converter.Convert(Content);
 
                if (Content.Width != 0)
                {
                    string scriptWidth = "$('#" + Id + "').setToastWidth(" + Content.Width + ")";
                    HtmlPage.Window.Eval(scriptWidth);
                }
                if (Content.Height != 0)
                {
                    string scriptHeight = "$('#" + Id + "').setToastHeight(" + Content.Height + ")";
                    HtmlPage.Window.Eval(scriptHeight);
                }                
                if (Duration != 0)
                {
                    string scriptDuration = "$('" + Id + "').setToastDelay(" + Duration + ")";
                    HtmlPage.Window.Eval(scriptDuration);
                }                                
                string script = "$('#" + Id + "').showToast()";
                HtmlPage.Window.Eval(script);
            }
        }

L’appel à Converter.Convert(Content) est chargé de convertir le contenu effectif du toast pour l’afficher dans la page. Dans notre cas, on utilise un rendu sous forme d’image et la méthode de conversion se présente comme ceci:

        public void Convert(FrameworkElement Content)
        {
            MeasureArrange(Content,Content);
 
            byte[] bytes = ControlToPng.RenderBytes(Content);
            string base64Content = System.Convert.ToBase64String(bytes);
            HtmlElement img = HtmlPage.Document.GetElementById(IdImg);
            img.SetAttribute("src", "data:image/png;base64," + base64Content);
        }
 
        private void MeasureArrange(UIElement el, FrameworkElement parentContent)
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(el); i++)
            {
                FrameworkElement child = VisualTreeHelper.GetChild(el, i) as FrameworkElement;
                if (child != null)
                {
                    child.Measure(new Size(parentContent.Width, parentContent.Height));
                    child.Arrange(new Rect(0, 0, parentContent.Width, parentContent.Height));
                    child.UpdateLayout();
 
                    if (VisualTreeHelper.GetChildrenCount(child) != 0)
                        MeasureArrange(child,parentContent);
                }
            }
            el.Measure(new Size(parentContent.Width, parentContent.Height));
            el.Arrange(new Rect(0, 0, parentContent.Width, parentContent.Height));
            el.UpdateLayout();
        }

L’appel à la méthode MeasureArrange est nécessaire car le FrameworkElement composant la notification a été instancié en mémoire mais n’a pas été affiché. Il faut donc appeler explicitement les méthodes qui règlent la disposition des élements visuels inclus. Nous utilisons ensuite la classe fournie par Pierre Belin ICI pour transformer le WriteableBitmap en image PNG puis nous convertissons cette image en base64. Ici j’avoue que j’ai choisi la solution de simplicité en utilisant les data uri qui permettent d’embarquer une image directement dans du html en base 64. Le problème de cette solution est qu’elle ne fonctionne pas sous IE (étonemment même pas sous IE9 d’ailleurs alors qu’elles sont censé être prises en charge depuis IE8). Pour ce navigateur, il vous faudra créer une page sur le serveur qui converti à la volée une chaîne en base 64 pour renvoyer l’image et ainsi utiliser une url classique. (il y a beaucoup d’exemples sur le net je vous laisse googler si ça vous intéresse).

Notre notification est maintenant prête il nous suffit maintenant de préparer un contenu et de l’utiliser par exemple grâce au code suivant:

    public partial class MainPage : UserControl
    {
        private Toast toast;
 
        public MainPage()
        {
            InitializeComponent();
            toast = ToastFactory.GetToast("TestToast", new WriteableBitmapConverter.WriteableBitmapConverter());
        }
 
        private void button1_Click(object sender, RoutedEventArgs e)
        {                        
            toast.Content = new NotificationContent { HeaderText = "Header du toast", ContentText = "Toast créé en silverlight!!" };
            toast.Show();
        }
    }

Avec ce mécanisme, nous pouvons afficher des notifications ayant le même rendu à la fois dans le browser et out of browser. Le mécanisme de IToastConverter rend la solution personnalisable au niveau du rendu et permet d’en faire à peu près ce qu’on veut. Pour aller plus loin, on pourrait imaginer de gérer les évènements click sur la notification de manière générique il suffirait pour ça de créer un gestionnaire de click côté javascript qui appèlerait un callback dans le code c# qui déclencherait lui même un event permettant à l’application de gérer le click. Si ça intéresse quelqu’un dites le dans les commentaires et je publierai le code de cette gestion d’évènements.

L’ultime étape sera bien sûr de transformer directement le xaml en html mais pour l’instant je n’ai trouvé aucune solution satisfaisante pour gérer ce cas correctement. Des convertisseurs existent mais aucun n’est capable de rendre correctement tous les contrôles XAML. Une autre solution serait de faire implémenter directement l’interface IToastConverter à une page XAML et d’insérer un second plugin silverlight dans la page dynamiquement puis de faire communiquer les 2 plugins par l’intermédiaire de javascript. Dans cette solution en revanche, les 2 applications devront connaître le type définissant l’élément à afficher dans la notification. J’ai encore d’autre solutions en tête alors laissez parler votre imagination :)

Ceux qui veulent une démo peuvent voir le code en action ICI (rappelez vous IE ne gère pas les data uri et je n’ai pas fait de proxy pour gérer le cas pour la démo donc testez avec firefox!)
Le code en lui même est disponible ICI

Enjoy…

About these ads
  1. Josefina
    12/10/2011 à 9:33  

    I need this post in english!!

  2. 13/10/2011 à 3:33  

    I am currently in the process of translating all my posts in English for my future English blog. It will be on line soon.

  1. No trackbacks yet.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

Suivre

Recevez les nouvelles publications par mail.

Rejoignez 297 autres abonnés

%d bloggers like this: