Enlace de datos y MVVM
El patrón Model-View-ViewModel (MVVM) exige una separación entre tres capas de software: la interfaz de usuario XAML, denominada vista, los datos subyacentes, denominado modelo, y un intermediario entre la vista y el 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 XAML. El BindingContext
objeto de la vista suele ser una instancia del modelo de vista.
Simple MVVM
En las extensiones de marcado XAML has visto 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. En el ejemplo siguiente se usa la x:Static
extensión de marcado para obtener la fecha y hora actuales de la propiedad estática DateTime.Now
en el System
espacio de nombres :
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
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">
<VerticalStackLayout BindingContext="{x:Static sys:DateTime.Now}"
Spacing="25" Padding="30,0"
VerticalOptions="Center" HorizontalOptions="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}'}" />
</VerticalStackLayout>
</ContentPage>
En este ejemplo, el valor recuperado DateTime
se establece como en BindingContext
.StackLayout Cuando se establece en BindingContext
un elemento, todos los elementos secundarios de ese elemento heredan. Esto significa que todos los elementos secundarios de StackLayout tienen el mismo BindingContext
y pueden contener enlaces a propiedades de ese objeto:
Sin embargo, el problema es que la fecha y hora se establecen una vez cuando se construye e inicializa la página, y nunca cambia.
Una página XAML puede mostrar un reloj que siempre muestra la hora actual, pero requiere código adicional. El patrón MVVM es una opción natural para las aplicaciones MAUI de .NET cuando los datos se enlazan desde propiedades entre objetos visuales y los datos subyacentes. Al pensar en términos de MVVM, el modelo y el modelo de vista son clases escritas completamente en el código. La vista suele ser un archivo XAML que hace referencia a las propiedades definidas en el modelo de vista a través de enlaces de datos. En MVVM, un modelo es ignorante del modelo de vista y un modelo de vista es ignorante de la vista. Sin embargo, a menudo se adaptan los tipos expuestos por el modelo de vista a los tipos asociados a la interfaz de usuario.
Nota
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.
En el ejemplo siguiente se muestra un modelo de vista para un reloj, con una sola propiedad denominada DateTime
que se actualiza cada segundo:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace XamlSamples;
class ClockViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private DateTime _dateTime;
private Timer _timer;
public DateTime DateTime
{
get => _dateTime;
set
{
if (_dateTime != value)
{
_dateTime = value;
OnPropertyChanged(); // reports this property
}
}
}
public ClockViewModel()
{
this.DateTime = DateTime.Now;
// Update the DateTime property every second.
_timer = new Timer(new TimerCallback((s) => this.DateTime = DateTime.Now),
null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
}
~ClockViewModel() =>
_timer.Dispose();
public void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Normalmente, los modelos de vista implementan la INotifyPropertyChanged
interfaz , que proporciona la capacidad de que una clase genere el PropertyChanged
evento cada vez que cambie una de sus propiedades. El mecanismo de enlace de datos de .NET MAUI adjunta un controlador a este PropertyChanged
evento para que se pueda notificar cuando una propiedad cambie y mantenga el destino actualizado con el nuevo valor. En el ejemplo de código anterior, el OnPropertyChanged
método controla la generación del evento al determinar automáticamente el nombre de origen de la propiedad: DateTime
.
En el ejemplo siguiente se muestra XAML que consume ClockViewModel
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.ClockPage"
Title="Clock Page">
<ContentPage.BindingContext>
<local:ClockViewModel />
</ContentPage.BindingContext>
<Label Text="{Binding DateTime, StringFormat='{0:T}'}"
FontSize="18"
HorizontalOptions="Center"
VerticalOptions="Center" />
</ContentPage>
En este ejemplo, ClockViewModel
se establece en la BindingContext
propiedad de mediante ContentPage etiquetas de elemento de propiedad. Como alternativa, el archivo de código subyacente podría crear instancias del modelo de vista.
La Binding
extensión de marcado de la Text
propiedad da Label formato a la DateTime
propiedad . En la captura de pantalla siguiente se muestra el resultado:
Además, es posible tener acceso a propiedades individuales de la DateTime
propiedad del modelo de vista separando las propiedades con puntos:
<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >
MVVM interactiva
MVVM se usa a menudo con enlaces de datos bidireccionales para una vista interactiva basada en un modelo de datos subyacente.
En el ejemplo siguiente se muestra que HslViewModel
convierte un Color
valor en Hue
valores , Saturation
y , y Luminosity
de nuevo:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace XamlSamples;
class HslViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private float _hue, _saturation, _luminosity;
private Color _color;
public float Hue
{
get => _hue;
set
{
if (_hue != value)
Color = Color.FromHsla(value, _saturation, _luminosity);
}
}
public float Saturation
{
get => _saturation;
set
{
if (_saturation != value)
Color = Color.FromHsla(_hue, value, _luminosity);
}
}
public float Luminosity
{
get => _luminosity;
set
{
if (_luminosity != value)
Color = Color.FromHsla(_hue, _saturation, value);
}
}
public Color Color
{
get => _color;
set
{
if (_color != value)
{
_color = value;
_hue = _color.GetHue();
_saturation = _color.GetSaturation();
_luminosity = _color.GetLuminosity();
OnPropertyChanged("Hue");
OnPropertyChanged("Saturation");
OnPropertyChanged("Luminosity");
OnPropertyChanged(); // reports this property
}
}
}
public void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
En este ejemplo, los cambios realizados en las Hue
propiedades , Saturation
y Luminosity
hacen que la Color
propiedad cambie y los cambios realizados en la Color
propiedad hacen que las otras tres propiedades cambien. Esto puede parecer un bucle infinito, salvo que el modelo de vista no invoca el PropertyChanged
evento a menos que la propiedad haya cambiado.
El ejemplo XAML siguiente contiene una BoxView cuya Color
propiedad está enlazada a la Color
propiedad del modelo de vista, y tres Slider y tres Label vistas enlazadas a las Hue
propiedades , Saturation
y Luminosity
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.HslColorScrollPage"
Title="HSL Color Scroll Page">
<ContentPage.BindingContext>
<local:HslViewModel Color="Aqua" />
</ContentPage.BindingContext>
<VerticalStackLayout Padding="10, 0, 10, 30">
<BoxView Color="{Binding Color}"
HeightRequest="100"
WidthRequest="100"
HorizontalOptions="Center" />
<Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Hue}"
Margin="20,0,20,0" />
<Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Saturation}"
Margin="20,0,20,0" />
<Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Luminosity}"
Margin="20,0,20,0" />
</VerticalStackLayout>
</ContentPage>
El enlace de cada es Label el valor predeterminado OneWay
. Solo necesita mostrar el valor. Sin embargo, el enlace predeterminado en cada Slider es TwoWay
. Esto permite Slider inicializar desde el modelo de vista. Cuando se crea una instancia del modelo de vista, la Color
propiedad se establece Aqua
en . Un cambio en un Slider establece un nuevo valor para la propiedad en el modelo de vista, que luego calcula un nuevo color:
Comandos
A veces, una aplicación tiene necesidades que van más allá de los enlaces de propiedades al exigir al usuario que inicie comandos que afecten a algo en el modelo de vista. Por lo general, estos comandos se señalizan mediante clics de botón o pulsaciones con el dedo, y tradicionalmente se procesan en el archivo de código subyacente en un controlador para el evento Clicked
del elemento Button o el evento Tapped
de un elemento TapGestureRecognizer.
La interfaz de comandos proporciona un enfoque alternativo para implementar comandos que se adapta mucho mejor a la arquitectura MVVM. El modelo de vista puede contener comandos, que son métodos que se ejecutan en reacción a una actividad específica en la vista, como un Button clic. Los enlaces de datos se definen entre estos comandos y el objeto Button.
Para permitir un enlace de datos entre un Button y un modelo de vista, Button define dos propiedades:
Command
de tipoSystem.Windows.Input.ICommand
CommandParameter
de tipoObject
Nota
Muchos otros controles también definen Command
y CommandParameter
propiedades.
La ICommand
interfaz se define en el espacio de nombres System.Windows.Input y consta de 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
. A continuación, puede enlazar estas propiedades a la Command
propiedad de uno Button u otro elemento, o quizás una vista personalizada que implemente esta interfaz. Opcionalmente, puede establecer la CommandParameter
propiedad para identificar objetos individuales Button (u otros elementos) enlazados a esta propiedad viewmodel. Internamente, Button llama al Execute
método cada vez que el usuario pulsa en Button, pasando al Execute
método su CommandParameter
.
El CanExecute
método y CanExecuteChanged
el evento se usan para los casos en los que una Button pulsación podría no ser válida actualmente, en cuyo caso se Button debe deshabilitar. Button Llama CanExecute
a cuando la Command
propiedad se establece por primera vez y cada vez que se genera el CanExecuteChanged
evento. Si CanExecute
devuelve false
, se Button deshabilita y no genera Execute
llamadas.
Puede usar la Command
clase o Command<T>
incluida en .NET MAUI para implementar la ICommand
interfaz . Estas dos clases definen varios constructores más un ChangeCanExecute
método al que el modelo de vista puede llamar para forzar que el Command
objeto genere el CanExecuteChanged
evento.
En el ejemplo siguiente se muestra un modelo de vista para un teclado simple destinado a escribir números de teléfono:
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace XamlSamples;
class KeypadViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _inputString = "";
private string _displayText = "";
private char[] _specialChars = { '*', '#' };
public ICommand AddCharCommand { get; private set; }
public ICommand DeleteCharCommand { get; private set; }
public string InputString
{
get => _inputString;
private set
{
if (_inputString != value)
{
_inputString = value;
OnPropertyChanged();
DisplayText = FormatText(_inputString);
// Perhaps the delete button must be enabled/disabled.
((Command)DeleteCharCommand).ChangeCanExecute();
}
}
}
public string DisplayText
{
get => _displayText;
private set
{
if (_displayText != value)
{
_displayText = value;
OnPropertyChanged();
}
}
}
public KeypadViewModel()
{
// Command to add the key to the input string
AddCharCommand = new Command<string>((key) => InputString += key);
// Command to delete a character from the input string when allowed
DeleteCharCommand =
new Command(
// Command will strip a character from the input string
() => InputString = InputString.Substring(0, InputString.Length - 1),
// CanExecute is processed here to return true when there's something to delete
() => InputString.Length > 0
);
}
string FormatText(string str)
{
bool hasNonNumbers = str.IndexOfAny(_specialChars) != -1;
string formatted = str;
// Format the string based on the type of data and the length
if (hasNonNumbers || str.Length < 4 || str.Length > 10)
{
// Special characters exist, or the string is too small or large for special formatting
// Do nothing
}
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;
}
public void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
En este ejemplo, los Execute
métodos y CanExecute
para los comandos se definen como funciones lambda en el constructor. El modelo de vista supone que la AddCharCommand
propiedad está enlazada a la Command
propiedad de varios botones (o cualquier otro control que tenga una interfaz de comandos), cada uno de los cuales se identifica mediante .CommandParameter
Estos botones agregan caracteres a una InputString
propiedad, que a continuación se da formato como un número de teléfono para la DisplayText
propiedad. También hay una segunda propiedad de tipo ICommand
denominada DeleteCharCommand
. Esto está enlazado a un botón de espaciado trasero, pero el botón debe deshabilitarse si no hay caracteres que eliminar.
En el ejemplo siguiente se muestra el código XAML que consume :KeypadViewModel
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.KeypadPage"
Title="Keypad Page">
<ContentPage.BindingContext>
<local:KeypadViewModel />
</ContentPage.BindingContext>
<Grid HorizontalOptions="Center" VerticalOptions="Center">
<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>
<Label Text="{Binding DisplayText}"
Margin="0,0,10,0" FontSize="20" LineBreakMode="HeadTruncation"
VerticalTextAlignment="Center" HorizontalTextAlignment="End"
Grid.ColumnSpan="2" />
<Button Text="⇦" Command="{Binding DeleteCharCommand}" Grid.Column="2"/>
<Button Text="1" Command="{Binding AddCharCommand}" CommandParameter="1" Grid.Row="1" />
<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" />
<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" />
<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" />
<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>
En este ejemplo, la Command
propiedad de la primera Button que está enlazada a .DeleteCharCommand
Los demás botones se enlazan a AddCharCommand
con un CommandParameter
que es el mismo que el carácter que aparece en :Button