Vue d'ensemble de la création de contrôles

Mise à jour : novembre 2007

L'extensibilité du modèle de contrôle Windows Presentation Foundation (WPF) réduit considérablement le besoin de créer un contrôle. Toutefois, vous serez parfois amené à devoir créer un contrôle personnalisé dans certains cas. Cette rubrique traite des fonctionnalités qui limitent votre besoin de créer un contrôle personnalisé et des différents modèles de création de contrôle dans Windows Presentation Foundation (WPF). Cette rubrique montre également comment créer un nouveau contrôle.

Cette rubrique comprend les sections suivantes.

  • Alternatives à l'écriture d'un nouveau contrôle
  • Modèles pour création de contrôle
  • Notions de base de la création de contrôle
  • Comparaison entre l'héritage d'un contrôle utilisateur et l'utilisation d'un modèle de contrôle
  • Rubriques connexes

Alternatives à l'écriture d'un nouveau contrôle

Historiquement, si vous souhaitiez obtenir une expérience personnalisée à partir d'un contrôle existant, vous étiez limité à la modification des propriétés standards du contrôle, telles que la couleur d'arrière-plan, la largeur de bordure et la taille de police. Si vous souhaitiez étendre l'apparence ou le comportement d'un contrôle au-delà de ces paramètres prédéfinis, vous deviez créer un contrôle, généralement via l'héritage d'un contrôle existant et la substitution de la méthode responsable du dessin du contrôle. Bien que ce soit toujours possible, WPF vous permet de personnaliser des contrôles existants en utilisant son modèle, ses styles, ses modèles et ses déclencheurs au contenu riche. La liste suivante propose des exemples d'utilisation de ces fonctionnalités pour créer des expériences personnalisées et cohérentes sans devoir créer un contrôle.

  • Contenu riche. De nombreux contrôles WPF standard prennent en charge le contenu riche. Par exemple, la propriété de contenu d'un Button est de type Object, de sorte que, théoriquement, il est possible de tout afficher sur un Button. Si vous voulez qu'un bouton affiche une image et un texte, vous pouvez ajouter une image et un TextBlock à un StackPanel et assigner le StackPanel à la propriété Content. Comme les contrôles peuvent afficher des éléments visuels WPF et des données arbitraires, il est moins souvent nécessaire de créer un contrôle ou de modifier un contrôle existant pour prendre en charge une visualisation complexe. Pour plus d'informations sur le modèle de contenu pour Button et d'autres contrôles, consultez Vue d'ensemble du modèle de contenu des contrôles. Pour plus d'informations sur les autres modèles de contenu dans WPF, consultez Modèles de contenu.

  • Styles. Un Style est une collection de valeurs qui représentent les propriétés d'un contrôle. L'utilisation de styles vous permet de créer une représentation réutilisable de l'apparence et du comportement souhaités d'un contrôle sans écrire un nouveau contrôle. Imaginons par exemple que vous vouliez que tous vos contrôles TextBlock utilisent une police Arial rouge d'une taille de 14. Vous pouvez créer un style en tant que ressource et définir les propriétés appropriées en conséquence. Chaque TextBlock que vous ajoutez à votre application a alors la même apparence.

  • Modèles de données. Un DataTemplate permet de personnaliser l'affichage des données sur un contrôle. Vous pouvez par exemple utiliser un DataTemplate pour définir l'affichage des données dans une ListBox. Pour obtenir un exemple, consultez Vue d'ensemble des modèles de données. En plus de personnaliser l'apparence des données, un DataTemplate peut inclure des éléments d'interface, ce qui vous offre une grande flexibilité dans les interfaces utilisateur personnalisées. Par exemple, vous pouvez créer une ComboBox dans laquelle chaque élément contient une case à cocher en utilisant un DataTemplate.

  • Modèles de contrôle. De nombreux contrôles dans WPF utilisent un ControlTemplate pour définir la structure et l'apparence du contrôle, ce qui distingue l'apparence d'un contrôle de ses fonctionnalités. Vous pouvez modifier considérablement l'apparence d'un contrôle en redéfinissant son ControlTemplate. Imaginons par exemple que vous souhaitiez créer un contrôle ressemblant à un feu rouge. Ce contrôle possède une interface utilisateur et des fonctionnalités simples. Il est constitué de trois cercles, dont un seul peut être allumé à la fois. Après quelques instants de réflexion, vous réalisez qu'une RadioButton offre la possibilité d'effectuer une sélection unique. L'apparence par défaut de la RadioButton ne ressemble toutefois en rien aux voyants d'un feu rouge. Comme la RadioButton utilise un modèle de contrôle pour définir son apparence, il est facile de redéfinir ce ControlTemplate pour répondre aux exigences du contrôle et utiliser des cases d'option pour créer votre feu rouge.

    Remarque :

    Bien qu'une RadioButton puisse utiliser un DataTemplate, ce dernier ne suffit pas dans cet exemple. Le DataTemplate définit l'apparence du contenu d'un contrôle. Dans le cas d'une RadioButton, le contenu est l'élément qui apparaît à droite du cercle qui indique si la RadioButton est sélectionnée. Dans l'exemple du feu rouge, la case d'option doit juste être un cercle capable de "s'allumer". Comme l'apparence du feu rouge est très différente de l'apparence par défaut de la RadioButton, il est nécessaire de redéfinir le ControlTemplate. En général, un DataTemplate est utilisé pour définir le contenu (ou les données) d'un contrôle et un ControlTemplate pour définir sa structure.

  • Déclencheurs. Un Trigger vous permet de modifier dynamiquement l'apparence et le comportement d'un contrôle sans créer de contrôle. Par exemple, imaginons vous ayez plusieurs contrôles ListBox dans votre application et souhaitiez que les éléments de chaque ListBox apparaissent en caractères gras et rouges lorsqu'ils sont sélectionnés. Votre premier réflexe vous dicte de créer une classe qui hérite de ListBox et substitue la méthode OnSelectionChanged pour modifier l'apparence de l'élément sélectionné. La solution idéale consiste toutefois à ajouter un déclencheur à un style de ListBoxItem qui modifie l'apparence de l'élément sélectionné. Un déclencheur vous permet de modifier les valeurs des propriétés ou de prendre des mesures sur la base de la valeur d'une propriété. Un EventTrigger vous permet de prendre des mesures lorsqu'un événement se produit.

