Archive

Archive for the ‘WPF’ Category

[Telerik] : SelectedItem dans les BarSeries du RadChartView

7 décembre 2012 2 commentaires

Je partage une petite astuce permettant de disposer de la sélection sur le composant RangeBar du RadCartesianChart de Telerik.
Nativement le composant n’offre pas un moyen simple de connaitre l’élément actuellement sélectionné via le pattern MVVM.
Pour cela j’ai dérivé le composant RadSerie afin de lui apporter cette fonctionnalité, dont voici le code :


/// <summary>
    /// Extended <see cref="BarSeries"/> that support a selected item
    /// </summary>
    public class ExtendedBarSeries : BarSeries
    {
        #region Members

        private ChartSelectionBehavior _selectionBehavior;

        #endregion

        #region DPs

        /// <summary>
        /// Define the selected item dependency property
        /// </summary>
        public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register("SelectedItem", typeof(Object), typeof(ExtendedBarSeries), new PropertyMetadata(null, SelectedItemChanged));

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the current selected item
        /// </summary>
        public Object SelectedItem
        {
            get
            {
                return GetValue(SelectedItemProperty);
            }
            set
            {
                SetValue(SelectedItemProperty, value);
            }
        }

        #endregion

        #region Handlers

        /// <summary>
        /// Occurs when the presenter has been successfully attached to its owning Telerik.Windows.Controls.ChartView.RadChartBase instance.
        /// </summary>
        protected override void OnAttached()
        {
            base.OnAttached();

            _selectionBehavior = Chart.Behaviors.OfType<ChartSelectionBehavior>().FirstOrDefault();

            if (_selectionBehavior == null)
            {
                _selectionBehavior = new ChartSelectionBehavior();
                Chart.Behaviors.Add(_selectionBehavior);
            }

            _selectionBehavior.SelectionChanged += OnSelectionBehaviorSelectionChanged;
        }

        /// <summary>
        /// Occurs when the presenter has been successfully detached from its owning Telerik.Windows.Controls.ChartView.RadChartBase instance.
        /// </summary>
        /// <param name="oldChart">The old chart instance</param>
        protected override void OnDetached(RadChartBase oldChart)
        {
            base.OnDetached(oldChart);

            if (_selectionBehavior != null)
            {
                _selectionBehavior.SelectionChanged -= OnSelectionBehaviorSelectionChanged;
                oldChart.Behaviors.Add(_selectionBehavior);
                _selectionBehavior = null;
            }
        }

        /// <summary>
        /// Occurs when the chart selection has changed.
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">The chart selection arguments</param>
        private void OnSelectionBehaviorSelectionChanged(object sender, ChartSelectionChangedEventArgs e)
        {
            DataPoint dp = Chart.SelectedPoints.FirstOrDefault(p => p.IsSelected);

            SetValue(SelectedItemProperty, (dp != null) ? dp.DataItem : null);
        }

        /// <summary>
        /// Occurs when the selected item dependency property value changed
        /// </summary>
        /// <param name="d">The dependecy object</param>
        /// <param name="e">The dependency value changes</param>
        private static void SelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var serie = d as ExtendedBarSeries;

            if (serie != null)
            {
                List<CategoricalDataPoint> serieItems = serie.DataPoints.OfType<CategoricalDataPoint>().ToList();

                if (serie._selectionBehavior != null)
                {
                    if (e.NewValue == null)
                    {
                        serie._selectionBehavior.ClearSelection(true, false);
                    }
                    else
                    {
                        CategoricalDataPoint p = serieItems.FirstOrDefault(dp => dp.DataItem == e.NewValue);

                        if (p != null)
                        {
                            if (!serie.Chart.SelectedPoints.Contains(p))
                            {
                                serie._selectionBehavior.ClearSelection(true, false);
                                p.IsSelected = true;
                            }
                        }
                    }
                }
            }
        }

        #endregion
    }

Maintenant a des fins de tests nous faisons rapidement deux classes pour simuler les données:

    public class Data
    {
        public String X
        {
            get;
            set;
        }
        public Int32 Y
        {
            get;
            set;
        }
    }

Puis un manager de données :

 public class DataManager : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private Data _selectedItem;
        private readonly List<Data> _items;

        public DataManager()
        {
            Random r = new Random();
            _items = new List<Data>();

            for (int i = 0; i < 20; i++)
            {
                _items.Add(new Data()
                {
                    X = i.ToString(),
                    Y = r.Next(0, 30)
                });
            }
        }

        public List<Data> Items
        {
            get
            {
                return _items;
            }
        }

        public Data SelectedItem
        {
            get
            {
                return _selectedItem;
            }
            set
            {
                if (_selectedItem == value)
                    return;

                _selectedItem = value;
                OnPropertyChanged("SelectedItem");
            }
        }

        public void OnPropertyChanged(String propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

Enfin nous créons une instance de la classe DataManager que nous affectons au DataContext de notre vue

/// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            DataContext = new DataManager();
        }
    }

Pour terminer nous créons une vue avec le graphique Telerik, notre composant ExtendedBarSeries et une ComoboBox nous permettant d’effectuer une sélection « programmé » par autre chose qu’un click sur le graphe.

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
        xmlns:controls="clr-namespace:TelerikChartViewSelection"
        x:Class="TelerikChartViewSelection.MainWindow"
        Title="MainWindow"
        Height="350"
        Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>

        <!--COMBO-->
        <ComboBox ItemsSource="{Binding Items}"
                  SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
                  DisplayMemberPath="X" />

        <!--CHART-->
        <telerik:RadCartesianChart Grid.Row="1">

            <telerik:RadCartesianChart.HorizontalAxis>
                <telerik:CategoricalAxis />
            </telerik:RadCartesianChart.HorizontalAxis>

            <telerik:RadCartesianChart.VerticalAxis>
                <telerik:LinearAxis />
            </telerik:RadCartesianChart.VerticalAxis>

            <telerik:RadCartesianChart.Grid>
                <telerik:CartesianChartGrid MajorLinesVisibility="Y" />
            </telerik:RadCartesianChart.Grid>

            <telerik:RadCartesianChart.SelectionPalette>
                <telerik:ChartPalette>
                    <telerik:ChartPalette.GlobalEntries>
                        <telerik:PaletteEntry Fill="Orange" />
                    </telerik:ChartPalette.GlobalEntries>
                </telerik:ChartPalette>
            </telerik:RadCartesianChart.SelectionPalette>

            <controls:ExtendedBarSeries ItemsSource="{Binding Items}"
                                        SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
                                        ValueBinding="Y"
                                        CategoryBinding="X" />
        </telerik:RadCartesianChart>

    </Grid>
</Window>

Enfin voici le résultat final :

Telerik-BarSerie-RadChartView-Selection-MVVM

Vous pouvez désormais manipuler en MVVM le composant BarSeries 🙂

Code source de l’exemple (à renommer) : TelerikChartViewSelection.7z

Publicités
Catégories :RadChartView, Silverlight, Telerik, WPF

[Blend] : Carousel étape par étape pour designer / graphistes sans coder

Bonjour à tous, voici un petit article concernant l’utilisation avancée du composant PathListBox pour réaliser un Carousel.

Suite à une question pertinente d’un designer / graphiste souhaitant créer un carousel le plus simplement du monde et ne connaissant pas forcement le développement WPF / Silverlight .

L’objectif de cet article est de montrer que nous pouvons arriver à réaliser des visuels et interactions simplement par un graphiste. L’alimentation des données pouvant rester à la charge du développeur.

Avant tout rendez-vous sur le site : http://expressionblend.codeplex.com/releases/view/57990 et téléchargez le pack d’extention pour les PathListBox de Microsoft Expression Blend.

Créez un projet WPF (ou Silverlight) et nommez le pour l’exemple « WpfSampleCarousel ».

Créez un UserControl nommé “CarouselUserControl”

Ajoutez une grille avec trois colonnes. Dans la première et la dernière ajoutez un bouton (précédent et suivant).

Et le composant PathListBox

Step1

Ensuite, créez un ‘path’ de la forme de vous souhaitez (toujours dans la deuxième colonne)

Step2

Cliquez sur votre PathListBox et cliquez sur la cible sans lacher et pointez votre souris sur le path créé ci-dessus.

Ensuite nous allons configurer le PathListBox, modifiez les paramètres comme présentés ci-dessous (Distibution : Even, Capacity : un nombre impair si vous voulez un effet central, Start : 10%, FillBehavior : NoOverlap et surtout cochez WrapItems)

Step4

A titre d’exemple ajoutons des rectangles à notre PathListBox afin de pouvoir visualiser le ‘Carousel’ vous pouvez y mettre ce que vous souhaitez (ex images)

<Rectangle MinWidth="100" MinHeight="100" Fill="#FFFF3237"/>
			<Rectangle MinWidth="100" MinHeight="100" Fill="#FF89C122"/>
			<Rectangle MinWidth="100" MinHeight="100" Fill="#FFFEB63E"/>
			<Rectangle MinWidth="100" MinHeight="100" Fill="#FF2ACC55"/>
			<Rectangle MinWidth="100" MinHeight="100" Fill="#FFFE198E"/>
			<Rectangle MinWidth="100" MinHeight="100" Fill="#FF949494"/>
			<Rectangle MinWidth="100" MinHeight="100" Fill="#FF58A2E6"/>
			<Rectangle MinWidth="100" MinHeight="100" Fill="#FF8A144F"/>
			<Rectangle MinWidth="100" MinHeight="100" Fill="#FF37E4F5"/>
			<Rectangle MinWidth="100" MinHeight="100" Fill="#FFA4D9FA"/>

 

Nous obtenons visuellement un ‘Carousel’ plutot satisfaisant !

Ajoutons un peu d’interactivité ! Pour cela ajoutez un ‘Behavior’ nommé ‘PathListBoxScrollBehavior’ sur votre PathListBox en faisant un glisser déposer.

Configurons ce Behavior, pour cela cliquez dessus et cliquez sur le petit plus de DecrementCommand, IncrementCommand et ScrollSelectedCommand afin d’y ajouter un ‘EventTrigger’ présent ci-dessous

