Parte 5. De enlaces de datos a MVVM
El patrón de arquitectura Modelo-Vista-Modelo de vista (MVVM) se inventó teniendo en cuenta XAML. El patrón aplica una separación entre tres capas de software: la interfaz de usuario de XAML, denominada Vista, los datos subyacentes, denominados Modelo, y un intermediario entre Vista y Modelo, denominado Modelo de vista. La Vista y el Modelo de vista a menudo se conectan a través de enlaces de datos definidos en el archivo XAML. El contexto de enlace (BindingContext) para la Vista suele ser una instancia de Modelo de vista.
Un Modelo de vista simple
Como introducción a los Modelos de vista, echemos un vistazo primero a un programa sin uno.
Anteriormente vimos cómo definir una nueva declaración de espacio de nombres XML para permitir que un archivo XAML haga referencia a clases en otros ensamblados. Este es un programa que define una declaración de espacio de nombres XML para el espacio de nombres System
:
xmlns:sys="clr-namespace:System;assembly=netstandard"
El programa puede usar x:Static
para obtener la fecha y hora actuales de la propiedad estática DateTime.Now
y establecer ese valor DateTime
a BindingContext
en StackLayout
:
<StackLayout BindingContext="{x:Static sys:DateTime.Now}" …>
BindingContext
es una propiedad especial: cuando se establece BindingContext
en un elemento, todos los elementos secundarios de ese elemento lo heredan. Esto significa que todos los elementos secundarios de StackLayout
tienen este mismo BindingContext
y pueden contener enlaces simples a propiedades de ese objeto.
En el programa One-Shot DateTime, dos de los elementos secundarios contienen enlaces a propiedades de ese valor de DateTime
, pero otros dos elementos secundarios contienen enlaces que parecen faltar en una ruta de enlace. Esto significa que el propio valor DateTime
se usa para StringFormat
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard"
x:Class="XamlSamples.OneShotDateTimePage"
Title="One-Shot DateTime Page">
<StackLayout BindingContext="{x:Static sys:DateTime.Now}"
HorizontalOptions="Center"
VerticalOptions="Center">
<Label Text="{Binding Year, StringFormat='The year is {0}'}" />
<Label Text="{Binding StringFormat='The month is {0:MMMM}'}" />
<Label Text="{Binding Day, StringFormat='The day is {0}'}" />
<Label Text="{Binding StringFormat='The time is {0:T}'}" />
</StackLayout>
</ContentPage>
El problema es que la fecha y la hora se establecen una vez cuando se compila por primera vez la página y nunca cambian:
Un archivo XAML puede mostrar un reloj que siempre muestra la hora actual, pero necesita código complementario. Al pensar en términos de MVVM, las clases Modelo y Modelo de vista se escriben completamente en el código. La Vista suele ser un archivo XAML que hace referencia a las propiedades definidas en Modelo de vista a través de enlaces de datos.
Un Modelo adecuado es independiente del Modelo de vista, y un Modelo de vista adecuado es independiente de la Vista. Sin embargo, a menudo un programador adapta los tipos de datos expuestos por el Modelo de vista a los tipos de datos asociados a interfaces de usuario concretas. Por ejemplo, si un Modelo tiene acceso a una base de datos que contiene cadenas ASCII de 8 bits de caracteres, el Modelo de vista tendrá que convertir entre esas cadenas a cadenas Unicode para dar cabida al uso exclusivo de Unicode en la interfaz de usuario.
En ejemplos sencillos de MVVM (como los que se muestran aquí), a menudo no hay ningún Modelo y el patrón implica solo una Vista y un Modelo de vista vinculados con enlaces de datos.
Este es un Modelo de vista para un reloj con una sola propiedad denominada DateTime
, que actualiza la propiedad DateTime
cada segundo:
using System;
using System.ComponentModel;
using Xamarin.Forms;
namespace XamlSamples
{
class ClockViewModel : INotifyPropertyChanged
{
DateTime dateTime;
public event PropertyChangedEventHandler PropertyChanged;
public ClockViewModel()
{
this.DateTime = DateTime.Now;
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
this.DateTime = DateTime.Now;
return true;
});
}
public DateTime DateTime
{
set
{
if (dateTime != value)
{
dateTime = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("DateTime"));
}
}
}
get
{
return dateTime;
}
}
}
}
Los Modelos de vista generalmente implementan la interfaz INotifyPropertyChanged
, lo cual significa que la clase activa un evento PropertyChanged
cada vez que cambia una de sus propiedades. El mecanismo de enlace de datos de Xamarin.Forms asocia un controlador a este evento PropertyChanged
para que se pueda notificar cuando cambia una propiedad y mantener el destino actualizado con el nuevo valor.
Un reloj basado en este Modelo de vista puede ser tan sencillo como lo siguiente:
<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.ClockPage"
Title="Clock Page">
<Label Text="{Binding DateTime, StringFormat='{0:T}'}"
FontSize="Large"
HorizontalOptions="Center"
VerticalOptions="Center">
<Label.BindingContext>
<local:ClockViewModel />
</Label.BindingContext>
</Label>
</ContentPage>
Observe cómo se establece ClockViewModel
en BindingContext
deLabel
mediante etiquetas de elemento de propiedad. Como alternativa, puede crear instancias de ClockViewModel
en una colección Resources
y establecerla en BindingContext
a través de una extensión de marcado StaticResource
. O bien, el archivo de código subyacente puede crear una instancia de Modelo de vista.
La extensión de marcado Binding
en la propiedad Text
de los formatos Label
a la propiedad de DateTime
. Esta es la pantalla:
También es posible acceder a las propiedades individuales de la propiedad DateTime
del Modelo de vista separando las propiedades con puntos:
<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >
MVVM interactivo
MVVM se suele usar con enlaces de datos bidireccionales para una vista interactiva basada en un modelo de datos subyacente.
Esta es una clase denominada HslViewModel
que convierte un valor Color
en los valores Hue
, Saturation
y Luminosity
y viceversa:
using System;
using System.ComponentModel;
using Xamarin.Forms;
namespace XamlSamples
{
public class HslViewModel : INotifyPropertyChanged
{
double hue, saturation, luminosity;
Color color;
public event PropertyChangedEventHandler PropertyChanged;
public double Hue
{
set
{
if (hue != value)
{
hue = value;
OnPropertyChanged("Hue");
SetNewColor();
}
}
get
{
return hue;
}
}
public double Saturation
{
set
{
if (saturation != value)
{
saturation = value;
OnPropertyChanged("Saturation");
SetNewColor();
}
}
get
{
return saturation;
}
}
public double Luminosity
{
set
{
if (luminosity != value)
{
luminosity = value;
OnPropertyChanged("Luminosity");
SetNewColor();
}
}
get
{
return luminosity;
}
}
public Color Color
{
set
{
if (color != value)
{
color = value;
OnPropertyChanged("Color");
Hue = value.Hue;
Saturation = value.Saturation;
Luminosity = value.Luminosity;
}
}
get
{
return color;
}
}
void SetNewColor()
{
Color = Color.FromHsla(Hue, Saturation, Luminosity);
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Los cambios realizados en las propiedades Hue
, Saturation
y Luminosity
hacen que la propiedad Color
cambie y los cambios en Color
hacen que cambien las otras tres propiedades. Esto puede parecer un bucle infinito, salvo que la clase no invoca el evento PropertyChanged
a menos que la propiedad haya cambiado. Esto pone punto final a lo que de otro modo sería un bucle de retroalimentación incontrolable.
El siguiente archivo XAML contiene un BoxView
cuya propiedad Color
está enlazada a la propiedad Color
del Modelo de vista, y tres Slider
y tres vistas Label
enlazadas a las propiedades Hue
, Saturation
y Luminosity
:
<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.HslColorScrollPage"
Title="HSL Color Scroll Page">
<ContentPage.BindingContext>
<local:HslViewModel Color="Aqua" />
</ContentPage.BindingContext>
<StackLayout Padding="10, 0">
<BoxView Color="{Binding Color}"
VerticalOptions="FillAndExpand" />
<Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Hue, Mode=TwoWay}" />
<Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Saturation, Mode=TwoWay}" />
<Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Luminosity, Mode=TwoWay}" />
</StackLayout>
</ContentPage>
El enlace en cada Label
es el valor predeterminado OneWay
. Solo necesita mostrar el valor. Pero el enlace en cada Slider
es TwoWay
. Esto permite que Slider
se inicialice a partir del Modelo de vista. Observe que la propiedad Color
está establecida en Aqua
cuando se crea una instancia de Modelo de vista. Pero un cambio en Slider
también debe establecer un nuevo valor para la propiedad en el Modelo de vista, que a continuación calcula un nuevo color.
Comandos con Modelos de vista
En muchos casos, el patrón MVVM está restringido a la manipulación de elementos de datos: objetos de interfaz de usuario en los objetos de datos paralelos de Vista en el Modelo de vista.
Sin embargo, a veces la Vista debe contener botones que desencadenen varias acciones en el Modelo de vista. Pero el Modelo de vista no debe contener controladores Clicked
para los botones porque eso vincularía el Modelo de vista a un paradigma de interfaz de usuario determinado.
Para permitir que los Modelos de vista sean más independientes respecto a determinados objetos de interfaz de usuario, pero seguir permitiendo llamar a métodos dentro del Modelo de vista, existe una interfaz de comandos. Esta interfaz de comandos es compatible con los siguientes elementos de Xamarin.Forms:
Button
MenuItem
ToolbarItem
SearchBar
TextCell
(y por lo tanto tambiénImageCell
)ListView
TapGestureRecognizer
Con la excepción del elemento SearchBar
y ListView
, estos elementos definen dos propiedades:
Command
de tipoSystem.Windows.Input.ICommand
CommandParameter
de tipoObject
SearchBar
define las propiedades SearchCommand
y SearchCommandParameter
, mientras que ListView
define una propiedad RefreshCommand
de tipo ICommand
.
La interfaz de ICommand
declara dos métodos y un evento:
void Execute(object arg)
bool CanExecute(object arg)
event EventHandler CanExecuteChanged
El Modelo de vista puede definir propiedades de tipo ICommand
. Después puedes enlazar estas propiedades a la propiedad Command
de cada Button
u otro elemento, o quizás una vista personalizada que implemente esta interfaz. Opcionalmente, puede establecer la propiedad CommandParameter
para identificar objetos Button
individuales (u otros elementos) enlazados a esta propiedad de Modelo de vista. Internamente, Button
llama al método Execute
cada vez que el usuario pulsa en Button
, pasando al método Execute
su CommandParameter
.
El método CanExecute
y el evento CanExecuteChanged
se usan para los casos en los que una pulsación Button
podría ser actualmente no válida, en cuyo caso Button
debe deshabilitarse. Button
llama a CanExecute
cuando la propiedad Command
se establece por primera vez y cada vez que se desencadena el evento CanExecuteChanged
. Si CanExecute
devuelve false
, Button
se deshabilita y no genera llamadas Execute
.
Para obtener ayuda para agregar comandos a los Modelos de vista, Xamarin.Forms define dos clases que implementan ICommand
: Command
y Command<T>
, donde T
es el tipo de los argumentos en Execute
y CanExecute
. Estas dos clases definen varios constructores más un método ChangeCanExecute
al que el Modelo de vista puede llamar para forzar el objeto Command
para desencadenar el evento CanExecuteChanged
.
Este es un Modelo de vista para un teclado simple destinado a escribir números de teléfono. Observe que el método Execute
y CanExecute
se definen como funciones lambda directamente en el constructor:
using System;
using System.ComponentModel;
using System.Windows.Input;
using Xamarin.Forms;
namespace XamlSamples
{
class KeypadViewModel : INotifyPropertyChanged
{
string inputString = "";
string displayText = "";
char[] specialChars = { '*', '#' };
public event PropertyChangedEventHandler PropertyChanged;
// Constructor
public KeypadViewModel()
{
AddCharCommand = new Command<string>((key) =>
{
// Add the key to the input string.
InputString += key;
});
DeleteCharCommand = new Command(() =>
{
// Strip a character from the input string.
InputString = InputString.Substring(0, InputString.Length - 1);
},
() =>
{
// Return true if there's something to delete.
return InputString.Length > 0;
});
}
// Public properties
public string InputString
{
protected set
{
if (inputString != value)
{
inputString = value;
OnPropertyChanged("InputString");
DisplayText = FormatText(inputString);
// Perhaps the delete button must be enabled/disabled.
((Command)DeleteCharCommand).ChangeCanExecute();
}
}
get { return inputString; }
}
public string DisplayText
{
protected set
{
if (displayText != value)
{
displayText = value;
OnPropertyChanged("DisplayText");
}
}
get { return displayText; }
}
// ICommand implementations
public ICommand AddCharCommand { protected set; get; }
public ICommand DeleteCharCommand { protected set; get; }
string FormatText(string str)
{
bool hasNonNumbers = str.IndexOfAny(specialChars) != -1;
string formatted = str;
if (hasNonNumbers || str.Length < 4 || str.Length > 10)
{
}
else if (str.Length < 8)
{
formatted = String.Format("{0}-{1}",
str.Substring(0, 3),
str.Substring(3));
}
else
{
formatted = String.Format("({0}) {1}-{2}",
str.Substring(0, 3),
str.Substring(3, 3),
str.Substring(6));
}
return formatted;
}
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Este Modelo de vista supone que la propiedad AddCharCommand
está enlazada a la propiedad Command
de varios botones (o cualquier otra cosa que tenga una interfaz de comando), cada una de las cuales se identifica mediante CommandParameter
. Estos botones agregan caracteres a una propiedad InputString
, a la que a luego se da formato como un número de teléfono para la propiedad DisplayText
.
También hay una segunda propiedad de tipo ICommand
denominada DeleteCharCommand
. Esto está enlazado a un botón de retroceso, pero el botón debe deshabilitarse si no hay caracteres que eliminar.
El teclado siguiente podría ser más sofisticado visualmente. Alternativamente, el marcado se ha reducido a un mínimo para demostrar con más claridad el uso de la interfaz de comandos:
<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.KeypadPage"
Title="Keypad Page">
<Grid HorizontalOptions="Center"
VerticalOptions="Center">
<Grid.BindingContext>
<local:KeypadViewModel />
</Grid.BindingContext>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80" />
<ColumnDefinition Width="80" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<!-- Internal Grid for top row of items -->
<Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Frame Grid.Column="0"
OutlineColor="Accent">
<Label Text="{Binding DisplayText}" />
</Frame>
<Button Text="⇦"
Command="{Binding DeleteCharCommand}"
Grid.Column="1"
BorderWidth="0" />
</Grid>
<Button Text="1"
Command="{Binding AddCharCommand}"
CommandParameter="1"
Grid.Row="1" Grid.Column="0" />
<Button Text="2"
Command="{Binding AddCharCommand}"
CommandParameter="2"
Grid.Row="1" Grid.Column="1" />
<Button Text="3"
Command="{Binding AddCharCommand}"
CommandParameter="3"
Grid.Row="1" Grid.Column="2" />
<Button Text="4"
Command="{Binding AddCharCommand}"
CommandParameter="4"
Grid.Row="2" Grid.Column="0" />
<Button Text="5"
Command="{Binding AddCharCommand}"
CommandParameter="5"
Grid.Row="2" Grid.Column="1" />
<Button Text="6"
Command="{Binding AddCharCommand}"
CommandParameter="6"
Grid.Row="2" Grid.Column="2" />
<Button Text="7"
Command="{Binding AddCharCommand}"
CommandParameter="7"
Grid.Row="3" Grid.Column="0" />
<Button Text="8"
Command="{Binding AddCharCommand}"
CommandParameter="8"
Grid.Row="3" Grid.Column="1" />
<Button Text="9"
Command="{Binding AddCharCommand}"
CommandParameter="9"
Grid.Row="3" Grid.Column="2" />
<Button Text="*"
Command="{Binding AddCharCommand}"
CommandParameter="*"
Grid.Row="4" Grid.Column="0" />
<Button Text="0"
Command="{Binding AddCharCommand}"
CommandParameter="0"
Grid.Row="4" Grid.Column="1" />
<Button Text="#"
Command="{Binding AddCharCommand}"
CommandParameter="#"
Grid.Row="4" Grid.Column="2" />
</Grid>
</ContentPage>
La propiedad Command
del primer Button
que aparece en este marcado está enlazada a DeleteCharCommand
; el resto se enlaza a AddCharCommand
con un CommandParameter
que es el mismo que el carácter que aparece en la cara del Button
. Este es el programa en acción:
Invocar métodos asincrónicos
Los comandos también pueden invocar métodos asincrónicos. Esto se logra mediante el uso de las palabras clave async
y await
al especificar el método Execute
:
DownloadCommand = new Command (async () => await DownloadAsync ());
Esto indica que el método DownloadAsync
es Task
y se debe esperar:
async Task DownloadAsync ()
{
await Task.Run (() => Download ());
}
void Download ()
{
...
}
Implementar un menú de navegación
El programa de ejemplo que contiene todo el código fuente de esta serie de artículos usa un ViewModel para su página principal. El Modelo de vista es una definición de una clase corta con tres propiedades denominadas Type
, Title
y Description
que contienen el tipo de cada una de las páginas de ejemplo, un título y una descripción breve. Además, el Modelo de vista define una propiedad estática denominada All
que es una colección de todas las páginas del programa:
public class PageDataViewModel
{
public PageDataViewModel(Type type, string title, string description)
{
Type = type;
Title = title;
Description = description;
}
public Type Type { private set; get; }
public string Title { private set; get; }
public string Description { private set; get; }
static PageDataViewModel()
{
All = new List<PageDataViewModel>
{
// Part 1. Getting Started with XAML
new PageDataViewModel(typeof(HelloXamlPage), "Hello, XAML",
"Display a Label with many properties set"),
new PageDataViewModel(typeof(XamlPlusCodePage), "XAML + Code",
"Interact with a Slider and Button"),
// Part 2. Essential XAML Syntax
new PageDataViewModel(typeof(GridDemoPage), "Grid Demo",
"Explore XAML syntax with the Grid"),
new PageDataViewModel(typeof(AbsoluteDemoPage), "Absolute Demo",
"Explore XAML syntax with AbsoluteLayout"),
// Part 3. XAML Markup Extensions
new PageDataViewModel(typeof(SharedResourcesPage), "Shared Resources",
"Using resource dictionaries to share resources"),
new PageDataViewModel(typeof(StaticConstantsPage), "Static Constants",
"Using the x:Static markup extensions"),
new PageDataViewModel(typeof(RelativeLayoutPage), "Relative Layout",
"Explore XAML markup extensions"),
// Part 4. Data Binding Basics
new PageDataViewModel(typeof(SliderBindingsPage), "Slider Bindings",
"Bind properties of two views on the page"),
new PageDataViewModel(typeof(SliderTransformsPage), "Slider Transforms",
"Use Sliders with reverse bindings"),
new PageDataViewModel(typeof(ListViewDemoPage), "ListView Demo",
"Use a ListView with data bindings"),
// Part 5. From Data Bindings to MVVM
new PageDataViewModel(typeof(OneShotDateTimePage), "One-Shot DateTime",
"Obtain the current DateTime and display it"),
new PageDataViewModel(typeof(ClockPage), "Clock",
"Dynamically display the current time"),
new PageDataViewModel(typeof(HslColorScrollPage), "HSL Color Scroll",
"Use a view model to select HSL colors"),
new PageDataViewModel(typeof(KeypadPage), "Keypad",
"Use a view model for numeric keypad logic")
};
}
public static IList<PageDataViewModel> All { private set; get; }
}
El archivo XAML para MainPage
define un ListBox
cuya propiedad ItemsSource
se establece en la propiedad All
y que contiene TextCell
para mostrar las propiedades Title
y Description
de cada 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.MainPage"
Padding="5, 0"
Title="XAML Samples">
<ListView ItemsSource="{x:Static local:PageDataViewModel.All}"
ItemSelected="OnListViewItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Title}"
Detail="{Binding Description}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage>
Las páginas se muestran en una lista desplazable:
El controlador del archivo de código subyacente se desencadena cuando el usuario selecciona un elemento. El controlador establece la propiedad SelectedItem
de ListBox
de nuevo en null
y, a continuación, crea una instancia de la página seleccionada y accede a ella:
private async void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
{
(sender as ListView).SelectedItem = null;
if (args.SelectedItem != null)
{
PageDataViewModel pageData = args.SelectedItem as PageDataViewModel;
Page page = (Page)Activator.CreateInstance(pageData.Type);
await Navigation.PushAsync(page);
}
}
Vídeo
Xamarin Evolve 2016: MVVM simplificado con Xamarin.Forms y Prism
Resumen
XAML es una herramienta eficaz para definir interfaces de usuario en aplicaciones de Xamarin.Forms, especialmente cuando se usan el enlace de datos y MVVM. El resultado es una representación limpia, elegante y potencialmente útil de una interfaz de usuario con toda la compatibilidad en segundo plano en el código.
Vínculos relacionados
- Parte 1. Introducción a XAML
- Parte 2. Sintaxis XAML esencial
- Parte 3. Extensiones de marcado XAML
- Parte 4. Conceptos básicos del enlace de datos