Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Este tutorial foi projetado para continuar o tutorial Criar um aplicativo .NET MAUI , que criou um aplicativo de anotação. Neste tutorial, você aprenderá a:
- Implemente o padrão modelo-visão-exibição de modelo (MVVM).
- 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.
Compreender o MVVM
A experiência do desenvolvedor do .NET MAUI normalmente envolve a criação de uma interface do usuário em XAML e, em seguida, a adição de código subjacente que atua sobre a 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 de 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 clara a lógica de negócio e apresentação de um aplicativo de sua interface de usuário (UI). 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 significativamente as oportunidades de reutilização de código e permite que desenvolvedores e designers de interface do usuário colaborem com mais facilidade ao desenvolver suas respectivas partes de 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 serve a uma finalidade distinta. 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 um alto nível, a visão tem conhecimento do modelo de visão, e o modelo de visão tem conhecimento do modelo, mas o modelo não tem conhecimento do modelo de visão, e o modelo de visão não tem conhecimento da visã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 efetivamente reside no entendimento de como fatorar o código do aplicativo nas classes corretas e como as classes interagem.
Visualizar
A visão é responsável por definir a estrutura, o layout e a aparência do que o usuário vê na tela. Idealmente, 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 que o modelo de exibição fornece 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 visualização também é responsável por coordenar as interações com qualquer classe de modelo necessária. Normalmente, há uma relação um-para-muitos entre o modelo de visualização e as classes de modelo.
Cada modelo de visualização fornece dados de um modelo em uma forma que a visualizaçã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, pois fornece propriedades às quais a exibição pode se associar. Por exemplo, o modelo de visualização pode combinar os valores de duas propriedades para facilitar a exibição pela visualização.
Importante
O .NET MAUI encaminha atualizações de vinculação para o thread da interface do usuário. Ao usar o MVVM, isso permite que você atualize as propriedades do viewmodel associadas a dados de qualquer thread, enquanto o mecanismo de vinculação do .NET MAUI traz as atualizações para a thread da interface do usuário.
Modelo
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 tabela a seguir descreve o modelo:
Arquivo de código | Descrição |
---|---|
Models/About.cs | O About modelo. 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 Note modelo. Representa uma anotação. |
Modelos/AllNotes.cs | O AllNotes modelo. Carrega todas as anotações no dispositivo em uma única coleção. |
Pensando no aplicativo em si, há apenas um dado que é usado pelo aplicativo, o Note
. As anotações são carregadas do dispositivo, salvas no dispositivo e editadas por meio da interface do usuário do aplicativo. Realmente não há necessidade dos modelos About
e AllNotes
. 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 Modelos\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 Note
modelo contém:
- Um identificador exclusivo, que é o nome do arquivo da anotação, conforme armazenado no dispositivo.
- O texto da nota.
- Uma data para indicar quando a anotação foi criada ou atualizada pela última vez.
Atualmente, carregar e salvar o modelo foi feito por meio das exibições e, em alguns casos, pelos outros tipos de modelo que você acabou de remover. O código que você tem para o Note
tipo 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 Note
modelo será expandido para lidar com o carregamento, salvamento e exclusão de anotações.
No painel Gerenciador de Soluções do Visual Studio, clique duas vezes em Modelos\Note.cs.
No editor de código, adicione os dois métodos a seguir à
Note
classe. Esses métodos são baseados em instância e lidam com o salvamento ou exclusão da nota atual no dispositivo ou a partir 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 implementado como
static
membros, não requerendo a execução de uma instância de classe.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 se ele existir.
A segunda maneira de carregar anotações é enumerar todas as anotações no dispositivo e carregá-las em uma coleção.
Adicione o seguinte código à 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
Note
modelo 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
, para carregar uma nota individual. Por fim, 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 Note
código de classe deve ser semelhante ao seguinte:
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 Note
modelo está concluído, os modelos de exibição podem ser criados.
Criar o ViewModel "Sobre"
Antes de 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 Notas e selecione >.
Selecione a guia Navegar.
Pesquise por communitytoolkit mvvm e selecione o
CommunityToolkit.Mvvm
pacote, 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 aceitar todos os prompts exibidos.
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 entre a view e o viewmodel depende bastante do sistema de vínculo fornecido pela .NET Multi-platform App UI (.NET MAUI). O aplicativo já está usando a associação nos modos de exibição para exibir 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 código subjacente da visualização e está diretamente vinculada à visualização. Por exemplo, quando um usuário está editando uma anotação e pressiona o botão Salvar , o Clicked
evento para o botão é acionado. Em seguida, o código subjacente do manipulador de eventos salva o texto da nota em um arquivo e retorna à 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 nota e em seguida 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 Anotações e selecione Adicionar>Nova Pasta. Nomeie a pasta ViewModels.
- Clique com o botão direito do mouse na pasta >>viewModels e nomeie-a AboutViewModel.cs.
- Repita a etapa anterior e crie mais dois modelos de exibição:
- NoteViewModel.cs
- NotesViewModel.cs
Sua estrutura de projeto deve ser semelhante à seguinte imagem:
Sobre o Viewmodel e Sobre o modo de exibição
A exibição Sobre exibe alguns dados na tela e, opcionalmente, navega até um site com mais informações. Como essa visão não tem dados a serem modificados, da mesma forma que um controle de entrada de texto ou ao selecionar itens de uma lista, é um bom candidato para demonstrar a adição de um viewmodel. Para o viewmodel Sobre, não há um modelo subjacente.
Crie o viewmodel Sobre:
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 trecho é exatamente o mesmo que o modelo Sobre que você excluiu anteriormente. No entanto, esse viewmodel contém um novo conceito, a ShowMoreInfoCommand
propriedade de comando.
Os comandos são ações associáveis que invocam código e são um ótimo lugar para colocar a lógica do aplicativo. Neste exemplo, o ShowMoreInfoCommand
aponta para o ShowMoreInfo
método, que abre o navegador da Web em uma página específica. Você aprenderá mais sobre o sistema de comandos na próxima seção.
Sobre a exibição
O visão Sobre precisa ser alterado ligeiramente para ligá-lo ao viewmodel criado na seção anterior. No arquivo Views\AboutPage.xaml , aplique as seguintes alterações:
- Atualize o namespace XML
xmlns:models
paraxmlns:viewModels
e direcione o namespace .NETNotes.ViewModels
. - Altere a
ContentPage.BindingContext
propriedade para uma nova instância doAbout
viewmodel. - Remova o manipulador de eventos do botão
Clicked
e use a propriedadeCommand
.
Atualize a visão 'Sobre':
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" x:DataType="viewModels:AboutViewModel"> <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 trecho de código anterior destaca as linhas que foram alteradas na versão desta visualização.
Observe que o botão está usando a Command
propriedade. Muitos controles têm uma Command
propriedade 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 Clicked
manipulador de eventos é invocado, exceto que você pode associar Command
a uma propriedade no viewmodel.
Nesse modo de exibição, quando o usuário pressiona o botão, o Command
é invocado. O Command
está vinculado à propriedade ShowMoreInfoCommand
no viewmodel e, quando invocado, executa o código no método ShowMoreInfo
, que abre o navegador web para uma página específica.
Limpar o code-behind sobre
O ShowMoreInfo
botão não está usando o manipulador de eventos, portanto, o LearnMore_Clicked
código deve ser removido do arquivo Views\AboutPage.xaml.cs . Excluir esse código, a classe deve conter apenas o construtor:
No painel Gerenciador de Soluções do Visual Studio, clique duas vezes em Exibições\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 de nota
O objetivo de atualizar o modo de exibição Observação é mover o máximo de funcionalidade possível para fora do code-behind XAML e colocá-lo no modo de exibição de Observação.
Observação de viewmodel
Com base no que a exibição de Nota requer, o viewmodel de Nota precisa fornecer os seguintes itens:
- O texto da nota.
- 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 de Nota:
No painel Gerenciador de Soluções do Visual Studio, clique duas vezes em ViewModels\NoteViewModel.cs.
Substitua o código neste 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 em branco
Note
em que você adicionará propriedades e comandos para dar suporte à visualizaçãoNote
. Observe que oCommunityToolkit.Mvvm.ComponentModel
namespace está sendo importado. Esse namespace fornece oObservableObject
usado como a classe base. Você aprenderá mais sobreObservableObject
na próxima etapa. OCommunityToolkit.Mvvm.Input
namespace também é importado. Esse namespace fornece alguns tipos de comando que invocam métodos de forma assíncrona.O
Models.Note
modelo 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
Date
propriedades e asIdentifier
propriedades são simples que apenas recuperam os valores correspondentes do modelo.Dica
Para propriedades, a sintaxe
=>
cria uma propriedade somente de leitura em que a instrução à direita de=>
deve ser avaliada para ser retornada.A
Text
propriedade 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 oOnPropertyChanged
método será chamado.O
OnPropertyChanged
método é fornecido pelaObservableObject
classe base. Esse método usa o nome do código de chamada, nesse caso, o nome da propriedade Text, e aciona 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, quando a propriedadeText
é alterada, o evento é disparado e qualquer elemento de interface do usuário ligado à 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 apoio, que é uma nota vazia, ou para criar um viewmodel que usa a instância de modelo especificada.
Os construtores também configuram os comandos para o modelo de visualização. 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 nota.Em seguida, adicione o
ApplyQueryAttributes
método à classe, que atende aos requisitos da IQueryAttributable interface: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
ApplyQueryAttributes
método. Este viewmodel é usado como contexto de associação para a visualização Nota. Quando se navega para a visualização Nota, o contexto de associação da visualização (este modelo de visualização) recebe os parâmetros da cadeia de consulta usados durante a navegação.Esse código verifica se a
load
chave foi fornecida noquery
dicionário. Se essa chave for encontrada, o valor deverá ser o identificador (o nome do arquivo) da anotação a ser carregada. Essa nota é carregada e definida como o objeto de modelo subjacente da instância de 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
Reload
método é um método auxiliar que atualiza o objeto de modelo de backup, recarregando-o do armazenamento do dispositivoO método
RefreshProperties
é outro auxiliar para garantir que todos os assinantes ligados a esse objeto sejam notificados de que as propriedadesText
eDate
foram alteradas. Como o modelo subjacente (o_note
campo) é alterado quando a nota é carregada durante a navegação, as propriedadesText
eDate
não são realmente definidas como novos valores. Como essas propriedades não são definidas diretamente, nenhuma associação anexada a essas propriedades não seria notificada porqueOnPropertyChanged
não é chamada 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));
}
}
Visualização de notas
Agora que o viewmodel foi criado, atualize o Note view. No arquivo Views\NotePage.xaml , aplique as seguintes alterações:
- Adicione o
xmlns:viewModels
namespace XML direcionado aoNotes.ViewModels
namespace do .NET. - Adicione um
BindingContext
à página. - Remova os manipuladores de eventos de botão
Clicked
excluir e salvar e substitua-os por comandos.
Atualize a visualização de nota:
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" x:DataType="viewModels:NoteViewModel"> <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, esse modo de exibição não declarava um contexto de associação, pois era fornecido pelo code-behind da própria página. Definir o contexto de associação diretamente no XAML fornece duas coisas:
Durante a execução, quando a página é acessada, ela exibe uma nota 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 de Observação produz uma nota em branco.
O intellisense no editor XAML mostra as propriedades disponíveis assim que você começa a digitar
{Binding
a sintaxe. A sintaxe também é validada e alerta você sobre um valor inválido. Tente alterar a sintaxe de associação deSaveCommand
paraSave123Command
. Se você passar o cursor do mouse sobre o texto, observará que uma dica de ferramenta é exibida 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 aviso pequeno 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 Nota
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 Exibições\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 modelo de visão de Anotações
O par viewmodel-view final é o Notes viewmodel e AllNotes view. No momento, entretanto, a exibição está vinculada diretamente ao modelo, que foi deletado no início deste tutorial. A meta na atualização da AllNotes view é mover o máximo de funcionalidade possível para fora do code-behind XAML e colocá-la no modelo de visualização. Novamente, o benefício é que a visualização pode alterar seu design com pouco efeito sobre o seu código.
Modelo de Visualização de Notas
Com base no que a exibição AllNotes vai mostrar e nas interações que o usuário realizará, 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 nova 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 neste 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 espaço em branco
NotesViewModel
em que você adicionará propriedades e comandos para dar suporte à exibiçãoAllNotes
.No código de
NotesViewModel
classe, adicione as seguintes propriedades:public ObservableCollection<ViewModels.NoteViewModel> AllNotes { get; } public ICommand NewCommand { get; } public ICommand SelectNoteCommand { get; }
A propriedade
AllNotes
é umaObservableCollection
que armazena todas as anotações carregadas do dispositivo. Os dois comandos serão usados pela visão para disparar as ações de criação de uma nota ou seleção de uma nota 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
AllNotes
coleção usa oModels.Note.LoadAll
método para preencher a coleção observável com anotações. OLoadAll
método retorna as anotações como oModels.Note
tipo, mas a coleção observável é uma coleção deViewModels.NoteViewModel
tipos. O código usa a extensão Linq para criar instâncias de ViewModel a partir dos modelos de anotação retornados deSelect
.Crie os métodos aos quais os comandos são direcionados:
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
NewNoteAsync
método não usa um parâmetro enquanto oSelectNoteAsync
faz. Opcionalmente, os comandos podem ter um único parâmetro fornecido quando o comando é invocado. Para o métodoSelectNoteAsync
, o parâmetro representa a nota que está sendo selecionada.Por fim, implemente o
IQueryAttributable.ApplyQueryAttributes
método: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 de Nota criado na etapa anterior do tutorial, utilizou a navegação quando a nota foi salva ou excluída. O viewmodel navegou de volta para o modo de exibição AllNotes, ao qual esse viewmodel está associado. Esse código detecta se a string de consulta contém a chave
deleted
ou a chavesaved
. O valor da chave é o identificador exclusivo da nota.Se a anotação tiver sido excluída, essa anotação será correspondida na
AllNotes
coleção pelo identificador fornecido e removida.Há dois motivos possíveis para salvar uma anotação. A anotação foi recém-criada ou uma nota existente foi alterada. Se a anotação já estiver na
AllNotes
coleção, 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 estiver ausente da 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)));
}
}
}
Visualização do AllNotes
Agora que o viewmodel foi criado, atualize a exibição AllNotes para apontar para as propriedades do viewmodel. No arquivo Views\AllNotesPage.xaml , aplique as seguintes alterações:
- Adicione o
xmlns:viewModels
namespace XML direcionado aoNotes.ViewModels
namespace do .NET. - Adicione um
BindingContext
à página. - Remova o evento do botão da barra de ferramentas
Clicked
e use a propriedadeCommand
. - Altere o
CollectionView
para vincular seuItemSource
aAllNotes
. - Altere o
CollectionView
para usar comandos que reagem às mudanças do item selecionado.
Atualize a visualizaçã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" x:DataType="viewModels:NotesViewModel"> <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 x:DataType='CollectionView', 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 x:DataType="viewModels:NoteViewModel"> <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 Clicked
evento e, em vez disso, usa um comando.
O CollectionView
oferece suporte a comandos com as propriedades SelectionChangedCommand
e SelectionChangedCommandParameter
. No XAML atualizado, a propriedade SelectionChangedCommand
está vinculada ao SelectNoteCommand
do viewmodel, o que significa que o comando é invocado quando o item selecionado é alterado. Quando o comando é invocado, o valor da SelectionChangedCommandParameter
propriedade é passado para o comando.
Olhe para a associação usada para o CollectionView
.
<CollectionView x:Name="notesCollection"
ItemsSource="{Binding AllNotes}"
Margin="20"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectNoteCommand}"
SelectionChangedCommandParameter="{Binding x:DataType='CollectionView', Source={RelativeSource Self}, Path=SelectedItem}">
A SelectionChangedCommandParameter
propriedade usa associação Source={RelativeSource Self}
. Faz Self
referência ao objeto atual, que é o CollectionView
. Portanto, x:DataType
especifica CollectionView
como o tipo para a associação compilada. Observe que o caminho de associação é a SelectedItem
propriedade. Quando o comando é invocado alterando o item selecionado, o SelectNoteCommand
comando é invocado e o item selecionado é passado para o comando como um parâmetro.
Para que a expressão de vinculação definida na propriedade SelectionChangedCommandParameter
seja compilada, é necessário instruir o projeto a habilitar vinculações compiladas em expressões que especificam a propriedade Source
. Para fazer isso, edite o arquivo de projeto para sua solução e adicione <MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
dentro do <PropertyGroup>
elemento:
<PropertyGroup>
<MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
</PropertyGroup>
Limpar o backend do código do AllNotes
Agora que a interação com o modo de 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 Exibições\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 nota, que abre o editor, pressione Salvar e, em seguida, tente selecionar a mesma nota, ela não funcionará.
- Sempre que uma anotação é alterada ou adicionada, a lista de anotações não é reordenada para mostrar as notas 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 na forma como o aplicativo se comporta. O aplicativo não permite que você reelege uma anotação já selecionada e a lista de anotações não é reordenada depois que uma anotação é criada ou alterada.
Colocar anotações no topo da lista
Primeiro, corrija o problema de reordenação com a lista de anotações. No arquivo ViewModels\NotesViewModel.cs , a AllNotes
coleção 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 obter 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 para a chave de consulta de cadeia de caracteres salva.Quando
matchedNote
não énull
, a anotação está sendo atualizada. Use o métodoAllNotes.Move
para mover omatchedNote
para o índice 0, que é a parte superior 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
AllNotes.Move
método 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. OAllNotes.IndexOf
método recupera o índice da anotação.Quando o
matchedNote
estivernull
, a nota será nova e será adicionada à lista. Em vez de adicioná-la, que acrescenta a nota ao final da lista, insira a nota no índice 0, que é a parte superior da lista. Altere oAllNotes.Add
método 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 se parecer com o seguinte trecho 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 selecionar uma nota duas vezes
No modo de exibição AllNotes, CollectionView
lista todas as anotações, mas não permite que você selecione a mesma nota duas vezes. Há duas maneiras de o item permanecer selecionado: quando o usuário altera uma nota existente e quando o usuário navega à força para trás. 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. Não importa como o modo de exibição Allnotes é navegado, o NavigatedTo
evento é gerado para a página. Este evento é uma oportunidade perfeita para forçar a desseleção do item selecionado no CollectionView
.
No entanto, com o padrão MVVM sendo aplicado aqui, o modelo-de-visão não pode disparar algo diretamente na visão, como limpar o item selecionado após a nota 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, especialmente quando ele está diretamente vinculado à exibição. O MVVM tem muitos ótimos designs e conceitos 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 descobrir que o MVVM incentiva o excesso de engenharia.
Não superengenhe uma solução para esse problema, e apenas use 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" x:DataType="viewModels:NotesViewModel"> <ContentPage.BindingContext> <viewModels:NotesViewModel />
Você pode adicionar um manipulador de eventos padrão clicando com o botão direito do mouse no nome
ContentPage_NavigatedTo
do método do evento e selecionando 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
. Este código usa esse nome para acessar oCollectionView
, e definirSelectedItem
comonull
. O item selecionado é removido sempre que a página é acessada.
Agora, execute seu aplicativo. Tente navegar até uma anotação, pressione o botão Voltar e selecione a mesma nota 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.
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: