CommunityToolkit.Mvvm .NET MAUI-ban: forrásgenerátorok és ObservableProperty útmutató (2026)
A CommunityToolkit.Mvvm forrásgenerátorai 60-70%-kal rövidebbé teszik a .NET MAUI ViewModel-eket. Gyakorlati útmutató ObservableProperty, RelayCommand, Messenger és validáció témákban éles projektből származó tippekkel.
A CommunityToolkit.Mvvm a Microsoft hivatalos MVVM könyvtára, ami Roslyn forrásgenerátorokkal automatikusan generálja az INotifyPropertyChanged és ICommand implementációkat, így a .NET MAUI ViewModel-jeid nagyjából 60-70%-kal rövidebbek lesznek. Egyetlen [ObservableProperty] attribútum kiváltja a teljes property-mezős, OnPropertyChanged-hívásos sablont, és mivel a generátor fordítási időben dolgozik, futási reflection-költség sincs.
A CommunityToolkit.Mvvm 8.4-es verziója (2026 márciusi kiadás) teljes .NET 9 és Hot Reload támogatást ad, és a forrásgenerátorok átlagos fordítási költségét körülbelül 35%-kal csökkentette.
Az [ObservableProperty] partial mezőből generál teljes property-t: a kódolt mező nevét camelCase-ből PascalCase-re alakítja, és automatikusan beszúrja az OnPropertyChanged hívást.
A [RelayCommand] attribútum metódusból generál IRelayCommand-ot, aszinkron metódusból pedig automatikusan IAsyncRelayCommand-ot készít, és kezeli a párhuzamos hívások blokkolását.
A WeakReferenceMessenger a publish/subscribe mintát valósítja meg memóriaszivárgás nélkül, ViewModel-ek közötti kommunikációra ideális, ahol a Dependency Injection már nem elegendő.
Az ObservableValidator a DataAnnotations attribútumait használja űrlap-validációhoz, és valós időben jelzi a hibákat az UI felé.
A [NotifyCanExecuteChangedFor] attribútummal automatikusan újraértékelhetők a parancsok engedélyezett állapotai, amikor egy kapcsolódó property változik.
Mi az a CommunityToolkit.Mvvm és miért használjam?
A CommunityToolkit.Mvvm (régebbi nevén Microsoft.Toolkit.Mvvm) egy .NET Standard 2.0 könyvtár, ami a Model-View-ViewModel mintát támogatja Roslyn forrásgenerátorok segítségével. A klasszikus MVVM-megközelítésben minden egyes property-hez le kell írnod egy privát mezőt, egy nyilvános property-t getterrel és setterrel, és meg kell hívnod az OnPropertyChanged-et a setterben. Ez 5-7 sor kód propertynként, vagyis egy 20-property-s ViewModel könnyen elér a 150 sorhoz pusztán boilerplate-ből.
A toolkit ezt egyetlen attribútumra redukálja. A forrásgenerátor a fordítás során olvassa a kódodat, és a megjelölt mezőkből generálja a teljes property-t a háttérben. Mivel ez build-time történik, futási reflection nincs, és a Hot Reload is hibátlanul működik. Ezt az utóbbit három éles MAUI projektben is teszteltem, és 8.4-es verziótól kezdve nálam stabilan elment.
Architekturális szempontból a fő érv a kódbiztonság. A forrásgenerátor nem felejti el az OnPropertyChanged-hívást, nem ír el property-nevet, és a refaktorálás során automatikusan együtt mozognak a hivatkozások. Az MVVM-mel közvetlenül összefüggő témákról bővebben írtam a .NET MAUI Shell navigációs útmutatóban, ahol a ViewModel-ek és a navigációs paraméterek kapcsolatát mutatom be.
Telepítés és beállítás .NET MAUI projektben
A csomag NuGet-en érhető el CommunityToolkit.Mvvm néven. A 2026 márciusában megjelent 8.4-es verzió a legfrissebb stabil kiadás, és teljes .NET 9 támogatást ad. Ha még .NET 8-on vagy, az is jól működik, mert a könyvtár .NET Standard 2.0-t céloz. A hivatalos Microsoft Learn dokumentáció tartalmazza az aktuális API-referenciát.
Telepítés után a MauiProgram.cs-ben regisztráld a ViewModel-eket és a hozzájuk tartozó oldalakat Dependency Injection-nel. Ez kritikus, mert a toolkit nem ír felül DI-megoldást, hanem az általad választott konténerrel együttműködik.
// MauiProgram.cs
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// ViewModel regisztrálás
builder.Services.AddSingleton<MainViewModel>();
builder.Services.AddTransient<ProductEditViewModel>();
// Page regisztrálás (a Page konstruktorba érkezik a ViewModel)
builder.Services.AddSingleton<MainPage>();
builder.Services.AddTransient<ProductEditPage>();
return builder.Build();
}
Egy fontos részlet, amit én is megtanultam a saját káromon: a ViewModel-osztálynak partial-nak kell lennie, mert a generátor partial kiegészítést készít. Ha lefelejted a partial kulcsszót, a build hibával fog elszállni. Ez tényleg a leggyakoribb kezdő hiba.
Az ObservableProperty attribútum használata
Az [ObservableProperty] egy mezőre tett attribútum, amelyből a generátor teljes property-t készít. A névkonvenció szigorú: a mezőnek camelCase-ben kell lennie (kötelezően kis kezdőbetű, opcionális aláhúzás-prefix), a generált property pedig PascalCase lesz. Tehát _userName-ből UserName, name-ből Name lesz.
using CommunityToolkit.Mvvm.ComponentModel;
public partial class UserProfileViewModel : ObservableObject
{
[ObservableProperty]
private string _userName = string.Empty;
[ObservableProperty]
private int _age;
[ObservableProperty]
private bool _isPremium;
}
A fenti 10 sor pontosan ugyanazt csinálja, mint a következő kézzel írt 45 sor:
public class UserProfileViewModel : INotifyPropertyChanged
{
private string _userName = string.Empty;
public string UserName
{
get => _userName;
set
{
if (_userName == value) return;
_userName = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UserName)));
}
}
// ... ugyanez Age-re és IsPremium-ra
public event PropertyChangedEventHandler? PropertyChanged;
}
A generátor a property setterben automatikusan meghív több hook-metódust is, ha létezik: OnUserNameChanging(string value), OnUserNameChanged(string value). Ezekkel kontrollálhatod az értékadást validáció vagy mellékhatás miatt: saját partial metódust írsz, és a generátor csak meghívja, ha létezik.
partial void OnUserNameChanged(string value)
{
// Például: logoljuk a változást vagy frissítsük a kapcsolt property-t
FullName = $"{value} ({Age})";
}
Kapcsolódó property-k automatikus frissítése
Ha az egyik property változása egy másikat is érint (computed property), használd a [NotifyPropertyChangedFor] attribútumot. Ezzel a forrásgenerátor a generált setterbe beszúr egy második OnPropertyChanged(nameof(FullName)) hívást.
A [RelayCommand] attribútum egy metódusra kerül, és a generátor készít hozzá egy automatikusan elnevezett IRelayCommand property-t. A névkonvenció: a metódusnévhez hozzáfűződik a „Command" utótag. Tehát Save metódusból SaveCommand lesz, DeleteUser-ből DeleteUserCommand.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class ProductListViewModel : ObservableObject
{
[ObservableProperty]
private string _searchText = string.Empty;
[RelayCommand]
private void Search()
{
// SearchCommand property generálódik automatikusan
// ide jön a keresési logika
}
[RelayCommand]
private void DeleteProduct(Product product)
{
// Paraméteres parancs, XAML-ben CommandParameter-rel hívható
Products.Remove(product);
}
}
XAML-ben a generált parancs ugyanúgy köthető, mint bármely más ICommand:
A parancsok engedélyezett állapotát egy második metódus határozza meg, amit az attribútum CanExecute paraméterében nevezel meg. Ez a metódus visszaad egy bool-t, és a generátor összeköti a CanExecute delegate-tel.
A [NotifyCanExecuteChangedFor] attribútum kritikus része ennek. Amikor a SearchText változik, a forrásgenerátor automatikusan meghívja a SearchCommand.NotifyCanExecuteChanged()-et, ami újraértékeli a gomb engedélyezettségét. E nélkül a gomb nem frissülne, és a felhasználó megnyomhatatlan állapotban ragadna (én pont ezt a bug-ot debugoltam fél órán át egy ügyfélprojektben, mire leesett, hogy ez hiányzik).
Hogyan kezeljem az aszinkron parancsokat MAUI-ban?
A [RelayCommand] aszinkron metódust is támogat. Ha a metódus Task-ot vagy Task<T>-t ad vissza, a generátor IAsyncRelayCommand-ot készít helyette. Ez tartalmazza az IsRunning tulajdonságot, amivel követhetjük, hogy a parancs még fut-e, és automatikusan megakadályozza a párhuzamos hívásokat.
A párhuzamos hívások viselkedése konfigurálható az attribútum paramétereivel. Az alapértelmezett AllowConcurrentExecutions = false megakadályozza, hogy a felhasználó kétszer kattintson és két párhuzamos kérést indítson el. Ez egyébként egy gyakori bug-forrás a régi MVVM-megoldásokban. A REST API-hívások részletes kezeléséről bővebben írtam a .NET MAUI HttpClient REST API útmutatóban.
Az IncludeCancelCommand = true egy különösen hasznos opció. A generátor egy második parancsot is készít DownloadCancelCommand néven, amivel a futó művelet megszakítható. Ezt köthetjük egy „Mégse" gombhoz, és a CancellationToken automatikusan injektálódik a metódusba.
A Messenger osztály: kommunikáció ViewModel-ek között
Amikor két ViewModel-nek beszélgetnie kell egymással (például a részletek oldal jelzi a listaoldalnak, hogy egy elem megváltozott), a Dependency Injection már nem elegáns megoldás. Itt jön képbe a WeakReferenceMessenger, a CommunityToolkit publish/subscribe implementációja, ami gyenge referenciákkal kerüli el a memóriaszivárgást.
// 1. Üzenet osztály (általában record)
public record ProductUpdatedMessage(int ProductId, string NewName);
// 2. Küldés (publish)
WeakReferenceMessenger.Default.Send(new ProductUpdatedMessage(42, "Új név"));
// 3. Feliratkozás (subscribe), pl. ViewModel konstruktorában
public ProductListViewModel()
{
WeakReferenceMessenger.Default.Register<ProductUpdatedMessage>(this, (recipient, message) =>
{
var product = Products.FirstOrDefault(p => p.Id == message.ProductId);
if (product != null)
product.Name = message.NewName;
});
}
Őszinte tapasztalat három éles appból: a Messenger túlhasználata gyorsan nehezen követhető kódhoz vezet. Amint öt különböző helyen küldesz ugyanazt az üzenetet, már fogalmad sincs, ki frissít mit és mikor. Tartsd a használatát olyan eseményekre, amik valóban több ViewModel-t érintenek, és amiket nem tudsz konstruktor-injekcióval megoldani.
Validáció ObservableValidator-ral
Űrlap-validációhoz az ObservableObject helyett az ObservableValidator ősosztályból származz le. Ez támogatja a standard System.ComponentModel.DataAnnotations attribútumokat, és valós időben jelzi a hibákat az INotifyDataErrorInfo-n keresztül.
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
public partial class RegistrationViewModel : ObservableValidator
{
[ObservableProperty]
[Required(ErrorMessage = "Az email cím kötelező")]
[EmailAddress(ErrorMessage = "Érvénytelen email formátum")]
[NotifyDataErrorInfo]
private string _email = string.Empty;
[ObservableProperty]
[Required]
[MinLength(8, ErrorMessage = "Legalább 8 karakter szükséges")]
[NotifyDataErrorInfo]
private string _password = string.Empty;
[RelayCommand(CanExecute = nameof(CanRegister))]
private async Task RegisterAsync()
{
ValidateAllProperties();
if (HasErrors) return;
// ... regisztráció
}
private bool CanRegister() => !HasErrors;
}
A [NotifyDataErrorInfo] attribútum kapcsolja össze a property változását a validációval. A setter minden értékadás után újra lefuttatja a property attribútumait, és az ErrorsChanged esemény tüzelődik az UI felé. Az .NET DataAnnotations referencia tartalmazza az összes elérhető validációs attribútumot.
Gyakori hibák és buktatók
Az alábbi hibákba a legtöbb fejlesztő belesétál. Ezeket éles projekteken tanultam meg, és valószínűleg neked sem fognak elsőre feltűnni.
Hiányzó partial kulcsszó: A generátornak partial osztályra van szüksége. „Type already defines member" hibát kapsz, ha lefelejted.
Property változás trigger nélkül: Ha a kódban közvetlenül a private mezőhöz (_userName = "X") írsz a property (UserName = "X") helyett, az UI nem frissül, mert nincs OnPropertyChanged hívás.
CanExecute nem frissül: Ha elfelejted a [NotifyCanExecuteChangedFor] attribútumot, a parancs engedélyezett állapota „beragad". A gomb addig disabled marad, amíg manuálisan meg nem hívod a NotifyCanExecuteChanged()-et.
Messenger leak: A WeakReferenceMessenger ugyan gyenge referenciákat használ, de ha lambda-ban capture-öltél egy erős referenciát, mégis szivároghat. Részesítsd előnyben az IRecipient<TMessage> interfész-implementációt.
UI szál hiba aszinkron parancsban: Ha a parancs metódusában háttérszálról frissítesz egy property-t, a binding nem mindig fut UI szálon. Használd a MainThread.BeginInvokeOnMainThread()-et szükség szerint.
További lépések és integráció
A CommunityToolkit.Mvvm nálam tökéletesen működik együtt a többi alapvető MAUI-eszközzel. Adatbázis-integrációhoz nézd meg a .NET MAUI SQLite helyi adatbázis útmutatót, ahol a Repository-mintát építem ki ViewModel-ekkel. Az általam preferált architektúra: ObservableObject az adatmodellekhez, ObservableObject ViewModel-ekhez, ObservableValidator űrlap-ViewModel-ekhez, és WeakReferenceMessenger kizárólag oldalfüggetlen eseményekhez.
A CommunityToolkit GitHub repó aktívan karbantartott, és a kiadási megjegyzések rendszeresen tartalmaznak migrációs útmutatókat is. Érdemes feliratkozni a release értesítésekre, mert a forrásgenerátor frissítései ritkán törő változással járnak, de a fordítási teljesítmény szempontjából jó naprakésznek lenni.
Gyakran ismételt kérdések
Mi a különbség a RelayCommand és az ICommand között?
Az ICommand a .NET beépített interfésze parancsokhoz, neked kell implementálnod az Execute és CanExecute metódusokat, valamint a CanExecuteChanged eseményt. A RelayCommand egy kész implementáció, ami delegate-eket vesz át, a [RelayCommand] attribútum pedig forrásgenerátorral még a delegate-eket sem kell kézzel írnod.
Cserélhetem a Prism vagy MVVMCross helyett?
Részben. A CommunityToolkit csak az MVVM alaprészét fedi le (property változás, parancsok, messaging, validáció). A teljes funkcionalitásukhoz (modulok, navigációs szolgáltatás, dialógusok) a Prism vagy MVVMCross továbbra is jobb választás. Sok projektben együtt használjuk őket: Prism a navigációhoz, CommunityToolkit a property-khez.
Lassítja a forrásgenerátor a fordítást?
Minimálisan. A 8.4-es verzióban a generátor körülbelül 100-300 millisecunddal növeli a fordítási időt egy közepes méretű (50-100 ViewModel) projektben. Ez töredéke annak az időnek, amit a kézzel írt boilerplate karbantartása venne el.
Működik a Hot Reload az ObservableProperty-vel?
Igen, a 8.3.0 verziótól teljesen támogatott. Új [ObservableProperty] mező hozzáadásakor a generátor újrafut, és a Hot Reload felveszi a változást. Komplex generátor-változások (új attribútum-kombinációk) néha teljes újrafordítást igényelnek.
Hogyan teszteljem a CommunityToolkit ViewModel-eket?
A generált property-k és parancsok ugyanúgy működnek unit tesztben, mint a kézzel írtak. Készíts ViewModel-példányt, mock-old a függőségeket, és tesztelheted a UserName = "X" setteren keresztül vagy a SearchCommand.Execute(null) hívással. Az IsRunning property aszinkron teszteknél hasznos a parancs befejezésének detektálásához.
Ismerd meg, hogyan integrálhatsz SQLite adatbázist .NET MAUI alkalmazásodba: az SQLite-net beállítástól a CRUD műveleteken át az MVVM összekötésig, teljesítményoptimalizálásig és offline szinkronig, működő kódpéldákkal.