Comandos

Browse sample. Examinar la muestra.

En una aplicación de.NET Multi-Platform UI (.NET MAUI) que usa el patrón Modelo-Vista-Modelo de vista (MVVM), los enlaces de datos se definen entre propiedades en el modelo de vista, que normalmente es una clase que deriva de INotifyPropertyChanged y las propiedades de la vista, que normalmente es el archivo XAML. A veces, una aplicación tiene necesidades que van más allá de estos enlaces de propiedad y solicita al usuario que inicie comandos que influyen 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 respuesta a una actividad específica en la vista, como un clic Button. Los enlaces de datos se definen entre estos comandos y el objeto Button.

Para permitir un enlace de datos entre Button y un modelo de vista, Button define dos propiedades:

Para usar la interfaz de comandos, define un enlace de datos que tenga como destino la propiedad Command de Button donde el origen sea una propiedad en el modelo de vista de tipo ICommand. El modelo de vista contiene código asociado con la propiedad ICommand que se ejecuta cuando se hace clic en el botón. Puedes establecer la propiedad CommandParameter en datos arbitrarios para distinguir entre varios botones si todos se enlazan a la misma propiedad ICommand en el modelo de vista.

Muchas otras vistas también definen las propiedades Command y CommandParameter. Todos estos comandos se pueden controlar en un modelo de vista con un enfoque que no dependa del objeto de interfaz de usuario de la vista.

ICommands

La interfaz ICommand se define en el espacio de nombres System.Windows.Input y consta de dos métodos y un evento:

public interface ICommand
{
    public void Execute (Object parameter);
    public bool CanExecute (Object parameter);
    public event EventHandler CanExecuteChanged;
}

Para usar la interfaz de comandos, el modelo de vista contiene propiedades de tipo ICommand:

public ICommand MyCommand { private set; get; }

El modelo de vista también debe hacer referencia a una clase que implemente la interfaz ICommand. En la vista, la propiedad Command de un objeto Button está enlazada a esa propiedad:

<Button Text="Execute command"
        Command="{Binding MyCommand}" />

Cuando el usuario presiona el elemento Button, Button llama al método Execute del objeto ICommand enlazado a su propiedad Command.

Cuando se define por primera vez el enlace en la propiedad Command del objeto Button, y cuando cambia de algún modo el enlace de datos, el objeto Button llama al método CanExecute del objeto ICommand. Si CanExecute devuelve false, el objeto Button se deshabilita a sí mismo. Esto indica que el comando concreto no está disponible actualmente o no es válido.

El objeto Button también adjunta un controlador al evento CanExecuteChanged de ICommand. El evento se genera desde dentro del modelo de vista. Cuando se desencadena ese evento, el objeto Button vuelve a llamar a CanExecute. El objeto Button se habilita a sí mismo si CanExecute devuelve true y se deshabilita si CanExecute devuelve false.

Advertencia

No use la propiedad IsEnabled de Button si usa la interfaz de comandos.

Cuando en el modelo de vista se define una propiedad de tipo ICommand, el modelo de vista también debe contener o hacer referencia a una clase que implemente la interfaz ICommand. Esta clase debe contener o hacer referencia a los métodos Execute y CanExecute, y desencadenar el evento CanExecuteChanged cada vez que el método CanExecute pueda devolver otro valor. Puedes usar la clase Command o Command<T> incluida en .NET MAUI para implementar la interfaz ICommand. Estas clases permiten especificar los cuerpos de los métodos Execute y CanExecute en los constructores de clase.

Sugerencia

Usa Command<T> cuando emplees la propiedad CommandParameter para distinguir entre varias vistas enlazadas a la misma propiedad ICommand, y la clase Command cuando no sea un requisito.

Comandos básicos

En los ejemplos siguientes se muestran los comandos básicos implementados en un modelo de vista.

La clase PersonViewModel define tres propiedades denominadas Name, Age y Skills que definen una persona.

public class PersonViewModel : INotifyPropertyChanged
{
    string name;
    double age;
    string skills;

    public event PropertyChangedEventHandler PropertyChanged;

    public string Name
    {
        set { SetProperty(ref name, value); }
        get { return name; }
    }

    public double Age
    {
        set { SetProperty(ref age, value); }
        get { return age; }
    }

