¿Cómo uso los enlaces WPF con RelativeSource?

Respuestas:

783

Si desea enlazar a otra propiedad en el objeto:

{Binding Path=PathToProperty, RelativeSource={RelativeSource Self}}

Si desea obtener una propiedad de un antepasado:

{Binding Path=PathToProperty,
    RelativeSource={RelativeSource AncestorType={x:Type typeOfAncestor}}}

Si desea obtener una propiedad en el padre con plantilla (para que pueda hacer enlaces bidireccionales en un ControlTemplate)

{Binding Path=PathToProperty, RelativeSource={RelativeSource TemplatedParent}}

o, más corto (esto solo funciona para enlaces OneWay):

{TemplateBinding Path=PathToProperty}
Abe Heidebrecht
fuente
15
Para este "{Binding Path = PathToProperty, RelativeSource = {RelativeSource AncestorType = {x: Type typeOfAncestor}}}", parece que debe tener "Mode = FindAncestor", antes de "AncestorType"
EdwardM
1
¿Para qué tecnología? En WPF, eso se infiere cuando especifica un AncestorType.
Abe Heidebrecht
2
Estoy de acuerdo con @EdwardM. Cuando omito FindAncestor, antes AncestorType, aparece el siguiente error: "RelativeSource no está en modo FindAncestor". (En VS2013, versión comunitaria)
kmote
1
@kmote, esto ha funcionado para mí desde .net 3.0, y una vez más verifiqué que funciona de esta manera en kaxaml ... Nuevamente, ¿qué tecnología está utilizando? El procesador XAML es diferente para WPF / Silverlight / UWP, por lo que puede tener diferentes resultados en diferentes tecnologías. También mencionaste VS Community, ¿entonces tal vez es una advertencia IDE, pero funciona en tiempo de ejecución?
Abe Heidebrecht
66
Sólo quería señalar aquí que si se quiere se unen a una propiedad en el DataContext de la RelativeSource continuación, debe especificar explícitamente que: {Binding Path=DataContext.SomeProperty, RelativeSource=.... Esto fue algo inesperado para mí como novato cuando intentaba vincularme al DataContext de un padre dentro de un DataTemplate.
DrEsperanto
133
Binding RelativeSource={
    RelativeSource Mode=FindAncestor, AncestorType={x:Type ItemType}
}
...

El atributo predeterminado de RelativeSourcees la Modepropiedad. Aquí se proporciona un conjunto completo de valores válidos ( de MSDN ):

  • PreviousData Le permite vincular el elemento de datos anterior (no ese control que contiene el elemento de datos) en la lista de elementos de datos que se muestran.

  • TemplatedParent Hace referencia al elemento al que se aplica la plantilla (en la que existe el elemento enlazado a datos). Esto es similar a establecer una extensión de TemplateBinding y solo es aplicable si el enlace está dentro de una plantilla.

  • Auto Se refiere al elemento en el que está estableciendo el enlace y le permite vincular una propiedad de ese elemento a otra propiedad en el mismo elemento.

  • FindAncestor Se refiere al antepasado en la cadena padre del elemento enlazado a datos. Puede usar esto para enlazar a un antepasado de un tipo específico o sus subclases. Este es el modo que utiliza si desea especificar AncestorType y / o AncestorLevel.

Drew Noakes
fuente
128

Aquí hay una explicación más visual en el contexto de una arquitectura MVVM:

ingrese la descripción de la imagen aquí

Jeffrey Knight
fuente
19
¿Me he perdido algo? ¿Cómo puedes considerar eso un gráfico simple y claro? 1: los cuadros del significado de la izquierda no están realmente relacionados con los de la derecha (¿por qué hay un archivo .cs dentro del ViewModel?) 2: ¿a qué apuntan estas flechas de DataContext? 3: ¿por qué la propiedad de mensaje no está en ViewModel1? y lo más importante 5: ¿Por qué necesita un enlace RelativeSource para acceder al DataContext de la ventana si TextBlock ya tiene ese mismo DataContext? Claramente me falta algo aquí, así que o soy bastante tonto o este gráfico no es tan simple y claro como todos piensan. Por favor, ilumíneme
Markus Hütter
2
@ MarkusHütter El diagrama muestra a un grupo Vistas anidadas y ViewModels correspondientes. El DataContext de View1 es ViewModel1, pero quiere vincularse a una propiedad de BaseViewModel. Dado que BaseViewModel es el DataContext de BaseView (que es una ventana), puede hacerlo buscando el primer contenedor primario que es una ventana y tomando su DataContext.
mcargille
66
@MatthewCargille Sé muy bien lo que se supone que significa, ese no era mi punto. Pero ponte en la posición de alguien que no conoce bien XAML y MVVM y verás que esto no es simple y claro .
Markus Hütter
1
Tengo que estar de acuerdo con @ MarkusHütter, por cierto, el enlace a la izquierda podría ser tan simple como esto: {Binding Message}(un poco más simple ...)
florien
@florien No lo creo, al menos para mi caso de uso. Tengo un DataTemplate que necesita referirse al DataContext de MainWindow (mi clase de modelo de vista) para obtener una lista de opciones para un menú desplegable (cargado desde una base de datos). DataTemplate está vinculado a un objeto modelo que también se carga desde la base de datos, pero solo tiene acceso a la opción seleccionada. Tuve que configurar explícitamente Path=DataContext.Messagepara que el enlace funcione. Esto tiene sentido, dado que puede hacer enlaces relativos a ancho / alto / etc. de un control.
DrEsperanto
47

Bechir Bejaoui expone los casos de uso de RelativeSources en WPF en su artículo aquí :

RelativeSource es una extensión de marcado que se utiliza en casos de vinculación particulares cuando intentamos vincular una propiedad de un objeto determinado a otra propiedad del objeto en sí, cuando intentamos vincular una propiedad de un objeto a otro de sus padres relativos, cuando se vincula un valor de propiedad de dependencia a una pieza de XAML en caso de desarrollo de control personalizado y, finalmente, en caso de utilizar un diferencial de una serie de datos vinculados. Todas esas situaciones se expresan como modos de fuente relativa. Expondré todos esos casos uno por uno.

  1. Modo propio:

Imagine este caso, un rectángulo que queremos que su altura siempre sea igual a su ancho, digamos un cuadrado. Podemos hacer esto usando el nombre del elemento

<Rectangle Fill="Red" Name="rectangle" 
                Height="100" Stroke="Black" 
                Canvas.Top="100" Canvas.Left="100"
                Width="{Binding ElementName=rectangle,
                Path=Height}"/>

