CodeS-SourceS
Rechercher un code, un tuto, une réponse

Designer Verbs et Smart Tags

Août 2017


Designer Verbs et Smart Tags

Préambule


Les commandes (Verbs) et les actions (Smart Tags) du Designer permettent aux Designers de fournir un ensemble d'actions pour un composant au moment du Design. L'utilisateur peut ensuite accéder à ces actions dans la fenêtre des propriétés ou encore dans la petite flèche qui ouvre un menu déroulant sur chaque contrôle. Il existe une troisième catégorie de commandes : les MenuCommands qui permettent, par le biais du service IMenuCommandService d'ajouter des commandes de menu dans ceux de l'environnement de développement. Le MenuCommand ne décrit pas son menu ni son texte mais simplement une commande de menu qui peut être associée à un menu existant.



Introduction


Les commandes ( Verbs ) et les actions (SmartTags) du Designer permettent aux Designers de fournir un ensemble d'actions pour un composant au moment du Design. L'utilisateur peut ensuite accéder à ces actions dans la fenêtre des propriétés ou encore dans la petite flèche qui ouvre un menu déroulant sur chaque contrôle. Il existe une troisième catégorie de commandes : les MenuCommands qui permettent, par le biais du service IMenuCommandService d'ajouter des commandes de menu dans ceux de l'environnement de développement. Le MenuCommand ne décrit pas son menu ni son texte mais simplement une commande de menu qui peut être associée à un menu existant.

Les deux modèles d'ajout de commandes


Le modèle Push : le Designer doit récupérer un service lui permettant d'ajouter ses commandes par ce service.

Le modèles Pull : un service demande au Designer ses commandes disponibles.

Designer


Que ce soit pour les Smart Tags ou pour les DesignerVerbs, il sera nécessaire de créer une classe dérivant de ControlDesigner/ComponentDesigner afin d'exposer les DesignersVerbs ou les SmartTags, puis de l'appliquer à la classe du Contrôle par le biais de l'attribut Designer.

Designer Verbs


Les Designers Verbs sont les liens cliquables qui apparaissent dans la fenêtre des propriétés de Visual Studio et éventuellement dans le menu contextuel du composant, dans le menu « Affichage » / « View » et dans les SmartTags.

Une fois que vous avez créé votre dérivée de ControlDesigner / ComponentDesigner, il vous faudra redéfinir la propriété Verbs qui renvoie un DesignerVerbCollection qui contient tous les DesignerVerb qui seront disponibles quand le composant sera sélectionné.


La démarche sera la suivante :
  • Créer un nouveau DesignerVerbCollection
  • Utiliser la méthode Add de cette nouvelle collection afin d'ajouter des DesignerVerb
  • Chaque nouveau DesignerVerb sera construit avec :


o le texte qui s'affichera dans le lien de la fenêtre des propriétés, le menucontextuel et le SmartTag

o un EventHandler qui contiendra la méthode à appeler pour exécuter l'action associée
  • Pour chaque DesignerVerb, créer la méthode de traitement qui sera exécutée au click sur le Verb. Cette méthode aura la signature traditionnelle d'un EventHandler, c'est-à-dire un paramètre sender de type Object et un paramètre e de type EventArg. Le paramètre sender recevra une instance de DesignerVerb.


Si les DesignerVerb n'apparaissent pas dans la fenêtre de propriétés mais uniquement dans les SmartTags et/ou dans le menucontextuel, cliquez à droite sur cette fenêtre et assurez vous que l'item « Commandes » / « Commands » est bien coché.

Exemple :
///	 <summary>
///	 Ce contrôle démontre l'utilisation des Designer Verbs
///	 (Designer) : permet de préciser le Designer associé à ce contrôle
///	 (par défaut ControlDesigner ou ComponentDesigner)
///	 </summary>
[Designer(typeof(VerbsUserControlDesigner),typeof(IDesigner))]
public partial class VerbsUserControl : UserControl
{
    public VerbsUserControl()
    {
        InitializeComponent();
    }

    private int myVar;

    public int MyProperty
    {
        get { return myVar; }
        set { myVar = value; }
    }
}
 
