Partilhar via


Vinculação de dados e MVVM

Browse sample. Navegue pelo exemplo

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

Importante

A interface do usuário do aplicativo multiplataforma .NET (.NET MAUI) marca as atualizações de vinculação ao thread da interface do usuário. Ao usar o MVVM, isso permite que você atualize as propriedades viewmodel vinculadas a dados de qualquer thread, com o mecanismo de associação do .NET MAUI trazendo as atualizações para o thread da interface do usuário.

Há várias abordagens para implementar o padrão MVVM, e este artigo se concentra em uma abordagem simples. Ele usa modos de exibição e modelos de exibição, mas não modelos, para se concentrar na vinculação de dados entre as duas camadas. Para obter uma explicação detalhada do uso do padrão MVVM no .NET MAUI, consulte Model-View-ViewModel (MVVM) em Enterprise Application Patterns using .NET MAUI. Para obter um tutorial que ajuda você a implementar o padrão MVVM, consulte Atualizar seu aplicativo com conceitos MVVM.

MVVM simples

Nas extensões de marcação XAML, 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. O exemplo a seguir usa a extensão de marcação para obter a data e a x:Static hora atuais da propriedade static DateTime.Now no System namespace:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             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">

    <VerticalStackLayout BindingContext="{x:Static sys:DateTime.Now}"
                         Spacing="25" Padding="30,0"
                         VerticalOptions="Center" HorizontalOptions="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}'}" />

    </VerticalStackLayout>

</ContentPage>

Neste exemplo, o valor recuperado DateTime é definido como o BindingContext em um StackLayoutarquivo . 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 o mesmo BindingContext, e eles podem conter ligações às propriedades desse objeto:

Screenshot of a page displaying the date and time.

No entanto, o problema é que a data e a hora são definidas uma vez quando a página é construída e inicializada, e nunca mudam.

Uma página XAML pode exibir um relógio que sempre mostra a hora atual, mas requer código adicional. O padrão MVVM é uma escolha natural para aplicativos .NET MAUI quando se vinculam dados de propriedades entre objetos visuais e os dados subjacentes. Ao pensar em termos de MVVM, o modelo e viewmodel são classes escritas inteiramente em código. O modo de exibição geralmente é um arquivo XAML que faz referência às propriedades definidas no viewmodel por meio de associações de dados. No MVVM, um modelo é ignorante do viewmodel, e um viewmodel é ignorante do view. No entanto, geralmente você personaliza os tipos expostos pelo viewmodel para os tipos associados à interface do usuário.

Observação

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

O exemplo a seguir mostra um viewmodel para um relógio, com uma única propriedade chamada DateTime que é atualizada a cada segundo:

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace XamlSamples;

class ClockViewModel: INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private DateTime _dateTime;
    private Timer _timer;

    public DateTime DateTime
    {
        get => _dateTime;
        set
        {
            if (_dateTime != value)
            {
                _dateTime = value;
                OnPropertyChanged(); // reports this property
            }
        }
    }

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

        // Update the DateTime property every second.
        _timer = new Timer(new TimerCallback((s) => this.DateTime = DateTime.Now),
                           null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
    }

    ~ClockViewModel() =>
        _timer.Dispose();

    public void OnPropertyChanged([CallerMemberName] string name = "") =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

Viewmodels normalmente implementam a interface, que fornece a INotifyPropertyChanged capacidade de uma classe para gerar o PropertyChanged evento sempre que uma de suas propriedades é alterada. O mecanismo de vinculação de dados no .NET MAUI 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. No exemplo de código anterior, o método manipula a elevação do evento enquanto determina automaticamente o OnPropertyChanged nome da origem da propriedade: DateTime.

O exemplo a seguir mostra XAML que consome ClockViewModel:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             x:Class="XamlSamples.ClockPage"
             Title="Clock Page">
    <ContentPage.BindingContext>
        <local:ClockViewModel />
    </ContentPage.BindingContext>

    <Label Text="{Binding DateTime, StringFormat='{0:T}'}"
           FontSize="18"
           HorizontalOptions="Center"
           VerticalOptions="Center" />
</ContentPage>

Neste exemplo, ClockViewModel é definido como o BindingContext das marcas de elemento de ContentPage propriedade using . Como alternativa, o arquivo code-behind pode instanciar o viewmodel.

A Binding extensão de marcação na Text propriedade dos formata Label a DateTime propriedade. A captura de tela a seguir mostra o resultado:

Screenshot of a page displaying the date and time via a viewmodel.

Além disso, é 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.

O exemplo a seguir mostra o HslViewModel que converte um Color valor em Hue, Saturatione Luminosity valores e vice-versa:

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace XamlSamples;

class HslViewModel: INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private float _hue, _saturation, _luminosity;
    private Color _color;

    public float Hue
    {
        get => _hue;
        set
        {
            if (_hue != value)
                Color = Color.FromHsla(value, _saturation, _luminosity);
        }
    }

    public float Saturation
    {
        get => _saturation;
        set
        {
            if (_saturation != value)
                Color = Color.FromHsla(_hue, value, _luminosity);
        }
    }

    public float Luminosity
    {
        get => _luminosity;
        set
        {
            if (_luminosity != value)
                Color = Color.FromHsla(_hue, _saturation, value);
        }
    }

    public Color Color
    {
        get => _color;
        set
        {
            if (_color != value)
            {
                _color = value;
                _hue = _color.GetHue();
                _saturation = _color.GetSaturation();
                _luminosity = _color.GetLuminosity();

                OnPropertyChanged("Hue");
                OnPropertyChanged("Saturation");
                OnPropertyChanged("Luminosity");
                OnPropertyChanged(); // reports this property
            }
        }
    }

    public void OnPropertyChanged([CallerMemberName] string name = "") =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

