Uppgradera din app med MVVM-begrepp

Den här självstudien är utformad för att fortsätta på självstudien Skapa en .NET MAUI-app, vilken skapade en anteckningsapp. I den här självstudien får du lära dig att:

  • Implementera MVVM-mönstret (model-view-viewmodel).
  • Använd ytterligare ett frågeformat för att skicka data under navigeringen.

Vi rekommenderar att du först följer självstudien Skapa en .NET MAUI-app eftersom koden som skapades i den självstudien är grunden för den här självstudien. Om du har förlorat koden eller vill börja om från början laddar du ned det här projektet.

Förstå MVVM

.NET MAUI-utvecklarupplevelsen innebär vanligtvis att skapa ett användargränssnitt i XAML och sedan lägga till kod bakom som fungerar i användargränssnittet. Komplexa underhållsproblem kan uppstå när appar ändras och växer i storlek och omfattning. Dessa problem omfattar den snäva kopplingen mellan användargränssnittskontrollerna och affärslogik, vilket ökar kostnaden för att göra ändringar i användargränssnittet och svårigheten med enhetstestning av sådan kod.

MVVM-mönstret (model-view-viewmodel) hjälper till att tydligt separera en applikations affärslogik och presentationslogik från dess användargränssnitt (UI). Genom att upprätthålla en ren separation mellan applogik och användargränssnittet kan du lösa många utvecklingsproblem och göra en app enklare att testa, underhålla och utveckla. Det kan också avsevärt förbättra möjligheterna till återanvändning av kod och göra det möjligt för utvecklare och användargränssnittsdesigners att samarbeta enklare när de utvecklar sina respektive delar av en app.

Mönstret

Det finns tre kärnkomponenter i MVVM-mönstret: modellen, vyn och vymodellen. Var och en har ett distinkt syfte. Följande diagram visar relationerna mellan de tre komponenterna.

Ett diagram som visar delarna i ett MVVM-modellerat program

Förutom att förstå ansvarsområden för varje komponent är det också viktigt att förstå hur de interagerar. På hög nivå känner vyn "till" vymodellen och vymodellen "känner till" modellen, men modellen är omedveten om vymodellen och vymodellen känner inte till vyn. Därför isolerar vymodellen vyn från modellen och gör att modellen kan utvecklas oberoende av vyn.

Nyckeln till att använda MVVM ligger i att förstå hur appkod ska räknas in i rätt klasser och hur klasserna interagerar.

Utsikt

Vyn ansvarar för att definiera strukturen, layouten och utseendet på det som användaren ser på skärmen. Helst definieras varje vy i XAML, med en begränsad bakomliggande kod som inte innehåller affärslogik. Men i vissa fall kan koden bakom innehålla UI-logik som implementerar visuellt beteende som är svårt att uttrycka i XAML, till exempel animeringar.

ViewModel

Vymodellen implementerar egenskaper och kommandon som vyn kan binda data till och meddelar vyn om eventuella tillståndsändringar genom ändringsmeddelandehändelser. De egenskaper och kommandon som visningsmodellen tillhandahåller definierar de funktioner som ska erbjudas av användargränssnittet, men vyn avgör hur den funktionen ska visas.

Vymodellen ansvarar också för att samordna vyns interaktioner med alla modellklasser som krävs. Det finns vanligtvis en en-till-många-relation mellan vymodellen och modellklasserna.

Varje vymodell innehåller data från en modell i ett formulär som vyn enkelt kan använda. För att åstadkomma detta utför vymodellen ibland datakonvertering. Det är en bra idé att placera datakonverteringen i vymodellen eftersom den innehåller egenskaper som vyn kan binda till. Vymodellen kan till exempel kombinera värdena för två egenskaper så att den blir enklare att visa i vyn.

Viktigt!

.NET MAUI hanterar bindningsuppdateringar till UI-tråden. När du använder MVVM kan du uppdatera databundna viewmodel-egenskaper från valfri tråd, med .NET MAUI:s bindningsmotor som ger uppdateringarna till användargränssnittstråden.

Modell

Modellklasser är icke-visuella klasser som kapslar in appens data. Därför kan modellen anses representera appens domänmodell, som vanligtvis innehåller en datamodell tillsammans med affärs- och valideringslogik.

Uppdatera modellen

I den första delen av handledningen implementerar du MVVM-mönstret (model-view-viewmodel). Starta genom att öppna lösningen Notes.sln i Visual Studio.

Rensa modellen

I den föregående tutorialen fungerade modelltyperna både som modell (data) och som en vy-modell (dataförberedelse), och mappades direkt till en vy. I följande tabell beskrivs modellen:

Kodfil Beskrivning
Models/About.cs Modellen About . Innehåller skrivskyddade fält som beskriver själva appen, till exempel appens titel och version.
Models/Note.cs Modellen Note . Representerar en anteckning.
Modeller/AllNotes.cs Modellen AllNotes . Läser in alla anteckningar på enheten till en samling.

