Akses DataContext induk dari DataTemplate

112

Saya memiliki ListBoxyang mengikat ke koleksi anak di ViewModel. Item listbox ditata dalam template data berdasarkan properti pada ViewModel induk:

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

Saya mendapatkan kesalahan keluaran berikut:

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

Jadi jika saya mengubah ekspresi binding agar "Path=DataContext.CurrentItem.CurveSpeedMustBeSpecified"berfungsi, tetapi hanya selama konteks data dari kontrol pengguna induk adalah a BindingListCollectionView. Ini tidak dapat diterima karena kontrol pengguna lainnya mengikat properti CurrentItemdi BindingListsecara otomatis.

Bagaimana cara menentukan ekspresi binding di dalam gaya agar berfungsi terlepas dari konteks data induk menjadi tampilan koleksi atau item tunggal?

Marius
sumber

Jawaban:

161

Saya memiliki masalah dengan sumber relatif di Silverlight. Setelah mencari dan membaca saya tidak menemukan solusi yang sesuai tanpa menggunakan beberapa perpustakaan Binding tambahan. Namun, berikut adalah pendekatan lain untuk mendapatkan akses ke DataContext induk dengan merujuk langsung elemen yang Anda ketahui konteks datanya. Ini menggunakan Binding ElementNamedan bekerja dengan cukup baik, selama Anda menghormati penamaan Anda sendiri dan tidak banyak menggunakan kembali templates/ styleslintas komponen:

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

Ini juga berfungsi jika Anda meletakkan tombol ke 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>

Awalnya saya berpikir bahwa x:Nameselemen induk tidak dapat diakses dari dalam item templated, tetapi karena saya tidak menemukan solusi yang lebih baik, saya hanya mencoba, dan berfungsi dengan baik.

Juve
sumber
1
Saya memiliki kode persis ini dalam proyek saya tetapi ViewModels bocor (Finalizer tidak disebut, Pengikatan perintah tampaknya mempertahankan DataContext). Dapatkah Anda memverifikasi bahwa masalah ini ada untuk Anda juga?
Joris Weimar
@Juve ini berfungsi, tetapi apakah mungkin untuk melakukan ini sehingga akan diaktifkan untuk semua itemscontrols yang menerapkan template yang sama? Nama itu unik sehingga kita memerlukan templat terpisah untuk masing-masing, kecuali saya melewatkan sesuatu.
Chris
1
@Juve mengabaikan saya yang terakhir, saya membuatnya bekerja dengan menggunakan relativeource dengan findancestor dan mencari berdasarkan leluhur, (jadi semuanya sama kecuali tidak mencari berdasarkan nama). Dalam kasus saya, saya mengulangi penggunaan ItemsControls masing-masing menerapkan template sehingga milik saya terlihat seperti ini: Command = "{Binding RelativeSource = {RelativeSource FindAncestor, AncestorType = {x: Type ItemsControl}}, Path = DataContext.OpenDocumentBtnCommand}"
Chris
48

Anda dapat menggunakan RelativeSourceuntuk menemukan elemen induk, seperti ini -

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

Lihat pertanyaan SO ini untuk lebih jelasnya RelativeSource.

akjoshi
sumber
10
Saya harus menentukannya Mode=FindAncestoragar berfungsi, tetapi ini berfungsi dan jauh lebih baik dalam skenario MVVM karena menghindari kontrol penamaan. Binding="{Binding Path=DataContext.CurveSpeedMustBeSpecified, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:YourParentElementType}}}"
Aphex
1
bekerja seperti pesona <3 dan tidak harus menentukan mode, .net 4.6.1
user2475096
30

RelativeSource vs. ElementName

Kedua pendekatan ini dapat mencapai hasil yang sama,

RelativeSrouce

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

Metode ini mencari kontrol dari jendela tipe (dalam contoh ini) di pohon visual dan ketika menemukannya Anda pada dasarnya dapat mengaksesnya DataContextmenggunakan Path=DataContext..... Kelebihan dari metode ini adalah Anda tidak perlu terikat dengan sebuah nama dan ini agak dinamis, namun, perubahan yang dibuat pada pohon visual Anda dapat memengaruhi metode ini dan mungkin merusaknya.

ElementName

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

Metode ini mengacu pada statis padat Namesehingga selama lingkup Anda dapat melihatnya, Anda baik-baik saja. Anda harus berpegang pada konvensi penamaan Anda untuk tidak merusak metode ini tentu saja Pendekatannya sederhana dan yang Anda butuhkan hanyalah menentukan a Name="..."untuk Window / UserControl Anda.

Meskipun ketiga jenis ( RelativeSource, Source, ElementName) mampu melakukan hal yang sama, namun menurut artikel MSDN berikut, masing-masing sebaiknya digunakan di bidang keahliannya masing-masing.

Cara: Tentukan Sumber Binding

Temukan deskripsi singkat masing-masing ditambah tautan ke detail lebih lanjut di tabel di bagian bawah halaman.

Mehrad
sumber
18

Saya sedang mencari cara melakukan sesuatu yang serupa di WPF dan saya mendapatkan solusi ini:

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

Saya harap ini berhasil untuk orang lain. Saya memiliki konteks data yang diatur secara otomatis ke ItemsControls, dan konteks data ini memiliki dua properti: MyItems-yang merupakan kumpulan-, dan satu perintah 'CustomCommand'. Karena ItemTemplatemenggunakan a DataTemplate, maka DataContextlevel atas tidak dapat langsung diakses. Kemudian solusi untuk mendapatkan DC induk adalah menggunakan jalur relatif dan menyaring berdasarkan ItemsControljenis.

hmadrigal.dll
sumber
0

masalahnya adalah bahwa DataTemplate bukan bagian dari elemen yang diterapkan padanya.

ini berarti jika Anda mengikat ke templat Anda mengikat sesuatu yang tidak memiliki konteks.

Namun jika Anda meletakkan elemen di dalam template maka ketika elemen itu diterapkan ke induknya, ia mendapatkan konteks dan binding kemudian berfungsi

jadi ini tidak akan berhasil

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

tapi ini bekerja dengan sempurna

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

karena setelah templat data diterapkan, kotak grup ditempatkan di induk dan akan memiliki akses ke Konteksnya

jadi yang harus Anda lakukan adalah menghapus gaya dari template dan memindahkannya ke elemen di template

perhatikan bahwa konteks untuk itemscontrol adalah item bukan kontrol yaitu ComboBoxItem untuk ComboBox bukan ComboBox itu sendiri dalam hal ini Anda harus menggunakan kontrol ItemContainerStyle sebagai gantinya

MikeT
sumber
0

Ya, Anda bisa mengatasinya dengan menggunakan ElementName=Somethingsaran Juve.

TAPI!

Jika elemen anak (di mana Anda menggunakan jenis pengikatan ini) adalah kontrol pengguna yang menggunakan nama elemen yang sama seperti yang Anda tentukan di kontrol induk, maka pengikatan menuju ke objek yang salah !!

Saya tahu posting ini bukanlah solusi tetapi saya pikir semua orang yang menggunakan ElementName dalam pengikatan harus tahu ini, karena ini kemungkinan bug 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>
Lumo
sumber