Compartilhar via


Parte 5. De associações de dados a MVVM

O padrão de arquitetura MVVM (Model-View-ViewModel) foi inventado com XAML em mente. O padrão impõe uma separação entre três camadas de software — a interface do usuário XAML, chamada de View; os dados subjacentes, chamados de Modelo; e um intermediário entre o View e o Model, chamado ViewModel. O View e o ViewModel geralmente são conectados por meio de associações de dados definidas no arquivo XAML. O BindingContext para o View é geralmente uma instância do ViewModel.

Um ViewModel simples

Como uma introdução ao ViewModels, vamos primeiro olhar para um programa sem um. Anteriormente, você viu como definir uma nova declaração de namespace XML para permitir que um arquivo XAML faça referência a classes em outros assemblies. Aqui está um programa que define uma declaração de namespace XML para o System namespace:

xmlns:sys="clr-namespace:System;assembly=netstandard"

O programa pode usar x:Static para obter a data e hora atuais da propriedade estática DateTime.Now e definir esse DateTime valor como on a BindingContextStackLayout:

<StackLayout BindingContext="{x:Static sys:DateTime.Now}" …>

BindingContext é uma propriedade especial: Quando você define o BindingContext em um elemento, ele é herdado por todos os filhos desse elemento. Isso significa que todos os filhos do StackLayout têm esse mesmo BindingContext, e eles podem conter ligações simples às propriedades desse objeto.

No programa One-Shot DateTime, dois dos filhos contêm ligações a propriedades desse DateTime valor, mas dois outros filhos contêm ligações que parecem estar faltando um caminho de vinculação. Isso significa que o DateTime próprio valor é usado para o StringFormat:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:sys="clr-namespace:System;assembly=netstandard"
             x:Class="XamlSamples.OneShotDateTimePage"
             Title="One-Shot DateTime Page">

    <StackLayout BindingContext="{x:Static sys:DateTime.Now}"
                 HorizontalOptions="Center"
                 VerticalOptions="Center">

        <Label Text="{Binding Year, StringFormat='The year is {0}'}" />
        <Label Text="{Binding StringFormat='The month is {0:MMMM}'}" />
        <Label Text="{Binding Day, StringFormat='The day is {0}'}" />
        <Label Text="{Binding StringFormat='The time is {0:T}'}" />

    </StackLayout>
</ContentPage>

O problema é que a data e a hora são definidas uma vez quando a página é criada pela primeira vez, e nunca mudam:

Exibir data e hora de exibição

Um arquivo XAML pode exibir um relógio que sempre mostra a hora atual, mas precisa de algum código para ajudar. Ao pensar em termos de MVVM, o Model e ViewModel são classes escritas inteiramente em código. O View geralmente é um arquivo XAML que faz referência a propriedades definidas no ViewModel por meio de associações de dados.

Um Modelo apropriado é ignorante do ViewModel, e um ViewModel apropriado é ignorante do View. No entanto, muitas vezes, um programador adapta os tipos de dados expostos pelo ViewModel aos tipos de dados associados a interfaces de usuário específicas. Por exemplo, se um Model acessa um banco de dados que contém cadeias de caracteres ASCII de 8 bits, o ViewModel precisará converter entre essas cadeias de caracteres em cadeias de caracteres Unicode para acomodar o uso exclusivo de Unicode na interface do usuário.

Em exemplos simples de MVVM (como os mostrados aqui), muitas vezes não há nenhum modelo, e o padrão envolve apenas um View e ViewModel vinculados a associações de dados.

Aqui está um ViewModel para um relógio com apenas uma única propriedade chamada DateTime, que atualiza essa DateTime propriedade a cada segundo:

using System;
using System.ComponentModel;
using Xamarin.Forms;

namespace XamlSamples
{
    class ClockViewModel : INotifyPropertyChanged
    {
        DateTime dateTime;

        public event PropertyChangedEventHandler PropertyChanged;

        public ClockViewModel()
        {
            this.DateTime = DateTime.Now;

            Device.StartTimer(TimeSpan.FromSeconds(1), () =>
                {
                    this.DateTime = DateTime.Now;
                    return true;
                });
        }

        public DateTime DateTime
        {
            set
            {
                if (dateTime != value)
                {
                    dateTime = value;

                    if (PropertyChanged != null)
                    {
                        PropertyChanged(this, new PropertyChangedEventArgs("DateTime"));
                    }
                }
            }
            get
            {
                return dateTime;
            }
        }
    }
}

ViewModels geralmente implementam a interface, o INotifyPropertyChanged que significa que a classe dispara um PropertyChanged evento sempre que uma de suas propriedades é alterada. O mecanismo de vinculação de dados em Xamarin.Forms anexa um manipulador a esse PropertyChanged evento para que ele possa ser notificado quando uma propriedade for alterada e manter o destino atualizado com o novo valor.

Um relógio baseado neste ViewModel pode ser tão simples como isto:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.ClockPage"
             Title="Clock Page">

    <Label Text="{Binding DateTime, StringFormat='{0:T}'}"
           FontSize="Large"
           HorizontalOptions="Center"
           VerticalOptions="Center">
        <Label.BindingContext>
            <local:ClockViewModel />
        </Label.BindingContext>
    </Label>
</ContentPage>

Observe como o ClockViewModel é definido como o BindingContext das marcas de elemento de Label propriedade de uso. Como alternativa, você pode instanciar o ClockViewModel em uma Resources coleção e defini-lo como o através de uma StaticResource extensão de BindingContext marcação. Ou, o arquivo code-behind pode instanciar o ViewModel.

A Binding extensão de marcação na Text propriedade dos formata Label a DateTime propriedade. Aqui está a exibição:

Exibir data e hora de exibição via ViewModel

Também é possível acessar propriedades individuais da DateTime propriedade do ViewModel separando as propriedades com pontos:

<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >

MVVM interativo

O MVVM é frequentemente usado com associações de dados bidirecionais para uma exibição interativa com base em um modelo de dados subjacente.

Aqui está uma classe chamada HslViewModel que converte um Color valor em Hue, Saturatione Luminosity valores, e vice-versa:

using System;
using System.ComponentModel;
using Xamarin.Forms;

namespace XamlSamples
{
    public class HslViewModel : INotifyPropertyChanged
    {
        double hue, saturation, luminosity;
        Color color;

        public event PropertyChangedEventHandler PropertyChanged;

        public double Hue
        {
            set
            {
                if (hue != value)
                {
                    hue = value;
                    OnPropertyChanged("Hue");
                    SetNewColor();
                }
            }
            get
            {
                return hue;
            }
        }

        public double Saturation
        {
            set
            {
                if (saturation != value)
                {
                    saturation = value;
                    OnPropertyChanged("Saturation");
                    SetNewColor();
                }
            }
            get
            {
                return saturation;
            }
        }

        public double Luminosity
        {
            set
            {
                if (luminosity != value)
                {
                    luminosity = value;
                    OnPropertyChanged("Luminosity");
                    SetNewColor();
                }
            }
            get
            {
                return luminosity;
            }
        }

        public Color Color
        {
            set
            {
                if (color != value)
                {
                    color = value;
                    OnPropertyChanged("Color");

                    Hue = value.Hue;
                    Saturation = value.Saturation;
                    Luminosity = value.Luminosity;
                }
            }
            get
            {
                return color;
            }
        }

        void SetNewColor()
        {
            Color = Color.FromHsla(Hue, Saturation, Luminosity);
        }

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

As alterações nas Huepropriedades , Saturatione Luminosity fazem com que a Color propriedade seja alterada e as alterações fazem Color com que as outras três propriedades sejam alteradas. Isso pode parecer um loop infinito, exceto que a classe não invoca o PropertyChanged evento, a menos que a propriedade tenha sido alterada. Isso põe fim ao loop de feedback incontrolável.

O arquivo XAML a seguir contém uma BoxView propriedade cuja Color está vinculada à Color propriedade ViewModel e três Slider e três Label modos de exibição vinculados às Huepropriedades , Saturatione Luminosity :

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.HslColorScrollPage"
             Title="HSL Color Scroll Page">
    <ContentPage.BindingContext>
        <local:HslViewModel Color="Aqua" />
    </ContentPage.BindingContext>

    <StackLayout Padding="10, 0">
        <BoxView Color="{Binding Color}"
                 VerticalOptions="FillAndExpand" />

        <Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
               HorizontalOptions="Center" />

        <Slider Value="{Binding Hue, Mode=TwoWay}" />

        <Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
               HorizontalOptions="Center" />

        <Slider Value="{Binding Saturation, Mode=TwoWay}" />

        <Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
               HorizontalOptions="Center" />

