Astuces DotNet (Sébastien Courtois)

25/01/2010

[Silverlight 3/4] Introduction sur les Behaviors

Filed under: Débutant, Silverlight, Silverlight 3, Silverlight 4, WPF, XAML — Étiquettes : , , , , — sebastiencourtois @ 19:47

Introduit dans la version 3 de Microsoft Expression Blend, les Behaviors sont un système permettant de créer des comportements génériques pour des contrôles. L’un des gros avantages est que ces comportements peuvent être utilisé directement en XAML ou par Blend.

Nous allons partir d’un exemple afin d’étudier comment marche les behaviors. Notre Behavior exemple sera un ResizeBehavior qui va doubler la taille du contrôle auquel il est associé lorsque l’on clique sur le bouton gauche et qui va diviser par deux sa taille quand on clique avec le bouton droit. Le but du jeu étant que ce behavior fonctionne pour un plus grand nombre de contrôles possibles.

  • Comment coder un behavior ?

Tout commence avec la classe Behavior<T>. Cette classe est la classe de base des Behaviors. Elle se trouve dans l’assembly System.Windows.Interactivity. Cet assembly ne se trouve pas dans la GAC et pour cause, il s’agit d’un ajout fait par Expression Blend.

Si vous n’avez pas Expression Blend, je vous encourage à télécharger la version Beta pour Silverlight 4 / .NET 4 (http://www.microsoft.com/downloads/details.aspx?FamilyID=6806e466-dd25-482b-a9b3-3f93d2599699&displaylang=en).

Une fois Blend installé, vous devriez trouver cet assembly dans le répertoire  :

C:\Program Files\Microsoft SDKs\Expression\Blend 3\Interactivity\Libraries\Silverlight

C:\Program Files\Microsoft SDKs\Expression\\Blend Preview for .NET 4\Interactivity\Libraries\Silverlight

Pour créer un nouveau behavior, il suffit de créer une classe dérivant de Behavior<T> en spécifiant, à la place du T, le type d’objet que va être appelé à manipuler le Behavior. Dans notre exemple, nous prendrons des contrôles de type FrameworkElement (afin d’avoir accès aux propriétés Width/Height).

    public class ResizeBehavior : Behavior<FrameworkElement>
    {
        public ResizeBehavior()    {}

        protected override void OnAttached()
        {
            base.OnAttached();
            //Code 
        }
    
        protected override void OnDetaching()
        {
            base.OnDetaching();
            //Code
        }
    }

Comme on peut le voir, la classe ResizeBehavior contient :

    • Un constructeur : Pour initialiser les données
    • Un méthode OnAttached : Méthode hérité de Behavior<T>. Cette méthode est appelé lorsque le behavior est “attaché” à l’objet. Nous verrons plus loin quand, exactement, à lieu l’attachement. Pour le moment,  on admettra que l’attachement se fait lors de la création du contrôle sur lequel va intervenir le behavior.
    • Une méthode OnDetaching : Méthode hérité de Behavior<T>. Cette méthode est appelé lorsque le behavior est “attaché” à l’objet. Le détachement est réalisé lorsque le contrôle sur lequel s’applique le behavior est détruit (Nettoyage de la mémoire, désabonnement des événements …).

On remarque aussi un appel aux méthodes OnAttached/OnDetaching parentes. Ces appels sont nécessaires au bon fonctionnement du behavior et doivent toujours être présente au début des méthodes correspondantes.

Comme il a été expliqué au début de cet article, un behavior est un comportement associé à un contrôle. Ce contrôle est disponible au sein du behavior sous la forme de la propriété AssociatedObject (défini dans Behavior<T>.). Vous pouvez ainsi manipuler l’objet comme si vous étiez dans le code behind du fichier XAML dont il dépend. Ainsi nous allons ajouter les évènements MouseLeftButtonDown/MouseRightButtonDown sur l’objet associé afin de modifier la taille de l’objet associé.

    public class ResizeBehavior : Behavior<FrameworkElement>
    {
        public ResizeBehavior()    {}

        protected override void OnAttached()
        {
            base.OnAttached();
            //Code 
            this.AssociatedObject.MouseLeftButtonDown += new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonDown);
            this.AssociatedObject.MouseRightButtonDown += new MouseButtonEventHandler(AssociatedObject_MouseRightButtonDown);
        }
    
        protected override void OnDetaching()
        {
            base.OnDetaching();
            //Code
            this.AssociatedObject.MouseLeftButtonDown -= new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonDown);
            this.AssociatedObject.MouseRightButtonDown -= new MouseButtonEventHandler(AssociatedObject_MouseRightButtonDown);
        }

        void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            this.AssociatedObject.Width *= 2;
            this.AssociatedObject.Height *= 2;
        }

        void AssociatedObject_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
        {
            this.AssociatedObject.Width /= 2;
            this.AssociatedObject.Height /= 2;
        }
    }
  • C’est bien beau tout ça, mais comment on utilise ça en XAML ?