När du tänker på själva appen, använder appen endast en datadel, Note. Anteckningar läses in från enheten, sparas på enheten och redigeras via appens användargränssnitt. Det finns verkligen inget behov av About modellerna och AllNotes . Ta bort dessa modeller från projektet:

  1. Leta upp solution explorer-fönstret i Visual Studio.
  2. Högerklicka på filen Models\About.cs och välj Ta bort. Tryck på OK för att ta bort filen.
  3. Högerklicka på filen Models\AllNotes.cs och välj Ta bort. Tryck på OK för att ta bort filen.

Den enda modellfil som återstår är filen Models\Note.cs .

Uppdatera modellen

Modellen Note innehåller:

  • En unik identifierare, som är filnamnet på anteckningen som lagras på enheten.
  • Texten i anteckningen.
  • Ett datum som anger när anteckningen skapades eller senast uppdaterades.

För närvarande utförs inläsning och sparande av modellen via vyerna, och i vissa fall av andra modelltyper som du just tagit bort. Koden som du har för typen Note bör vara följande:

namespace Notes.Models;

internal class Note
{
    public string Filename { get; set; }
    public string Text { get; set; }
    public DateTime Date { get; set; }
}

Modellen Note kommer att utökas för att hantera inläsning, sparande och borttagning av anteckningar.

  1. Dubbelklicka på Modeller\Note.cs i fönstret Solution Explorer i Visual Studio.

  2. Lägg till följande två metoder i Note klassen i kodredigeraren. Dessa metoder är instansbaserade och hanterar sparande eller borttagning av den aktuella anteckningen till eller från enheten:

    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));
    
  3. Appen måste läsa in anteckningar på två sätt, läsa in en enskild anteckning från en fil och läsa in alla anteckningar på enheten. Koden för att hantera inläsning kan vara static medlemmar och kräver inte att en klassinstans körs.

    Lägg till följande kod i klassen för att läsa in en anteckning efter filnamn:

    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)
            };
    }
    

    Den här koden tar filnamnet som en parameter, skapar sökvägen till den plats där anteckningarna lagras på enheten och försöker läsa in filen om den finns.

  4. Det andra sättet att läsa in anteckningar är att räkna upp alla anteckningar på enheten och läsa in dem i en samling.

    Lägg till följande kod i klassen:

    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);
    }
    

    Den här koden returnerar en uppräkningsbar samling Note modelltyper genom att hämta filerna på enheten som matchar anteckningsfilmönstret: *.notes.txt. Varje filnamn skickas till Load metoden och läser in en enskild anteckning. Slutligen sorteras insamlingen av anteckningar efter datumet för varje anteckning och returneras till anroparen.

  5. Slutligen lägger du till en konstruktor i klassen som anger standardvärdena för egenskaperna, inklusive ett slumpmässigt filnamn:

    public Note()
    {
        Filename = $"{Path.GetRandomFileName()}.notes.txt";
        Date = DateTime.Now;
        Text = "";
    }
    

Klasskoden Note bör se ut så här:

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);
    }
}

Nu när Note modellen är klar kan vymodellerna skapas.

Skapa vyrutan Om

Innan du lägger till visningsmodeller i projektet lägger du till en referens till MVVM Community Toolkit. Det här biblioteket är tillgängligt på NuGet och innehåller typer och system som hjälper dig att implementera MVVM-mönstret.

  1. I fönstret Solution Explorer i Visual Studio högerklickar du på Notes-projektet>Hantera NuGet-paket.

  2. Välj fliken Bläddra.

  3. Sök efter communitytoolkit mvvm och välj CommunityToolkit.Mvvm paketet, som ska vara det första resultatet.

  4. Kontrollera att minst version 8 är markerad. Den här handledningen skrevs med version 8.0.0.

  5. Välj sedan Installera och acceptera alla frågor som visas.

    Söker efter CommunityToolkit.Mvvm-paketet i NuGet.

Nu är du redo att börja uppdatera projektet genom att lägga till vymodeller.

Frikoppla med visningsmodeller

View-to-viewmodel-relationen är starkt beroende av bindningssystemet som tillhandahålls av .NET Multi-platform App UI (.NET MAUI). Appen använder redan bindning i vyerna för att visa en lista med anteckningar och för att presentera texten och datumet för en enskild anteckning. Applogik tillhandahålls för närvarande av vyns kod bakom och är direkt kopplad till vyn. När en användare till exempel redigerar en anteckning och trycker på knappen Spara aktiveras Clicked händelsen för knappen. Sedan sparar koden bakom för händelsehanteraren anteckningstexten i en fil och navigerar tillbaka till föregående skärm.

