Partilhar via


Comando

Procurar exemplo. Procurar o exemplo

Em um aplicativo .NET Multi-platform App UI (.NET MAUI) que usa o padrão Model-View-ViewModel (MVVM), as associações de dados são definidas entre propriedades no viewmodel, que normalmente é uma classe que deriva de INotifyPropertyChanged, e propriedades na vista, que normalmente é o arquivo XAML. Às vezes, um aplicativo tem necessidades que vão além dessas associações de propriedade, exigindo que o usuário inicie comandos que afetam algo no viewmodel. Esses comandos geralmente são sinalizados por cliques de botão ou toques de dedo, e tradicionalmente são processados no ficheiro code-behind, num manipulador para o evento Clicked de Button ou o evento Tapped de um TapGestureRecognizer.

A interface de comando fornece uma abordagem alternativa para implementar comandos que é muito mais adequada para a arquitetura MVVM. O viewmodel pode conter comandos, que são métodos que são executados em reação a uma atividade específica na vista, como um Button clique. As associações de dados são definidas entre esses comandos e o Button.

Para permitir uma associação de dados entre um Button e um viewmodel, o Button define duas propriedades:

Para usar a interface de comando, define-se uma ligação de dados que tem como alvo a propriedade Command, onde a origem é uma propriedade no modelo de vista do tipo Button. O viewmodel contém código associado a essa ICommand propriedade que é executada quando o botão é clicado. Você pode definir a CommandParameter propriedade como dados arbitrários para distinguir entre vários botões se todos estiverem vinculados à mesma ICommand propriedade no viewmodel.

Muitas outras visualizações também definem Command e CommandParameter propriedades. Todos esses comandos podem ser manipulados dentro de um viewmodel usando uma abordagem que não depende do objeto da interface do usuário na exibição.

ICommands

A ICommand interface é definida no namespace System.Windows.Input e consiste em dois métodos e um evento:

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

Para usar a interface de comando, seu viewmodel deve conter propriedades do tipo ICommand:

public ICommand MyCommand { private set; get; }

O viewmodel também deve fazer referência a uma classe que implementa a ICommand interface. Na visualização, a Command propriedade da Button está associada a essa propriedade:

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

Quando o utilizador pressiona o Button, o Button chama o método Execute no objeto ICommand associado à propriedade Command.

Quando a vinculação é definida pela primeira vez na propriedade Command do Button, e quando a vinculação de dados muda de alguma forma, o Button chama o método CanExecute no objeto ICommand. Se CanExecute retornar false, então Button desativa-se. Isso indica que o comando específico está indisponível no momento ou é inválido.

O Button também anexa um manipulador ao evento CanExecuteChanged de ICommand. O evento deve ser levantado manualmente a partir do viewmodel sempre que as condições que afetam o CanExecute resultado mudarem. Quando esse evento é disparado, o Button chama CanExecute novamente. O Button habilita-se se CanExecute retorna true e desativa-se se CanExecute retorna false.

Importante

Ao contrário de alguns frameworks de interface (como o WPF), o .NET MAUI não deteta automaticamente quando o valor de retorno de CanExecute pode mudar. Deves levantar manualmente o evento CanExecuteChanged (ou chamar ChangeCanExecute() na classe Command) sempre que uma condição se altera e afeta o resultado CanExecute. Isto é tipicamente feito quando propriedades de que CanExecute dependem são modificadas.

Observação

Você também pode usar a IsEnabled propriedade de Button em vez do CanExecute método, ou em conjunto com ele. No .NET MAUI 7 e anteriores, não era possível utilizar a propriedade IsEnabled ao usar a interface de comandos, pois o valor de retorno do método Button sempre sobrepunha a propriedade CanExecute. Isto foi corrigido no .NET MAUI 8 e superior; a propriedade IsEnabled agora pode ser utilizada em Buttons baseados em comandos. No entanto, esteja ciente de que a IsEnabled propriedade e o CanExecute método agora devem ambos retornar true para que o Button seja habilitado (e o controlo principal também deve estar habilitado).

