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.

CommunityToolkit.Mvvm MAUI Útmutató (2026)

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.

  • 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.

dotnet add package CommunityToolkit.Mvvm --version 8.4.0

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.

[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ó 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.

Marcus Chen
A Szerzőről Marcus Chen

Senior mobile architect with a decade of cross-platform experience. Spent the last five years going deep on .NET MAUI in production.