Att ha applogik i koden bakom en vy kan bli ett problem när vyn ändras. Om knappen till exempel ersätts med en annan indatakontroll, eller om namnet på en kontroll ändras, kan händelsehanterare bli ogiltiga. Oavsett hur vyn är utformad är syftet med vyn att anropa någon form av applogik och att presentera information för användaren. För den här appen Save sparar knappen anteckningen och navigerar sedan tillbaka till föregående skärm.

Viewmodel ger appen en specifik plats för att placera applogik oavsett hur användargränssnittet är utformat eller hur data läses in eller sparas. Viewmodel är limmet som representerar och interagerar med datamodellen för vyns räkning.

Vymodellerna lagras i en ViewModels-mapp .

  1. Leta upp solution explorer-fönstret i Visual Studio.
  2. Högerklicka på projektet Anteckningar och välj Lägg till>ny mapp. Ge mappen namnet ViewModels.
  3. Högerklicka på mappen >Lägg till>klass och ge den namnet AboutViewModel.cs.
  4. Upprepa föregående steg och skapa ytterligare två vymodeller:
    • NoteViewModel.cs
    • NotesViewModel.cs

Projektstrukturen bör se ut så här:

Solution Explorer som visar MVVM-mappar.

Om viewmodel- och About-vyn

I vyn Om visas vissa data på skärmen och du kan också navigera till en webbplats med mer information. Eftersom den här vyn inte har några data att ändra, till exempel med en textinmatningskontroll eller val av objekt från en lista, är det en bra kandidat att visa hur du lägger till en viewmodel. För About viewmodel finns det ingen stödmodell.

Skapa viewmodel för Om:

  1. Dubbelklicka på ViewModels\AboutViewModel.cs i fönstret Solution Explorer i Visual Studio.

  2. Klistra in följande kod:

    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);
    }
    

Det tidigare kodfragmentet innehåller vissa egenskaper som representerar information om appen, till exempel namn och version. Det här kodfragmentet är exakt samma som om-modellen som du tog bort tidigare. Den här viewmodel innehåller dock ett nytt koncept, kommandoegenskapen ShowMoreInfoCommand .

Kommandon är bindbara åtgärder som anropar kod och är en bra plats för att placera applogik. I det här exemplet pekar ShowMoreInfoCommandShowMoreInfo metoden, som öppnar webbläsaren till en specifik sida. Du får lära dig mer om kommandosystemet i nästa avsnitt.

Om vy

Om-vyn måste ändras något för att koppla den till den vymodell som skapades i föregående avsnitt. Använd följande ändringar i filen Views\AboutPage.xaml :

  • xmlns:models Uppdatera XML-namnområdet till xmlns:viewModels och rikta in dig på Notes.ViewModels .NET-namnområdet.
  • Ändra egenskapen ContentPage.BindingContext till en ny instans av About-viewmodell.
  • Ta bort knappens Clicked händelsehanterare och använd egenskapen Command .

Uppdatera vyn Om:

  1. Dubbelklicka på Vyer\AboutPage.xaml i fönstret Solution Explorer i Visual Studio.

  2. Klistra in följande kod:

    <?xml version="1.0" encoding="utf-8" ?>
    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:viewModels="clr-namespace:Notes.ViewModels"
                 x:Class="Notes.Views.AboutPage"
                 x:DataType="viewModels:AboutViewModel">
        <ContentPage.BindingContext>
            <viewModels:AboutViewModel />
        </ContentPage.BindingContext>
        <VerticalStackLayout Spacing="10" Margin="10">
            <HorizontalStackLayout Spacing="10">
                <Image Source="dotnet_bot.png"
                       SemanticProperties.Description="The dot net bot waving hello!"
                       HeightRequest="64" />
                <Label FontSize="22" FontAttributes="Bold" Text="{Binding Title}" VerticalOptions="End" />
                <Label FontSize="22" Text="{Binding Version}" VerticalOptions="End" />
            </HorizontalStackLayout>
    
            <Label Text="{Binding Message}" />
            <Button Text="Learn more..." Command="{Binding ShowMoreInfoCommand}" />
        </VerticalStackLayout>
    
    </ContentPage>
    

    Det tidigare kodfragmentet markerar de rader som har ändrats i den här versionen av vyn.

Observera att knappen använder Command egenskapen . Många kontroller har en Command egenskap som anropas när användaren interagerar med kontrollen. När det används med en knapp anropas kommandot när en användare trycker på knappen, ungefär som händelsehanteraren Clicked anropas, förutom att du kan binda Command till en egenskap i viewmodel.

När användaren trycker på knappen i den här vyn, anropas Command. Command är bunden till ShowMoreInfoCommand-egenskapen i en viewmodel, och när den ShowMoreInfo anropas, körs koden i metoden, vilket öppnar webbläsaren till en specifik sida.

Rensa om kod bakom