Pour plus d'informations sur les styles, les modèles et les déclencheurs, consultez Application d'un style et création de modèles.

En règle générale, si votre contrôle reflète les fonctionnalités d'un contrôle existant, mais que vous souhaitez qu'il ait l'air différent, vous devez d'abord vous demander s'il est possible d'utiliser une des méthodes examinées de cette section pour modifier l'apparence du contrôle existant.

Modèles pour création de contrôle

Le modèle de contenu riche, les styles, les modèles et les déclencheurs réduisent la nécessité de créer des contrôles. Toutefois, si vous devez créer un contrôle, il est important de comprendre les différents modèles de création de contrôle proposés dans WPF. À cet effet, WPF propose trois modèles généraux offrant chacun un jeu de fonctionnalités et un niveau de flexibilité différent. Les classes de base de ces trois modèles sont UserControl, Control et FrameworkElement.

Dériver de UserControl

La méthode la plus simple pour créer un contrôle dans WPF consiste à le dériver d'un UserControl. Lorsque vous générez un contrôle qui hérite de UserControl, vous ajoutez des composants existants au UserControl, nommez les composants et référencez les gestionnaires d'événements dans XAML (Extensible Application Markup Language). Vous pouvez ensuite référencer les éléments nommés et définir les gestionnaires d'événements dans le code. Ce modèle de développement ressemble très fort au modèle utilisé pour le développement d'applications dans WPF.

Si la génération s'est déroulée correctement, un UserControl peut tirer parti des avantages d'un contenu riche, de styles et de déclencheurs. Toutefois, si votre contrôle hérite de UserControl, les personnes qui utilisent votre contrôle ne pourront pas utiliser un DataTemplate ou un ControlTemplate pour personnaliser son apparence. Il est nécessaire de dériver à partir de la classe Control ou d'une de ses classes dérivées (autre que UserControl) pour créer un contrôle personnalisé qui prend en charge des modèles.

Avantages de dériver de UserControl

Vous pouvez envisager de dériver de UserControl si toutes les conditions suivantes s'appliquent :

  • Vous voulez créer votre contrôle de la même manière que vous générez une application.

  • Votre contrôle contient uniquement des composants existants.

  • Vous n'avez pas besoin de prendre en charge une personnalisation complexe.

Dériver de Control

Dériver de la classe Control est le modèle utilisé par la plupart des contrôles WPF existants. Lorsque vous créez un contrôle qui hérite de la classe Control, vous définissez son apparence à l'aide de modèles. En agissant de la sorte, vous séparez la logique opérationnelle de la représentation visuelle. Vous pouvez également effectuer le découplage de l'interface utilisateur et de la logique en utilisant des commandes et des liaisons au lieu d'événements et en évitant chaque fois que possible de référencer des éléments dans le ControlTemplate. Si l'interface utilisateur et la logique de votre contrôle sont correctement découplées, les utilisateurs de votre contrôle pourront redéfinir le ControlTemplate afin de personnaliser son apparence. Bien que la génération d'un Control personnalisé ne soit pas aussi simple que celle d'un UserControl, un Control personnalisé assure une flexibilité optimale.

Avantages de dériver de Control

Vous pouvez envisager de dériver de Control au lieu d'utiliser la classe UserControl lorsqu'une ou plusieurs des conditions suivantes s'appliquent :

  • Vous voulez pouvoir personnaliser l'apparence de votre contrôle via le ControlTemplate.

  • Vous souhaitez que votre contrôle prenne en charge des thèmes différents.

Dériver de FrameworkElement

Les contrôles qui dérivent de UserControl ou de Control reposent sur la composition d'éléments existants. Cette solution est acceptable dans de nombreux scénarios, car tout objet héritant de FrameworkElement peut appartenir à un ControlTemplate. Dans certains cas, l'apparence d'un contrôle requiert toutefois des fonctionnalités autres que celles de la composition d'éléments simples. Pour ces scénarios, baser un composant sur FrameworkElement est le bon choix.

Il existe deux méthodes standard pour générer des composants basés sur FrameworkElement : le rendu direct et la composition d'éléments personnalisés. Le rendu direct implique de substituer la méthode OnRender de FrameworkElement et de fournir des opérations DrawingContext qui définissent explicitement les visuels du composant. C'est la méthode utilisée par Image et Border. La composition d'éléments personnalisés implique l'utilisation d'objets de type Visual pour composer l'apparence du composant. Pour obtenir un exemple, consultez Utilisation d'objets DrawingVisual. Track est un exemple de contrôle dans WPF qui utilise la composition d'éléments personnalisés. Il est également possible de combiner le rendu direct et la composition d'éléments personnalisés au sein du même contrôle.

