Uzyskaj dostęp do danych nadrzędnych DataContext z DataTemplate

112

Mam, ListBoxktóry wiąże się z kolekcją podrzędną w ViewModel. Elementy listy są stylizowane na tabliczce danych na podstawie właściwości w nadrzędnym modelu ViewModel:

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

Otrzymuję następujący błąd wyjściowy:

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

Więc jeśli zmienię wyrażenie powiązania, aby "Path=DataContext.CurrentItem.CurveSpeedMustBeSpecified"działało, ale tylko tak długo, jak kontekst danych nadrzędnej kontrolki użytkownika to BindingListCollectionView. Jest to niedopuszczalne, ponieważ reszta formantu użytkownika wiąże się automatycznie z właściwościami elementu CurrentItemon BindingList.

Jak mogę określić wyrażenie wiążące wewnątrz stylu, aby działało niezależnie od kontekstu danych nadrzędnych będącego widokiem kolekcji lub pojedynczym elementem?

Marius
źródło

Odpowiedzi:

161

Miałem problemy z odpowiednim źródłem w Silverlight. Po przeszukaniu i przeczytaniu nie znalazłem odpowiedniego rozwiązania bez użycia dodatkowej biblioteki Binding. Ale tutaj jest inne podejście do uzyskiwania dostępu do nadrzędnego DataContext poprzez bezpośrednie odwoływanie się do elementu, którego znasz kontekst danych. Używa Binding ElementNamei działa całkiem dobrze, o ile szanujesz własne nazewnictwo i nie masz dużego ponownego wykorzystania templates/ stylesmiędzy komponentami:

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

Działa to również, jeśli umieścisz przycisk w 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>

Początkowo myślałem, że x:Nameselementy nadrzędne nie są dostępne z poziomu elementu szablonu, ale ponieważ nie znalazłem lepszego rozwiązania, po prostu spróbowałem i działa dobrze.

Juve
źródło
1
Mam ten dokładny kod w moim projekcie, ale przecieka ViewModels (nie wywołano finalizatora, powiązanie polecenia wydaje się zachowywać DataContext). Czy możesz zweryfikować, że ten problem dotyczy również Ciebie?
Joris Weimar
@Juve to działa, ale czy można to zrobić, aby uruchamiał się dla wszystkich elementów kontrolnych, które implementują ten sam szablon? Nazwa jest unikalna, więc potrzebowalibyśmy osobnego szablonu dla każdego, chyba że czegoś mi brakuje.
Chris
1
@Juve zignoruj ​​moje ostatnie, uruchomiłem go, używając źródła krewnego z findancestor i wyszukując według typu przodka (tak samo, z wyjątkiem wyszukiwania według nazwy). W moim przypadku powtarzam używanie ItemsControls, z których każdy implementuje szablon, więc mój wygląda tak: Command = "{Binding RelativeSource = {RelativeSource FindAncestor, AncestorType = {x: Type ItemsControl}}, Path = DataContext.OpenDocumentBtnCommand}"
Chris
48

Możesz użyć, RelativeSourceaby znaleźć element nadrzędny, na przykład:

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

Zobacz to pytanie SO, aby uzyskać więcej informacji na temat RelativeSource.

akjoshi
źródło
10
Musiałem określić Mode=FindAncestor, aby działał, ale to działa i jest znacznie lepsze w scenariuszu MVVM, ponieważ pozwala uniknąć kontroli nazewnictwa. Binding="{Binding Path=DataContext.CurveSpeedMustBeSpecified, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:YourParentElementType}}}"
Aphex
1
działa jak urok <3 i nie musiał określać trybu, .net 4.6.1
user2475096
30

RelativeSource a ElementName

Te dwa podejścia mogą przynieść ten sam wynik,

RelativeSrouce

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

Ta metoda wyszukuje formant Window typu (w tym przykładzie) w drzewie wizualnym, a gdy go znajdzie, w zasadzie można uzyskać dostęp do niego DataContextprzy użyciu Path=DataContext..... Zalety tej metody polegają na tym, że nie musisz być przywiązany do nazwy i jest to rodzaj dynamiki, jednak zmiany wprowadzone w drzewie wizualnym mogą wpłynąć na tę metodę i prawdopodobnie ją uszkodzić.

ElementName

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

Ta metoda odnosi się do stałej statycznej, Namewięc tak długo, jak twój zasięg może to zobaczyć, wszystko w porządku. Powinieneś trzymać się konwencji nazewnictwa, aby oczywiście nie łamać tej metody.Podejście jest dość proste i wystarczy określić a Name="..."dla twojego Window / UserControl.

Chociaż wszystkie trzy typy ( RelativeSource, Source, ElementName) mogą robić to samo, ale zgodnie z następującym artykułem MSDN, każdy z nich lepiej jest używany w swojej dziedzinie specjalizacji.

Instrukcje: określanie źródła powiązania

Znajdź krótki opis każdego z nich oraz link do bardziej szczegółowych informacji w tabeli na dole strony.

Mehrad
źródło
18

Szukałem, jak zrobić coś podobnego w WPF i otrzymałem takie rozwiązanie:

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

Mam nadzieję, że to zadziała dla kogoś innego. Mam kontekst danych, który jest automatycznie ustawiany na ItemsControls, a ten kontekst danych ma dwie właściwości: MyItems-co jest kolekcją- i jedno polecenie „CustomCommand”. Ponieważ ItemTemplateużywa a DataTemplate,DataContext górne poziomy nie są bezpośrednio dostępne. Następnie obejściem, aby uzyskać kontroler domeny rodzica, jest użycie ścieżki względnej i filtrowanie według ItemsControltypu.

hmadrigal
źródło
0

problem polega na tym, że DataTemplate nie jest częścią elementu, do którego został zastosowany.

oznacza to, że jeśli łączysz się z szablonem, wiążesz się z czymś, co nie ma kontekstu.

jednak jeśli umieścisz element wewnątrz szablonu, wtedy gdy ten element zostanie zastosowany do rodzica, uzyska on kontekst i powiązanie będzie działać

więc to nie zadziała

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

ale to działa doskonale

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

ponieważ po zastosowaniu arkusza danych groupbox jest umieszczany w nadrzędnym i będzie miał dostęp do jego kontekstu

więc wszystko, co musisz zrobić, to usunąć styl z szablonu i przenieść go do elementu w szablonie

zwróć uwagę, że kontekstem dla itemscontrol jest element, a nie element sterujący tj. ComboBoxItem dla ComboBox, a nie sam ComboBox w takim przypadku powinieneś zamiast tego użyć kontrolek ItemContainerStyle

MikeT
źródło
0

Tak, możesz to rozwiązać za pomocą ElementName=Something sugestii Juve.

ALE!

Jeśli element podrzędny (w którym używasz tego rodzaju powiązania) jest kontrolką użytkownika, która używa tej samej nazwy elementu, co określona w kontrolce nadrzędnej, wówczas powiązanie trafia do niewłaściwego obiektu !!

Wiem, że ten post nie jest rozwiązaniem, ale pomyślałem, że każdy, kto używa elementu ElementName w powiązaniu, powinien to wiedzieć, ponieważ jest to możliwy błąd w czasie wykonywania.

<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
źródło