Accedi a DataContext padre da DataTemplate


112

Ho un ListBoxche si lega a una raccolta figlio su un ViewModel. Gli elementi della casella di riepilogo hanno uno stile in un datatemplate basato su una proprietà sul ViewModel genitore:

<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>

Ottengo il seguente errore di output:

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')

Quindi, se cambio l'espressione di associazione in modo che "Path=DataContext.CurrentItem.CurveSpeedMustBeSpecified"funzioni, ma solo fino a quando il datacontext del controllo utente padre è un file BindingListCollectionView. Ciò non è accettabile perché il resto del controllo utente si lega automaticamente alle proprietà di CurrentItemsu BindingList.

Come posso specificare l'espressione di associazione all'interno dello stile in modo che funzioni indipendentemente dal fatto che il contesto dei dati padre sia una vista di raccolta o un singolo elemento?

Risposte:


161

Ho avuto problemi con la relativa fonte in Silverlight. Dopo aver cercato e letto non ho trovato una soluzione adatta senza utilizzare qualche libreria di Binding aggiuntiva. Tuttavia, ecco un altro approccio per ottenere l'accesso al DataContext padre facendo riferimento direttamente a un elemento di cui si conosce il contesto dei dati. Utilizza Binding ElementNamee funziona abbastanza bene, purché rispetti la tua stessa denominazione e non hai un pesante riutilizzo di templates/ stylestra i componenti:

<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>

Funziona anche se metti il ​​pulsante in 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>

All'inizio pensavo che gli x:Nameselementi genitore non fossero accessibili dall'interno di un elemento modellato, ma poiché non ho trovato una soluzione migliore, ho semplicemente provato e funziona bene.


1
Ho questo codice esatto nel mio progetto ma perde ViewModels (Finalizer non chiamato, il binding dei comandi sembra mantenere DataContext). Puoi verificare che questo problema esista anche per te?
Joris Weimar

@ Juve funziona, ma è possibile farlo in modo che venga attivato per tutti i controlli degli elementi che implementano lo stesso modello? Il nome è univoco, quindi avremmo bisogno di un modello separato per ciascuno, a meno che non mi manchi qualcosa.
Chris

1
@ Juve ignora il mio ultimo, l'ho fatto funzionare usando relativesource con findancestor e cercando per ancestortype, (quindi lo stesso tranne non cercare per nome). Nel mio caso ripeto l'uso di ItemsControls ciascuno implementando un modello, quindi il mio assomiglia a questo: Command = "{Binding RelativeSource = {RelativeSource FindAncestor, AncestorType = {x: Type ItemsControl}}, Path = DataContext.OpenDocumentBtnCommand}"
Chris

48

Puoi usare RelativeSourceper trovare l'elemento genitore, in questo modo:

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

Vedi questa domanda SO per maggiori dettagli su RelativeSource.


10
Ho dovuto specificare Mode=FindAncestorperché funzionasse, ma funziona ed è molto meglio in uno scenario MVVM perché evita di denominare i controlli. Binding="{Binding Path=DataContext.CurveSpeedMustBeSpecified, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:YourParentElementType}}}"
Aphex

1
funziona come un fascino <3 e non era necessario specificare la modalità, .net 4.6.1
user2475096

30

RelativeSource rispetto a ElementName

Questi due approcci possono ottenere lo stesso risultato,

RelativeSrouce

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

Questo metodo cerca un controllo di un tipo Window (in questo esempio) nella struttura ad albero visuale e quando lo trova puoi accedervi fondamentalmente DataContextusando il Path=DataContext..... Il vantaggio di questo metodo è che non è necessario essere legati a un nome ed è un po 'dinamico, tuttavia, le modifiche apportate al tuo albero visivo possono influenzare questo metodo e possibilmente interromperlo.

ElementName

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

Questo metodo fa riferimento a una statica solida, Namequindi finché il tuo ambito può vederlo, stai bene Dovresti attenersi alla tua convenzione di denominazione per non rompere questo metodo, naturalmente L'approccio è semplice e tutto ciò di cui hai bisogno è specificare a Name="..."per il tuo Window / UserControl.

Sebbene tutti e tre i tipi ( RelativeSource, Source, ElementName) siano in grado di fare la stessa cosa, ma secondo il seguente articolo di MSDN, ciascuno di essi è meglio utilizzato nella propria area di specializzazione.

Procedura: specificare l'origine del binding

Trova la breve descrizione di ciascuno più un collegamento a uno più dettagliato nella tabella in fondo alla pagina.


18

Stavo cercando come fare qualcosa di simile in WPF e ho ottenuto questa soluzione:

<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>

Spero che funzioni per qualcun altro. Ho un contesto dati impostato automaticamente su ItemsControls e questo contesto dati ha due proprietà: MyItems-che è una raccolta- e un comando "CustomCommand". A causa ItemTemplatedell'utilizzo di a DataTemplate, i DataContextlivelli superiori non sono direttamente accessibili. Quindi la soluzione alternativa per ottenere il controller di dominio del padre è utilizzare un percorso relativo e filtrare per ItemsControltipo.


0

il problema è che un DataTemplate non fa parte di un elemento a cui viene applicato.

questo significa che se ti leghi al modello stai vincolando a qualcosa che non ha contesto.

tuttavia, se metti un elemento all'interno del modello, quando quell'elemento viene applicato al genitore ottiene un contesto e l'associazione funziona

quindi questo non funzionerà

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

ma funziona perfettamente

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

perché dopo l'applicazione del datatemplate la groupbox viene posizionata nel genitore e avrà accesso al suo contesto

quindi tutto ciò che devi fare è rimuovere lo stile dal modello e spostarlo in un elemento nel modello

nota che il contesto per un itemcontrolèl'elemento non il controllo cioè ComboBoxItem per ComboBox non il ComboBox stesso nel qual caso dovresti usare invece i controlli ItemContainerStyle


0

Sì, puoi risolverlo usando il ElementName=Somethingcome suggerito dalla Juve.

MA!

Se un elemento figlio (su cui si utilizza questo tipo di associazione) è un controllo utente che utilizza lo stesso nome di elemento specificato nel controllo padre, l'associazione va all'oggetto sbagliato !!

So che questo post non è una soluzione, ma ho pensato che chiunque utilizzi ElementName nell'associazione dovrebbe saperlo, poiché è un possibile bug di runtime.

<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>
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.