Coté XAML, il faut tout d’abord ajouter deux références. Une pour avoir accès au namespace System.Windows.Interactivity et une pour avoir accès au behavior.

    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:local="clr-namespace:BehaviorDemo"  >

Ensuite, on ajoute un contrôle de type FrameworkElement (un Rectangle par exemple).

        <Rectangle Fill="Red" Width="200" Height="100">
            <i:Interaction.Behaviors>
                <local:ResizeBehavior />
            </i:Interaction.Behaviors>
        </Rectangle>

Le namespace System.Windows.Interactivity ajoute une classe Interaction permettant d’associer des Behaviors (mais aussi des Triggers / Actions… qui seront l’objet d’autres posts de blogs :)) .

Lors de l’exécution de l’application, Silverlight va créer l’objet Rectangle puis le ResizeBehavior. Une fois les deux objets créés, la méthode OnAttached de Behavior<T> est appelée. Lors de la destruction de l’objet Rectangle, c’est la méthode OnDetaching qui est appelée.

  • Moi j’aime pas le XAML. Je veux du C# …

Le code suivant permet d’associer un behavior à un contrôle.

Rectangle b = new Rectangle();
b.Fill = new SolidColorBrush(Colors.Purple);
b.Width = 400;
b.Height = 200;
Interaction.GetBehaviors(b).Add(new ResizeBehavior());
this.LayoutRoot.Children.Add(b);

  • Pourquoi utiliser des behaviors ?

L’intérêt des behavior est multiple :

    • Réutilisation du code : Le code de comportement n’est pas lié à un contrôle défini mais à une famille de contrôle. Cela évite de réécrire le code à chaque fois.
    • Instanciation directement dans le XAML (donc utilisable dans Blend par un designer)
    • Intégration dans Blend : Blend 3/4 gère les behaviors nativement et permet d’ajouter facilement ses propres behaviors.
  • Saupoudrons avec un peu d’animations …  

