.NET MAUI MVVM: CommunityToolkit.Mvvm ile Modern Yaklaşım (2026)

CommunityToolkit.Mvvm 8.4 ve .NET MAUI 9 ile MVVM desenini sıfırdan kurun. ObservableProperty, RelayCommand, WeakReferenceMessenger, Shell navigasyonu ve DI için pratik kod örnekleri ve sık yapılan hatalar.

.NET MAUI MVVM Toolkit Rehberi (2026)

Güncellendi: 16 Haziran 2026

.NET MAUI'de MVVM, CommunityToolkit.Mvvm paketinin kaynak üreteçleri (source generators) sayesinde yalnızca birkaç öznitelikle uygulanabilen modern bir mimari desendir. [ObservableProperty] ve [RelayCommand] öznitelikleri, eskiden onlarca satır INotifyPropertyChanged kodu gerektiren ViewModel sınıflarını üç-beş satıra indirir. Geçen ay bir kurumsal saha uygulamasını Xamarin.Forms'tan MAUI'ye taşırken, ViewModel katmanının yaklaşık %60'ını bu paket sayesinde sildiğimi söyleyebilirim. Bu rehberde, .NET MAUI 9 ve CommunityToolkit.Mvvm 8.4 sürümüyle MVVM desenini sıfırdan kuruyor; Shell navigasyonu, Dependency Injection ve mesajlaşma gibi gerçek dünya senaryolarını kod örnekleriyle ele alıyoruz.

  • CommunityToolkit.Mvvm 8.4, .NET MAUI 9 ile tam uyumlu çalışır ve Roslyn kaynak üreteçleri ile derleme zamanında INotifyPropertyChanged kodu üretir.
  • [ObservableProperty] özniteliği özel bir alandan (örn. _name) otomatik olarak public bir özellik üretir ve değişiklik bildirimini yönetir.
  • [RelayCommand] ile işaretlenen bir metot, görünümün Command bağlamalarına otomatik olarak hazır ICommand örneği sağlar; CanExecute ve async destekler.
  • WeakReferenceMessenger, gevşek bağlı ViewModel'ler arasında bellek sızıntısı oluşturmadan iletişim sağlar.
  • ViewModel'leri MauiAppBuilder.Services üzerinden DI konteynerına kaydetmek, test edilebilirlik ve Shell navigasyonu için önerilen yaklaşımdır.

CommunityToolkit.Mvvm nedir ve neden kullanmalısınız?

CommunityToolkit.Mvvm, Microsoft tarafından sürdürülen, MVVM desenini .NET uygulamalarında uygulamayı basitleştiren açık kaynaklı bir kütüphanedir. 2022'de duyurulan ve önceden Microsoft.Toolkit.Mvvm adıyla bilinen bu paket, Roslyn kaynak üreteçlerini kullanarak INotifyPropertyChanged, ICommand ve mesajlaşma gibi tekrar eden MVVM altyapısını derleme zamanında otomatik üretir. Sonuç, klasik MVVM kodunun yaklaşık üçte birine kadar inen, ama davranış olarak aynı şekilde çalışan ViewModel sınıflarıdır.

.NET MAUI tarafında bu paket, hem performans hem geliştirici deneyimi açısından net bir kazanç sağlar. Reflection kullanmadığı için AOT (Ahead-of-Time) derleme ve .NET MAUI performans optimizasyonu hedefleriyle uyumludur; iOS'ta zorunlu olan AOT senaryolarında herhangi bir sorun çıkarmaz. Ayrıca MvvmLight, Prism veya ReactiveUI gibi alternatiflerin aksine ek bir runtime bağımlılığı taşımaz; tüm üretilen kod sizin assembly'nizin içinde yer alır.

Bu paketi seçmenin pratik nedenleri şunlardır. Birincisi, Microsoft tarafından desteklendiği için .NET sürüm yükseltmelerinde uyumluluk riskinin düşük olmasıdır. İkincisi, Visual Studio ve Rider gibi IDE'lerin üretilen koda tam IntelliSense desteği sunmasıdır. Üçüncüsü ise CommunityToolkit ekosistemindeki diğer paketlerle (örneğin CommunityToolkit.Maui) sorunsuz çalışmasıdır. CommunityToolkit/dotnet GitHub deposu aktif olarak geliştirilmekte ve 8.4 sürümüyle birlikte ObservablePropertyAttribute'un partial özelliklere uygulanabilmesi gibi C# 13 yeniliklerini de desteklemektedir.