Avantages de dériver de FrameworkElement

Vous pouvez envisager de dériver de FrameworkElement dans l'un des cas suivants :

  • Vous souhaitez avoir un contrôle précis sur l'apparence de votre contrôle allant au-delà des fonctionnalités proposées par la composition d'éléments simples.

  • Vous souhaitez définir l'apparence de votre contrôle en définissant votre propre logique de rendu.

  • Vous souhaitez composer des éléments existants de manière innovante au delà de ce qui est possible avec UserControl et Control.

Notions de base de la création de contrôle

Ainsi qu'il a été dit précédemment, l'une des fonctions les plus puissantes de WPF est la possibilité d'aller au-delà de la définition des propriétés de base d'un contrôle pour modifier son apparence et son comportement, sans qu'il soit nécessaire de créer un contrôle personnalisé. L'utilisation de fonctions de styles, de liaison de données et de déclencheur est rendue possible grâce aux systèmes de propriétés et d'événements de WPF. Si vous implémentez des propriétés de dépendance et des événements routés dans votre contrôle, les utilisateurs de votre contrôle personnalisé peuvent utiliser ces fonctions de la même manière que pour un contrôle livré avec WPF, quel que soit le modèle que vous avez utilisé pour créer le contrôle personnalisé. 

Utiliser des propriétés de dépendance

Dans le cas d'une propriété de dépendance, il est possible d'effectuer les opérations suivantes :

  • Définir la propriété dans un style.

  • Lier la propriété à une source de données.

  • Utiliser une ressource dynamique en tant que valeur de la propriété.

  • Animer la propriété.

Si vous voulez qu'une propriété de votre contrôle prenne en charge l'une de ces fonctionnalités, vous devez l'implémenter en tant que propriété de dépendance. L'exemple suivant montre comment définir une propriété de dépendance appelée Value.

  • Définissez un identificateur DependencyProperty appelé ValueProperty en tant que champ public static readonly.

  • Enregistre le nom de la propriété avec le système de propriétés, en appelant DependencyProperty.Register, afin de spécifier les éléments suivants :

    • le nom de la propriété ;

    • le type de la propriété ;

    • le type propriétaire de la propriété ;

    • Les métadonnées de la propriété. Les métadonnées contiennent la valeur par défaut de la propriété, un CoerceValueCallback et un PropertyChangedCallback.

  • Définissez une propriété de "wrapper" CLR appelée Value, qui est le nom utilisé pour enregistrer la propriété de dépendance, en implémentant les accesseurs get et set de la propriété. Notez que les accesseurs get et set appellent uniquement GetValue et SetValue, respectivement. Il est conseillé de ne pas inclure de logique supplémentaire dans les accesseurs de propriétés de dépendance car les clients et WPF peuvent ignorer les accesseurs et appeler directement GetValue et SetValue. Par exemple, lorsqu'une propriété est liée à une source de données, l'accesseur set de la propriété n'est pas appelé. Au lieu d'ajouter une logique supplémentaire aux accesseurs get et set, utilisez les délégués ValidateValueCallback, CoerceValueCallback et PropertyChangedCallback pour répondre ou vérifier la valeur lorsque celle-ci change. Pour plus d'informations sur ces rappels, consultez Validation et rappels de propriétés de dépendance.

  • Définissez une méthode pour le CoerceValueCallback nommé CoerceValue. CoerceValue vérifie que Value est supérieur ou égal à MinValue et inférieur ou égal à MaxValue.

  • Définissez une méthode pour le PropertyChangedCallback appelé OnValueChanged. OnValueChanged crée un objet RoutedPropertyChangedEventArgs<T> et se prépare à déclencher l'événement routé ValueChanged. Les événements routés sont examinés dans la section suivante.

/// <summary>
/// Identifies the Value dependency property.
/// </summary>
public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register(
        "Value", typeof(decimal), typeof(NumericUpDown),
        new FrameworkPropertyMetadata(MinValue, new PropertyChangedCallback(OnValueChanged),
                                      new CoerceValueCallback(CoerceValue)));

/// <summary>
/// Gets or sets the value assigned to the control.
/// </summary>
public decimal Value
{          
    get { return (decimal)GetValue(ValueProperty); }
    set { SetValue(ValueProperty, value); }
}

private static object CoerceValue(DependencyObject element, object value)
{
    decimal newValue = (decimal)value;
    NumericUpDown control = (NumericUpDown)element;

    newValue = Math.Max(MinValue, Math.Min(MaxValue, newValue));

    return newValue;
}

private static void OnValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    NumericUpDown control = (NumericUpDown)obj;         

    RoutedPropertyChangedEventArgs<decimal> e = new RoutedPropertyChangedEventArgs<decimal>(
        (decimal)args.OldValue, (decimal)args.NewValue, ValueChangedEvent);
    control.OnValueChanged(e);
}

Pour plus d'informations, consultez Propriétés de dépendance personnalisées.

Utiliser RoutedEvents