Knappen ShowMoreInfo använder inte händelsehanteraren, så LearnMore_Clicked koden bör tas bort från filen Views\AboutPage.xaml.cs . Ta bort koden, klassen ska bara innehålla konstruktorn:

  1. Dubbelklicka på Vyer\AboutPage.xaml.cs i fönstret Solution Explorer i Visual Studio.

    Tips/Råd

    Du kan behöva expandera Views\AboutPage.xaml för att visa filen.

  2. Ersätt koden med följande kodfragment:

    namespace Notes.Views;
    
    public partial class AboutPage : ContentPage
    {
        public AboutPage()
        {
            InitializeComponent();
        }
    }
    

Skapa anteckningsviewmodelen

Målet med att uppdatera anteckningsvyn är att flytta så mycket funktionalitet som möjligt från XAML-koden bakom och placera den i anteckningsvyn.

Anteckningsvymodell

Baserat på vad note-vyn kräver, måste note-vymodellen tillhandahålla följande objekt:

  • Texten i anteckningen.
  • Datum/tid då anteckningen skapades eller senast uppdaterades.
  • Ett kommando som sparar anteckningen.
  • Ett kommando som tar bort anteckningen.

Skapa Note viewmodel:

  1. Dubbelklicka på ViewModels\NoteViewModel.cs i fönstret Solution Explorer i Visual Studio.

  2. Ersätt koden i den här filen med följande kodfragment:

    using CommunityToolkit.Mvvm.Input;
    using CommunityToolkit.Mvvm.ComponentModel;
    using System.Windows.Input;
    
    namespace Notes.ViewModels;
    
    internal class NoteViewModel : ObservableObject, IQueryAttributable
    {
        private Models.Note _note;
    
    }
    

    Den här koden är den tomma Note vymodellen där du lägger till egenskaper och kommandon som stödjer vyn Note. Observera att CommunityToolkit.Mvvm.ComponentModel namnområdet importeras. Det här namnområdet innehåller den ObservableObject som används som basklass. Du lär dig mer om ObservableObject i nästa steg. Namnområdet CommunityToolkit.Mvvm.Input importeras också. Det här namnområdet innehåller vissa kommandotyper som anropar metoder asynkront.

    Modellen Models.Note lagras som ett privat fält. Egenskaperna och metoderna för den här klassen använder det här fältet.

  3. Lägg till följande egenskaper i klassen:

    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;
    

    Egenskaperna Date och Identifier är enkla egenskaper som bara hämtar motsvarande värden från modellen.

    Tips/Råd

    För egenskaper skapar syntaxen => en get-only-egenskap där -instruktionen till höger om => måste utvärderas till ett värde som ska returneras.

    Egenskapen Text kontrollerar först om värdet som anges är ett annat värde. Om värdet är annorlunda skickas det värdet vidare till modellens egenskap och OnPropertyChanged metoden anropas.

    Metoden OnPropertyChanged tillhandahålls av basklassen ObservableObject . Den här metoden använder namnet på den anropande koden, i det här fallet egenskapsnamnet text och genererar ObservableObject.PropertyChanged händelsen. Den här händelsen tillhandahåller namnet på egenskapen till alla händelseprenumeranter. Bindningssystemet som tillhandahålls av .NET MAUI identifierar den här händelsen och uppdaterar eventuella relaterade bindningar i användargränssnittet. När egenskapen ändras för note viewmodelText aktiveras händelsen och alla gränssnittselement som är bundna till Text egenskapen meddelas om att egenskapen har ändrats.

  4. Lägg till följande kommandoegenskaper i klassen, som är de kommandon som vyn kan binda till:

    public ICommand SaveCommand { get; private set; }
    public ICommand DeleteCommand { get; private set; }
    
  5. Lägg till följande konstruktorer i klassen:

    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);
    }
    

    Dessa två konstruktorer används antingen för att skapa viewmodel med en ny bakgrundsmodell, som är en tom anteckning, eller för att skapa en vymodell som använder den angivna modellinstansen.

    Konstruktorerna konfigurerar också kommandona för viewmodel. Lägg sedan till koden för dessa kommandon.

  6. Lägg till Save och Delete metoderna:

    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}");
    }
    

    Dessa metoder anropas av associerade kommandon. De utför relaterade åtgärder för modellen och gör att appen navigerar till föregående sida. En frågesträngsparameter läggs till i .. navigeringssökvägen, som anger vilken åtgärd som vidtogs och anteckningens unika identifierare.

  7. Lägg sedan till ApplyQueryAttributes metoden i klassen, som uppfyller kraven i IQueryAttributable gränssnittet:

    void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
    {
        if (query.ContainsKey("load"))
        {
            _note = Models.Note.Load(query["load"].ToString());
            RefreshProperties();
        }
    }
    

    När en sida, eller bindningskontexten för en sida, implementerar det här gränssnittet skickas de frågesträngsparametrar som används i navigeringen ApplyQueryAttributes till metoden. Den här viewmodel används som bindningskontext för anteckningsvyn. När anteckningsvyn navigeras till, får vyns bindningskontext (vymodellen) de frågesträngsparametrar som användes under navigeringen.

    Den här koden kontrollerar om load nyckeln angavs i query ordlistan. Om den här nyckeln hittas ska värdet vara identifieraren (filnamnet) för anteckningen som ska läsas in. Den anteckningen läses in och anges som det underliggande modellobjektet för den här viewmodel-instansen.

  8. Lägg slutligen till följande två hjälpmetoder i klassen:

    public void Reload()
    {
        _note = Models.Note.Load(_note.Filename);
        RefreshProperties();
    }
    
    private void RefreshProperties()
    {
        OnPropertyChanged(nameof(Text));
        OnPropertyChanged(nameof(Date));
    }
    

    Metoden Reload är en hjälpmetod som uppdaterar säkerhetskopieringsmodellobjektet och läser in det från enhetslagringen igen

    Metoden RefreshProperties är en annan hjälpmetod för att säkerställa att alla prenumeranter som är bundna till det här objektet meddelas om att Text egenskaperna och Date har ändrats. Eftersom den underliggande modellen (fältet _note) ändras när anteckningen läses in under navigeringen, ställs inte egenskaperna Text och Date in på nya värden. Eftersom dessa egenskaper inte anges direkt meddelas inte bindningar som är kopplade till dessa egenskaper eftersom OnPropertyChanged de inte anropas för varje egenskap. RefreshProperties säkerställer att bindningar till dessa egenskaper uppdateras.