Puis configurez DecrementCommand, pour cela cliquez sur EventTrigger dans le SourceName cliquez sur la cible et glissez votre souris sur le bouton de gauche.

Sélectionnez ‘Click’ dans la zone EventName.

Répétez pour IncrementCommand, en pointant vers le bouton de droite.

Pour la zone ScrollSelectedCommand selectionnez SelectionChanged dans EventName (sourcename doit correspondre à votre PathListBox)

Vous pouvez si vous le souhaitez modifier la vitesse, et la façon dont les éléments se déplacent.

Si vous exécutez votre application votre ‘carousel’ est pleinement fonctionnel.

Nous allons à présent voir comment rendre celui ci plus sympathique visuellement. Nous allons faire en sorte que nos éléments soient plus petits sur les extrémités et que l’élément central soit centré 🙂

Pour cela faites un clic droit sur votre PathListBox Edit Additional templates > Edit Generated Container > Edit a Copy

Groupez l’élément principal dans une grille en faisant un clic droit sur l’élément racine :

Ajoutez un composant ‘PathListBoxItemTransformer’ en le glissant dans votre élément racine.

Vous obtenez :

Ensuite glissez la grille du bas dans le PathListBoxItemTransformer, vous obtenez :

Ensuite nous allons modifier le comportement d’un élément par rapport aux autres, pour cela cliquez sur ‘PathListBoxItemTransformer’ et allez dans le panneau des propriétés à droite, modifiez les options OpacityRange (50% d’opacité aux extrémités dans ce cas), ScaleRange (20% de la taille d’origine aux extrémités et 150% au centre), Ease à SineOut et surtout la propriété IsCentered pour disposer d’un élément central :

Nous obtenons ainsi :

Vous n’avez plus qu’à cliquez sur le ‘path’ (ici rouge) définit au début de l’exercice et de mettre transparent afin de ne plus le voir.

Dernière chose déposez votre UserControl dans votre fenêtre principale (via Assets si vous êtes designer ou tout simplement dans le xaml si vous savez le faire en référençant le bon namespace)

Exécutez votre application et vous disposez d’un carousel plutot sympathique facilement réalisable par un Designer sans avoir à faire le moindre code 😀

L’ensemble des sources de cet exemple sont disponibles ici : WpfSampleCarousel.zip (renommez le .doc en .zip)

Bon design 🙂

Catégories :Blend, Silverlight, WPF Étiquettes : ,

[WPF] : Custom RoutedEvents Tunnel-Bubble-Direct

Plusieurs personnes m’ont déjà contacté au sujet de soucis d’utilisation / compréhension des RoutedEvents présents dans WPF.

Voici un exemple complet d’utilisation simple des RoutedEvent Bubble et Tunnel !

Présentation

WPF à introduit un nouvelle notion au sein du mécanisme standard de gestion des évènements que l’on peut connaitre dans le framework .net, ce sont des évènements « intelligents » nommés RoutedEvent.
Pour que vous puissiez utiliser cette notion vous devez disposer d’un controle héritant au minimum de UIElement.

Les différents types de RoutedEvent

Il existe 3 types de RoutedEvents :

  • Bubble : L’évènement est déclenché sur la source et est ensuite déclenché à tous ses parents en remontant jusqu’à ce qu’il arrive à la racine.
  • Tunnel : C’est l’inverse de Bubble, dans ce cas l’évènement est déclanché depuis l’élément racine et parcours tous ses fils jusqu’à ce qu’il rencontre l’élément à l’origine de l’évènement.
  • Direct : Signifie que seul l’élément lui même peut déclencher l’évènement. C’est la notion d’évènement classique que l’on peut connaitre dans .net ou les évènements WinForm pour ce qui est des composants utilisateur.

Voici un graphique illustrant le fonctionnement des RoutedEvent Bubble et Tunnel :

WPF-RoutedEvent-Bubble-Tunnel

Comment définir un RoutedEvent Bubble ?

Rien de plus simple, à l’instar des DependencyProperty il suffit de déclarer l’évènement de la manière suivante :

public static readonly RoutedEvent FOOEvent = EventManager.RegisterRoutedEvent("FOO", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MonCustomControl));

Ensuite vous devez écrire le Wrapper .net

public event RoutedEventHandler FOO
{
add
{
AddHandler(FOOEvent, value);
}
remove
{
RemoveHandler(FOOEvent, value);
}
}

Comment définir un RoutedEvent Tunnel ?

C’est exactement la même chose sauf qu’il faut mettre Tunnel et ne pas oublier de préfixer les évènement par « Preview » afin de respecter les règles de nommages instaurées dans le framework.

public static readonly RoutedEvent PreviewFOOEvent = EventManager.RegisterRoutedEvent("PreviewFOO", RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(MonCustomControl));

Ensuite vous devez écrire le Wrapper .net en n’oubliant pas « Preview »

public event RoutedEventHandler PreviewFOO
{
add
{
AddHandler(PreviewFOOEvent, value);
}
remove
{
RemoveHandler(PreviewFOOEvent, value);
}
}