De même que les propriétés de dépendance étendent la notion de propriétés CLR avec des fonctionnalités supplémentaires, les événements routés étendent la notion d'événements CLR standard. Lorsque vous créez un contrôle WPF, il est également conseillé d'implémenter votre événement sous forme d'événement routé car un tel événement prend en charge le comportement suivant :

  • Les événements peuvent être gérés sur le parent de plusieurs contrôles. Si un événement est un événement de propagation, un parent unique de l'arborescence d'éléments peut s'abonner à l'événement. Les auteurs d'applications peuvent ensuite utiliser un gestionnaire unique pour répondre à l'événement de plusieurs contrôles. Par exemple, si votre contrôle fait partie de chaque élément d'une ListBox (parce qu'il est inclus dans un DataTemplate), le développeur d'applications peut définir le gestionnaire de l'événement de votre contrôle sur la ListBox. Dans ce cas, chaque fois que l'événement se produit sur un des contrôles, le gestionnaire d'événements est appelé.

  • Des événements routés peuvent être utilisés dans un EventSetter, ce qui permet aux développeurs d'applications de spécifier le gestionnaire d'un événement dans un style.

  • Il est possible d'utiliser des événements routés dans un EventTrigger, ce qui est utile pour animer des propriétés à l'aide de XAML. Pour plus d'informations, consultez Vue d'ensemble de l'animation.

L'exemple suivant montre comment définir un événement routé :

  • Définissez un identificateur RoutedEvent appelé ValueChangedEvent en tant que champ public static readonly.

  • Enregistrez l'événement routé en appelant la méthode EventManager.RegisterRoutedEvent. L'exemple spécifie les informations suivantes lorsqu'il appelle RegisterRoutedEvent :

    • Le nom de l'événement est ValueChanged.

    • La stratégie de routage est Bubble, ce qui signifie qu'un gestionnaire d'événements sur la source (l'objet qui déclenche l'événement) est appelé en premier, puis que les gestionnaires d'événements sur les éléments parents de la source sont appelés dans l'ordre, en commençant par le gestionnaire d'événements sur l'élément parent le plus proche.

    • Le type du gestionnaire d'événements est RoutedPropertyChangedEventHandler<T>, qui est construit avec un type Decimal.

    • Le type propriétaire de l'événement est NumericUpDown.

  • Déclarez un événement public appelé ValueChanged et incluez des déclarations d'accesseurs d'événement. L'exemple appelle AddHandler dans la déclaration d'accesseur add et RemoveHandler dans la déclaration d'accesseur remove afin d'utiliser les services d'événement WPF.

  • Créez une méthode virtuelle protégée appelée OnValueChanged, qui déclenche l'événement ValueChanged.

/// <summary>
/// Identifies the ValueChanged routed event.
/// </summary>
public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(
    "ValueChanged", RoutingStrategy.Bubble, 
    typeof(RoutedPropertyChangedEventHandler<decimal>), typeof(NumericUpDown));

/// <summary>
/// Occurs when the Value property changes.
/// </summary>
public event RoutedPropertyChangedEventHandler<decimal> ValueChanged
{
    add { AddHandler(ValueChangedEvent, value); }
    remove { RemoveHandler(ValueChangedEvent, value); }
}

/// <summary>
/// Raises the ValueChanged event.
/// </summary>
/// <param name="args">Arguments associated with the ValueChanged event.</param>
protected virtual void OnValueChanged(RoutedPropertyChangedEventArgs<decimal> args)
{
    RaiseEvent(args);
}

Pour plus d'informations, consultez Vue d'ensemble des événements routés et Comment : créer un événement routé personnalisé.

Utilisation de la liaison

Pour découpler l'interface utilisateur de votre contrôle de sa logique, pensez à utiliser la liaison de données. C'est particulièrement important si vous définissez l'apparence de votre contrôle à l'aide d'un ControlTemplate. L'utilisation de la liaison de données permet d'éviter de référencer des parties spécifiques de l'interface utilisateur au départ du code. Il est préférable d'éviter de référencer des éléments qui se trouvent dans le ControlTemplate parce que lorsque le code référence des éléments présents dans le ControlTemplate et que le ControlTemplate est modifié, l'élément référencé doit être inclus dans le nouveau ControlTemplate.

L'exemple suivant met à jour le TextBlock du contrôle NumericUpDown, en lui assignant un nom et référençant la zone de texte dans le code par son nom.

<Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
        Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">
  <TextBlock Name="valueText" Width="60" TextAlignment="Right" Padding="5"/>
</Border>
private void UpdateTextBlock()
{
    valueText.Text = Value.ToString();
}

L'exemple suivant utilise la liaison pour faire la même chose.

<Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
        Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">

    <!--Bind the TextBlock to the Value property-->
    <TextBlock 
        Width="60" TextAlignment="Right" Padding="5"
        Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                       AncestorType={x:Type local:NumericUpDown}}, 
                       Path=Value}"/>

</Border>

Pour plus d'informations sur la liaison de données, consultez Vue d'ensemble de la liaison de données.

Définir et utiliser des commandes

Envisagez de définir et d'utiliser des commandes au lieu de gérer des événements pour fournir les fonctionnalités. Lorsque vous utilisez des gestionnaires d'événements dans votre contrôle, l'action effectuée dans le gestionnaire d'événements est inaccessible aux applications. En revanche, si vous implémentez des commandes sur votre contrôle, les applications peuvent accéder aux fonctionnalités.

L'exemple suivant fait partie d'un contrôle qui gère l'événement de type clic pour deux boutons visant à modifier la valeur du contrôle NumericUpDown. Que le contrôle soit un UserControl ou un Control avec un ControlTemplate, l'interface utilisateur et la logique sont étroitement liées parce que le contrôle utilise des gestionnaires d'événements.

  <RepeatButton Name="upButton" Click="upButton_Click" 
                  Grid.Column="1" Grid.Row="0">Up</RepeatButton>
                  
    <RepeatButton Name="downButton" Click="downButton_Click" 
                  Grid.Column="1" Grid.Row="1">Down</RepeatButton>
