Compartilhar via


Comandando

Navegar na amostra. Navegar na amostra

Em um aplicativo .NET MAUI (Interface de Usuário de Aplicativo Multiplataforma do .NET) 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 derivada de INotifyPropertyChanged, e propriedades na view, 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 arquivo code-behind em um manipulador para o evento de Clicked de Button ou o evento de Tapped de um TapGestureRecognizer.

A interface de comando fornece uma abordagem alternativa para implementar comandos que são muito mais adequados para a arquitetura MVVM. O viewmodel pode conter comandos, que são métodos executados em reação a uma atividade específica na visão, 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, define Button duas propriedades:

Para usar a interface de comando, você define uma vinculação de dados direcionada à propriedade Command do Button onde a origem é uma propriedade no viewmodel do tipo ICommand. O viewmodel contém o código associado à propriedade ICommand que é executado quando o botão é clicado. Você pode definir a CommandParameter propriedade como dados arbitrários para distinguir entre vários botões se todos eles estiverem associados à mesma ICommand propriedade no viewmodel.

Muitas outras exibições também definem propriedades Command e CommandParameter. Todos esses comandos podem ser tratados em um viewmodel usando uma abordagem que não depende do objeto de 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, o viewmodel deve conter propriedades do tipo ICommand:

public ICommand MyCommand { private set; get; }

O viewmodel também deve referenciar uma classe que implementa a ICommand interface. Na visualização, a propriedade Command de um Button está vinculada a essa propriedade:

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

Quando o usuário pressiona o Button, o Button chama o método Execute no objeto ICommand associado à sua propriedade Command.

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

O Button também anexa um manipulador no evento CanExecuteChanged de ICommand. O evento deve ser gerado manualmente a partir do viewmodel sempre que as condições forem alteradas, afetando o CanExecute resultado. Quando esse evento é acionado, Button chama CanExecute novamente. Habilita-se o Button se CanExecute retornar true e desabilita-se se CanExecute retornar false.

Importante

Ao contrário de algumas estruturas de interface do usuário (como o WPF), o MAUI do .NET não detecta automaticamente quando o valor retornado CanExecute pode ser alterado. Você deve gerar o evento CanExecuteChanged manualmente (ou chamar ChangeCanExecute() na classe Command) sempre que qualquer condição seja alterada que afete CanExecute resultado. Isso normalmente é feito quando as propriedades das quais CanExecute depende são modificadas.

Observação

Você também pode usar a propriedade IsEnabled do Button em vez do método CanExecute ou em conjunto com ele. No .NET MAUI 7 e anteriores, não era possível usar a propriedade IsEnabled com a interface de comandos Button, pois o valor retornado da função CanExecute sempre substituía a propriedade IsEnabled. Isso é corrigido no .NET MAUI 8 e superior; a propriedade IsEnabled agora é utilizável em comandos baseados em Button. No entanto, lembre-se de que tanto a propriedade IsEnabled quanto o método CanExecute agora devem retornar true para que o seja habilitado (e o controle pai também deve ser habilitado).

Quando o 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. Essa classe deve conter ou fazer referência aos Execute métodos e CanExecute acionar manualmente o CanExecuteChanged evento sempre que o CanExecute método pode retornar um valor diferente. Você pode usar a classe Command ou a Command<T> incluída no .NET MAUI para implementar a interface ICommand. Essas classes permitem que você especifique os corpos dos métodos Execute e CanExecute nos construtores de classe.

Dica

Use Command<T> quando você utilizar a propriedade CommandParameter para distinguir entre várias exibições vinculadas à mesma propriedade ICommand, e a classe Command quando isso não for necessário.

Comando básico

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

A PersonViewModel classe define três propriedades nomeadas Namee AgeSkills 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 de tipo PersonViewModel e permite que o usuário preencha os dados. Para essa finalidade, a classe define IsEditing, do tipo bool, e PersonEdit, do tipo PersonViewModel, propriedades. Além disso, a classe define três propriedades do tipo ICommand e uma propriedade nomeada Persons do 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 ICommand propriedades e na Persons propriedade não resultam em PropertyChanged eventos sendo disparados. Todas essas propriedades são 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 BindingContext página é definida como PersonCollectionViewModel. O Grid contém um Button com o texto Novo com sua Command propriedade 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. Exibe ListView o grupo de pessoas já cadastradas.

A captura de tela a seguir mostra o botão Enviar habilitado após a definição de uma idade:

Registro 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 começar de novo. Somente quando um nome e uma idade válida tiverem sido inseridos é o botão Enviar habilitado. 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 entrada será desmarcado e o botão Novo será habilitado novamente.

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

Um construtor da classe Command permite que você passe argumentos do tipo Action e Func<bool>, correspondentes aos métodos Execute e CanExecute. Essa 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 objeto PersonViewModel, define um manipulador no evento PropertyChanged desse 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. Seu viewmodel deve chamar ChangeCanExecute para uma propriedade ICommand sempre que algo acontecer que possa alterar o valor retornado do método CanExecute. Uma chamada para 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 chama o método ButtoncanExecute, que agora retorna false porque a propriedade IsEditing está agora true.

O PropertyChanged manipulador do 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 é chamada SubmitCommand sempre que uma propriedade é alterada no objeto PersonViewModel que está sendo editado. Ele retorna true somente quando a Name propriedade tem pelo menos um caractere de comprimento e Age é maior que 0. Nesse momento, o botão Enviar fica habilitado.

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

A execute função do 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 que um PersonViewModel está sendo editado.

Observação

Não é necessário definir os métodos execute e canExecute como funções lambda. Você pode gravá-los como métodos privados no viewmodel e referenciá-los 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 ICommand propriedade no ViewModel. 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 os métodos execute e canExecute 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 execute métodos e os canExecute métodos especificados 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, a página BindingContext é um DecimalKeypadViewModel. A propriedade Entry deste viewmodel está ligada à propriedade Text de um Label. Todos os Button objetos são associados a comandos no viewmodel: ClearCommand, e BackspaceCommandDigitCommand. Os 11 botões para os 10 dígitos e o ponto decimal compartilham uma associação para DigitCommand. O CommandParameter distingue entre esses botões. O valor definido para CommandParameter é geralmente o mesmo que o texto exibido pelo botão, exceto pelo ponto decimal, que para fins de clareza é exibido com um caractere de ponto médio:

Teclado decimal.

O DecimalKeypadViewModel define uma Entry propriedade do tipo string e três propriedades do 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 habilitado e retorna 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 é habilitado somente quando o comprimento da entrada é maior que 1 ou se Entry não for igual à cadeia de caracteres "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 da função execute para o botão Backspace garante que Entry seja, no mínimo, uma cadeia de caracteres "0".

A DigitCommand propriedade está associada a 11 botões, cada um dos quais se identifica com a CommandParameter propriedade. O DigitCommand é definido como uma instância da classe Command<T>. Ao usar a interface de comando com XAML, as CommandParameter propriedades geralmente são cadeias de caracteres, que é o tipo do argumento genérico. As funções execute e canExecute têm 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 de cadeia de caracteres à Entry propriedade. No entanto, se o resultado começar com um zero (mas não um zero e um ponto decimal), esse zero inicial deverá ser removido usando a Substring função. O canExecute método retornará 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 então chama ChangeCanExecute para ambos DigitCommand e 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.