Comment définir un RoutedEvent Direct?

Il faut faire comme Bubble mais mettre Direct pour la stratégie de routage.

public static readonly RoutedEvent FOOEvent = EventManager.RegisterRoutedEvent("FOO", RoutingStrategy.Direct, typeof(RoutedEventHandler), typeof(MonCustomControl));

Comment déclencher un RoutedEvent ?

Il suffit d’appeler la séquence de code suivante

RaiseEvent(new RoutedEventArgs(MonCustomControl.FOOEvent));

Exemple complet d’un CustomControl avec un RoutedEvent Bubble

 public class CustomButton : Button
    {
        public static readonly RoutedEvent HelloWorldEvent = EventManager.RegisterRoutedEvent("HelloWorld", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(CustomButton));

        public event RoutedEventHandler HelloWorld
        {
            add
            {
                AddHandler(HelloWorldEvent, value);
            }
            remove
            {
                RemoveHandler(HelloWorldEvent, value);
            }
        }

        protected override void OnClick()
        {
            RaiseEvent(new RoutedEventArgs(CustomButton.HelloWorldEvent));
        }
    }

Exemple complet d’un CustomControl avec un RoutedEvent Tunnel

 public class CustomButton : Button
    {
        public static readonly RoutedEvent PreviewHelloWorldEvent = EventManager.RegisterRoutedEvent("PreviewHelloWorld", RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(CustomButton));

        public event RoutedEventHandler PreviewHelloWorld
        {
            add
            {
                AddHandler(PreviewHelloWorldEvent, value);
            }
            remove
            {
                RemoveHandler(PreviewHelloWorldEvent, value);
            }
        }

        protected override void OnClick()
        {
            RaiseEvent(new RoutedEventArgs(CustomButton.PreviewHelloWorldEvent));
        }
    }

Comment utiliser les RoutedEvent en XAML ?

Il suffit de déclarer les évènements en tant que propriétés attachées.

<Grid x:Name="root" local:CustomButton.HelloWorld="HelloWorldEventHandler"></Grid>

Exemple complet permettant de voir le fonctionnement du mode Bubble

XAML

<Window x:Class="RoutedEventSample.MainWindow"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		Title="MainWindow"
		xmlns:local="clr-namespace:RoutedEventSample">

	<Grid x:Name="root"
		  local:CustomButton.HelloWorld="EventCatchedHandler">
		<Grid.RowDefinitions>
			<RowDefinition Height="450" />
			<RowDefinition />
		</Grid.RowDefinitions>
		<Grid x:Name="GrilleJaune"
			  local:CustomButton.HelloWorld="EventCatchedHandler"
			  Background="#FFFFF36B">
			<Grid.ColumnDefinitions>
				<ColumnDefinition />
				<ColumnDefinition />
			</Grid.ColumnDefinitions>

			<Border Background="#FF91FF6B"
					local:CustomButton.HelloWorld="EventCatchedHandler"
					x:Name="BorderVert"
					Margin="40,40,20,40">
				<Border Background="#FFFD7171"
						Margin="40"
						x:Name="BorderRouge"
						local:CustomButton.HelloWorld="EventCatchedHandler">
					<local:CustomButton x:Name="BoutonGauche"
										Content="Button"
										HorizontalAlignment="Center"
										VerticalAlignment="Center"
										local:CustomButton.HelloWorld="EventCatchedHandler" />
				</Border>
			</Border>

			<Border Background="#FF92A7FF"
					Grid.Column="1"
					x:Name="BorderBleu"
					local:CustomButton.HelloWorld="EventCatchedHandler"
					Margin="20,40,40,40">
				<Border Background="#FFBC71FD"
						Margin="40"
						Grid.Column="1"
						local:CustomButton.HelloWorld="EventCatchedHandler"
						x:Name="BorderViolet">
					<local:CustomButton x:Name="BoutonDroite"
										Content="Button"
										HorizontalAlignment="Center"
										VerticalAlignment="Center"
										local:CustomButton.HelloWorld="EventCatchedHandler" />
				</Border>
			</Border>

		</Grid>

		<GridSplitter Grid.Row="1"
					  HorizontalAlignment="Stretch" VerticalAlignment="Top" Height="5" Panel.ZIndex="1"/>

		<ListBox x:Name="EventList"
				 Grid.Row="1"
				 Grid.ColumnSpan="2" />
		<Button Content="Effacer"
				Click="Button_Click"
				HorizontalAlignment="Right"
				VerticalAlignment="Top"
				Margin="0,5,5,0" Grid.Row="1"/>

	</Grid>
</Window>

CS

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void EventCatchedHandler(object sender, RoutedEventArgs e)
        {
            EventList.Items.Add(String.Format("{0:T} - Evenement {1} reçu sur {2}", DateTime.Now, e.RoutedEvent.Name, ((FrameworkElement)sender).Name));
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            EventList.Items.Clear();
        }
    }

Aperçu

On s’aperçoit bien que les évènements remontent du bouton jusqu’à la racine 🙂

RoutedEventSample

