Comment utiliser les liaisons WPF avec RelativeSource?

Réponses:

783

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

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

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

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

Si vous souhaitez obtenir une propriété sur le parent du modèle (vous pouvez donc effectuer des liaisons bidirectionnelles dans un ControlTemplate)

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

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

{TemplateBinding Path=PathToProperty}
Abe Heidebrecht
la source
15
Pour celui-ci "{Binding Path = PathToProperty, RelativeSource = {RelativeSource AncestorType = {x: Type typeOfAncestor}}}", il semble qu'il ait besoin d'avoir "Mode = FindAncestor", avant "AncestorType"
EdwardM
1
Pour quelle technologie? Dans WPF, cela est déduit lorsque vous spécifiez un AncestorType.
Abe Heidebrecht
2
Je suis d'accord avec @EdwardM. Lorsque j'omet FindAncestor, avant AncestorType, j'obtiens l'erreur suivante: "RelativeSource n'est pas en mode FindAncestor". (Dans VS2013, version communautaire)
kmote
1
@kmote, cela fonctionne pour moi depuis .net 3.0, et j'ai une fois de plus vérifié que cela fonctionne de cette façon dans kaxaml ... Encore une fois, quelle technologie utilisez-vous? Le processeur XAML est différent pour WPF / Silverlight / UWP, vous pouvez donc avoir des résultats différents sur différentes technologies. Vous avez également mentionné VS Community, alors c'est peut-être un avertissement IDE, mais fonctionne à l'exécution?
Abe Heidebrecht
6
Je voulais juste souligner ici que si vous voulez lier à une propriété DataContext du RelativeSource alors vous devez spécifier explicitement: {Binding Path=DataContext.SomeProperty, RelativeSource=.... Cela était quelque peu inattendu pour moi en tant que débutant lorsque j'essayais de me lier au DataContext d'un parent dans un DataTemplate.
DrEsperanto
133
Binding RelativeSource={
    RelativeSource Mode=FindAncestor, AncestorType={x:Type ItemType}
}
...

L'attribut par défaut de RelativeSourceest la Modepropriété. Un ensemble complet de valeurs valides est donné ici (à partir de MSDN ):

  • PreviousData Vous permet de lier l'élément de données précédent (pas le contrôle qui contient 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'un 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é sur le même élément.

  • FindAncestor Fait référence à l'ancêtre dans la chaîne parent 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.

Drew Noakes
la source
128

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

entrez la description de l'image ici

Jeffrey Knight
la source
19
ai-je oublié quelque chose? Comment pouvez-vous considérer cela comme un graphique simple et clair? 1: les cases à gauche ne sont pas vraiment liées à celles de droite (pourquoi y a-t-il un fichier .cs à l'intérieur du ViewModel?) 2: à quoi pointent ces flèches DataContext? 3: pourquoi la propriété Message n'est-elle pas dans le ViewModel1? et le plus important 5: Pourquoi avez-vous besoin d'une liaison RelativeSource pour accéder au DataContext de la fenêtre si le TextBlock a déjà ce même DataContext? Il me manque clairement quelque chose ici, donc soit je suis assez stupide, soit ce graphique n'est pas aussi simple et clair que tout le monde le pense! Veuillez m'éclairer
Markus Hütter
2
@ MarkusHütter Le diagramme montre un groupe de vues imbriquées et les ViewModels correspondants. Le DataContext de View1 est ViewModel1, mais il veut se lier à une propriété de BaseViewModel. Parce que BaseViewModel est le DataContext de BaseView (qui est une fenêtre), il peut le faire en trouvant le premier conteneur parent qui est une fenêtre et en prenant son DataContext.
mcargille
6
@MatthewCargille Je sais très bien ce que c'est censé signifier, ce n'était pas mon propos. Mais mettez-vous dans la position de quelqu'un qui ne connaît pas bien XAML et MVVM et vous verrez que ce n'est pas simple et clair .
Markus Hütter du
1
Je suis d'accord avec @ MarkusHütter, au fait, la reliure à gauche pourrait être aussi simple que cela: {Binding Message}(un peu plus simple ...)
florien
@florien Je ne pense pas, du moins pour mon cas d'utilisation. J'ai un DataTemplate qui doit faire référence au DataContext de MainWindow (ma classe viewmodel) pour obtenir une liste d'options pour un menu déroulant (chargé à partir d'une base de données). Le DataTemplate est lié à un objet modèle qui est également chargé à partir de la base de données, mais il n'a accès qu'à l'option sélectionnée. J'ai dû définir explicitement Path=DataContext.Messagepour que la liaison fonctionne. Cela a du sens, étant donné que vous pouvez faire des liaisons relatives à largeur / hauteur / etc. d'un contrôle.
DrEsperanto
47

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

RelativeSource est une extension de balisage qui est 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 à un autre de ses parents relatifs, lors de la liaison d'une valeur de propriété de dépendance à un morceau 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 en modes source relatifs. Je vais exposer tous ces cas un par un.

  1. Mode auto:

Imaginez ce cas, un rectangle dont on veut que sa hauteur soit toujours égale à sa largeur, disons un carré. 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 ci-dessus, nous sommes obligés d'indiquer le nom de l'objet de liaison, à savoir le rectangle. Nous pouvons atteindre le même objectif 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 reliure et la largeur sera toujours égale à la hauteur chaque fois que la hauteur est modifiée.

Si vous souhaitez que la largeur soit la moitié de la hauteur, vous pouvez le faire en ajoutant un convertisseur à l'extension de balisage de liaison. Imaginons maintenant un autre cas:

 <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'un de ses parents directs, car cet élément contient une propriété appelée Parent. Cela nous amène à un autre mode source relatif qui est 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, Of Corse. La principale différence avec le cas ci-dessus est le fait que c'est à vous de déterminer le type d'ancêtre et le rang d'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 est constituée de deux éléments TextBlock qui sont intégrés dans une série de bordures et des éléments canvas qui représentent leurs parents hiérarchiques. Le deuxième TextBlock affichera le nom du parent donné au niveau de la source relative.

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

Le texte affiché changera selon le type et le niveau d'ancêtre. Que se passe-t-il alors si le niveau de l'ancêtre ne convient pas 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. TemplatedParent

Ce mode permet de lier une propriété ControlTemplate donnée à une propriété du contrôle auquel 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 qui est le TemplateBinding qui est une sorte de raccourci de la première, mais le TemplateBinding est évalué au moment de la compilation au contraste du TemplatedParent qui est évalué juste après la première exécution. Comme vous pouvez le remarquer sur la figure ci-dessous, l'arrière-plan et le contenu sont appliqués à l'intérieur du bouton au modèle de contrôle.

Cornel Marian
la source
De très bons exemples pour moi, ont utilisé la recherche d'ancêtre pour lier à une commande dans le contexte de données d'un parent ListView. Le parent a 2 ListViewniveaux de plus en dessous. Cela m'a permis d' éviter le passage de données dans chaque vm subséquente de chaque ListViews »DataTemplate
Caleb W.
34

Dans WPF, la RelativeSourceliaison en expose trois propertiesà définir:

1. Mode: c'est un enumqui pourrait avoir quatre valeurs:

une. PreviousData ( value=0): il affecte la valeur précédente de lapropertyà celle liée

b. TemplatedParent ( value=1): Ceci est utilisé lors de la définitiontemplatesde tout contrôle et que vous souhaitez lier à une valeur / propriété decontrol.

Par exemple, définissez ControlTemplate:

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

c. Soi ( value=2): quand nous voulons nous lier à partir d'unselfou d'unpropertysoi.

Par exemple: envoyer l'état vérifié de checkboxas CommandParameterlors de l'activation CommanddeCheckBox

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

ré. FindAncestor ( value=3): lorsque vous souhaitez effectuer une liaison à partir d'un parentcontrol dansVisual Tree.

Par exemple: lier un checkboxdans recordssi un grid, si header checkboxest coché

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

2. AncestorType: lorsque le mode est FindAncestoralors définir quel type d'ancêtre

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

3. AncestorLevel: quand le mode estFindAncestoralors à quel niveau d'ancêtre (s'il y a deux mêmes types de parentvisual tree)

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

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

