Доступ к родительскому DataContext из DataTemplate

112

У меня есть ListBoxпривязка к дочерней коллекции на ViewModel. Элементы списка имеют стиль в табличке данных на основе свойства родительской 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>

Я получаю следующую ошибку вывода:

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

Поэтому, если я изменю выражение привязки, "Path=DataContext.CurrentItem.CurveSpeedMustBeSpecified"оно будет работать, но только до тех пор, пока контекст данных родительского пользовательского элемента управления будет BindingListCollectionView. Это неприемлемо , так как остальная часть пользовательского элемента управления связывается с свойствами CurrentItemна BindingListавтоматически.

Как я могу указать выражение привязки внутри стиля, чтобы оно работало независимо от того, является ли контекст родительских данных представлением коллекции или отдельным элементом?

Мариус
источник

Ответы:

161

У меня были проблемы с относительным источником в Silverlight. После поиска и чтения я не нашел подходящего решения без использования дополнительной библиотеки привязки. Но вот другой подход к получению доступа к родительскому DataContext путем прямой ссылки на элемент, контекст данных которого вам известен. Он использует Binding ElementNameи работает довольно хорошо, если вы уважаете свое собственное именование и не имеете интенсивного повторного использования templates/ stylesмежду компонентами:

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

Это также работает, если вы поместите кнопку в 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>

Сначала я подумал, что x:Namesродительские элементы недоступны из шаблонного элемента, но, поскольку я не нашел лучшего решения, я просто попробовал, и он отлично работает.

Ювентус
источник
1
У меня есть этот точный код в моем проекте, но он пропускает ViewModels (Finalizer не вызывается, привязка команд, похоже, сохраняет DataContext). Можете ли вы убедиться, что эта проблема существует и для вас?
Joris Weimar
@Juve, это работает, но можно ли сделать это так, чтобы он запускался для всех элементов управления, которые реализуют один и тот же шаблон? Имя уникально, поэтому нам понадобится отдельный шаблон для каждого, если я чего-то не упускаю.
Крис
1
@Juve не обращайте внимания на мой последний, я заставил его работать, используя родственник с findancestor и поиск по типу предка (так что все то же самое, кроме поиска по имени). В моем случае я повторяю использование ItemsControls, каждый из которых реализует шаблон, поэтому мой выглядит так: Command = "{Binding RelativeSource = {RelativeSource FindAncestor, AncestorType = {x: Type ItemsControl}}, Path = DataContext.OpenDocumentBtnCommand}"
Крис
48

Вы можете использовать RelativeSourceдля поиска родительского элемента, например:

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

См. Этот вопрос SO для получения более подробной информации о RelativeSource.

akjoshi
источник
10
Мне пришлось указать, Mode=FindAncestorчтобы он работал, но это работает и намного лучше в сценарии MVVM, поскольку позволяет избежать элементов управления именами. Binding="{Binding Path=DataContext.CurveSpeedMustBeSpecified, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:YourParentElementType}}}"
Aphex
1
работают как шарм <3 и не нужно указывать режим, .net 4.6.1
user2475096
30

RelativeSource против ElementName

Эти два подхода позволяют достичь одного и того же результата,

RelativeSrouce

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

Этот метод ищет элемент управления типа Window (в этом примере) в визуальном дереве, и когда он его находит, вы можете получить к нему доступ, DataContextиспользуя расширение Path=DataContext..... Плюсы этого метода в том, что вам не нужно быть привязанным к имени, и это своего рода динамический, однако изменения, внесенные в ваше визуальное дерево, могут повлиять на этот метод и, возможно, сломать его.

ElementName

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

Этот метод относится к твердой статике, Nameпоэтому, пока ваша область видимости может видеть это, все в порядке. Вы должны придерживаться своего соглашения об именах, чтобы не нарушать этот метод, конечно. Подход очень прост, и все, что вам нужно, это указать a Name="..."для вашего Window / UserControl.

Хотя все три типа (RelativeSource, Source, ElementName ) способны делать одно и то же, но, согласно следующей статье MSDN, каждый из них лучше использовать в своей области специализации.

Как: указать источник привязки

Найдите краткое описание каждого из них и ссылку на более подробную информацию в таблице внизу страницы.

Mehrad
источник
18

Я искал, как сделать что-то подобное в WPF, и получил это решение:

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

Я надеюсь, что это сработает для кого-нибудь еще. У меня есть контекст данных, который автоматически устанавливается в ItemsControls, и этот контекст данных имеет два свойства: MyItems-который является коллекцией- и одну команду CustomCommand. Поскольку ItemTemplateиспользуется a DataTemplate, DataContextпрямой доступ к верхним уровням невозможен. Тогда обходной путь для получения DC родительского элемента - использовать относительный путь и фильтровать по ItemsControlтипу.

гмадригал
источник
0

проблема в том, что DataTemplate не является частью применяемого к нему элемента.

это означает, что если вы привязываетесь к шаблону, вы привязываетесь к чему-то, что не имеет контекста.

однако если вы поместите элемент в шаблон, тогда, когда этот элемент применяется к родительскому, он получает контекст, и привязка затем работает

так что это не сработает

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

но это отлично работает

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

потому что после применения таблички данных групповой ящик помещается в родительский элемент и будет иметь доступ к его контексту

поэтому все, что вам нужно сделать, это удалить стиль из шаблона и переместить его в элемент в шаблоне

обратите внимание, что контекст для элемента управления - это элемент, а не элемент управления, т.е. ComboBoxItem для ComboBox, а не сам ComboBox, и в этом случае вместо этого следует использовать элементы управления ItemContainerStyle

MikeT
источник
0

Да, вы можете решить эту проблему с помощью ElementName=Something предложенный Жювом.

НО!

Если дочерний элемент (для которого вы используете такую ​​привязку) является пользовательским элементом управления, который использует то же имя элемента, что и вы указали в родительском элементе управления, то привязка переходит к неправильному объекту !!

Я знаю, что этот пост не является решением, но я думал, что все, кто использует ElementName в привязке, должны знать об этом, поскольку это возможная ошибка времени выполнения.

<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
источник