Compartir a través de


Comando

Examinar ejemplo. Examinar el ejemplo

En una aplicación de .NET MAUI de interfaz de usuario multiplataforma que utiliza el patrón Model-View-ViewModel (MVVM), los enlaces de datos se definen entre las propiedades del modelo de vista, que normalmente es una clase que deriva de INotifyPropertyChanged, y las propiedades de la vista, que normalmente se encuentran en el archivo XAML. A veces, una aplicación tiene necesidades que van más allá de estos enlaces de propiedades exigiendo al usuario que inicie comandos que afecten a algo en el modelo de vista. Por lo general, estos comandos se indican mediante clics de botón o pulsaciones de dedo y tradicionalmente se procesan en el archivo de código subyacente en un controlador para el evento Clicked de Button o el evento Tapped de un TapGestureRecognizer.

La interfaz de comandos proporciona un enfoque alternativo a la implementación de comandos que es mucho más adecuado para la arquitectura de 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 vínculos de datos se definen entre estos comandos y el Button.

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

Para usar la interfaz de comandos, defina un enlace de datos destinado a la Command propiedad de Button donde el origen es una propiedad en el modelo de vista de tipo ICommand. El modelo de vista contiene código asociado a esa ICommand propiedad que se ejecuta cuando se hace clic en el botón. Puede establecer la propiedad CommandParameter en datos arbitrarios para distinguir entre varios botones si todos están enlazados a la misma propiedad ICommand en el modelo de vista.

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

ICommands

La ICommand interfaz 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 debe contener 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 ICommand interfaz . En la vista, la Command propiedad de un Button está vinculada a esa propiedad:

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

Cuando el usuario presiona Button, llama al Button método en el Execute objeto enlazado a su ICommandCommand propiedad .

Cuando el enlace se define por primera vez en la Command propiedad de Button, y cuando el enlace de datos cambia de alguna manera, Button llama al método CanExecute en el objeto ICommand. Si CanExecute devuelve false, Button se deshabilita. Esto indica que el comando concreto no está disponible o no es válido.

También, Button asocia un controlador en el evento CanExecuteChanged de ICommand. El evento se debe generar manualmente desde dentro del modelo de vista siempre que cambien las condiciones que afecten al CanExecute resultado. Cuando se genera ese evento, Button vuelve a llamar a CanExecute. Button se habilita si CanExecute devuelve true y se deshabilita si CanExecute devuelve false.

Importante

A diferencia de algunos marcos de interfaz de usuario (como WPF), .NET MAUI no detecta automáticamente cuándo puede cambiar el valor devuelto de CanExecute . Debe generar manualmente el evento CanExecuteChanged (o llamar a ChangeCanExecute() en la clase Command) siempre que cambie cualquier condición que afecte al resultado CanExecute. Normalmente, esto se hace cuando se modifican las propiedades que CanExecute dependen de .

Nota:

También puede usar la IsEnabled propiedad de Button en lugar del CanExecute método o junto con ella. En .NET MAUI 7 y versiones anteriores, no era posible usar la IsEnabled propiedad de Button mientras se usaba la interfaz de comando, ya que el CanExecute valor devuelto del método siempre sobreescribía la IsEnabled propiedad. Esto se ha corregido en .NET MAUI 8 y versiones posteriores; la propiedad IsEnabled ahora se puede usar en Button basados en comandos. Sin embargo, tenga en cuenta que tanto la IsEnabled propiedad como el CanExecute método ahora deben devolver true para que Button se habilite (y también debe estar habilitado el control principal).

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

Sugerencia

Se usa Command<T> cuando se usa la CommandParameter propiedad para distinguir entre varias vistas enlazadas a la misma ICommand propiedad y la Command clase cuando no es un requisito.

Comandos básicos

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

La PersonViewModel clase define tres propiedades denominadas Name, Agey 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 PersonCollectionViewModel clase que se muestra a continuación crea nuevos objetos de tipo PersonViewModel y permite al usuario rellenar los datos. Para ello, la clase define IsEditing, de tipo bool, y PersonEdit, de tipo PersonViewModel, propiedades . 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 en las tres propiedades ICommand y la propiedad Persons no dan lugar a que se levanten eventos PropertyChanged. Estas propiedades se establecen cuando se crea la clase por primera vez y no cambian.

En el ejemplo siguiente se muestra el 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"
             x:DataType="local:PersonCollectionViewModel">             
    <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 x:DataType="local:PersonViewModel"
                  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 objeto con el texto Nuevo, con su propiedad Command vinculada a la propiedad NewCommand en el modelo de vista, un formulario de entrada con propiedades vinculadas a la propiedad IsEditing, así como a las propiedades de PersonViewModel, y dos botones más vinculados a las propiedades SubmitCommand y CancelCommand del modelo de vista. ListView Muestra la colección de personas ya introducidas:

En la captura de pantalla siguiente se muestra el botón Enviar habilitado después de establecer una edad:

Entrada de persona.

Cuando el usuario presiona por primera vez el botón Nuevo , esto habilita el formulario de entrada, pero deshabilita el botón Nuevo . A continuación, el usuario escribe un nombre, una edad y aptitudes. En cualquier momento durante la edición, el usuario puede presionar el botón Cancelar para empezar. Solo cuando se ha escrito un nombre y una edad válida es el botón Enviar habilitado. Al presionar este botón Enviar, se transfiere la persona a la colección mostrada por .ListView Después de presionar el botón Cancelar o Enviar , se borra el formulario de entrada y el botón Nuevo se vuelve a habilitar.

Toda la lógica de los botones Nuevo, Enviar y Cancelar se controla mediante PersonCollectionViewModel definiciones de las NewCommandpropiedades , SubmitCommandy CancelCommand . El constructor de PersonCollectionViewModel establece estas tres propiedades en objetos de tipo Command.

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

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 Nuevo, se ejecuta la función pasada al constructor. Esto crea un PersonViewModel nuevo objeto, establece un controlador en el evento PropertyChanged de ese objeto, establece IsEditing a true, y llama al método RefreshCanExecutes definido después del constructor.

Además de implementar la ICommand interfaz, la Command clase también define un método denominado ChangeCanExecute. Su modelo de vista debe llamar a ChangeCanExecute para la propiedad ICommand siempre que ocurra algo que pueda cambiar el valor devuelto por el método CanExecute. Una llamada a ChangeCanExecute hace que la Command clase active el CanExecuteChanged evento. Button ha asociado un controlador para ese evento y responde llamando a CanExecute de nuevo y, a continuación, se habilita a sí mismo en función del valor devuelto de ese método.

Cuando el método de 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 ahora true.

El PropertyChanged controlador del objeto nuevo 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;
            });
        ···
    }
    ···
}

Se llama a la función canExecute para SubmitCommand cada vez que cambie una propiedad en el objeto PersonViewModel que se edita. Devuelve true solo cuando la Name propiedad es al menos un carácter largo y Age es mayor que 0. En ese momento, el botón Enviar se habilita.

La función execute quita el controlador de cambios de propiedad de , agrega el objeto a la colección PersonViewModel y devuelve todo a su estado inicial.

La execute función del botón Cancelar hace todo lo que hace el botón Enviar 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 en que se está editando un PersonViewModel.

Nota:

No es necesario definir los execute métodos y canExecute como funciones lambda. Puede escribirlos como métodos privados en el modelo de vista y hacer referencia a ellos en los Command constructores. Sin embargo, este enfoque puede dar lugar a una gran cantidad de métodos a los que se hace referencia solo 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 ICommand propiedad en el modelo de vista. En este caso, puede usar la CommandParameter propiedad para distinguir entre los botones.

Puede seguir usando la Command clase para estas propiedades compartidas ICommand . La clase define un constructor alternativo que acepta métodos execute y canExecute con parámetros de tipo Object. Así es como CommandParameter se pasa a estos métodos. Sin embargo, al especificar un CommandParameter, es más fácil usar la clase genérica Command<T> 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"
             x:DataType="local:DecimalKeypadViewModel">
    <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, el BindingContext de la página es un DecimalKeypadViewModel. La propiedad Entry de este ViewModel está enlazada a la propiedad Text de un Label. Todos los Button objetos están enlazados a comandos en el modelo de vista: ClearCommand, BackspaceCommandy DigitCommand. Los 11 botones de los 10 dígitos y el punto decimal comparten un enlace a DigitCommand. El CommandParameter distingue entre estos botones. El valor asignado a CommandParameter generalmente es el mismo que el texto que muestra el botón, excepto para el separador decimal, que para mayor claridad se muestra con un carácter de punto central.

Teclado decimal.

DecimalKeypadViewModel define una Entry propiedad 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 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();
    }
    ···
}

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

El botón Retroceso solo está habilitado cuando la longitud de la entrada es mayor que 1 o 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 de la función execute para el botón Retroceso asegura que Entry sea al menos la cadena "0".

La DigitCommand propiedad está enlazada a 11 botones, cada uno de los cuales se identifica con la CommandParameter propiedad . DigitCommand se asigna a una instancia de la clase Command<T>. Al usar la interfaz de comandos con XAML, las CommandParameter propiedades suelen ser cadenas, que es el tipo del argumento genérico. A continuación, las execute funciones 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 execute método anexa el argumento string a la Entry propiedad . Sin embargo, si el resultado comienza con un cero (pero no un cero y un separador decimal), ese cero inicial debe quitarse mediante la Substring función . El canExecute método solo devuelve false si el argumento es el separador decimal (que indica que se está presionando el separador decimal) y Entry ya contiene un separador decimal. Todos los execute métodos llaman a RefreshCanExecutes, que luego llama a ChangeCanExecute para DigitCommand y ClearCommand. Esto garantiza que los botones de punto decimal y retroceso estén habilitados o deshabilitados en función de la secuencia actual de dígitos introducidos.