Koden för klassen bör se ut som följande kodfragment:

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));
    }
}

Anteckningsvy

Nu när viewmodel har skapats uppdaterar du anteckningsvyn. Använd följande ändringar i filen Views\NotePage.xaml :

  • xmlns:viewModels Lägg till XML-namnområdet som är avsett för Notes.ViewModels .NET-namnområdet.
  • Lägg till en BindingContext på sidan.
  • Ta bort händelsehanterarna för knapparna för radera och spara Clicked och ersätt dem med kommandon.

Uppdatera anteckningsvyn:

  1. I fönstret Solution Explorer i Visual Studio dubbelklickar du på Views\NotePage.xaml för att öppna XAML-redigeraren.

  2. Klistra in följande kod:

    <?xml version="1.0" encoding="utf-8" ?>
    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:viewModels="clr-namespace:Notes.ViewModels"
                 x:Class="Notes.Views.NotePage"
                 Title="Note"
                 x:DataType="viewModels:NoteViewModel">
        <ContentPage.BindingContext>
            <viewModels:NoteViewModel />
        </ContentPage.BindingContext>
        <VerticalStackLayout Spacing="10" Margin="5">
            <Editor x:Name="TextEditor"
                    Placeholder="Enter your note"
                    Text="{Binding Text}"
                    HeightRequest="100" />
    
            <Grid ColumnDefinitions="*,*" ColumnSpacing="4">
                <Button Text="Save"
                        Command="{Binding SaveCommand}"/>
    
                <Button Grid.Column="1"
                        Text="Delete"
                        Command="{Binding DeleteCommand}"/>
    
            </Grid>
        </VerticalStackLayout>
    </ContentPage>
    