        <Slider Value="{Binding Luminosity, Mode=TwoWay}" />
    </StackLayout>
</ContentPage>

A associação em cada um Label é o padrão OneWay. Ele só precisa exibir o valor. Mas a vinculação em cada um Slider é TwoWay. Isso permite que o Slider seja inicializado a partir do ViewModel. Observe que a Color propriedade é definida como Aqua quando o ViewModel é instanciado. Mas uma alteração no Slider também precisa definir um novo valor para a propriedade no ViewModel, que calcula uma nova cor.

MVVM usando ligações de dados bidirecionais

Comandando com ViewModels

Em muitos casos, o padrão MVVM é restrito à manipulação de itens de dados: objetos de interface do usuário no Exibir objetos de dados paralelos no ViewModel.

No entanto, às vezes, o View precisa conter botões que disparam várias ações no ViewModel. Mas o ViewModel não deve conter Clicked manipuladores para os botões porque isso vincularia o ViewModel a um paradigma de interface do usuário específico.

Para permitir que ViewModels seja mais independente de objetos de interface do usuário específicos, mas ainda permitir que métodos sejam chamados dentro do ViewModel, existe uma interface de comando . Essa interface de comando é suportada pelos seguintes elementos no Xamarin.Forms:

  • Button
  • MenuItem
  • ToolbarItem
  • SearchBar
  • TextCell (e, portanto, também ImageCell)
  • ListView
  • TapGestureRecognizer

Com exceção do SearchBar elemento and ListView , esses elementos definem duas propriedades:

  • Command do tipo System.Windows.Input.ICommand
  • CommandParameter do tipo Object

O SearchBar define SearchCommand e SearchCommandParameter propriedades, enquanto o ListView define uma RefreshCommand propriedade do tipo ICommand.

A ICommand interface define dois métodos e um evento:

  • void Execute(object arg)
  • bool CanExecute(object arg)
  • event EventHandler CanExecuteChanged

O ViewModel pode definir propriedades do tipo ICommand. Em seguida, você pode vincular essas propriedades à Command propriedade de cada Button um ou outro elemento, ou talvez a um modo de exibição personalizado que implemente essa interface. Opcionalmente, você pode definir a CommandParameter propriedade para identificar objetos individuais Button (ou outros elementos) que estão vinculados a essa propriedade ViewModel. Internamente, o Button chama o Execute método sempre que o usuário toca no Button, passando para o Execute método o seu CommandParameter.

O CanExecute método e CanExecuteChanged o evento são usados para casos em que um Button toque pode ser inválido no momento, caso em que o Button deve se desabilitar. As Button chamadas CanExecute quando a Command propriedade é definida pela primeira vez e sempre que o CanExecuteChanged evento é acionado. Se CanExecute retornar false, o Button se desativa e não gera Execute chamadas.

Para obter ajuda com a adição de comandos aos seus ViewModels, Xamarin.Forms define duas classes que implementam ICommand: Command e Command<T> onde T é o tipo dos argumentos para Execute e CanExecute. Essas duas classes definem vários construtores mais um ChangeCanExecute método que o ViewModel pode chamar para forçar o Command objeto a disparar o CanExecuteChanged evento.

Aqui está um ViewModel para um teclado simples que se destina a inserir números de telefone. Observe que o Execute método e CanExecute são definidos como funções lambda diretamente no construtor:

using System;
using System.ComponentModel;
using System.Windows.Input;
using Xamarin.Forms;

namespace XamlSamples
{
    class KeypadViewModel : INotifyPropertyChanged
    {
        string inputString = "";
        string displayText = "";
        char[] specialChars = { '*', '#' };

        public event PropertyChangedEventHandler PropertyChanged;

        // Constructor
        public KeypadViewModel()
        {
            AddCharCommand = new Command<string>((key) =>
                {
                    // Add the key to the input string.
                    InputString += key;
                });

            DeleteCharCommand = new Command(() =>
                {
                    // Strip a character from the input string.
                    InputString = InputString.Substring(0, InputString.Length - 1);
                },
                () =>
                {
                    // Return true if there's something to delete.
                    return InputString.Length > 0;
                });
        }

        // Public properties
        public string InputString
        {
            protected set
            {
                if (inputString != value)
                {
                    inputString = value;
                    OnPropertyChanged("InputString");
                    DisplayText = FormatText(inputString);

                    // Perhaps the delete button must be enabled/disabled.
                    ((Command)DeleteCharCommand).ChangeCanExecute();
                }
            }

            get { return inputString; }
        }

        public string DisplayText
        {
            protected set
            {
                if (displayText != value)
                {
                    displayText = value;
                    OnPropertyChanged("DisplayText");
                }
            }
            get { return displayText; }
        }

        // ICommand implementations
        public ICommand AddCharCommand { protected set; get; }

        public ICommand DeleteCharCommand { protected set; get; }

