Hvorfor arkitektur faktisk er det vigtigste i din mobilapp
Okay, lad os starte med et scenarie, som du sikkert kender alt for godt: du starter et nyt .NET MAUI-projekt, koden flyder let, funktionerne vokser hurtigt — og så, fire måneder inde i projektet, er din kodebase pludselig blevet til noget, der minder om spaghetti. Hver ændring bryder noget andet. Test er nærmest umulige at skrive. Og din code-behind-fil til hovedsiden? Den fylder 800 linjer.
Det er ikke et spørgsmål om talent. Det handler om arkitektur.
MVVM (Model-View-ViewModel) er det arkitektoniske mønster, der løser dette problem i .NET MAUI. Og med fremskridtene i CommunityToolkit.Mvvm, .NET 9/10 og de nye source generators er MVVM-udvikling i 2026 mere strømlinet og elegant end nogensinde.
I denne guide gennemgår vi alt, du skal vide for at bygge en .NET MAUI-app med en ren, testbar og vedligeholdbar arkitektur. Vi dækker MVVM-mønsteret fra bunden, CommunityToolkit.Mvvm med source generators, dependency injection, Clean Architecture-principper, messaging mellem ViewModels og Shell-navigation med DI — og vi binder det hele sammen med praktiske kodeeksempler, du kan bruge med det samme.
MVVM-mønsteret: Kernen i .NET MAUI-arkitektur
MVVM er bygget op omkring tre centrale komponenter, der hver har et klart ansvarsområde:
- Model — Repræsenterer data og forretningslogik. Det kan være en bruger, en ordre eller resultatet af et API-kald. Modellen ved intet om brugergrænsefladen.
- View — Den visuelle del. I .NET MAUI er det dine XAML-sider og kontroller. View'en viser data og sender brugerhandlinger videre, men indeholder selv minimal logik.
- ViewModel — Bindeledet mellem Model og View. Den eksponerer data som properties og handlinger som commands. View'en binder sig til ViewModel'en via databinding.
Denne adskillelse giver dig tre afgørende fordele:
- Testbarhed — ViewModels kan enhedstestes uden en brugergrænseflade. Du kan verificere al din logik i hurtige, automatiserede tests.
- Vedligeholdelighed — Når UI-logikken er isoleret fra forretningslogikken, kan du ændre det ene uden at bryde det andet.
- Genbrugelighed — ViewModels og services kan deles på tværs af platforme og endda mellem .NET MAUI og WPF/Blazor.
Det lyder måske lidt abstrakt, men tro mig — når du først har prøvet at refaktorere en 500-linjers code-behind til et rent MVVM-setup, forstår du værdien med det samme.
CommunityToolkit.Mvvm: Det MVVM-bibliotek du bør bruge
CommunityToolkit.Mvvm (tidligere kendt som Microsoft.Toolkit.Mvvm) er i 2026 det officielt anbefalede MVVM-bibliotek til .NET MAUI. Det vedligeholdes af Microsoft, er en del af .NET Foundation, og bruges i flere førstepartsapps i Windows — herunder Microsoft Store.
Hvad gør det så godt? Kort sagt:
- Hurtigt — Bruger source generators i stedet for reflection, hvilket gør det ideelt til AOT-kompilering.
- Modulært — Du bruger kun det, du har brug for. Ingen tvungne patterns eller all-in-frameworks.
- Platformuafhængigt — Fungerer identisk på .NET MAUI, WPF, WinForms og mere.
- Letvægtigt — Ingen eksterne afhængigheder.
Installation er simpel — bare tilføj NuGet-pakken:
dotnet add package CommunityToolkit.Mvvm
Source Generators: Farvel til boilerplate-kode
Den største game-changer i moderne MVVM-udvikling er source generators. I stedet for at skrive hundredvis af linjer gentagen kode til properties og commands, lader CommunityToolkit.Mvvm compileren gøre det tunge arbejde for dig.
Ærligt talt, da jeg første gang så forskellen, var det lidt af en åbenbaring.
ObservableProperty: Fra felt til property på én linje
Traditionelt krævede en observable property i MVVM en del kode:
// Den gamle måde — verbose og fejlbehæftet
private string _brugernavn;
public string Brugernavn
{
get => _brugernavn;
set => SetProperty(ref _brugernavn, value);
}
Med [ObservableProperty] og den nye partial property-syntaks fra CommunityToolkit.Mvvm 8.4+ (kræver .NET 9 SDK) bliver det her reduceret til:
using CommunityToolkit.Mvvm.ComponentModel;
public partial class LoginViewModel : ObservableObject
{
[ObservableProperty]
public partial string Brugernavn { get; set; }
[ObservableProperty]
public partial string Adgangskode { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(KanLoggeInd))]
public partial bool ErOptaget { get; set; }
public bool KanLoggeInd => !ErOptaget;
}
Source generatoren opretter automatisk backing fields, INotifyPropertyChanged-implementeringen og al den nødvendige infrastruktur. [NotifyPropertyChangedFor]-attributten sikrer, at når ErOptaget ændres, bliver KanLoggeInd også opdateret i UI'en. Ret elegant, ikke?
Partial properties vs. annoterede felter
Før version 8.4 brugte man annoterede private felter:
// Ældre tilgang (stadig understøttet)
[ObservableProperty]
private string _brugernavn;
Den nye partial property-tilgang er dog klart at foretrække. Her er hvorfor:
- Bedre AOT-kompatibilitet (Ahead-of-Time kompilering)
- Understøttelse af
required,sealed,overrideognewmodifiers - Bedre nullability-annotation
- Mulighed for at tilpasse accessibility på individuelle accessors
- Fuld integration med C#-sproget — ikke en workaround
RelayCommand: Commands uden ceremoni
[RelayCommand]-attributten eliminerer behovet for manuelt at oprette command-objekter:
public partial class LoginViewModel : ObservableObject
{
[ObservableProperty]
public partial string Brugernavn { get; set; }
[ObservableProperty]
public partial string Adgangskode { get; set; }
[RelayCommand]
private async Task LogIndAsync()
{
// Source generatoren opretter automatisk en
// LogIndCommand af typen AsyncRelayCommand
var resultat = await _authService.LogIndAsync(Brugernavn, Adgangskode);
if (resultat.Succes)
{
await Shell.Current.GoToAsync("//forside");
}
}
[RelayCommand(CanExecute = nameof(KanSlette))]
private void SletKonto()
{
// SletKontoCommand oprettes automatisk
// med CanExecute-logik bundet til KanSlette
}
private bool KanSlette() => !string.IsNullOrEmpty(Brugernavn);
}
En ting der er værd at nævne: AsyncRelayCommand forhindrer som standard concurrent execution. Så hvis en bruger trykker på en knap flere gange hurtigt, vil kommandoen kun udføres én gang ad gangen. Knappen deaktiveres automatisk under udførelsen — det er faktisk ret smart.
Dependency Injection: Fundamentet for løs kobling
.NET MAUI har førsteklasses understøttelse af dependency injection via Microsoft.Extensions.DependencyInjection. DI er ikke bare en nice-to-have — det er fundamentet for testbar og vedligeholdbar kode.
Registrering af services, ViewModels og Views
Al registrering foregår i MauiProgram.cs:
using Microsoft.Extensions.Logging;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Services
builder.Services.AddSingleton<IAuthService, AuthService>();
builder.Services.AddSingleton<IApiClient, ApiClient>();
builder.Services.AddTransient<IOrdreService, OrdreService>();
// ViewModels
builder.Services.AddSingleton<LoginViewModel>();
builder.Services.AddSingleton<ForsideViewModel>();
builder.Services.AddTransient<OrdreDetaljeViewModel>();
// Views (VIGTIGT: glem ikke dette!)
builder.Services.AddSingleton<LoginPage>();
builder.Services.AddSingleton<ForsidePage>();
builder.Services.AddTransient<OrdreDetaljePage>();
return builder.Build();
}
}
Levetider: Singleton vs. Transient vs. Scoped
Valget af levetid er afgørende — og det her er noget, mange udviklere snubler over i starten:
- Singleton — Én instans for hele appens levetid. Brug til services med delt state (f.eks. autentificering, caching) og sider der skal bevare state.
- Transient — Ny instans hver gang den efterspørges. Brug til detailsider, dialoger og services uden state.
- Scoped — Én instans per scope. Sjældnere brugt i MAUI, men nyttigt i visse Blazor Hybrid-scenarier.
En klassisk fejl: Mange udviklere registrerer deres ViewModels men glemmer at registrere de tilhørende Pages. Resultatet? En MissingMethodException under runtime, fordi Shell ikke kan oprette en side med konstruktør-parametre uden DI-registrering. Jeg har selv begået den fejl mere end én gang.
Constructor Injection i praksis
Når både Page og ViewModel er registreret, injicerer DI-containeren automatisk afhængighederne:
public partial class ForsidePage : ContentPage
{
public ForsidePage(ForsideViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}
public partial class ForsideViewModel : ObservableObject
{
private readonly IApiClient _apiClient;
private readonly IOrdreService _ordreService;
public ForsideViewModel(IApiClient apiClient, IOrdreService ordreService)
{
_apiClient = apiClient;
_ordreService = ordreService;
}
[ObservableProperty]
public partial ObservableCollection<Ordre> Ordrer { get; set; }
[RelayCommand]
private async Task HentOrdrerAsync()
{
var data = await _ordreService.HentAlleAsync();
Ordrer = new ObservableCollection<Ordre>(data);
}
}
Clean Architecture i .NET MAUI
MVVM alene strukturerer forholdet mellem UI og logik. Men for større projekter har du brug for en bredere arkitektonisk strategi. Clean Architecture kombineret med MVVM giver dig en lagdelt struktur, der skalerer — og som gør dit liv markant nemmere, når projektet vokser.
Lagene i Clean Architecture
En typisk Clean Architecture-struktur for en .NET MAUI-app ser sådan ud:
MinApp/
├── MinApp.Core/ # Inderste lag — ingen afhængigheder
│ ├── Models/
│ │ ├── Bruger.cs
│ │ └── Ordre.cs
│ ├── Interfaces/
│ │ ├── IBrugerRepository.cs
│ │ └── IOrdreRepository.cs
│ └── Services/
│ ├── IBrugerService.cs
│ └── IOrdreService.cs
│
├── MinApp.Infrastructure/ # Ydre lag — implementerer Core-interfaces
│ ├── Repositories/
│ │ ├── BrugerRepository.cs
│ │ └── OrdreRepository.cs
│ ├── Api/
│ │ └── ApiClient.cs
│ └── Data/
│ └── LokalDatabase.cs
│
├── MinApp.Maui/ # Præsentationslag
│ ├── Views/
│ │ ├── LoginPage.xaml
│ │ └── ForsidePage.xaml
│ ├── ViewModels/
│ │ ├── LoginViewModel.cs
│ │ └── ForsideViewModel.cs
│ └── MauiProgram.cs
Afhængighedsreglen
Den vigtigste regel i Clean Architecture er afhængighedsreglen: afhængigheder peger altid indad. Core-laget ved intet om Infrastructure eller MAUI. Infrastructure implementerer Core's interfaces. Og MAUI-laget orkestrerer det hele via DI.
Hvad giver denne struktur dig helt konkret?
- Portabilitet — Core-laget kan genbruges i en ASP.NET Core-backend, en Blazor-app eller et WPF-projekt.
- Testbarhed — Core-laget kan testes helt isoleret med mock-implementeringer.
- Fleksibilitet — Du kan udskifte din database, dit API eller din UI uden at røre forretningslogikken.
Et praktisk eksempel: Service med interface
// I MinApp.Core/Interfaces/IOrdreService.cs
public interface IOrdreService
{
Task<IEnumerable<Ordre>> HentAlleAsync();
Task<Ordre?> HentVedIdAsync(int id);
Task OpretAsync(Ordre ordre);
}
// I MinApp.Infrastructure/Services/OrdreService.cs
public class OrdreService : IOrdreService
{
private readonly IApiClient _apiClient;
private readonly ILokalDatabase _database;
public OrdreService(IApiClient apiClient, ILokalDatabase database)
{
_apiClient = apiClient;
_database = database;
}
public async Task<IEnumerable<Ordre>> HentAlleAsync()
{
try
{
var ordrer = await _apiClient.GetAsync<List<Ordre>>("/api/ordrer");
await _database.GemOrdrerAsync(ordrer);
return ordrer;
}
catch (HttpRequestException)
{
// Fallback til lokal cache ved netværksfejl
return await _database.HentOrdrerAsync();
}
}
public async Task<Ordre?> HentVedIdAsync(int id)
{
return await _apiClient.GetAsync<Ordre>($"/api/ordrer/{id}");
}
public async Task OpretAsync(Ordre ordre)
{
await _apiClient.PostAsync("/api/ordrer", ordre);
}
}
Læg mærke til fallback-logikken i HentAlleAsync — den prøver først API'et og falder tilbage til lokal cache ved netværksfejl. Det er et mønster, du vil bruge igen og igen i mobilapps.
Messaging mellem ViewModels med WeakReferenceMessenger
Sommetider skal ViewModels kommunikere med hinanden. F.eks. når en bruger logger ind, og forsiden skal opdateres. Men direkte referencer mellem ViewModels skaber tæt kobling, og det vil vi gerne undgå.
Løsningen er WeakReferenceMessenger fra CommunityToolkit.Mvvm.
Vigtigt: I .NET 10 er den gamle MessagingCenter gjort intern. WeakReferenceMessenger er nu den officielle erstatning — så det er den, du skal bruge.
Definer en besked
using CommunityToolkit.Mvvm.Messaging.Messages;
// En besked der sendes når brugeren logger ind
public class BrugerLoggetIndMessage : ValueChangedMessage<Bruger>
{
public BrugerLoggetIndMessage(Bruger bruger) : base(bruger) { }
}
// En simpel notifikationsbesked uden data
public class OrdreOprettetMessage
{
public int OrdreId { get; }
public OrdreOprettetMessage(int ordreId) => OrdreId = ordreId;
}
Send en besked
public partial class LoginViewModel : ObservableObject
{
private readonly IAuthService _authService;
public LoginViewModel(IAuthService authService)
{
_authService = authService;
}
[RelayCommand]
private async Task LogIndAsync()
{
var bruger = await _authService.LogIndAsync(Brugernavn, Adgangskode);
if (bruger is not null)
{
// Send besked til alle lyttere
WeakReferenceMessenger.Default.Send(
new BrugerLoggetIndMessage(bruger));
await Shell.Current.GoToAsync("//forside");
}
}
}
Modtag en besked
public partial class ForsideViewModel : ObservableRecipient
{
private readonly IOrdreService _ordreService;
public ForsideViewModel(IOrdreService ordreService)
{
_ordreService = ordreService;
// Aktivér modtagelse af beskeder
IsActive = true;
}
protected override void OnActivated()
{
// Registrer modtagere
Messenger.Register<BrugerLoggetIndMessage>(this, (r, m) =>
{
var viewModel = (ForsideViewModel)r;
viewModel.AktuelBruger = m.Value;
viewModel.HentOrdrerCommand.Execute(null);
});
}
[ObservableProperty]
public partial Bruger? AktuelBruger { get; set; }
[ObservableProperty]
public partial ObservableCollection<Ordre> Ordrer { get; set; }
[RelayCommand]
private async Task HentOrdrerAsync()
{
var data = await _ordreService.HentAlleAsync();
Ordrer = new ObservableCollection<Ordre>(data);
}
}
WeakReferenceMessenger bruger svage referencer, hvilket betyder, at modtagere automatisk bliver fjernet af garbage collector, når de ikke længere bruges. Du behøver altså ikke manuelt at afregistrere dem — selvom det stadig er god praksis i performance-kritiske scenarier.
Shell-navigation med Dependency Injection
.NET MAUI Shell håndterer navigation og bruger automatisk DI-containeren til at oprette sider. Det giver en pæn integration mellem navigation og arkitektur.
Registrering af ruter
// I AppShell.xaml.cs
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
// Registrer ruter for sider der ikke er i Shell's visuelle hierarki
Routing.RegisterRoute("ordredetalje", typeof(OrdreDetaljePage));
Routing.RegisterRoute("profilrediger", typeof(ProfilRedigerPage));
}
}
Navigation med parametre
public partial class ForsideViewModel : ObservableObject
{
[RelayCommand]
private async Task VisOrdreDetaljeAsync(Ordre ordre)
{
// Navigation med query-parametre
await Shell.Current.GoToAsync("ordredetalje", new Dictionary<string, object>
{
{ "Ordre", ordre }
});
}
}
// Modtag parametre via QueryProperty
[QueryProperty(nameof(Ordre), "Ordre")]
public partial class OrdreDetaljeViewModel : ObservableObject
{
[ObservableProperty]
public partial Ordre Ordre { get; set; }
partial void OnOrdreChanged(Ordre value)
{
// Reagér på at ordren er sat
HentDetaljerCommand.Execute(null);
}
[RelayCommand]
private async Task HentDetaljerAsync()
{
// Hent yderligere detaljer for ordren
}
}
Bemærk brugen af partial void OnOrdreChanged — det er endnu en source generator-funktion fra CommunityToolkit.Mvvm. For enhver [ObservableProperty] genereres automatisk partielle metoder, som du kan implementere for at reagere på ændringer. Virkelig praktisk.
Avancerede mønstre: Navigation Service
For større apps kan det blive nødvendigt at abstrahere navigationen bag et interface, så ViewModels ikke har en direkte afhængighed til Shell.Current:
// Interface for navigation
public interface INavigationService
{
Task NavigerTilAsync(string rute);
Task NavigerTilAsync(string rute, IDictionary<string, object> parametre);
Task GåTilbageAsync();
}
// Implementering
public class MauiNavigationService : INavigationService
{
public async Task NavigerTilAsync(string rute)
{
await Shell.Current.GoToAsync(rute);
}
public async Task NavigerTilAsync(string rute,
IDictionary<string, object> parametre)
{
await Shell.Current.GoToAsync(rute, parametre);
}
public async Task GåTilbageAsync()
{
await Shell.Current.GoToAsync("..");
}
}
// Registrering i MauiProgram.cs
builder.Services.AddSingleton<INavigationService, MauiNavigationService>();
Nu kan ViewModels navigere via interfacet, og du kan nemt mocke det i dine tests:
public partial class LoginViewModel : ObservableObject
{
private readonly IAuthService _authService;
private readonly INavigationService _navigation;
public LoginViewModel(IAuthService authService, INavigationService navigation)
{
_authService = authService;
_navigation = navigation;
}
[RelayCommand]
private async Task LogIndAsync()
{
var resultat = await _authService.LogIndAsync(Brugernavn, Adgangskode);
if (resultat.Succes)
{
await _navigation.NavigerTilAsync("//forside");
}
}
}
Anti-mønstre du absolut skal undgå
God arkitektur handler lige så meget om, hvad du ikke gør. Her er de faldgruber, jeg ser oftest:
1. "God Object" ViewModel
En ViewModel der håndterer dataadgang, navigation, forretningslogik, UI-state og validering på én gang. Resultatet er en klasse på 500+ linjer, der er umulig at teste.
Løsning: Anvend Single Responsibility Principle. Bryd store ViewModels op i mindre, fokuserede enheder, og deleger forretningslogik til services.
2. Tung code-behind
At placere logik i code-behind-filer (f.eks. MainPage.xaml.cs) underminerer hele formålet med MVVM. Hvis din code-behind indeholder mere end konstruktøren og eventuel UI-specifik logik som animationer, er det tid til at refaktorere.
Løsning: Brug databinding, commands og DI til at flytte logik ind i ViewModels eller services.
3. Statisk state
At bruge statiske variabler til app-state (f.eks. App.CurrentUser) introducerer svært-debugbare fejl og gør test nærmest umuligt.
Løsning: Brug DI med passende levetider. En singleton-service kan holde global state på en kontrolleret og testbar måde.
4. Direkte UI-kald i ViewModel
Kald som DisplayAlert() eller Navigation.PushAsync() direkte i en ViewModel binder den til .NET MAUI's specifikke API'er.
Løsning: Brug abstraktioner som INavigationService og IDialogService, der kan mockes i tests.
5. For tidlig abstraktion
Det her er faktisk lidt ironisk i en artikel om arkitektur, men: at oprette interfaces, base-klasser og utility-metoder for alt fra starten skaber unødig kompleksitet. Tre ens linjer kode er bedre end en for tidlig abstraktion.
Løsning: Abstrahér først, når du ser et gentagende mønster mindst tre gange. Hold det simpelt.
Et komplet eksempel: Todo-app med ren arkitektur
Nok teori — lad os samle alle principper i et komplet (om end forenklet) eksempel. En klassisk todo-app:
Model
// Core/Models/TodoItem.cs
public class TodoItem
{
public int Id { get; set; }
public string Titel { get; set; } = string.Empty;
public string? Beskrivelse { get; set; }
public bool ErFuldført { get; set; }
public DateTime OprettetDato { get; set; } = DateTime.UtcNow;
}
Service interface og implementering
// Core/Interfaces/ITodoService.cs
public interface ITodoService
{
Task<IEnumerable<TodoItem>> HentAlleAsync();
Task TilføjAsync(TodoItem item);
Task OpdaterAsync(TodoItem item);
Task SletAsync(int id);
}
// Infrastructure/Services/TodoService.cs
public class TodoService : ITodoService
{
private readonly List<TodoItem> _items = new();
private int _næsteId = 1;
public Task<IEnumerable<TodoItem>> HentAlleAsync()
=> Task.FromResult<IEnumerable<TodoItem>>(_items.ToList());
public Task TilføjAsync(TodoItem item)
{
item.Id = _næsteId++;
_items.Add(item);
return Task.CompletedTask;
}
public Task OpdaterAsync(TodoItem item)
{
var eksisterende = _items.FirstOrDefault(x => x.Id == item.Id);
if (eksisterende is not null)
{
eksisterende.Titel = item.Titel;
eksisterende.Beskrivelse = item.Beskrivelse;
eksisterende.ErFuldført = item.ErFuldført;
}
return Task.CompletedTask;
}
public Task SletAsync(int id)
{
_items.RemoveAll(x => x.Id == id);
return Task.CompletedTask;
}
}
ViewModel
// ViewModels/TodoListeViewModel.cs
public partial class TodoListeViewModel : ObservableObject
{
private readonly ITodoService _todoService;
private readonly INavigationService _navigation;
public TodoListeViewModel(ITodoService todoService, INavigationService navigation)
{
_todoService = todoService;
_navigation = navigation;
}
[ObservableProperty]
public partial ObservableCollection<TodoItem> TodoItems { get; set; }
[ObservableProperty]
public partial string NyTodoTitel { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HarIngenItems))]
public partial bool Indlæser { get; set; }
public bool HarIngenItems => !Indlæser
&& (TodoItems is null || TodoItems.Count == 0);
[RelayCommand]
private async Task IndlæsAsync()
{
Indlæser = true;
try
{
var items = await _todoService.HentAlleAsync();
TodoItems = new ObservableCollection<TodoItem>(items);
}
finally
{
Indlæser = false;
}
}
[RelayCommand]
private async Task TilføjTodoAsync()
{
if (string.IsNullOrWhiteSpace(NyTodoTitel)) return;
var nyItem = new TodoItem { Titel = NyTodoTitel };
await _todoService.TilføjAsync(nyItem);
TodoItems.Add(nyItem);
NyTodoTitel = string.Empty;
}
[RelayCommand]
private async Task SkiftFuldførtAsync(TodoItem item)
{
item.ErFuldført = !item.ErFuldført;
await _todoService.OpdaterAsync(item);
}
[RelayCommand]
private async Task SletTodoAsync(TodoItem item)
{
await _todoService.SletAsync(item.Id);
TodoItems.Remove(item);
}
}
View (XAML)
<!-- Views/TodoListePage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MinApp.ViewModels"
x:Class="MinApp.Views.TodoListePage"
x:DataType="vm:TodoListeViewModel"
Title="Mine Opgaver">
<Grid RowDefinitions="Auto,*" Padding="16">
<!-- Tilføj ny todo -->
<HorizontalStackLayout Spacing="8">
<Entry Text="{Binding NyTodoTitel}"
Placeholder="Ny opgave..."
HorizontalOptions="FillAndExpand" />
<Button Text="Tilføj"
Command="{Binding TilføjTodoCommand}" />
</HorizontalStackLayout>
<!-- Todo-liste -->
<CollectionView Grid.Row="1"
ItemsSource="{Binding TodoItems}"
EmptyView="Ingen opgaver endnu">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:TodoItem">
<SwipeView>
<SwipeView.RightItems>
<SwipeItems>
<SwipeItem Text="Slet"
BackgroundColor="Red"
Command="{Binding
Source={RelativeSource
AncestorType={x:Type vm:TodoListeViewModel}},
Path=SletTodoCommand}"
CommandParameter="{Binding .}" />
</SwipeItems>
</SwipeView.RightItems>
<Grid ColumnDefinitions="Auto,*" Padding="8">
<CheckBox IsChecked="{Binding ErFuldført}" />
<Label Grid.Column="1"
Text="{Binding Titel}"
VerticalOptions="Center" />
</Grid>
</SwipeView>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>
DI-registrering
// MauiProgram.cs
builder.Services.AddSingleton<ITodoService, TodoService>();
builder.Services.AddSingleton<INavigationService, MauiNavigationService>();
builder.Services.AddSingleton<TodoListeViewModel>();
builder.Services.AddSingleton<TodoListePage>();
Sammenfatning og næste skridt
God arkitektur i .NET MAUI handler ikke om at følge regler blindt. Det handler om at træffe bevidste valg, der gør din kode lettere at forstå, teste og udvide over tid.
Her er de vigtigste takeaways:
- Brug MVVM konsekvent — Adskil UI fra logik med Views, ViewModels og Models. Brug databinding og commands i stedet for code-behind.
- Udnyt CommunityToolkit.Mvvm — Source generators med
[ObservableProperty]og[RelayCommand]eliminerer boilerplate og gør din kode renere. - Opdatér til partial properties — Med CommunityToolkit.Mvvm 8.4+ og .NET 9 SDK får du bedre AOT-kompatibilitet og fuld C#-integration.
- Dependency Injection er obligatorisk — Registrer services, ViewModels og Views i
MauiProgram.cs. Husk at registrere begge dele! - Overvej Clean Architecture — For større projekter giver en lagdelt struktur markant bedre testbarhed og portabilitet.
- Brug WeakReferenceMessenger — For kommunikation mellem ViewModels uden tæt kobling.
MessagingCenterer forældet i .NET 10. - Abstrahér navigation — Et
INavigationService-interface holder ViewModels uafhængige af Shell. - Undgå anti-mønstre — "God Object" ViewModels, tung code-behind, statisk state og for tidlig abstraktion er dine fjender.
Med disse principper på plads er du klar til at bygge .NET MAUI-apps, der ikke bare virker i dag, men som også kan vedligeholdes og udvides i mange år fremover.