Tidigare deklarerade den här vyn inte någon bindningskontext, eftersom den angavs av själva sidans kod bakom. Om du ställer in bindningskontexten direkt i XAML finns det två saker:

  • När sidan navigeras till vid körning visas en tom anteckning. Det beror på att den parameterlösa konstruktorn för bindningskontexten, viewmodel, anropas. Om du kommer ihåg rätt skapar den parameterlösa konstruktorn för note viewmodel en tom anteckning.

  • Intellisense i XAML-redigeraren visar de tillgängliga egenskaperna så snart du börjar skriva {Binding syntax. Syntaxen verifieras också och aviserar dig om ett ogiltigt värde. Prova att ändra bindningssyntaxen SaveCommand för till Save123Command. Om du hovrar musmarkören över texten ser du att en knappbeskrivning visas som informerar dig om att Save123Command inte hittas. Det här meddelandet betraktas inte som ett fel eftersom bindningar är dynamiska, det är verkligen en liten varning som kan hjälpa dig att märka när du skrev fel egenskap.

    Om du har ändrat SaveCommand till ett annat värde återställer du det nu.

Rensa anteckningskoden bakom

Nu när interaktionen med vyn har ändrats från händelsehanterare till kommandon öppnar du filen Views\NotePage.xaml.cs och ersätter all kod med en klass som bara innehåller konstruktorn:

  1. Dubbelklicka på Vyer\NotePage.xaml.cs i fönstret Solution Explorer i Visual Studio.

    Tips/Råd

    Du kan behöva expandera Views\NotePage.xaml för att visa filen.

  2. Ersätt koden med följande kodfragment:

    namespace Notes.Views;
    
    public partial class NotePage : ContentPage
    {
        public NotePage()
        {
            InitializeComponent();
        }
    }
    

Skapa vyn Anteckningar

Det sista viewmodel-view-paret är vyn Anteckningar viewmodel och AllNotes. För närvarande binds vyn dock direkt till modellen, som togs bort i början av den här handledningen. Målet med att uppdatera AllNotes-vyn är att flytta så mycket funktionalitet som möjligt från XAML-koden bakom och placera den i viewmodel. Återigen är fördelen att vyn kan ändra sin design med liten effekt på koden.

Visningsmodell för anteckningar

Baserat på vad AllNotes-vyn kommer att visa och vilka interaktioner användaren kommer att göra, måste notes viewmodel tillhandahålla följande objekt:

  • En samling anteckningar.
  • Ett kommando för att hantera navigering till en anteckning.
  • Ett kommando för att skapa en ny anteckning.
  • Uppdatera listan med anteckningar när en skapas, tas bort eller ändras.

Skapa Antecknings-vymodellen:

  1. Dubbelklicka på ViewModels\NotesViewModel.cs i fönstret Solution Explorer i Visual Studio.

  2. Ersätt koden i den här filen med följande kod:

    using CommunityToolkit.Mvvm.Input;
    using System.Collections.ObjectModel;
    using System.Windows.Input;
    
    namespace Notes.ViewModels;
    
    internal class NotesViewModel: IQueryAttributable
    {
    }
    

    Den här koden är tom NotesViewModel där du lägger till egenskaper och kommandon för att stödja AllNotes vyn.

  3. NotesViewModel Lägg till följande egenskaper i klasskoden:

    public ObservableCollection<ViewModels.NoteViewModel> AllNotes { get; }
    public ICommand NewCommand { get; }
    public ICommand SelectNoteCommand { get; }
    

    Egenskapen AllNotes är en ObservableCollection som lagrar alla anteckningar som läses in från enheten. De två kommandona används av vyn för att utlösa åtgärderna för att skapa en anteckning eller välja en befintlig anteckning.

  4. Lägg till en parameterlös konstruktor i klassen, som initierar kommandona och läser in anteckningarna från modellen:

    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);
    }
    

    Observera att AllNotes samlingen använder Models.Note.LoadAll metoden för att fylla den observerbara samlingen med anteckningar. Metoden LoadAll returnerar anteckningarna Models.Note som typ, men den observerbara samlingen är en samling ViewModels.NoteViewModel typer. Koden använder Select Linq-tillägget för att skapa viewmodel-instanser från anteckningsmodellerna som returneras från LoadAll.

  5. Skapa de metoder som kommandona riktar in sig på:

    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}");
    }
    

    Observera att NewNoteAsync metoden inte tar en parameter medan den SelectNoteAsync gör det. Kommandon kan också ha en enda parameter som tillhandahålls när kommandot anropas. För SelectNoteAsync-metoden representerar parametern noten som väljs.

  6. Implementera slutligen IQueryAttributable.ApplyQueryAttributes metoden:

    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)));
        }
    }
    

    Note viewmodel som skapades i det föregående steget av handledningen utnyttjade navigering när anteckningen sparades eller togs bort. Viewmodelle navigerade tillbaka till AllNotes-vyn som denna viewmodelle är associerad med. Den här koden identifierar om frågesträngen innehåller antingen deleted eller-nyckeln saved . Värdet för nyckeln är den unika identifieraren för anteckningen.

    Om anteckningen har tagits bort, matchas anteckningen mot den angivna identifieraren i AllNotes-samlingen och tas bort.

    Det finns två möjliga orsaker till att en anteckning sparas. Anteckningen skapades eller så ändrades en befintlig anteckning. Om anteckningen redan finns i AllNotes samlingen är det en anteckning som har uppdaterats. I det här fallet behöver anteckningsinstansen i samlingen bara uppdateras. Om anteckningen saknas i samlingen är det en ny anteckning och måste läggas till i samlingen.

Koden för klassen bör se ut som följande kodfragment:

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)));
        }
    }
}

Alla anteckningsvyn

Nu när viewmodel har skapats uppdaterar du AllNotes-vyn så att den pekar på egenskaperna för viewmodel. Använd följande ändringar i filen Views\AllNotesPage.xaml :

  • xmlns:viewModels Lägg till XML-namnområdet som är avsett för Notes.ViewModels .NET-namnområdet.
  • Lägg till en BindingContext på sidan.
  • Ta bort händelsen för verktygsfältsknappen Clicked och använd egenskapen Command .
  • Ändra CollectionView för att binda dess ItemSource till AllNotes.
  • CollectionView Ändra till att använda kommandon för att reagera på när det valda objektet ändras.

