CommunityToolkit.Mvvm on Microsoftin ylläpitämä avoimen lähdekoodin kirjasto, joka tarjoaa .NET MAUI -sovelluksille modernit lähdegeneraattoreihin (source generators) perustuvat MVVM-rakennuspalikat ilman heijastuksen (reflection) suorituskykyhittiä. Käytännössä kirjoitat puolet vähemmän liimakoodia. [ObservableProperty] generoi PropertyChanged-tapahtumat, [RelayCommand] kääräisee metodit ICommand-toteutukseksi, ja WeakReferenceMessenger hoitaa heikosti viittaavan komponenttien välisen viestinnän. Tämä opas käy läpi koko paketin tuotantokäytön, ei vain hello world -esimerkit.
CommunityToolkit.Mvvm 8.4 (julkaistu marraskuussa 2025) tukee .NET 9- ja .NET 10 -versioita sekä trimmausta ja AOT-käännöstä ilman erityismäärityksiä.
[ObservableProperty]-attribuutti generoi täydellisen ominaisuuden ja PropertyChanged-kutsut käännösvaiheessa, ei heijastusta eikä suorituskykyhittiä.
[RelayCommand] tukee asynkronisia metodeja, peruutustokeneita ja CanExecute-tilan automaattista päivitystä.
WeakReferenceMessenger korvaa MessagingCenterin (joka poistettiin .NET MAUI 9:ssä) heikosti viittaavalla, tyyppiturvallisella mallilla.
ObservableValidator integroi System.ComponentModel.DataAnnotations -validoinnin suoraan ViewModeliin.
Yhdistä CommunityToolkit.Mvvm Microsoft.Extensions.DependencyInjection -rekisteröintiin MauiProgram.cs-tiedostossa. Ei staattisia ServiceLocator-malleja.
Mikä CommunityToolkit.Mvvm on ja miksi se on .NET MAUI:n oletusvalinta 2026?
CommunityToolkit.Mvvm (entiseltä nimeltään Microsoft.Toolkit.Mvvm) on .NET Foundationin hallinnoima MVVM-kirjasto, joka on käytännössä korvannut MVVM Light Toolkitin, Prismin yksinkertaisemmat käyttötapaukset ja MAUI:n alkuperäisen MessagingCenterin. Olen siirtänyt viimeisen kahden vuoden aikana neljä Xamarin.Forms-sovellusta MAUI:hin, ja jokaisessa migraatiossa ensimmäinen päätös oli vaihtaa käsinkirjoitetut INotifyPropertyChanged-toteutukset CommunityToolkitin lähdegeneraattoreihin. Säästyneet rivit eivät ole pelkkä esteettinen voitto, vähemmän koodia tarkoittaa vähemmän paikkoja, joissa esimerkiksi nameof-merkkijono voi mennä väärin ja rikkoa sidonnan hiljaisesti.
Vuoden 2026 alussa CommunityToolkit.Mvvm 8.4 toi täyden tuen .NET 10:n natiivi-AOT:lle, mikä tarkoittaa että iOS-julkaisubuildit pakkautuvat pienemmiksi eikä trimmausvaroituksia tule. Tämä on tärkeää siksi, että monet vanhat MVVM-kirjastot luottavat ajonaikaiseen tyyppiheijastukseen, joka ei toimi AOT:n kanssa. Lähdegeneraattorit ratkaisevat ongelman tekemällä työn käännösvaiheessa: generoitu koodi näkyy IDE:ssä tavallisena C#:na, ja debuggeri voi astua sen läpi.
Tiimimme on standardisoinut kirjaston kaikkiin uusiin MAUI-projekteihin. Se ei ole hopealuoti (esimerkiksi navigaatiokerrokseen tarvitaan silti joko Shell tai oma palvelu), mutta ViewModel-puolen toistuva liimakoodi häviää lähes kokonaan.
Asennus ja määritys MauiProgram.cs:ssä
Asennus on suoraviivainen NuGet-paketin lisäys. Lisää paketti MAUI-projektin .csproj-tiedostoon tai komentoriviltä:
Paketti on yhteensopiva net8.0-, net9.0- ja net10.0-kohteiden kanssa. Lisävalmistelua ei tarvita, sillä lähdegeneraattorit aktivoituvat automaattisesti, kun käytät attribuutteja. Suosittelen kuitenkin rekisteröimään ViewModelit DI-konttiin MauiProgram.cs:ssä singletonien ja transientien välillä tietoisesti valiten:
using CommunityToolkit.Mvvm.Messaging;
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");
});
// Messenger jaettu kaikille, heikko viittaus ei vuoda muistia
builder.Services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
// Palvelut
builder.Services.AddSingleton<IProductService, ProductService>();
builder.Services.AddSingleton<IAuthService, AuthService>();
// ViewModelit transientteina, uusi instanssi joka sivulle
builder.Services.AddTransient<ProductListViewModel>();
builder.Services.AddTransient<ProductDetailViewModel>();
// Sivut transientteina
builder.Services.AddTransient<ProductListPage>();
builder.Services.AddTransient<ProductDetailPage>();
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
ObservableProperty-attribuutti käytännössä
Perinteinen INotifyPropertyChanged-toteutus vaatii backing-kentän, getterin, setterin ja OnPropertyChanged-kutsun. CommunityToolkit korvaa tämän osittaisluokalla (partial) ja attribuutilla. Tässä reaalinen tuoteluettelon ViewModel:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
public partial class ProductListViewModel : ObservableObject
{
private readonly IProductService _productService;
[ObservableProperty]
private ObservableCollection<Product> products = new();
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasNoProducts))]
private bool isLoading;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(RefreshCommand))]
private string searchText = string.Empty;
public bool HasNoProducts => !IsLoading && Products.Count == 0;
public ProductListViewModel(IProductService productService)
{
_productService = productService;
}
partial void OnSearchTextChanged(string value)
{
// Generoitu osittaismetodi, kutsutaan automaattisesti, kun arvo muuttuu
System.Diagnostics.Debug.WriteLine($"Haku muuttui: {value}");
}
}
Huomaa kolme asiaa. Luokka on partial (generaattori lisää loput), kenttä on pieni-alkuinen products (generoitu ominaisuus on iso-alkuinen Products), ja [NotifyPropertyChangedFor] kertoo generaattorille, että jokin riippuvainen laskettu ominaisuus pitää myös päivittää. OnSearchTextChanged on osittaismetodi, jota generaattori kutsuu. Määrittele se, jos haluat reagoida muutokseen, jätä pois muuten.
Riippuvuusominaisuudet ja laskettu tila
Yksi yleinen tarve on, että UI-elementin näkyvyys riippuu kahdesta tai useammasta ominaisuudesta. Aikaisemmin tämä vaati manuaalisesti kuunnella molempien ominaisuuksien muutoksia ja kutsua OnPropertyChanged riippuvalle ominaisuudelle. CommunityToolkit ratkaisee tämän [NotifyPropertyChangedFor]-attribuutilla, jota voi pinota:
Tämä on yksi niistä pikkupiirteistä, jotka tekevät CommunityToolkitista käytännössä korvaamattoman tuotantokoodissa. Olemme nähneet, että näiden riippuvuuksien ylläpito käsin on yksi yleisimmistä syistä UI-bugeihin, joissa nappi pysyy harmaana, vaikka sen pitäisi aktivoitua. Honestly, törmäsin tähän juuri viime projektissa, kun checkout-nappi jäi disabloituun tilaan kahdessa täysin eri tilanteessa.
RelayCommand ja asynkroniset komennot
ICommand-toteutus on toinen alue, jossa CommunityToolkit poistaa paljon liimakoodia. Vanha tapa vaati erillisen RelayCommand-luokan tai Prismin DelegateCommand-instanssin per komento. Uusi tapa: kirjoita tavallinen metodi ja merkitse se attribuutilla:
Generaattori tuottaa kolme asiaa: RefreshCommand-ominaisuuden tyyppiä IAsyncRelayCommand, OpenDetailCommand-ominaisuuden tyyppiä IAsyncRelayCommand<Product> ja oikeat CanExecute-sidonnat. XAML-puolella sidot ne suoraan:
Asynkronisten komentojen CancellationToken-parametri on automaattinen: generaattori tunnistaa sen ja toimittaa peruutustokenin, joka peruuttaa edellisen ajon, jos komento käynnistetään uudelleen sen ollessa kesken. Tämä on ratkaiseva ominaisuus hakukentissä, joissa käyttäjä kirjoittaa nopeasti. Hit tätä bugia itse parin viime sovelluksen kanssa ennen kuin opin käyttämään tokenia oikein.
WeakReferenceMessenger ja viestintä ViewModelien välillä
MAUI 9 poisti vanhan MessagingCenter-luokan. Korvaaja on WeakReferenceMessenger, joka pitää tilaajat heikkoina viitteinä. Tämä tarkoittaa, että unohdettu tilaus ei pidä ViewModelia elossa eikä aiheuta muistivuotoja. Käytännön esimerkki tuotteen päivityksestä:
// Viestiluokka, pelkkä DTO
public sealed record ProductUpdatedMessage(Product Product);
// Lähettäjä, yksityiskohtasivu
public partial class ProductDetailViewModel : ObservableObject
{
private readonly IMessenger _messenger;
public ProductDetailViewModel(IMessenger messenger)
{
_messenger = messenger;
}
[RelayCommand]
private async Task SaveAsync()
{
// ... tallenna palveluun ...
_messenger.Send(new ProductUpdatedMessage(CurrentProduct));
await Shell.Current.GoToAsync("..");
}
}
// Vastaanottaja, listasivu
public partial class ProductListViewModel
: ObservableObject, IRecipient<ProductUpdatedMessage>
{
public ProductListViewModel(IMessenger messenger, IProductService service)
{
_productService = service;
messenger.Register(this);
}
public void Receive(ProductUpdatedMessage message)
{
var existing = Products.FirstOrDefault(p => p.Id == message.Product.Id);
if (existing != null)
{
var idx = Products.IndexOf(existing);
Products[idx] = message.Product;
}
}
}
Olemme kokeneet tämän mallin luotettavammaksi kuin tapahtumat. Heikko viittaus estää yleisen MAUI-muistivuodon, jossa Page-instanssi jää roskanlerääjältä piiloon, koska ViewModel viittaa sen tapahtumakäsittelijään. Jos rakennat REST API -integraation MAUI-sovellukseesi, Messenger on luonnollinen tapa ilmoittaa onnistuneista latauksista listanäkymille.
Kanavoidut viestit ja vastaanottotokenit
Joissain tilanteissa haluat lähettää saman viestityypin eri "kanaviin", esimerkiksi erillisen kanavan kullekin avoimelle välilehdelle. WeakReferenceMessenger tukee tätä token-parametrilla:
Käytä tätä säästeliäästi. Useimmiten viestityyppi itse on riittävän tarkka erotin. Token-malli on hyödyllinen lähinnä, kun sama ViewModel-tyyppi instansioidaan monta kertaa yhtä aikaa eri kontekstissa.
Kun ObservableObject ei riitä: ObservableRecipient
ViewModel, joka sekä rekisteröityy moneen viestiin että hallinnoi omaa tilaansa, voi periytyä ObservableRecipient-luokasta ObservableObject-luokan sijaan. Tämä antaa kaksi etua: IsActive-ominaisuus, joka kytkee tilaukset päälle ja pois, sekä konstruktorin kautta saatu IMessenger-instanssi. Käytännössä tämä yksinkertaistaa elinkaaren hallintaa Shell-navigoinnin kanssa. Kun sivu poistuu näkyvistä, asetat IsActive = false, ja kaikki tilaukset purkautuvat automaattisesti ilman manuaalista UnregisterAll-kutsua. Tämä on erityisen hyödyllistä sovelluksissa, joissa on paljon välilehtiä ja jokainen välilehti tilaa eri viestejä. Jos käytät tähän Shell-navigointia, lue myös .NET MAUI Shell -navigoinnin opas, jossa käymme läpi välilehtien ja flyout-valikon elinkaaren.
Validointi ObservableValidator-luokalla
Lomakekentät tarvitsevat validointia, ja CommunityToolkit tarjoaa ObservableValidator-pohjaluokan, joka integroituu System.ComponentModel.DataAnnotations -attribuutteihin. Käyttäjän rekisteröintilomake näyttää tältä:
[NotifyDataErrorInfo] kertoo, että ominaisuuden muutoksen yhteydessä validoinnit ajetaan ja virheet näkyvät INotifyDataErrorInfo-rajapinnan kautta. Sama mekanismi, jota XAML-sidonnat odottavat. Käytä HasErrors-ominaisuutta tallennusnapin IsEnabled-sidonnassa.
Suorituskyky, AOT ja trimmaus
Lähdegeneraattorit eivät vain vähennä koodia, ne ovat myös merkittävästi nopeampia kuin heijastukseen perustuvat MVVM-kirjastot. Kun siirsimme aikaisemmin BindableBaseen perustuneet ViewModelit CommunityToolkitiin, propertyChanged-tapahtumien kustannus laski mittauksissamme noin 30–40 %. Tärkeämpää kuitenkin on, että generoitu koodi on staattisesti analysoitavissa, joten iOS:n AOT-kääntäjä ja MAUI 9:n trimmaaja eivät tuota varoituksia.
Jos otat AOT:n käyttöön julkaisubuildissa, lisää .csproj-tiedostoon:
Kolme yleisintä virhettä, joita olen nähnyt tiimeissä (ja jotka osuivat omalle kohdalleni jossain vaiheessa):
Kentän nimi alkaa isolla kirjaimella. Generaattori odottaa private string name;, ei private string Name;. Jos käytät isoa alkukirjainta, et saa generoitua ominaisuutta, ja kaikki sidonnat hajoavat hiljaisesti.
Unohdettu partial-avainsana. Sekä luokan että joissain tapauksissa sisemmän metodin pitää olla osittaisia. Virheviestit ovat epäselviä.
Singleton-ViewModel transient-sivulla. Tämä on kallis virhe: käyttäjä näkee vanhaa dataa, kun palaa sivulle. Käytä transient-rekisteröintiä, ellei sinulla ole tietoinen syy pitää tila yli navigoinnin (esim. ostoskori).
Messenger.Register ilman Unregister-kutsua dispose-vaiheessa. Vaikka WeakReferenceMessenger on heikko, on hyvä siivota tilaukset eksplisiittisesti, kun ViewModel poistetaan. Toteuta IDisposable ja kutsu messenger.UnregisterAll(this).
Jos rakennat tietokantakerroksen samaan sovellukseen, suosittelen lukemaan myös oppaamme SQLite-paikallisesta tietokannasta ja MVVM-arkkitehtuurista. Sieltä näkee, miten ObservableCollection ja SQLite-async-API toimivat yhdessä ilman UI-jäätymistä.
Usein kysytyt kysymykset
Mikä on CommunityToolkit.Mvvm-paketin ja MAUI:n alkuperäisen MVVM-tuen ero?
.NET MAUI ei tarjoa omaa MVVM-toteutusta, se vain tukee data-sidontaa INotifyPropertyChanged-rajapinnan kautta. CommunityToolkit.Mvvm on erillinen kirjasto, joka tarjoaa lähdegeneraattorit, RelayCommand-toteutuksen ja Messenger-mallin tämän rajapinnan päälle.
Voiko CommunityToolkit.Mvvm-kirjastoa käyttää ilman MAUI:ta?
Kyllä. Paketti on UI-riippumaton ja toimii WPF-, WinUI-, Avalonia-, Blazor- ja Uno-sovelluksissa sekä konsoliprojekteissa. Se vaatii vain .NET 6 tai uudemman.
Miten ObservableProperty eroaa tavallisesta INotifyPropertyChanged-toteutuksesta?
[ObservableProperty] generoi käännösvaiheessa täyden ominaisuuden, backing-kentän ja PropertyChanged-kutsut. Tavallinen toteutus vaatii sinun kirjoittavan kaiken käsin, mikä on virheherkkää ja monisanaista. Ajonaikainen suorituskyky on käytännössä sama, mutta koodirivien määrä putoaa noin 70 %.
Kyllä. MessagingCenter merkittiin vanhentuneeksi MAUI 8:ssa ja poistettiin MAUI 9:ssä. WeakReferenceMessenger on suora korvaaja, mutta tyyppiturvallisempi (ei merkkijonokanavia) ja se käyttää heikkoja viittauksia, jotka estävät yleisimmät muistivuodot.
Tukeeko CommunityToolkit.Mvvm AOT-käännöstä ja trimmausta?
Kyllä, versiosta 8.2 lähtien täysin. Koska kaikki MVVM-rakenne generoidaan käännösvaiheessa eikä heijastusta käytetä, trimmaaja ja AOT-kääntäjä eivät tuota varoituksia. Tämä on yksi merkittävimmistä syistä valita se vanhempien kirjastojen, kuten MVVM Light, sijaan.
Cross-platform engineering lead who's shipped apps to millions on both Play Store and App Store. Believes shared codebases shouldn't mean shared mediocrity.
Push-ilmoitukset .NET MAUI -sovelluksessa vaativat sekä Firebase FCM:n Androidille että APNs:n iOS:lle. Tässä käytännön oppaassa käydään läpi HTTP v1 -rajapinta, token-rekisteröinti, notification channels, deep linking Shell-reitille ja tuotannon kompastuskivet.