A Xamarin.Forms interface de comando

Na arquitetura MVVM (Model-View-ViewModel), as associações de dados são definidas entre propriedades no ViewModel, que geralmente é uma classe derivada de INotifyPropertyChanged, e propriedades no View, que geralmente é o arquivo XAML. Às vezes, um aplicativo tem necessidades que vão além dessas associações de propriedade ao exigir que o usuário inicie os comandos que afetam algo no ViewModel. Esses comandos geralmente são sinalizados por cliques de botões ou toques de dedos e são tradicionalmente processados no arquivo code-behind em um manipulador para o evento Clicked do Button ou o evento Tapped de um TapGestureRecognizer.

A interface de comando oferece uma abordagem alternativa à implementação de comandos, que é bem mais adequada à arquitetura MVVM. O próprio ViewModel pode conter comandos, que são métodos executados em reação a uma atividade específica no View, como um clique Button. 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, defina uma associação de dados que direcione a propriedade Command do Button, em que a origem é uma propriedade no ViewModel do tipo ICommand. O ViewModel contém um código associado a essa propriedade ICommand executada quando se clica no botão. É possível definir CommandParameter como dados arbitrários para distinguir entre vários botões se eles forem todos associados à mesma propriedade ICommand no ViewModel.

As propriedades Command e CommandParameter também são definidas pelas seguintes classes:

SearchBar define uma propriedade SearchCommand do tipo ICommand e uma propriedade SearchCommandParameter. A propriedade RefreshCommand de ListView também é do tipo ICommand.

Todos esses comandos podem ser manipulados dentro de um ViewModel, de forma que ele não dependa do objeto específico da interface do usuário no View.

A interface ICommand

A System.Windows.Input.ICommand interface não faz parte do Xamarin.Forms. Ela é definida no lugar do 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 contém as propriedades do tipo ICommand:

public ICommand MyCommand { private set; get; }

O ViewModel também deve referenciar uma classe que implementa a interface ICommand. Essa classe será descrita em breve. No View, a propriedade Command de um Button é associada 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 a sua propriedade Command. Essa é a parte mais simples da interface de comando.

O método CanExecute é mais complexo. Quando a associação é definida na propriedade Command do Button pela primeira vez 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 desabilitará. Isso indica que o comando específico não está disponível no momento ou é inválido.

O Button também anexa um manipulador no evento CanExecuteChanged de ICommand. O evento é acionado de dentro do ViewModel. Quando o evento é acionado, o Button chama o CanExecute novamente. O Button habilita a si mesmo se o CanExecute retorna true e desabilita a si mesmo se CanExecute retorna false.

Importante

Não use a propriedade IsEnabled de Button se estiver usando a interface de comando.

A classe de comando

Quando o ViewModel define uma propriedade do tipo ICommand, ele também deve conter ou referenciar uma classe que implementa a interface ICommand. Essa classe deverá conter ou referenciar os métodos Execute e CanExecute e acionar o evento CanExecuteChanged sempre que o método CanExecute puder retornar um valor diferente.

É possível gravar essa classe sozinho ou usar uma classe que alguém tenha gravado. Como o ICommand faz parte do Microsoft Windows, ele tem sido usado por anos com aplicativos da MVVM do Windows. O uso de uma classe Windows implementa ICommand permite que você compartilhe seus ViewModels entre aplicativos e Xamarin.Forms aplicativos do Windows.

Se o compartilhamento de ViewModels entre Windows e Xamarin.Forms não for uma preocupação, você poderá usar a Command classe or Command<T> incluída para Xamarin.Forms implementar a ICommand interface. Essas classes permitem que você especifique os corpos dos métodos Execute e CanExecute em construtores de classe. Use Command<T> quando você usar a propriedade CommandParameter para distinguir entre vários modos de exibição associados à mesma propriedade ICommand, e a classe Command mais simples quando isso não for um requisito.

Comando básico