Pero en este caso anterior, estamos obligados a indicar el nombre del objeto de enlace, es decir, el rectángulo. Podemos alcanzar el mismo propósito de manera diferente usando RelativeSource

<Rectangle Fill="Red" Height="100" 
               Stroke="Black" 
               Width="{Binding RelativeSource={RelativeSource Self},
               Path=Height}"/>

Para ese caso, no estamos obligados a mencionar el nombre del objeto de enlace y el ancho siempre será igual a la altura cada vez que se cambie la altura.

Si desea parametrizar el ancho para que sea la mitad de la altura, puede hacerlo agregando un convertidor a la extensión de marcado de enlace. Imaginemos otro caso ahora:

 <TextBlock Width="{Binding RelativeSource={RelativeSource Self},
               Path=Parent.ActualWidth}"/>

El caso anterior se utiliza para vincular una propiedad dada de un elemento dado a uno de sus elementos primarios directos, ya que este elemento posee una propiedad que se llama Principal. Esto nos lleva a otro modo de fuente relativa que es el FindAncestor.

  1. Modo FindAncestor

En este caso, una propiedad de un elemento dado estará vinculada a uno de sus padres, Of Corse. La principal diferencia con el caso anterior es el hecho de que depende de usted determinar el tipo de antepasado y el rango de antepasado en la jerarquía para vincular la propiedad. Por cierto, intenta jugar con esta pieza de XAML

<Canvas Name="Parent0">
    <Border Name="Parent1"
             Width="{Binding RelativeSource={RelativeSource Self},
             Path=Parent.ActualWidth}"
             Height="{Binding RelativeSource={RelativeSource Self},
             Path=Parent.ActualHeight}">
        <Canvas Name="Parent2">
            <Border Name="Parent3"
            Width="{Binding RelativeSource={RelativeSource Self},
           Path=Parent.ActualWidth}"
           Height="{Binding RelativeSource={RelativeSource Self},
              Path=Parent.ActualHeight}">
               <Canvas Name="Parent4">
               <TextBlock FontSize="16" 
               Margin="5" Text="Display the name of the ancestor"/>
               <TextBlock FontSize="16" 
                 Margin="50" 
            Text="{Binding RelativeSource={RelativeSource  
                       FindAncestor,
                       AncestorType={x:Type Border}, 
                       AncestorLevel=2},Path=Name}" 
                       Width="200"/>
                </Canvas>
            </Border>
        </Canvas>
     </Border>
   </Canvas>

La situación anterior es de dos elementos TextBlock que están incrustados dentro de una serie de bordes y elementos de lienzo que representan a sus padres jerárquicos. El segundo TextBlock mostrará el nombre del padre dado en el nivel de fuente relativo.

Así que trate de cambiar AncestorLevel = 2 a AncestorLevel = 1 y vea qué sucede. Luego intente cambiar el tipo del antepasado de AncestorType = Border a AncestorType = Canvas y vea qué sucede.

El texto que se muestra cambiará de acuerdo con el tipo y nivel de Ancestro. Entonces, ¿qué sucede si el nivel de ancestro no es adecuado para el tipo de ancestro? Esta es una buena pregunta, sé que estás a punto de hacerla. La respuesta es que no se lanzarán excepciones y no se mostrará nada en el nivel de TextBlock.

  1. Padre con plantilla

Este modo permite vincular una propiedad ControlTemplate dada a una propiedad del control al que se aplica ControlTemplate. Para comprender bien el problema aquí hay un ejemplo a continuación

<Window.Resources>
<ControlTemplate x:Key="template">
        <Canvas>
            <Canvas.RenderTransform>
                <RotateTransform Angle="20"/>
                </Canvas.RenderTransform>
            <Ellipse Height="100" Width="150" 
                 Fill="{Binding 
            RelativeSource={RelativeSource TemplatedParent},
            Path=Background}">

              </Ellipse>
            <ContentPresenter Margin="35" 
                  Content="{Binding RelativeSource={RelativeSource  
                  TemplatedParent},Path=Content}"/>
        </Canvas>
    </ControlTemplate>
</Window.Resources>
    <Canvas Name="Parent0">
    <Button   Margin="50" 
              Template="{StaticResource template}" Height="0" 
              Canvas.Left="0" Canvas.Top="0" Width="0">
        <TextBlock FontSize="22">Click me</TextBlock>
    </Button>
 </Canvas>

Si quiero aplicar las propiedades de un control dado a su plantilla de control, entonces puedo usar el modo TemplatedParent. También hay una similar a esta extensión de marcado, que es TemplateBinding, que es una especie de abreviatura de la primera, pero TemplateBinding se evalúa en tiempo de compilación en contraste con TemplatedParent, que se evalúa justo después del primer tiempo de ejecución. Como puede observar en la siguiente figura, el fondo y el contenido se aplican desde el botón a la plantilla de control.

Cornel Marian
fuente
Muy buenos ejemplos para mí, utilicé Find Ancestor para vincular a un comando en el contexto de datos de un padre ListView. El padre tiene 2 ListViewniveles más por debajo. Esto me ayudó a prevenir el contagio de datos en cada máquina virtual posterior de cada ListView'sDataTemplate
Caleb W.
34

En WPF, el RelativeSourceenlace expone tres propertiespara establecer:

1. Modo: este enumpodría tener cuatro valores:

a. PreviousData ( value=0): asigna el valor anterior depropertyal al límite

si. TemplatedParent ( value=1): Esto se usa cuando se define eltemplatescontrol de cualquier control y se quiere vincular a un valor / Propiedad delcontrol.

Por ejemplo, defina ControlTemplate:

  <ControlTemplate>
        <CheckBox IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
 </ControlTemplate>

C. Self ( value=2): cuando queremos unirnos desde aselfo apropertyof self.

Por ejemplo: Enviar comprobado estado de checkboxque CommandParametermientras se ajusta la CommanddeCheckBox

<CheckBox ...... CommandParameter="{Binding RelativeSource={RelativeSource Self},Path=IsChecked}" />

re. FindAncestor ( value=3): cuando se quiere vincular desde un padrecontrol enVisual Tree.

Por ejemplo: enlace a checkboxin recordsif a grid, if header checkboxestá marcado

<CheckBox IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid}}, Path=DataContext.IsHeaderChecked, Mode=TwoWay}" />

2.FindAncestor Tipo de antepasado : cuando el modo es , defina qué tipo de antepasado

RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid}}

3. AncestorLevel: cuando mode esFindAncestorentonces qué nivel de ancestro (si hay dos tipos de padres en el mismovisual tree)

RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid, AncestorLevel=1}}

Arriba están todos los casos de uso para RelativeSource binding.

Aquí hay un enlace de referencia .