///	 <summary>
///	 Le designer associé à ce contrôle
///	 Pour un contrôle, le Designer doit être une classe dérivant de ControlDesigner
///	 </summary>
private class VerbsUserControlDesigner : ControlDesigner
{
    //service de changement des composants
    private IComponentChangeService compChangeSrv = null;

    ///	 <summary>
    ///	 La méthode exécutée par un Designer Verb a
    ///	 la signature d'un handler d'évènement
    ///	 </summary>
    ///	 <param name="sender"></param>
    ///	 <param name="e"></param>
    private void MonDesignerVerb(object sender, EventArgs e)
    {
        MessageBox.Show("Exécution du Designer Verb");
    }

    ///	 <summary>
    ///	 Cette méthode permet de définir une propriété et d'en avertir
    ///	 tout l'environnement de développement
    ///	 </summary>
    ///	 <param name="sender"></param>
    ///	 <param name="e"></param>
    private void VerbSetProperty1(object sender, EventArgs e)
    {
        //récupère le descripteur de propriété de la propriété à définir
        PropertyDescriptor p = TypeDescriptor.GetProperties(this.Control)["MyProperty"];
        //affecte la valeur à la propriété
        //(SetValue déclenche automatiquement le service de changement de composants)
        p.SetValue(this.Control, 12);
    }
    
    ///	 <summary>
    ///	 Cette méthode définit une propriété sans en avertir
    ///	 l'environnement donc la fenêtre de propriétés ne se rafraîchira pas
    ///	 </summary>
    ///	 <param name="sender"></param>
    ///	 <param name="e"></param>
    private void VerbSetProperty2(object sender, EventArgs e)
    {
        ((VerbsUserControl)this.Control).MyProperty = 13;
    }
    
    ///	 <summary>
    ///	 Cette méthode définit une propriété en avertissant manuellement
    ///	 le service de changement des composants
    ///	 </summary>
    ///	 <param name="sender"></param>
    ///	 <param name="e"></param>
    private void VerbSetProperty3(object sender, EventArgs e)
    {
        //récupère le descripteur de la propriété
        PropertyDescriptor p = TypeDescriptor.GetProperties(this.Control)["MyProperty"];
        //récupère la valeur avant affectation de la propriété
        int	 oldValue = ((VerbsUserControl)this.Control).MyProperty;
        //avertit que la propriété va être changée
        compChangeSrv.OnComponentChanging(this.Control, p);
        //change la propriété
        ((VerbsUserControl)this.Control).MyProperty = 14;
        //avertit que la propriété a été changée
        compChangeSrv.OnComponentChanged(this.Control, p, oldValue,((VerbsUserControl)this.Control).MyProperty);
    }
    
    ///	 <summary>
    ///	 Cette méthode définit une propriété en avertissant manuellement
    ///	 le service de changement des composants et en créant
    ///	 une transaction d'annulation
    ///	 </summary>
    ///	 <param name="sender"></param>
    ///	 <param name="e"></param>
    private	 void VerbSetProperty4(object sender, EventArgs e)
    {
        //récupère le service d'hôte de l'environnement
        IDesignerHost host = (IDesignerHost)this.GetService(typeof(IDesignerHost));
        //récupère le descripteur de la propriété
        PropertyDescriptor p = TypeDescriptor .GetProperties(this.Control)["MyProperty"];
        //une nouvelle transaction
        DesignerTransaction trans = null;
        try
        {
            //valeur de la propriété avant affectation
            int	 oldValue = ((VerbsUserControl)this.Control).MyProperty;
            try
            {
                //crée une nouvelle transaction d'annulation
                trans = host.CreateTransaction("Une action d'annulation");
                //avertit que la propriété va être changée
                compChangeSrv.OnComponentChanging(this.Control, p);
                //change la propriété
                ((VerbsUserControl)this.Control).MyProperty = 15;
            }
            catch
            {
                //si erreur, on annule le changement
                trans.Cancel();
                return;
            }
            //avertit que la propriété a été changée                   
            compChangeSrv.OnComponentChanged(this.Control, p, oldValue, ((VerbsUserControl)this.Control).MyProperty);
         }
        finally
        {
            //si pas d'erreur, on commit la transaction d'annulation
            trans.Commit();
        }
    }

    public	 override	 void Initialize(IComponent component)
    {
        base.Initialize(component);

        //récupère le service de changement de composant
        compChangeSrv = (IComponentChangeService)this.GetService(typeof(IComponentChangeService));
    }

