Enlace de datos y MVVM

Examinar ejemplo. Examinar el ejemplo

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 BindingContexty pueden contener enlaces a propiedades de ese objeto:

Captura de pantalla de una página que muestra la fecha y la hora.

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:

Captura de pantalla de una página que muestra la fecha y la hora a través de un modelo de vista.

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 Huevalores , Saturationy , 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 Huepropiedades , Saturationy 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 Huepropiedades , Saturationy 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 Aquaen . Un cambio en un Slider establece un nuevo valor para la propiedad en el modelo de vista, que luego calcula un nuevo color:

MVVM mediante enlaces de datos bidireccionales.

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:

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="&#x21E6;" 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

Captura de pantalla de una calculadora mediante MVVM y comandos.