private void upButton_Click(object sender, EventArgs e)
{
        Value++;
}

private void downButton_Click(object sender, EventArgs e)
{
        Value--;
}

L'exemple suivant définit deux commandes qui modifient la valeur du contrôle NumericUpDown.

public static RoutedCommand IncreaseCommand
{
    get
    {
        return _increaseCommand;
    }
}
public static RoutedCommand DecreaseCommand
{
    get
    {
        return _decreaseCommand;
    }
}

private static void InitializeCommands()
{
    _increaseCommand = new RoutedCommand("IncreaseCommand", typeof(NumericUpDown));
    CommandManager.RegisterClassCommandBinding(typeof(NumericUpDown), 
                            new CommandBinding(_increaseCommand, OnIncreaseCommand));
    CommandManager.RegisterClassInputBinding(typeof(NumericUpDown), 
                            new InputBinding(_increaseCommand, new KeyGesture(Key.Up)));

    _decreaseCommand = new RoutedCommand("DecreaseCommand", typeof(NumericUpDown));
    CommandManager.RegisterClassCommandBinding(typeof(NumericUpDown), 
                            new CommandBinding(_decreaseCommand, OnDecreaseCommand));
    CommandManager.RegisterClassInputBinding(typeof(NumericUpDown), 
                            new InputBinding(_decreaseCommand, new KeyGesture(Key.Down)));
}

private static void OnIncreaseCommand(object sender, ExecutedRoutedEventArgs e)
{
    NumericUpDown control = sender as NumericUpDown;
    if (control != null)
    {
        control.OnIncrease();
    }
}
private static void OnDecreaseCommand(object sender, ExecutedRoutedEventArgs e)
{
    NumericUpDown control = sender as NumericUpDown;
    if (control != null)
    {
        control.OnDecrease();
    }
}

protected virtual void OnIncrease()
{
    Value++;
}
protected virtual void OnDecrease()
{
    Value--;
}

private static RoutedCommand _increaseCommand;
private static RoutedCommand _decreaseCommand;

Les éléments contenus dans le modèle peuvent ensuite référencer les commandes, comme dans l'exemple suivant.

<RepeatButton 
    Command="{x:Static local:NumericUpDown.IncreaseCommand}"  
    Grid.Column="1" Grid.Row="0">Up</RepeatButton>
<RepeatButton 
    Command="{x:Static local:NumericUpDown.DecreaseCommand}"  
    Grid.Column="1" Grid.Row="1">Down</RepeatButton>

Les applications peuvent à présent référencer les liaisons pour accéder aux fonctionnalités qui étaient inaccessibles lorsque le contrôle utilisait des gestionnaires d'événements. Pour plus d'informations sur les commandes, consultez Vue d'ensemble des commandes.

Spécifier qu'un élément est requis dans un modèle de contrôle

Les sections précédentes expliquent comment utiliser la liaison de données et les commandes pour qu'un contrôle ne référence pas d'éléments dans son ControlTemplate au départ du code. Toutefois, il peut arriver que le référencement d'un élément soit inévitable. Dans ce cas, vous devez appliquer le TemplatePartAttribute à votre contrôle. Cet attribut informe les auteurs du modèle des types et des noms des éléments du ControlTemplate. Tous les éléments d'un ControlTemplate ne doivent pas nécessairement être nommés dans un TemplatePartAttribute. En fait, moins ils sont nombreux à l'être, mieux c'est. Toutefois, si vous référencez l'élément dans le code, il est conseillé d'utiliser le TemplatePartAttribute.

Pour plus d'informations sur la conception d'un contrôle qui utilise un ControlTemplate, consultez Recommandations pour la conception de contrôles auxquels un style peut être appliqué.

Conception pour les concepteurs

Pour obtenir de l'aide pour les contrôles WPF personnalisés dans Concepteur Windows Presentation Foundation (WPF) pour Visual Studio (par exemple, pour modifier des propriétés avec la fenêtre Propriétés), suivez les indications ci-après. Pour plus d'informations sur le développement pour Concepteur WPF, consultez Concepteur WPF.

Propriétés de dépendance

Veillez à implémenter des assesseurs get et setCLR comme décrit précédemment dans la section « Utiliser des propriétés de dépendance ». Les concepteurs peuvent utiliser le wrapper pour détecter la présence d'une propriété de dépendance, mais, à l'instar de WPF et des clients du contrôle, ils ne sont pas tenus d'appeler les accesseurs lors de l'obtention ou de la définition de la propriété.

Propriétés attachées

Vous devez implémenter des propriétés attachées sur des contrôles personnalisés à l'aide des indications suivantes :

  • Préparez une DependencyPropertypublicstaticreadonly de type NomPropriétéProperty créé à l'aide de l'une des méthodes RegisterAttached. Le nom de propriété passé à RegisterAttached doit correspondre à NomPropriété.

  • Implémentez deux méthodes publicstatic CLR nommées SetNomPropriété et GetNomPropriété. Les deux méthodes doivent accepter une classe dérivée de DependencyProperty pour leur premier argument. La méthode SetNomPropriété accepte également un argument dont le type correspond au type de données enregistré pour la propriété. La méthode GetNomPropriété doit retourner une valeur du même type. Si la méthode SetNomPropriété fait défaut, la propriété est marquée en lecture seule.

  • SetNomPropriété et GetNomPropriété doivent router directement aux méthodes GetValue et SetValue sur l'objet de dépendance cible, respectivement. Les concepteurs peuvent accéder à la propriété attachée en l'appelant via le wrapper de méthode ou lançant un appel direct à l'objet de dépendance cible.