Quando seu viewmodel define uma propriedade do tipo ICommand, o viewmodel também deve conter ou fazer referência a uma classe que implementa a ICommand interface. Esta classe deve conter ou referenciar os Execute métodos and CanExecute , e disparar manualmente o CanExecuteChanged evento sempre que o CanExecute método possa devolver um valor diferente. Você pode usar a Command classe ou Command<T> incluída no .NET MAUI para implementar a ICommand interface. Essas classes permitem que especifiques os corpos dos métodos Execute e CanExecute em construtores de classe.

Sugestão

Use Command<T> quando você usa a CommandParameter propriedade para distinguir entre várias exibições vinculadas à mesma ICommand propriedade e a Command classe quando isso não é um requisito.

Comando básico

Os exemplos a seguir demonstram comandos básicos implementados em um viewmodel.

A PersonViewModel classe define três propriedades chamadas Name, Agee Skills que definem uma pessoa:

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));
    }
}

A PersonCollectionViewModel classe mostrada abaixo cria novos objetos do tipo PersonViewModel e permite que o usuário preencha os dados. Para esse efeito, a classe define IsEditing, do tipo bool, e PersonEdit, do tipo PersonViewModel, propriedades. Além disso, a classe define três propriedades de tipo ICommand e uma propriedade chamada 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));
    }
}

Neste exemplo, as alterações nas três propriedades ICommand e na propriedade Persons não resultam na geração de eventos PropertyChanged. Essas propriedades são todas definidas quando a classe é criada pela primeira vez e não são alteradas.

O exemplo a seguir mostra o XAML que consome o 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>

Neste exemplo, a propriedade da página BindingContext é definida como PersonCollectionViewModel. O Grid contém um Button com o texto New com a sua propriedade Command vinculada à propriedade NewCommand no viewmodel, um formulário de entrada com propriedades vinculadas à propriedade IsEditing, bem como propriedades de PersonViewModel, e mais dois botões vinculados às propriedades SubmitCommand e CancelCommand do viewmodel. O ListView exibe a coleção de pessoas já inseridas:

A captura de tela a seguir mostra o botão Enviar ativado após uma idade ter sido definida:

Entrada de Pessoa.

Quando o usuário pressiona o botão Novo pela primeira vez, isso habilita o formulário de entrada, mas desabilita o botão Novo . Em seguida, o usuário insere um nome, idade e habilidades. A qualquer momento durante a edição, o usuário pode pressionar o botão Cancelar para recomeçar. Somente quando um nome e uma idade válidos tiverem sido inseridos é que o botão Enviar será ativado. Pressionar este botão Enviar transfere a pessoa para a coleção exibida pelo ListView. Depois que o botão Cancelar ou Enviar for pressionado, o formulário de inscrição será desmarcado e o botão Novo será ativado novamente.

Toda a lógica dos botões Novo, Enviar e Cancelar é tratada por PersonCollectionViewModel meio das definições das NewCommandpropriedades , SubmitCommande CancelCommand . O construtor do PersonCollectionViewModel define essas três propriedades para objetos do tipo Command.

Um construtor da Command classe permite que você passe argumentos de tipo Action e Func<bool> correspondentes aos Execute métodos e CanExecute . Esta ação e função podem ser definidas como funções lambda no Command construtor:

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();
    }
    ···
}

Quando o usuário clica no botão Novo , a execute função passada para o Command construtor é executada. Isso cria um novo PersonViewModel objeto, define um manipulador no evento desse PropertyChanged objeto, define IsEditing como true, e chama o método RefreshCanExecutes definido após o construtor.

Além de implementar a ICommand interface, a Command classe também define um método chamado ChangeCanExecute. O seu viewmodel deve chamar ChangeCanExecute para a propriedade ICommand sempre que algo acontecer que possa alterar o valor de retorno do método CanExecute. Um chamado a ChangeCanExecute faz com que a classe Command dispare o evento CanExecuteChanged. O Button anexou um manipulador para esse evento e responde chamando CanExecute novamente e, em seguida, habilitando-se com base no valor de retorno desse método.

Quando o execute método de NewCommand chama RefreshCanExecutes, a NewCommand propriedade recebe uma chamada para ChangeCanExecute, e o método Button é chamado por canExecute, que agora retorna false porque a propriedade IsEditing está agora true.