Kylo Ren
fuente
2
Impresionante ... esto funcionó para mí: <DataGridCheckBoxColumn Header = "Paid" Width = "35" Binding = "{Binding RelativeSource = {RelativeSource Mode = FindAncestor, AncestorType = {x: Type Window}}, Path = DataContext.SelectedBuyer.IsPaid , Mode = OneWay} "/> donde estaba intentando vincular al comprador seleccionado de la ventana principal. Propiedad IsPaid
Michael K
21

No te olvides de TemplatedParent:

<Binding RelativeSource="{RelativeSource TemplatedParent}"/>

o

{Binding RelativeSource={RelativeSource TemplatedParent}}
Bob King
fuente
16

Creé una biblioteca para simplificar la sintaxis de enlace de WPF, lo que facilita el uso de RelativeSource. Aquí hay unos ejemplos. Antes de:

{Binding Path=PathToProperty, RelativeSource={RelativeSource Self}}
{Binding Path=PathToProperty, RelativeSource={RelativeSource AncestorType={x:Type typeOfAncestor}}}
{Binding Path=PathToProperty, RelativeSource={RelativeSource TemplatedParent}}
{Binding Path=Text, ElementName=MyTextBox}

Después:

{BindTo PathToProperty}
{BindTo Ancestor.typeOfAncestor.PathToProperty}
{BindTo Template.PathToProperty}
{BindTo #MyTextBox.Text}

Aquí hay un ejemplo de cómo se simplifica el enlace de métodos. Antes de:

// C# code
private ICommand _saveCommand;
public ICommand SaveCommand {
 get {
  if (_saveCommand == null) {
   _saveCommand = new RelayCommand(x => this.SaveObject());
  }
  return _saveCommand;
 }
}

private void SaveObject() {
 // do something
}

// XAML
{Binding Path=SaveCommand}

Después:

// C# code
private void SaveObject() {
 // do something
}

// XAML
{BindTo SaveObject()}

Puede encontrar la biblioteca aquí: http://www.simplygoodcode.com/2012/08/simpler-wpf-binding.html

Tenga en cuenta en el ejemplo 'ANTES' que uso para el enlace del método que el código ya estaba optimizado usando RelayCommandel último que verifiqué que no es una parte nativa de WPF. Sin eso, el ejemplo 'ANTES' hubiera sido aún más largo.

Luis perez
fuente
2
Este tipo de ejercicios de sujeción de la mano demuestran la debilidad de XAML; manera demasiado complicado.
dudeNumber4
16

Algunas partes útiles:

Aquí se explica cómo hacerlo principalmente en código:

Binding b = new Binding();
b.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, this.GetType(), 1);
b.Path = new PropertyPath("MyElementThatNeedsBinding");
MyLabel.SetBinding(ContentProperty, b);

Copié esto en gran parte de Binding Relative Source en el código Behind .

Además, la página MSDN es bastante buena en lo que respecta a los ejemplos: Clase RelativeSource

Nathan Cooper
fuente
55
Sin embargo, mi vago recuerdo de WPF es que hacer enlaces en el código no suele ser lo mejor.
Nathan Cooper
12

Acabo de publicar otra solución para acceder al DataContext de un elemento padre en Silverlight que funciona para mí. Lo utiliza Binding ElementName.

Juve
fuente
10

No leí todas las respuestas, pero solo quiero agregar esta información en caso de un comando de fuente relativa de un botón.

Cuando utiliza una fuente relativa con Mode=FindAncestor, el enlace debe ser como:

Command="{Binding Path=DataContext.CommandProperty, RelativeSource={...}}"

Si no agrega DataContext en su ruta, en el momento de la ejecución no puede recuperar la propiedad.

Kevin VDF
fuente
9

Este es un ejemplo del uso de este patrón que funcionó para mí en cuadrículas de datos vacías.

<Style.Triggers>
    <DataTrigger Binding="{Binding Items.Count, RelativeSource={RelativeSource Self}}" Value="0">
        <Setter Property="Background">
            <Setter.Value>
                <VisualBrush Stretch="None">
                    <VisualBrush.Visual>
                        <TextBlock Text="We did't find any matching records for your search..." FontSize="16" FontWeight="SemiBold" Foreground="LightCoral"/>
                    </VisualBrush.Visual>
                </VisualBrush>
            </Setter.Value>
        </Setter>
    </DataTrigger>
</Style.Triggers>
Edd
fuente
6

Si un elemento no es parte del árbol visual, RelativeSource nunca funcionará.

En este caso, debe probar una técnica diferente, iniciada por Thomas Levesque.

Tiene la solución en su blog en [WPF] Cómo enlazar a datos cuando el DataContext no se hereda . ¡Y funciona absolutamente brillante!