On modifie le ResizeBehavior afin d’avoir une animation un peu plus sympa.

    public class ResizeBehavior : Behavior<FrameworkElement>
    {
        public ResizeBehavior()    {}

        protected override void OnAttached()
        {
            base.OnAttached();
            //Code 
            this.AssociatedObject.MouseLeftButtonDown += new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonDown);
            this.AssociatedObject.MouseRightButtonDown += new MouseButtonEventHandler(AssociatedObject_MouseRightButtonDown);
        }
    
        protected override void OnDetaching()
        {
            base.OnDetaching();
            //Code
            this.AssociatedObject.MouseLeftButtonDown -= new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonDown);
            this.AssociatedObject.MouseRightButtonDown -= new MouseButtonEventHandler(AssociatedObject_MouseRightButtonDown);
        }

        void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            Storyboard sb = new Storyboard();
            DoubleAnimation da = new DoubleAnimation()
            {
                From = this.AssociatedObject.Width,
                To = this.AssociatedObject.Width * 1.5,
                Duration = new Duration(new TimeSpan(0, 0, 1)),
                EasingFunction = new ElasticEase()
            };
            Storyboard.SetTarget(da, this.AssociatedObject);
            Storyboard.SetTargetProperty(da, new PropertyPath("(Width)"));
            sb.Children.Add(da);

            DoubleAnimation da2 = new DoubleAnimation()
            {
                From = this.AssociatedObject.Height,
                To = this.AssociatedObject.Height * 1.5,
                Duration = new Duration(new TimeSpan(0, 0, 1)),
                EasingFunction = new ElasticEase()
            };
            Storyboard.SetTarget(da2, this.AssociatedObject);
            Storyboard.SetTargetProperty(da2, new PropertyPath("(Height)"));
            sb.Children.Add(da2);

            sb.Begin();
        }

        void AssociatedObject_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
        {
            Storyboard sb = new Storyboard();
            DoubleAnimation da = new DoubleAnimation()
            {
                From = this.AssociatedObject.Width,
                To = this.AssociatedObject.Width / 1.5,
                Duration = new Duration(new TimeSpan(0, 0, 1)),
                EasingFunction = new ElasticEase()
            };
            Storyboard.SetTarget(da, this.AssociatedObject);
            Storyboard.SetTargetProperty(da, new PropertyPath("(Width)"));
            sb.Children.Add(da);

            DoubleAnimation da2 = new DoubleAnimation()
            {
                From = this.AssociatedObject.Height,
                To = this.AssociatedObject.Height / 1.5,
                Duration = new Duration(new TimeSpan(0, 0, 1)),
                EasingFunction = new ElasticEase()
            };
            Storyboard.SetTarget(da2, this.AssociatedObject);
            Storyboard.SetTargetProperty(da2, new PropertyPath("(Height)"));
            sb.Children.Add(da2);

            sb.Begin();
        }
    }

Code XAML Associé :

<UserControl x:Class="BehaviorDemo.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:local="clr-namespace:BehaviorDemo"  >
    <StackPanel x:Name="LayoutRoot" Background="White">
        <Rectangle Fill="Red" Width="200" Height="100">
            <i:Interaction.Behaviors>
                <local:ResizeBehavior />
            </i:Interaction.Behaviors>
        </Rectangle>
        <Ellipse Fill="Yellow" Width="200" Height="100">
            <i:Interaction.Behaviors>
                <local:ResizeBehavior />
            </i:Interaction.Behaviors>
        </Ellipse>
    </StackPanel>
</UserControl>
  • … et voyons le résultat :

Voici des copies d”écrans du résultats :

behaviorAvant behaviorApres

Gauche : Après chargement / Droite : Après avoir cliqué avec le bouton gauche sur le rectangle rouge et l’ellipse jaune.

Ce post n’était qu’une introduction sur les behaviors. Je vais surement continuer sur ce sujet dans les prochains jours (notamment sur les behaviors/triggers et leurs applications plus business).

Si vous avez des questions et/ou remarques et/ou sujet de futur post, n’hésitez pas à poster un commentaire …

Remarque : Afin d’anticiper certains commentaires, je précise qu’il aurait été possible de créer l’animation dans le OnAttached et non en créer une nouvelle à chaque click :). Je referais peut être l’exemple afin de le rendre plus propre. Toutefois, le but de cet article est de montrer le principe des behaviors.

18/03/2009

[Silverlight 2] Les converters typés

Filed under: Débutant, Intermediaire, Silverlight, WPF — Étiquettes : , , , — sebastiencourtois @ 15:46

 

Ceux qui ont déjà touchés à Silverlight 2 ou à WPF ont été amené à utilisé les converters. En travaillant, je me suis aperçu qu’une partie du code que j’écrivais dans un converter était principalement de la vérification de type d’entrée et de sortie. La cause : Les converters ne sont pas typés. Cela est, je pense, une fonctionnalité voulue par Microsoft afin de rendre ce système le plus flexible possible. Toutefois, j’aime bien les choses typés et ce principe me dérangeait d’autant que, la plupart du temps, je voulais un type d’entrée spécifique et un type de sortie spécifique.

