从DataTemplate访问父级DataContext

125

我有一个 ListBox 绑定到 ViewModel 上的一个子集合。基于父 ViewModel 上的某个属性, DataTemplate 样式化了 ListBox 的项目:

<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" ,它就能工作,但前提是父用户控件的 DataContext 是 BindingListCollectionView。这是不可接受的,因为用户控件的其余部分自动绑定到 BindingList 上的 CurrentItem 的属性。

如何在样式中指定绑定表达式,以便无论父数据上下文是集合视图还是单个项,都能正常工作?

7个回答

175

我在Silverlight中使用相对源(relative source)时遇到了问题。经过搜索和阅读,我没有找到合适的解决方案,而不使用一些额外的绑定库。但是,这里有另一种获取父级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 这个可以用,但是能不能让它对所有实现相同模板的项控件都起作用呢?因为名称是唯一的,所以我们需要为每个控件单独创建一个模板,除非我漏掉了什么。 - Chris
1
@Juve 不用理会我之前的消息,我已经通过使用RelativeSource和FindAncestor来搜索祖先类型(所以与按名称搜索相同,只是不按名称搜索)。在我的情况下,我重复使用ItemsControls,每个都实现一个模板,所以我的代码看起来像这样:Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}, Path=DataContext.OpenDocumentBtnCommand}"。 - Chris
在我的情况下不起作用,XAML绑定失败显示DataContext为空。 - dellos

55

您可以使用RelativeSource来找到父元素,像这样 -

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

请看这篇SO问题,了解有关RelativeSource的更多细节。


11
我必须指定 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

40

RelativeSource vs. ElementName

这两种方法可以达到相同的结果,

RelativeSource

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

这种方法会在可视树中查找指定类型的控件(此例中是Window),并且一旦找到,你就可以使用Path=DataContext....来访问它的DataContext。该方法的优点是你不需要绑定名称,而且它比较动态,但是,对可视树所做的更改可能会影响此方法,并可能导致其失效。

ElementName

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

此方法是引用一个固定的静态Name,只要您的作用域可以看到它,您就可以使用它。当然,您应该遵循自己的命名约定以不破坏此方法。这种方法非常简单,您只需要为您的Window/UserControl指定一个Name="..."即可。

虽然这三种类型(RelativeSource, Source, ElementName)都可以做同样的事情,但根据下面的MSDN文章,在各自的专业领域中最好使用每个类型。

如何指定绑定源

在页面底部的表格中找到每个类型的简要说明以及更详细信息的链接。


19

我在搜索如何在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使用了DataTemplate,所以父级的DataContext不能直接访问。然后获取父级的数据上下文的解决方法是使用相对路径并按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}" >

因为应用数据模板后,GroupBox将被放置在父级中,并且将可以访问其上下文

所以您只需要从模板中删除样式并将其移动到模板中的元素中即可

注意,ItemsControl的上下文是项而不是控件,例如ComboBox的ComboBoxItem而不是ComboBox本身,在这种情况下,您应该使用控件的ItemContainerStyle。


0

是的,你可以通过使用Juve建议的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>

0

也可以通过 . 进行操作。使用此运算符,您可以访问 Source这里

可选地,可以使用点(.)路径绑定到当前源。例如,Text="{Binding}" 等效于 Text="{Binding Path=.}"。

例如,我喜欢以这种方式操作(将 DataContext 放在 Tag 中,然后访问 parentDataContext):

ResourceDictionary.xaml:

<Style x:Key="SomeControl" TargetType="ItemsControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate>
                    <Grid Tag="{Binding .}">
                        <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag.DataContext.SomeName}"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

MainWindow.xaml:

<ItemsControl Style="{StaticResource SomeControl}"/>

MainWindow.xaml.cs:

public partial class MainWindow : Window
{
   public MainWindow()
   {
      //...
      SomeName = "Your value";
   } 
   public string SomeName { get; set; }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接