    public override System.ComponentModel.Design.DesignerVerbCollection Verbs
    {
        get
        {
            //liste des Designer Verbs
            DesignerVerbCollection retVerbs = newDesignerVerbCollection();
            //crée les Verbs en passant un texte qui sera affiché
            //dans le menu contextuel et dans la partie Commands
            //de la fenêtre de propriétés
            //et la méthode qui sera exécutée lors du click
            retVerbs.Add(newDesignerVerb("Mon DesignerVerb",MonDesignerVerb));
            retVerbs.Add(newDesignerVerb("Set Property par TypeDescriptor",VerbSetProperty1));
            retVerbs.Add(newDesignerVerb("Set Property sans IComponentChangeService",VerbSetProperty2));
            retVerbs.Add(newDesignerVerb("Set Property avec IComponentChangeService",VerbSetProperty3));
            retVerbs.Add(newDesignerVerb("Set Property avec Undo",VerbSetProperty4));
            return retVerbs;
        }
    }

    ///	 <summary>
    ///	 Dessine sur le contrôle au moment du Design
    ///	 </summary>
    ///	 <param name="pe"></param>
    protected override void OnPaintAdornments(PaintEventArgs pe)
    {
        base.OnPaintAdornments(pe);

        //dessine une bordure en pointillés autour du contrôle pendant le design.
        Pen	 p = newPen(Brushes.Green);
        p.DashStyle = System.Drawing.Drawing2D.DashStyle.DashDot;
        pe.Graphics.DrawRectangle(p, 0, 0, this.Control.Width - 1, this.Control.Height - 1);
        p.Dispose();
    }
}

Smart Tags

Introduction


Les Smart Tags permettent aux composants et aux contrôles d'afficher des commandes et des informations en fonction du contexte. En fait, il s'agit presque d'un remplacement des DesignersVerbs car les SmartTags permettent de spécifier si on les affiche dans leur paneldéroulant habituel et/ou dans le menucontextuel du contrôle et/ou dans les liens clickables de la fenêtre des propriétés.

Le modèle objet DesignerAction


Voici les principales classes de ce modèle :
  • DesignerActionList : contient une liste d'items nécessaires à la construction du panel SmartTag
  • DesignerActionService : service permettant la gestion des items de SmartTags pour tous les composants
  • DesignerActionItem : représente un item dans un panel de SmartTag (classe de base pour les 4 classes ci-dessous)
  • DesignerActionHeaderItem : dérivé de DesignerActionItem. Représente un item d'entêtestatique dans un panel de SmartTags
  • DesignerActionTextItem : dérivé de DesignerActionItem. Représente un item textuel statique dans un panel de SmartTags
  • DesignerActionPropertyItem : dérivé de DesignerActionItem. Représente un item associé à une propriété du composant dans un panel de SmartTags. Cette propriété doit être contenue dans une classe dérivée de DesignerActionList
  • DesignerActionMethodItem : dérivé de DesignerActionItem. Représente un item associé à une méthode du composant dans un panel de SmartTags. Cette méthode doit être contenue dans une classe dérivée de DesignerActionList.

Utiliser le modèle de DesignerAction pour créer son Smart Tag

Liste des items du Smart Tag


Pour activer les Smart Tags sur votre composant ou votre contrôle, vous devez créer une classe dérivant de DesignerActionList pour pouvoir compléter la liste des items de SmartTags. Cette classe devra avoir un constructeur prenant en paramètre un IComponent et le passer au constructeur de la classe de base. A l'instanciation de votre classe dans le Designer (la propriété Actions), vous devrez lui passer le contenu de la propriété Component du Designer afin de lui associer son composant/contrôle.

Ensuite, il vous faut redéfinir la méthode GetSortedActionItems pour renvoyer une nouvelle collection de DesignerActionItem. Ces objets représentent les items de votre SmartTag et doivent être dans l'ordre d'affichage (à moins que vous utilisiez les catégories dans vos actions).

Comment créer les différents types d'items ?

DesignerActionHeaderItem

Ce type d'action représente un entête dans le panel des Smart Tags. Cela peut servir à titrer et regrouper les actions qui suivent. On peut construire ce type d'action en passant simplement le texte à afficher dans l'entête.