.NET MAUI projesine kurulum ve yapılandırma

Şimdi pratik tarafa geçelim. CommunityToolkit.Mvvm'in .NET MAUI projesine eklenmesi tek bir NuGet komutuyla gerçekleşir. Aşağıdaki adımlar, .NET 9 SDK ve Visual Studio 2022 17.12 (veya üstü) ya da Rider 2025.1 ile çalışan bir proje varsayılarak hazırlanmıştır.

Önce paketi proje dosyanıza ekleyin:

<ItemGroup>
  <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
</ItemGroup>

Veya CLI üzerinden:

dotnet add package CommunityToolkit.Mvvm --version 8.4.0

Paket yüklendikten sonra ek bir başlatma kodu gerekmez. Tüm öznitelikler ve sınıflar CommunityToolkit.Mvvm.ComponentModel, CommunityToolkit.Mvvm.Input ve CommunityToolkit.Mvvm.Messaging ad alanları altında doğrudan kullanılabilir hale gelir. Ancak proje dosyanızda <LangVersion> değerinin latest veya en az 11 olduğundan emin olun; kaynak üreteçlerinin partial sınıflar üretebilmesi için C# 11+ gerekir.

Ek olarak, ViewModel sınıflarınızı tutmak için projenizde ViewModels/ klasörü açmanız ve Models/, Views/, Services/ klasörlerini ayırmanız önerilir. .NET MAUI şablonu varsayılan olarak bu yapıyı oluşturmaz; klasör organizasyonu, MVVM disiplinini takım genelinde sürdürmek için kritik bir adımdır. Klasör adlandırmasında Pages/ yerine Views/ kullanmak, Shell tabanlı navigasyonla daha tutarlıdır.

ObservableObject ve ObservableProperty ile ViewModel

MVVM'in temeli, ViewModel'in özelliklerindeki değişiklikleri görünüme bildirmesidir. CommunityToolkit.Mvvm bunu iki katmanda sağlar: ObservableObject temel sınıfı ve [ObservableProperty] özniteliği. ObservableObject, INotifyPropertyChanged ve INotifyPropertyChanging arayüzlerini önceden uygular; siz yalnızca özel alanları tanımlar ve özniteliği eklersiniz.

İşte tipik bir login ekranı ViewModel'i:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace MyApp.ViewModels;

public partial class LoginViewModel : ObservableObject
{
    [ObservableProperty]
    private string _email = string.Empty;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(IsFormValid))]
    private string _password = string.Empty;

    [ObservableProperty]
    private bool _isBusy;

    public bool IsFormValid =>
        !string.IsNullOrWhiteSpace(Email) &&
        Password?.Length >= 6 &&
        !IsBusy;
}

Burada kaynak üreteci, _email alanı için arka planda public string Email { get; set; } özelliğini, ilgili OnEmailChanging ve OnEmailChanged partial metotlarını ve PropertyChanged tetiklemesini üretir. [NotifyPropertyChangedFor(nameof(IsFormValid))] özniteliği ise Password değiştiğinde IsFormValid için de bir bildirim yayınlanmasını sağlar. Böylece XAML tarafındaki IsEnabled="{Binding IsFormValid}" bağlaması otomatik güncellenir.

Üretilen partial metotları doldurarak yan etki ekleyebilirsiniz:

partial void OnEmailChanged(string value)
{
    EmailValidationError = IsValidEmail(value) ? null : "Geçersiz e-posta";
}

Bu yaklaşım, eski set { _email = value; OnPropertyChanged(); } kalıbına kıyasla yalnızca daha kısa değil, aynı zamanda daha az hata yapılabilir bir yapıdır. Backing field adlandırma kuralı (_camelCase veya m_camelCase) doğru takip edildiğinde üretilen özellik adı Pascal case olarak otomatik türetilir. Microsoft'un ObservableProperty kaynak üreteci belgesinde daha gelişmiş senaryoları (validation öznitelikleri, broadcast, custom setter) bulabilirsiniz.

RelayCommand ile komut yönetimi ve async destek

XAML'de düğme tıklamaları için ICommand uygulamak, eski MVVM kütüphanelerinde sıkça yazılan boilerplate'in büyük bölümünü oluşturuyordu. [RelayCommand] özniteliği bu sorunu çözer: işaretlediğiniz metot için otomatik olarak bir IRelayCommand özelliği üretilir.

