Acesse DataContext pai de DataTemplate

112

Eu tenho um ListBoxque se liga a uma coleção filho em um ViewModel. Os itens da caixa de listagem são estilizados em um datatemplate com base em uma propriedade no ViewModel pai:

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

Recebo o seguinte erro de saída:

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

Portanto, se eu alterar a expressão de ligação, "Path=DataContext.CurrentItem.CurveSpeedMustBeSpecified"ela funcionará, mas apenas enquanto o texto de dados do controle de usuário pai for um BindingListCollectionView. Isso não é aceitável porque o restante do controle do usuário se liga às propriedades do CurrentItemno BindingListautomaticamente.

Como posso especificar a expressão de ligação dentro do estilo para que funcione independentemente do contexto de dados pai ser uma exibição de coleção ou um único item?

Marius
fonte

Respostas:

161

Tive problemas com a fonte relativa no Silverlight. Depois de pesquisar e ler, não encontrei uma solução adequada sem usar alguma biblioteca Binding adicional. Mas, aqui está outra abordagem para obter acesso ao DataContext pai , referenciando diretamente um elemento do qual você conhece o contexto de dados. Ele usa Binding ElementNamee funciona muito bem, contanto que você respeite sua própria nomenclatura e não tenha uma grande reutilização de templates/ stylesentre os componentes:

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

Isso também funciona se você colocar o botão em 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>

A princípio, pensei que os x:Nameselementos dos pais não eram acessíveis de dentro de um item com modelo, mas como não encontrei solução melhor, tentei e funcionou bem.

Juve
fonte
1
Eu tenho esse código exato em meu projeto, mas ele está vazando ViewModels (Finalizer não chamado, ligação de comando parece reter DataContext). Você pode verificar se esse problema também existe para você?
Joris Weimar
@Juve isso funciona, mas é possível fazer isso para que ele seja disparado para todos os controles de itens que implementam o mesmo modelo? O nome é único, então precisaríamos de um modelo separado para cada um, a menos que esteja faltando alguma coisa.
Chris
1
@Juve desconsidere meu último, eu fiz funcionar usando relativesource com findancestor e pesquisando por ancestortype, (então tudo igual exceto não pesquisar por nome). No meu caso, repito o uso de ItemsControls, cada um implementando um modelo, então o meu fica assim: Command = "{Binding RelativeSource = {RelativeSource FindAncestor, AncestorType = {x: Type ItemsControl}}, Path = DataContext.OpenDocumentBtnCommand}"
Chris
48

Você pode usar RelativeSourcepara encontrar o elemento pai, como este -

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

Veja esta pergunta do SO para mais detalhes sobre RelativeSource.

Akjoshi
fonte
10
Tive que especificar Mode=FindAncestorpara que funcionasse, mas funciona e é muito melhor em um cenário MVVM porque evita controles de nomenclatura. Binding="{Binding Path=DataContext.CurveSpeedMustBeSpecified, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:YourParentElementType}}}"
Aphex de
1
funcionou
perfeitamente
30

RelativeSource vs. ElementName

Essas duas abordagens podem alcançar o mesmo resultado,

RelativeSrouce

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

Este método procura um controle do tipo Window (neste exemplo) na árvore visual e quando o encontra você basicamente pode acessá-lo DataContextusando o Path=DataContext..... Os prós sobre esse método é que você não precisa estar vinculado a um nome e é meio dinâmico; no entanto, as alterações feitas em sua árvore visual podem afetar esse método e possivelmente quebrá-lo.

ElementName

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

Este método se refere a uma estática sólida Name então, desde que seu escopo possa ver, você está bem. Você deve seguir sua convenção de nomenclatura para não quebrar este método, é claro. A abordagem é bastante simples e tudo que você precisa é especificar a Name="..."para sua janela / controle de usuário.

Embora todos os três tipos (RelativeSource, Source, ElementName ) sejam capazes de fazer a mesma coisa, mas de acordo com o seguinte artigo do MSDN, cada um deve ser usado em sua própria área de especialidade.

Como: Especificar a fonte de ligação

Encontre a breve descrição de cada um e um link para mais detalhes na tabela na parte inferior da página.

Mehrad
fonte
18

Eu estava pesquisando como fazer algo semelhante no WPF e encontrei esta solução:

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

Espero que funcione para outra pessoa. Eu tenho um contexto de dados que é definido automaticamente para ItemsControls, e esse contexto de dados tem duas propriedades: MyItems-que é uma coleção- e um comando 'CustomCommand'. Por ItemTemplateestar usando um DataTemplate, o DataContextdos níveis superiores não está diretamente acessível. Em seguida, a solução alternativa para obter o DC do pai é usar um caminho relativo e filtrar por ItemsControltipo.

hmadrigal
fonte
0

o problema é que um DataTemplate não faz parte de um elemento aplicado a ele.

isso significa que se você se vincular ao modelo, estará vinculando a algo que não tem contexto.

no entanto, se você colocar um elemento dentro do modelo, quando esse elemento for aplicado ao pai, ele ganhará um contexto e a ligação funcionará

então isso não vai funcionar

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

mas isso funciona perfeitamente

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

porque depois que o datatemplate é aplicado, o groupbox é colocado no pai e terá acesso ao seu contexto

então tudo que você precisa fazer é remover o estilo do modelo e movê-lo para um elemento no modelo

observe que o contexto de um controle de itens é o item e não o controle, isto é, ComboBoxItem para ComboBox e não o próprio ComboBox, caso em que você deve usar os controles ItemContainerStyle

MikeT
fonte
0

Sim, você pode resolver isso usando o ElementName=Something sugerido pela Juve.

MAS!

Se um elemento filho (no qual você usa esse tipo de ligação) é um controle de usuário que usa o mesmo nome de elemento que você especificou no controle pai, a ligação vai para o objeto errado !!

Sei que este post não é uma solução, mas achei que todos que usam o ElementName na ligação deveriam saber disso, já que é um possível bug de 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
fonte