Pour plus d'informations sur les propriétés attachées, consultez Vue d'ensemble des propriétés attachées.

Définition et utilisation de ressources partagées pour votre contrôle

Vous pouvez inclure votre contrôle dans le même assembly que votre application, ou l'intégrer dans un assembly séparé qui peut être utilisé dans plusieurs applications. Pour la plupart, les informations abordées dans cette rubrique sont valables, quelle que soit la méthode que vous utilisez. Toutefois, il existe une différence notable. Lorsque vous placez un contrôle dans le même assembly qu'une application, vous êtes libre d'ajouter des ressources globales au fichier app.xaml. Cependant, un assembly qui ne contient que des contrôles n'est associé à aucun objet Application et aucun fichier app.xaml n'est donc disponible.

Lorsqu'une application recherche une ressource, elle examine trois niveaux dans l'ordre suivant :

  1. niveau de l'élément ; le système commence par l'élément qui référence la ressource, puis il passe en revue les ressources du parent logique, et ainsi de suite, jusqu'à atteindre l'élément racine ;

  2. niveau de l'application : ressources définies par l'objet Application ;

  3. niveau du thème : les dictionnaires au niveau du thème sont enregistrés dans un sous-dossier appelé Themes. Les fichiers contenus dans ce dossier correspondent aux thèmes. Par exemple, vous pouvez avoir Aero.NormalColor.xaml, Luna.NormalColor.xaml, Royale.NormalColor.xaml, etc. Vous pouvez également avoir un fichier appelé generic.xaml. Lorsque le système recherche une ressource au niveau des thèmes, il la cherche en premier lieu dans le fichier spécifique au thème puis dans generic.xaml.

Lorsque votre contrôle se trouve dans un assembly distinct de l'application, vous devez placer vos ressources globales au niveau de l'élément ou du thème. Les deux méthodes ont leurs avantages.

Définition de ressources au niveau de l'élément

Vous pouvez définir des ressources partagées au niveau de l'élément en créant un dictionnaire de ressources personnalisé et en le fusionnant avec le dictionnaire de ressources de votre contrôle. Lorsque vous utilisez cette méthode, vous pouvez donner à votre fichier de ressources un nom de votre choix, et il peut se trouver dans le même dossier que vos contrôles. Les ressources au niveau de l'élément peuvent également utiliser des chaînes simples comme clés. L'exemple suivant crée un fichier de ressources LinearGradientBrush appelé Dictionary1.XAML.

<ResourceDictionary 
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <LinearGradientBrush 
    x:Key="myBrush"  
    StartPoint="0,0" EndPoint="1,1">
    <GradientStop Color="Red" Offset="0.25" />
    <GradientStop Color="Blue" Offset="0.75" />
  </LinearGradientBrush>

</ResourceDictionary>

Une fois que vous avez défini votre dictionnaire, vous devez le fusionner avec le dictionnaire de ressources de votre contrôle. Pour ce faire, vous pouvez utiliser le XAML ou du code.

L'exemple suivant fusionne un dictionnaire de ressources à l'aide du XAML.

<UserControl.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Dictionary1.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</UserControl.Resources>

L'inconvénient de cette approche réside dans le fait qu'un objet ResourceDictionary est créé chaque fois que vous le référencez. Par exemple, si vous avez dix contrôles personnalisés dans votre bibliothèque et fusionnez les dictionnaires de ressources partagées pour chaque contrôle en utilisant le XAML, vous créez 10 objets ResourceDictionary identiques. Vous pouvez éviter ceci en créant une classe statique qui fusionne les ressources dans le code et retourne le ResourceDictionary résultant.

L'exemple de code suivant crée une classe qui retourne un ResourceDictionary partagé.

internal static class SharedDictionaryManager
{
    internal static ResourceDictionary SharedDictionary
    {
        get
        {
            if (_sharedDictionary == null)
            {
                System.Uri resourceLocater =
                    new System.Uri("/ElementResourcesCustomControlLibrary;component/Dictionary1.xaml", 
                                    System.UriKind.Relative);

                _sharedDictionary = 
                    (ResourceDictionary)Application.LoadComponent(resourceLocater);
            }

            return _sharedDictionary;
        }
    }

    private static ResourceDictionary _sharedDictionary;
}

L'exemple suivant fusionne la ressource partagée avec les ressources d'un contrôle personnalisé dans le constructeur du contrôle avant d'appeler InitilizeComponent. SharedDictionaryManager.SharedDictionary étant une propriété statique, le ResourceDictionary n'est créé qu'une seule fois. Comme le dictionnaire de ressources a été fusionné avant l'appel de InitializeComponent, les ressources sont mises à la disposition du contrôle dans son fichier XAML.

public NumericUpDown()
{
    this.Resources.MergedDictionaries.Add(SharedDictionaryManager.SharedDictionary);
    InitializeComponent();

}

Définition de ressources au niveau du thème

