Acceda al DataContext principal desde DataTemplate

112

Tengo un ListBoxque se une a una colección secundaria en un ViewModel. Los elementos del cuadro de lista tienen un estilo en una plantilla de datos basada en una propiedad en el ViewModel principal:

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

Recibo el siguiente error de salida:

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

Entonces, si cambio la expresión de enlace a "Path=DataContext.CurrentItem.CurveSpeedMustBeSpecified"que funciona, pero solo mientras el contexto de datos del control de usuario principal sea un BindingListCollectionView. Esto no es aceptable porque el resto del control de usuario se une a las propiedades del CurrentItemen BindingListautomáticamente.

¿Cómo puedo especificar la expresión de enlace dentro del estilo para que funcione independientemente de que el contexto de datos principal sea una vista de colección o un solo elemento?

Marius
fuente

Respuestas:

161

Tuve problemas con la fuente relativa en Silverlight. Después de buscar y leer, no encontré una solución adecuada sin usar alguna biblioteca de enlace adicional. Pero aquí hay otro enfoque para obtener acceso al DataContext principal haciendo referencia directamente a un elemento del que conoce el contexto de datos. Se usa Binding ElementNamey funciona bastante bien, siempre que respete su propio nombre y no tenga una gran reutilización de templates/ stylesentre 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>

Esto también funciona si pones el botón en 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>

Al principio pensé que los x:Nameselementos principales de of no son accesibles desde un elemento con plantilla, pero como no encontré una solución mejor, lo intenté y funciona bien.

Juve
fuente
1
Tengo este código exacto en mi proyecto, pero está filtrando ViewModels (no se llama al finalizador, el enlace de comandos parece retener DataContext). ¿Puede verificar que este problema también existe para usted?
Joris Weimar
@Juve esto funciona, pero ¿es posible hacer esto para que se active para todos los controles de elementos que implementan la misma plantilla? El nombre es único, por lo que necesitaríamos una plantilla separada para cada uno, a menos que me falte algo.
Chris
1
@Juve hizo caso omiso de mi último, lo hice funcionar usando parientesource con findancestor y buscando por ancestortype, (de todos modos, excepto que no busqué por nombre). En mi caso, repito el uso de ItemsControls, cada uno implementando una plantilla para que la mía se vea así: Command = "{Binding RelativeSource = {RelativeSource FindAncestor, AncestorType = {x: Type ItemsControl}}, Path = DataContext.OpenDocumentBtnCommand}"
Chris
48

Puede usar RelativeSourcepara encontrar el elemento padre, así:

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

Consulte esta pregunta SO para obtener más detalles sobre RelativeSource.

Akjoshi
fuente
10
Tuve que especificar Mode=FindAncestorque funcionara, pero esto funciona y es mucho mejor en un escenario MVVM porque evita los controles de nombres. Binding="{Binding Path=DataContext.CurveSpeedMustBeSpecified, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:YourParentElementType}}}"
Aphex
1
funciona como un encanto <3 y no tenía que especificar el modo, .net 4.6.1
user2475096
30

RelativeSource frente a ElementName

Estos dos enfoques pueden lograr el mismo resultado,

RelativeSrouce

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

Este método busca un control de un tipo Window (en este ejemplo) en el árbol visual y cuando lo encuentra, básicamente puede acceder a él DataContextusando Path=DataContext..... Las ventajas de este método es que no es necesario estar vinculado a un nombre y es algo dinámico; sin embargo, los cambios realizados en su árbol visual pueden afectar este método y posiblemente romperlo.

ElementName

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

Este método se refiere a una estática sólida, Namepor lo que siempre que su osciloscopio pueda verlo, está bien. Debería ceñirse a su convención de nomenclatura para no romper este método, por supuesto. El enfoque es bastante simple y todo lo que necesita es especificar a Name="..."para su Window / UserControl.

Aunque los tres tipos ( RelativeSource, Source, ElementName) son capaces de hacer lo mismo, pero según el siguiente artículo de MSDN, es mejor utilizar cada uno en su propia área de especialidad.

Cómo: especificar el origen de enlace

Encuentre la descripción breve de cada uno más un enlace a uno más detalles en la tabla en la parte inferior de la página.

Mehrad
fuente
18

Estaba buscando cómo hacer algo similar en WPF y obtuve esta solución:

<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 esto funcione para alguien más. Tengo un contexto de datos que se establece automáticamente en ItemsControls, y este contexto de datos tiene dos propiedades: MyItems-que es una colección- y un comando 'CustomCommand'. Debido a que ItemTemplatese usa a DataTemplate, DataContextno se puede acceder directamente a los niveles superiores. Luego, la solución para obtener el DC del padre es usar una ruta relativa y filtrar por ItemsControltipo.

hmadrigal
fuente
0

el problema es que un DataTemplate no es parte de un elemento que se le aplica.

esto significa que si se vincula a la plantilla, se vincula a algo que no tiene contexto.

sin embargo, si coloca un elemento dentro de la plantilla, cuando ese elemento se aplica al padre, obtiene un contexto y el enlace funciona

entonces esto no funcionará

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

pero esto funciona perfectamente

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

porque después de que se aplica la plantilla de datos, el cuadro de grupo se coloca en el padre y tendrá acceso a su contexto

así que todo lo que tiene que hacer es eliminar el estilo de la plantilla y moverlo a un elemento en la plantilla

tenga en cuenta que el contexto para un control de elementos es el elemento, no el control, es decir, ComboBoxItem para ComboBox, no el ComboBox en sí, en cuyo caso debe usar los controles ItemContainerStyle en su lugar

MikeT
fuente
0

Sí, puedes solucionarlo usando el ElementName=Somethingcomo sugiere la Juve.

¡PERO!

Si un elemento secundario (en el que usa este tipo de enlace) es un control de usuario que usa el mismo nombre de elemento que usted especifica en el control principal, entonces el enlace va al objeto equivocado.

Sé que esta publicación no es una solución, pero pensé que todos los que usan ElementName en el enlace deberían saber esto, ya que es un posible error de tiempo de ejecución.

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