Il existe un attribut en WPF pour “gérer” ce problème : ValueConversionAttribute. Toutefois, l’utilisation de cet attribut est de l’ordre informatif et il ne réalise aucune vérification. Le texte qui suit est donc valable pour WPF et Silverlight.

Partons d’un converter classique : Le BoolToVisibilityConverter

Le principe de ce converter est de fournir un booléen en entrée qui va définir un System.Windows.Visibility en sortie.

Voici le code du converter classique : 

Blog7-1

Comme vous pouvez le remarquer, 14 lignes de code pour écrire ce converter et 8 ne concerne pas la conversion en elle même mais plus les vérifications de types. J’ai ainsi factoriser ce code dans une classe abstraite générique afin n’avoir plus QUE de la conversion de type au sein de mes converters.

Code de la classe générique abstraite mère :  (VType représente le type d’entrée. TType représente le type de sortie.)

Blog7-2

Par héritage, on obtient le BoolToVisibiltyConverter suivant :

Blog7-3

Le développeur n’a plus qu’à remplir les méthodes abstraites héritées de TypedConverter. Ces méthode seront correctement typés (que ce soit dans leur paramètres et dans leur valeur de retour). De plus, la vérification de type est garanti par la classe mère. Au sein de BoolToVisibilityConverter, on ne retrouve que le code de conversion.

Certains pourront dire que ce code ne sert à rien car il fait perdre la flexibilité de Silverlight 2 (qui est un de ses grands atouts). Même si cela est vrai, 90 % des cas d’utilisations des converters se réalise avec un type d’entrée bien défini et un type de sortie bien défini. Autant essayé de gagner de la lisibilité dans le code sur ces cas là. Pour des cas nécessitant plus de flexibilité, rien n’empêche d’utiliser le bon vieux IValueConverter.

EDIT 18/03/09 : Suite à des tests, j’ai découvert un petit bug sur le code du TypedConverter. En effet, l’héritage au niveau du TargetType n’était pas pris en compte.

Exemple : Si on crée un TypedConverter<object,object> (i.e un converter standard), et qu’on lui fournit un targetType de Visibility, on obtient l’exception : “le type de sortie doit être …”. En effet, Visibility n’est pas object. Toutefois, vu que Visibility hérite de object, on aimerait pouvoir passer ce type de donnée pour qu’il soit valider “par héritage”. Or le test recherche une différence entre deux System.Type sans regarder la hiérarchie des classes. J’ai donc modifié le code du TypedConverter :

public abstract class TypedConverter<VType,TType>  : IValueConverter
{
	public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
	{
		//Vérification du type d'entrée
		if (!(value is VType))
			throw new InvalidOperationException("La paramètre d'entrée doit être de type "+ typeof(VType));
		//Vérification du type de sortie
		if(!this.IsTargetType(targetType,typeof(TType)))
			throw new InvalidOperationException("La type de sortie doit être de type " + typeof(TType));
		return this.Convert((VType)value, culture);
	}

	public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
	{
		//Vérification du type d'entrée
		if (!(value is TType))
			throw new InvalidOperationException("La paramètre d'entrée doit être de type " + typeof(VType));
		//Vérification du type de sortie
		if (!this.IsTargetType(targetType, typeof(VType)))
			throw new InvalidOperationException("La type de sortie doit être de type " + typeof(TType));
		return this.ConvertBack((TType)value, culture);
	}

	protected abstract TType Convert(VType p, CultureInfo culture);
	protected abstract VType ConvertBack(TType p, CultureInfo culture);

	//Méthode recherchant un type correspondant dans la hierarchie des classes
	private bool IsTargetType(Type targetType, Type type)
	{
		while(targetType != null)
		{
			if (targetType == type)
				return true;
			targetType = targetType.BaseType;
		}
		return false;
	}
}

L’héritage est ainsi gérer par l’utilisation de la méthode IsTargetType qui remonte l’arborescence des classes de targetType jusqu’à trouver une classe correspondant au type demandé. Si ce n’est pas le cas, une exception est lancée.

Créez un site Web ou un blog gratuitement sur WordPress.com.

%d blogueurs aiment cette page :