A página Entrada de Pessoa no programa de exemplo demonstra alguns comandos simples implementados em um ViewModel.

O PersonViewModel define três propriedades denominadas Name, Age e Skills que definem uma pessoa. Essa classe não contém nenhuma propriedade ICommand:

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

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

Essa listagem abreviada não inclui o construtor da classe, que é onde as três propriedades do tipo ICommand são definidas, que serão mostradas em breve. Observe que as alterações nas três propriedades do tipo ICommand e a propriedade Persons não faz os eventos PropertyChanged serem acionados. Todas essas propriedades são definidas quando a classe é criada pela primeira vez e não são alteradas depois disso.

Antes de examinar o construtor da classe PersonCollectionViewModel, vamos examinar se há o programa Entrada de pessoa no arquivo XAML. Ele contém um Grid com sua propriedade BindingContext definida como o PersonCollectionViewModel. O Grid contém um Button com o texto Novo com sua propriedade Command associada à propriedade NewCommand no ViewModel, um formulário de entrada com propriedades associadas à propriedade IsEditing, bem como propriedades de PersonViewModel e mais dois botões associados às propriedades SubmitCommand e CancelCommand do ViewModel. O último ListView exibe a coleção de pessoas já que já entraram:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DataBindingDemos"
             x:Class="DataBindingDemos.PersonEntryPage"
             Title="Person Entry">
    <Grid Margin="10">
        <Grid.BindingContext>
            <local:PersonCollectionViewModel />
        </Grid.BindingContext>

        <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="CenterAndExpand" />

            <Button Text="Cancel"
                    Grid.Column="1"
                    Command="{Binding CancelCommand}"
                    VerticalOptions="CenterAndExpand" />
        </Grid>

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

Veja como ele funciona: o usuário primeiro pressiona o botão Novo. Isso habilita o formulário de entrada, mas desabilita o botão Novo. O usuário insere, então, 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 depois que um nome e uma idade válida forem inseridos, o botão Enviar será habilitado. Pressionar esse botão Enviar transfere a pessoa para a coleção exibida pelo ListView. Depois que o botão Cancelar ou Enviar é pressionado, o formulário de entrada é limpo e o botão Novo é habilitado novamente.

A tela do iOS à esquerda mostra o layout antes que uma idade válida seja inserida. A tela do Android mostra o botão Enviar ativado após a definição de uma idade:

Entrada de pessoa

O programa não tem nenhum recurso para editar entradas existentes nem salva as entradas quando você sai da página.

Toda a lógica dos botões Novo, Enviar, e Cancelar é tratado 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. É mais fácil definir essas ações e funções como funções lambda direto no construtor Command. Veja a definição do objeto Command para a propriedade NewCommand:

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 função execute passada para o construtor Command é 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 interface ICommand, a classe Command também define um método chamado ChangeCanExecute. O ViewModel deve chamar ChangeCanExecute para uma propriedade ICommand sempre que acontecer qualquer coisa que possa alterar o valor retornado do método CanExecute. Uma chamada ao ChangeCanExecute faz a classe Command acionar o método CanExecuteChanged. O Button anexou um manipulador para o evento, responde chamando CanExecute novamente e permitindo que se baseie no valor retornado desse método.

Quando o método execute de NewCommand chama RefreshCanExecutes, a propriedade NewCommand obtém uma chamada a ChangeCanExecute, e o Button chama o método canExecute, que agora retorna false porque a propriedade IsEditing agora é true.

O manipulador PropertyChanged do novo objeto PersonViewModel chama o método ChangeCanExecute de SubmitCommand. Veja como essa propriedade de comando é implementada:

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 há uma propriedade alterada no objeto PersonViewModel que está sendo editado. Ela retorna true somente quando a propriedade Name 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 propriedade alterada do PersonViewModel, adiciona o objeto à coleção Persons e retorna tudo para as condições iniciais.

A função execute 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 método canExecute retorna true a qualquer momento que um PersonViewModel está sendo editado.

