Accéder au DataContext parent à partir de DataTemplate

112

J'ai un ListBoxqui se lie à une collection enfant sur un ViewModel. Les éléments de la zone de liste sont stylisés dans un modèle de données basé sur une propriété du ViewModel parent:

<Style x:Key="curveSpeedNonConstantParameterCell">
   <Style.Triggers>
      <DataTrigger Binding="{Binding Path=DataContext.CurveSpeedMustBeSpecified, 
          ElementName=someParentElementWithReferenceToRootDataContext}" 
          Value="True">
          <Setter Property="Control.Visibility" Value="Hidden"></Setter>
      </DataTrigger>
   </Style.Triggers>
</Style>

J'obtiens l'erreur de sortie suivante:

System.Windows.Data Error: 39 : BindingExpression path error: 
 'CurveSpeedMustBeSpecified' property not found on 
   'object' ''BindingListCollectionView' (HashCode=20467555)'. 
 BindingExpression:Path=DataContext.CurveSpeedMustBeSpecified; 
 DataItem='Grid' (Name='nonConstantCurveParametersGrid');
 target element is 'TextBox' (Name=''); 
 target property is 'NoTarget' (type 'Object')

Donc, si je change l'expression de liaison, "Path=DataContext.CurrentItem.CurveSpeedMustBeSpecified"cela fonctionne, mais seulement tant que le contexte de données du contrôle utilisateur parent est un BindingListCollectionView. Cela n'est pas acceptable car le reste du contrôle utilisateur se lie aux propriétés du CurrentItemsur le BindingListautomatiquement.

Comment puis-je spécifier l'expression de liaison à l'intérieur du style afin qu'elle fonctionne indépendamment du fait que le contexte de données parent soit une vue de collection ou un élément unique?

Marius
la source

Réponses:

161

J'ai eu des problèmes avec la source relative dans Silverlight. Après avoir cherché et lu, je n'ai pas trouvé de solution appropriée sans utiliser une bibliothèque de liaison supplémentaire. Mais voici une autre approche pour accéder au DataContext parent en référençant directement un élément dont vous connaissez le contexte de données. Il utilise Binding ElementNameet fonctionne très bien, tant que vous respectez votre propre dénomination et que vous n'avez pas de réutilisation intensive de templates/ stylesentre les composants:

<ItemsControl x:Name="level1Lister" ItemsSource={Binding MyLevel1List}>
  <ItemsControl.ItemTemplate>
    <DataTemplate>
      <Button Content={Binding MyLevel2Property}
              Command={Binding ElementName=level1Lister,
                       Path=DataContext.MyLevel1Command}
              CommandParameter={Binding MyLevel2Property}>
      </Button>
    <DataTemplate>
  <ItemsControl.ItemTemplate>
</ItemsControl>

Cela fonctionne également si vous mettez le bouton dans Style/ Template:

<Border.Resources>
  <Style x:Key="buttonStyle" TargetType="Button">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="Button">
          <Button Command={Binding ElementName=level1Lister,
                                   Path=DataContext.MyLevel1Command}
                  CommandParameter={Binding MyLevel2Property}>
               <ContentPresenter/>
          </Button>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</Border.Resources>

<ItemsControl x:Name="level1Lister" ItemsSource={Binding MyLevel1List}>
  <ItemsControl.ItemTemplate>
    <DataTemplate>
      <Button Content="{Binding MyLevel2Property}" 
              Style="{StaticResource buttonStyle}"/>
    <DataTemplate>
  <ItemsControl.ItemTemplate>
</ItemsControl>

Au début, je pensais que les x:Nameséléments parents ne sont pas accessibles à partir d'un élément basé sur un modèle, mais comme je n'ai trouvé aucune meilleure solution, j'ai juste essayé et cela fonctionne bien.

Juve
la source
1
J'ai ce code exact dans mon projet mais il fuit des ViewModels (Finalizer non appelé, la liaison de commande semble conserver DataContext). Pouvez-vous vérifier que ce problème existe également pour vous?
Joris Weimar
@Juve cela fonctionne, mais est-il possible de le faire pour qu'il se déclenche pour tous les contrôles d'éléments qui implémentent le même modèle? Le nom est unique, nous aurions donc besoin d'un modèle distinct pour chacun, sauf si quelque chose me manque.
Chris
1
@Juve ne tient pas compte de mon dernier, je l'ai fait fonctionner en utilisant la source relative avec findancestor et en recherchant par type d'ancêtre, (donc tout de même sauf pas de recherche par nom). Dans mon cas, je répète l'utilisation de ItemsControls, chacun implémentant un modèle donc le mien ressemble à ceci: Command = "{Binding RelativeSource = {RelativeSource FindAncestor, AncestorType = {x: Type ItemsControl}}, Path = DataContext.OpenDocumentBtnCommand}"
Chris
48

Vous pouvez utiliser RelativeSourcepour trouver l'élément parent, comme ceci -

Binding="{Binding Path=DataContext.CurveSpeedMustBeSpecified, 
RelativeSource={RelativeSource AncestorType={x:Type local:YourParentElementType}}}"

Voir cette question SO pour plus de détails sur RelativeSource.