ret.Add(newDesignerActionHeaderItem("Mon autre entête"));
DesignerActionTextItem

Ce type d'action représente un texte statique (ne pouvant donc pas être modifié). Cela permet de fournir une information dans le panel des SmartTags . On peut construire ce type d'action en passant simplement le texte à afficher.

ret.Add(newDesignerActionTextItem("Mon texte",null));
DesignerActionPropertyItem

Ce type d'action fournit un éditeur pour une propriété. La propriété doit faire partie de notre classe dérivant de DesignerActionList et doit se charger de mettre à jour la propriété du composant associé ou tout traitement approprié. Cette propriété sera accédée par réflexion. Aussi, le constructeur de ce type d'action prendra en paramètres :
  • Le nom de la propriété sous forme de chaîne
  • Une éventuelle catégorie ou null/Nothing
  • Une éventuelle description pour la tooltip de l'action.


ret.Add(newDesignerActionPropertyItem("BackColor", "Ma propriété", null, "Une belle propriété"));
DesignerActionMethodItem

Ce type d'action permet, un peu à la manière des Designers Verbs, d'exécuter une méthode qui ne renvoie pas de valeur et qui ne prend pas de paramètre. Cette méthode doit faire partie de notre classe dérivée de DesignerActionList et doit implémenter un traitement approprié. Elle dispose de la propriété Component de DesignerActionList, qui contient le composant associé au panel SmartTag. On construit ce type d'action avec les paramètres suivants :
  • L'instance de notre classe dérivée de DesignerActionList (qui implémente la méthode) : this ou Me suivant le langage
  • Le nom de la méthode sous forme de chaîne
  • Le texte qui apparaîtra dans le panel sur le lien permettant d'exécuter la méthode
  • Une éventuelle catégorie
  • Une éventuelle description pour la tooltip
  • Un éventuel booléen indiquant si cet item d'action doit aussi être mis dans les DesignersVerbs.


ret.Add(newDesignerActionMethodItem(this,"Close","Fermer"));

Les catégories


Un des paramètres de ces actions est une chaîne pouvant contenir un nom de catégorie. Cela permet de trier les Smart Tags suivant ces catégories. En général, on passera null/Nothing ou « » afin de ne pas perturber l'ordre d'apparition de nos SmartTags. Si l'on précise plusieurs noms de catégories différents, les SmartTags seront regroupés indépendamment de l'ordre de renvoi dans GetSortedActionItems .

Interaction des Designers Verbs avec les Smart Tags


Si votre composant ou contrôle possède aussi des DesignerVerb, ceux-ci viennent se greffer dans les Smart tags. L'environnement de Design demande aux Designers des composants, une liste d'action (SmartTags), et si aucune n'est disponible, une liste d'action est créée pour les DesignerVerbs existants.

Interaction des Smart Tags avec les Designers Verbs


Si vous voulez qu'un item de Smart Tag apparaisse dans les DesignerVerbs et dans le menucontextuel du composant, il faudra deux conditions :
  • Que votre DesignerActionItem soit un DesignerActionMethodItem, c'est-à-dire une action qui exécute une commande
  • Qu'à la construction de ce DesignerActionMethodItem, vous passiez « True » pour e paramètre de la propriété IncludeAsDesignerVerb.


Ceci permet au service DesignerActionService de parcourir la liste des actions afin de trouver celles qui ont la propriété IncludeAsDesignerVerb à true et de les ajouter à la liste des DesignerVerbs (avec la méthode AddVerb de ce service).

Associer le SmartTag au Designer


Si vous utilisez le mode « Pull », ce qui est le cas la plupart du temps, dans la classe de votre Designer (dérivée de ComponentDesigner ou ControlDesigner), redéfinissez la propriété ActionLists afin de renvoyer une nouvelle DesignerActionListCollection contenant une instance de la classe dérivée de DesignerActionList créée précédemment.

Dans le cas du mode « Push », vous devez récupérer une instance du service DesignerActionService à l'aide de la méthode GetService de votre Designer puis appeler la méthode Add de ce service en lui passant l'instance de votre classe dérivée de DesignerActionList. Ces actions seront ajoutées à celles connues du service et de votre Designer.

Le service DesignerActionUIService


