it-swarm-fr.com

Comment utiliser les liaisons WPF avec RelativeSource?

Comment utiliser RelativeSource avec des liaisons WPF et quels sont les différents cas d'utilisation?

555
David Schmitt

Si vous souhaitez lier une autre propriété sur l'objet:

{Binding Path=PathToProperty, RelativeSource={RelativeSource Self}}

Si vous voulez obtenir une propriété sur un ancêtre:

{Binding Path=PathToProperty,
    RelativeSource={RelativeSource AncestorType={x:Type typeOfAncestor}}}

Si vous voulez obtenir une propriété sur le parent basé sur un modèle (vous pouvez donc faire des liaisons à double sens dans un ControlTemplate)

{Binding Path=PathToProperty, RelativeSource={RelativeSource TemplatedParent}}

ou plus court (cela ne fonctionne que pour les liaisons OneWay):

{TemplateBinding Path=PathToProperty}
743
Abe Heidebrecht
Binding RelativeSource={
    RelativeSource Mode=FindAncestor, AncestorType={x:Type ItemType}
}
...

L'attribut par défaut de RelativeSource est la propriété Mode. Un ensemble complet de valeurs valides est donné ici ( from MSDN ):

  • PreviousData Vous permet de lier l'élément de données précédent (pas ce contrôle contenant l'élément de données) dans la liste des éléments de données affichés.

  • TemplatedParent Fait référence à l'élément auquel le modèle (dans lequel l'élément lié aux données existe) est appliqué. Ceci est similaire à la définition d'une TemplateBindingExtension et n'est applicable que si la liaison est dans un modèle.

  • Self Fait référence à l'élément sur lequel vous définissez la liaison et vous permet de lier une propriété de cet élément à une autre propriété du même élément.

  • FindAncestor Fait référence à l'ancêtre dans la chaîne parente de l'élément lié aux données. Vous pouvez l'utiliser pour vous lier à un ancêtre d'un type spécifique ou à ses sous-classes. C'est le mode que vous utilisez si vous souhaitez spécifier AncestorType et/ou AncestorLevel.

127
Drew Noakes

Voici une explication plus visuelle dans le contexte d'une architecture MVVM:

enter image description here

121
Jeffrey Knight

Imaginons ce cas, un rectangle dans lequel nous voulons que sa hauteur soit toujours égale à sa largeur, un carré par exemple. Nous pouvons le faire en utilisant le nom de l'élément

<Rectangle Fill="Red" Name="rectangle" 
                    Height="100" Stroke="Black" 
                    Canvas.Top="100" Canvas.Left="100"
                    Width="{Binding ElementName=rectangle,
                    Path=Height}"/>

Mais dans ce cas, nous sommes obligés d'indiquer le nom de l'objet de liaison, à savoir le rectangle. Nous pouvons atteindre le même but différemment en utilisant RelativeSource

<Rectangle Fill="Red" Height="100" 
                   Stroke="Black" 
                   Width="{Binding RelativeSource={RelativeSource Self},
                   Path=Height}"/>

Dans ce cas, nous ne sommes pas obligés de mentionner le nom de l'objet de liaison et la largeur sera toujours égale à la hauteur chaque fois que la hauteur est modifiée.

Si vous souhaitez paramétrer la largeur comme étant la moitié de la hauteur, vous pouvez le faire en ajoutant un convertisseur à l'extension de balise Binding. Imaginons un autre cas maintenant:

 <TextBlock Width="{Binding RelativeSource={RelativeSource Self},
                   Path=Parent.ActualWidth}"/>

Le cas ci-dessus est utilisé pour lier une propriété donnée d'un élément donné à l'une de ses propriétés parent directes, car cet élément contient une propriété appelée Parent. Cela nous conduit à un autre mode source relatif, celui de FindAncestor.

41

Bechir Bejaoui expose les cas d'utilisation de RelativeSources dans WPF dans son article ici :

RelativeSource est une extension de balisage utilisée dans des cas de liaison particuliers lorsque nous essayons de lier une propriété d'un objet donné à une autre propriété de l'objet lui-même, lorsque nous essayons de lier une propriété d'un objet à une autre de ses parents relatifs. lors de la liaison d'une valeur de propriété de dépendance à une partie de XAML en cas de développement de contrôle personnalisé et enfin en cas d'utilisation d'un différentiel d'une série de données liées. Toutes ces situations sont exprimées sous forme de modes source relatifs. Je vais exposer tous ces cas un par un.

  1. Mode auto:

Imaginons ce cas, un rectangle dans lequel nous voulons que sa hauteur soit toujours égale à sa largeur, un carré par exemple. Nous pouvons le faire en utilisant le nom de l'élément

<Rectangle Fill="Red" Name="rectangle" 
                Height="100" Stroke="Black" 
                Canvas.Top="100" Canvas.Left="100"
                Width="{Binding ElementName=rectangle,
                Path=Height}"/>

Mais dans ce cas, nous sommes obligés d'indiquer le nom de l'objet de liaison, à savoir le rectangle. Nous pouvons atteindre le même but différemment en utilisant RelativeSource

<Rectangle Fill="Red" Height="100" 
               Stroke="Black" 
               Width="{Binding RelativeSource={RelativeSource Self},
               Path=Height}"/>

Dans ce cas, nous ne sommes pas obligés de mentionner le nom de l'objet de liaison et la largeur sera toujours égale à la hauteur chaque fois que la hauteur est modifiée.

Si vous souhaitez paramétrer la largeur comme étant la moitié de la hauteur, vous pouvez le faire en ajoutant un convertisseur à l'extension de marquage. Imaginons un autre cas maintenant:

 <TextBlock Width="{Binding RelativeSource={RelativeSource Self},
               Path=Parent.ActualWidth}"/>

Le cas ci-dessus est utilisé pour lier une propriété donnée d'un élément donné à l'une de ses propriétés parent directes, car cet élément contient une propriété appelée Parent. Cela nous conduit à un autre mode source relatif, celui de FindAncestor.

  1. Mode FindAncestor

Dans ce cas, une propriété d'un élément donné sera liée à l'un de ses parents, Corse. La principale différence avec le cas ci-dessus réside dans le fait que c'est à vous de déterminer le type d'ancêtre et le rang de l'ancêtre dans la hiérarchie pour lier la propriété. Au fait, essayez de jouer avec ce morceau de XAML

<Canvas Name="Parent0">
    <Border Name="Parent1"
             Width="{Binding RelativeSource={RelativeSource Self},
             Path=Parent.ActualWidth}"
             Height="{Binding RelativeSource={RelativeSource Self},
             Path=Parent.ActualHeight}">
        <Canvas Name="Parent2">
            <Border Name="Parent3"
            Width="{Binding RelativeSource={RelativeSource Self},
           Path=Parent.ActualWidth}"
           Height="{Binding RelativeSource={RelativeSource Self},
              Path=Parent.ActualHeight}">
               <Canvas Name="Parent4">
               <TextBlock FontSize="16" 
               Margin="5" Text="Display the name of the ancestor"/>
               <TextBlock FontSize="16" 
                 Margin="50" 
            Text="{Binding RelativeSource={RelativeSource  
                       FindAncestor,
                       AncestorType={x:Type Border}, 
                       AncestorLevel=2},Path=Name}" 
                       Width="200"/>
                </Canvas>
            </Border>
        </Canvas>
     </Border>
   </Canvas>

La situation ci-dessus concerne deux éléments TextBlock: ceux-ci sont incorporés dans une série de bordures et les éléments de canevas représentent ceux de leurs parents hiérarchiques. Le second TextBlock affichera le nom du parent donné au niveau source relatif.

Essayez donc de changer AncestorLevel = 2 en AncestorLevel = 1 et voyez ce qui se passe. Ensuite, essayez de changer le type de l'ancêtre d'AncestorType = Border à AncestorType = Canvas et voyez ce qui se passe.

Le texte affiché changera en fonction du type et du niveau de l'ancêtre. Alors que se passe-t-il si le niveau d'ancêtre n'est pas adapté au type d'ancêtre? C'est une bonne question, je sais que vous êtes sur le point de la poser. La réponse est qu'aucune exception ne sera levée et rien ne sera affiché au niveau TextBlock.

  1. Parent modèle

Ce mode permet d'associer une propriété ControlTemplate donnée à une propriété du contrôle auquel le ControlTemplate est appliqué. Pour bien comprendre le problème, voici un exemple ci-dessous

<Window.Resources>
<ControlTemplate x:Key="template">
        <Canvas>
            <Canvas.RenderTransform>
                <RotateTransform Angle="20"/>
                </Canvas.RenderTransform>
            <Ellipse Height="100" Width="150" 
                 Fill="{Binding 
            RelativeSource={RelativeSource TemplatedParent},
            Path=Background}">

              </Ellipse>
            <ContentPresenter Margin="35" 
                  Content="{Binding RelativeSource={RelativeSource  
                  TemplatedParent},Path=Content}"/>
        </Canvas>
    </ControlTemplate>
</Window.Resources>
    <Canvas Name="Parent0">
    <Button   Margin="50" 
              Template="{StaticResource template}" Height="0" 
              Canvas.Left="0" Canvas.Top="0" Width="0">
        <TextBlock FontSize="22">Click me</TextBlock>
    </Button>
 </Canvas>

Si je veux appliquer les propriétés d'un contrôle donné à son modèle de contrôle, je peux utiliser le mode TemplatedParent. Il existe également une extension similaire à cette extension de balisage, TemplateBinding, qui est une sorte de raccourci de la première, mais TemplateBinding est évalué au moment de la compilation, contrairement à TemplatedParent, qui est évalué juste après le premier temps d'exécution. Comme vous pouvez le constater dans l'illustration ci-dessous, l'arrière-plan et le contenu sont appliqués depuis le bouton au modèle de contrôle.

34
Cornel Marian

Dans WPF RelativeSource la liaison expose trois properties pour définir:

1. Mode: Ceci est un enum qui pourrait avoir quatre valeurs:

a. PreviousData (_value=0_): Assigne la valeur précédente de la property à la valeur liée.

b. TemplatedParent (_value=1_): Ceci est utilisé lors de la définition de la templates de tout contrôle et que vous souhaitez vous lier à une valeur/propriété du control.

Par exemple, définissez ControlTemplate:

_  <ControlTemplate>
        <CheckBox IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
 </ControlTemplate>
_

c. Self (_value=2_): Lorsque nous voulons créer une liaison à partir d'un self ou d'un property de self.

Par exemple: Envoie l'état vérifié de checkbox en tant que CommandParameter lors du réglage de Command sur CheckBox

_<CheckBox ...... CommandParameter="{Binding RelativeSource={RelativeSource Self},Path=IsChecked}" />
_

d. FindAncestor (_value=3_): Lorsque vous souhaitez créer une liaison à partir d'un parent control dans _Visual Tree_.

Par exemple: Lier un checkbox dans records si un grid, si headercheckbox est coché

_<CheckBox IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid}}, Path=DataContext.IsHeaderChecked, Mode=TwoWay}" />
_

2. AncestorType: quand le mode est FindAncestor alors définir quel type d'ancêtre

_RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid}}
_

3. AncestorLevel: quand le mode est FindAncestor alors quel niveau d'ancêtre (s'il y a deux mêmes types de parent dans _visual tree_)

_RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid, AncestorLevel=1}}
_

Ci-dessus figurent tous les cas d'utilisation de RelativeSource binding.

Voici un lien de référence .

25
Kylo Ren

N'oubliez pas TemplatedParent:

<Binding RelativeSource="{RelativeSource TemplatedParent}"/>

ou

{Binding RelativeSource={RelativeSource TemplatedParent}}
18
Bob King

J'ai créé une bibliothèque pour simplifier la syntaxe de liaison de WPF, notamment en facilitant l'utilisation de RelativeSource. Voici quelques exemples. Avant:

{Binding Path=PathToProperty, RelativeSource={RelativeSource Self}}
{Binding Path=PathToProperty, RelativeSource={RelativeSource AncestorType={x:Type typeOfAncestor}}}
{Binding Path=PathToProperty, RelativeSource={RelativeSource TemplatedParent}}
{Binding Path=Text, ElementName=MyTextBox}

Après:

{BindTo PathToProperty}
{BindTo Ancestor.typeOfAncestor.PathToProperty}
{BindTo Template.PathToProperty}
{BindTo #MyTextBox.Text}

Voici un exemple de la manière dont la liaison de méthode est simplifiée. Avant:

// C# code
private ICommand _saveCommand;
public ICommand SaveCommand {
 get {
  if (_saveCommand == null) {
   _saveCommand = new RelayCommand(x => this.SaveObject());
  }
  return _saveCommand;
 }
}

private void SaveObject() {
 // do something
}

// XAML
{Binding Path=SaveCommand}

Après:

// C# code
private void SaveObject() {
 // do something
}

// XAML
{BindTo SaveObject()}

Vous pouvez trouver la bibliothèque ici: http://www.simplygoodcode.com/2012/08/simpler-wpf-binding.html

Notez dans l'exemple 'BEFORE' que j'utilise pour la liaison de méthode que le code a déjà été optimisé à l'aide de RelayCommand que j'ai vérifié en dernier lieu ne fait pas partie de WPF. Sans cela, l'exemple 'AVANT' aurait été encore plus long.

13
Luis Perez

Il est à noter que pour ceux qui tombent sur cette pensée de Silverlight:

Silverlight propose uniquement un sous-ensemble réduit de ces commandes

13
Matthew Black

Quelques morceaux utiles:

Voici comment le faire principalement dans le code:

Binding b = new Binding();
b.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, this.GetType(), 1);
b.Path = new PropertyPath("MyElementThatNeedsBinding");
MyLabel.SetBinding(ContentProperty, b);

J'ai en grande partie copié ceci de source relative de liaison dans le code derrière.

En outre, la page MSDN est plutôt bonne en ce qui concerne les exemples: classe RelativeSource

12
Nathan Cooper

Je viens de poster une autre solution pour accéder au DataContext d'un élément parent dans Silverlight qui fonctionne pour moi. Il utilise Binding ElementName.

10
Juve

Je n'ai pas lu toutes les réponses, mais je veux simplement ajouter cette information dans le cas d'une liaison de commande de source relative à un bouton.

Lorsque vous utilisez une source relative avec Mode=FindAncestor, la liaison doit ressembler à:

Command="{Binding Path=DataContext.CommandProperty, RelativeSource={...}}"

Si vous n'ajoutez pas DataContext dans votre chemin, il ne peut pas récupérer la propriété au moment de l'exécution.

9
Kevin VDF

Ceci est un exemple d'utilisation de ce modèle qui a fonctionné pour moi sur des datagrids vides.

<Style.Triggers>
    <DataTrigger Binding="{Binding Items.Count, RelativeSource={RelativeSource Self}}" Value="0">
        <Setter Property="Background">
            <Setter.Value>
                <VisualBrush Stretch="None">
                    <VisualBrush.Visual>
                        <TextBlock Text="We did't find any matching records for your search..." FontSize="16" FontWeight="SemiBold" Foreground="LightCoral"/>
                    </VisualBrush.Visual>
                </VisualBrush>
            </Setter.Value>
        </Setter>
    </DataTrigger>
</Style.Triggers>
8
Edd

Si un élément ne fait pas partie de l'arborescence visuelle, RelativeSource ne fonctionnera jamais.

Dans ce cas, vous devez essayer une technique différente, mise au point par Thomas Levesque.

Il a la solution sur son blog sous [WPF] Comment lier des données lorsque le DataContext n’est pas hérité . Et ça marche absolument génial!

Dans le cas peu probable où son blog serait en panne, l’Annexe A contient une copie miroir de son article .

S'il vous plaît ne commentez pas ici, s'il vous plaît commentez directement sur son blog .

Annexe A: Miroir de l'article de blog

La propriété DataContext dans WPF est extrêmement pratique car elle est automatiquement héritée par tous les enfants de l'élément auquel vous l'attribuez. Par conséquent, vous n'avez pas besoin de le redéfinir sur chaque élément que vous souhaitez lier. Cependant, dans certains cas, le DataContext n'est pas accessible: cela se produit pour des éléments qui ne font pas partie de l'arborescence visuelle ou logique. Il peut alors être très difficile de lier une propriété sur ces éléments…

Illustrons par un exemple simple: nous voulons afficher une liste de produits dans un DataGrid. Dans la grille, nous voulons pouvoir afficher ou masquer la colonne Price, en fonction de la valeur d'une propriété ShowPrice exposée par ViewModel. L'approche évidente consiste à lier la visibilité de la colonne à la propriété ShowPrice:

<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                Visibility="{Binding ShowPrice,
                Converter={StaticResource visibilityConverter}}"/>

Malheureusement, changer la valeur de ShowPrice n'a aucun effet et la colonne est toujours visible… pourquoi? Si nous regardons la fenêtre de sortie dans Visual Studio, nous remarquons la ligne suivante:

Erreur System.Windows.Data: 2: impossible de trouver le FrameworkElement ou le FrameworkContentElement en vigueur pour l'élément cible. BindingExpression: Path = ShowPrice; DataItem = null; l’élément cible est ‘DataGridTextColumn’ (HashCode = 32685253); propriété cible est ‘Visibility’ (type ‘Visibility’)

Le message est plutôt cryptique, mais la signification est en réalité assez simple: WPF ne sait pas quel FrameworkElement utiliser pour obtenir le DataContext, car la colonne n’appartient pas à l’arborescence visuelle ou logique du DataGrid.

Nous pouvons essayer d'ajuster la liaison pour obtenir le résultat souhaité, par exemple en définissant RelativeSource sur le DataGrid lui-même:

<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                Visibility="{Binding DataContext.ShowPrice,
                Converter={StaticResource visibilityConverter},
                RelativeSource={RelativeSource FindAncestor, AncestorType=DataGrid}}"/>

Ou nous pouvons ajouter un CheckBox lié à ShowPrice et essayer de lier la visibilité de la colonne à la propriété IsChecked en spécifiant le nom de l'élément:

<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                Visibility="{Binding IsChecked,
                Converter={StaticResource visibilityConverter},
                ElementName=chkShowPrice}"/>

Mais aucune de ces solutions ne semble fonctionner, nous obtenons toujours le même résultat…

À ce stade, il semble que la seule approche viable consisterait à modifier la visibilité de la colonne dans code-behind, ce que nous préférons généralement éviter en utilisant le modèle MVVM… Mais je ne vais pas abandonner si tôt, du moins pas alors qu'il y a d'autres options à considérer ????

La solution à notre problème est en fait assez simple et tire parti de la classe Freezable. Le but principal de cette classe est de définir des objets modifiables et en lecture seule, mais la fonctionnalité intéressante dans notre cas est que les objets Freezable peuvent hériter du DataContext même s’ils ne sont pas dans l’arborescence visuelle ou logique. Je ne connais pas le mécanisme exact qui permet ce comportement, mais nous allons en tirer parti pour que notre liaison fonctionne.

L'idée est de créer une classe (je l'ai appelée BindingProxy pour des raisons qui devraient devenir évidentes très bientôt) qui hérite de Freezable et déclare une propriété de dépendance de données:

public class BindingProxy : Freezable
{
    #region Overrides of Freezable

    protected override Freezable CreateInstanceCore()
    {
        return new BindingProxy();
    }

    #endregion

    public object Data
    {
        get { return (object)GetValue(DataProperty); }
        set { SetValue(DataProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Data.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty DataProperty =
        DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}

Nous pouvons ensuite déclarer une instance de cette classe dans les ressources du DataGrid et lier la propriété Data au DataContext actuel:

<DataGrid.Resources>
    <local:BindingProxy x:Key="proxy" Data="{Binding}" />
</DataGrid.Resources>

La dernière étape consiste à spécifier cet objet BindingProxy (facilement accessible avec StaticResource) en tant que source de la liaison:

<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                Visibility="{Binding Data.ShowPrice,
                Converter={StaticResource visibilityConverter},
                Source={StaticResource proxy}}"/>

Notez que le chemin de liaison a été préfixé par "Données", car il est maintenant relatif à l'objet BindingProxy.

La liaison fonctionne désormais correctement et la colonne est correctement affichée ou masquée en fonction de la propriété ShowPrice.

4
Contango