Exemple complet permettant de voir le fonctionnement du mode Tunnel

XAML

<Window x:Class="RoutedEventSample.MainWindow"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		Title="MainWindow"
		xmlns:local="clr-namespace:RoutedEventSample">

	<Grid x:Name="root"
		  local:CustomButton.PreviewHelloWorld="EventCatchedHandler">
		<Grid.RowDefinitions>
			<RowDefinition Height="450" />
			<RowDefinition />
		</Grid.RowDefinitions>
		<Grid x:Name="GrilleJaune"
			  local:CustomButton.PreviewHelloWorld="EventCatchedHandler"
			  Background="#FFFFF36B">
			<Grid.ColumnDefinitions>
				<ColumnDefinition />
				<ColumnDefinition />
			</Grid.ColumnDefinitions>

			<Border Background="#FF91FF6B"
					local:CustomButton.PreviewHelloWorld="EventCatchedHandler"
					x:Name="BorderVert"
					Margin="40,40,20,40">
				<Border Background="#FFFD7171"
						Margin="40"
						x:Name="BorderRouge"
						local:CustomButton.PreviewHelloWorld="EventCatchedHandler">
					<local:CustomButton x:Name="BoutonGauche"
										Content="Button"
										HorizontalAlignment="Center"
										VerticalAlignment="Center"
										local:CustomButton.PreviewHelloWorld="EventCatchedHandler" />
				</Border>
			</Border>

			<Border Background="#FF92A7FF"
					Grid.Column="1"
					x:Name="BorderBleu"
					local:CustomButton.PreviewHelloWorld="EventCatchedHandler"
					Margin="20,40,40,40">
				<Border Background="#FFBC71FD"
						Margin="40"
						Grid.Column="1"
						local:CustomButton.PreviewHelloWorld="EventCatchedHandler"
						x:Name="BorderViolet">
					<local:CustomButton x:Name="BoutonDroite"
										Content="Button"
										HorizontalAlignment="Center"
										VerticalAlignment="Center"
										local:CustomButton.PreviewHelloWorld="EventCatchedHandler" />
				</Border>
			</Border>

		</Grid>

		<GridSplitter Grid.Row="1"
					  HorizontalAlignment="Stretch" VerticalAlignment="Top" Height="5" Panel.ZIndex="1"/>

		<ListBox x:Name="EventList"
				 Grid.Row="1"
				 Grid.ColumnSpan="2" />
		<Button Content="Effacer"
				Click="Button_Click"
				HorizontalAlignment="Right"
				VerticalAlignment="Top"
				Margin="0,5,5,0" Grid.Row="1"/>

	</Grid>
</Window>

CS

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void EventCatchedHandler(object sender, RoutedEventArgs e)
        {
            EventList.Items.Add(String.Format("{0:T} - Evenement {1} reçu sur {2}", DateTime.Now, e.RoutedEvent.Name, ((FrameworkElement)sender).Name));
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            EventList.Items.Clear();
        }
    }

Aperçu

On s’aperçoit bien que les évènements sont déclanchés de la racine vers le bouton ! 🙂

RoutedEventSampleTunnel

Avec ces exemples tout devrait être beaucoup plus clair à présent 😀

Catégories :WPF

[SL5] : Silverlight WPF’s {x:Static} MarkupExtension

Silverlight 5 Beta fournit à présent la classe de base MarkupExtension qui lorsqu’on implémente la méthode ProvideValue(IServiceProvider), permet de définir une MarkupExtension customisée, qui peut être interprétée par le parseur de XAML de Silverlight 5.

Ceci permet de réaliser des tâches qui n’étaient pas possible, plutot complexes ou verbeuse à mettre en place. Par exemple il est à présent possible de réaliser un comportement similaire à ce que l’on connait en WPF tel que {x:Type}, {x:Static}, etc.

Dans ce post nous allons voir comment réaliser l’équivalent de x:Static connu en WPF utilisé dans certains cas comme le référencement de constantes dans le XAML. Les adeptes de PRISM l’utilisent généralement pour nommer les régions suivant des constantes définies dans l’infrastructure du projet.

Généralités

Pour faire une MarkupExtension vous devez implémenter une classe dérivant de l’interface IMarkupExtension<T> ou la classe de base MarkupExtension.


    public class YourMarkupExtension: IMarkupExtension<Object>
    {
        public Object ProvideValue(IServiceProvider serviceProvider)
        {
                throw new NotImplementedException();
        }
    }

Implémentation du comportement de {x:Static}


using System;
using System.Windows.Markup;
using System.Reflection;
using System.Xaml;

namespace SL5MarkupExtension.MarkupExtensions
{
    /// <summary>
    ///  Classe permettant de faire une MarkupExtension XAML pour récupérer les valeurs de champs ou propriétés.
    /// </summary>
    public class Static : IMarkupExtension<Object>
    {
        #region Data
        private String _member;
        private Type _memberType;
        #endregion

        #region Ctors
        /// <summary>
        ///  Crée une instance de la classe permettant de faire une MarkupExtension XAML pour récupérer les valeurs de champs ou propriétés.
        /// </summary>
        public Static()
        {
        }
        #endregion