public partial class LoginViewModel : ObservableObject
{
    private readonly IAuthService _auth;
    private readonly INavigationService _nav;

    public LoginViewModel(IAuthService auth, INavigationService nav)
    {
        _auth = auth;
        _nav = nav;
    }

    [ObservableProperty]
    private string _email = string.Empty;

    [ObservableProperty]
    private string _password = string.Empty;

    [ObservableProperty]
    private bool _isBusy;

    [RelayCommand(CanExecute = nameof(CanSignIn))]
    private async Task SignInAsync(CancellationToken ct)
    {
        IsBusy = true;
        try
        {
            var result = await _auth.SignInAsync(Email, Password, ct);
            if (result.Success)
                await _nav.GoToAsync("//home");
            else
                ErrorMessage = result.ErrorMessage;
        }
        finally
        {
            IsBusy = false;
        }
    }

    private bool CanSignIn() =>
        !IsBusy &&
        !string.IsNullOrWhiteSpace(Email) &&
        !string.IsNullOrWhiteSpace(Password);
}

Kaynak üreteci burada SignInCommand adlı bir IAsyncRelayCommand üretir ve XAML'de Command="{Binding SignInCommand}" şeklinde bağlayabilirsiniz. CancellationToken parametresi otomatik olarak desteklenir; komut yeniden tetiklendiğinde önceki çağrı iptal edilir. Bu özellik, kullanıcı düğmeye iki kez bastığında veya sayfa kapatıldığında istenmeyen ağ isteklerinin sürmesini engeller. Doğrusunu söylemek gerekirse, bir önceki projede tam olarak bu CancellationToken davranışı, "neden iki kez login deneyince iki request gidiyor" şeklindeki sinir bozucu bir bug'ı tek satırda kapatmıştı.

CanExecute parametresi, komutun ne zaman aktif olacağını belirleyen metodu işaret eder. Ancak CanExecute'un yeniden değerlendirilmesi için bağlı özelliklerin değiştiğinde komutu uyarmamız gerekir. Bunu [NotifyCanExecuteChangedFor(nameof(SignInCommand))] özniteliğini bağımlılık özelliklere ekleyerek yapabilirsiniz:

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SignInCommand))]
private string _email = string.Empty;

Böylece Email her değiştiğinde komutun CanExecute değerlendirmesi tekrar tetiklenir ve düğme aktif/pasif durumu otomatik güncellenir.

WeakReferenceMessenger ile ViewModel'ler arası iletişim

ViewModel'lerin birbirine doğrudan referans tutmaması, MVVM'in temel ilkelerinden biridir. CommunityToolkit.Mvvm bu sorunu WeakReferenceMessenger (varsayılan) ve StrongReferenceMessenger sınıflarıyla çözer. Birincisi, dinleyicileri zayıf referans ile tutar; abone olmayı unutsanız bile bellek sızıntısı oluşmaz.

// Mesaj sınıfı
public sealed record UserLoggedInMessage(User User);

// Yayıncı (Publisher)
public partial class LoginViewModel : ObservableObject
{
    [RelayCommand]
    private async Task SignInAsync()
    {
        var user = await _auth.SignInAsync(Email, Password);
        WeakReferenceMessenger.Default.Send(new UserLoggedInMessage(user));
    }
}

// Abone (Subscriber)
public partial class ShellViewModel : ObservableObject, IRecipient<UserLoggedInMessage>
{
    public ShellViewModel()
    {
        WeakReferenceMessenger.Default.Register(this);
    }

    public void Receive(UserLoggedInMessage message)
    {
        WelcomeText = $"Hoş geldin, {message.User.DisplayName}";
    }
}

Bu desen özellikle alt-üst ekran iletişiminde, arka plan servislerden UI bildirimleri yayınlarken veya bir Tab'in başka bir Tab'i veri yenilemesi için tetiklemesi gerektiğinde kullanışlıdır. StrongReferenceMessenger'a yalnızca dinleyicinin ömrü kontrol altındaysa ve performans kritikse başvurun; tipik MAUI senaryolarında varsayılan zayıf referans yeterlidir.

.NET MAUI handler mimarisi üzerinden özel kontroller geliştirirken, bu kontrollerin ViewModel olaylarını mesajlaşma ile yakalayıp arayüze yansıtmak temiz bir mimari sağlar. Mesajlar, payload taşıyabilen ValueChangedMessage<T> tabanlı kayıtlar olabileceği gibi, basit boolean sinyaller için boş record türleri de kullanılabilir.

