Comment utiliser RelativeSource
avec les liaisons WPF et quels sont les différents cas d'utilisation?
Comment utiliser RelativeSource
avec les liaisons WPF et quels sont les différents cas d'utilisation?
Réponses:
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}
AncestorType
.
FindAncestor
, avant AncestorType
, j'obtiens l'erreur suivante: "RelativeSource n'est pas en mode FindAncestor". (Dans VS2013, version communautaire)
{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.
Binding RelativeSource={
RelativeSource Mode=FindAncestor, AncestorType={x:Type ItemType}
}
...
L'attribut par défaut de RelativeSource
est la Mode
proprié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.
Voici une explication plus visuelle dans le contexte d'une architecture MVVM:
{Binding Message}
(un peu plus simple ...)
Path=DataContext.Message
pour que la liaison fonctionne. Cela a du sens, étant donné que vous pouvez faire des liaisons relatives à largeur / hauteur / etc. d'un contrôle.
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.
- 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.
- 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.
- 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.
ListView
. Le parent a 2 ListView
niveaux de plus en dessous. Cela m'a permis d' éviter le passage de données dans chaque vm subséquente de chaque ListView
s »DataTemplate
Dans WPF, la RelativeSource
liaison en expose trois properties
à définir:
1. Mode: c'est un enum
qui pourrait avoir quatre valeurs:
une. PreviousData (
value=0
): il affecte la valeur précédente de laproperty
à celle liéeb. TemplatedParent (
value=1
): Ceci est utilisé lors de la définitiontemplates
de 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'unself
ou d'unproperty
soi.Par exemple: envoyer l'état vérifié de
checkbox
asCommandParameter
lors de l'activationCommand
deCheckBox
<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
checkbox
dansrecords
si ungrid
, siheader
checkbox
est coché
<CheckBox IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid}}, Path=DataContext.IsHeaderChecked, Mode=TwoWay}" />
2. AncestorType: lorsque 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 estFindAncestor
alors à 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
.
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
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 RelayCommand
derniè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.
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
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
.
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é.
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>
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 .
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.