Ce service permet de manipuler les panels de Smart Tags. Il possède principalement les méthodes suivantes :
  • ShowUI permet d'afficher le panel de SmartTags pour le composant passé en paramètre
  • HideUI permet de cacher le panel de SmartTags pour le composant passé en paramètre
  • Refresh permet de rafraîchir le panel de SmartTags pour le composant passé en paramètre
  • ShouldAutoShow permet de préciser si le panel de SmartTags doit être automatiquement affiché pour le composant passé en paramètre.


Il possède de plus, l'évènement DesignerActionUIStateChange permettant d'être notifié des demandes d'affichage, de rafraîchissement et de fermeture des panels de SmartTags de tous les composants de la surfacedeDesign.

On pourra par exemple définir une action permettant de fermer le panel en appelant la méthode HideUI pour notre Component :

this.uiSrv = (DesignerActionUIService) this.GetService(typeof(DesignerActionUIService));
//on teste toujours si on a réussi à récupérer un service avant de l'utiliser 
//car ce n'est absolument pas garanti
//puis ferme l'interface utilisateur de notre contrôle
if(uiSrv != null) uiSrv.HideUI(this.Component);

Exemple de Smart Tags


///	 <summary>
///	 Démontre l'utilisation
///	 </summary>
[Designer(typeof(SmartTagUserControl.SmartTagUserControlDesigner), typeof(IDesigner))]
public partial class SmartTagUserControl : UserControl
{
    public SmartTagUserControl()
    {
        InitializeComponent();
    }


    ///	 <summary>
    ///	 Designer permettant de fournir des Smart Tags
    ///	 </summary>
    private class SmartTagUserControlDesigner : ControlDesigner
    {
        ///	 <summary>
        ///	 Renvoie la liste des Smart Tags de ce contrôle
        ///	 </summary>
        public override DesignerActionListCollection ActionLists
        {
            get
            {
                //nouvelle liste de liste de Smart Tag
                DesignerActionListCollection ret = newDesignerActionListCollection();
                //ajoute notre liste d'actions/Smart Tags
                ret.Add(newMyDesignerActionList(this.Component));
                return	 ret;
            }
        }

        ///	 <summary>
        ///	 Contient la liste et le traitement des actions des Smart Tags
        ///	 Les Actions Smarts tags ne peuvent pas être implémentées directement
        ///	 dans le Designer
        ///	 mais uniquement dans une classe séparée dérivant de DesignerActionList
        ///	 Le Designer ne fait que renvoyer une instance de cette classe.
        ///	 Cette classe rassemble la liste des Smart Tags et leur traitement.
        ///	 </summary>
        private class MyDesignerActionList : DesignerActionList
        {
            //Service d'affichage
            private	 DesignerActionUIService uiSrv = null;

            ///	 <summary>
            ///	 Construit notre liste d'action. On a besoin de connaître
            ///	 l'instance de notre composant pour pouvoir agir dessus
            ///	 </summary>
            ///	 <param name="component"></param>
            public MyDesignerActionList(IComponent component) : base(component)
            {
                this.uiSrv = (DesignerActionUIService)this.GetService(typeof(DesignerActionUIService));
            }

            ///	 <summary>
            ///	 Définit une propriété qui sera éditable dans un Smart Tag
            ///	 qui sera de type "propriété"
            ///	 On est obligé de définir une propriété sur cette classe dérivée
            ///	 par DesignerActionList
            ///	 afin de modifier le contrôle (que l'on a reçu en paramètre).
            ///	 On ne peut pas dire directement au Smart tag de modifier
            ///	 la propriété réelle du contrôle
            ///	 </summary>
            public Color BackColor
            {
                get
                {
                    //récupère la valeur de la propriété du contrôle
                    return ((Control)this.Component).BackColor;
                }
                set
                {
                    //définit la propriété par TypeDescriptor
                    //afin d'avertir le service de changement de composant
                    //et de rafraichîr la fenêtre de propriétés
                    TypeDescriptor.GetProperties(this.Component)["BackColor"].SetValue(this.Component, value);
                }
            }

            ///	 <summary>
            ///	 Définit une méthode qui sera appelée par un Smart Tag de type "méthode"
            ///	 </summary>
            private void AMethod()
            {
                MessageBox.Show("Une méthode appelée par un Smart Tag");
            }