        #region MarkupExtension implementation
        /// <summary>
        ///  Obtient un objet correspondant au type fournit en paramètre. Permet de récupérer un champ ou une propriété
        /// </summary>
        /// <param name="serviceProvider">Service permettant de récuperer le service utilisé pour les MarkupExtension.</param>
        /// <returns>
        ///  Un objet correspondant à la chaine fournie en paramettre (Member)
        /// </returns>
        public Object ProvideValue(IServiceProvider serviceProvider)
        {
            Object ret = null;
            Boolean typeResolveFailed = true;
            Type type = MemberType;
            String fieldMemberName = null;
            String fullFieldMemberName = null;

            if (Member != null)
            {
                if (MemberType != null)//on a le type et le membre
                {
                    fieldMemberName = Member;
                    fullFieldMemberName = String.Format("{0}.{1}", type.FullName, Member);
                }
                else //on a pas le type, on regarde si la chaine est bien formatée ex : local:MyEnum.MyEnumeValue et on essaie de résoudre le type
                {
                    Int32 index = Member.IndexOf('.');

                    if (index >= 0)
                    {
                        string typeName = Member.Substring(0, index);

                        if (!String.IsNullOrEmpty(typeName))
                        {
                            IXamlTypeResolver xamlTypeResolver = serviceProvider.GetService(typeof(IXamlTypeResolver)) as IXamlTypeResolver;

                            if (xamlTypeResolver != null)
                            {
                                type = xamlTypeResolver.Resolve(typeName);
                                fieldMemberName = Member.Substring(index + 1); //, Member.Length - index - 1
                                typeResolveFailed = String.IsNullOrEmpty(fieldMemberName);
                            }
                        }
                    }
                }

                if (typeResolveFailed)
                {
                    throw new InvalidOperationException("Member");
                }
                else
                {
                    if (type.IsEnum) //si c'est un enum alors on essaie de résoudre le membre
                    {
                        ret = Enum.Parse(type, fieldMemberName, true);
                    }
                    else //ce n'est pas un enum : probablement un champ ou une propriété
                    {
                        Boolean fail = true;

                        FieldInfo field = type.GetField(fieldMemberName, BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Static);

                        if (field != null)
                        {
                            fail = false;
                            ret = field.GetValue(null);
                        }
                        else//ce n'est pas un champ, on regarde si c'est une propriété
                        {
                            //on regarde si c'est une propriété
                            PropertyInfo property = type.GetProperty(fieldMemberName, BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Static);

                            if (property != null)//c'est une propriété
                            {
                                fail = false;
                                ret = property.GetValue(null, null);
                            }
                        }

                        if (fail)
                        {
                            throw new ArgumentException(fullFieldMemberName);
                        }
                    }
                }
            }
            else
            {
                throw new InvalidOperationException();
            }

            return ret;
        }
        #endregion

        #region Membres
        /// <summary>
        /// Obtient ou définit le membre statique à résoudre ex : local:MyEnum.MyEnumValue si pas de MemberType sinon MyEnumValue
        /// </summary>
        public string Member
        {
            get
            {
                return _member;
            }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException("Member value");
                }

                _member = value;
            }
        }

        /// <summary>
        /// Obtient ou définit le type du membre à résoudre
        /// </summary>
        public Type MemberType
        {
            get
            {
                return _memberType;
            }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException("MemberType value");
                }

                _memberType = value;
            }
        }
        #endregion
    }
}

Action !

Sample objects

Nous définissons ici une classe Module contenant 3 constantes


public class Modules
    {
        public static readonly string ModuleA = "NewsModule";
        public static readonly string ModuleB = "MediasModule";
        public static readonly string ModuleC = "CalendarModule";
    }

Puis un enum contenant 3 valeurs


    public enum Gender
    {
        Homme,
        Femme,
        Indefini
    }

Mise en pratique

Tout d’abord vous devez référencer votre MarkupExtension et les objets que vous souhaitez référencer.

xmlns:markup="clr-namespace:SL5MarkupExtension.MarkupExtensions"
xmlns:local="clr-namespace:SL5MarkupExtension"

Puis vous n’avez plus qu’à l’utiliser dans votre XAML avec la syntaxe suivante :

{markup:Static Member=local:Modules.ModuleA}

Exemple complet

<UserControl x:Class="SL5MarkupExtension.MainPage"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:markup="clr-namespace:SL5MarkupExtension.MarkupExtensions"
             xmlns:local="clr-namespace:SL5MarkupExtension"
             xmlns:app="clr-namespace:System.Windows;assembly=System.Windows">

    <Grid x:Name="LayoutRoot"
          Background="White">
        <StackPanel VerticalAlignment="Center"
                    HorizontalAlignment="Center">
            <!--CONST-->
            <TextBlock Text="{markup:Static Member=local:Modules.ModuleA}" />
            <TextBlock Text="{markup:Static Member=local:Modules.ModuleB}" />

            <!--CLASS-->
            <TextBlock Text="{Binding Path=InstallState, Source={markup:Static Member=app:Application.Current}}" />

            <!--ENUM-->
            <TextBlock Text="{Binding Source={markup:Static Member=local:Gender.Homme}}" />
        </StackPanel>
    </Grid>
</UserControl>

Résultat

silverlight-x-static-markupextension-like-wpf

Le résultat est très intéressant et ouvre de nouvelles possibilités dans le développement Silverlight 🙂

 

Update 4/7/2012 :
Said Outgajjouft m’a fait part d’une refactorisation du code pour le rendre plus clair, merci à lui 😉

using System;
    using System.Reflection;
    using System.Windows.Markup;
    using System.Xaml;


    /// <summary>
    /// Provides WPFs StaticExtension to Silverligth.
    /// The Static class implements a markup extension that returns static field and property references. 
    /// </summary>
    public class StaticExtension : IMarkupExtension<object>
    {
        /// <summary>"StaticExtension  must have Member property set before ProvideValue can be called.</summary>
        private const string MarkupExtensionStaticMember = "StaticExtension must have Member property set before ProvideValue can be called.";
        /// <summary>'{0}' StaticExtension value cannot be resolved to an enumeration, static field, or static property.</summary>
        private const string MarkupExtensionBadStatic = "'{0}' StaticExtension value cannot be resolved to an enumeration, static field, or static property.";
        /// <summary>Markup extension '{0}' requires '{1}' be implemented in the IServiceProvider for ProvideValue.</summary>
        private const string MarkupExtensionNoContext = "Markup extension '{0}' requires '{1}' be implemented in the IServiceProvider for ProvideValue.";

        private string _member;
        private Type _memberType;


        /// <summary>Gets or sets a member name string that is used to resolve a static field or property based on the service-provided type resolver.</summary>
        /// <exception cref="ArgumentNullException">Attempted to set <see cref="Member"/> to <b>null</b>.</exception>
        public string Member
        {
            get { return _member; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                _member = value;
            }
        }


        /// <summary>Gets or sets the <see cref="Type"/> that defines the static member to return.</summary>
        /// <exception cref="ArgumentNullException">Attempted to set <see cref="MemberType"/> to <b>null</b>.</exception>
        public Type MemberType
        {
            get { return _memberType; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                _memberType = value;
            }
        }


        /// <summary>
        /// Returns an object value to set on the property where you apply this extension.
        /// For <see cref="StaticExtension"/>, the return value is the static value that is evaluated for the requested static member.
        /// </summary>
        /// <param name="serviceProvider">
        /// An object that can provide services for the markup extension. The service provider is expected to provide a service 
        /// that implements a type resolver (<see cref="IXamlTypeResolver"/>).
        /// </param>
        /// <returns>The static value to set on the property where the extension is applied.</returns>
        /// <exception cref="InvalidOperationException">The <see cref="Member"/> for the extension is <b>null</b> at the time of evaluation.</exception>
        /// <exception cref="ArgumentException">
        /// <p>Some part of the <see cref="Member"/> string did not parse properly</p>
        /// <p>-or-</p>
        /// <p><paramref name="serviceProvider"/> did not provide a service for <see cref="IXamlTypeResolver"/></p>
        /// <p>-or-</p>
        /// <p><see cref="Member"/> value did not resolve to a static member.</p>
        /// </exception>
        /// <exception cref="ArgumentNullException"><paramref name="serviceProvider"/> is <b>null</b>.</exception>
        public object ProvideValue(IServiceProvider serviceProvider)
        {
            var member = Member;
            var memberType = MemberType;
            string memberName;


            if (member == null)
                throw new InvalidOperationException(MarkupExtensionStaticMember);

            if (memberType != null)
            {
                memberName = member;
            }
            else
            {
                var index = member.IndexOf('.');
                if (index == -1)
                    throw new ArgumentException(string.Format(MarkupExtensionBadStatic, member));

                var qualifiedTypeName = member.Substring(0, index);
                if (qualifiedTypeName.Length == 0)
                    throw new ArgumentException(string.Format(MarkupExtensionBadStatic, member));

                if (serviceProvider == null)
                    throw new ArgumentNullException("serviceProvider");

                var xamlTypeResolver = serviceProvider.GetService(typeof(IXamlTypeResolver)) as IXamlTypeResolver;
                if (xamlTypeResolver == null)
                    throw new ArgumentException(string.Format(MarkupExtensionNoContext, GetType().Name, typeof(IXamlTypeResolver).Name));

                memberType = xamlTypeResolver.Resolve(qualifiedTypeName);
                memberName = member.Substring(index + 1);
                if (memberName.Length == 0)
                    throw new ArgumentException(string.Format(MarkupExtensionBadStatic, member));
            }

            if (memberType.IsEnum)
                return Enum.Parse(memberType, memberName, true);

            var field = memberType.GetField(memberName, BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Static);
            if (field != null)
                return field.GetValue(null);

            var property = memberType.GetProperty(memberName, BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Static);
            if (property != null)
                return property.GetValue(null, null);

            throw new ArgumentException(string.Format(MarkupExtensionBadStatic, memberType.FullName + "." + memberName));
        }
    }

 

Catégories :Silverlight, Silverlight 5, WPF

[SL4 – WPF] : Pixel shader performance tips

23 février 2011 2 commentaires

Je partage aujourd’hui une petite astuce que j’utilise depuis longtemps pour pouvoir utiliser un pixel shader tel que le DropShadowEffect sans perte de performance.

Démonstration du problème

Nous allons faire un test avec un border arrondi avec un DropShadowEffect et contenant une TextBox.

<Border HorizontalAlignment="Center" Height="300" VerticalAlignment="Center" Width="300" Background="White" CornerRadius="5">
	<Border.Effect>
		<DropShadowEffect Opacity="0.5" ShadowDepth="0"/>
	</Border.Effect>
	<TextBox Margin="8"/>
</Border>

Maintenant activons les outils de débug inclus nativement dans Silverlight. Pour cela ajoutez les lignes suivantes dans la page aspx ou html affichant votre xap :

<param name="enableGPUAcceleration" value="true"/>
<param name="enableFrameRateCounter" value="true"/>
<param name="enableRedrawRegions" value="true"/>

Lancez l’application et saisissez un texte dans la TextBox.
 SilverlightBorderWithTextBoxAndDropShadow

On s’apercoit que le border est recalculé à chaque fois que la TextBox effectue un Rendering ! Ceci est très peu performant…

La solution au problème !

La technique que j’utilise depuis pas mal de temps est de réaliser le DropShadowEffect sur un arbre visuel très réduit et présent en background comme présenté ci dessous :

<Grid HorizontalAlignment="Center" Height="300" VerticalAlignment="Center" Width="300">
	<!--BACKGROUND + DROPSHADOW-->
	<Rectangle Fill="White" RadiusX="5" RadiusY="5">
		<Rectangle.Effect>
			<DropShadowEffect Opacity="0.5" ShadowDepth="0"/>
		</Rectangle.Effect>
	</Rectangle>

	<!--CONTENU-->
	<TextBox Margin="8"/>
</Grid>

Le résultat est sans appel ! :
SilverlightBorderWithTextBoxAndDropShadowFastPerformance

Conclusion

L’idée étant de ne pas effectuer un Effect sur un arbre visuel complexe. Pour cela je vous conseille de mettre un controle le plus simple possible par exemple un Rectangle plutot qu’un Border qui est plus complexe à afficher du fait que ce soir un ContentControl.
Puis vous englobez ce Rectangle dans une Grid et vous mettez votre arbre visuel complexe (ici notre TextBox).
Ainsi comme le rectangle ne recoit pas de demande de rafraichissement lors de la saisie de texte dans la TextBox, celui-ci ne réexécute pas le calcul du DropShadowEffect à chaque changement.
Vous disposerez ainsi d’une IHM très réactive tout en pouvant utiliser les Effects de WPF et Silverlight !

Enjoy 🙂

Catégories :Silverlight, WPF

[SL4–WPF] : HorizontalContentAlignment ListBox problem

8 février 2011 1 commentaire

Une petite astuce suite à une question d’un lecteur ayant un problème pour étirer un élément sur toute la largeur dans une ListBox.

Ceci est un bug présent dans Silverlight mais n’existe pas dans WPF.

Illustration du problème

Nous obtenons un alignement sur la gauche de notre ItemTemplate !

image

Alors que nous souhaiterions :

image

Base de code

XAML

        <UserControl x:Class="SilverlightApplication2.MainPage"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Grid Background="White">
        <ListBox ItemsSource="{Binding}">
            <!--ITEM TEMPLATE-->
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border BorderThickness="1"
                            BorderBrush="Blue"
                            Margin="5"
                            Padding="5">
                        <TextBlock Text="{Binding}" />
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</UserControl>

CS

  public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();

            DataContext = new List<String>()
            {
                "A",
                "B",
                "C",
                "D",
                "E"
            };
        }
    }

Solution en WPF : une ligne !

La solution est de mettre HorizontalContentAlignment=”Strech” en attribut de la ListBox…

 <ListBox ItemsSource="{Binding}"
                 HorizontalContentAlignment="Stretch">
<!-- ... -->
                 </ListBox>

Solution en Silverlight

Redéfinir le style par défaut du ListBoxItem, qui ne contient cette notion pour le moment.

 <ListBox ItemsSource="{Binding}">
            <!--ITEM CONTAINER STYLE-->
            <ListBox.ItemContainerStyle>
                <Style TargetType="ListBoxItem">
                    <Setter Property="HorizontalContentAlignment"
                            Value="Stretch" />
                </Style>
            </ListBox.ItemContainerStyle>

            <!--...-->
           
        </ListBox>
Catégories :Silverlight, WPF

Expression Studio 4 Ultimate

Une nouvelle version de la suite Microsoft Expression vient d’être publiée.

image

Vous pourrez la trouver à l’adresse suivante : http://www.microsoft.com/expression/products/StudioUltimate_Overview.aspx

Au titre des nouveautés, essentiellement des mises à jour et corrections de bugs par rapport à la RC de :

Catégories :Silverlight, Silverlight 4, WPF, WPF4