WPF permet de créer des ressources pour les différents thèmes Windows. En tant qu'auteur du contrôle, vous pouvez définir une ressource pour un thème spécifique afin de modifier l'apparence de votre contrôle selon le thème utilisé. Par exemple, l'apparence d'un Button dans le thème Windows Classic (thème par défaut de Windows 2000) diffère de celle d'un Button avec le thème Windows Luna (thème par défaut de Windows XP) parce que le Button utilise un ControlTemplate différent pour chaque thème.  

Les ressources spécifiques à un thème sont conservées dans un dictionnaire de ressources avec un nom de fichier spécifique. Ces fichiers doivent se trouver dans un dossier appelé Themes, dépendant du dossier qui contient le contrôle. Le tableau suivant répertorie les fichiers des dictionnaires de ressources et le thème associé à chaque fichier :

Nom de fichier du dictionnaire de ressources

Thème de Windows

Classic.xaml

Classic – aspect habituel de Windows 9x/2000 sous Windows XP.

Luna.NormalColor.xaml

Thème bleu par défaut sous Windows XP

Luna.Homestead.xaml

Thème vert olive sur Windows XP

Luna.Metallic.xaml

Thème argent sur Windows XP

Royale.NormalColor.xaml

Thème par défaut sous Windows XP Édition Media Center

Aero.NormalColor.xaml

Thème par défaut sous Windows Vista

Il n'est pas nécessaire de définir une ressource pour chaque thème. Si une ressource n'est pas définie pour un thème spécifique, le contrôle utilise la ressource générique, qui se trouve dans un fichier de dictionnaire de ressources appelé generic.xaml, enregistré dans le même dossier que les fichiers de dictionnaire de ressources spécifiques au thème. Bien que generic.xaml ne corresponde pas à un thème spécifique de Windows, il s'agit néanmoins d'un dictionnaire au niveau du thème.

Contrôle personnalisé NumericUpDown avec thème et prise en charge d'UI Automation, exemple contient deux dictionnaires de ressources pour le contrôle NumericUpDown : le premier dans generic.xaml et le second dans Luna.NormalColor.xaml. Vous pouvez exécuter l'application et basculer entre le thème Argent de Windows XP et un autre thème afin de voir la différence entre les deux modèles de contrôle. (Si vous travaillez sous Windows Vista, vous pouvez renommer Luna.NormalColor.xaml en Aero.NormalColor.xaml et basculer entre deux thèmes, tels que Windows Classic et le thème par défaut de Windows Vista.)

Lorsque vous placez un ControlTemplate dans l'un des fichiers de dictionnaire de ressources spécifiques au thème, vous devez créer un constructeur statique pour votre contrôle et appeler la méthode OverrideMetadata(Type, PropertyMetadata) de la DefaultStyleKey, comme illustré dans l'exemple suivant.

static NumericUpDown()
{
    DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown),
               new FrameworkPropertyMetadata(typeof(NumericUpDown)));
}

Définition et référencement de clés pour les ressources de thème

Lorsque vous définissez une ressource au niveau de l'élément, vous pouvez assigner une chaîne comme clé et accéder à la ressource via la chaîne. Lorsque vous définissez une ressource au niveau du thème, vous devez utiliser une ComponentResourceKey comme clé. L'exemple suivant définit une ressource dans generic.xaml.

L'exemple suivant référence la ressource en spécifiant la ComponentResourceKey comme clé.

Spécification de l'emplacement des ressources spécifiques aux thèmes

Pour rechercher les ressources relatives à un contrôle, l'application d'hébergement doit savoir quel l'assembly contient des ressources spécifiques au contrôle. Pour ce faire, vous pouvez ajouter le ThemeInfoAttribute à l'assembly qui contient le contrôle. Le ThemeInfoAttribute comporte une propriété GenericDictionaryLocation qui spécifie l'emplacement des ressources génériques, et une propriété ThemeDictionaryLocation qui spécifie l'emplacement des ressources spécifiques au thème.

L'exemple suivant affecte aux propriétés GenericDictionaryLocation et ThemeDictionaryLocation la valeur SourceAssembly, pour spécifier que les ressources génériques et spécifiques au thème se trouvent dans le même assembly que le contrôle.

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

Comparaison entre l'héritage d'un contrôle utilisateur et l'utilisation d'un modèle de contrôle

Plusieurs exemples illustrent différentes méthodes de rédaction et d'intégration du contrôle NumericUpDown. Dans UserControl NumericUpDown avec DependencyProperty et RoutedEvent, exemple, NumericUpDown hérite de UserControl ; dans Contrôle personnalisé NumericUpDown avec thème et prise en charge d'UI Automation, exemple, NumericUpDown hérite de Control et utilise un ControlTemplate. Cette section décrit brièvement quelques-unes des différences entre les deux et explique pourquoi les contrôles qui utilisent un ControlTemplate sont plus extensibles.

La première différence majeure réside dans le fait que le NumericUpDown qui hérite de UserControl n'utilise pas de ControlTemplate, contrairement au contrôle qui hérite directement de Control. L'exemple suivant montre la méthode XAML d'un contrôle personnalisé qui hérite de UserControl. Comme vous pouvez le voir, le XAML est très semblable à ce que vous pouvez voir quand vous créez une application et commencez par un Window ou Page.

<!--XAML for NumericUpDown that inherits from UserControl.-->
<UserControl x:Class="MyUserControl.NumericUpDown"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MyUserControl">
    <Grid Margin="3">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
                Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">

            <!--Bind the TextBlock to the Value property-->
            <TextBlock 
                Width="60" TextAlignment="Right" Padding="5"
                Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                               AncestorType={x:Type local:NumericUpDown}}, 
                               Path=Value}"/>

        </Border>

        <RepeatButton Name="upButton" Click="upButton_Click" 
                      Grid.Column="1" Grid.Row="0">Up</RepeatButton>

        <RepeatButton Name="downButton" Click="downButton_Click" 
                      Grid.Column="1" Grid.Row="1">Down</RepeatButton>

    </Grid>