Neste exemplo, as alterações nas propriedades , e fazem com que a Color propriedade seja alterada, Saturatione Luminosity as alterações na propriedade fazem com que as HueColor outras três propriedades sejam alteradas. Isso pode parecer um loop infinito, exceto que o viewmodel não invoca o evento, a PropertyChanged menos que a propriedade tenha sido alterada.

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

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

    <VerticalStackLayout Padding="10, 0, 10, 30">
        <BoxView Color="{Binding Color}"
                 HeightRequest="100"
                 WidthRequest="100"
                 HorizontalOptions="Center" />
        <Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
               HorizontalOptions="Center" />
        <Slider Value="{Binding Hue}"
                Margin="20,0,20,0" />
        <Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
               HorizontalOptions="Center" />
        <Slider Value="{Binding Saturation}"
                Margin="20,0,20,0" />
        <Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
               HorizontalOptions="Center" />
        <Slider Value="{Binding Luminosity}"
                Margin="20,0,20,0" />
    </VerticalStackLayout>
</ContentPage>

A associação em cada um Label é o padrão OneWay. Ele só precisa exibir o valor. No entanto, a associação padrão em cada um Slider é TwoWay. Isso permite que o seja inicializado a Slider partir do viewmodel. Quando o viewmodel é instanciado, Color sua propriedade é definida como Aqua. Uma alteração em um define um Slider novo valor para a propriedade no viewmodel, que calcula uma nova cor:

MVVM using two-way data bindings.

Comando

Às vezes, um aplicativo tem necessidades que vão além das 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õ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 viewmodel pode conter comandos, que são métodos executados em reação a uma atividade específica no modo de exibição, como um Button clique. Associações de dados são definidas entre esses comandos e o Button.

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

Observação

Muitos outros controles também definem Command e CommandParameter propriedades.

A ICommand interface é definida no namespace System.Windows.Input e consiste em 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 propriedade para identificar objetos individuais Button (ou outros elementos) que estão vinculados a CommandParameter essa propriedade viewmodel. Internamente, o chama o método sempre que o usuário toca no , passando para o método o ExecuteButtonExecute seu CommandParameter.Button

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 é gerado. Se CanExecute retornar false, o Button se desativa e não gera Execute chamadas.

Você pode usar a classe ou Command<T> incluída no .NET MAUI para implementar a CommandICommand interface. Essas duas classes definem vários construtores mais um ChangeCanExecute método que o viewmodel pode chamar para forçar o objeto a gerar o CanExecuteChangedCommand evento.

O exemplo a seguir mostra um viewmodel para um teclado simples destinado a inserir números de telefone:

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace XamlSamples;

class KeypadViewModel: INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string _inputString = "";
    private string _displayText = "";
    private char[] _specialChars = { '*', '#' };

    public ICommand AddCharCommand { get; private set; }
    public ICommand DeleteCharCommand { get; private set; }

    public string InputString
    {
        get => _inputString;
        private set
        {
            if (_inputString != value)
            {
                _inputString = value;
                OnPropertyChanged();
                DisplayText = FormatText(_inputString);

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

    public string DisplayText
    {
        get => _displayText;
        private set
        {
            if (_displayText != value)
            {
                _displayText = value;
                OnPropertyChanged();
            }
        }
    }

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

        // Command to delete a character from the input string when allowed
        DeleteCharCommand =
            new Command(
                // Command will strip a character from the input string
                () => InputString = InputString.Substring(0, InputString.Length - 1),

                // CanExecute is processed here to return true when there's something to delete
                () => InputString.Length > 0
            );
    }

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

        // Format the string based on the type of data and the length
        if (hasNonNumbers || str.Length < 4 || str.Length > 10)
        {
            // Special characters exist, or the string is too small or large for special formatting
            // Do nothing
        }

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


    public void OnPropertyChanged([CallerMemberName] string name = "") =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

Neste exemplo, os métodos e CanExecute para os Execute comandos são definidos como funções lambda no construtor. O viewmodel assume que a AddCharCommand propriedade está vinculada à Command propriedade de vários botões (ou qualquer outro controle 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 exemplo a seguir mostra o XAML que consome o KeypadViewModel:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             x:Class="XamlSamples.KeypadPage"
             Title="Keypad Page">
    <ContentPage.BindingContext>
        <local:KeypadViewModel />
    </ContentPage.BindingContext>

    <Grid HorizontalOptions="Center" VerticalOptions="Center">
        <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>

        <Label Text="{Binding DisplayText}"
               Margin="0,0,10,0" FontSize="20" LineBreakMode="HeadTruncation"
               VerticalTextAlignment="Center" HorizontalTextAlignment="End"
               Grid.ColumnSpan="2" />

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

        <Button Text="1" Command="{Binding AddCharCommand}" CommandParameter="1" Grid.Row="1" />
        <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" />
        <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" />
        <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" />
        <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>

Neste exemplo, a Command propriedade do primeiro Button que está vinculada ao DeleteCharCommand. Os outros botões são vinculados ao AddCharCommand com um CommandParameter que é o mesmo que o caractere que aparece no Button:

Screenshot of a calculator using MVVM and commands.