Parte 4. Conceptos básicos del enlace de datos

Download SampleDescargar el ejemplo

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:

View-to-View Bindings

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

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.

Backwards Bindings

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:

Binding to a Collection

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:

Binding to a Collection with a DataTemplate

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:

Binding to a Collection with a DataTemplate and Converters

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.