En el improbable caso de que su blog esté caído, el Apéndice A contiene una copia espejo de su artículo .

Por favor no comente aquí, comente directamente en su blog .

Apéndice A: espejo de la publicación del blog

La propiedad DataContext en WPF es extremadamente útil, ya que es heredada automáticamente por todos los elementos secundarios del elemento donde lo asigna; por lo tanto, no necesita configurarlo de nuevo en cada elemento que desee vincular. Sin embargo, en algunos casos el DataContext no es accesible: sucede para elementos que no son parte del árbol visual o lógico. Puede ser muy difícil vincular una propiedad a esos elementos ...

Vamos a ilustrar con un ejemplo simple: queremos mostrar una lista de productos en un DataGrid. En la cuadrícula, queremos poder mostrar u ocultar la columna Precio, según el valor de una propiedad ShowPrice expuesta por ViewModel. El enfoque obvio es vincular la Visibilidad de la columna a la propiedad ShowPrice:

<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                Visibility="{Binding ShowPrice,
                Converter={StaticResource visibilityConverter}}"/>

Desafortunadamente, cambiar el valor de ShowPrice no tiene ningún efecto, y la columna siempre está visible ... ¿por qué? Si miramos la ventana Salida en Visual Studio, notamos la siguiente línea:

System.Windows.Data Error: 2: No se puede encontrar FrameworkElement o FrameworkContentElement para el elemento de destino. BindingExpression: Path = ShowPrice; DataItem = nulo; el elemento de destino es 'DataGridTextColumn' (HashCode = 32685253); la propiedad de destino es 'Visibilidad' (tipo 'Visibilidad')

El mensaje es bastante críptico, pero el significado es bastante simple: WPF no sabe qué FrameworkElement usar para obtener el DataContext, porque la columna no pertenece al árbol visual o lógico de DataGrid.

Podemos intentar ajustar el enlace para obtener el resultado deseado, por ejemplo, estableciendo RelativeSource en DataGrid:

<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                Visibility="{Binding DataContext.ShowPrice,
                Converter={StaticResource visibilityConverter},
                RelativeSource={RelativeSource FindAncestor, AncestorType=DataGrid}}"/>

O podemos agregar un CheckBox vinculado a ShowPrice e intentar vincular la visibilidad de la columna a la propiedad IsChecked especificando el nombre del elemento:

<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                Visibility="{Binding IsChecked,
                Converter={StaticResource visibilityConverter},
                ElementName=chkShowPrice}"/>

Pero ninguna de estas soluciones parece funcionar, siempre obtenemos el mismo resultado ...

En este punto, parece que el único enfoque viable sería cambiar la visibilidad de la columna en código subyacente, que generalmente preferimos evitar al usar el patrón MVVM ... Pero no me voy a rendir tan pronto, al menos no mientras que hay otras opciones a considerar 😉

La solución a nuestro problema es realmente bastante simple y aprovecha la clase Freezable. El propósito principal de esta clase es definir objetos que tienen un estado modificable y de solo lectura, pero la característica interesante en nuestro caso es que los objetos Freezable pueden heredar el DataContext incluso cuando no están en el árbol visual o lógico. No sé el mecanismo exacto que permite este comportamiento, pero vamos a aprovecharlo para que nuestro enlace funcione ...

La idea es crear una clase (la llamé BindingProxy por razones que deberían ser obvias muy pronto) que hereda Freezable y declara una propiedad de dependencia de datos:

public class BindingProxy : Freezable
{
    #region Overrides of Freezable

    protected override Freezable CreateInstanceCore()
    {
        return new BindingProxy();
    }

    #endregion

    public object Data
    {
        get { return (object)GetValue(DataProperty); }
        set { SetValue(DataProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Data.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty DataProperty =
        DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}

Luego podemos declarar una instancia de esta clase en los recursos de DataGrid y vincular la propiedad Data al DataContext actual:

<DataGrid.Resources>
    <local:BindingProxy x:Key="proxy" Data="{Binding}" />
</DataGrid.Resources>

El último paso es especificar este objeto BindingProxy (de fácil acceso con StaticResource) como el origen del enlace:

<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                Visibility="{Binding Data.ShowPrice,
                Converter={StaticResource visibilityConverter},
                Source={StaticResource proxy}}"/>

Tenga en cuenta que la ruta de enlace se ha prefijado con "Datos", ya que la ruta ahora es relativa al objeto BindingProxy.

El enlace ahora funciona correctamente y la columna se muestra u oculta correctamente según la propiedad ShowPrice.

Aplazamiento de pago
fuente