Actualización de la aplicación con conceptos MVVM
Esta serie de tutoriales está diseñada para continuar con el tutorial Creación de una aplicación de .NET MAUI, que creó una aplicación de toma de notas. En esta parte de la serie, aprenderá a:
- Implementa el patrón Modelo-Vista-Modelo de vista (MVVM).
- Usa un estilo adicional de cadena de consulta para pasar datos durante la navegación.
Te recomendamos encarecidamente que sigas primero el tutorial Creación de una aplicación de .NET MAUI, ya que el código creado en ese tutorial es la base de este tutorial. Si perdiste el código o deseas iniciarlo desde cero, descarga este proyecto.
Descripción de MVVM
La experiencia del desarrollador de .NET MAUI normalmente supone crear una interfaz de usuario en XAML y, luego, agregar código subyacente que funcione 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 coste de realizar modificaciones de la interfaz de usuario y la dificultad de realizar pruebas unitarias de este código.
El patrón Modelo-Vista-Modelo de Vista (MVVM) ayuda a separar limpiamente la lógica de presentación y de negocios 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 abordar numerosos problemas de desarrollo y facilita la prueba, el mantenimiento y la evolución de una aplicación. También puede mejorar considerablemente las oportunidades de reutilización del código y permite a los desarrolladores y a los diseñadores de la interfaz de usuario colaborar más fácilmente al desarrollar sus respectivas partes de una aplicación.
El 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.
Vista
La vista es responsable de definir la estructura, el diseño y el aspecto de lo que ve el usuario en la pantalla. Idealmente, cada vista se define en XAML, con un código subyacente 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.
Modelo de vista
El modelo de vista implementa propiedades y comandos a los que la vista puede enlazar datos, y avisa 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 modelos.
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. Es una buena idea situar esta conversión de datos en el modelo de vista 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 te permite actualizar las propiedades del modelo de vista enlazado a datos desde cualquier subproceso, y el motor de enlace de .NET MAUI lleva las actualizaciones al subproceso de la interfaz de usuario.
Modelo
Las clases de modelo son clases no visuales que encapsulan los datos de la aplicación. Por lo tanto, se puede considerar que el modelo 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 empresarial.
Actualización del modelo
En esta primera parte del tutorial, implementarás el patrón Modelo-Vista-Modelo de vista (MVVM). Para empezar, abre 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 | El modelo de About . Contiene campos de solo lectura que describen la propia aplicación, como su título y versión. |
Models/Note.cs | El modelo de Note . Representa una nota. |
Models/AllNotes.cs | El modelo de 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, la Note
. Las notas se cargan desde el dispositivo, se guardan en él y se editan a través de la interfaz de usuario de la aplicación. En realidad, los modelos About
y AllNotes
no son necesarios. Quita estos modelos del proyecto:
- Busca el panel Explorador de soluciones de Visual Studio.
- Haz clic con el botón derecho en el archivo Models\About.cs y selecciona Eliminar. Presiona Aceptar para eliminar el archivo.
- Haz clic con el botón derecho en el archivo Models\AllNotes.cs y selecciona Eliminar. Presiona Aceptar para eliminar el archivo.
El único archivo de modelo restante es Models\Note.cs.
Actualización del modelo
El modelo Note
contiene lo siguiente:
- Un identificador único, que es el nombre de archivo de la nota tal como se almacena en el dispositivo.
- El texto de la nota.
- Fecha para indicar cuándo se creó o se actualizó por última vez la nota.
En la actualidad, cargar y guardar el modelo se efectúa a través de las vistas y, en algunos casos, mediante los otros tipos de modelo que acabas de eliminar. El código que tienes para el tipo Note
debería 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 modelo Note
se expandirá para controlar la carga, el guardado y la eliminación de notas.
En el panel Explorador de soluciones de Visual Studio, haz doble clic en Models\Note.cs.
En el editor de código, agrega los dos métodos siguientes a la clase
Note
. 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 pueden ser miembros de
static
, que no requieren que se ejecute una instancia de clase.Agrega 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 del dispositivo y cargarlas en una colección.
Agregue el siguiente código 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 modelo
Note
recuperando los archivos del dispositivo que coinciden con el patrón de archivo de notas: *.notes.txt. Cada nombre de archivo se pasa al métodoLoad
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, agrega 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 código de la clase Note
debe tener este aspecto:
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 modelo Note
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, agrega 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 de MVVM.
En el panel del Explorador de soluciones de Visual Studio, haz clic con el botón secundario en el proyecto de Notas>Administrar paquetes NuGet.
Seleccione la pestaña Examinar.
Busca communitytoolkit mvvm y selecciona el paquete
CommunityToolkit.Mvvm
, que debe ser el primer resultado.Asegúrate de que se seleccionas al menos la versión 8. Este tutorial se escribió con la versión 8.0.0.
A continuación, selecciona Instalar y acepta las indicaciones que se muestran.
Ahora estás listo para empezar a actualizar el proyecto agregando modelos de vista.
Desacoplar con modelos de vista
La relación de modelo de vista a vista se basa en gran medida en el sistema de enlace que proporciona .NET Multi-platform App UI (.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 la proporciona actualmente el código subyacente de la vista y está directamente vinculada a la vista. Por ejemplo, cuando un usuario está editando una nota y presionas el botón Guardar, se genera el evento Clicked
del botón. Después, el código subyacente del controlador de eventos guarda el texto de la nota en un archivo y va a la pantalla anterior.
Tener lógica de aplicación en el código subyacente de una vista puede convertirse en un problema cuando la vista cambia. Por ejemplo, si el botón se reemplaza por un control de entrada diferente o se cambia el nombre de un control, es posible que los controladores de eventos no sean 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 botón Save
guarda la nota y después 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 carguen o guarden los datos. El modelo de vista es el pegamento 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.
- Busca el panel Explorador de soluciones de Visual Studio.
- Haz clic con el botón derecho en el proyecto SignInMaui y selecciona Agregar>Nueva carpeta. Asigna a la nueva carpeta el nombre ViewModels.
- Haz clic con el botón derecho en la carpeta ViewModels>Agregar>Clase) y asígnale el nombre AboutViewModel.cs.
- Repite el paso anterior y crea dos modelos de vista más:
- NoteViewModel.cs
- NotesViewModel.cs
La estructura del proyecto debe parecerse a la siguiente imagen:
Modelo de vista Acerca de y 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 que cambiar ningún dato, como con un control de entrada de texto o la selección de elementos de una lista, es un buen candidato para demostrar la adición de un viewmodel. En el caso del modelo de vista Acerca de, no hay ningún modelo de respaldo.
Creación del modelo de vista Acerca de:
En el panel Explorador de soluciones de Visual Studio, haz 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 Acerca de que has eliminado anteriormente. Pero este modelo de vista contiene un nuevo concepto, la propiedad del comando 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, ShowMoreInfoCommand
apunta al método ShowMoreInfo
, que abre el explorador web en una página específica. Aprenderás más sobre el sistema de comandos en la siguiente sección.
Vista Acerca de
La vista Acerca de debe cambiarse un poco para enlazarse al modelo de vista que se creó en la sección anterior. En el archivo Views\AboutPage.xaml, aplica los cambios siguientes:
- Actualiza el espacio de nombres XML
xmlns:models
axmlns:viewModels
y apunta al espacio de nombres .NETNotes.ViewModels
. - Cambia la propiedad
ContentPage.BindingContext
a una nueva instancia del modelo de vistaAbout
. - Quita el controlador de eventos
Clicked
del botón y usa la propiedadCommand
.
Actualiza la vista Acerca de:
En el panel Explorador de soluciones de Visual Studio, haz 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.
Observa que el botón utiliza la propiedad Command
. Muchos controles tienen una propiedad Command
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 lo presiona, de forma similar a cómo se invoca el controlador de eventos Clicked
, excepto que se puede enlazar Command
a una propiedad en el modelo de vista.
En esta vista, cuando el usuario presiona el botón, el Command
se invoca. El Command
se enlaza a la propiedad ShowMoreInfoCommand
en el modelo de vista y, cuando se invoca, ejecuta el código en el método ShowMoreInfo
, que abre el explorador web en una página específica.
Limpieza del código subyacente de Acerca de
El botón ShowMoreInfo
no usa el controlador de eventos, por lo que el código LearnMore_Clicked
debe eliminarse del archivo Views\AboutPage.xaml.cs. Elimina ese código; la clase solo debe contener el constructor:
En el panel Explorador de soluciones de Visual Studio, haz doble clic en Views\AboutPage.xaml.cs.
Sugerencia
Es posible que tengas que expandir Views\AboutPage.xaml para mostrar el archivo.
Reemplaza todo el código por el fragmento siguiente:
namespace Notes.Views; public partial class AboutPage : ContentPage { public AboutPage() { InitializeComponent(); } }
Crea el 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 Nota
En función de lo que requiere la vista Nota, el modelo de vista Nota debe proporcionar los siguientes elementos:
- El texto de la nota.
- Fecha y hora en que se creó la nota o se actualizó por última vez.
- Un comando que guarda la nota.
- Un comando que elimina la nota.
Crea el modelo de vista Nota:
En el panel Explorador de soluciones de Visual Studio, haz doble clic en ViewModels\NoteViewModel.cs.
Reemplaza todo el código de este archivo por el siguiente fragmento:
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ás propiedades y comandos para admitir la vistaNote
. Observa que el espacio de nombresCommunityToolkit.Mvvm.ComponentModel
se importa. Este espacio de nombres proporciona elObservableObject
utilizado como clase base. Aprenderás más sobreObservableObject
en la etapa siguiente. El espacio de nombresCommunityToolkit.Mvvm.Input
también se importa. Este espacio de nombres suministra algunos tipos de comandos que invocan métodos de forma asincrónica.El modelo
Models.Note
se almacena como un campo privado. Las propiedades y los métodos de esta clase usarán este campo.Agregue las propiedades siguientes 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 propiedades
Date
yIdentifier
son propiedades simples que solo 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 propiedad
Text
comprueba primero si el valor que se va a establecer es uno diferente. Si el valor es diferente, se pasa a la propiedad del modelo y se llama al métodoOnPropertyChanged
.La clase base
OnPropertyChanged
proporciona el métodoObservableObject
. Este método usa el nombre del código que realiza la llamada, en este caso, el nombre de propiedad de Texto y genera el eventoObservableObject.PropertyChanged
. 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. Para el modelo de vista Nota, cuando cambia la propiedadText
, se genera el evento y se notifica este cambio a cualquier elemento de interfaz de usuario enlazado a la propiedadText
.Agrega 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; }
Agrega 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 usa la instancia de modelo especificada.
Los constructores también configuran los comandos para el modelo de vista. Agrega después el código para estos comandos.
Agrega los métodos
Save
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 vaya 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.Después agrega el método
ApplyQueryAttributes
a la clase, que cumple los requisitos de la interfaz IQueryAttributable:void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { if (query.ContainsKey("load")) { _note = Models.Note.Load(query["load"].ToString()); RefreshProperties(); } }
Cuando una página o su contexto de enlace implementa esta interfaz, los parámetros de cadena de consulta usados en la navegación se pasan al método
ApplyQueryAttributes
. 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 clave
load
se proporcionó en el diccionarioquery
. 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, agrega 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 método
Reload
es auxiliar que actualiza el objeto del modelo de respaldo y lo vuelve a cargar desde el almacenamiento del dispositivo.El método
RefreshProperties
es otro auxiliar para asegurarse de que los suscriptores enlazados a este objeto reciben una notificación de que las propiedadesText
yDate
han cambiado. Dado que el modelo subyacente (el campo_note
) se cambia cuando se carga la nota durante la navegación, las propiedadesText
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 porque no se llamaOnPropertyChanged
para cada propiedad.RefreshProperties
garantiza que se actualicen los enlaces a estas propiedades.
El código de esta clase debería tener el aspecto de este fragmento:
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 nota
Ahora que se ha creado el modelo de vista, actualiza la vista Nota. En el archivo Views\NotesPage.xaml, aplica los cambios siguientes:
- Agrega el espacio de nombres XML
xmlns:viewModels
que tiene como destino el espacio de nombres .NETNotes.ViewModels
. - Agrega
BindingContext
a la página. - Quita los controladores de eventos
Clicked
de eliminar y guardar y reemplázalos por comandos.
Actualiza la vista Nota:
- En el panel Explorador de soluciones de Visual Studio, haz 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 proporcionaba el código subyacente de la propia página. Al establecerse el contexto de enlace directamente en el XAML ocurren 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 no recuerdas mal, 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 empieces a escribir la sintaxis
{Binding
. La sintaxis también se valida y te avisa de un valor no válido. Intenta cambiar la sintaxis de enlace deSaveCommand
aSave123Command
. Si mantienes el cursor del ratón sobre el texto, observarás que se muestra una información sobre herramientas que informa de que no se encuentra el Save123Command. Esta notificación no se considera un error porque los enlaces son dinámicos, es realmente una advertencia pequeña que puede ayudarte a notar al escribir la propiedad incorrecta.Si ha cambiados el SaveCommand a un valor diferente, restáuralo 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, abre el archivo Views\NotePage.xaml.cs y reemplaza todo el código por una clase que solo contenga el constructor:
En el panel Explorador de soluciones de Visual Studio, haz doble clic en View\NotePage.xaml.cs.
Sugerencia
Es posible que tengas que expandir Views\NotePage.xaml para mostrar el archivo.
Reemplaza todo el código por el fragmento siguiente:
namespace Notes.Views; public partial class NotePage : ContentPage { public NotePage() { InitializeComponent(); } }
Creación del modelo de vista Notas
El par final del Modelo vista-vista es el Modelo de vista Notas y Vista todas las notas. Pero la vista se enlaza directamente al modelo, que se eliminó al principio de este tutorial. El objetivo de actualizar la Vista todas las notas 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 notas
En función de lo que va a mostrar la Vista todas las notas y qué interacciones hará el usuario, el Modelo de vista notas debe proporcionar los siguientes elementos:
- Una colección de notas.
- Un comando para controlar la navegación a una nota.
- Un comando para crear una nueva nota.
- Actualiza la lista de notas cuando se crea, se elimina o se cambia.
Crea el Modelo de vista notas:
En el panel Explorador de soluciones de Visual Studio, haz doble clic en ViewModels\NotesViewModel.cs.
Reemplaza el código de este archivo por el siguiente.
using CommunityToolkit.Mvvm.Input; using System.Collections.ObjectModel; using System.Windows.Input; namespace Notes.ViewModels; internal class NotesViewModel: IQueryAttributable { }
Este código es
NotesViewModel
en blanco donde agregarás propiedades y comandos para admitir la vistaAllNotes
.Agrega las propiedades siguientes al código de clase
NotesViewModel
:public ObservableCollection<ViewModels.NoteViewModel> AllNotes { get; } public ICommand NewCommand { get; } public ICommand SelectNoteCommand { get; }
La propiedad
AllNotes
es unObservableCollection
que almacena todas las notas cargadas desde el dispositivo. La vista usará los dos comandos para desencadenar las acciones de creación de una nota o seleccionar una nota existente.Agrega 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); }
Observa que la colección
AllNotes
usa el métodoModels.Note.LoadAll
para rellenar la colección observable con notas. El métodoLoadAll
devuelve las notas como el tipoModels.Note
, pero la colección observable es una colección de tiposViewModels.NoteViewModel
. El código usa la extensión LinqSelect
para crear instancias de modelo de vista a partir de los modelos de nota devueltos desdeLoadAll
.Crea los métodos destinados por 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}"); }
Observa que el método
NewNoteAsync
no toma un parámetro mientrasSelectNoteAsync
sí lo hace. Opcionalmente, los comandos pueden tener un único parámetro que se proporciona cuando se invoca el comando. Para el método , el parámetroSelectNoteAsync
representa la nota que se va a seleccionar.Por último, implementa el método
IQueryAttributable.ApplyQueryAttributes
:void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { if (query.ContainsKey("deleted")) { string noteId = query["deleted"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note exists, delete it if (matchedNote != null) AllNotes.Remove(matchedNote); } else if (query.ContainsKey("saved")) { string noteId = query["saved"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note is found, update it if (matchedNote != null) matchedNote.Reload(); // If note isn't found, it's new; add it. else AllNotes.Add(new NoteViewModel(Note.Load(noteId))); } }
El modelo de vista Nota creado en el paso anterior del tutorial, usó la navegación cuando se guardó o eliminó la nota. 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 clave
deleted
osaved
. El valor de la clave es el identificador único de la nota.Si la nota se eliminó, esa nota coincide con la colección
AllNotes
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 colección
AllNotes
, se trata de 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 esta clase debería tener el aspecto de este fragmento:
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, actualiza la vista AllNotes para que apunte a las propiedades del modelo de vista. En el archivo Views\AllNotesPage.xaml, aplica los cambios siguientes:
- Agrega el espacio de nombres XML
xmlns:viewModels
que tiene como destino el espacio de nombres .NETNotes.ViewModels
. - Agrega
BindingContext
a la página. - Quita el evento del botón de la barra de herramientas
Clicked
y usa la propiedadCommand
. - Cambia
CollectionView
para enlazar tuItemSource
aAllNotes
. - Cambia el
CollectionView
para usar los comandos para reaccionar cuando cambie el elemento seleccionado.
Actualiza la vista AllNotes:
En el panel Explorador de soluciones de Visual Studio, haz clic con el botón derecho 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=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>
La barra de herramientas ya no usa el Clicked
evento y, en su lugar, usa un comando.
CollectionView
admite comandos con las propiedades SelectionChangedCommand
y SelectionChangedCommandParameter
. En el XAML actualizado, la propiedad SelectionChangedCommand
está enlazada al modelo de vista SelectNoteCommand
, lo que significa que el comando se invoca cuando cambia el elemento seleccionado. Cuando se invoca el comando, el valor de la propiedad SelectionChangedCommandParameter
se pasa al comando.
Examina 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 propiedad SelectionChangedCommandParameter
utiliza un enlace Source={RelativeSource Self}
. Self
hace referencia al objeto actual, que es CollectionView
. Observa que la ruta de acceso al enlace es la propiedad SelectedItem
. Cuando se invoca el comando cambiando el elemento seleccionado, se invoca el comando SelectNoteCommand
y el elemento seleccionado se pasa al comando como parámetro.
Limpieza del código subyacente de AllNotes
Ahora que la interacción con la vista ha cambiado de controladores de eventos a comandos, abre el archivo Views\AllNotesPage.xaml.cs y reemplaza todo el código por una clase que solo contenga el constructor:
En el panel Explorador de soluciones de Visual Studio, haz doble clic en Views\AllNotesPage.xaml.cs.
Sugerencia
Es posible que tengas que expandir Views\AllNotesPage.xaml para mostrar el archivo.
Reemplaza todo el código por el fragmento siguiente:
namespace Notes.Views; public partial class AllNotesPage : ContentPage { public AllNotesPage() { InitializeComponent(); } }
Ejecución de la aplicación
Ahora puedes ejecutar la aplicación y todo funciona. Sin embargo, hay dos problemas con el comportamiento de la aplicación:
- Si seleccionas una nota, que abre el editor, presionas Guardar y después intentas 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 hayas 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 reordena después de crear o cambiar una nota.
Poner notas en la parte superior de la lista
En primer lugar, corrige el problema de reordenación con la lista de notas. En el archivo ViewModels\NotesViewModel.cs, la colección AllNotes
contiene todas las notas que se van a presentar al usuario. Desafortunadamente, la desventaja de usar ObservableCollection
es que debe ordenarse manualmente. Para obtener los elementos nuevos o actualizados en la parte superior de la lista, realiza los pasos siguientes:
En el panel Explorador de soluciones de Visual Studio, haz doble clic en ViewModels\NotesViewModel.cs.
En el método
ApplyQueryAttributes
, examina la lógica de la clave de cadena de consulta guardada.Cuando
matchedNote
no esnull
, la nota se está actualizando. Usa el métodoAllNotes.Move
para movermatchedNote
al í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 método
AllNotes.Move
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 se va a mover el objeto. El métodoAllNotes.IndexOf
recupera el índice de la nota.Cuando
matchedNote
esnull
, la nota es nueva y se agrega a la lista. En lugar de agregarla, que anexa la nota al final de la lista, inserta la nota en el índice 0, que es la parte superior de la lista. Cambia el métodoAllNotes.Add
para usarAllNotes.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 método ApplyQueryAttributes
debe ser similar al fragmento de código siguiente:
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, CollectionView
enumera 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 forzadamente hacia atrás. El caso en el que el usuario guarda una nota se ha corregido con el cambio de código en la sección anterior que usa AllNotes.Move
, por lo que no tienes que preocuparte por ese caso.
El problema que tienes que resolver ahora está relacionado con la navegación. Independientemente de cómo se navegue la Allnotes view, el evento NavigatedTo
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. Entonces, ¿cómo consigues que eso 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 te ayudan a compartimentar la aplicación, mejorar la capacidad de mantenimiento y facilitar la adición de nuevas características. Sin embargo, en algunos casos, es posible que encuentres que MVVM fomenta la sobreingeniería.
No realices sobreingeniería de una solución para este problema; solo usa el evento NavigatedTo
para borrar el elemento seleccionado de CollectionView
.
En el panel Explorador de soluciones de Visual Studio, haz clic con el botón derecho en Views\AllNotesPage.xaml.
En el XAML para
<ContentPage>
, agrega el eventoNavigatedTo
:<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:Notes.ViewModels" x:Class="Notes.Views.AllNotesPage" Title="Your Notes" NavigatedTo="ContentPage_NavigatedTo"> <ContentPage.BindingContext> <viewModels:NotesViewModel /> </ContentPage.BindingContext>
Para agregar un controlador de eventos predeterminado, haz clic con el botón derecho en el nombre del método de evento,
ContentPage_NavigatedTo
, y selecciona Ir a definición. Esta acción abre Views\AllNotesPage.xaml.cs en el editor de código.Reemplaza 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, a
CollectionView
se le dio el nombre denotesCollection
. Este código usa ese nombre para tener acceso aCollectionView
y establecer ennull
SelectedItem
. El elemento seleccionado se borra cada vez que se navega a la página.
Ahora, ejecuta la aplicación. Intenta navegar a una nota, presiona el botón de atrás y selecciona la misma nota una segunda vez. El comportamiento de la aplicación es fijo.
Explorar el código de este tutorial.. Si desea descargar una copia del proyecto completado con el que comparar el código, descargue este proyecto.
¡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 has aprendido en este tutorial:
¿Tiene algún problema con esta sección? Si es así, envíenos sus comentarios para que podamos mejorarla.