Essas técnicas poderiam ser adaptadas para cenários mais complexos: uma propriedade no PersonCollectionViewModel poderia estar associada à propriedade SelectedItem do ListView para editar itens existentes e um botão Excluir poderia ser adicionado para excluir esses itens.

Não é necessário definir os métodos execute e canExecute como funções lambda. É possível gravá-los como métodos privados regulares no ViewModel e referenciá-los nos construtores Command. No entanto, essa abordagem tende a resultar em uma grande quantidade de métodos referenciados apenas uma vez no ViewModel.

Uso de parâmetros de comando

Às vezes, é conveniente para um ou mais botões (ou outros objetos da interface do usuário) compartilhar a mesma propriedade ICommand no ViewModel. Nesse caso, você usa a propriedade CommandParameter para distinguir entre os botões.

É possível continuar usando a classe Command para essas propriedades ICommand compartilhadas. A classe define um construtor alternativo que aceita os métodos execute e canExecute com parâmetros do tipo Object. Isso é como o CommandParameter é passado para esses métodos.

No entanto, ao usar CommandParameter, é mais fácil de usar a classe Command<T> genérica para especificar o tipo do objeto definido como CommandParameter. Os métodos execute e canExecute que você especificar têm parâmetros desse tipo.

A página Teclado decimal ilustra essa técnica mostrando como implementar um teclado numérico para inserir números decimais. O BindingContext para o Grid é um DecimalKeypadViewModel. A propriedade Entry desse ViewModel está associada à propriedade Text de um Label. Todos os objetos Button estão associados a vários comandos no ViewModel: ClearCommand, BackspaceCommand e DigitCommand:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DataBindingDemos"
             x:Class="DataBindingDemos.DecimalKeypadPage"
             Title="Decimal Keyboard">

    <Grid WidthRequest="240"
          HeightRequest="480"
          ColumnSpacing="2"
          RowSpacing="2"
          HorizontalOptions="Center"
          VerticalOptions="Center">

        <Grid.BindingContext>
            <local:DecimalKeypadViewModel />
        </Grid.BindingContext>

        <Grid.Resources>
            <ResourceDictionary>
                <Style TargetType="Button">
                    <Setter Property="FontSize" Value="32" />
                    <Setter Property="BorderWidth" Value="1" />
                    <Setter Property="BorderColor" Value="Black" />
                </Style>
            </ResourceDictionary>
        </Grid.Resources>

        <Label Text="{Binding Entry}"
               Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"
               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>

Os 11 botões para os 10 dígitos e o ponto decimal compartilham uma associação a DigitCommand. O CommandParameter faz distinção entre esses 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 esclarecimento, é exibido com um caractere de ponto no meio.

Veja o programa em ação:

Teclado decimal

Observe que o botão para o ponto decimal em todas as três capturas de tela está desabilitado, porque o número inserido já contém um ponto decimal.

O DecimalKeypadViewModel define uma propriedade Entry do tipo string (que é a única propriedade que dispara um evento PropertyChanged) 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 simplesmente define a entrada de volta 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 sempre está habilitado, não é necessário especificar um argumento canExecute no construtor Command.

A lógica para inserir números e backspace é um pouco complicada, porque se nenhum dígito foi inserido, a propriedade Entry será a cadeia de caracteres "0". Se o usuário digitar mais zeros, então o Entry ainda conterá apenas um zero. Se o usuário digita qualquer outro dígito, esse dígito substitui o zero. Mas se o usuário digita um ponto decimal antes de qualquer outro dígito, então Entry é a cadeia de caracteres "0".

O botão Backspace é habilitado apenas quando o comprimento da entrada é maior que 1 ou se Entry não é 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 para a função execute do botão Backspace garante que o Entry é pelo menos uma cadeia de caracteres de comprimento "0".