akjoshi
la source
10
J'ai dû spécifier Mode=FindAncestorpour que cela fonctionne, mais cela fonctionne et est bien meilleur dans un scénario MVVM car cela évite de nommer les contrôles. Binding="{Binding Path=DataContext.CurveSpeedMustBeSpecified, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:YourParentElementType}}}"
Aphex
1
fonctionne comme un charme <3 et n'a pas eu à spécifier le mode, .net 4.6.1
user2475096
30

RelativeSource et ElementName

Ces deux approches peuvent aboutir au même résultat,

RelativeSrouce

Binding="{Binding Path=DataContext.MyBindingProperty, 
          RelativeSource={RelativeSource AncestorType={x:Type Window}}}"

Cette méthode recherche un contrôle de type Window (dans cet exemple) dans l'arborescence visuelle et quand il le trouve, vous pouvez essentiellement y accéder en DataContextutilisant le Path=DataContext..... L'avantage de cette méthode est que vous n'avez pas besoin d'être lié à un nom et que c'est un peu dynamique, cependant, les modifications apportées à votre arborescence visuelle peuvent affecter cette méthode et éventuellement la casser.

Nom de l'élément

Binding="{Binding Path=DataContext.MyBindingProperty, ElementName=MyMainWindow}

Cette méthode fait référence à une statique solide Namedonc tant que votre portée peut la voir, tout va bien.Vous devriez vous en tenir à votre convention de nommage pour ne pas casser cette méthode bien sûr.L'approche est très simple et tout ce dont vous avez besoin est de spécifier a Name="..."pour votre Window / UserControl.

Bien que les trois types ( RelativeSource, Source, ElementName) soient capables de faire la même chose, mais selon l'article MSDN suivant, chacun est mieux utilisé dans son propre domaine de spécialité.

Comment: spécifier la source de liaison

Trouvez la brève description de chacun ainsi qu'un lien vers une description plus détaillée dans le tableau en bas de la page.

Mehrad
la source
18

Je cherchais comment faire quelque chose de similaire dans WPF et j'ai obtenu cette solution:

<ItemsControl ItemsSource="{Binding MyItems,Mode=OneWay}">
<ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
        <StackPanel Orientation="Vertical" />
    </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
    <DataTemplate>
        <RadioButton 
            Content="{Binding}" 
            Command="{Binding Path=DataContext.CustomCommand, 
                        RelativeSource={RelativeSource Mode=FindAncestor,      
                        AncestorType={x:Type ItemsControl}} }"
            CommandParameter="{Binding}" />
    </DataTemplate>
</ItemsControl.ItemTemplate>

J'espère que cela fonctionne pour quelqu'un d'autre. J'ai un contexte de données qui est défini automatiquement sur les ItemsControls, et ce contexte de données a deux propriétés: MyItems-qui est une collection-, et une commande «CustomCommand». En raison de l' ItemTemplateutilisation de a DataTemplate, les DataContextniveaux supérieurs ne sont pas directement accessibles. Ensuite, la solution de contournement pour obtenir le contrôleur de domaine du parent consiste à utiliser un chemin d'accès relatif et à filtrer par ItemsControltype.

hmadrigal
la source
0

le problème est qu'un DataTemplate ne fait pas partie d'un élément qui lui est appliqué.

cela signifie que si vous vous liez au modèle, vous liez à quelque chose qui n'a pas de contexte.

Cependant, si vous mettez un élément dans le modèle, lorsque cet élément est appliqué au parent, il obtient un contexte et la liaison fonctionne alors

donc cela ne fonctionnera pas

<DataTemplate >
    <DataTemplate.Resources>
        <CollectionViewSource x:Key="projects" Source="{Binding Projects}" >

mais cela fonctionne parfaitement

<DataTemplate >
    <GroupBox Header="Projects">
        <GroupBox.Resources>
            <CollectionViewSource x:Key="projects" Source="{Binding Projects}" >

car une fois le modèle de données appliqué, la boîte de groupe est placée dans le parent et aura accès à son contexte

il vous suffit donc de supprimer le style du modèle et de le déplacer dans un élément du modèle

notez que le contexte d'un itemscontrol est l'élément pas le contrôle ie ComboBoxItem pour ComboBox pas le ComboBox lui-même auquel cas vous devriez utiliser les contrôles ItemContainerStyle à la place

MikeT
la source
0

Oui, vous pouvez le résoudre en utilisant le ElementName=Somethingcomme suggéré par la Juve.

MAIS!

Si un élément enfant (sur lequel vous utilisez ce type de liaison) est un contrôle utilisateur qui utilise le même nom d'élément que celui que vous spécifiez dans le contrôle parent, alors la liaison va vers le mauvais objet !!

Je sais que cet article n'est pas une solution, mais je pensais que tous ceux qui utilisent le nom de l'élément dans la liaison devraient le savoir, car il s'agit d'un bogue d'exécution possible.

<UserControl x:Class="MyNiceControl"
             x:Name="TheSameName">
   the content ...
</UserControl>

<UserControl x:Class="AnotherUserControl">
        <ListView x:Name="TheSameName">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <MyNiceControl Width="{Binding DataContext.Width, ElementName=TheSameName}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
</UserControl>
Lumo
la source