        string FormatText(string str)
        {
            bool hasNonNumbers = str.IndexOfAny(specialChars) != -1;
            string formatted = str;

            if (hasNonNumbers || str.Length < 4 || str.Length > 10)
            {
            }
            else if (str.Length < 8)
            {
                formatted = String.Format("{0}-{1}",
                                          str.Substring(0, 3),
                                          str.Substring(3));
            }
            else
            {
                formatted = String.Format("({0}) {1}-{2}",
                                          str.Substring(0, 3),
                                          str.Substring(3, 3),
                                          str.Substring(6));
            }
            return formatted;
        }

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Este ViewModel pressupõe que a AddCharCommand propriedade está vinculada à Command propriedade de vários botões (ou qualquer outra coisa que tenha uma interface de comando), cada um dos quais é identificado pelo CommandParameter. Esses botões adicionam caracteres a uma InputString propriedade, que é formatada como um número de telefone para a DisplayText propriedade.

Há também uma segunda propriedade do tipo ICommand chamada DeleteCharCommand. Isso está vinculado a um botão de espaçamento entre trás, mas o botão deve ser desativado se não houver caracteres para excluir.

O teclado a seguir não é tão visualmente sofisticado quanto poderia ser. Em vez disso, a marcação foi reduzida ao mínimo para demonstrar mais claramente o uso da interface de comando:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.KeypadPage"
             Title="Keypad Page">

    <Grid HorizontalOptions="Center"
          VerticalOptions="Center">
        <Grid.BindingContext>
            <local:KeypadViewModel />
        </Grid.BindingContext>

        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="80" />
            <ColumnDefinition Width="80" />
            <ColumnDefinition Width="80" />
        </Grid.ColumnDefinitions>

        <!-- Internal Grid for top row of items -->
        <Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <Frame Grid.Column="0"
                   OutlineColor="Accent">
                <Label Text="{Binding DisplayText}" />
            </Frame>

            <Button Text="&#x21E6;"
                    Command="{Binding DeleteCharCommand}"
                    Grid.Column="1"
                    BorderWidth="0" />
        </Grid>

        <Button Text="1"
                Command="{Binding AddCharCommand}"
                CommandParameter="1"
                Grid.Row="1" Grid.Column="0" />

        <Button Text="2"
                Command="{Binding AddCharCommand}"
                CommandParameter="2"
                Grid.Row="1" Grid.Column="1" />

        <Button Text="3"
                Command="{Binding AddCharCommand}"
                CommandParameter="3"
                Grid.Row="1" Grid.Column="2" />

        <Button Text="4"
                Command="{Binding AddCharCommand}"
                CommandParameter="4"
                Grid.Row="2" Grid.Column="0" />

        <Button Text="5"
                Command="{Binding AddCharCommand}"
                CommandParameter="5"
                Grid.Row="2" Grid.Column="1" />

        <Button Text="6"
                Command="{Binding AddCharCommand}"
                CommandParameter="6"
                Grid.Row="2" Grid.Column="2" />

        <Button Text="7"
                Command="{Binding AddCharCommand}"
                CommandParameter="7"
                Grid.Row="3" Grid.Column="0" />

        <Button Text="8"
                Command="{Binding AddCharCommand}"
                CommandParameter="8"
                Grid.Row="3" Grid.Column="1" />

        <Button Text="9"
                Command="{Binding AddCharCommand}"
                CommandParameter="9"
                Grid.Row="3" Grid.Column="2" />

        <Button Text="*"
                Command="{Binding AddCharCommand}"
                CommandParameter="*"
                Grid.Row="4" Grid.Column="0" />

        <Button Text="0"
                Command="{Binding AddCharCommand}"
                CommandParameter="0"
                Grid.Row="4" Grid.Column="1" />

        <Button Text="#"
                Command="{Binding AddCharCommand}"
                CommandParameter="#"
                Grid.Row="4" Grid.Column="2" />
    </Grid>
</ContentPage>

A Command propriedade do primeiro Button que aparece nessa marcação está ligada ao DeleteCharCommand, os demais estão ligados ao AddCharCommand com um CommandParameter que é o mesmo que o caractere que aparece no Button rosto. Aqui está o programa em ação:

Calculadora usando MVVM e Comandos

Invocando métodos assíncronos

Os comandos também podem invocar métodos assíncronos. Isso é obtido usando as async palavras-chave e await ao especificar o Execute método:

DownloadCommand = new Command (async () => await DownloadAsync ());

Isso indica que o DownloadAsync método é um Task e deve ser aguardado:

async Task DownloadAsync ()
{
    await Task.Run (() => Download ());
}

void Download ()
{
    ...
}

Implementando um menu de navegação

O programa de exemplo que contém todo o código-fonte nesta série de artigos usa um ViewModel para sua home page. Este ViewModel é uma definição de uma classe curta com três propriedades chamadas Type, Titlee Description que contêm o tipo de cada uma das páginas de exemplo, um título e uma breve descrição. Além disso, o ViewModel define uma propriedade estática chamada All que é uma coleção de todas as páginas no programa:

public class PageDataViewModel
{
    public PageDataViewModel(Type type, string title, string description)
    {
        Type = type;
        Title = title;
        Description = description;
    }

