Parte 4. Conceptos básicos del enlace de datos
Los enlaces de datos permiten vincular las propiedades de dos objetos para que un cambio en uno de ellos provoque un cambio en el otro. Se trata de una herramienta muy valiosa y, aunque los enlaces de datos se pueden definir completamente con código, XAML proporciona métodos abreviados y comodidad. Por lo tanto, una de las extensiones de marcado más importantes de Xamarin.Forms es la extensión Binding.
Enlaces de datos
Los enlaces de datos conectan las propiedades de dos objetos, denominadas origen y destino. En el código, se requieren dos pasos: la propiedad BindingContext
del objeto de destino se debe establecer en el objeto de origen y se debe llamar al método SetBinding
(que se usa a menudo junto con la clase Binding
) en el objeto de destino para enlazar una propiedad de ese objeto a una propiedad del objeto de origen.
La propiedad de destino debe ser una propiedad enlazable, lo que significa que el objeto de destino debe derivar de BindableObject
. La documentación en línea de Xamarin.Forms indica qué propiedades son propiedades enlazables. Una propiedad de tipo Label
, como Text
, está asociada a la propiedad enlazable TextProperty
.
En el marcado, también debe realizar los mismos dos pasos necesarios en el código, excepto que la extensión de marcado Binding
ocupa el lugar de la llamada a SetBinding
y la clase Binding
.
Sin embargo, cuando defines enlaces de datos en XAML, hay varias formas de establecer el BindingContext
del objeto de destino. A veces se establece desde el archivo de código subyacente, a veces con una extensión de marcado StaticResource
o x:Static
, y a veces como contenido de etiquetas de elemento de propiedad BindingContext
.
Los enlaces se usan con más frecuencia para conectar los objetos visuales de un programa con un modelo de datos subyacente, normalmente en una implementación de la arquitectura de una aplicación MVVM (Modelo-Vista-Modelo de vista), como se describe en Parte 5. De los enlaces de datos a MVVM, aunque son posibles otros escenarios.
Enlaces de vista a vista
Puedes definir enlaces de datos para vincular propiedades de dos vistas en la misma página. En este caso, estableces el BindingContext
del objeto de destino con la extensión de marcado x:Reference
.
Este es un archivo XAML que contiene un elemento Slider
y dos vistas Label
, una de las cuales se ha girado el valor de Slider
y otra que muestra el valor de Slider
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XamlSamples.SliderBindingsPage"
Title="Slider Bindings Page">
<StackLayout>
<Label Text="ROTATION"
BindingContext="{x:Reference Name=slider}"
Rotation="{Binding Path=Value}"
FontAttributes="Bold"
FontSize="Large"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
<Slider x:Name="slider"
Maximum="360"
VerticalOptions="CenterAndExpand" />
<Label BindingContext="{x:Reference slider}"
Text="{Binding Value, StringFormat='The angle is {0:F0} degrees'}"
FontAttributes="Bold"
FontSize="Large"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
</StackLayout>
</ContentPage>
Slider
contiene un atributo x:Name
al que hacen referencia las dos vistas Label
con la extensión de marcado x:Reference
.
La extensión de enlace x:Reference
define una propiedad denominada Name
para establecer el nombre del elemento referenciado, en este caso slider
. Sin embargo, la clase ReferenceExtension
que define la extensión de marcado x:Reference
también define un atributo ContentProperty
para Name
, lo que significa que no se requiere de forma explícita. Para variar, el primer elemento x:Reference
incluye "Name=" pero el segundo no:
BindingContext="{x:Reference Name=slider}"
…
BindingContext="{x:Reference slider}"
La propia extensión de marcado Binding
puede tener varias propiedades, al igual que la clase BindingBase
y Binding
. El ContentProperty
para Binding
es Path
, pero la parte "Path=" de la extensión de marcado puede omitirse si la ruta de acceso es el primer elemento de la extensión de marcado Binding
. El primer ejemplo tiene "Path=", pero el segundo ejemplo lo omite:
Rotation="{Binding Path=Value}"
…
Text="{Binding Value, StringFormat='The angle is {0:F0} degrees'}"
Las propiedades pueden estar todas en una línea o separadas en varias líneas:
Text="{Binding Value,
StringFormat='The angle is {0:F0} degrees'}"
Haga lo que sea conveniente.
Observe la propiedad StringFormat
en la segunda extensión de marcado Binding
. En Xamarin.Forms, los enlaces no realizan ninguna conversión de tipos implícita y, si necesita mostrar un objeto que no es de tipo cadena como una cadena, debe proporcionar un convertidor de tipos o usar StringFormat
. En segundo plano, el método estático String.Format
se usa para implementar StringFormat
. Esto podría ser un problema, ya que las especificaciones de formato de .NET implican llaves, que también se usan para delimitar las extensiones de marcado. Esto crea el riesgo de confundir al analizador de XAML. Para evitarlo, coloque toda la cadena de formato entre comillas simples:
Text="{Binding Value, StringFormat='The angle is {0:F0} degrees'}"
Este es el programa en ejecución:
Modo de enlace
Una sola vista puede tener enlaces de datos en varias de sus propiedades. Sin embargo, cada vista solo puede tener un BindingContext
, por lo que varios enlaces de datos en esa vista deben hacer referencia a todas las propiedades del mismo objeto.
La solución a este y otros problemas implica la propiedad Mode
, que se establece en un miembro de la enumeración BindingMode
:
Default
OneWay
: los valores se transfieren del origen al destino.OneWayToSource
: los valores se transfieren del destino al origen.TwoWay
: los valores se transfieren de ambas maneras entre el origen y el destino.OneTime
: los datos van del origen al destino, pero solo cuando cambia elBindingContext
En el siguiente programa, se muestra un uso común de los modos de enlace OneWayToSource
y TwoWay
. Las cuatro vistas del elemento Slider
están diseñadas para controlar las propiedades Scale
, Rotate
, RotateX
y RotateY
de un elemento Label
. Al principio, parece que estas cuatro propiedades de Label
deben ser destinos del enlace de datos porque cada una se establece mediante Slider
. Sin embargo, BindingContext
de Label
solo puede ser un objeto y hay cuatro controles deslizantes diferentes.
Por ese motivo, todos los enlaces se establecen de maneras aparentemente inversas: el elemento BindingContext
de cada uno de los cuatro controles deslizantes se establece en el elemento Label
y los enlaces se establecen en las propiedades Value
de los controles deslizantes. Mediante el uso de los modos OneWayToSource
y TwoWay
, estas propiedades Value
pueden establecer las propiedades de origen, que son las propiedades Scale
, Rotate
, RotateX
y RotateY
del elemento Label
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XamlSamples.SliderTransformsPage"
Padding="5"
Title="Slider Transforms Page">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Scaled and rotated Label -->
<Label x:Name="label"
Text="TEXT"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
<!-- Slider and identifying Label for Scale -->
<Slider x:Name="scaleSlider"
BindingContext="{x:Reference label}"
Grid.Row="1" Grid.Column="0"
Maximum="10"
Value="{Binding Scale, Mode=TwoWay}" />
<Label BindingContext="{x:Reference scaleSlider}"
Text="{Binding Value, StringFormat='Scale = {0:F1}'}"
Grid.Row="1" Grid.Column="1"
VerticalTextAlignment="Center" />
<!-- Slider and identifying Label for Rotation -->
<Slider x:Name="rotationSlider"
BindingContext="{x:Reference label}"
Grid.Row="2" Grid.Column="0"
Maximum="360"
Value="{Binding Rotation, Mode=OneWayToSource}" />
<Label BindingContext="{x:Reference rotationSlider}"
Text="{Binding Value, StringFormat='Rotation = {0:F0}'}"
Grid.Row="2" Grid.Column="1"
VerticalTextAlignment="Center" />
<!-- Slider and identifying Label for RotationX -->
<Slider x:Name="rotationXSlider"
BindingContext="{x:Reference label}"
Grid.Row="3" Grid.Column="0"
Maximum="360"
Value="{Binding RotationX, Mode=OneWayToSource}" />
<Label BindingContext="{x:Reference rotationXSlider}"
Text="{Binding Value, StringFormat='RotationX = {0:F0}'}"
Grid.Row="3" Grid.Column="1"
VerticalTextAlignment="Center" />
<!-- Slider and identifying Label for RotationY -->
<Slider x:Name="rotationYSlider"
BindingContext="{x:Reference label}"
Grid.Row="4" Grid.Column="0"
Maximum="360"
Value="{Binding RotationY, Mode=OneWayToSource}" />
<Label BindingContext="{x:Reference rotationYSlider}"
Text="{Binding Value, StringFormat='RotationY = {0:F0}'}"
Grid.Row="4" Grid.Column="1"
VerticalTextAlignment="Center" />
</Grid>
</ContentPage>
Los enlaces de tres de las vistas Slider
son OneWayToSource
, lo que significa que el valor de Slider
produce un cambio en la propiedad de su BindingContext
que es el elemento Label
denominado label
. Estas tres vistas del elemento Slider
provocan cambios en las propiedades Rotate
, RotateX
y RotateY
del elemento Label
.
No obstante, el enlace predeterminado de la propiedad Scale
es TwoWay
. Esto se debe a que la propiedad Scale
tiene un valor predeterminado de 1 y el uso de un enlace TwoWay
hace que el valor inicial de Slider
se establezca en 1 en lugar de 0. Si ese enlace fuera OneWayToSource
, la propiedad Scale
se establecería inicialmente en 0 a partir del valor predeterminado de Slider
. El elemento Label
no sería visible y esto podría causar cierta confusión al usuario.
Nota:
La clase VisualElement
también tiene propiedades ScaleX
y ScaleY
, que escalan el objeto VisualElement
en el eje X y el eje Y respectivamente.
Enlaces y colecciones
Nada ilustra mejor la eficacia de los enlaces de datos y XAML que un elemento ListView
con plantilla.
ListView
define una propiedad ItemsSource
de tipo IEnumerable
, y muestra los elementos de esa colección. Estos elementos pueden ser objetos de cualquier tipo. De forma predeterminada, ListView
usa el método ToString
de cada elemento para mostrar ese elemento. A veces es esto lo que quieres, pero, en muchos casos, ToString
devuelve solo el nombre de clase completo del objeto.
Sin embargo, los elementos de la colección ListView
se pueden mostrar de la forma que quieras mediante el uso de una plantilla, lo que implica una clase que deriva de Cell
. La plantilla se clona para cada elemento de ListView
, y los enlaces de datos que se han establecido en la plantilla se transfieren a los clones individuales.
Con mucha frecuencia, querrá crear una celda personalizada para estos elementos mediante la clase ViewCell
. Este proceso es algo desordenado en el código, pero en XAML se vuelve muy sencillo.
En el proyecto XamlSamples, se incluye una clase llamada NamedColor
. Cada objeto NamedColor
tiene las propiedades Name
y FriendlyName
de tipo string
, y la propiedad Color
de tipo Color
. Además, NamedColor
tiene 141 campos estáticos de solo lectura de tipo Color
correspondientes a los colores definidos en la clase Color
de Xamarin.Forms. Un constructor estático crea una colección IEnumerable<NamedColor>
que contiene los objetos NamedColor
correspondientes a estos campos estáticos y los asigna a su propiedad estática pública All
.
Establecer la propiedad estática NamedColor.All
en el elemento ItemsSource
de un elemento ListView
es fácil mediante la extensión de marcado x:Static
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
x:Class="XamlSamples.ListViewDemoPage"
Title="ListView Demo Page">
<ListView ItemsSource="{x:Static local:NamedColor.All}" />
</ContentPage>
La presentación resultante establece que los elementos son verdaderamente de tipo XamlSamples.NamedColor
:
No es mucha información, pero el elemento ListView
se puede desplazar y seleccionar.
Para definir una plantilla para los elementos, querrá dividir la propiedad ItemTemplate
como un elemento de propiedad y establecerla en un elemento DataTemplate
, que a continuación hace referencia a ViewCell
. Para la propiedad View
de ViewCell
, puede definir un diseño de una o varias vistas para mostrar cada elemento. A continuación, se incluye un ejemplo sencillo:
<ListView ItemsSource="{x:Static local:NamedColor.All}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.View>
<Label Text="{Binding FriendlyName}" />
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Nota:
El origen de enlace de las celdas y sus elementos secundarios es la colección ListView.ItemsSource
.
El elemento Label
se establece en la propiedad View
del elemento ViewCell
. (Las etiquetas ViewCell.View
no son necesarias porque la propiedad View
es la propiedad de contenido de ViewCell
). Este marcado muestra la propiedad FriendlyName
de cada objeto NamedColor
:
Mucho mejor. Ahora, solo falta retocar la plantilla de elemento con más información y el color real. Para admitir esta plantilla, se han definido algunos valores y objetos en el diccionario de recursos de la página:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.ListViewDemoPage"
Title="ListView Demo Page">
<ContentPage.Resources>
<ResourceDictionary>
<OnPlatform x:Key="boxSize"
x:TypeArguments="x:Double">
<On Platform="iOS, Android, UWP" Value="50" />
</OnPlatform>
<OnPlatform x:Key="rowHeight"
x:TypeArguments="x:Int32">
<On Platform="iOS, Android, UWP" Value="60" />
</OnPlatform>
<local:DoubleToIntConverter x:Key="intConverter" />
</ResourceDictionary>
</ContentPage.Resources>
<ListView ItemsSource="{x:Static local:NamedColor.All}"
RowHeight="{StaticResource rowHeight}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Padding="5, 5, 0, 5"
Orientation="Horizontal"
Spacing="15">
<BoxView WidthRequest="{StaticResource boxSize}"
HeightRequest="{StaticResource boxSize}"
Color="{Binding Color}" />
<StackLayout Padding="5, 0, 0, 0"
VerticalOptions="Center">
<Label Text="{Binding FriendlyName}"
FontAttributes="Bold"
FontSize="Medium" />
<StackLayout Orientation="Horizontal"
Spacing="0">
<Label Text="{Binding Color.R,
Converter={StaticResource intConverter},
ConverterParameter=255,
StringFormat='R={0:X2}'}" />
<Label Text="{Binding Color.G,
Converter={StaticResource intConverter},
ConverterParameter=255,
StringFormat=', G={0:X2}'}" />
<Label Text="{Binding Color.B,
Converter={StaticResource intConverter},
ConverterParameter=255,
StringFormat=', B={0:X2}'}" />
</StackLayout>
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage>
Observe el uso de OnPlatform
para definir el tamaño de un elemento BoxView
y el alto de las filas del elemento ListView
. Aunque los valores de todas las plataformas son los mismos, el marcado podría adaptarse fácilmente a otros valores para ajustar la presentación.
Enlace de convertidores de valores
El archivo XAML de la demo de ListView anterior muestra las propiedades individuales R
, G
y B
de la estructura Color
de Xamarin.Forms. Estas propiedades son de tipo double
y van de 0 a 1. Si quieres mostrar los valores hexadecimales, no puedes usar StringFormat
solo con una especificación de formato "X2". Esto solo funciona para enteros y además, los valores double
deben multiplicarse por 255.
Este pequeño problema se resolvió con un convertidor de valores, también llamado convertidor de enlace. Se trata de una clase que implementa la interfaz IValueConverter
, lo que significa que tiene dos métodos denominados Convert
y ConvertBack
. Se llama al método Convert
cuando se transfiere un valor del origen al destino; se llama al método ConvertBack
para transferencias del destino al origen en enlaces OneWayToSource
o TwoWay
:
using System;
using System.Globalization;
using Xamarin.Forms;
namespace XamlSamples
{
class DoubleToIntConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
double multiplier;
if (!Double.TryParse(parameter as string, out multiplier))
multiplier = 1;
return (int)Math.Round(multiplier * (double)value);
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
double divider;
if (!Double.TryParse(parameter as string, out divider))
divider = 1;
return ((double)(int)value) / divider;
}
}
}
El método ConvertBack
no desempeña un papel en este programa porque los enlaces son solo de una dirección, del origen al destino.
Un enlace hace referencia a un convertidor de enlaces con la propiedad Converter
. Un convertidor de enlaces también puede aceptar un parámetro especificado con la propiedad ConverterParameter
. Para obtener cierta versatilidad, se especifica así el multiplicador. El convertidor de enlaces comprueba si el parámetro del convertidor tiene un valor double
válido.
Se crea una instancia del convertidor en el diccionario de recursos para que se pueda compartir entre varios enlaces:
<local:DoubleToIntConverter x:Key="intConverter" />
Tres enlaces de datos hacen referencia a esta única instancia. Observe que la extensión de marcado Binding
contiene una extensión de marcado StaticResource
insertada:
<Label Text="{Binding Color.R,
Converter={StaticResource intConverter},
ConverterParameter=255,
StringFormat='R={0:X2}'}" />
Este es el resultado:
El elemento ListView
es bastante sofisticado en el control de los cambios que se puedan producir dinámicamente en los datos subyacentes, pero solo si realiza determinados pasos. Si la colección de elementos asignados a la propiedad ItemsSource
del elemento ListView
cambia durante el tiempo de ejecución (es decir, si los elementos se pueden agregar o quitar de la colección), utilice una clase ObservableCollection
para estos elementos. ObservableCollection
implementa la interfaz INotifyCollectionChanged
, y ListView
instalará un controlador para el evento CollectionChanged
.
Si las propiedades de los elementos en cuestión cambian durante el tiempo de ejecución, los elementos de la colección deben implementar la interfaz INotifyPropertyChanged
y señalar los cambios en los valores de las propiedades con el evento PropertyChanged
. Esto se muestra en la siguiente parte de esta serie, la Parte 5. De los enlaces de datos a MVVM.
Resumen
Los enlaces de datos proporcionan un mecanismo eficaz para vincular propiedades entre dos objetos dentro de una página o entre objetos visuales y datos subyacentes. Pero cuando la aplicación comienza a trabajar con orígenes de datos, comienza a surgir un patrón popular de arquitectura de aplicaciones como paradigma útil. Esto se trata en la Parte 5. De los enlaces de datos a MVVM.