.net - ¿Cómo uso los enlaces WPF con RelativeSource?
xaml data-binding (14)
Acabo de publicar otra solución para acceder al DataContext de un elemento padre en Silverlight que funciona para mí. Utiliza Binding ElementName
.
¿Cómo uso RelativeSource
con los enlaces de WPF y cuáles son los diferentes casos de uso?
Algunos bits y piezas útiles:
Aquí está 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);
En gran parte copié esto de Binding Relative Source en el código Detrás .
Además, la página de MSDN es bastante buena en cuanto a los ejemplos: RelativeSource Class
Aquí hay una explicación más visual en el contexto de una arquitectura MVVM:
Bechir Bejaoui expone los casos de uso de RelativeSources en WPF en su artículo aquí :
El RelativeSource es una extensión de marcado que se usa en casos de vinculación particulares cuando intentamos vincular una propiedad de un objeto dado a otra propiedad del objeto en sí, cuando intentamos vincular una propiedad de un objeto a otra de sus padres relativos, al vincular un valor de propiedad de dependencia a una parte de XAML en caso de desarrollo de control personalizado y, finalmente, en caso de utilizar un diferencial de una serie de datos enlazados. Todas esas situaciones se expresan como modos de fuente relativos. Voy a exponer todos esos casos uno por uno.
- Modo auto
Imagina este caso, un rectángulo que queremos que su altura sea siempre igual a su anchura, 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 vinculante, es decir, el rectángulo. Podemos alcanzar el mismo propósito de manera diferente utilizando RelativeSource
<Rectangle Fill="Red" Height="100" Stroke="Black" Width="{Binding RelativeSource={RelativeSource Self}, Path=Height}"/>
En ese caso, no estamos obligados a mencionar el nombre del objeto de enlace y el Ancho siempre será igual a la Altura siempre 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 vinculación. Imaginemos otro caso ahora:
<TextBlock Width="{Binding RelativeSource={RelativeSource Self}, Path=Parent.ActualWidth}"/>
El caso anterior se usa para vincular una propiedad dada de un elemento dado a una de sus propiedades primarias directas, ya que este elemento posee una propiedad que se llama Padre. Esto nos lleva a otro modo de fuente relativa que es el FindAncestor.
- Modo FindAncestor
En este caso, una propiedad de un elemento dado estará vinculada a uno de sus padres, De 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. A continuación, intente cambiar el tipo de antepasado de AncestorType = Border to AncestorType = Canvas y vea qué sucede.
El texto mostrado cambiará de acuerdo con el tipo y nivel de Ancestro. Entonces, ¿qué sucede si el nivel de antepasado no es adecuado para el tipo de antepasado? Esta es una buena pregunta, sé que estás a punto de hacerla. La respuesta es que no se lanzarán excepciones y nada se mostrará en el nivel de TextBlock.
- TemplatedParent
Este modo permite vincular una propiedad de ControlTemplate dada a una propiedad del control al que se aplica ControlTemplate. Para entender bien el tema aquí hay un ejemplo abajo.
<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 el TemplateBinding que es una especie de mano corta de la primera, pero el TemplateBinding se evalúa en el momento de la compilación en contraste con el TemplatedParent que se evalúa justo después del primer tiempo de ejecución. Como puede observar en la figura de abajo, el fondo y el contenido se aplican desde dentro del botón a la plantilla de control.
Creé una biblioteca para simplificar la sintaxis de enlace de WPF, incluso para facilitar 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 del método. 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 que en el ejemplo "ANTES" que uso para el enlace de métodos, el código que ya estaba optimizado mediante RelayCommand
que fue el último que verifiqué, no es una parte nativa de WPF. Sin eso, el ejemplo ''ANTES'' hubiera sido aún más largo.
Es digno de notar que para aquellos que tropiezan con este pensamiento de Silverlight:
Silverlight ofrece solo un subconjunto reducido, de estos comandos
Este es un ejemplo del uso de este patrón que me funcionó en áreas 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>
Imagina este caso, un rectángulo que queremos que su altura sea siempre igual a su anchura, 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 vinculante, es decir, el rectángulo. Podemos alcanzar el mismo propósito de manera diferente utilizando RelativeSource
<Rectangle Fill="Red" Height="100"
Stroke="Black"
Width="{Binding RelativeSource={RelativeSource Self},
Path=Height}"/>
En ese caso, no estamos obligados a mencionar el nombre del objeto de enlace y el Ancho siempre será igual a la Altura siempre 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 vinculación. Imaginemos otro caso ahora:
<TextBlock Width="{Binding RelativeSource={RelativeSource Self},
Path=Parent.ActualWidth}"/>
El caso anterior se usa para vincular una propiedad dada de un elemento dado a una de sus propiedades primarias directas, ya que este elemento posee una propiedad que se llama Padre. Esto nos lleva a otro modo de fuente relativa que es el FindAncestor.
No leí todas las respuestas, pero solo quiero agregar esta información en caso de un enlace de comando de origen relativo de un botón.
Cuando usa 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.
No te olvides de TemplatedParent:
<Binding RelativeSource="{RelativeSource TemplatedParent}"/>
o
{Binding RelativeSource={RelativeSource TemplatedParent}}
Si desea enlazar a otra propiedad en el objeto:
{Binding Path=PathToProperty, RelativeSource={RelativeSource Self}}
Si desea obtener una propiedad en un antepasado:
{Binding Path=PathToProperty,
RelativeSource={RelativeSource AncestorType={x:Type typeOfAncestor}}}
Si desea obtener una propiedad en el padre con plantilla (por lo que puede hacer enlaces de dos vías en una plantilla ControlTemplate)
{Binding Path=PathToProperty, RelativeSource={RelativeSource TemplatedParent}}
o, más corto (esto solo funciona para los enlaces de OneWay):
{TemplateBinding Path=PathToProperty}
Si un elemento no forma 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 con datos cuando el DataContext no se hereda . ¡Y funciona absolutamente brillante!
En el improbable caso de que su blog esté inactivo, el Apéndice A contiene una copia espejo de su artículo .
Por favor, no comentes aquí, comenta directamente en su blog .
Apéndice A: espejo de la entrada del blog
La propiedad DataContext en WPF es extremadamente útil, ya que es heredada automáticamente por todos los elementos secundarios del elemento donde la asigna; por lo tanto, no es necesario volver a establecerlo en cada elemento que desee enlazar. Sin embargo, en algunos casos, el DataContext no es accesible: sucede con los elementos que no forman parte del árbol visual o lógico. Puede ser muy difícil entonces vincular una propiedad sobre esos elementos ...
Ilustrémoslo 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, en función del 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 observamos la ventana de resultados en Visual Studio, notamos la siguiente línea:
System.Windows.Data Error: 2: No se puede encontrar FrameworkElement o FrameworkContentElement gobernante para el elemento de destino. BindingExpression: Path = ShowPrice; DataItem = null; 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 del DataGrid.
Podemos intentar ajustar el enlace para obtener el resultado deseado, por ejemplo, configurando el RelativeSource al DataGrid en sí:
<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 el código subyacente, que generalmente preferimos evitar al usar el patrón MVVM ... Pero no voy a rendirme tan pronto, al menos no Si bien hay otras opciones a considerar
La solución a nuestro problema es en realidad bastante simple, y aprovecha la clase Freezable. El propósito principal de esta clase es definir los 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 conozco el mecanismo exacto que permite este comportamiento, pero vamos a aprovecharlo para que nuestro trabajo de enlace ...
La idea es crear una clase (lo llamé BindingProxy por razones que deberían ser evidentes 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 del 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 (fácilmente accesible con StaticResource) como la Fuente para el 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.
En el enlace WPF RelativeSource
exponen tres properties
para establecer:
1. Modo: esta es una enum
que podría tener cuatro valores:
a. PreviousData (
value=0
): Asigna el valor anterior de laproperty
a la unidasegundo. TemplatedParent (
value=1
): se usa cuando se definen lastemplates
de cualquier control y se desean vincular a un valor / propiedad delcontrol
.Por ejemplo, defina
ControlTemplate
:
<ControlTemplate>
<CheckBox IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</ControlTemplate>
do. Self (
value=2
): cuando queremos vincularnos desde unself
o unaproperty
de self.Por ejemplo: envíe el estado marcado de la
checkbox
decheckbox
comoCommandParameter
mientras configura elCommand
enCheckBox
<CheckBox ...... CommandParameter="{Binding RelativeSource={RelativeSource Self},Path=IsChecked}" />
re. FindAncestor (
value=3
): cuando se desea enlazar desde uncontrol
principal enVisual Tree
.Por ejemplo: enlazar una
checkbox
decheckbox
en losrecords
si hay unagrid
, si lacheckbox
header
está marcada
<CheckBox IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid}}, Path=DataContext.IsHeaderChecked, Mode=TwoWay}" />
2. AncestorType: cuando el modo es FindAncestor
, defina qué tipo de antepasado
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid}}
3. AncestorLevel: cuando el modo es FindAncestor
entonces qué nivel de antepasado (si hay dos tipos iguales de padre en visual tree
)
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid, AncestorLevel=1}}
Más arriba están todos los casos de uso para el
RelativeSource binding
.
Binding RelativeSource={
RelativeSource Mode=FindAncestor, AncestorType={x:Type ItemType}
}
...
El atributo predeterminado de RelativeSource
es la propiedad Mode
. Aquí se proporciona un conjunto completo de valores válidos ( de MSDN ):
Datos anteriores Le permite vincular el elemento de datos anterior (no el control que contiene el elemento de datos) en la lista de elementos de datos que se muestran.
TemplatedParent Se refiere al elemento al que se aplica la plantilla (en la que existe el elemento enlazado a datos). Esto es similar a establecer una TemplateBindingExtension y solo es aplicable si el enlace está dentro de una plantilla.
Self Se refiere al elemento en el que está configurando el enlace y le permite enlazar una propiedad de ese elemento a otra propiedad en el mismo elemento.
FindAncestor Se refiere al antepasado en la cadena principal 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.