    public Type Type { private set; get; }

    public string Title { private set; get; }

    public string Description { private set; get; }

    static PageDataViewModel()
    {
        All = new List<PageDataViewModel>
        {
            // Part 1. Getting Started with XAML
            new PageDataViewModel(typeof(HelloXamlPage), "Hello, XAML",
                                  "Display a Label with many properties set"),

            new PageDataViewModel(typeof(XamlPlusCodePage), "XAML + Code",
                                  "Interact with a Slider and Button"),

            // Part 2. Essential XAML Syntax
            new PageDataViewModel(typeof(GridDemoPage), "Grid Demo",
                                  "Explore XAML syntax with the Grid"),

            new PageDataViewModel(typeof(AbsoluteDemoPage), "Absolute Demo",
                                  "Explore XAML syntax with AbsoluteLayout"),

            // Part 3. XAML Markup Extensions
            new PageDataViewModel(typeof(SharedResourcesPage), "Shared Resources",
                                  "Using resource dictionaries to share resources"),

            new PageDataViewModel(typeof(StaticConstantsPage), "Static Constants",
                                  "Using the x:Static markup extensions"),

            new PageDataViewModel(typeof(RelativeLayoutPage), "Relative Layout",
                                  "Explore XAML markup extensions"),

            // Part 4. Data Binding Basics
            new PageDataViewModel(typeof(SliderBindingsPage), "Slider Bindings",
                                  "Bind properties of two views on the page"),

            new PageDataViewModel(typeof(SliderTransformsPage), "Slider Transforms",
                                  "Use Sliders with reverse bindings"),

            new PageDataViewModel(typeof(ListViewDemoPage), "ListView Demo",
                                  "Use a ListView with data bindings"),

            // Part 5. From Data Bindings to MVVM
            new PageDataViewModel(typeof(OneShotDateTimePage), "One-Shot DateTime",
                                  "Obtain the current DateTime and display it"),

            new PageDataViewModel(typeof(ClockPage), "Clock",
                                  "Dynamically display the current time"),

            new PageDataViewModel(typeof(HslColorScrollPage), "HSL Color Scroll",
                                  "Use a view model to select HSL colors"),

            new PageDataViewModel(typeof(KeypadPage), "Keypad",
                                  "Use a view model for numeric keypad logic")
        };
    }

    public static IList<PageDataViewModel> All { private set; get; }
}

O arquivo XAML para MainPage define uma ListBox propriedade cuja ItemsSource é definida como essa All propriedade e que contém um TextCell para exibir as Title propriedades e Description de cada página:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             x:Class="XamlSamples.MainPage"
             Padding="5, 0"
             Title="XAML Samples">

    <ListView ItemsSource="{x:Static local:PageDataViewModel.All}"
              ItemSelected="OnListViewItemSelected">
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextCell Text="{Binding Title}"
                          Detail="{Binding Description}" />
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

As páginas são mostradas em uma lista rolável:

Lista rolável de páginas

O manipulador no arquivo code-behind é acionado quando o usuário seleciona um item. O manipulador define a SelectedItem propriedade do ListBox back para null e, em seguida, instancia a página selecionada e navega até ela:

private async void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
{
    (sender as ListView).SelectedItem = null;

    if (args.SelectedItem != null)
    {
        PageDataViewModel pageData = args.SelectedItem as PageDataViewModel;
        Page page = (Page)Activator.CreateInstance(pageData.Type);
        await Navigation.PushAsync(page);
    }
}

Vídeo

Xamarin Evolve 2016: MVVM simplificado com Xamarin.Forms e Prism

Resumo

O XAML é uma ferramenta poderosa para definir interfaces de usuário em Xamarin.Forms aplicativos, especialmente quando a vinculação de dados e o MVVM são usados. O resultado é uma representação limpa, elegante e potencialmente ferramental de uma interface do usuário com todo o suporte em segundo plano no código.

Encontre mais vídeos sobre o Xamarin no Channel 9 e no YouTube.