Actualización de la aplicación con conceptos de MVVM
Esta serie de tutoriales está diseñada para continuar con el tutorial Creación de una aplicación MAUI de .NET , que creó una aplicación de toma de notas. En esta parte de la serie, aprenderá a:
- Actualice la aplicación a .NET 7, si actualmente es .NET 6.
- Implemente el patrón model-view-viewmodel (MVVM).
- Use un estilo adicional de cadena de consulta para pasar datos durante la navegación.
Le recomendamos encarecidamente que siga primero el tutorial Creación de una aplicación MAUI de .NET , ya que el código creado en ese tutorial es la base de este tutorial. Si perdió el código o desea iniciarlo, descargue este proyecto.
Actualización de la aplicación de .NET 6 a .NET 7
Omita este paso si:
- Descargó el proyecto de inicio en el paso anterior.
- Completó la serie de tutoriales anterior a 2023.
Para actualizar la aplicación a .NET 7:
Abra el archivo Notes.csproj en Visual Studio, Visual Studio Code o cualquier otro editor de texto.
Busque el elemento
Project/PropertyGroup/TargetFrameworks
. Si coincide con el fragmento de código siguiente, puede omitir este paso del tutorial; de lo contrario, cámbielo a lo siguiente:<TargetFrameworks>net7.0-android;net7.0-ios;net7.0-maccatalyst</TargetFrameworks>
Busque el
Project/PropertyGroup/TargetFrameworks
elemento con uncondition
elemento que especifique Windows y cámbielo a lo siguiente:<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net7.0-windows10.0.19041.0</TargetFrameworks>
Cambie las versiones mínimas del sistema operativo necesarias para iOS y Mac. Reemplace los dos
SupportedOSPlatformVersion
elementos por lo siguiente:<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">11.0</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion>
Guarde el archivo de proyecto.
Cierre Visual Studio si abrió el proyecto con Visual Studio.
Elimine las carpetas ./bin y ./obj en la misma carpeta que el archivo Notes.csproj .
Descripción de MVVM
La experiencia de desarrollador de .NET MAUI suele implicar la creación de una interfaz de usuario en XAML y, a continuación, agregar código subyacente que funciona en la interfaz de usuario. A medida que las aplicaciones se modifican y aumentan de tamaño y ámbito, pueden surgir problemas de mantenimiento complejos. Estos problemas incluyen el acoplamiento estricto entre los controles de interfaz de usuario y la lógica de negocios, lo que aumenta el costo de realizar modificaciones de la interfaz de usuario y la dificultad de realizar pruebas unitarias de este código.
El patrón model-view-viewmodel (MVVM) ayuda a separar limpiamente la lógica empresarial y de presentación de una aplicación de su interfaz de usuario (UI). Mantener una separación limpia entre la lógica de la aplicación y la interfaz de usuario ayuda a solucionar numerosos problemas de desarrollo y facilita la prueba, el mantenimiento y la evolución de una aplicación. También puede mejorar significativamente las oportunidades de reutilización de código y permite a los desarrolladores y diseñadores de interfaz de usuario colaborar más fácilmente al desarrollar sus respectivas partes de una aplicación.
Patrón
Hay tres componentes principales en el patrón MVVM: el modelo, la vista y el modelo de vista. Cada uno de ellos sirve para un propósito diferente. En el diagrama siguiente se muestran las relaciones entre los tres componentes.
Además de comprender las responsabilidades de cada componente, también es importante comprender cómo interactúan. En general, la vista "conoce" el modelo de vista y el modelo de vista "conoce" el modelo, pero el modelo desconoce el modelo de vista y el modelo de vista desconoce la vista. Por lo tanto, el modelo de vista aísla la vista del modelo y permite que el modelo evolucione independientemente de la vista.
La clave para usar MVVM de forma eficaz consiste en comprender cómo factorizar el código de la aplicación en las clases correctas y cómo interactúan las clases.
Ver
La vista es responsable de definir la estructura, el diseño y la apariencia de lo que ve el usuario en la pantalla. Idealmente, cada vista se define en XAML, con un código limitado que no contiene lógica de negocios. Sin embargo, en algunos casos, el código subyacente podría contener lógica de interfaz de usuario que implementa el comportamiento visual que es difícil de expresar en XAML, como es el caso de las animaciones.
ViewModel
El modelo de vista implementa propiedades y comandos a los que la vista puede enlazar datos, y notifica a la vista los cambios de estado mediante eventos de notificación de cambios. Las propiedades y comandos que proporciona el modelo de vista definen la funcionalidad que ofrece la interfaz de usuario, pero la vista determina cómo se va a mostrar esa funcionalidad.
El modelo de vista también es responsable de coordinar las interacciones de la vista con las clases de modelo necesarias. Normalmente hay una relación uno a varios entre el modelo de vista y las clases de modelo.
Cada modelo de vista proporciona datos de un modelo en un formato que la vista puede consumir fácilmente. Para ello, el modelo de vista a veces realiza la conversión de datos. Colocar esta conversión de datos en el modelo de vista es una buena idea porque proporciona propiedades que la vista puede enlazar. Por ejemplo, el modelo de vista podría combinar los valores de dos propiedades para facilitar la visualización por parte de la vista.
Importante
.NET MAUI serializa las actualizaciones de enlace al subproceso de interfaz de usuario. Al usar MVVM, esto le permite actualizar las propiedades del modelo de vista enlazado a datos desde cualquier subproceso, con el motor de enlace de .NET MAUI que lleva las actualizaciones al subproceso de interfaz de usuario.
Modelo
Las clases de modelo son clases no visuales que encapsulan los datos de la aplicación. Por lo tanto, el modelo se puede considerar como que representa el modelo de dominio de la aplicación, que normalmente incluye un modelo de datos junto con la lógica de validación y de negocios.
Actualizar el modelo
En esta primera parte del tutorial, implementará el patrón model-view-viewmodel (MVVM). Para empezar, abra la solución Notes.sln en Visual Studio.
Limpieza del modelo
En el tutorial anterior, los tipos de modelo actuaban como modelo (datos) y como modelo de vista (preparación de datos), que se asignaba directamente a una vista. En la tabla siguiente se describe el modelo:
Archivo de código | Descripción |
---|---|
Models/About.cs | Modelo About . Contiene campos de solo lectura que describen la propia aplicación, como el título y la versión de la aplicación. |
Models/Note.cs | Modelo Note . Representa una nota. |
Models/AllNotes.cs | Modelo AllNotes . Carga todas las notas del dispositivo en una colección. |
Pensando en la propia aplicación, solo hay un fragmento de datos que usa la aplicación, .Note
Las notas se cargan desde el dispositivo, se guardan en el dispositivo y se editan a través de la interfaz de usuario de la aplicación. Realmente no hay necesidad de los About
modelos y AllNotes
. Quite estos modelos del proyecto:
- Busque el panel Explorador de soluciones de Visual Studio.
- Haga clic con el botón derecho en el archivo Models\About.cs y seleccione Eliminar. Presione Aceptar para eliminar el archivo.
- Haga clic con el botón derecho en el archivo Models\AllNotes.cs y seleccione Eliminar. Presione Aceptar para eliminar el archivo.
El único archivo de modelo restante es el archivo Models\Note.cs .
Actualizar el modelo
El Note
modelo contiene:
- Identificador único, que es el nombre de archivo de la nota tal como se almacena en el dispositivo.
- Texto de la nota.
- Fecha para indicar cuándo se creó la nota o se actualizó por última vez.
Actualmente, la carga y el guardado del modelo se realizaron a través de las vistas y, en algunos casos, por los otros tipos de modelo que acaba de quitar. El código que tiene para el Note
tipo debe ser el siguiente:
namespace Notes.Models;
internal class Note
{
public string Filename { get; set; }
public string Text { get; set; }
public DateTime Date { get; set; }
}
El Note
modelo se expandirá para controlar la carga, el guardado y la eliminación de notas.
En el panel Explorador de soluciones de Visual Studio, haga doble clic en Models\Note.cs.
En el editor de código, agregue los dos métodos siguientes a la
Note
clase . Estos métodos se basan en instancias y controlan el guardado o eliminación de la nota actual en o desde el dispositivo, 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));
La aplicación debe cargar notas de dos maneras, cargando una nota individual desde un archivo y cargando todas las notas en el dispositivo. El código para controlar la carga puede ser
static
miembros, no requerir que se ejecute una instancia de clase.Agregue el código siguiente a la clase para cargar una nota por nombre de archivo:
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) }; }
Este código toma el nombre de archivo como parámetro, compila la ruta de acceso a donde se almacenan las notas en el dispositivo e intenta cargar el archivo si existe.
La segunda manera de cargar notas es enumerar todas las notas del dispositivo y cargarlas en una colección.
Agregue el código siguiente a la clase :
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); }
Este código devuelve una colección enumerable de tipos de
Note
modelo recuperando los archivos del dispositivo que coinciden con el patrón de archivo de notas: *.notes.txt. Cada nombre de archivo se pasa alLoad
método , cargando una nota individual. Por último, la colección de notas se ordena por la fecha de cada nota y se devuelve al autor de la llamada.Por último, agregue un constructor a la clase que establece los valores predeterminados para las propiedades, incluido un nombre de archivo aleatorio:
public Note() { Filename = $"{Path.GetRandomFileName()}.notes.txt"; Date = DateTime.Now; Text = ""; }
El Note
código de clase debe tener un aspecto similar al siguiente:
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);
}
}
Ahora que el Note
modelo está completo, se pueden crear los modelos de vista.
Creación del modelo de vista Acerca de
Antes de agregar modelos de vista al proyecto, agregue una referencia al kit de herramientas de la comunidad de MVVM. Esta biblioteca está disponible en NuGet y proporciona tipos y sistemas que ayudan a implementar el patrón MVVM.
En el panel Explorador de soluciones de Visual Studio, haga clic con el botón derecho en el proyecto >NotesManage NuGet Packages (Administrar paquetes NuGet).
Seleccione la pestaña Examinar.
Busque communitytoolkit mvvm y seleccione el
CommunityToolkit.Mvvm
paquete, que debe ser el primer resultado.Asegúrese de que se selecciona al menos la versión 8. Este tutorial se escribió con la versión 8.0.0.
A continuación, seleccione Instalar y acepte los mensajes que se muestran.
Ahora está listo para empezar a actualizar el proyecto agregando modelos de vista.
Desacoplamiento con modelos de vista
La relación de vista a modelo de vista se basa en gran medida en el sistema de enlace proporcionado por la interfaz de usuario de aplicaciones multiplataforma de .NET (.NET MAUI). La aplicación ya usa el enlace en las vistas para mostrar una lista de notas y presentar el texto y la fecha de una sola nota. La lógica de la aplicación se proporciona actualmente mediante el código subyacente de la vista y está directamente vinculado a la vista. Por ejemplo, cuando un usuario está editando una nota y presiona el botón Guardar , se genera el Clicked
evento del botón. A continuación, el código subyacente del controlador de eventos guarda el texto de la nota en un archivo y navega a la pantalla anterior.
Tener lógica de aplicación en el código subyacente de una vista puede convertirse en un problema cuando cambia la vista. Por ejemplo, si el botón se reemplaza por un control de entrada diferente o se cambia el nombre de un control, los controladores de eventos pueden dejar de ser válidos. Independientemente de cómo se diseñe la vista, el propósito de la vista es invocar algún tipo de lógica de aplicación y presentar información al usuario. Para esta aplicación, el Save
botón guarda la nota y, a continuación, vuelve a la pantalla anterior.
El modelo de vista proporciona a la aplicación un lugar específico para colocar la lógica de la aplicación independientemente de cómo se diseñe la interfaz de usuario o cómo se cargan o se guardan los datos. El modelo de vista es el pegado que representa e interactúa con el modelo de datos en nombre de la vista.
Los modelos de vista se almacenan en una carpeta ViewModels .
- Busque el panel Explorador de soluciones de Visual Studio.
- Haga clic con el botón derecho en el proyecto Notes y seleccione Agregar>nueva carpeta. Asigne a la carpeta el nombre ViewModels.
- Haga clic con el botón derecho en la carpeta >ViewModelsAgregar>clase y asígnele el nombre AboutViewModel.cs.
- Repita el paso anterior y cree dos modelos de vista más:
- NoteViewModel.cs
- NotesViewModel.cs
La estructura del proyecto debe parecerse a la siguiente imagen:
Acerca del modelo de vista y la vista Acerca de
La vista Acerca de muestra algunos datos en la pantalla y, opcionalmente, navega a un sitio web con más información. Dado que esta vista no tiene datos que cambiar, como con un control de entrada de texto o la selección de elementos de una lista, es un buen candidato para mostrar la adición de un modelo de vista. En el caso del modelo de vista Acerca de, no hay ningún modelo de respaldo.
Cree el modelo de vista Acerca de:
En el panel Explorador de soluciones de Visual Studio, haga doble clic en ViewModels\AboutViewModel.cs.
Pegue el código siguiente:
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); }
El fragmento de código anterior contiene algunas propiedades que representan información sobre la aplicación, como el nombre y la versión. Este fragmento de código es exactamente el mismo que el modelo About que eliminó anteriormente. Sin embargo, este modelo de vista contiene un nuevo concepto, la propiedad command ShowMoreInfoCommand
.
Los comandos son acciones enlazables que invocan código y son un excelente lugar para colocar la lógica de la aplicación. En este ejemplo, apunta ShowMoreInfoCommand
al ShowMoreInfo
método , que abre el explorador web en una página específica. Obtendrá más información sobre el sistema de comandos en la sección siguiente.
Acerca de la vista
La vista Acerca de debe cambiarse ligeramente para enlazarla al modelo de vista que se creó en la sección anterior. En el archivo Views\AboutPage.xaml , aplique los siguientes cambios:
- Actualice el
xmlns:models
espacio de nombres XML axmlns:viewModels
y el espacio de nombres .NET como destinoNotes.ViewModels
. - Cambie la
ContentPage.BindingContext
propiedad a una nueva instancia delAbout
modelo de vista. - Quite el controlador de eventos del
Clicked
botón y use laCommand
propiedad .
Actualice la vista Acerca de:
En el panel Explorador de soluciones de Visual Studio, haga doble clic en Views\AboutPage.xaml.
Pegue el código siguiente:
<?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>
El fragmento de código anterior resalta las líneas que han cambiado en esta versión de la vista.
Observe que el botón usa la Command
propiedad . Muchos controles tienen una Command
propiedad que se invoca cuando el usuario interactúa con el control. Cuando se usa con un botón, se invoca el comando cuando un usuario presiona el botón, de forma similar a cómo se invoca el Clicked
controlador de eventos, salvo que se puede enlazar Command
a una propiedad en el modelo de vista.
En esta vista, cuando el usuario presiona el botón , Command
se invoca . Command
está enlazado a la ShowMoreInfoCommand
propiedad en el modelo de vista y, cuando se invoca, ejecuta el código en el ShowMoreInfo
método , que abre el explorador web en una página específica.
Limpieza del código subyacente acerca de
El ShowMoreInfo
botón no usa el controlador de eventos, por lo que el LearnMore_Clicked
código debe quitarse del archivo Views\AboutPage.xaml.cs . Elimine ese código; la clase solo debe contener el constructor :
En el panel Explorador de soluciones de Visual Studio, haga doble clic en Views\AboutPage.xaml.cs.
Sugerencia
Es posible que tengas que expandir Views\AboutPage.xaml para mostrar el archivo.
Reemplace el código por el siguiente fragmento de código:
namespace Notes.Views; public partial class AboutPage : ContentPage { public AboutPage() { InitializeComponent(); } }
Creación del modelo de vista nota
El objetivo de actualizar la vista Nota es mover la mayor cantidad de funcionalidad posible fuera del código XAML subyacente y colocarla en el modelo de vista Nota.
Modelo de vista de nota
En función de lo que requiere la vista Nota , el modelo de vista Nota debe proporcionar los siguientes elementos:
- Texto de la nota.
- Fecha y hora en que se creó o actualizó por última vez la nota.
- Comando que guarda la nota.
- Comando que elimina la nota.
Cree el modelo de vista Nota:
En el panel Explorador de soluciones de Visual Studio, haga doble clic en ViewModels\NoteViewModel.cs.
Reemplace el código de este archivo por el siguiente fragmento de código:
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.ComponentModel; using System.Windows.Input; namespace Notes.ViewModels; internal class NoteViewModel : ObservableObject, IQueryAttributable { private Models.Note _note; }
Este código es el modelo de vista en blanco
Note
donde agregará propiedades y comandos para admitir laNote
vista. Observe que elCommunityToolkit.Mvvm.ComponentModel
espacio de nombres se está importando. Este espacio de nombres proporciona elObservableObject
utilizado como clase base. Obtendrá más información sobreObservableObject
en el paso siguiente. ElCommunityToolkit.Mvvm.Input
espacio de nombres también se importa. Este espacio de nombres proporciona algunos tipos de comandos que invocan métodos de forma asincrónica.El
Models.Note
modelo se almacena como un campo privado. Las propiedades y los métodos de esta clase usarán este campo.Agregue las siguientes propiedades a la clase :
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;
Las
Date
propiedades yIdentifier
son propiedades simples que simplemente recuperan los valores correspondientes del modelo.Sugerencia
En el caso de las propiedades, la
=>
sintaxis crea una propiedad get-only donde la instrucción a la derecha de=>
debe evaluarse como un valor que se va a devolver.La
Text
propiedad comprueba primero si el valor que se establece es un valor diferente. Si el valor es diferente, ese valor se pasa a la propiedad del modelo y se llama alOnPropertyChanged
método .La
OnPropertyChanged
clase base proporciona elObservableObject
método . Este método usa el nombre del código que realiza la llamada, en este caso, el nombre de propiedad de Text y genera elObservableObject.PropertyChanged
evento . Este evento proporciona el nombre de la propiedad a cualquier suscriptor de eventos. El sistema de enlace proporcionado por .NET MAUI reconoce este evento y actualiza los enlaces relacionados de la interfaz de usuario. En el modelo de vista Nota, cuando cambia laText
propiedad, se genera el evento y se notifica a cualquier elemento de la interfaz de usuario enlazado a laText
propiedad que la propiedad haya cambiado.Agregue las siguientes propiedades de comando a la clase , que son los comandos a los que puede enlazar la vista:
public ICommand SaveCommand { get; private set; } public ICommand DeleteCommand { get; private set; }
Agregue los siguientes constructores a la clase :
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); }
Estos dos constructores se usan para crear el modelo de vista con un nuevo modelo de respaldo, que es una nota vacía, o para crear un modelo de vista que use la instancia de modelo especificada.
Los constructores también configuran los comandos para el modelo de vista. A continuación, agregue el código para estos comandos.
Agregue los
Save
métodos yDelete
: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}"); }
Estos métodos se invocan mediante comandos asociados. Realizan las acciones relacionadas en el modelo y hacen que la aplicación navegue a la página anterior. Se agrega un parámetro de cadena de consulta a la
..
ruta de navegación, que indica qué acción se realizó y el identificador único de la nota.A continuación, agregue el
ApplyQueryAttributes
método a la clase , que cumple los requisitos de laIQueryAttributable
interfaz :void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { if (query.ContainsKey("load")) { _note = Models.Note.Load(query["load"].ToString()); RefreshProperties(); } }
Cuando una página o el contexto de enlace de una página implementa esta interfaz, los parámetros de cadena de consulta usados en la navegación se pasan al
ApplyQueryAttributes
método . Este modelo de vista se usa como contexto de enlace para la vista Nota. Cuando se navega a la vista Nota , el contexto de enlace de la vista (este modelo de vista) se pasa los parámetros de cadena de consulta usados durante la navegación.Este código comprueba si la
load
clave se proporcionó en elquery
diccionario. Si se encuentra esta clave, el valor debe ser el identificador (el nombre de archivo) de la nota que se va a cargar. Esa nota se carga y se establece como el objeto de modelo subyacente de esta instancia de modelo de vista.Por último, agregue estos dos métodos auxiliares a la clase :
public void Reload() { _note = Models.Note.Load(_note.Filename); RefreshProperties(); } private void RefreshProperties() { OnPropertyChanged(nameof(Text)); OnPropertyChanged(nameof(Date)); }
El
Reload
método es un método auxiliar que actualiza el objeto del modelo de respaldo y lo vuelve a cargar desde el almacenamiento del dispositivo.El
RefreshProperties
método es otro método auxiliar para asegurarse de que todos los suscriptores enlazados a este objeto reciben una notificación de que lasText
propiedades yDate
han cambiado. Dado que el modelo subyacente (el_note
campo) se cambia cuando se carga la nota durante la navegación, lasText
propiedades yDate
no se establecen realmente en nuevos valores. Dado que estas propiedades no se establecen directamente, no se notificará ningún enlace adjunto a esas propiedades porqueOnPropertyChanged
no se llama a para cada propiedad.RefreshProperties
garantiza que se actualicen los enlaces a estas propiedades.
El código de la clase debe ser similar al siguiente fragmento de código:
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));
}
}
Vista de notas
Ahora que se ha creado el modelo de vista, actualice la vista Nota. En el archivo Views\NotePage.xaml , aplique los siguientes cambios:
- Agregue el
xmlns:viewModels
espacio de nombres XML que tiene como destino elNotes.ViewModels
espacio de nombres .NET. - Agregue un objeto
BindingContext
a la página. - Quite los controladores de eventos de botón
Clicked
eliminar y guardar y reemplácelos por comandos.
Actualice la vista Nota:
- En el panel Explorador de soluciones de Visual Studio, haga doble clic en Views\NotePage.xaml para abrir el editor XAML.
- Pegue el código siguiente:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:Notes.ViewModels"
x:Class="Notes.Views.NotePage"
Title="Note">
<ContentPage.BindingContext>
<viewModels:NoteViewModel />
</ContentPage.BindingContext>
<VerticalStackLayout Spacing="10" Margin="5">
<Editor x:Name="TextEditor"
Placeholder="Enter your note"
Text="{Binding Text}"
HeightRequest="100" />
<Grid ColumnDefinitions="*,*" ColumnSpacing="4">
<Button Text="Save"
Command="{Binding SaveCommand}"/>
<Button Grid.Column="1"
Text="Delete"
Command="{Binding DeleteCommand}"/>
</Grid>
</VerticalStackLayout>
</ContentPage>
Anteriormente, esta vista no declaraba un contexto de enlace, ya que lo proporcionó el código subyacente de la propia página. Establecer el contexto de enlace directamente en xaml proporciona dos cosas:
En tiempo de ejecución, cuando se navega a la página, muestra una nota en blanco. Esto se debe a que se invoca el constructor sin parámetros para el contexto de enlace, el modelo de vista. Si recuerda correctamente, el constructor sin parámetros del modelo de vista Nota crea una nota en blanco.
IntelliSense en el editor XAML muestra las propiedades disponibles en cuanto empiezas a escribir
{Binding
la sintaxis. La sintaxis también se valida y le alerta de un valor no válido. Intente cambiar la sintaxis de enlace deSaveCommand
aSave123Command
. Si mantiene el cursor del mouse sobre el texto, observará que se muestra una información sobre herramientas que le informa de que no se encuentra Save123Command . Esta notificación no se considera un error porque los enlaces son dinámicos, es realmente una pequeña advertencia que puede ayudarle a observar al escribir la propiedad incorrecta.Si ha cambiado SaveCommand a un valor diferente, restáurelo ahora.
Limpiar el código subyacente de la nota
Ahora que la interacción con la vista ha cambiado de controladores de eventos a comandos, abra el archivo Views\NotePage.xaml.cs y reemplace todo el código por una clase que solo contenga el constructor:
En el panel Explorador de soluciones de Visual Studio, haga doble clic en Views\NotePage.xaml.cs.
Sugerencia
Es posible que tengas que expandir Views\NotePage.xaml para mostrar el archivo.
Reemplace el código por el siguiente fragmento de código:
namespace Notes.Views; public partial class NotePage : ContentPage { public NotePage() { InitializeComponent(); } }
Crear el modelo de vista Notas
El par final viewmodel-view es el modelo de vista Notas y la vista AllNotes. Sin embargo, la vista se enlaza directamente al modelo, que se eliminó al principio de este tutorial. El objetivo de actualizar la vista AllNotes es mover la mayor cantidad de funcionalidad posible fuera del código XAML subyacente y colocarla en el modelo de vista. De nuevo, la ventaja es que la vista puede cambiar su diseño con poco efecto en el código.
Modelo de vista de notas
En función de lo que va a mostrar la vista AllNotes y de las interacciones que hará el usuario, el modelo de vista Notas debe proporcionar los siguientes elementos:
- Colección de notas.
- Comando para controlar la navegación a una nota.
- Comando para crear una nueva nota.
- Actualice la lista de notas cuando se crea, elimina o cambia una.
Cree el modelo de vista Notas:
En el panel Explorador de soluciones de Visual Studio, haga doble clic en ViewModels\NotesViewModel.cs.
Reemplace el código de este archivo por el código siguiente:
using CommunityToolkit.Mvvm.Input; using System.Collections.ObjectModel; using System.Windows.Input; namespace Notes.ViewModels; internal class NotesViewModel: IQueryAttributable { }
Este código es el espacio en blanco
NotesViewModel
donde agregará propiedades y comandos para admitir laAllNotes
vista.En el código de
NotesViewModel
clase, agregue las siguientes propiedades:public ObservableCollection<ViewModels.NoteViewModel> AllNotes { get; } public ICommand NewCommand { get; } public ICommand SelectNoteCommand { get; }
La
AllNotes
propiedad es unObservableCollection
objeto que almacena todas las notas cargadas desde el dispositivo. La vista usará los dos comandos para desencadenar las acciones de crear una nota o seleccionar una nota existente.Agregue un constructor sin parámetros a la clase , que inicializa los comandos y carga las notas del 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 la
AllNotes
colección usa elModels.Note.LoadAll
método para rellenar la colección observable con notas. ElLoadAll
método devuelve las notas como tipoModels.Note
, pero la colección observable es una colección deViewModels.NoteViewModel
tipos. El código usa laSelect
extensión Linq para crear instancias de modelo de vista a partir de los modelos de nota devueltos deLoadAll
.Cree los métodos de destino de los comandos:
private async Task NewNoteAsync() { await Shell.Current.GoToAsync(nameof(Views.NotePage)); } private async Task SelectNoteAsync(ViewModels.NoteViewModel note) { if (note != null) await Shell.Current.GoToAsync($"{nameof(Views.NotePage)}?load={note.Identifier}"); }
Observe que el
NewNoteAsync
método no toma un parámetro mientrasSelectNoteAsync
lo hace. Opcionalmente, los comandos pueden tener un único parámetro que se proporciona cuando se invoca el comando. Para el método , elSelectNoteAsync
parámetro representa la nota que se está seleccionado.Por último, implemente el
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))); } }
El modelo de vista nota creado en el paso anterior del tutorial, usó la navegación cuando la nota se guardó o eliminó. El modelo de vista volvió a la vista AllNotes, a la que está asociado este modelo de vista. Este código detecta si la cadena de consulta contiene la
deleted
clave osaved
. El valor de la clave es el identificador único de la nota.Si se eliminó la nota, esa nota coincide con la
AllNotes
colección por el identificador proporcionado y se quita.Hay dos razones posibles por las que se guarda una nota. La nota se acaba de crear o se cambió una nota existente. Si la nota ya está en la
AllNotes
colección, es una nota que se actualizó. En este caso, la instancia de nota de la colección solo debe actualizarse. Si falta la nota de la colección, se trata de una nueva nota y se debe agregar a la colección.
El código de la clase debe ser similar al siguiente fragmento de código:
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)));
}
}
}
Vista AllNotes
Ahora que se ha creado el modelo de vista, actualice la vista AllNotes para que apunte a las propiedades del modelo de vista. En el archivo Views\AllNotesPage.xaml , aplique los siguientes cambios:
- Agregue el
xmlns:viewModels
espacio de nombres XML que tiene como destino elNotes.ViewModels
espacio de nombres .NET. - Agregue un
BindingContext
elemento a la página. - Quite el evento del botón de la barra de
Clicked
herramientas y use laCommand
propiedad . - Cambie para
CollectionView
enlazar suItemSource
aAllNotes
. - Cambie para
CollectionView
usar el comando para reaccionar cuando cambie el elemento seleccionado.
Actualice la vista AllNotes:
En el panel Explorador de soluciones de Visual Studio, haga doble clic en Views\AllNotesPage.xaml.
Pegue el código siguiente:
<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=White, 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>
La barra de herramientas ya no usa el Clicked
evento y, en su lugar, usa un comando .
CollectionView
admite comandos con las SelectionChangedCommand
propiedades y SelectionChangedCommandParameter
. En el XAML actualizado, la SelectionChangedCommand
propiedad se enlaza al modelo de vista SelectNodeCommand
, lo que significa que el comando se invoca cuando cambia el elemento seleccionado. Cuando se invoca el comando, el valor de la SelectionChangedCommandParameter
propiedad se pasa al comando .
Examine el enlace usado para :CollectionView
<CollectionView x:Name="notesCollection"
ItemsSource="{Binding AllNotes}"
Margin="20"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectNoteCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
La SelectionChangedCommandParameter
propiedad usa Source={RelativeSource Self}
el enlace. Self
hace referencia al objeto actual, que es .CollectionView
Observe que la ruta de acceso de enlace es la SelectedItem
propiedad . Cuando se invoca el comando cambiando el elemento seleccionado, se invoca el SelectNoteCommand
comando y el elemento seleccionado se pasa al comando como parámetro.
Limpiar el código subyacente de AllNotes
Ahora que la interacción con la vista ha cambiado de controladores de eventos a comandos, abra el archivo Views\AllNotesPage.xaml.cs y reemplace todo el código por una clase que solo contenga el constructor:
En el panel Explorador de soluciones de Visual Studio, haga doble clic en Views\AllNotesPage.xaml.cs.
Sugerencia
Es posible que tengas que expandir Views\AllNotesPage.xaml para mostrar el archivo.
Reemplace el código por el siguiente fragmento de código:
namespace Notes.Views; public partial class AllNotesPage : ContentPage { public AllNotesPage() { InitializeComponent(); } }
Ejecución de la aplicación
Ahora puede ejecutar la aplicación y todo funciona. Sin embargo, hay dos problemas con el comportamiento de la aplicación:
- Si selecciona una nota, que abre el editor, presiona Guardar y, a continuación, intenta seleccionar la misma nota, no funciona.
- Cada vez que se cambia o se agrega una nota, la lista de notas no se reordena para mostrar las notas más recientes en la parte superior.
Estos dos problemas se corrigen en el siguiente paso del tutorial.
Corrección del comportamiento de la aplicación
Ahora que el código de la aplicación puede compilarse y ejecutarse, es probable que haya observado que hay dos errores con el comportamiento de la aplicación. La aplicación no permite volver a seleccionar una nota que ya está seleccionada y la lista de notas no se vuelve a ordenar después de crear o cambiar una nota.
Obtener notas en la parte superior de la lista
En primer lugar, corrija el problema de reordenación con la lista de notas. En el archivo ViewModels\NotesViewModel.cs , la AllNotes
colección contiene todas las notas que se van a presentar al usuario. Desafortunadamente, el inconveniente de usar un ObservableCollection
es que debe ordenarse manualmente. Para obtener los elementos nuevos o actualizados en la parte superior de la lista, realice los pasos siguientes:
En el panel Explorador de soluciones de Visual Studio, haga doble clic en ViewModels\NotesViewModel.cs.
En el
ApplyQueryAttributes
método , examine la lógica de la clave de cadena de consulta guardada .matchedNote
Cuando nonull
es , la nota se está actualizando. Use elAllNotes.Move
método para mover almatchedNote
índice 0, que es la parte superior de la 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); }
El
AllNotes.Move
método toma dos parámetros para mover la posición de un objeto en la colección. El primer parámetro es el índice del objeto que se va a mover y el segundo parámetro es el índice de dónde mover el objeto. ElAllNotes.IndexOf
método recupera el índice de la nota.matchedNote
Cuando esnull
, la nota es nueva y se agrega a la lista. En lugar de agregarla, que anexa la nota al final de la lista, inserte la nota en el índice 0, que es la parte superior de la lista. Cambie elAllNotes.Add
método aAllNotes.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)));
El ApplyQueryAttributes
método debe tener un aspecto similar al siguiente fragmento 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 seleccionar una nota dos veces
En la vista AllNotes, enumera CollectionView
todas las notas, pero no le permite seleccionar la misma nota dos veces. Hay dos maneras en que el elemento permanece seleccionado: cuando el usuario cambia una nota existente y cuando el usuario navega forzosamente hacia atrás. El caso en el que el usuario guarda una nota se corrige con el cambio de código en la sección anterior que usa AllNotes.Move
, por lo que no tiene que preocuparse por ese caso.
El problema que tiene que resolver ahora está relacionado con la navegación. Independientemente de cómo se navegue la vista Allnotes , el NavigatedTo
evento se genera para la página. Este evento es un lugar perfecto para anular la selección forzada del elemento seleccionado en .CollectionView
Sin embargo, con el patrón MVVM que se aplica aquí, el modelo de vista no puede desencadenar algo directamente en la vista, como borrar el elemento seleccionado después de guardar la nota. ¿Cómo consigues que esto suceda? Una buena implementación del patrón MVVM minimiza el código subyacente en la vista. Hay varias maneras diferentes de resolver este problema para admitir el patrón de separación de MVVM. Sin embargo, también es correcto colocar código en el código subyacente de la vista, especialmente cuando está directamente vinculado a la vista. MVVM tiene muchos diseños y conceptos excelentes que le ayudan a compartimentar la aplicación, lo que mejora la mantenimiento y facilita la adición de nuevas características. Sin embargo, en algunos casos, es posible que encuentre que MVVM fomenta la sobreengineering.
No sobreengineer una solución para este problema y simplemente use el NavigatedTo
evento para borrar el elemento seleccionado de CollectionView
.
En el panel Explorador de soluciones de Visual Studio, haga doble clic en Views\AllNotesPage.xaml.
En el CÓDIGO XAML para
<ContentPage>
, agregue elNavigatedTo
evento :<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:Notes.ViewModels" x:Class="Notes.Views.AllNotesPage" Title="Your Notes" NavigatedTo="ContentPage_NavigatedTo"> <ContentPage.BindingContext> <viewModels:NotesViewModel /> </ContentPage.BindingContext>
Para agregar un controlador de eventos predeterminado, haga clic con el botón derecho en el nombre del método de evento,
ContentPage_NavigatedTo
y seleccione Ir a definición. Esta acción abre Views\AllNotesPage.xaml.cs en el editor de código.Reemplace el código del controlador de eventos por el siguiente fragmento de código:
private void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e) { notesCollection.SelectedItem = null; }
En el XAML,
CollectionView
se le ha asignado el nombre denotesCollection
. Este código usa ese nombre para tener accesoCollectionView
a y se establece enSelectedItem
null
. El elemento seleccionado se borra cada vez que se navega a la página.
Ahora, ejecute la aplicación. Intente navegar a una nota, presione el botón Atrás y seleccione la misma nota una segunda vez. El comportamiento de la aplicación es fijo.
¡Enhorabuena!
La aplicación ahora usa patrones de MVVM.
Pasos siguientes
Los vínculos siguientes proporcionan más información relacionada con algunos de los conceptos que ha aprendido en este tutorial:
¿Tiene algún problema con esta sección? Si es así, envíenos sus comentarios para que podamos mejorarla.
Comentarios
Enviar y ver comentarios de