</UserControl>

L'exemple suivant montre la méthode ControlTemplate d'un contrôle personnalisé qui hérite de Control. Remarquez que le ControlTemplate ressemble au XAML du UserControl, mises à part quelques différences dans la syntaxe.

<!--ControlTemplate for NumericUpDown that inherits from
    Control.-->
<Style TargetType="{x:Type local:NumericUpDown}">
  <Setter Property="HorizontalAlignment" Value="Center"/>
  <Setter Property="VerticalAlignment" Value="Center"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type local:NumericUpDown}">
        <Grid Margin="3">
          <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
          </Grid.RowDefinitions>
          <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
          </Grid.ColumnDefinitions>

          <Border BorderThickness="1" BorderBrush="Gray" 
                  Margin="2" Grid.RowSpan="2" 
                  VerticalAlignment="Center" HorizontalAlignment="Stretch">

            <TextBlock Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Value}" 
                       Width="60" TextAlignment="Right" Padding="5"/>
          </Border>

          <RepeatButton Command="{x:Static local:NumericUpDown.IncreaseCommand}"  
                        Grid.Column="1" Grid.Row="0">Up</RepeatButton>

          <RepeatButton Command="{x:Static local:NumericUpDown.DecreaseCommand}"
                        Grid.Column="1" Grid.Row="1">Down</RepeatButton>

        </Grid>

      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

La principale différence entre les deux exemples précédents réside dans le fait que celui qui utilise le ControlTemplate a une apparence personnalisable, contrairement à celui qui hérite de UserControl. Dans le cas où NumericUpDown hérite de UserControl, un développeur d'applications ne peut rien faire pour modifier l'apparence du contrôle. En fait, bien que le NumericUPDown ait une propriété ControlTemplate (parce que UserControl hérite de Control), une exception se produira au moment de l'exécution si quelqu'un essaie d'en modifier la valeur. En revanche, un développeur d'applications qui utilise le NumericUpDown qui hérite de Control est libre de créer un ControlTemplate pour le contrôle. Par exemple, quelqu'un pourrait créer un ControlTemplate qui place les boutons à gauche et à droite du TextBlock et non au-dessus et en-dessous de celui-ci.

La différence entre les deux approches est manifeste dans la différence syntaxique que révèlent les exemples précédents. Le contrôle qui utilise un ControlTemplate définit la propriété Template d'un Style pour NumericUpDown. Il s'agit d'une méthode courante pour créer des modèles de contrôle. En définissant la propriété Template dans un style, vous indiquez que toutes les instances du contrôle utiliseront ce ControlTemplate. Les développeurs d'applications sont libres de modifier la propriété Template d'un NumericcUpDown pour personnaliser son apparence. En revanche, le XAML du contrôle qui hérite du UserControl renseigne la propriété Content de NumericUpDown (<UserControl.Content> est inclus implicitement dans le XAML). Si un développeur d'applications ne peut pas modifier la propriété Content, le NumericUpDown n'est pas utilisable.

Une autre différence entre les exemples réside dans la manière dont les contrôles répondent aux boutons Haut et Bas. Le contrôle qui hérite de UserControl gère l'événement de type clic, et le contrôle qui utilise un ControlTemplate implémente des commandes et se lie aux commandes de son ControlTemplate. En conséquence, un développeur d'applications qui crée un ControlTemplate pour le NumericUpDown peut également se lier aux commandes et conserver les fonctionnalités du contrôle. Si le ControlTemplate a géré l'événement de type clic au lieu de créer une liaison avec les commandes, un développeur d'applications devrait implémenter des gestionnaires d'événements lors de la création d'un ControlTemplate, mettant ainsi un terme à l'encapsulation du NumericUpDown.

Autre différence : la syntaxe de la liaison entre la propriété Text du TextBlock et la propriété Value. Dans le cas du UserControl, la liaison spécifie que la RelativeSource est le contrôle NumericUpDown parent et se lie à la propriété Value. Dans le cas du ControlTemplate, la RelativeSource est le contrôle auquel appartient le modèle. Ils accomplissent la même chose, mais il est préférable de mentionner que la syntaxe de liaison diffère dans les deux exemples.

Dans Contrôle personnalisé NumericUpDown avec thème et prise en charge d'UI Automation, exemple, le contrôle NumericUpDown se trouve dans un assembly distinct de l'application. Il définit et utilise les ressources au niveau du thème. En revanche, dans UserControl NumericUpDown avec DependencyProperty et RoutedEvent, exemple, le contrôle NumericUpDown se trouve dans le même assembly que l'application. Il ne définit et n'utilise pas les ressources au niveau du thème.

Enfin, Contrôle personnalisé NumericUpDown avec thème et prise en charge d'UI Automation, exemple montre comment créer un AutomationPeer pour le contrôle NumericUpDown. Pour plus d'informations sur la prise en charge des contrôles UI Automation personnalisés, consultez UI Automation d'un contrôle personnalisé WPF.

Voir aussi

Concepts

URI à en-tête pack dans Windows Presentation Foundation

Autres ressources

Concepteur WPF

Personnalisation des contrôles

Exemples de personnalisation de contrôles