Uppdatera AllNotes-vyn:

  1. Dubbelklicka på Views\AllNotesPage.xaml i fönstret Solution Explorer i Visual Studio.

  2. Klistra in följande kod:

    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:viewModels="clr-namespace:Notes.ViewModels"
                 x:Class="Notes.Views.AllNotesPage"
                 Title="Your Notes"
                 x:DataType="viewModels:NotesViewModel">
        <ContentPage.BindingContext>
            <viewModels:NotesViewModel />
        </ContentPage.BindingContext>
    
        <!-- Add an item to the toolbar -->
        <ContentPage.ToolbarItems>
            <ToolbarItem Text="Add" Command="{Binding NewCommand}" IconImageSource="{FontImageSource Glyph='+', Color=Black, Size=22}" />
        </ContentPage.ToolbarItems>
    
        <!-- Display notes in a list -->
        <CollectionView x:Name="notesCollection"
                        ItemsSource="{Binding AllNotes}"
                        Margin="20"
                        SelectionMode="Single"
                        SelectionChangedCommand="{Binding SelectNoteCommand}"
                        SelectionChangedCommandParameter="{Binding x:DataType='CollectionView', Source={RelativeSource Self}, Path=SelectedItem}">
            <!-- Designate how the collection of items are laid out -->
            <CollectionView.ItemsLayout>
                <LinearItemsLayout Orientation="Vertical" ItemSpacing="10" />
            </CollectionView.ItemsLayout>
    
            <!-- Define the appearance of each item in the list -->
            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="viewModels:NoteViewModel">
                    <StackLayout>
                        <Label Text="{Binding Text}" FontSize="22"/>
                        <Label Text="{Binding Date}" FontSize="14" TextColor="Silver"/>
                    </StackLayout>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </ContentPage>
    

Verktygsfältet använder Clicked inte längre händelsen och använder i stället ett kommando.

CollectionView stödjer kommandohantering med egenskaperna SelectionChangedCommand och SelectionChangedCommandParameter. I den uppdaterade XAML-filen SelectionChangedCommand är egenskapen bunden till viewmodels SelectNoteCommand, vilket innebär att kommandot anropas när det valda objektet ändras. När kommandot anropas skickas egenskapsvärdet SelectionChangedCommandParameter till kommandot.

Titta på bindningen som används för CollectionView:

<CollectionView x:Name="notesCollection"
                ItemsSource="{Binding AllNotes}"
                Margin="20"
                SelectionMode="Single"
                SelectionChangedCommand="{Binding SelectNoteCommand}"
                SelectionChangedCommandParameter="{Binding x:DataType='CollectionView', Source={RelativeSource Self}, Path=SelectedItem}">

Egenskapen SelectionChangedCommandParameter använder Source={RelativeSource Self} bindning. Refererar Self till det aktuella objektet, som är CollectionView. Anger x:DataTypeCollectionView därför som typ för den kompilerade bindningen. Observera att bindningsvägen är egenskapen SelectedItem. När kommandot anropas genom att det valda objektet SelectNoteCommand ändras anropas kommandot och det markerade objektet skickas till kommandot som en parameter.

För det bindningsuttryck som definierats i SelectionChangedCommandParameter egenskapen som ska kompileras är det nödvändigt att instruera projektet att aktivera kompilerade bindningar i uttryck som anger Source egenskapen. Om du vill göra detta redigerar du projektfilen för din lösning och lägger till <MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation> i elementet <PropertyGroup> :

<PropertyGroup>
  <MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
</PropertyGroup>

Rensa AllNotes-koden bakom

Nu när interaktionen med vyn har ändrats från händelsehanterare till kommandon öppnar du filen Views\AllNotesPage.xaml.cs och ersätter all kod med en klass som bara innehåller konstruktorn:

  1. I fönstret Solution Explorer i Visual Studio dubbelklickar du på Vyer\AllNotesPage.xaml.cs.

    Tips/Råd

    Du kan behöva expandera Views\AllNotesPage.xaml för att visa filen.

  2. Ersätt koden med följande kodfragment:

    namespace Notes.Views;
    
    public partial class AllNotesPage : ContentPage
    {
        public AllNotesPage()
        {
            InitializeComponent();
        }
    }
    

Kör appen

Nu kan du köra appen och allt fungerar. Det finns dock två problem med hur appen beter sig:

  • Om du väljer en anteckning, som öppnar redigeraren, trycker du på Spara och sedan försöker välja samma anteckning fungerar den inte.
  • När en anteckning ändras eller läggs till ordnas inte listan med anteckningar om för att visa de senaste anteckningarna högst upp.

De här två problemen åtgärdas i nästa självstudiesteg.

Åtgärda appbeteendet

Nu när appkoden kan kompileras och köras har du förmodligen märkt att det finns två fel med hur appen beter sig. Appen låter dig inte markera en anteckning som redan är markerad och listan med anteckningar ordnas inte om när en anteckning har skapats eller ändrats.

Flytta anteckningar överst i listan