Voici un lien de référence .

Kylo Ren
la source
2
Génial .. cela a fonctionné pour moi: <DataGridCheckBoxColumn Header = "Paid" Width = "35" Binding = "{Binding RelativeSource = {RelativeSource Mode = FindAncestor, AncestorType = {x: Type Window}}, Path = DataContext.SelectedBuyer.IsPaid , Mode = OneWay} "/> où j'essayais de me lier à la propriété selectedbuyer.IsPaid de la fenêtre parent
Michael K
21

N'oubliez pas TemplatedParent:

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

ou

{Binding RelativeSource={RelativeSource TemplatedParent}}
Bob King
la source
16

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 simplification de la liaison de méthode. 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 «AVANT» que j'utilise pour la liaison de méthode que le code a déjà été optimisé en utilisant la RelayCommanddernière que j'ai vérifiée n'est pas une partie native de WPF. Sans cela, l'exemple «AVANT» aurait été encore plus long.

Luis Perez
la source
2
Ce genre d'exercices de tenue de main démontre la faiblesse de XAML; façon trop compliquée.
dudeNumber4
16

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 largement copié cela depuis Binding Relative Source dans le code Behind .

De plus, la page MSDN est assez bonne en ce qui concerne les exemples: Classe RelativeSource

Nathan Cooper
la source
5
Mon vague souvenir de WPF est que faire des liaisons dans du code n'est généralement pas la meilleure chose.
Nathan Cooper
12

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