A propriedade DigitCommand está associada a 11 botões, cada um dos quais identifica-se com a propriedade CommandParameter. O DigitCommand poderia ser definido como uma instância da classe Command regular, mas é mais fácil usar a classe genérica Command<T>. Ao usar a interface de comando com o XAML, as propriedades CommandParameter geralmente são cadeias de caracteres e esse é o tipo de 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 método execute acrescenta o argumento de cadeia de caracteres à propriedade Entry. 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 função Substring.

O método canExecute retornará false somente se o argumento for o ponto decimal (que indica que o ponto decimal está sendo pressionado) e Entry já contém um ponto decimal.

Todos os métodos execute chamam RefreshCanExecutes, que chama ChangeCanExecute para DigitCommand e ClearCommand. Isso garante que o ponto decimal e os botões backspace estão habilitados ou desabilitados com base na sequência atual de dígitos inseridos.

Comando assíncrono para menus de navegação

O comando é conveniente para implementar menus de navegação. Veja uma parte do MainPage.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DataBindingDemos"
             x:Class="DataBindingDemos.MainPage"
             Title="Data Binding Demos"
             Padding="10">
    <TableView Intent="Menu">
        <TableRoot>
            <TableSection Title="Basic Bindings">

                <TextCell Text="Basic Code Binding"
                          Detail="Define a data-binding in code"
                          Command="{Binding NavigateCommand}"
                          CommandParameter="{x:Type local:BasicCodeBindingPage}" />

                <TextCell Text="Basic XAML Binding"
                          Detail="Define a data-binding in XAML"
                          Command="{Binding NavigateCommand}"
                          CommandParameter="{x:Type local:BasicXamlBindingPage}" />

                <TextCell Text="Alternative Code Binding"
                          Detail="Define a data-binding in code without a BindingContext"
                          Command="{Binding NavigateCommand}"
                          CommandParameter="{x:Type local:AlternativeCodeBindingPage}" />

                ···

            </TableSection>
        </TableRoot>
    </TableView>
</ContentPage>

Ao usar comandos com XAML, as propriedades CommandParameter geralmente são definidas como cadeias de caracteres. No entanto, nesse caso, uma extensão de marcação XAML é usada para que o CommandParameter seja do tipo System.Type.

Cada propriedade Command está associada a uma propriedade chamada NavigateCommand. Se a propriedade estiver definida no arquivo code-behind, MainPage.xaml.cs:

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();

        NavigateCommand = new Command<Type>(
            async (Type pageType) =>
            {
                Page page = (Page)Activator.CreateInstance(pageType);
                await Navigation.PushAsync(page);
            });

        BindingContext = this;
    }

    public ICommand NavigateCommand { private set; get; }
}

O construtor define a propriedade NavigateCommand como um método execute que cria a instância do parâmetro System.Type e navega até ela. Como a chamada PushAsync requer um operador await, o método execute deve ser sinalizado como assíncrono. Isso é feito com a palavra-chave async antes da lista de parâmetros.

O construtor também define o BindingContext da página como a si mesmo para que as associações referenciem o NavigateCommand nessa classe.

A ordem do código nesse construtor faz a diferença: a chamada InitializeComponent faz o XAML ser analisado, mas, nesse momento, a associação a uma propriedade denominada NavigateCommand não pode ser resolvida, porque BindingContext é definido como null. Se o BindingContext for definido no construtor antes de NavigateCommand ser definido, a associação poderá ser resolvida quando BindingContext for definida, mas nesse momento, NavigateCommand ainda nullé . Definir NavigateCommand após BindingContext não terá efeito na associação, porque uma alteração no NavigateCommand não dispara um evento PropertyChanged, e a associação não sabe que NavigateCommand é válido agora.

Definir NavigateCommand e BindingContext (em qualquer ordem) antes da chamada a InitializeComponent funcionará, porque os componentes da associação são definidos quando o analisador XAML encontra a definição da associação.

Às vezes, as associações de dados podem ser complicadas, mas, como você viu nessa série de artigos, elas são poderosas e versáteis e ajudam bastante a organizar seu código separando a lógica subjacente da interface do usuário.