Comment créer votre propre contrôle avec WPF

Thomas LEBRUN

Windows Presentation Foundation offre un grand nombre de contrôles disponible pour développer vos applications. Cet article vous permettra de voir les deux options qui vous sont offertes si vous souhaitez, vous-même, créer votre propre contrôle.

N'hésitez pas à commenter cet article ! Commentez ♪

- Titre : Comment créer votre propre contrôle avec WPF

- Auteur : Thomas LEBRUN

- Parution : 3 juin 2008

- Mise à jour : 3 juin 2008

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2008 Thomas LEBRUN. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- I. Créer un contrôle utilisateur

- II. Hériter d'un contrôle existant

- III. Conclusions

- Sources

- Remerciements

- Contact

I - Créer un contrôle utilisateur

Avec Windows Presentation Foundation, il existe deux moyens, offerts aux développeurs, pour développer leurs propres contrôles.

Tout d'abord, ils peuvent créer un contrôle utilisateur (« UserControl »). L'autre possibilité est d'hériter d'un contrôle déjà existant. Chacune de ces techniques possède ses avantages et ses inconvénients que nous allons détailler dans la suite de cet article. L'objectif est de vous permettre de faire le bon choix lorsque vous serez amené à vous poser la question de comment faire pour développer votre contrôle.

Windows Presentation Foundation permet de créer ce que l'on appelle des contrôles utilisateur (« User Controls »). Si vous êtes développeur WindowsForms ou WebForms, cette notion ne doit pas vous être étrangère, car c'est un mécanisme utilisé très souvent.

Le but des contrôles utilisateur est de vous permettre de développer un contrôle qui est composé d'autres contrôles. Pour que cela soit plus simple à comprendre, prenez l'exemple d'un formulaire de connexion. Il s'agit d'une chose que l'on fait très souvent : une page/un formulaire composé de labels, de zones de saisies et de boutons. Si vous deviez développer une dizaine de fois un tel formulaire, vous vous rendriez vite compte que le code utilisé est toujours le même et que très peu de différences existent entre chacune des versions. Pour vous éviter d'avoir à écrire plusieurs fois la même chose, il est possible de regrouper ces contrôles dans un contrôle utilisateur, que vous déposerez sur vos formulaires : vous gagnez ainsi en temps de développement !

Avec WPF, il s'agit d'une technique tout à fait envisageable/utilisable dans vos projets. Pour créer ce type de contrôle, rien de plus simple : dans Visual Studio, faîtes un clic droit sur votre projet et choisissez « Add » => « User Control » :

Là, Visual Studio vous demande le nom de votre contrôle :

À ce moment-là, le contrôle est automatiquement ajouté à votre projet et vous disposez du designer WPF de Visual Studio pour définir le contenu de votre contrôle.

Si l'on regarde le code behind, on se rend compte que votre contrôle utilisateur n'est rien d'autre qu'une classe qui hérite de la classe « UserControl » :

public partial class DemoUC : UserControl

Cependant, vous pouvez vous demander quels sont les avantages/intérêts de créer un contrôle de ce type.

Le premier avantage réside dans le fait que les contrôles utilisateur supportent les contenus riches. Autrement dit, vous pouvez mettre ce que vous voulez (et au nombre que vous voulez) dans le code de votre contrôle.

Le deuxième avantage de ces contrôles est simple, mais très utilisé dans le cadre d'un développement WPF : ils supportent les styles et les triggers. Pour rappel, les styles sont utilisés pour définir l'apparence d'un contrôle (les couleurs, les styles de polices, etc.) tandis que les triggers sont des actions effectuées lorsqu'une condition est vérifiée.

Bien que pratique, les contrôles utilisateur possèdent un désavantage important qu'il faudra toujours bien garder à l'esprit : ils ne supportent pas les templates. De ce fait, ils ne peuvent pas être customisés afin d'être représentés différemment (via l'utilisation d'un ControlTemplate).

Pour savoir si la création d'un contrôle utilisateur est ce que vous devez faire, répondez à ces deux questions :

- votre contrôle est-il composé d'autres composants existants ?

- pouvez-vous vous passer de la customisation de votre contrôle ?

Si la réponse est « Oui » à chaque fois, alors il n'y a pas à hésiter : ce qu'il vous faut est un contrôle utilisateur !

Il est important de noter que ce type de contrôle possède une limitation : le binding, les animations, etc. ne sont pas supportés en l'état et il faut passer par des DependencyProperty.

II - Hériter d'un contrôle existant

Hériter d'un contrôle existant pour créer votre propre contrôle est une autre technique envisageable. Celle-ci vous permet bien plus de possibilités qu'implémenter un contrôle utilisateur, mais se trouve être légèrement plus complexe à mettre en place.

Utiliser les « Custom Controls » (c'est le nom que l'on donne aux contrôles que l'on crée via cette technique) vous offrira l'avantage de bien séparer l'interface utilisateur du contrôle et sa logique opérationnelle via l'utilisation de templates, cela afin d'offrir le maximum de flexibilité.

Pour créer un « Custom Controls », rien de plus simple avec Visual Studio. Faites un clic droit sur votre projet et choisissez « Add » => « New Item » :

Là, rendez-vous sur le nœud « WPF », sélectionnez « Custom Control (WPF) », entrez le nom « CheckedComboBox.cs » et cliquez sur « Add » :

À ce moment-là, Visual Studio va rajouter plusieurs éléments à votre projet :

- un répertoire « Themes » avec un fichier « Generic.xaml » : c'est dans ce fichier que nous allons définir, via du code XAML, l'apparence de notre contrôle ;

- un fichier contenant la logique de notre contrôle (CheckedComboBox.cs).

Comme vous pouvez le constater, Visual Studio fait, de lui-même, la séparation entre le code C# et l'interface. Maintenant, vous pouvez vous demander comment fait le moteur WPF pour savoir quel est le style à appliquer et où le trouver.

Pour savoir qu'il y a un style particulier à appliquer à votre contrôle, WPF se fie à cette ligne, écrite dans le constructeur statique de la classe représentant votre contrôle :

DefaultStyleKeyProperty.OverrideMetadata(typeof(CheckedComboBox), new FrameworkPropertyMetadata(typeof(CheckedComboBox)));

Via cette ligne, nous informons le moteur WPF que le style par défaut du contrôle est surchargé. À présent, il faut lui indiquer où aller chercher ce nouveau style par défaut. Pour cela, encore une fois, vous n'avez rien à faire, car Visual Studio a fait le travail pour vous : si vous ouvrez le fichier « AssemblyInfo.cs », vous remarquerez ce code :

[assembly: ThemeInfo(
ResourceDictionaryLocation.None,
ResourceDictionaryLocation.SourceAssembly
)]

Cette portion de code est utilisée pour indiquer où (entendez par là dans quelle assembly .NET) se trouve les dictionnaires de ressources (dont le générique) à utiliser si une ressource n'est pas trouvée dans la page, dans l'application ou dans un dictionnaire de ressource spécifique à un thème.

Maintenant que nous savons comment cela fonctionne, il est temps de le mettre en pratique. La première chose que nous allons faire, c'est modifier la classe parente de notre contrôle. En effet, par défaut, nous héritons de la classe « Control », qui est la classe parente de tous les contrôles WPF. C'est un bon début, mais dans notre cas, nous voulons créer une classe qui s'apparente plus à la ComboBox(avec des fonctionnalités supplémentaires) : il convient donc de spécifier que nous souhaitons hériter de ce contrôle :

public class CheckedComboBox : ComboBox

Une fois cette étape faîte, nous allons écrire l'interface utilisateur de notre ComboBox personnalisée. Pour cela, ouvrez le fichier « Generic.xaml » et remplacez le code suivant :

<Style TargetType="{x:Type local:CheckedComboBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CheckedComboBox}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Par ce code :

<Style x:Key="{x:Type local:CheckedComboBox}" TargetType="{x:Type local:CheckedComboBox}" BasedOn="{StaticResource {x:Type ComboBox}}">
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="{x:Type ComboBoxItem}" >
<Setter Property="Margin" Value="2, 2, 2, 0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBoxItem}">
<Border Background="Transparent" x:Name="borderSelect">
<CheckBox Content="{TemplateBinding Content}" x:Name="chkSelect" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Style>

Regardons de plus près ce que fait ce code. Nous définissons un style, qui s'appliquera à tous les contrôles de type CheckedComboBox. Dans ce style, nous redéfinissons le style de la propriété ItemContainerStyle. Ensuite, nous appliquons un style (sur tous les éléments de type ComboBoxItem) dont nous modifions le template pour qu'il soit composé d'une bordure incluant une case à cocher. On ne peut plus simple non !? ??

À présent, nous allons modifier le constructeur de notre contrôle pour spécifier que nous voulons, sur le clic d'une ComboBox, appeler une méthode spécifique. Pour cela, nous allons utiliser la méthode AddHandler, qui prend en paramètres l'évènement à intercepter et un délégué, qui représente la méthode à appeler :

public CheckedComboBox()
: base()
{
this.AddHandler(CheckBox.ClickEvent, new RoutedEventHandler(chkSelect_Click));
}

Maintenant, il nous faut écrire le code de la méthode chkSelect_Click, qui sera chargée, à chaque clic sur une case à cocher, de récupérer le contenu de l'ensemble des éléments sélectionnés pour les afficher dans notre ComboBox.

Voici donc le code de cette méthode :

private void chkSelect_Click(object sender, RoutedEventArgs e)
{
string TextToDisplay = string.Empty;

foreach (ComboBoxItem item in this.Items)
{
Border borderSelect = item.Template.FindName("borderSelect", item) as Border;

if (borderSelect != null)
{
CheckBox chkSelect = borderSelect.FindName("chkSelect") as CheckBox;

if (chkSelect != null)
{
if (chkSelect.IsChecked.GetValueOrDefault())
{
if (!string.IsNullOrEmpty(TextToDisplay))
{
TextToDisplay += ", ";
}

TextToDisplay += chkSelect.Content;
}
}
}
}

if (!string.IsNullOrEmpty(TextToDisplay))
{
// Remove item from the list
if (ItemToDisplay != null)
{
if (this.Items.Contains(ItemToDisplay))
{
this.Items.Remove(ItemToDisplay);
}
}

// Add item to the list for displaying the text
ItemToDisplay = new ComboBoxItem();
ItemToDisplay.Content = TextToDisplay;
ItemToDisplay.Visibility = Visibility.Collapsed;

this.Items.Add(ItemToDisplay);

this.SelectedItem = ItemToDisplay;
}
}

Lors de l'appel de cette méthode, nous parcourons l'ensemble des éléments de notre ComboBox. Sur chacun de ces éléments, nous accédons à la bordure (via son nom) puis nous accédons au contenu de la case à cocher (là encore via son nom) pour le stocker dans une chaîne de caractères. Comment est-ce possible ? Tout simplement, car nous avons défini, dans le template de notre contrôle, un style qui s'applique aux éléments de notre ComboBox et nous avons, dans ce style, indiqué que les éléments étaient en fait composés d'une bordure incluant une case à cocher (reportez-vous au contenu du fichier Generic.xamlen cas de doute).

Enfin, une fois que nous avons parcouru tous les éléments, nous créons un nouveau ComboBoxItem dont le contenu est la chaîne de caractères utilisée pour stocker le contenu de chacune des cases à cocher ! Comme cela est sans doute un peu abstrait, testez le contrôle pour vous rendre compte du résultat. Pour cela, ouvrez le fichier Window1.xaml et trouvez la ligne suivante :

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

Juste en dessous, ajoutez cette ligne de code :

xmlns:local="clr-namespace:ArticleCreationControlesWPF"

À présent, dans votre application, il ne vous reste plus qu'à faire appel à votre contrôle :

<local:CheckedComboBox Width="150" Height="25">
<ComboBoxItem>Un</ComboBoxItem>
<ComboBoxItem>Deux</ComboBoxItem>
<ComboBoxItem>Trois</ComboBoxItem>
<ComboBoxItem>Quatre</ComboBoxItem>
<ComboBoxItem>Cinq</ComboBoxItem>
</local:CheckedComboBox>

À l'exécution, vous constatez que vous obtenez bien une ComboBox dont les éléments sont des cases à cocher. De plus, à chaque fois que vous sélectionnez/désélectionnez un élément, l'ensemble des éléments cochés apparait dans la ComboBox :

Comme vous pouvez le voir, nous avons bien réussi à séparer la logique de notre contrôle de son interface graphique.

Bien sûr, le contrôle que nous avons développé est simpliste et loin d'être optimisé (il aurait par exemple fallu utiliser un StringBuilderpour stocker le texte, etc.), mais il vous permet de vous rendre compte des possibilités techniques.

III - Conclusions

Comme vous avez pu vous en rendre compte, les deux techniques évoquées précédemment possèdent chacune leurs points forts et leurs points faibles. C'est à vous, développeur WPF/.NET, de bien évaluer vos besoins et faire le bon choix. Enfin, si la création de contrôles personnalisés vous intéresse, je vous recommande de regarder le fameux « Bag'O'Trick » de Kevin Moore (http://j832.com/bagotricks/) ou bien le projet Codeplex WPFDeveloperTools(dont je suis l'auteur et qui est disponible à l'adresse suivante : http://www.codeplex.com/WPFDeveloperTools) : en effet, ils contiennent tous les deux un grand nombre de contrôles et les étudier vous permettra de bien maitriser ce concept et les différents patterns qui y sont liés !

Sources

Téléchargez la solution Visual Studio 2008.

Remerciements

J'adresse ici tous mes remerciements à l'équipe de rédaction de « developpez.com » pour le temps qu'ils ont bien voulu passer à la correction et à l'amélioration de cet article.

Contact

Si vous constatez une erreur ou pour toutes informations, n'hésitez pas à me contacter par le forum.

Thomas Lebrun

Consultant/Formateur

Microsoft MVP C#