Frissítve: 2026. május 28.
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.
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.
dotnet add package CommunityToolkit.Mvvm --version 8.4.0Telepí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.
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string _firstName = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string _lastName = string.Empty;
public string FullName => $"{FirstName} {LastName}";RelayCommand: parancsok forrásgenerátorral
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:
<Button Text="Keresés" Command="{Binding SearchCommand}" />
<Button Text="Törlés"
Command="{Binding DeleteProductCommand}"
CommandParameter="{Binding .}" />CanExecute logika
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.
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string _searchText = string.Empty;
[RelayCommand(CanExecute = nameof(CanSearch))]
private void Search() { /* ... */ }
private bool CanSearch() => !string.IsNullOrWhiteSpace(SearchText);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.
[RelayCommand]
private async Task LoadProductsAsync()
{
IsBusy = true;
try
{
var items = await _productService.GetAllAsync();
Products.Clear();
foreach (var item in items)
Products.Add(item);
}
catch (HttpRequestException ex)
{
await Shell.Current.DisplayAlert("Hiba", ex.Message, "OK");
}
finally
{
IsBusy = false;
}
}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.
// Megengedjük a párhuzamos futást (ritkán kell)
[RelayCommand(AllowConcurrentExecutions = true)]
private async Task RefreshAsync() { /* ... */ }
// Cancellation token automatikus injektálás
[RelayCommand(IncludeCancelCommand = true)]
private async Task DownloadAsync(CancellationToken cancellationToken)
{
await _fileService.DownloadAsync(cancellationToken);
}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ó
partialkulcsszó: 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 nincsOnPropertyChangedhí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 aNotifyCanExecuteChanged()-et. - Messenger leak: A
WeakReferenceMessengerugyan 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 azIRecipient<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.