Dependency Injection ile ViewModel kaydı

.NET MAUI, Microsoft.Extensions.DependencyInjection tabanlı bir konteyner ile gelir ve MauiAppBuilder üzerinden yapılandırılır. ViewModel'leri ve sayfaları bu konteynere kaydetmek, hem test edilebilirlik hem de Shell navigasyonu için kritiktir.

// MauiProgram.cs
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .ConfigureFonts(fonts =>
        {
            fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
        });

    // Servisler (tek örnek, uygulama ömrü)
    builder.Services.AddSingleton<IAuthService, AuthService>();
    builder.Services.AddSingleton<INavigationService, ShellNavigationService>();
    builder.Services.AddSingleton<HttpClient>();

    // ViewModel'ler (her sayfa için yeni örnek)
    builder.Services.AddTransient<LoginViewModel>();
    builder.Services.AddTransient<HomeViewModel>();
    builder.Services.AddTransient<ProfileViewModel>();

    // Sayfalar
    builder.Services.AddTransient<LoginPage>();
    builder.Services.AddTransient<HomePage>();
    builder.Services.AddTransient<ProfilePage>();

    return builder.Build();
}

Sayfa sınıflarında ViewModel'i constructor enjeksiyonu ile alın ve BindingContext'e atayın:

public partial class LoginPage : ContentPage
{
    public LoginPage(LoginViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = viewModel;
    }
}

Singleton, Transient ve Scoped ömürlerini bilinçli seçin. Kimlik doğrulama veya HTTP istemcisi gibi paylaşılan kaynaklar Singleton, ekran başına state taşıyan ViewModel'ler Transient olmalıdır. Bir ViewModel'i Singleton kaydederseniz, kullanıcı sayfayı kapatıp tekrar açtığında eski state ile karşılaşır (ve bu nadiren istenen bir davranıştır).

Shell navigasyonu ve parametre geçişi

Shell, .NET MAUI'nin önerilen navigasyon altyapısıdır ve MVVM ile birlikte iyi uyum sağlar. URL benzeri rotalar ile sayfalar arası geçiş yapar ve IQueryAttributable arayüzü üzerinden ViewModel'lere parametre aktarır.

Önce rotaları kaydedin:

// AppShell.xaml.cs
public AppShell()
{
    InitializeComponent();
    Routing.RegisterRoute("product/details", typeof(ProductDetailsPage));
}

ViewModel içinden navigasyon:

[RelayCommand]
private async Task OpenProductAsync(Product product)
{
    var parameters = new Dictionary<string, object>
    {
        ["productId"] = product.Id,
        ["product"] = product
    };
    await Shell.Current.GoToAsync("product/details", parameters);
}

Hedef ViewModel parametreleri [QueryProperty] öznitelikleri ile alır:

[QueryProperty(nameof(ProductId), "productId")]
[QueryProperty(nameof(Product), "product")]
public partial class ProductDetailsViewModel : ObservableObject
{
    [ObservableProperty]
    private int _productId;

    [ObservableProperty]
    private Product? _product;

    partial void OnProductIdChanged(int value)
    {
        // Veri yükleme tetiklenir
        LoadDetailsCommand.Execute(null);
    }
}

Bu yaklaşım, derin bağlantı (deep linking) senaryolarında da çalışır. app://product/details?productId=42 URL'i ile uygulama açıldığında aynı parametre akışı kullanılır. .NET MAUI Blazor Hybrid projelerinde dahi BlazorWebView dışındaki sayfalar için Shell navigasyonu MVVM ile aynı şekilde işler. Microsoft'un Shell navigasyon belgesi, geri yığını yönetme, modal sayfalar ve rota gözlemcileri gibi gelişmiş konuları detaylı işler.

Yaygın hatalar ve performans önerileri

CommunityToolkit.Mvvm pratik olsa da, MAUI uygulamalarında sık karşılaşılan birkaç tuzak vardır. Birincisi, [ObservableProperty] ile işaretlenen alanların doğrudan değiştirilmesidir. Eğer _email = "[email protected]"; yazarsanız PropertyChanged tetiklenmez ve UI güncellenmez. Her zaman üretilen özelliği (Email = "...") kullanın. Açıkçası bu hatayı bir keresinde stage ortamına bile taşımıştım; "binding neden çalışmıyor" sorusuyla yarım gün kaybetmek istemiyorsanız Pascal-case özelliği kullanmaya dikkat edin.

İkinci yaygın hata, ViewModel constructor'ında ağır işlemler yapmaktır. Shell, sayfa açılışında ViewModel'i hemen oluşturur; constructor uzun sürerse navigasyon kasılır. Veri yüklemeyi OnAppearing içinde tetiklenen bir InitializeAsync komutuna taşıyın. ContentPage'inizin OnNavigatedTo veya Appearing olayında ViewModel üzerindeki bir komutu çağırmak iyi bir desendir.

Üçüncü konu, WeakReferenceMessenger aboneliklerinin yönetilmemesidir. Zayıf referanslar bellek sızıntısını engeller, ancak Recipient nesnesinin Garbage Collector tarafından erkenden toplanması durumunda mesajları kaçırabilirsiniz. Singleton ViewModel'lerde sorun değildir; Transient ViewModel'lerde abone olmadan önce ömür stratejisini değerlendirin.

Performans tarafında, çok sayıda ObservableProperty içeren ViewModel'lerde tek tek atama yerine SetProperty'yi batch hale getiren bir yardımcı metot kullanmak yararlı olabilir. CollectionView bağlamalarında ise ViewModel listelerinin ObservableCollection<T> olduğundan emin olun; List<T> değişiklikleri bildirmez. Performans yan etkilerini .NET MAUI başlangıç ve UI optimizasyonu rehberinde daha derin inceleyebilirsiniz.

Son olarak, kaynak üreteci tarafından üretilen kodu görüntülemek isterseniz Visual Studio'da Solution Explorer altında "Dependencies, Analyzers, CommunityToolkit.Mvvm.SourceGenerators" düğümünü genişleterek üretilen .g.cs dosyalarını okuyabilirsiniz. Bu, beklenmedik davranışları teşhis etmenin en hızlı yoludur.

Sıkça Sorulan Sorular

CommunityToolkit.Mvvm, MAUI'nin yerleşik BindableObject sınıfının yerini mi alır?

Hayır. BindableObject, BindableProperty sistemiyle birlikte .NET MAUI UI elemanlarının (Control, View) temel sınıfıdır. CommunityToolkit.Mvvm'in ObservableObject sınıfı ise ViewModel katmanı için tasarlanmıştır ve yalnızca INotifyPropertyChanged sağlar. İkisi farklı katmanlarda çalışır ve birbirinin yerine geçmez.

MVVM ve MVU desenleri arasındaki fark nedir?

MVVM, View ile ViewModel arasında çift yönlü veri bağlama üzerine kuruludur; MAUI'nin XAML tabanlı UI yaklaşımı bu deseni doğal olarak destekler. MVU (Model-View-Update) ise tek yönlü veri akışı sunar ve Comet veya .NET MAUI Markup gibi C# tabanlı UI yaklaşımlarında kullanılır. .NET MAUI XAML projeleriniz için MVVM hala önerilen desendir.

ObservableProperty kullanırken neden partial sınıf gereklidir?

Çünkü kaynak üreteci, sizin yazdığınız sınıf dosyasından ayrı bir .g.cs dosyası oluşturup üretilen özellikleri orada tanımlar. C# derleyicisinin iki dosyayı tek bir sınıf olarak birleştirebilmesi için partial anahtar kelimesi şarttır.

RelayCommand ile standart ICommand arasında ne fark vardır?

ICommand, .NET'in kendi temel arayüzüdür ve Execute, CanExecute, CanExecuteChanged tanımlar. RelayCommand bu arayüzün hazır bir uygulamasıdır; async desteği, CancellationToken yönetimi ve NotifyCanExecuteChanged gibi MAUI/MVVM senaryolarına özgü kolaylıkları ekler.

CommunityToolkit.Mvvm AOT derleme ile çalışır mı?

Evet. Paket reflection kullanmadan, derleme zamanında kaynak üretici aracılığıyla çalışır. Bu sayede iOS'ta zorunlu AOT senaryolarında ve .NET 9'un Native AOT derlemesinde sorunsuz çalışır. ReactiveUI gibi reflection ağırlıklı alternatiflerin aksine, trimming sırasında özelliklerin yanlışlıkla kaldırılma riski yoktur.

Editorial Team
Yazar Hakkında Editorial Team

Our team of expert writers and editors.