Åtgärda först omordningsproblemet med anteckningslistan. I filen AllNotes innehåller samlingen alla anteckningar som ska visas för användaren. Tyvärr är nackdelen med att använda en ObservableCollection att den måste sorteras manuellt. Utför följande steg för att hämta de nya eller uppdaterade objekten överst i listan:

  1. Dubbelklicka på ViewModels\NotesViewModel.cs i fönstret Solution Explorer i Visual Studio.

  2. ApplyQueryAttributes I metoden tittar du på logiken för den sparade frågesträngsnyckeln.

  3. När matchedNote inte null, uppdateras anteckningen. AllNotes.Move Använd metoden för att flytta matchedNote till index 0, som är överst i listan.

    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);
    }
    

    Metoden AllNotes.Move tar två parametrar för att flytta ett objekts position i samlingen. Den första parametern är indexet för det objekt som ska flyttas, och den andra parametrarna är indexet för var objektet ska flyttas. Metoden AllNotes.IndexOf hämtar anteckningens index.

  4. matchedNote När null är, är anteckningen ny och läggs till i listan. I stället för att lägga till den, som lägger till anteckningen i slutet av listan, infogar du anteckningen vid index 0, som är överst i listan. AllNotes.Add Ändra metoden till AllNotes.Insert.

    string noteId = query["saved"].ToString();
    NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
    
    // If note is found, update it
    if (matchedNote != null)
    {
        matchedNote.Reload();
        AllNotes.Move(AllNotes.IndexOf(matchedNote), 0);
    }
    // If note isn't found, it's new; add it.
    else
        AllNotes.Insert(0, new NoteViewModel(Models.Note.Load(noteId)));
    

Metoden ApplyQueryAttributes bör se ut som följande kodfragment:

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)));
    }
}

Tillåt att du väljer en anteckning två gånger

I vyn CollectionView visas alla anteckningar, men du kan inte välja samma anteckning två gånger. Det finns två sätt som objektet förblir valt: när användaren ändrar en befintlig anteckning och när användaren tvingas navigera bakåt. Fallet där användaren sparar en anteckning korrigeras med kodändringen i föregående avsnitt som använder AllNotes.Move, så du behöver inte bekymra dig om det fallet.

Problemet som du måste lösa nu är relaterat till navigering. Oavsett hur man navigerar till Allnotes-vyn så genereras händelsen för sidan. Den här händelsen är ett perfekt tillfälle att tvångsavmarkera det markerade objektet i CollectionView.

Men med MVVM-mönstret som tillämpas här kan viewmodel inte utlösa något direkt i vyn, till exempel rensa det valda objektet när anteckningen har sparats. Så hur får man det att hända? En bra implementering av MVVM-mönstret minimerar bakomliggande kod i vyn. Det finns några olika sätt att lösa det här problemet för att stödja MVVM-separationsmönstret. Men det är också OK att placera kod i code-behind för vyn, särskilt när den är direkt kopplad till vyn. MVVM har många bra design och koncept som hjälper dig att dela upp din app, förbättra underhållsbarheten och göra det enklare för dig att lägga till nya funktioner. I vissa fall kan det dock hända att MVVM uppmuntrar till överengineering.

Överarbeta inte lösningen för det här problemet och använd helt enkelt NavigatedTo-händelsen för att ta bort det valda objektet från CollectionView.

  1. Dubbelklicka på Views\AllNotesPage.xaml i fönstret Solution Explorer i Visual Studio.

  2. I XAML för <ContentPage>lägger du till NavigatedTo händelsen:

    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:viewModels="clr-namespace:Notes.ViewModels"
                 x:Class="Notes.Views.AllNotesPage"
                 Title="Your Notes"
                 NavigatedTo="ContentPage_NavigatedTo"
                 x:DataType="viewModels:NotesViewModel">
        <ContentPage.BindingContext>
            <viewModels:NotesViewModel />
    
  3. Du kan lägga till en standardhändelsehanterare genom att högerklicka på händelsemetodens namn ContentPage_NavigatedTooch välja Gå till definition. Den här åtgärden öppnar views\AllNotesPage.xaml.cs i kodredigeraren.

  4. Ersätt händelsehanterarkoden med följande kodfragment:

    private void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e)
    {
        notesCollection.SelectedItem = null;
    }
    

    I XAML gavs CollectionView namnet notesCollection. Den här koden använder det namnet för att komma åt CollectionViewoch ange SelectedItem till null. Det markerade objektet rensas varje gång sidan navigeras till.

Kör nu appen. Försök att navigera till en anteckning, tryck på bakåtknappen och välj samma anteckning en andra gång. Appbeteendet är åtgärdat!

Utforska koden. Utforska koden för den här självstudien.. Om du vill ladda ned en kopia av det slutförda projektet för att jämföra koden med laddar du ned det här projektet.

Din app använder nu MVVM-mönster!

Nästa steg

Följande länkar innehåller mer information om några av de begrepp som du har lärt dig i den här självstudien: