Astuces DotNet (Sébastien Courtois)

15/04/2010

[Silverlight] Sortie de Silverlight 4 RTW

Filed under: .NET, Débutant, Silverlight, Silverlight 3, Silverlight 4 — Étiquettes : , , , , — sebastiencourtois @ 23:40

Ca y est : Silverlight 4 est disponible à la page suivante : http://www.silverlight.net/getstarted/

Beaucoup de nouveautés (drag/drop, impression,clic droit …) dont certains ont déja été décrit sur ce blog :

D’autres posts sur le sujets arriveront bientôt.

EDIT : Silverlight 4 Tools NE FONCTIONNE PAS SUR VS 2008…. Il faut obligatoirement VS 2010 pour développer en SL4… De plus les outils de développement sont en RC pour le moment car il fallait que Silverlight 4 soit complètement terminé pour finaliser les outils de développement (ils ont du finir sur le fil SL4 :)).

Publicités

01/02/2010

[Silverlight 3/4] Comment faire des contrôles avec deux faces différentes ?

Filed under: Intermediaire, Silverlight, Silverlight 3, Silverlight 4, XAML — Étiquettes : , , , , — sebastiencourtois @ 21:23

Au commencement Microsoft créa Silverlight. Silverlight, la technologie RIA nouvelle génération pour créer des applications vectorielles entièrement en code managé. Microsoft vit que Silverlight était bon… mais qu’il lui manquait un petit truc pour rivaliser avec son rival. Alors Microsoft dit : “Que la 3D soit” et la 3D fut intégrée dans Silverlight … enfin pas tout à fait.

Tout d’abord, la « 3D » dans Silverlight n’est qu’un moteur de perspective qui ne permet que de faire des rotations/translations de contrôles. Point de moteur ou de monde 3D ici. On parle uniquement ici que de pouvoir faire des jolis effets en alliant animations et perspectives.

Ensuite cette perspective a une feature (« not a bug ») assez ennuyeuse à mon sens : Si on prend l’exemple d’un contrôle contenant une image, lorsque l’on retourne ce contrôle, on se retrouve avec … la même image inversée.

Cap3

Personnellement j’aurais aimé avoir accès à une deuxième face pour pouvoir ajouter des nouveaux contrôles et, comme on n’est jamais mieux servi que par soi-même, j’ai donc cherché un moyen de réaliser ce contrôle. J’ai appelé ce contrôle : FacesPanel.

Fonctionnement de base du contrôle FacesPanel

En parcourant les forums, la pratique général est d’avoir deux contrôles sur un même conteneur (un Grid par exemple) et de rendre visible l’un ou l’autre selon la propriété RotationX/RotationY d’un PlaneProjection. L’algorithme est le suivant :

double Roty = Math.Abs(((PlaneProjection)this.Projection).RotationY);
double Rotx = Math.Abs(((PlaneProjection)this.Projection).RotationX);
if ((Roty % 360 > 90 && Roty % 360 < 270) || (Rotx % 360 > 90 && Rotx % 360 < 270))
{
    //Back Face
}
else
{
    //Front Face
}

Remarque : Vous vous demander peut être pourquoi on ne trouve pas de RotationZ. La raison est qu’une Rotation sur l’axe Z se fera toujours “face” à l’utilisateur. Par conséquent, aucun cas de changement de face. Une autre interrogation pourrait être au sujet des Math.Abs et des %. Cela est simplement dû au fait que les rotations peuvent être négatives comme supérieure à 360°.

Cette méthode a deux inconvénients qu’il va falloir gérer :

1°) Lors du retournement du contrôle, le fait de changer la visibilité des contrôles ne résout pas le problème de l’inversion graphique car c’est le container qui tourne donc le contrôles interne suit le mouvement. Pour gérer ce problème, on utilise un ScaleTransform afin de remettre le contrôle dans le bon sens.

2°) Je n’ai pas réussi à récupérer les valeurs RotationX/RotationY au cours d’une animation lorsque je me binde sur ces propriétés. Cela est problématique pour avoir un comportement naturel au cours d’une animation (le changement de face se faisant uniquement à la fin de l’animation). Pour parer ce problème, j’ai utilisé une solution peu élégant/optimisé mais qui à le mérite de fonctionner (si vous avez mieux, n’hésitez pas à commenter). J’utilise un DispatcherTimer afin que, périodiquement, je regarde le valeurs de RotationX/RotationY pour voir si un changement de face est nécessaire.

Propriétés/Methodes de FacesPanel :

Voila pour la base du fonctionnement. Afin de rendre le contrôle exploitable, j’ai rajouté les propriétés suivantes :

  • FrontFace (type : UIElement) : Contrôle pour la face principale
  • BackFace (type : UIElement) : Contrôle pour la face caché
  • AnimationDirection (type: TurnDirection ) : Permet de définir le sens du retournement de la face. Se base sur l’énum TurnDirection défini dans le même fichier .cs. 5 valeurs (NONE,LEFT,RIGHT,UP,BOTTOM)
  • AnimationDuration (type : int) : Durée de l’animation de retournement
  • AnimationEasing (Type : IEasingFunction) : Animation Easing pour l’animation de  retournement.
  • GoToFrontFace() : Montre la face principale.
  • GoToBackFace() : Montre la face “caché”.
  • TurnFace() : Change de face

Cas d’utilisation : Photo Bucket

Afin de faire un cas d’utilisation, j’ai choisi de reprendre une démonstration Silverlight 4 sur glisser/déposer faite par Audrey Petit. Ce que je souhaitait faire était de pouvoir glisser déposer des images puis, par click de souris sur une image, la retourner pour marquer des commentaires au dos. J’ai donc utilisé l’exemple d’Audrey en rajoutant un contrôle spécifique pour gérer les images.

PictureControl.xaml :

<UserControl x:Class="PhotoBucket.PictureControl"
    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:local="clr-namespace:PhotoBucket"
    mc:Ignorable="d" Width="150" Height="150">
    <local:FacesPanel x:Name="FacesPanelControl" AnimationDirection="LEFT" AnimationDuration="3" MouseLeftButtonDown="FacesPanelControl_MouseLeftButtonDown">
        <local:FacesPanel.AnimationEasing>
            <ElasticEase />
        </local:FacesPanel.AnimationEasing>
        <local:FacesPanel.FrontFace>
            <Border Background="White" BorderBrush="Black" BorderThickness="1">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="120" />
                        <RowDefinition Height="30" />
                    </Grid.RowDefinitions>
                    <Image x:Name="ImageZone"  Margin="10,5,10,0" />
                    <TextBlock Text="{Binding ElementName=txtTitle,Path=Text}" FontSize="10" FontStyle="Italic" 
                               Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" />
                </Grid>
            </Border>
        </local:FacesPanel.FrontFace>
        <local:FacesPanel.BackFace>
            <Border Background="White" Margin="5" BorderBrush="Black" BorderThickness="1">
                <StackPanel>
                    <TextBlock Text="Titre de la photo" Margin="2" />
                    <TextBox x:Name="txtTitle"  Text="Test" Margin="2" />
                </StackPanel>
            </Border>
        </local:FacesPanel.BackFace>
    </local:FacesPanel>
</UserControl>

Picture.xaml.cs :

    public partial class PictureControl : UserControl
    {
        public PictureControl()
        {
            InitializeComponent();
        }

        private void FacesPanelControl_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            ((FacesPanel)sender).TurnFace();
        }
    }

Et voici le résultat en image :

Cap1 Cap2

Et en code

  • Lien du source de la démo du contrôle sur VS 2008 / SL3 (pas d’application photo car pas de drag / drop en SL3)
  • Lien du source de la démo VS  (Appli Photo Bucket)

Si vous avez des commentaires pour améliorer ce contrôle, n’hésitez pas.

25/01/2010

[Silverlight 4] Supprimer le context menu “Silverlight” lors d’un clic droit

Filed under: Débutant, Silverlight 4, XAML — Étiquettes : , , , , , — sebastiencourtois @ 22:41

Silverlight 4 apporte une nouveauté très demandée : La gestion du clic droit. Le problème est que, par défaut, le clic droit s’accompagne d’un menu contextuel “Silverlight” un peu énervant. Toutefois, il y a une manière de s’affranchir de celui-ci.

L’évènement MouseRightButtonDown est un RoutedEvent qui, lorsqu’il est déclenché, est transmis de contrôle enfant à contrôle parent (ainsi de suite jusqu’au contrôle racine). Or, s’il arrive en haut de l’arbre visuel, il affiche par défaut le menu contextuel “Silverlight”.

1°)Une solution simple est d’indiquer que l’évènement a été géré (e.Handled = true) dans la callback de l’événement.

<UserControl x:Class="RightClickHandled.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"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    <Grid x:Name="LayoutRoot" Background="White">
        <Rectangle Fill="Red" Width="300" Height="200" MouseRightButtonDown="Rectangle_MouseRightButtonDown"></Rectangle>
    </Grid>
</UserControl>
public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
        }

        private void Rectangle_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
        {
            e.Handled = true;
        }
    }

Le problème de cette méthode est qu’il faut le mettre sur chacune des callbacks de MouseRightButtonDown. De plus, cela arrête la remontée de l’évenement MouseRightClickDown aux contrôles parent qui doivent peut être y réagir.

2°) Utilisation d’un behavior : Etant donné mon dernier post, je ne pouvais m’empêcher de voir si un behavior peut régler la solution…. et c’est le cas. Pour ceux qui ne connaissent pas les behaviors, vous pouvez vous référez à cette introduction sur les behaviors).

    public class EraseSilverlightContextMenuBehavior : Behavior<UIElement>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.MouseRightButtonDown += new MouseButtonEventHandler(AssociatedObject_MouseRightButtonDown);
        }

        void AssociatedObject_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
        {
            e.Handled = true;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            this.AssociatedObject.MouseRightButtonDown -= new MouseButtonEventHandler(AssociatedObject_MouseRightButtonDown);
        }
    }

Après on associé le behavior au contrôle et le tour est joué.

<UserControl x:Class="RightClickHandled.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:RightClickHandled"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    <Grid x:Name="LayoutRoot" Background="White">
        <Rectangle Fill="Red" Width="300" Height="200">
            <i:Interaction.Behaviors>
                <local:EraseSilverlightContextMenuBehavior />
            </i:Interaction.Behaviors>
        </Rectangle>
    </Grid>
</UserControl>

Le soucis avec cette méthode est qu’il faut appliquer le behavior à tous les contrôles ce qui peut devenir lourd.

3°)La solution la plus générique serait de s’abonner à l’événement MouseRightButtonDown du RootVisual contenu dans le App.xaml.cs afin de bloquer l’événement juste afin sa sortie de l’arbre visuel.

private void Application_Startup(object sender, StartupEventArgs e)
{
    this.RootVisual = new MainPage();
    this.RootVisual.MouseRightButtonDown += (sender2, args) => { args.Handled = true; };
}

Ainsi tous les contrôles de l’arbre visuel peuvent avoir accès à l’évènement sans contrainte. Plus besoin de l’implémenter sur chacun des contrôles car cela est géré au niveau de la racine de l’arbre.

Si vous avez des méthodes plus smart pour faire cela, n’hésitez pas à me les indiquer :).

[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.

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

%d blogueurs aiment cette page :