Juve
la source
10

Je n'ai pas lu toutes les réponses, mais je veux juste ajouter ces informations en cas de liaison de commande source relative d'un bouton.

Lorsque vous utilisez une source relative avec Mode=FindAncestor, la liaison doit être comme:

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

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

Kevin VDF
la source
9

Ceci est un exemple de l'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>
Edd
la source
6

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 se lier aux données lorsque le DataContext n'est pas hérité . Et cela fonctionne absolument avec brio!

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

Veuillez ne pas commenter ici, veuillez commenter directement sur son article de blog .

Annexe A: Miroir d'un 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 où vous l'assignez; vous n'avez donc pas besoin de le redéfinir sur chaque élément que vous souhaitez lier. Cependant, dans certains cas, le DataContext n'est pas accessible: il 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 avec 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, la modification de la valeur de ShowPrice n'a aucun effet et la colonne est toujours visible… pourquoi? Si nous regardons la fenêtre Sortie dans Visual Studio, nous remarquons la ligne suivante:

Erreur System.Windows.Data: 2: impossible de trouver FrameworkElement ou FrameworkContentElement pour l'élément cible. BindingExpression: Path = ShowPrice; DataItem = null; l'élément cible est «DataGridTextColumn» (HashCode = 32685253); la propriété cible est «Visibilité» (tapez «Visibilité»)

Le message est plutôt cryptique, mais la signification est en fait 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 de modifier 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 de contournement ne semble fonctionner, nous obtenons toujours le même résultat…

À ce stade, il semble que la seule approche viable serait de modifier la visibilité des colonnes en code-behind, ce que nous préférons généralement éviter lors de l'utilisation du modèle MVVM… Mais je ne vais pas abandonner si tôt, du moins pas alors qu'il existe 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 qui ont un état modifiable et en lecture seule, mais la caractéristique intéressante dans notre cas est que les objets Freezable peuvent hériter du DataContext même lorsqu'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 profiter pour faire fonctionner notre reliure…

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 des 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) comme source pour 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é avec «Data», car le chemin 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.

Contango
la source