Atualizar seu aplicativo com conceitos de MVVM
Esta série de tutoriais foi projetada para continuar o tutorial Criar um aplicativo .NET MAUI , que criou um aplicativo de anotação. Nesta parte da série, você aprenderá a:
- Implemente o padrão MVVM (model-view-viewmodel).
- Use um estilo adicional de cadeia de caracteres de consulta para passar dados durante a navegação.
Sugerimos que você siga primeiro o tutorial Criar um aplicativo .NET MAUI, pois o código criado nesse tutorial é a base para este tutorial. Se você perdeu o código ou deseja iniciar novamente, baixe este projeto.
Noções básicas sobre MVVM
A experiência do desenvolvedor do .NET MAUI normalmente envolve a criação de uma interface do usuário no XAML e, em seguida, a adição de code-behind que opera na interface do usuário. Problemas complexos de manutenção podem surgir à medida que os aplicativos são modificados e crescem em tamanho e escopo. Esses problemas incluem o acoplamento apertado entre os controles de interface do usuário e a lógica de negócios, o que aumenta o custo de fazer modificações na interface do usuário e a dificuldade de testar esse código por unidade.
O padrão MVVM (model-view-viewmodel) ajuda a separar de forma limpa a lógica de negócios e apresentação de um aplicativo de sua interface do usuário. Manter uma separação limpa entre a lógica do aplicativo e a interface do usuário ajuda a resolver vários problemas de desenvolvimento e facilita o teste, a manutenção e a evolução de um aplicativo. Ele também pode melhorar de forma significativa as oportunidades de reutilização de código e facilitar a colaboração entre desenvolvedores e designers de interface do usuário durante o desenvolvimento de suas partes específicas em um aplicativo.
O padrão
Há três componentes principais no padrão MVVM: o modelo, a exibição e o modelo de exibição. Cada um tem uma finalidade diferente. O diagrama a seguir mostra as relações entre os três componentes.
Além de entender as responsabilidades de cada componente, também é importante entender como eles interagem. Em alto nível, a exibição "sabe sobre" o modelo de exibição e o modelo de exibição "sabe sobre" o modelo, mas o modelo não está ciente do modelo de exibição e o modelo de exibição não está ciente da exibição. Portanto, o modelo de exibição isola a exibição do modelo e permite que o modelo evolua independentemente da exibição.
A chave para usar o MVVM está efetivamente no entendimento de como fatorar o código do aplicativo nas classes corretas e como as classes interagem.
Exibir
A exibição é responsável por definir a estrutura, o layout e a aparência do que o usuário vê na tela. De preferência, cada exibição é definida em XAML, com um code-behind limitado que não contém lógica de negócios. No entanto, em alguns casos, o code-behind pode conter lógica de interface do usuário que implementa um comportamento visual difícil de expressar em XAML, como animações.
ViewModel
O modelo de exibição implementa propriedades e comandos aos quais a exibição pode associar dados e notifica a exibição de quaisquer alterações de estado por meio de eventos de notificação de alteração. As propriedades e os comandos fornecidos pelo modelo de exibição definem a funcionalidade a ser oferecida pela interface do usuário, mas a exibição determina como essa funcionalidade deve ser exibida.
O modelo de exibição também é responsável por coordenar as interações da exibição com todas as classes de modelo necessárias. Normalmente, há uma relação um-para-muitos entre o modelo de exibição e as classes de modelo.
Cada modelo de exibição fornece dados de um modelo de uma forma que a exibição pode consumir facilmente. Para fazer isso, o modelo de exibição às vezes executa a conversão de dados. Colocar essa conversão de dados no modelo de exibição é uma boa ideia porque fornece propriedades às quais a exibição pode se associar. Por exemplo, o modelo de exibição pode combinar os valores de duas propriedades para facilitar a exibição pelo modo de exibição.
Importante
O .NET MAUI realiza marshals de atualizações de associação para o thread da interface do usuário. Ao usar o MVVM, isso permite que você atualize as propriedades viewmodel associadas 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.
Modelar
Classes de modelo são classes não visuais que encapsulam os dados do aplicativo. Portanto, o modelo pode ser considerado como representando o modelo de domínio do aplicativo, que geralmente inclui um modelo de dados junto com a lógica de negócios e validação.
Atualizar o modelo
Nesta primeira parte do tutorial, você implementará o padrão MVVM (model-view-viewmodel). Para começar, abra a solução Notes.sln no Visual Studio.
Limpar o modelo
No tutorial anterior, os tipos de modelo estavam agindo como o modelo (dados) e como um modelo de exibição (preparação de dados), que foi mapeado diretamente para uma exibição. A seguinte tabela descreve o modelo:
Arquivo de código | Descrição |
---|---|
Models/About.cs | O modelo About . Contém campos somente leitura que descrevem o próprio aplicativo, como o título e a versão do aplicativo. |
Models/Note.cs | O modelo Note . Representa uma anotação. |
Models/AllNotes.cs | O modelo AllNotes . Carrega todas as anotações no dispositivo em uma coleção. |
Considerando o próprio aplicativo, somente uma parte dos dados é realmente utilizada por ele, o Note
. As anotações são carregadas e salvas no dispositivo e editadas por meio da interface do usuário do aplicativo. Na verdade, os modelos About
e AllNotes
não são necessários. Remova esses modelos do projeto:
- Localize o painel Gerenciador de Soluções do Visual Studio.
- Clique com o botão direito do mouse no arquivo Models\About.cs e selecione Excluir. Pressione OK para excluir o arquivo.
- Clique com o botão direito do mouse no arquivo Models\AllNotes.cs e selecione Excluir. Pressione OK para excluir o arquivo.
O único arquivo de modelo restante é o arquivo Models\Note.cs.
Atualizar o modelo
O modelo Note
contém:
- Um identificador exclusivo, que é o nome do arquivo da anotação, conforme armazenado no dispositivo.
- O texto da anotação.
- Uma data para indicar quando a anotação foi criada ou atualizada pela última vez.
Neste momento, o carregamento e o salvamento do modelo foram realizados por meio das exibições e, eventualmente, por outros tipos de modelos que você acabou de remover. O código que você tem para o tipo Note
deve ser o seguinte:
namespace Notes.Models;
internal class Note
{
public string Filename { get; set; }
public string Text { get; set; }
public DateTime Date { get; set; }
}
O modelo Note
será expandido para lidar com as operações de carregamento, salvamento e exclusão de anotações.
No painel Gerenciador de Soluções do Visual Studio, clique duas vezes em Models\Note.cs.
No editor de código, adicione os dois métodos a seguir à classe
Note
. Esses métodos são baseados em instância que salvam a nota atual no dispositivo ou a excluem dele, respectivamente:public void Save() => File.WriteAllText(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename), Text); public void Delete() => File.Delete(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename));
O aplicativo precisa carregar anotações de duas maneiras: carregando uma anotação individual de um arquivo e carregando todas as anotações no dispositivo. O código para lidar com o carregamento pode ser o membro
static
, não exigindo que uma instância de classe seja executada.Adicione o seguinte código à classe para carregar uma anotação pelo nome do arquivo:
public static Note Load(string filename) { filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename); if (!File.Exists(filename)) throw new FileNotFoundException("Unable to find file on local storage.", filename); return new() { Filename = Path.GetFileName(filename), Text = File.ReadAllText(filename), Date = File.GetLastWriteTime(filename) }; }
Esse código usa o nome do arquivo como um parâmetro, cria o caminho para onde as anotações são armazenadas no dispositivo e tenta carregar o arquivo caso ele exista.
A segunda maneira de carregar anotações é enumerar todas as anotações no dispositivo e carregá-las em uma coleção.
Adicione o código a seguir à classe :
public static IEnumerable<Note> LoadAll() { // Get the folder where the notes are stored. string appDataPath = FileSystem.AppDataDirectory; // Use Linq extensions to load the *.notes.txt files. return Directory // Select the file names from the directory .EnumerateFiles(appDataPath, "*.notes.txt") // Each file name is used to load a note .Select(filename => Note.Load(Path.GetFileName(filename))) // With the final collection of notes, order them by date .OrderByDescending(note => note.Date); }
Esse código retorna uma coleção enumerável de tipos de modelo
Note
, recuperando os arquivos no dispositivo que correspondem ao padrão de arquivo de anotações: *.notes.txt. Cada nome de arquivo é passado para o métodoLoad
, carregando uma anotação individual. Finalmente, a coleção de anotações é ordenada pela data de cada anotação e retornada ao chamador.Por fim, adicione um construtor à classe que define os valores padrão para as propriedades, incluindo um nome de arquivo aleatório:
public Note() { Filename = $"{Path.GetRandomFileName()}.notes.txt"; Date = DateTime.Now; Text = ""; }
O código de classe Note
deve ter a seguinte aparência:
namespace Notes.Models;
internal class Note
{
public string Filename { get; set; }
public string Text { get; set; }
public DateTime Date { get; set; }
public Note()
{
Filename = $"{Path.GetRandomFileName()}.notes.txt";
Date = DateTime.Now;
Text = "";
}
public void Save() =>
File.WriteAllText(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename), Text);
public void Delete() =>
File.Delete(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename));
public static Note Load(string filename)
{
filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename);
if (!File.Exists(filename))
throw new FileNotFoundException("Unable to find file on local storage.", filename);
return
new()
{
Filename = Path.GetFileName(filename),
Text = File.ReadAllText(filename),
Date = File.GetLastWriteTime(filename)
};
}
public static IEnumerable<Note> LoadAll()
{
// Get the folder where the notes are stored.
string appDataPath = FileSystem.AppDataDirectory;
// Use Linq extensions to load the *.notes.txt files.
return Directory
// Select the file names from the directory
.EnumerateFiles(appDataPath, "*.notes.txt")
// Each file name is used to load a note
.Select(filename => Note.Load(Path.GetFileName(filename)))
// With the final collection of notes, order them by date
.OrderByDescending(note => note.Date);
}
}
Agora que o modelo Note
está concluído, os modelos de exibição podem ser criados.
Criar o Sobre o viewmodel
Para adicionar modelos de exibição ao projeto, adicione uma referência ao Kit de Ferramentas da Comunidade MVVM. Essa biblioteca está disponível no NuGet e fornece tipos e sistemas que ajudam a implementar o padrão MVVM.
No painel Gerenciador de Soluções do Visual Studio, clique com o botão direito do mouse no projeto de Anotações >Gerenciar Pacotes NuGet.
Selecione a guia Procurar.
Pesquise communitytoolkit mvvm e selecione o pacote
CommunityToolkit.Mvvm
, que deve ser o primeiro resultado.Verifique se pelo menos a versão 8 está selecionada. Este tutorial foi escrito usando a versão 8.0.0.
Em seguida, selecione Instalar e aceite todas as solicitações exibidas.
Agora, você está pronto para começar a atualizar o projeto adicionando modelos de exibição.
Desacoplar com modelos de exibição
A relação view-to-viewmodel depende muito do sistema de associação fornecido pelo .NET MAUI (.NET Multi-platform App UI). O aplicativo já está usando a associação nas exibições para mostrar uma lista de anotações e apresentar o texto e a data de uma única anotação. Atualmente, a lógica do aplicativo é fornecida pelo code-behind da exibição e está diretamente vinculada à exibição. Por exemplo, quando um usuário está editando uma anotação e pressiona o botão Salvar, o evento Clicked
para o botão é acionado. Em seguida, o code-behind do manipulador de eventos salva o texto da anotação em um arquivo e navega até a tela anterior.
Ter a lógica do aplicativo no code-behind de uma exibição pode se tornar um problema quando a exibição é alterada. Por exemplo, se o botão for substituído por um controle de entrada diferente ou o nome de um controle for alterado, os manipuladores de eventos poderão se tornar inválidos. Independentemente de como a exibição é projetada, a finalidade da exibição é invocar algum tipo de lógica de aplicativo e apresentar informações ao usuário. Para este aplicativo, o botão Save
está salvando a anotação e navegando de volta para a tela anterior.
O viewmodel fornece ao aplicativo um lugar específico para colocar a lógica do aplicativo, independentemente de como a interface do usuário foi projetada ou como os dados estão sendo carregados ou salvos. O viewmodel é a cola que representa e interage com o modelo de dados em nome da exibição.
Os modelos de exibição são armazenados em uma pasta ViewModels.
- Localize o painel Gerenciador de Soluções do Visual Studio.
- Clique com o botão direito do mouse no projeto de Anotações e selecione Adicionar>Nova Pasta. Nomeie a pasta como ViewModels.
- Clique com o botão direito do mouse na pasta ViewModels >Adicionar>Classe e nomeie-a como AboutViewModel.cs.
- Repita a etapa anterior e crie mais dois modelos de exibição:
- NoteViewModel.cs
- NotesViewModel.cs
A estrutura do seu projeto deve ser semelhante à da imagem a seguir:
Sobre o viewmodel e Sobre a exibição
Sobre a exibição mostra alguns dados na tela e, opcionalmente, navega até um site com mais informações. Como essa exibição não tem dados a serem alterados, como ocorre com um controle de entrada de texto ou selecionando itens de uma lista, é um bom candidato demonstrar a adição de um viewmodel. Em Sobre o viewmodel, não há um modelo de suporte.
Crie o Sobre o viewmodel:
No painel Gerenciador de Soluções do Visual Studio, clique duas vezes em ViewModels\AboutViewModel.cs.
Cole o código a seguir:
using CommunityToolkit.Mvvm.Input; using System.Windows.Input; namespace Notes.ViewModels; internal class AboutViewModel { public string Title => AppInfo.Name; public string Version => AppInfo.VersionString; public string MoreInfoUrl => "https://aka.ms/maui"; public string Message => "This app is written in XAML and C# with .NET MAUI."; public ICommand ShowMoreInfoCommand { get; } public AboutViewModel() { ShowMoreInfoCommand = new AsyncRelayCommand(ShowMoreInfo); } async Task ShowMoreInfo() => await Launcher.Default.OpenAsync(MoreInfoUrl); }
O snippet de código anterior contém algumas propriedades que representam informações sobre o aplicativo, como o nome e a versão. Esse snippet é exatamente o mesmo que Sobre o modelo você excluiu anteriormente. No entanto, esse viewmodel contém um novo conceito, a propriedade de comando ShowMoreInfoCommand
.
Os comandos são ações associáveis que invocam código e são um ótimo local para colocar a lógica do aplicativo. Neste exemplo, o ShowMoreInfoCommand
aponta para o método ShowMoreInfo
, que abre o navegador da Web para uma página específica. Você aprenderá mais sobre o sistema de comando na próxima seção.
Sobre a exibição
Sobre a exibição precisa ser ligeiramente alterado para conectá-lo ao viewmodel criado na seção anterior. No arquivo Views\AboutPage.xaml, aplique as seguintes alterações:
- Atualize o namespace de XML
xmlns:models
paraxmlns:viewModels
e direcione o namespace do .NETNotes.ViewModels
. - Defina a propriedade
ContentPage.BindingContext
como uma nova instância do viewmodelAbout
. - Remova o manipulador de eventos
Clicked
do botão e use a propriedadeCommand
.
Atualize Sobre a exibição:
No painel Gerenciador de Soluções do Visual Studio, clique duas vezes em Views\AboutPage.xaml.
Cole o código a seguir:
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:Notes.ViewModels" x:Class="Notes.Views.AboutPage"> <ContentPage.BindingContext> <viewModels:AboutViewModel /> </ContentPage.BindingContext> <VerticalStackLayout Spacing="10" Margin="10"> <HorizontalStackLayout Spacing="10"> <Image Source="dotnet_bot.png" SemanticProperties.Description="The dot net bot waving hello!" HeightRequest="64" /> <Label FontSize="22" FontAttributes="Bold" Text="{Binding Title}" VerticalOptions="End" /> <Label FontSize="22" Text="{Binding Version}" VerticalOptions="End" /> </HorizontalStackLayout> <Label Text="{Binding Message}" /> <Button Text="Learn more..." Command="{Binding ShowMoreInfoCommand}" /> </VerticalStackLayout> </ContentPage>
O snippet de código anterior realça as linhas que foram alteradas nesta versão da exibição.
Observe que o botão está usando a propriedade Command
. Muitos controles têm uma propriedade Command
que é invocada quando o usuário interage com o controle. Quando usado com um botão, o comando é invocado quando um usuário pressiona o botão, semelhante à maneira como o manipulador de eventos Clicked
é invocado, exceto que você pode associar Command
a uma propriedade no viewmodel.
Nessa exibição, quando o usuário pressiona o botão, o Command
é invocado. O Command
é associado à propriedade ShowMoreInfoCommand
no viewmodel e, quando invocado, executa o código no método ShowMoreInfo
, que abre o navegador da Web para uma página específica.
Limpar o Sobre o code-behind
O botão ShowMoreInfo
não está usando o manipulador de eventos, portanto, o código LearnMore_Clicked
deve ser removido do arquivo Views\AboutPage.xaml.cs. Exclua esse código; a classe deve conter apenas o construtor:
No painel Gerenciador de Soluções do Visual Studio, clique duas vezes em Views\AboutPage.xaml.cs.
Dica
Talvez seja necessário expandir Views\AboutPage.xaml para mostrar o arquivo.
Substitua o código pelo seguinte snippet:
namespace Notes.Views; public partial class AboutPage : ContentPage { public AboutPage() { InitializeComponent(); } }
Criar o viewmodel da Anotação
O objetivo de atualizar a exibição Anotação é mover o máximo de funcionalidade possível para fora do code-behind XAML e colocá-la no viewmodel da Anotação.
viewmodel da Anotação
Com base no que a exibição Anotação requer, o viewmodel da Anotação precisa fornecer os seguintes itens:
- O texto da anotação.
- A data/hora em que a anotação foi criada ou atualizada pela última vez.
- Um comando que salva a anotação.
- Um comando que exclui a anotação.
Crie o viewmodel da Anotação:
No painel Gerenciador de Soluções do Visual Studio, clique duas vezes em ViewModels\NoteViewModel.cs.
Substitua o código nesse arquivo pelo seguinte snippet:
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.ComponentModel; using System.Windows.Input; namespace Notes.ViewModels; internal class NoteViewModel : ObservableObject, IQueryAttributable { private Models.Note _note; }
Esse código é o viewmodel
Note
em branco em que você adicionará propriedades e comandos para dar suporte à exibiçãoNote
. Observe que o namespaceCommunityToolkit.Mvvm.ComponentModel
está sendo importado. Esse namespace fornece oObservableObject
usado como a classe base. Você aprenderá mais sobreObservableObject
na próxima etapa. O namespaceCommunityToolkit.Mvvm.Input
também é importado. Esse namespace fornece alguns tipos de comando que invocam métodos de forma assíncrona.O modelo
Models.Note
está sendo armazenado como um campo privado. As propriedades e os métodos dessa classe usarão esse campo.Adicione as seguintes propriedades à classe :
public string Text { get => _note.Text; set { if (_note.Text != value) { _note.Text = value; OnPropertyChanged(); } } } public DateTime Date => _note.Date; public string Identifier => _note.Filename;
As propriedades
Date
eIdentifier
são simples que recuperam apenas os valores correspondentes do modelo.Dica
Para propriedades, a sintaxe
=>
cria uma propriedade get-only em que a instrução à direita de=>
deve ser avaliada como um valor a ser retornado.A propriedade
Text
primeiro verifica se o valor que está sendo definido é um valor diferente. Se o valor for diferente, esse valor será passado para a propriedade do modelo e o métodoOnPropertyChanged
será chamado.O método
OnPropertyChanged
é fornecido pela classe baseObservableObject
. Esse método usa o nome do código de chamada (nesse caso, o nome da propriedade de Text) e gera o eventoObservableObject.PropertyChanged
. Esse evento fornece o nome da propriedade para todos os assinantes do evento. O sistema de associação fornecido pelo .NET MAUI reconhece esse evento e atualiza todas as associações relacionadas na interface do usuário. Para o viewmodel da Anotação, quando a propriedadeText
é alterada, o evento é acionado, e qualquer elemento da interface do usuário associado à propriedadeText
é notificado de que a propriedade foi alterada.Adicione as seguintes propriedades de comando à classe, que são os comandos aos quais a exibição pode se associar:
public ICommand SaveCommand { get; private set; } public ICommand DeleteCommand { get; private set; }
Adicione os seguintes construtores à classe:
public NoteViewModel() { _note = new Models.Note(); SaveCommand = new AsyncRelayCommand(Save); DeleteCommand = new AsyncRelayCommand(Delete); } public NoteViewModel(Models.Note note) { _note = note; SaveCommand = new AsyncRelayCommand(Save); DeleteCommand = new AsyncRelayCommand(Delete); }
Esses dois construtores são usados para criar o viewmodel com um novo modelo de suporte, que é uma anotação vazia, ou para criar um viewmodel que usa a instância de modelo especificada.
Os construtores também configuram os comandos para o viewmodel. Em seguida, adicione o código para esses comandos.
Adicione os métodos
Save
eDelete
:private async Task Save() { _note.Date = DateTime.Now; _note.Save(); await Shell.Current.GoToAsync($"..?saved={_note.Filename}"); } private async Task Delete() { _note.Delete(); await Shell.Current.GoToAsync($"..?deleted={_note.Filename}"); }
Esses métodos são invocados por comandos associados. Eles executam as ações relacionadas no modelo e fazem com que o aplicativo navegue até a página anterior. Um parâmetro de cadeia de caracteres de consulta é adicionado ao caminho de navegação
..
, indicando qual ação foi executada e o identificador exclusivo da anotação.Em seguida, adicione o método
ApplyQueryAttributes
à classe, que atende aos requisitos da interface IQueryAttributable:void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { if (query.ContainsKey("load")) { _note = Models.Note.Load(query["load"].ToString()); RefreshProperties(); } }
Quando uma página ou o contexto de associação de uma página implementa essa interface, os parâmetros de cadeia de caracteres de consulta usados na navegação são passados para o método
ApplyQueryAttributes
. Esse viewmodel é usado como o contexto de associação para a exibição Anotação. Quando a exibição Anotação é acessada, o contexto de associação da exibição (este viewmodel) é passado pelos parâmetros de cadeia de caracteres de consulta usados durante a navegação.Esse código verifica se a chave
load
foi fornecida no dicionárioquery
. Se essa chave for encontrada, o valor deverá ser o identificador (o nome do arquivo) da anotação a ser carregada. Essa anotação é carregada e definida como o objeto de modelo subjacente dessa instância do viewmodel.Por fim, adicione estes dois métodos auxiliares à classe:
public void Reload() { _note = Models.Note.Load(_note.Filename); RefreshProperties(); } private void RefreshProperties() { OnPropertyChanged(nameof(Text)); OnPropertyChanged(nameof(Date)); }
O método
Reload
é um método auxiliar que atualiza o objeto de modelo de suporte, recarregando-o do armazenamento do dispositivoO método
RefreshProperties
é outro método auxiliar para garantir que todos os assinantes associados a esse objeto sejam notificados de que as propriedadesText
eDate
foram alteradas. Como o modelo subjacente (o campo_note
) é alterado quando a anotação é carregada durante a navegação, as propriedadesText
eDate
não são realmente definidas como valores novos. Como essas propriedades não são definidas diretamente, nenhuma associação anexada a essas propriedades não seria notificada porqueOnPropertyChanged
não é chamado para cada propriedade.RefreshProperties
garante que as associações a essas propriedades sejam atualizadas.
O código da classe deve ser semelhante ao seguinte snippet:
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Windows.Input;
namespace Notes.ViewModels;
internal class NoteViewModel : ObservableObject, IQueryAttributable
{
private Models.Note _note;
public string Text
{
get => _note.Text;
set
{
if (_note.Text != value)
{
_note.Text = value;
OnPropertyChanged();
}
}
}
public DateTime Date => _note.Date;
public string Identifier => _note.Filename;
public ICommand SaveCommand { get; private set; }
public ICommand DeleteCommand { get; private set; }
public NoteViewModel()
{
_note = new Models.Note();
SaveCommand = new AsyncRelayCommand(Save);
DeleteCommand = new AsyncRelayCommand(Delete);
}
public NoteViewModel(Models.Note note)
{
_note = note;
SaveCommand = new AsyncRelayCommand(Save);
DeleteCommand = new AsyncRelayCommand(Delete);
}
private async Task Save()
{
_note.Date = DateTime.Now;
_note.Save();
await Shell.Current.GoToAsync($"..?saved={_note.Filename}");
}
private async Task Delete()
{
_note.Delete();
await Shell.Current.GoToAsync($"..?deleted={_note.Filename}");
}
void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.ContainsKey("load"))
{
_note = Models.Note.Load(query["load"].ToString());
RefreshProperties();
}
}
public void Reload()
{
_note = Models.Note.Load(_note.Filename);
RefreshProperties();
}
private void RefreshProperties()
{
OnPropertyChanged(nameof(Text));
OnPropertyChanged(nameof(Date));
}
}
Exibição Anotação
Agora que o viewmodel foi criado, atualize a exibição Anotação. No arquivo Views\NotePage.xaml, aplique as seguintes alterações:
- Adicione o namespace de XML
xmlns:viewModels
direcionado ao namespace do .NETNotes.ViewModels
. - Adicione um
BindingContext
à página. - Remova os manipuladores de eventos
Clicked
do botão excluir e salvar e substitua-os por comandos.
Atualize a exibição Anotação:
- No painel Gerenciador de Soluções do Visual Studio, clique duas vezes em Views\NotePage.xaml para abrir o editor XAML.
- Cole o código a seguir:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:Notes.ViewModels"
x:Class="Notes.Views.NotePage"
Title="Note">
<ContentPage.BindingContext>
<viewModels:NoteViewModel />
</ContentPage.BindingContext>
<VerticalStackLayout Spacing="10" Margin="5">
<Editor x:Name="TextEditor"
Placeholder="Enter your note"
Text="{Binding Text}"
HeightRequest="100" />
<Grid ColumnDefinitions="*,*" ColumnSpacing="4">
<Button Text="Save"
Command="{Binding SaveCommand}"/>
<Button Grid.Column="1"
Text="Delete"
Command="{Binding DeleteCommand}"/>
</Grid>
</VerticalStackLayout>
</ContentPage>
Anteriormente, essa exibição não declarava um contexto de associação, pois era fornecida pelo code-behind da própria página. Definir o contexto de associação diretamente no XAML fornece duas vantagens:
Em tempo de execução, quando a página é acessada, ela exibe uma anotação em branco. Isso ocorre porque o construtor sem parâmetros para o contexto de associação, o viewmodel, é invocado. Se você se lembrar corretamente, o construtor sem parâmetros do viewmodel da Anotação criará uma anotação em branco.
O IntelliSense no editor XAML mostra as propriedades disponíveis assim que você começa a digitar a sintaxe
{Binding
. A sintaxe também é validada e alerta você sobre um valor inválido. Tente alterar a sintaxe de associação doSaveCommand
paraSave123Command
. Se você passar o cursor do mouse sobre o texto, observará uma dica de ferramenta informando que Save123Command não foi encontrado. Essa notificação não é considerada um erro porque as associações são dinâmicas; é realmente um pequeno aviso que pode ajudá-lo a notar quando você digitou a propriedade errada.Se você alterou o SaveCommand para um valor diferente, restaure-o agora.
Limpar o code-behind da Anotação
Agora que a interação com a exibição foi alterada de manipuladores de eventos para comandos, abra o arquivo Views\NotePage.xaml.cs e substitua todo o código por uma classe que contém apenas o construtor:
No painel Gerenciador de Soluções do Visual Studio, clique duas vezes em Views\NotePage.xaml.cs.
Dica
Talvez seja necessário expandir Views\NotePage.xaml para mostrar o arquivo.
Substitua o código pelo seguinte snippet:
namespace Notes.Views; public partial class NotePage : ContentPage { public NotePage() { InitializeComponent(); } }
Criar o viewmodel de Anotações
O par final viewmodel-view é o viewmodel das Anotações e a exibição AllNotes. No momento, porém, a exibição está vinculando diretamente ao modelo, que foi excluído no início deste tutorial. O objetivo de atualizar a exibição AllNotes é mover o máximo de funcionalidade possível para fora do code-behind XAML e colocá-la no viewmodel. Novamente, o benefício é que a exibição pode alterar seu design com pouco efeito no seu código.
viewmodel de Anotações
Com base no que a exibição AllNotes vai exibir e em quais interações o usuário fará, o viewmodel de Anotações deve fornecer os seguintes itens:
- Uma coleção de anotações.
- Um comando para lidar com a navegação até uma anotação.
- Um comando para criar uma anotação.
- Atualize a lista de anotações quando uma é criada, excluída ou alterada.
Crie o viewmodel de Anotações:
No painel Gerenciador de Soluções do Visual Studio, clique duas vezes em ViewModels\NotesViewModel.cs.
Substitua o código nesse arquivo pelo seguinte código:
using CommunityToolkit.Mvvm.Input; using System.Collections.ObjectModel; using System.Windows.Input; namespace Notes.ViewModels; internal class NotesViewModel: IQueryAttributable { }
Esse código é o
NotesViewModel
em branco em que você adicionará propriedades e comandos para dar suporte à exibiçãoAllNotes
.No código de classe
NotesViewModel
, adicione as seguintes propriedades:public ObservableCollection<ViewModels.NoteViewModel> AllNotes { get; } public ICommand NewCommand { get; } public ICommand SelectNoteCommand { get; }
A propriedade
AllNotes
é umObservableCollection
que armazena todas as anotações carregadas do dispositivo. Os dois comandos serão usados pela exibição para disparar as ações de criação de uma anotação ou seleção de uma anotação existente.Adicione um construtor sem parâmetros à classe, que inicializa os comandos e carrega as anotações do modelo:
public NotesViewModel() { AllNotes = new ObservableCollection<ViewModels.NoteViewModel>(Models.Note.LoadAll().Select(n => new NoteViewModel(n))); NewCommand = new AsyncRelayCommand(NewNoteAsync); SelectNoteCommand = new AsyncRelayCommand<ViewModels.NoteViewModel>(SelectNoteAsync); }
Observe que a coleção
AllNotes
usa o métodoModels.Note.LoadAll
para preencher a coleção observável com anotações. O métodoLoadAll
retorna as anotações como o tipoModels.Note
, mas a coleção observável é uma coleção de tiposViewModels.NoteViewModel
. O código usa a extensão LinqSelect
para criar instâncias de viewmodel dos modelos de anotação retornados deLoadAll
.Crie os métodos direcionados pelos comandos:
private async Task NewNoteAsync() { await Shell.Current.GoToAsync(nameof(Views.NotePage)); } private async Task SelectNoteAsync(ViewModels.NoteViewModel note) { if (note != null) await Shell.Current.GoToAsync($"{nameof(Views.NotePage)}?load={note.Identifier}"); }
Observe que o método
NewNoteAsync
não usa um parâmetro enquanto oSelectNoteAsync
usa. Opcionalmente, os comandos podem ter um único parâmetro fornecido quando o comando é invocado. Para o métodoSelectNoteAsync
, o parâmetro representa a anotação que está sendo selecionada.Por fim, implemente o método
IQueryAttributable.ApplyQueryAttributes
:void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { if (query.ContainsKey("deleted")) { string noteId = query["deleted"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note exists, delete it if (matchedNote != null) AllNotes.Remove(matchedNote); } else if (query.ContainsKey("saved")) { string noteId = query["saved"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note is found, update it if (matchedNote != null) matchedNote.Reload(); // If note isn't found, it's new; add it. else AllNotes.Add(new NoteViewModel(Note.Load(noteId))); } }
O viewmodel da Anotação criado na etapa anterior do tutorial usou a navegação quando a anotação foi salva ou excluída. O viewmodel navegou de volta para a exibição AllNotes, à qual esse viewmodel está associado. Esse código detecta se a cadeia de caracteres de consulta contém a chave
deleted
ousaved
. O valor da chave é o identificador exclusivo da nota.Se a anotação tiver sido excluída, ela será correspondida na coleção
AllNotes
pelo identificador fornecido e removida.Há dois motivos possíveis para que uma anotação seja salva. A anotação foi criada ou uma anotação existente foi alterada. Se a anotação já estiver na coleção
AllNotes
, será uma anotação que foi atualizada. Nesse caso, a instância de anotação na coleção só precisa ser atualizada. Se a anotação não estiver na coleção, ela será uma nova anotação e deverá ser adicionada à coleção.
O código da classe deve ser semelhante ao seguinte snippet:
using CommunityToolkit.Mvvm.Input;
using Notes.Models;
using System.Collections.ObjectModel;
using System.Windows.Input;
namespace Notes.ViewModels;
internal class NotesViewModel : IQueryAttributable
{
public ObservableCollection<ViewModels.NoteViewModel> AllNotes { get; }
public ICommand NewCommand { get; }
public ICommand SelectNoteCommand { get; }
public NotesViewModel()
{
AllNotes = new ObservableCollection<ViewModels.NoteViewModel>(Models.Note.LoadAll().Select(n => new NoteViewModel(n)));
NewCommand = new AsyncRelayCommand(NewNoteAsync);
SelectNoteCommand = new AsyncRelayCommand<ViewModels.NoteViewModel>(SelectNoteAsync);
}
private async Task NewNoteAsync()
{
await Shell.Current.GoToAsync(nameof(Views.NotePage));
}
private async Task SelectNoteAsync(ViewModels.NoteViewModel note)
{
if (note != null)
await Shell.Current.GoToAsync($"{nameof(Views.NotePage)}?load={note.Identifier}");
}
void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.ContainsKey("deleted"))
{
string noteId = query["deleted"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note exists, delete it
if (matchedNote != null)
AllNotes.Remove(matchedNote);
}
else if (query.ContainsKey("saved"))
{
string noteId = query["saved"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note is found, update it
if (matchedNote != null)
matchedNote.Reload();
// If note isn't found, it's new; add it.
else
AllNotes.Add(new NoteViewModel(Note.Load(noteId)));
}
}
}
Exibição AllNotes
Agora que o viewmodel foi criado, atualize a exibição AllNotes para apontar para as propriedades viewmodel. No arquivo Views\AllNotesPage.xaml, aplique as seguintes alterações:
- Adicione o namespace de XML
xmlns:viewModels
direcionado ao namespace do .NETNotes.ViewModels
. - Adicione um
BindingContext
à página. - Remova o evento
Clicked
do botão da barra de ferramentas e use a propriedadeCommand
. - Altere o
CollectionView
para associar seuItemSource
aAllNotes
. - Altere o
CollectionView
para usar comandos para reagir quando o item selecionado for alterado.
Atualize a exibição AllNotes:
No painel Gerenciador de Soluções do Visual Studio, clique duas vezes em Views\AllNotesPage.xaml.
Cole o código a seguir:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:Notes.ViewModels" x:Class="Notes.Views.AllNotesPage" Title="Your Notes"> <ContentPage.BindingContext> <viewModels:NotesViewModel /> </ContentPage.BindingContext> <!-- Add an item to the toolbar --> <ContentPage.ToolbarItems> <ToolbarItem Text="Add" Command="{Binding NewCommand}" IconImageSource="{FontImage Glyph='+', Color=Black, Size=22}" /> </ContentPage.ToolbarItems> <!-- Display notes in a list --> <CollectionView x:Name="notesCollection" ItemsSource="{Binding AllNotes}" Margin="20" SelectionMode="Single" SelectionChangedCommand="{Binding SelectNoteCommand}" SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}"> <!-- Designate how the collection of items are laid out --> <CollectionView.ItemsLayout> <LinearItemsLayout Orientation="Vertical" ItemSpacing="10" /> </CollectionView.ItemsLayout> <!-- Define the appearance of each item in the list --> <CollectionView.ItemTemplate> <DataTemplate> <StackLayout> <Label Text="{Binding Text}" FontSize="22"/> <Label Text="{Binding Date}" FontSize="14" TextColor="Silver"/> </StackLayout> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> </ContentPage>
A barra de ferramentas não usa mais o evento Clicked
e, em vez disso, usa um comando.
O CollectionView
dá suporte ao comando com as propriedades SelectionChangedCommand
e SelectionChangedCommandParameter
. No XAML atualizado, a propriedade SelectionChangedCommand
é associada ao SelectNoteCommand
do viewmodel, o que significa que o comando é invocado quando o item selecionado é alterado. Quando o comando é invocado, o valor da propriedade SelectionChangedCommandParameter
é passado para o comando.
Examine a associação usada para o CollectionView
:
<CollectionView x:Name="notesCollection"
ItemsSource="{Binding AllNotes}"
Margin="20"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectNoteCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
A propriedade SelectionChangedCommandParameter
usa a associação Source={RelativeSource Self}
. O Self
faz referência ao objeto atual, que é o CollectionView
. Observe que o caminho de associação é a propriedade SelectedItem
. Quando o comando é invocado ao alterar o item selecionado, o comando SelectNoteCommand
é invocado e o item selecionado é passado para o comando como um parâmetro.
Limpar o code-behind de AllNotes
Agora que a interação com a exibição foi alterada de manipuladores de eventos para comandos, abra o arquivo Views\AllNotesPage.xaml.cs e substitua todo o código por uma classe que contém apenas o construtor:
No painel Gerenciador de Soluções do Visual Studio, clique duas vezes em Views\AllNotesPage.xaml.cs.
Dica
Talvez seja necessário expandir Views\AllNotesPage.xaml para mostrar o arquivo.
Substitua o código pelo seguinte snippet:
namespace Notes.Views; public partial class AllNotesPage : ContentPage { public AllNotesPage() { InitializeComponent(); } }
Executar o aplicativo
Agora, você pode executar o aplicativo, e tudo está funcionando. No entanto, há dois problemas com o comportamento do aplicativo:
- Se você selecionar uma anotação que abre o editor, pressione Salvar e tente selecionar a mesma anotação; ela não funcionará.
- Sempre que uma anotação é alterada ou adicionada, a lista de anotações não é reordenada para mostrar as anotações mais recentes na parte superior.
Esses dois problemas são corrigidos na próxima etapa do tutorial.
Corrigir o comportamento do aplicativo
Agora que o código do aplicativo pode compilar e executar, você provavelmente terá notado que há duas falhas no comportamento do aplicativo. O aplicativo não permite que você selecione novamente uma anotação já selecionada, e a lista de anotações não é reordenada depois que uma anotação é criada ou alterada.
Colocar as anotações na parte superior da lista
Primeiro, corrija o problema de reordenação com a lista de anotações. No arquivo ViewModels\NotesViewModel.cs, a coleção AllNotes
contém todas as anotações a serem apresentadas ao usuário. Infelizmente, a desvantagem de usar um ObservableCollection
é que ele deve ser classificado manualmente. Para colocar os itens novos ou atualizados na parte superior da lista, execute as seguintes etapas:
No painel Gerenciador de Soluções do Visual Studio, clique duas vezes em ViewModels\NotesViewModel.cs.
No método
ApplyQueryAttributes
, examine a lógica na chave da cadeia de caracteres de consulta salva.Quando o
matchedNote
não énull
, a anotação está sendo atualizada. Use o métodoAllNotes.Move
para mover omatchedNote
para o índice 0, que é o topo da lista.string noteId = query["saved"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note is found, update it if (matchedNote != null) { matchedNote.Reload(); AllNotes.Move(AllNotes.IndexOf(matchedNote), 0); }
O método
AllNotes.Move
usa dois parâmetros para mover a posição de um objeto na coleção. O primeiro parâmetro é o índice do objeto que será movido, e o segundo parâmetro é o índice de onde mover o objeto. O métodoAllNotes.IndexOf
recupera o índice da anotação.Quando o
matchedNote
énull
, a anotação é nova e está sendo adicionada à lista. Em vez de adicioná-la, que acrescenta a anotação ao final da lista, insira a anotação no índice 0, que é a parte superior da lista. Altere o métodoAllNotes.Add
paraAllNotes.Insert
.string noteId = query["saved"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note is found, update it if (matchedNote != null) { matchedNote.Reload(); AllNotes.Move(AllNotes.IndexOf(matchedNote), 0); } // If note isn't found, it's new; add it. else AllNotes.Insert(0, new NoteViewModel(Models.Note.Load(noteId)));
O método ApplyQueryAttributes
deve ser semelhante ao seguinte snippet de código:
void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.ContainsKey("deleted"))
{
string noteId = query["deleted"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note exists, delete it
if (matchedNote != null)
AllNotes.Remove(matchedNote);
}
else if (query.ContainsKey("saved"))
{
string noteId = query["saved"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note is found, update it
if (matchedNote != null)
{
matchedNote.Reload();
AllNotes.Move(AllNotes.IndexOf(matchedNote), 0);
}
// If note isn't found, it's new; add it.
else
AllNotes.Insert(0, new NoteViewModel(Models.Note.Load(noteId)));
}
}
Permitir a seleção de uma anotação duas vezes
Na exibição AllNotes, o CollectionView
lista todas as anotações, mas não permite que você selecione a mesma anotação duas vezes. Há duas maneiras de o item permanecer selecionado: quando o usuário altera uma anotação existente e quando o usuário retrocede a navegação impreterivelmente. O caso em que o usuário salva uma anotação é corrigido com a alteração de código na seção anterior que usa AllNotes.Move
, para que você não precise se preocupar com esse caso.
O problema que você precisa resolver agora está relacionado à navegação. Independentemente de como a exibição Allnotes é acessada, o evento NavigatedTo
é gerado para a página. Esse evento é um lugar perfeito para desmarcar impreterivelmente o item selecionado no CollectionView
.
No entanto, com o padrão MVVM sendo aplicado aqui, o viewmodel não pode disparar algo diretamente na exibição, como limpar o item selecionado após a anotação ser salva. Então, como você faz isso acontecer? Uma boa implementação do padrão MVVM minimiza o code-behind na exibição. Há algumas maneiras diferentes de resolver esse problema para dar suporte ao padrão de separação MVVM. No entanto, também não há problema em colocar o código no code-behind da exibição, sobretudo quando ele está diretamente vinculado à exibição. O MVVM tem muitos designs e conceitos excelentes que ajudam a compartimentalizar seu aplicativo, melhorando a manutenção e facilitando a adição de novos recursos. No entanto, em alguns casos, você pode achar que o MVVM incentiva o overengineering.
Não faça overengineering de uma solução para esse problema e use apenas o evento NavigatedTo
para limpar o item selecionado do CollectionView
.
No painel Gerenciador de Soluções do Visual Studio, clique duas vezes em Views\AllNotesPage.xaml.
No XAML do
<ContentPage>
, adicione o eventoNavigatedTo
:<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:Notes.ViewModels" x:Class="Notes.Views.AllNotesPage" Title="Your Notes" NavigatedTo="ContentPage_NavigatedTo"> <ContentPage.BindingContext> <viewModels:NotesViewModel /> </ContentPage.BindingContext>
Para adicionar um manipulador de eventos padrão, clique com o botão direito do mouse no nome do método do evento,
ContentPage_NavigatedTo
, e selecione Ir para Definição. Essa ação abre o Views\AllNotesPage.xaml.cs no editor de código.Substitua o código do manipulador de eventos pelo seguinte snippet:
private void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e) { notesCollection.SelectedItem = null; }
No XAML, o
CollectionView
recebeu o nome denotesCollection
. Esse código usa esse nome para acessar oCollectionView
e definirSelectedItem
comonull
. O item selecionado é desmarcado sempre que a página é acessada.
Agora, execute o aplicativo. Tente navegar até uma anotação, pressione o botão voltar e selecione a mesma anotação uma segunda vez. O comportamento do aplicativo foi corrigido!
Explore o código deste tutorial.. Se você quiser baixar uma cópia do projeto concluído com a qual comparar seu código, baixe este projeto.
Parabéns!
Seu aplicativo agora está usando padrões MVVM!
Próximas etapas
Os seguintes links fornecem mais informações relacionadas a alguns dos conceitos que você aprendeu neste tutorial:
Tem algum problema com essa seção? Se tiver, envie seus comentários para que possamos melhorar esta seção.