            ///	 <summary>
            ///	 Méthode appelée par un Smart Tag de type "méthode"
            ///	 et permettant de fermer le panel de Smart Tag du contrôle
            ///	 </summary>
            private void Close()
            {
                //on teste toujours si on a réussi à récupérer un service
                //avant de l'utiliser car ce n'est absolument pas garanti
                //puis ferme l'interface utilisateur de notre contrôle
                if(uiSrv != null) uiSrv.HideUI(this.Component);
            }

            ///	 <summary>
            ///	 Renvoie la liste des Smart Tags dans leur ordre d'apparition
            ///	 dans le panel (et triée par catégorie)
            ///	 </summary>
            ///	 <returns></returns>
            public override DesignerActionItemCollection GetSortedActionItems()
            {
                //Note : si l'on spécifie un nom de catégorie à la place de null
                //dans les exemples suivants
                //Les Smart Tags sont regroupés suivant leur catégorie
                //et ne sont donc pas pris exactement suivant leur ordre d'apparition
                //dans la collection

                //crée une nouvelle collection d'item action Smart Tags
                DesignerActionItemCollection ret = newDesignerActionItemCollection();
                //ajoute une entête : juste son texte
                ret.Add(newDesignerActionHeaderItem("Mon entête",null));
                //ajoute un texte statique : juste un texte comme un label
                ret.Add(newDesignerActionTextItem("Mon texte",null));

                ret.Add(newDesignerActionHeaderItem("Mon autre entête"));
                //ajoute une édition de propriété (propriété qui doit être
                //  obligatoirement de cette classe)
                //on passe (au maximum) :
                //-> le nom de la propriété sous forme de chaîne
                     //-> le texte du label à côté de la zone d'édition de la propriété
                //-> une catégorie pour regrouper les items ou null
                //      si l'on ne veut pas avoir de regroupement
                //-> une description pour la tooltip ou statusbar
                ret.Add(newDesignerActionPropertyItem("BackColor", "Ma propriété", null, "Une belle propriété"));
                //ajoute une action (lien cliquable) associée à une méthode
                //de cette classe
                //on passe (au maximum) :
                //-> le nom de la méthode sous forme de chaîne
                //-> le texte de l'action/du lien
                //-> une catégorie pour regrouper les items
                //      ou null si l'on ne veut pas avoir de regroupement
                //-> une description pour la tooltip ou statusbar
                //-> true si on veut que l'action soit aussi incluse
                //dans les Designer Verbs, false sinon (par défaut)
                ret.Add(newDesignerActionMethodItem(this, "AMethod", "Ma méthode", null, "Une belle méthode", true));
                ret.Add(newDesignerActionMethodItem(this,"Close","Fermer"));

                return	 ret;
            }
        }

        ///	 <summary>
        ///	 Dessine des "ornements" sur le contrôle au Design
        ///	 Normalement en .Net 2.0, on devrait utiliser le BehaviorService
        ///	 mais pour dessiner des objets non intéractifs, c'est suffisant
        ///	 </summary>
        ///	 <param name="pe"></param>
        protected override void OnPaintAdornments(PaintEventArgs pe)
        {
            //dessine les ornements de base : les poignées et autres
            base.OnPaintAdornments(pe);

            //dessine un cadre en pointillés autour
            Pen	 p = newPen(Brushes.Blue);
            p.DashStyle = System.Drawing.Drawing2D.DashStyle.DashDot;
            pe.Graphics.DrawRectangle(p, 0, 0, this.Control.Width - 1, this.Control.Height - 1);
            p.Dispose();
        }
    }
}

Références


[http://msdn.microsoft.com/en-us/library/ms171567(VS.80).aspx Designer Commands and the DesignerAction Object Model for Windows Forms}

[http://msdn.microsoft.com/en-us/library/ms171830(VS.80).aspx How to: Attach Smart Tags to a Windows Forms Component}

Designer Verbs

A voir également

Publié par ShareVB.
Ce document intitulé «  Designer Verbs et Smart Tags  » issu de CodeS-SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Ajouter un commentaire

Commentaires

Donnez votre avis
Behavior Service : la nouvelle méthode d'ornement au Design Time
Le livre blanc de Jounce - MVVM et MEF avec Silverlight