Access parent DataContext from DataTemplate

I have a ListBox which binds to a child collection on a ViewModel. The listbox items are styled in a datatemplate based on a property on the parent 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>

I get the following output error:

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

So if I change the the binding expression to "Path=DataContext.CurrentItem.CurveSpeedMustBeSpecified" it works, but only as long as the datacontext of the parent user control is a BindingListCollectionView. This is not acceptable because the rest of the user control binds to the properties of the CurrentItem on the BindingList automatically.

How can I specify the binding expression inside the style so that it works regardless of the parent data context being a collection view or a single item?


Solution 1:

I had problems with the relative source in Silverlight. After searching and reading I did not find a suitable solution without using some additional Binding library. But, here is another approach for gaining access to the parent DataContext by directly referencing an element of which you know the data context. It uses Binding ElementName and works quite well, as long as you respect your own naming and don't have heavy reuse of templates/styles across components:

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

This also works if you put the button into 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>

At first I thought that the x:Names of parent elements are not accessible from within a templated item, but since I found no better solution, I just tried, and it works fine.

Solution 2:

You can use RelativeSource to find the parent element, like this -

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

See this SO question for more details about RelativeSource.

Solution 3:

RelativeSource vs. ElementName

These two approaches can achieve the same result,

RelativeSource

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

This method looks for a control of a type Window (in this example) in the visual tree and when it finds it you basically can access it's DataContext using the Path=DataContext..... The Pros about this method is that you don't need to be tied to a name and it's kind of dynamic, however, changes made to your visual tree can affect this method and possibly break it.

ElementName

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

This method referes to a solid static Name so as long as your scope can see it, you're fine.You should be sticking to your naming convention not to break this method of course.The approach is qute simple and all you need is to specify a Name="..." for your Window/UserControl.

Although all three types (RelativeSource, Source, ElementName) are capable of doing the same thing, but according to the following MSDN article, each one better be used in their own area of specialty.

How to: Specify the Binding Source

Find the brief description of each plus a link to a more details one in the table on the bottom of the page.

Solution 4:

I was searching how to do something similar in WPF and I got this solution:

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

I hope this works for somebody else. I have a data context which is set automatically to the ItemsControls, and this data context has two properties: MyItems -which is a collection-, and one command 'CustomCommand'. Because of the ItemTemplate is using a DataTemplate, the DataContext of upper levels is not directly accessible. Then the workaround to get the DC of the parent is use a relative path and filter by ItemsControl type.