Обновление приложения с помощью концепций MVVM
Эта серия учебников предназначена для продолжения работы с руководством по созданию приложения .NET MAUI, которое создало приложение для заметок. Из этой части вы узнаете, как выполнять такие задачи:
- Реализуйте шаблон модели-view-viewmodel (MVVM).
- Используйте дополнительный стиль строки запроса для передачи данных во время навигации.
Мы настоятельно рекомендуем сначала следовать руководству по созданию приложения .NET MAUI, так как код, созданный в этом руководстве, является основой для этого руководства. Если вы потеряли код или хотите начать новую версию, скачайте этот проект.
Общие сведения о MVVM
Интерфейс разработчика .NET MAUI обычно включает создание пользовательского интерфейса в XAML, а затем добавление кода, работающего в пользовательском интерфейсе. Сложные проблемы с обслуживанием могут возникнуть, так как приложения изменяются и увеличиваются в размерах и области. Эти проблемы включают тесное взаимодействие между элементами управления пользовательским интерфейсом и бизнес-логикой, что повышает затраты на изменение пользовательского интерфейса и трудности модульного тестирования такого кода.
Шаблон модели представления представления (MVVM) помогает четко отделять бизнес-логику приложения и логику презентации от пользовательского интерфейса. Поддержание четкого разделения между логикой приложения и пользовательским интерфейсом помогает решить многочисленные проблемы разработки и упрощает тестирование, обслуживание и развитие приложения. Он также может значительно улучшить возможности повторного использования кода и позволяет разработчикам и конструкторам пользовательского интерфейса более легко сотрудничать при разработке соответствующих частей приложения.
Шаблон
В шаблоне MVVM есть три основных компонента: модель, представление и модель представления. Каждый служит отдельной целью. На следующей схеме показаны связи между тремя компонентами.
Помимо понимания обязанностей каждого компонента, важно также понять, как они взаимодействуют. На высоком уровне представление "знает о" модели представления, и модель представления "знает о" модели, но модель не знает о модели представления, и модель представления не знает о представлении. Поэтому модель представления изолирует представление от модели и позволяет модели развиваться независимо от представления.
Ключ к эффективному использованию MVVM заключается в понимании того, как фактор кода приложения в правильные классы и как классы взаимодействуют.
Представления
Представление отвечает за определение структуры, макета и внешнего вида того, что пользователь видит на экране. В идеале каждое представление определяется в XAML с ограниченным кодом, который не содержит бизнес-логику. Однако в некоторых случаях код программной части может содержать логику пользовательского интерфейса, которая реализует визуальное поведение, которое трудно выразить в XAML, например анимации.
ViewModel
Модель представления реализует свойства и команды, к которым представление может привязать данные, и уведомляет представление о любых изменениях состояния с помощью событий уведомлений об изменении. Свойства и команды, предоставляемые моделью представления, определяют функциональные возможности, предоставляемые пользовательским интерфейсом, но представление определяет способ отображения этой функции.
Модель представления также отвечает за координацию взаимодействия представления с любыми необходимыми классами моделей. Обычно между моделью представления и классами моделей обычно существует связь "один ко многим".
Каждая модель представления предоставляет данные из модели в форме, которую представление может легко использовать. Для этого модель представления иногда выполняет преобразование данных. Размещение этого преобразования данных в модели представления является хорошей идеей, так как она предоставляет свойства, к которым может привязаться представление. Например, модель представления может объединить значения двух свойств, чтобы упростить отображение в представлении.
Внимание
Маршалирует обновления привязки к потоку пользовательского интерфейса .NET MAUI. При использовании MVVM это позволяет обновлять свойства представления, привязанные к данным, из любого потока, с подсистемой привязки .NET MAUI, которая позволяет обновлять поток пользовательского интерфейса.
Модель
Классы модели — это не визуальные классы, которые инкапсулируют данные приложения. Поэтому модель можно рассматривать как представляющую модель домена приложения, которая обычно включает модель данных вместе с бизнес-логикой и логикой проверки.
Обновление модели
В этой первой части руководства вы реализуете шаблон модели-представления представления (MVVM). Чтобы начать, откройте решение Notes.sln в Visual Studio.
Очистка модели
В предыдущем руководстве типы моделей действовали как модель (данные), так и как модель представления (подготовка данных), которая была сопоставлена непосредственно с представлением. В следующей таблице описывается модель:
Файл кода | Description |
---|---|
Модели/About.cs | Модель About . Содержит поля только для чтения, описывающие само приложение, например название и версию приложения. |
Модели/Note.cs | Модель Note . Представляет примечание. |
Модели/AllNotes.cs | Модель AllNotes . Загружает все заметки на устройстве в коллекцию. |
Думая о самом приложении, существует только одна часть данных, которая используется приложением.Note
Заметки загружаются с устройства, сохраняются на устройстве и редактируются с помощью пользовательского интерфейса приложения. На самом деле нет необходимости в About
моделях и AllNotes
моделях. Удалите эти модели из проекта:
- Найдите область Обозреватель решений Visual Studio.
- Щелкните правой кнопкой мыши файл Models\About.cs и выберите "Удалить". Нажмите кнопку ОК , чтобы удалить файл.
- Щелкните правой кнопкой мыши файл Models\AllNotes.cs и выберите "Удалить". Нажмите кнопку ОК , чтобы удалить файл.
Единственным оставшимся файлом модели является файл Models\Note.cs .
Обновление модели
Модель содержит следующее Note
:
- Уникальный идентификатор, который является именем файла заметки, хранящейся на устройстве.
- Текст заметки.
- Дата, указываемая при создании или последнем обновлении заметки.
В настоящее время загрузка и сохранение модели была выполнена через представления, и в некоторых случаях другими типами моделей, которые вы только что удалили. Код, который имеется для Note
типа, должен иметь следующий вид:
namespace Notes.Models;
internal class Note
{
public string Filename { get; set; }
public string Text { get; set; }
public DateTime Date { get; set; }
}
Модель Note
будет развернута для обработки загрузки, сохранения и удаления заметок.
В области Обозреватель решений Visual Studio дважды щелкните "Модели\Note.cs".
В редакторе кода добавьте в класс следующие два метода
Note
. Эти методы основаны на экземплярах и обрабатывают сохранение или удаление текущей заметки на устройство или с устройства соответственно: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));
Приложение должно загружать заметки двумя способами, загружая отдельную заметку из файла и загружая все заметки на устройстве. Код для обработки загрузки может быть
static
членом, не требуя выполнения экземпляра класса.Добавьте следующий код в класс, чтобы загрузить заметку по имени файла:
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); }
Этот код возвращает перечисленную коллекцию типов моделей
Note
путем извлечения файлов на устройстве, соответствующего шаблону файла заметок: *.notes.txt. Каждое имя файла передается методуLoad
, загружая отдельную заметку. Наконец, коллекция заметок упорядочена по дате каждой заметки и возвращается вызывающему объекту.Наконец, добавьте конструктор в класс, который задает значения по умолчанию для свойств, включая случайное имя файла:
public Note() { Filename = $"{Path.GetRandomFileName()}.notes.txt"; Date = DateTime.Now; Text = ""; }
Код Note
класса должен выглядеть следующим образом:
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);
}
}
Теперь, когда Note
модель завершена, можно создать модели представления.
Создание объекта About viewmodel
Перед добавлением моделей представления в проект добавьте ссылку на набор средств сообщества MVVM. Эта библиотека доступна в NuGet и предоставляет типы и системы, которые помогают реализовать шаблон MVVM.
В области Обозреватель решений Visual Studio щелкните правой кнопкой мыши проект >"Заметки" "Управление пакетами NuGet".
Откройте вкладку Browse (Обзор).
Найдите mvm communitytoolkit и выберите
CommunityToolkit.Mvvm
пакет, который должен быть первым результатом.Убедитесь, что выбрана по крайней мере версия 8. Это руководство было написано с помощью версии 8.0.0.
Затем нажмите кнопку "Установить " и примите отображаемые запросы.
Теперь вы готовы начать обновление проекта, добавив модели представления.
Разделение моделей представления
Связь представления к представлению зависит от системы привязки, предоставляемой интерфейсом мультиплатформенного приложения .NET (.NET MAUI). Приложение уже использует привязку в представлениях для отображения списка заметок и представления текста и даты одной заметки. Логика приложения в настоящее время предоставляется кодом представления и напрямую привязана к представлению. Например, когда пользователь редактирует заметку и нажимает кнопку "Сохранить ", Clicked
вызывается событие для кнопки. Затем код для обработчика событий сохраняет текст заметки в файл и переходит к предыдущему экрану.
Наличие логики приложения в коде представления может стать проблемой при изменении представления. Например, если кнопка заменена другим элементом управления ввода или именем элемента управления изменяется, обработчики событий могут стать недопустимыми. Независимо от того, как это представление разработано, цель представления заключается в вызове какой-либо логики приложения и представления информации пользователю. Для этого приложения Save
кнопка сохраняет заметку, а затем возвращается к предыдущему экрану.
Viewmodel предоставляет приложению определенное место для размещения логики приложения независимо от того, как пользовательский интерфейс разработан или как загружаются или сохраняются данные. Модель представления — это клей, представляющий и взаимодействующий с моделью данных от имени представления.
Модели представления хранятся в папке ViewModels .
- Найдите область Обозреватель решений Visual Studio.
- Щелкните правой кнопкой мыши проект "Заметки" и выберите "Добавить>новую папку". Назовите папку ViewModels.
- Щелкните правой кнопкой мыши папку >ViewModels Add>Class и назовите ее AboutViewModel.cs.
- Повторите предыдущий шаг и создайте еще две модели представления:
- NoteViewModel.cs
- NotesViewModel.cs
Структура проекта должна выглядеть следующим образом:
Сведения о представлении viewmodel и About view
В представлении "Сведения" отображаются некоторые данные на экране и при необходимости переход к веб-сайту с дополнительными сведениями. Так как в этом представлении нет данных для изменения, например с элементом управления вводом текста или выбором элементов из списка, это хороший кандидат для демонстрации добавления представления. Для модели "Сведения о представлении" не существует резервной модели.
Создайте модель представления About:
В области Обозреватель решений Visual Studio дважды щелкните ViewModels\AboutViewModel.cs.
Вставьте следующий код.
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); }
Предыдущий фрагмент кода содержит некоторые свойства, представляющие сведения о приложении, такие как имя и версия. Этот фрагмент точно такой же, как и модель About , удаленная ранее. Однако этот модуль представления содержит новую концепцию, ShowMoreInfoCommand
свойство команды.
Команды — это привязываемые действия, которые вызывают код и являются отличным местом для размещения логики приложения. В этом примере ShowMoreInfoCommand
указывает на ShowMoreInfo
метод, который открывает веб-браузер на определенную страницу. Дополнительные сведения о командной системе см. в следующем разделе.
Сведения о представлении
Представление About необходимо немного изменить, чтобы подключить его к модели представления, созданной в предыдущем разделе. В файле Views\AboutPage.xaml примените следующие изменения:
xmlns:models
Обновите пространство имен XML иxmlns:viewModels
нацелите его наNotes.ViewModels
пространство имен .NET.- Измените
ContentPage.BindingContext
свойство на новый экземплярAbout
представления. - Удалите обработчик событий кнопки
Clicked
и используйтеCommand
это свойство.
Обновите представление "Сведения":
В области Обозреватель решений Visual Studio дважды щелкните Views\AboutPage.xaml.
Вставьте следующий код.
<?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>
В предыдущем фрагменте кода выделены строки, которые изменились в этой версии представления.
Обратите внимание, что кнопка использует Command
свойство. Многие элементы управления имеют Command
свойство, вызываемое при взаимодействии пользователя с элементом управления. При использовании с кнопкой команда вызывается при нажатии кнопки, аналогично тому, как Clicked
вызывается обработчик событий, за исключением того, что можно привязать Command
к свойству в режиме просмотра.
В этом представлении, когда пользователь нажимает кнопку, Command
вызывается. Привязывается Command
к ShowMoreInfoCommand
свойству в режиме просмотра и при вызове запускает код в ShowMoreInfo
методе, который открывает веб-браузер на определенную страницу.
Очистка кода программной части
Кнопка ShowMoreInfo
не использует обработчик событий, поэтому LearnMore_Clicked
код должен быть удален из файла Views\AboutPage.xaml.cs . Удалите этот код, класс должен содержать только конструктор:
В области Обозреватель решений Visual Studio дважды щелкните представления\AboutPage.xaml.cs.
Совет
Чтобы отобразить файл, может потребоваться развернуть views\AboutPage.xaml .
Замените код следующим фрагментом кода:
namespace Notes.Views; public partial class AboutPage : ContentPage { public AboutPage() { InitializeComponent(); } }
Создание модели представления заметок
Цель обновления представления заметок заключается в том, чтобы переместить как можно больше функций из кода XAML за пределами кода XAML и поместить его в модель представления Заметок.
Модель представления заметок
В зависимости от того, что требуется представлению заметок, необходимо предоставить следующие элементы:
- Текст заметки.
- Дата и время создания или последнего обновления заметки.
- Команда, которая сохраняет заметку.
- Команда, которая удаляет заметку.
Создайте модель представления заметок:
В области Обозреватель решений Visual Studio дважды щелкните ViewModels\NoteViewModel.cs.
Замените код в этом файле следующим фрагментом кода:
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.ComponentModel; using System.Windows.Input; namespace Notes.ViewModels; internal class NoteViewModel : ObservableObject, IQueryAttributable { private Models.Note _note; }
Этот код является пустым
Note
представлением, где вы добавите свойства и команды для поддержкиNote
представления. Обратите внимание, чтоCommunityToolkit.Mvvm.ComponentModel
пространство имен импортируется. Это пространство имен предоставляет используемоеObservableObject
в качестве базового класса. Дополнительные сведенияObservableObject
см. на следующем шаге. ПространствоCommunityToolkit.Mvvm.Input
имен также импортируется. Это пространство имен предоставляет некоторые типы команд, которые вызывают методы асинхронно.Модель
Models.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;
Identifier
СвойстваDate
— это простые свойства, которые просто извлекают соответствующие значения из модели.Совет
Для свойств
=>
синтаксис создает свойство только для получения, в котором инструкция справа=>
от значения должна быть возвращена.Свойство
Text
сначала проверяет, является ли заданное значение другим значением. Если значение отличается, это значение передается свойству модели иOnPropertyChanged
вызывается метод.Метод
OnPropertyChanged
предоставляется базовым классомObservableObject
. Этот метод использует имя вызывающего кода, в данном случае имя свойства Text и вызываетObservableObject.PropertyChanged
событие. Это событие предоставляет имя свойства подписчикам событий. Система привязки, предоставляемая .NET MAUI, распознает это событие и обновляет все связанные привязки в пользовательском интерфейсе. При изменении свойства событие вызывается в режиме представленияText
Note, а любой элемент пользовательского интерфейса, привязанный кText
свойству, уведомляется об изменении свойства.Добавьте в класс следующие свойства команды, к которым может привязаться представление:
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); }
Эти два конструктора используются либо для создания модели представления с новой резервной моделью, которая является пустой заметкой, либо для создания модели представления, использующего указанный экземпляр модели.
Конструкторы также настраивают команды для viewmodel. Затем добавьте код для этих команд.
Save
Добавьте и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}"); }
Эти методы вызываются связанными командами. Они выполняют связанные действия в модели и делают приложение переходом на предыдущую страницу. Параметр строки запроса добавляется в
..
путь навигации, указывающий, какое действие было предприняно и уникальный идентификатор заметки.Затем добавьте метод в
ApplyQueryAttributes
класс, который удовлетворяет требованиям IQueryAttributable интерфейса:void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { if (query.ContainsKey("load")) { _note = Models.Note.Load(query["load"].ToString()); RefreshProperties(); } }
Когда страница или контекст привязки страницы реализует этот интерфейс, параметры строки запроса, используемые в навигации, передаются методу
ApplyQueryAttributes
. Этот режим представления используется в качестве контекста привязки для представления Заметки. При переходе к представлению заметок контекст привязки представления (этот модель представления) передается параметры строки запроса, используемые во время навигации.Этот код проверяет, указан ли
load
ключ вquery
словаре. Если этот ключ найден, значение должно быть идентификатором (именем файла) заметки для загрузки. Это примечание загружается и задается в качестве объекта базовой модели данного экземпляра viewmodel.Наконец, добавьте в класс следующие два вспомогательных метода:
public void Reload() { _note = Models.Note.Load(_note.Filename); RefreshProperties(); } private void RefreshProperties() { OnPropertyChanged(nameof(Text)); OnPropertyChanged(nameof(Date)); }
Метод
Reload
— это вспомогательный метод, который обновляет объект резервной модели, перезагрузив его из хранилища устройств.Этот
RefreshProperties
метод является другим вспомогательным методом, чтобы убедиться, что все подписчики, привязанные к этому объекту, уведомляются о том, чтоText
изменены свойства иDate
свойства. Так как базовая модель (_note
поле) изменяется при загрузке заметки во время навигации,Text
аDate
свойства фактически не задаются новыми значениями. Так как эти свойства не заданы напрямую, все привязки, присоединенные к этим свойствам, не будут получать уведомления, так какOnPropertyChanged
не вызываются для каждого свойства.RefreshProperties
гарантирует, что привязки к этим свойствам обновляются.
Код класса должен выглядеть следующим фрагментом кода:
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));
}
}
Представление заметок
Теперь, когда была создана модель просмотра, обновите представление Заметок. В файле Views\NotePage.xaml примените следующие изменения:
- Добавьте пространство имен XML, предназначенное
xmlns:viewModels
дляNotes.ViewModels
пространства имен .NET. - Добавьте на
BindingContext
страницу. - Удалите обработчики событий кнопки
Clicked
удаления и сохранения и замените их командами.
Обновите представление заметок:
- В области Обозреватель решений Visual Studio дважды щелкните Views\NotePage.xaml, чтобы открыть редактор XAML.
- Вставьте следующий код.
<?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>
Ранее это представление не объявляло контекст привязки, так как оно было предоставлено кодом самой страницы. Настройка контекста привязки непосредственно в XAML обеспечивает две вещи:
Во время выполнения при переходе на страницу отображается пустая заметка. Это связано с тем, что вызывается конструктор без параметров для контекста привязки , viewmodel. Если вы помните правильно, конструктор без параметров для представления Note создает пустую заметку.
Intellisense в редакторе XAML отображает доступные свойства сразу после начала ввода синтаксиса
{Binding
. Синтаксис также проверяется и оповещает вас о недопустимом значении. Попробуйте изменить синтаксис привязки для параметраSaveCommand
Save123Command
. Если навести указатель мыши на текст, вы заметите, что подсказка отображается, информируя вас о том, что Save123Command не найден. Это уведомление не считается ошибкой, так как привязки являются динамическими, это действительно небольшое предупреждение, которое может помочь вам заметить, когда вы ввели неправильное свойство.Если вы изменили SaveCommand на другое значение, восстановите его.
Очистка кода заметки за пределами
Теперь, когда взаимодействие с представлением изменилось с обработчиков событий на команды, откройте файл Views\NotePage.xaml.cs и замените весь код классом, который содержит только конструктор:
В области Обозреватель решений Visual Studio дважды щелкните представления\NotePage.xaml.cs.
Совет
Чтобы отобразить файл, может потребоваться развернуть Views\NotePage.xaml .
Замените код следующим фрагментом кода:
namespace Notes.Views; public partial class NotePage : ContentPage { public NotePage() { InitializeComponent(); } }
Создание представления заметок
Последняя пара представления представления — это представление " Заметки" и "AllNotes". В настоящее время представление привязано непосредственно к модели, которая была удалена в начале этого руководства. Цель обновления представления AllNotes заключается в том, чтобы переместить как можно больше функциональных возможностей из кода XAML и поместить его в модель просмотра. Опять же, преимуществом является то, что представление может изменить его дизайн с небольшим эффектом к коду.
Представление примечаний
В зависимости от того, что будет отображаться представление AllNotes и какие действия будет выполнять пользователь, в представлении "Заметки" должны быть указаны следующие элементы:
- Коллекция заметок.
- Команда для обработки перехода к заметке.
- Команда для создания новой заметки.
- Обновите список заметок при создании, удалении или изменении.
Создайте модель представления заметок:
В области Обозреватель решений Visual Studio дважды щелкните ViewModels\NotesViewModel.cs.
Замените код в этом файле следующим кодом:
using CommunityToolkit.Mvvm.Input; using System.Collections.ObjectModel; using System.Windows.Input; namespace Notes.ViewModels; internal class NotesViewModel: IQueryAttributable { }
Этот код является пустым
NotesViewModel
, где вы добавите свойства и команды для поддержкиAllNotes
представления.В коде
NotesViewModel
класса добавьте следующие свойства:public ObservableCollection<ViewModels.NoteViewModel> AllNotes { get; } public ICommand NewCommand { get; } public ICommand SelectNoteCommand { get; }
Это
AllNotes
свойство является хранилищемObservableCollection
всех заметок, загруженных с устройства. Две команды будут использоваться представлением для активации действий по созданию заметки или выбору существующей заметки.Добавьте конструктор без параметров в класс, который инициализирует команды и загружает заметки из модели:
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); }
Обратите внимание, что
AllNotes
коллекция используетModels.Note.LoadAll
метод для заполнения наблюдаемой коллекции заметками. МетодLoadAll
возвращает заметки в качествеModels.Note
типа, но наблюдаемая коллекция — это коллекцияViewModels.NoteViewModel
типов. Код используетSelect
расширение Linq для создания экземпляров viewmodel из моделей заметок, возвращаемых изLoadAll
.Создайте методы, предназначенные для команд:
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}"); }
Обратите внимание, что метод
NewNoteAsync
не принимает параметр во время выполненияSelectNoteAsync
. При необходимости команды могут иметь один параметр, который предоставляется при вызове команды.SelectNoteAsync
Для метода параметр представляет выбранную заметку.Наконец, реализуйте
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))); } }
Модель представления заметок, созданная на предыдущем шаге руководства, использовала навигацию при сохранении или удалении заметки. Объект viewmodel вернулся к представлению AllNotes, с которым связан этот объект представления. Этот код определяет, содержит
deleted
saved
ли строка запроса либо ключ. Значение ключа — уникальный идентификатор заметки.Если заметка была удалена, это примечание сопоставляется в
AllNotes
коллекции указанным идентификатором и удаляется.Существует две возможные причины сохранения заметки. Заметка была только что создана, либо была изменена существующая заметка. Если заметка уже находится в
AllNotes
коллекции, это примечание, которое было обновлено. В этом случае экземпляр заметки в коллекции необходимо обновить. Если заметка отсутствует из коллекции, это новая заметка и должна быть добавлена в коллекцию.
Код класса должен выглядеть следующим фрагментом кода:
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)));
}
}
}
Представление AllNotes
Теперь, когда была создана модель представления, обновите представление AllNotes, чтобы указать свойства представления представления. В файле Views\AllNotesPage.xaml примените следующие изменения:
- Добавьте пространство имен XML, предназначенное
xmlns:viewModels
дляNotes.ViewModels
пространства имен .NET. - Добавьте на
BindingContext
страницу. - Удалите событие кнопки
Clicked
панели инструментов и используйтеCommand
это свойство. - Измените привязку к ней
CollectionView
ItemSource
AllNotes
. - Измените
CollectionView
команду, чтобы реагировать на изменения выбранного элемента.
Обновите представление AllNotes:
В области Обозреватель решений Visual Studio дважды щелкните Views\AllNotesPage.xaml.
Вставьте следующий код.
<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>
Панель инструментов больше не использует Clicked
событие и вместо этого использует команду.
Поддерживает CollectionView
команду со свойствами SelectionChangedCommand
и SelectionChangedCommandParameter
свойствами. В обновленном XAML SelectionChangedCommand
свойство привязано к объекту viewmodel SelectNoteCommand
, что означает, что команда вызывается при изменении выбранного элемента. При вызове SelectionChangedCommandParameter
команды значение свойства передается команде.
Просмотрите привязку, используемую для CollectionView
:
<CollectionView x:Name="notesCollection"
ItemsSource="{Binding AllNotes}"
Margin="20"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectNoteCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
Свойство SelectionChangedCommandParameter
использует Source={RelativeSource Self}
привязку. Ссылается Self
на текущий объект, являющийся CollectionView
объектом . Обратите внимание, что путь привязки является свойством SelectedItem
. При вызове команды путем изменения выбранного элемента вызывается команда, SelectNoteCommand
а выбранный элемент передается команде в качестве параметра.
Очистка кода AllNotes за пределами
Теперь, когда взаимодействие с представлением изменилось с обработчиков событий на команды, откройте файл Views\AllNotesPage.xaml.cs и замените весь код классом, который содержит только конструктор:
В области Обозреватель решений Visual Studio дважды щелкните представления\AllNotesPage.xaml.cs.
Совет
Чтобы отобразить файл, может потребоваться развернуть Views\AllNotesPage.xaml .
Замените код следующим фрагментом кода:
namespace Notes.Views; public partial class AllNotesPage : ContentPage { public AllNotesPage() { InitializeComponent(); } }
Выполнить приложение
Теперь вы можете запустить приложение, и все работает. Однако существует две проблемы с поведением приложения:
- Если выбрать заметку, которая открывает редактор, нажмите клавишу Save, а затем попытаетесь выбрать ту же заметку, она не работает.
- При изменении или добавлении заметки список заметок не переупорядочен, чтобы отобразить последние заметки в верхней части.
Эти две проблемы устранены на следующем шаге руководства.
Исправление поведения приложения
Теперь, когда код приложения может скомпилировать и запустить, скорее всего, вы заметили, что есть два недостатка в том, как работает приложение. Приложение не позволяет повторно выбрать заметку, которая уже выбрана, и список заметок не переупорядочен после создания или изменения заметки.
Получение заметок в верхней части списка
Сначала исправьте проблему переупорядочения списка заметок. В файле AllNotes
ViewModels\NotesViewModel.cs коллекция содержит все заметки, которые будут представлены пользователю. К сожалению, недостатком использования ObservableCollection
является то, что он должен быть отсортирован вручную. Чтобы получить новые или обновленные элементы в верхней части списка, выполните следующие действия:
В области Обозреватель решений Visual Studio дважды щелкните ViewModels\NotesViewModel.cs.
В методе просмотрите
ApplyQueryAttributes
логику для сохраненного ключа строки запроса.Если это
matchedNote
неnull
так, обновляется заметка.AllNotes.Move
Используйте метод для перемещенияmatchedNote
индекса 0, который является верхней частью списка.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); }
Метод
AllNotes.Move
принимает два параметра для перемещения позиции объекта в коллекции. Первый параметр — это индекс объекта, который нужно переместить, а второй — индекс места перемещения объекта. МетодAllNotes.IndexOf
извлекает индекс заметки.matchedNote
В этомnull
случае заметка новая и добавляется в список. Вместо добавления, который добавляет заметку в конец списка, вставьте заметку по индексу 0, которая является верхней частью списка. ИзменитеAllNotes.Add
методAllNotes.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)));
Метод 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();
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)));
}
}
Разрешить выбор заметки дважды
В представлении AllNotes список всех заметок, CollectionView
но не позволяет дважды выбрать одну и ту же заметку. Элемент остается выбранным двумя способами: когда пользователь изменяет существующую заметку, и когда пользователь принудительно перемещается назад. Случай, когда пользователь сохраняет заметку, исправлен с изменением кода в предыдущем разделе, который используется AllNotes.Move
, поэтому вам не нужно беспокоиться об этом случае.
Проблема, из-за которых необходимо решить, связана с навигацией. Независимо от способа перехода NavigatedTo
к представлению Allnotes событие создается для страницы. Это событие идеально подходит для принудительного отмены выбора выбранного элемента в элементе CollectionView
.
Однако при применении здесь шаблона MVVM модель представления не может активировать что-то непосредственно в представлении, например очистка выбранного элемента после сохранения заметки. Так как вы получите это, чтобы случиться? Хорошая реализация шаблона MVVM сводит к минимуму код в представлении. Существует несколько различных способов решения этой проблемы для поддержки шаблона разделения MVVM. Тем не менее, это также хорошо, чтобы поместить код в код позади представления, особенно если он напрямую привязан к представлению. MVVM имеет множество отличных конструкций и концепций, которые помогают разделять приложение, повышая удобство обслуживания и упрощая добавление новых функций. Однако в некоторых случаях может оказаться, что MVVM поощряет чрезмерное развертывание.
Не перезаверяйте решение для этой проблемы и просто используйте NavigatedTo
событие для очистки выбранного элемента из CollectionView
.
В области Обозреватель решений Visual Studio дважды щелкните Views\AllNotesPage.xaml.
Добавьте
NavigatedTo
событие в XAML для этого<ContentPage>
события:<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>
Можно добавить обработчик событий по умолчанию, щелкнув правой кнопкой мыши имя
ContentPage_NavigatedTo
метода события и выбрав "Перейти к определению". Это действие открывает представления\AllNotesPage.xaml.cs в редакторе кода.Замените код обработчика событий следующим фрагментом кода:
private void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e) { notesCollection.SelectedItem = null; }
В XAML
CollectionView
было присвоено имяnotesCollection
. Этот код использует это имя, чтобы получить доступ кCollectionView
и задать значениеSelectedItem
null
. Выбранный элемент очищается при каждом переходе на страницу.
Теперь запустите приложение. Попробуйте перейти к заметке, нажмите кнопку "Назад" и выберите одну и ту же заметку во второй раз. Исправлено поведение приложения!
Изучите код для этого руководства.. Если вы хотите скачать копию завершенного проекта для сравнения кода с этим проектом.
Поздравляем!
Теперь приложение использует шаблоны MVVM!
Следующие шаги
Следующие ссылки содержат дополнительные сведения, связанные с некоторыми понятиями, которые вы узнали в этом руководстве:
Возникла проблема с этим разделом? Если это так, отправьте нам отзыв, чтобы мы исправили этот раздел.