    public string Skills
    {
        set { SetProperty(ref skills, value); }
        get { return skills; }
    }

    public override string ToString()
    {
        return Name + ", age " + Age;
    }

    bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (Object.Equals(storage, value))
            return false;

        storage = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

La clase PersonCollectionViewModel que se muestra a continuación crea nuevos objetos de tipo PersonViewModel y permite al usuario rellenar los datos. Para ello, la clase define las propiedades IsEditing de tipo bool y PersonEdit de tipo PersonViewModel. Además, la clase define tres propiedades de tipo ICommand y una propiedad denominada Persons de tipo IList<PersonViewModel>:

public class PersonCollectionViewModel : INotifyPropertyChanged
{
    PersonViewModel personEdit;
    bool isEditing;

    public event PropertyChangedEventHandler PropertyChanged;
    ···

    public bool IsEditing
    {
        private set { SetProperty(ref isEditing, value); }
        get { return isEditing; }
    }

    public PersonViewModel PersonEdit
    {
        set { SetProperty(ref personEdit, value); }
        get { return personEdit; }
    }

    public ICommand NewCommand { private set; get; }
    public ICommand SubmitCommand { private set; get; }
    public ICommand CancelCommand { private set; get; }

    public IList<PersonViewModel> Persons { get; } = new ObservableCollection<PersonViewModel>();

    bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (Object.Equals(storage, value))
            return false;

        storage = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

En este ejemplo, los cambios realizados en las tres propiedades ICommand y la propiedad Persons no dan lugar a que se generen eventos PropertyChanged. Estas propiedades se establecen al crear la clase por primera vez y no cambian.

En el siguiente ejemplo se muestra XAML que consume PersonCollectionViewModel:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DataBindingDemos"
             x:Class="DataBindingDemos.PersonEntryPage"
             Title="Person Entry">
    <ContentPage.BindingContext>
        <local:PersonCollectionViewModel />
    </ContentPage.BindingContext>
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <!-- New Button -->
        <Button Text="New"
                Grid.Row="0"
                Command="{Binding NewCommand}"
                HorizontalOptions="Start" />

        <!-- Entry Form -->
        <Grid Grid.Row="1"
              IsEnabled="{Binding IsEditing}">
            <Grid BindingContext="{Binding PersonEdit}">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>

                <Label Text="Name: " Grid.Row="0" Grid.Column="0" />
                <Entry Text="{Binding Name}"
                       Grid.Row="0" Grid.Column="1" />
                <Label Text="Age: " Grid.Row="1" Grid.Column="0" />
                <StackLayout Orientation="Horizontal"
                             Grid.Row="1" Grid.Column="1">
                    <Stepper Value="{Binding Age}"
                             Maximum="100" />
                    <Label Text="{Binding Age, StringFormat='{0} years old'}"
                           VerticalOptions="Center" />
                </StackLayout>
                <Label Text="Skills: " Grid.Row="2" Grid.Column="0" />
                <Entry Text="{Binding Skills}"
                       Grid.Row="2" Grid.Column="1" />
            </Grid>
        </Grid>

        <!-- Submit and Cancel Buttons -->
        <Grid Grid.Row="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>

            <Button Text="Submit"
                    Grid.Column="0"
                    Command="{Binding SubmitCommand}"
                    VerticalOptions="Center" />
            <Button Text="Cancel"
                    Grid.Column="1"
                    Command="{Binding CancelCommand}"
                    VerticalOptions="Center" />
        </Grid>

        <!-- List of Persons -->
        <ListView Grid.Row="3"
                  ItemsSource="{Binding Persons}" />
    </Grid>
</ContentPage>

En este ejemplo, la propiedad BindingContext de la página se establece en PersonCollectionViewModel. Grid contiene un Button con el texto Nuevo con su propiedad Command enlazada a la propiedad NewCommand del modelo de vista, un formulario de entrada con propiedades enlazadas a la propiedad IsEditing, así como las propiedades de PersonViewModel, y dos botones más enlazados a las propiedades SubmitCommand y CancelCommand del modelo de vista. ListView muestra la colección de las personas que ya se han introducido:

En las siguientes capturas de pantalla se muestra el botón Enviar habilitado después de haber establecido una edad:

Person Entry.

Cuando el usuario presiona por primera vez el botón Nuevo, esto habilita el formulario de entrada, pero deshabilita el botón Nuevo. Después, el usuario escribe un nombre, la edad y las habilidades. En cualquier momento durante la edición, el usuario puede presionar el botón Cancel (Cancelar) para volver a empezar. El botón Submit (Enviar) solo se habilita cuando se ha escrito un nombre y una edad válida. Al presionar este botón Submit, se transfiere a la persona a la colección mostrada por ListView. Después de presionar el botón Cancel o Submit, se borra el formulario de entrada y se vuelve a habilitar el botón New.

Toda la lógica para los botones New, Submit y Cancel se controla en PersonCollectionViewModel a través de definiciones de las propiedades NewCommand, SubmitCommand y CancelCommand. El constructor de PersonCollectionViewModel establece estas tres propiedades en objetos de tipo Command.

Un constructor de la clase Command permite pasar argumentos de tipo Action y Func<bool> correspondientes a los métodos Execute y CanExecute. Esta acción y función se pueden definir como funciones lambda en el constructor Command:

public class PersonCollectionViewModel : INotifyPropertyChanged
{
    ···
    public PersonCollectionViewModel()
    {
        NewCommand = new Command(
            execute: () =>
            {
                PersonEdit = new PersonViewModel();
                PersonEdit.PropertyChanged += OnPersonEditPropertyChanged;
                IsEditing = true;
                RefreshCanExecutes();
            },
            canExecute: () =>
            {
                return !IsEditing;
            });
        ···
    }

    void OnPersonEditPropertyChanged(object sender, PropertyChangedEventArgs args)
    {
        (SubmitCommand as Command).ChangeCanExecute();
    }

    void RefreshCanExecutes()
    {
        (NewCommand as Command).ChangeCanExecute();
        (SubmitCommand as Command).ChangeCanExecute();
        (CancelCommand as Command).ChangeCanExecute();
    }
    ···
}

Cuando el usuario hace clic en el botón New, se ejecuta la función execute pasada al constructor de Command. Esto crea un objeto PersonViewModel, establece un controlador en el evento PropertyChanged de ese objeto, establece IsEditing en true, y llama al método RefreshCanExecutes definido después del constructor.

Además de implementar la interfaz ICommand, la clase Command también define un método denominado ChangeCanExecute. El modelo de vista debe llamar a ChangeCanExecute para una propiedad ICommand cada vez que suceda algo que pueda cambiar el valor devuelto del método CanExecute. Una llamada a ChangeCanExecute hace que la clase Command desencadene el método CanExecuteChanged. El objeto Button ha adjuntado un controlador para ese evento y responde mediante una nueva llamada a CanExecute y, después, se habilita a sí mismo en función del valor devuelto de ese método.

Cuando el método execute de NewCommand llama a RefreshCanExecutes, la propiedad NewCommand recibe una llamada a ChangeCanExecute, y Button llama al método canExecute, que ahora devuelve false porque la propiedad IsEditing es true.

El controlador PropertyChanged para el nuevo objeto PersonViewModel llama al método ChangeCanExecute de SubmitCommand:

public class PersonCollectionViewModel : INotifyPropertyChanged
{
    ···
    public PersonCollectionViewModel()
    {
        ···
        SubmitCommand = new Command(
            execute: () =>
            {
                Persons.Add(PersonEdit);
                PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
                PersonEdit = null;
                IsEditing = false;
                RefreshCanExecutes();
            },
            canExecute: () =>
            {
                return PersonEdit != null &&
                       PersonEdit.Name != null &&
                       PersonEdit.Name.Length > 1 &&
                       PersonEdit.Age > 0;
            });
        ···
    }
    ···
}

La función canExecute para SubmitCommand se llama cada vez que cambia una propiedad en el objeto PersonViewModel que se está editando. Solo devuelve true cuando la propiedad Name tiene al menos un carácter de longitud, y Age es mayor que 0. En ese momento, se habilita el botón Submit.

La función execute para Enviar quita el controlador de cambio de propiedad de PersonViewModel, agrega el objeto a la colección Persons y devuelve todo a las condiciones iniciales.

La función execute para el botón Cancel hace lo mismo que el botón Submit excepto agregar el objeto a la colección:

public class PersonCollectionViewModel : INotifyPropertyChanged
{
    ···
    public PersonCollectionViewModel()
    {
        ···
        CancelCommand = new Command(
            execute: () =>
            {
                PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
                PersonEdit = null;
                IsEditing = false;
                RefreshCanExecutes();
            },
            canExecute: () =>
            {
                return IsEditing;
            });
    }
    ···
}

El método canExecute devuelve true en cualquier momento que se modifique un elemento PersonViewModel.

Nota:

No es necesario definir los métodos execute y canExecute como funciones lambda. Puedes escribirlos como métodos privados en el modelo de vista y hacer referencia a ellos en los constructores Command. Pero este enfoque tiende a crear una gran cantidad de métodos a los que solo se hace referencia una vez en el modelo de vista.

Uso de parámetros de comando

A veces es conveniente que uno o varios botones u otros objetos de interfaz de usuario compartan la misma propiedad ICommand en el modelo de vista. En este caso, se usa la propiedad CommandParameter para distinguir entre los botones.

Se puede seguir usando la clase Command para estas propiedades ICommand compartidas. La clase define un constructor alternativo que acepta los métodos execute y canExecute con parámetros de tipo Object. Esta es la forma de pasar CommandParameter a estos métodos. Pero cuando se especifica CommandParameter, resulta más fácil utilizar la clase Command<T> genérica para especificar el tipo del objeto establecido en CommandParameter. Los métodos execute y canExecute que especifique tienen parámetros de ese tipo.

En el ejemplo siguiente se muestra un teclado para escribir números decimales:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DataBindingDemos"
             x:Class="DataBindingDemos.DecimalKeypadPage"
             Title="Decimal Keyboard">
    <ContentPage.BindingContext>
        <local:DecimalKeypadViewModel />
    </ContentPage.BindingContext>
    <ContentPage.Resources>
        <Style TargetType="Button">
            <Setter Property="FontSize" Value="32" />
            <Setter Property="BorderWidth" Value="1" />
            <Setter Property="BorderColor" Value="Black" />
        </Style>
    </ContentPage.Resources>

    <Grid WidthRequest="240"
          HeightRequest="480"
          ColumnDefinitions="80, 80, 80"
          RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto"
          ColumnSpacing="2"
          RowSpacing="2"
          HorizontalOptions="Center"
          VerticalOptions="Center">
        <Label Text="{Binding Entry}"
               Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"
               Margin="0,0,10,0"
               FontSize="32"
               LineBreakMode="HeadTruncation"
               VerticalTextAlignment="Center"
               HorizontalTextAlignment="End" />
        <Button Text="CLEAR"
                Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
                Command="{Binding ClearCommand}" />
        <Button Text="&#x21E6;"
                Grid.Row="1" Grid.Column="2"
                Command="{Binding BackspaceCommand}" />
        <Button Text="7"
                Grid.Row="2" Grid.Column="0"
                Command="{Binding DigitCommand}"
                CommandParameter="7" />
        <Button Text="8"
                Grid.Row="2" Grid.Column="1"
                Command="{Binding DigitCommand}"
                CommandParameter="8" />        
        <Button Text="9"
                Grid.Row="2" Grid.Column="2"
                Command="{Binding DigitCommand}"
                CommandParameter="9" />
        <Button Text="4"
                Grid.Row="3" Grid.Column="0"
                Command="{Binding DigitCommand}"
                CommandParameter="4" />
        <Button Text="5"
                Grid.Row="3" Grid.Column="1"
                Command="{Binding DigitCommand}"
                CommandParameter="5" />
        <Button Text="6"
                Grid.Row="3" Grid.Column="2"
                Command="{Binding DigitCommand}"
                CommandParameter="6" />
        <Button Text="1"
                Grid.Row="4" Grid.Column="0"
                Command="{Binding DigitCommand}"
                CommandParameter="1" />
        <Button Text="2"
                Grid.Row="4" Grid.Column="1"
                Command="{Binding DigitCommand}"
                CommandParameter="2" />
        <Button Text="3"
                Grid.Row="4" Grid.Column="2"
                Command="{Binding DigitCommand}"
                CommandParameter="3" />
        <Button Text="0"
                Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2"
                Command="{Binding DigitCommand}"
                CommandParameter="0" />
        <Button Text="&#x00B7;"
                Grid.Row="5" Grid.Column="2"
                Command="{Binding DigitCommand}"
                CommandParameter="." />
    </Grid>
</ContentPage>

En este ejemplo, BindingContext de la página es DecimalKeypadViewModel. La propiedad Entry de este modelo de vista se enlaza a la propiedad Text de Label. Todos los objetos Button están enlazados a comandos del modelo de vista: ClearCommand, BackspaceCommand y DigitCommand: Los 11 botones para los 10 dígitos y el separador decimal comparten un enlace a DigitCommand. CommandParameter distingue entre estos botones. El valor establecido en CommandParameter suele ser el mismo que el texto que se muestra en el botón, excepto por el separador decimal que, para una mayor claridad, se muestra con un carácter de punto central:

Decimal keyboard.

DecimalKeypadViewModel define una propiedad Entry de tipo string y tres propiedades de tipo ICommand:

public class DecimalKeypadViewModel : INotifyPropertyChanged
{
    string entry = "0";

    public event PropertyChangedEventHandler PropertyChanged;
    ···

    public string Entry
    {
        private set
        {
            if (entry != value)
            {
                entry = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Entry"));
            }
        }
        get
        {
            return entry;
        }
    }

    public ICommand ClearCommand { private set; get; }
    public ICommand BackspaceCommand { private set; get; }
    public ICommand DigitCommand { private set; get; }
}

El botón correspondiente a ClearCommand siempre está habilitado y simplemente establece la entrada en "0":

public class DecimalKeypadViewModel : INotifyPropertyChanged
{
    ···
    public DecimalKeypadViewModel()
    {
        ClearCommand = new Command(
            execute: () =>
            {
                Entry = "0";
                RefreshCanExecutes();
            });
        ···
    }

    void RefreshCanExecutes()
    {
        ((Command)BackspaceCommand).ChangeCanExecute();
        ((Command)DigitCommand).ChangeCanExecute();
    }
    ···
}

Como el botón siempre está habilitado, no es necesario especificar un argumento canExecute en el constructor de Command.

El botón Backspace (Retroceso) solo se habilita cuando la longitud de la entrada es mayor que 1, o bien si Entry no es igual a la cadena "0":

public class DecimalKeypadViewModel : INotifyPropertyChanged
{
    ···
    public DecimalKeypadViewModel()
    {
        ···
        BackspaceCommand = new Command(
            execute: () =>
            {
                Entry = Entry.Substring(0, Entry.Length - 1);
                if (Entry == "")
                {
                    Entry = "0";
                }
                RefreshCanExecutes();
            },
            canExecute: () =>
            {
                return Entry.Length > 1 || Entry != "0";
            });
        ···
    }
    ···
}

La lógica para la función execute para el botón Backspace garantiza que Entry es al menos una cadena de "0".

La propiedad DigitCommand está enlazada a 11 botones, que se identifican a sí mismos con la propiedad CommandParameter de forma individual. DigitCommand está establecido en una instancia de la clase Command<T>. Cuando se usa la interfaz de comandos con XAML, las propiedades CommandParameter suelen ser cadenas, y ese es el tipo del argumento genérico. Las funciones execute y canExecute tienen argumentos de tipo string:

public class DecimalKeypadViewModel : INotifyPropertyChanged
{
    ···
    public DecimalKeypadViewModel()
    {
        ···
        DigitCommand = new Command<string>(
            execute: (string arg) =>
            {
                Entry += arg;
                if (Entry.StartsWith("0") && !Entry.StartsWith("0."))
                {
                    Entry = Entry.Substring(1);
                }
                RefreshCanExecutes();
            },
            canExecute: (string arg) =>
            {
                return !(arg == "." && Entry.Contains("."));
            });
    }
    ···
}

El método execute anexa el argumento de cadena a la propiedad Entry. Pero si el resultado comienza con un cero (pero no un cero y un separador decimal), ese cero inicial se debe quitar mediante la función Substring. El método canExecute devuelve false solo si el argumento es el separador decimal (lo que indica que se presiona el separador decimal) y Entry ya contiene un separador decimal. Todos los métodos execute llaman a RefreshCanExecutes, que llama a ChangeCanExecute para DigitCommand y ClearCommand. Esto garantiza que los botones de retroceso y separador decimal se habilitan o deshabilitan en función de la secuencia actual de dígitos especificados.