O PropertyChanged manipulador para o objeto PersonViewModel novo chama o 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;
            });
        ···
    }
    ···
}

A função canExecute para SubmitCommand é chamada sempre que uma propriedade for alterada no objeto PersonViewModel que está a ser editado. Retorna true apenas quando a propriedade Name tem pelo menos um caractere e Age é maior que 0. Nesse momento, o botão Enviar fica ativado.

A execute função para Submit remove o manipulador de propriedade alterada do PersonViewModel, adiciona o objeto à Persons coleção e retorna tudo ao seu estado inicial.

A execute função para o botão Cancelar faz tudo o que o botão Enviar faz, exceto adicionar o objeto à coleção:

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

O canExecute método retorna true a qualquer momento em que um PersonViewModel está sendo editado.

Observação

Não é necessário definir os execute métodos e canExecute como funções lambda. Você pode escrevê-los como métodos privados no viewmodel e fazer referência a eles nos Command construtores. No entanto, essa abordagem pode resultar em muitos métodos que são referenciados apenas uma vez no viewmodel.

Usando parâmetros de comando

Às vezes, é conveniente que um ou mais botões ou outros objetos de interface do usuário compartilhem a mesma propriedade do ICommand no modelo de visualização. Nesse caso, você pode usar a CommandParameter propriedade para distinguir entre os botões.

Você pode continuar a usar a Command classe para essas propriedades compartilhadas ICommand . A classe define um construtor alternativo que aceita execute e canExecute métodos com parâmetros do tipo Object. É assim que o CommandParameter é passado para esses métodos. No entanto, ao especificar um CommandParameter, é mais fácil usar a classe genérica Command<T> para especificar o tipo do objeto definido como CommandParameter. Os métodos execute e canExecute que você especificar têm parâmetros desse tipo.

O exemplo a seguir demonstra um teclado para inserir números decimais:

<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>

Neste exemplo, o BindingContext da página é um DecimalKeypadViewModel. A Entry propriedade deste viewmodel está vinculada à Text propriedade de um Label. Todos os Button objetos estão vinculados a comandos no viewmodel: ClearCommand, BackspaceCommande DigitCommand. Os 11 botões para os 10 dígitos e o ponto decimal compartilham uma ligação com DigitCommand. O CommandParameter distingue entre estes botões. O valor definido como CommandParameter é geralmente o mesmo que o texto exibido pelo botão, exceto para o ponto decimal, que para fins de clareza é exibido com um caractere de ponto do meio:

Teclado decimal.

O DecimalKeypadViewModel define uma Entry propriedade de tipo string e três propriedades 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; }
}

O botão correspondente ao ClearCommand está sempre ativado e repõe a entrada para "0".

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

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

Como o botão está sempre habilitado, não é necessário especificar um canExecute argumento no Command construtor.

O botão Backspace é ativado somente quando o comprimento da entrada é maior que 1, ou se Entry não for igual à string "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";
            });
        ···
    }
    ···
}

A lógica para a função execute do botão Retroceder garante que o Entry seja pelo menos uma cadeia de caracteres composta apenas por "0".

A DigitCommand propriedade está vinculada a 11 botões, cada um dos quais se identifica com a CommandParameter propriedade. O DigitCommand é definido como uma instância da Command<T> classe. Ao usar a interface de comando com XAML, as CommandParameter propriedades geralmente são strings, que é o tipo do argumento genérico. As execute funções e canExecute têm então argumentos do 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("."));
            });
    }
    ···
}

O execute método acrescenta o argumento string à Entry propriedade. No entanto, se o resultado começar com um zero (mas não um zero e um ponto decimal), então esse zero inicial deve ser removido usando a Substring função. O canExecute método retorna false somente se o argumento for o ponto decimal (indicando que o ponto decimal está sendo pressionado) e Entry já contiver um ponto decimal. Todos os execute métodos chamam RefreshCanExecutes, que por sua vez chama ChangeCanExecute tanto para DigitCommand quanto para ClearCommand. Isso garante que os botões de ponto decimal e backspace estejam habilitados ou desabilitados com base na sequência atual de dígitos inseridos.