← Tüm yazılar

WPF ve Prism: kurulumdan MVVM, komut ve region gezinmesine

PrismApplication, DryIoc, ViewModelLocator, BindableBase, DelegateCommand ve region gezinmesini çalışan küçük bir WPF örneği üzerinden kurmak.

t.Turgay
WPF ve Prism: kurulumdan MVVM, komut ve region gezinmesine

Bir WPF ekranının ilk düğmesini code-behind içinde çalıştırmak kolay. Ekrana ikinci görünüm, ortak servis, kaydetme durumu ve birden fazla modül eklendiğinde asıl soru kodun nerede duracağı oluyor. Prism bu noktada MVVM, bağımlılık çözümleme, komut ve region gezinmesini aynı uygulama omurgasında birleştiriyor. İş kuralını ortadan kaldırmıyor; onu pencere olaylarının arasından çıkaracak yer sağlıyor.

Bu yazı için küçük, bağımsız bir not uygulaması hazırladım. Örnek .NET 10 WPF ve Prism.DryIoc 9.0.537 kullanıyor. Sitedeki Angular/.NET API mimarisine Prism eklemiyorum; WPF örneği ayrı. Arşivdeki her masaüstü projesinin Prism kullandığı gibi doğrulanmamış bir iddiam da yok. Buradaki amaç kütüphaneyi sıfırdan anlaşılır bir akışla kullanmak.

Paketi eklemek ve başlangıç penceresini Prism’e bırakmak

dotnet new wpf -n PrismNotes --framework net10.0
cd PrismNotes
dotnet add package Prism.DryIoc --version 9.0.537

WPF çalıştırmak için Windows gerekir. DryIoc seçimi bağımlılık container’ını belirler; aynı uygulamada Unity ve DryIoc paketlerini birlikte kurmuyorum. Prism 9 Community/Commercial lisansına tabidir. Bir projeye eklemeden önce sürümün kendi lisans koşulları değerlendirilmelidir; eski sürümlerin lisansını güncel sürüme otomatik taşımıyorum.

<prism:PrismApplication x:Class="PrismNotes.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:prism="http://prismlibrary.com/">
    <Application.Resources />
</prism:PrismApplication>

App.xaml artık Application yerine PrismApplication kökünü kullanıyor. StartupUri kaldırılıyor; ana pencereyi CreateShell oluşturacak. İkisini birden bırakmak iki pencere üretmeye kadar uzanan bir başlangıç karışıklığına neden olabilir. x:Class ile C# namespace/sınıfı aynı olmalı; XAML ve code-behind farklı taban sınıflarla devam edemez.

using System.Windows;
using Prism.DryIoc;
using Prism.Ioc;
using PrismNotes.Services;
using PrismNotes.ViewModels;
using PrismNotes.Views;

namespace PrismNotes;

public partial class App : PrismApplication
{
    protected override Window CreateShell() => Container.Resolve<MainWindow>();

    protected override void RegisterTypes(IContainerRegistry registry)
    {
        registry.RegisterSingleton<INoteStore, MemoryNoteStore>();
        registry.RegisterForNavigation<NotesView, NotesViewModel>("Notes");
    }
}

CreateShell içindeki Resolve, uygulamanın composition root’undaki bilinçli çözümleme noktasıdır. ViewModel’in içinde sürekli Container.Resolve çağırmak yerine bağımlılıkları constructor’dan alıyorum. RegisterSingleton ile INoteStore aynı uygulama ömründe ortak kalır; NotesView ve NotesViewModel ise gezinme hedefi olarak Notes adıyla kayıtlıdır. Ekranı veritabanı bağlantısına doğrudan bağlamak bu örneğin yaptığı bir şey değil.

Shell sadece yerleştirme alanını bilir

<Window x:Class="PrismNotes.Views.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:prism="http://prismlibrary.com/"
    prism:ViewModelLocator.AutoWireViewModel="True"
    Title="Prism Notes" Width="640" Height="420">
    <DockPanel Margin="24">
        <Button DockPanel.Dock="Top" Content="Notlar / Notes" Margin="0,0,0,16"
            Command="{Binding OpenNotesCommand}" />
        <ContentControl prism:RegionManager.RegionName="MainRegion" />
    </DockPanel>
</Window>

ContentControl üzerindeki MainRegion, görünümlerin yerleştirileceği alanın adıdır. Region bir Window değildir; uygulama kabuğunun içindeki sunum alanıdır. Notlar düğmesi bir görünümü bu alana yönlendirir. Ana pencerenin kullanıcı kontrolünü new ile üretip başka ekranların kontrollerine erişmesine ihtiyaç yok.

AutoWireViewModel, ViewModelLocator’ı açıkça etkinleştiriyor. Örnekte PrismNotes.Views.MainWindow ile PrismNotes.ViewModels.MainWindowViewModel isimleri aynı convention’ı izler. Klasörün adı tek başına yetmez; namespace de doğru olmalıdır. Adlandırma farklıysa eşlemeyi açıkça kaydetmek gerekir; sessizce new MainWindowViewModel() ile DataContext atamak DI zincirini devre dışı bırakabilir.

Depo sözleşmesi ve basit bellek uygulaması

INoteStore.cs

namespace PrismNotes.Services;

public interface INoteStore
{
    IReadOnlyList<string> Read();
    void Add(string title);
}

MemoryNoteStore.cs

namespace PrismNotes.Services;

public sealed class MemoryNoteStore : INoteStore
{
    private readonly List<string> notes = [];
    public IReadOnlyList<string> Read() => notes.ToArray();
    public void Add(string title)
    {
        if (string.IsNullOrWhiteSpace(title)) throw new ArgumentException("A title is required.");
        notes.Add(title.Trim());
    }
}

Bu iki sınıf Services klasöründe ayrı dosyalarda bulunur. Gerçek API adapter’ına geçildiğinde ViewModel aynı INoteStore sözleşmesini kullanmaya devam eder; bellekteki uygulama yalnız öğretici örneği bağımsız tutar.

BindableBase ve CanExecute birlikte çalışsın

using System.Collections.ObjectModel;
using Prism.Commands;
using Prism.Mvvm;
using PrismNotes.Services;

namespace PrismNotes.ViewModels;

public sealed class NotesViewModel : BindableBase
{
    private string title = "";
    public string Title { get => title; set => SetProperty(ref title, value); }
    public ObservableCollection<string> Notes { get; }
    public DelegateCommand AddCommand { get; }

    public NotesViewModel(INoteStore store)
    {
        Notes = new ObservableCollection<string>(store.Read());
        AddCommand = new DelegateCommand(() =>
        {
            var note = Title.Trim();
            store.Add(note);
            Notes.Add(note);
            Title = "";
        }, () => !string.IsNullOrWhiteSpace(Title)).ObservesProperty(() => Title);
    }
}

Title setter’ındaki SetProperty, değer değiştiğinde PropertyChanged üretir. Notes ObservableCollection olduğundan eklenen eleman arayüzde görünür. AddCommand’ın ikinci parametresi düğmenin ne zaman kullanılabileceğini söyler; ObservesProperty, Title değiştiğinde bu koşulu yeniden sorgulatır. Koşulu bir kez hesaplayıp düğmenin kendiliğinden fark etmesini beklemiyorum.

Komut başlığı kırpar, store’a ekler, görünür listeyi günceller ve alanı temizler. Gerçek uygulamada servis de girdiyi doğrular; düğmenin pasif olması tek başına iş kuralı değildir. MemoryNoteStore da boş başlığı reddeder. Bu küçük depo yalnız bellektedir; uygulama kapanınca veri kaybolur. Singleton olması, kalıcı veritabanı olduğu anlamına gelmiyor.

<UserControl x:Class="PrismNotes.Views.NotesView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:prism="http://prismlibrary.com/"
    prism:ViewModelLocator.AutoWireViewModel="True">
    <StackPanel>
        <TextBlock Text="Yeni not / New note" />
        <TextBox Margin="0,8" Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" />
        <Button Content="Ekle / Add" Command="{Binding AddCommand}" />
        <ItemsControl Margin="0,16,0,0" ItemsSource="{Binding Notes}" />
    </StackPanel>
</UserControl>

TextBox üzerinde UpdateSourceTrigger=PropertyChanged kullanınca başlık her tuşta ViewModel’e gider; kullanıcı alanı terk etmeden Ekle düğmesi etkinleşebilir. Command bağlama iş davranışını taşırken XAML sunumu taşır. Küçük InitializeComponent code-behind’ları normaldir; MVVM, code-behind dosyasını fiziksel olarak yok etme yarışması değil.

Region gezinmesinde kayıt adı ve alan adı eşleşmeli

using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation.Regions;

namespace PrismNotes.ViewModels;

public sealed class MainWindowViewModel : BindableBase
{
    public DelegateCommand OpenNotesCommand { get; }

    public MainWindowViewModel(IRegionManager regions)
    {
        OpenNotesCommand = new DelegateCommand(() => regions.RequestNavigate("MainRegion", "Notes"));
    }
}

Örnekte düğmeye basılınca MainRegion içine Notes istenir. Prism 9 örneğinde region türleri Prism.Navigation.Regions namespace’indedir; eski Prism.Regions import’unu sürüm kontrol etmeden kopyalamıyorum. RegisterForNavigation kaydındaki Notes ile RequestNavigate hedefi, ContentControl üzerindeki MainRegion ile çağrıdaki alan adı aynı olmalı.

Daha büyük bir akışta NavigationParameters ile kayıt kimliği taşınır; bütün ViewModel’i parametre diye dolaştırmak yerine hedef servis üzerinden veriyi okur. Kaydedilmemiş düzenlemeyi terk etme veya görünümün yeniden kullanılması gibi kararlar ilgili navigation sözleşmeleriyle ele alınır. Bu minimal not ekranında böyle bir editör yaşam döngüsü uygulanmış değil; gezinme örneğine ait olmayan davranışı varmış gibi sunmuyorum.

Modül ne zaman anlamlı olur?

using Prism.Ioc;
using Prism.Modularity;
using PrismNotes.Views;
using PrismNotes.ViewModels;

public sealed class NotesModule : IModule
{
    public void RegisterTypes(IContainerRegistry registry) =>
        registry.RegisterForNavigation<NotesView, NotesViewModel>("Notes");

    public void OnInitialized(IContainerProvider provider) { }
}

// In App, when moving the Notes registration into the module:
// protected override void ConfigureModuleCatalog(IModuleCatalog catalog)
//     => catalog.AddModule<NotesModule>();

Notlar özelliği ayrı assembly ve bağımlılık sınırı kazandığında bu kayıtlar IModule içine taşınabilir. O zaman aynı Notes kaydını hem App.RegisterTypes hem modül içinde tekrar etmiyorum. RegisterTypes servis/görünüm kayıtlarını, OnInitialized gerekiyorsa başlangıç işini taşır. Birkaç sınıfı başka klasöre almak tek başına modüler uygulama kurmaz; sözleşmenin ve bağımlılık yönünün de ayrılması gerekir.

EventAggregator ile haber vermek, işlemi saklamak değildir

using Prism.Events;

public sealed class NoteSaved : PubSubEvent<string> { }

// Publisher: called after a successful save.
events.GetEvent<NoteSaved>().Publish(noteId);

// Subscriber: events is an injected IEventAggregator.
var token = events.GetEvent<NoteSaved>().Subscribe(
    id => Status = $"Saved: {id}", ThreadOption.UIThread);

// When this subscriber is disposed or no longer needs notifications:
events.GetEvent<NoteSaved>().Unsubscribe(token);

Bu ek parça, bir özellikteki başarılı kayıttan sonra başka bir panelin durumunu güncelleyebileceğini gösteriyor. IEventAggregator constructor’dan alınır; UIThread aboneliği UI bağlamında kurulan uygulamada kullanılır. Aboneliğin ve token’ın ömrü düşünülür; görünüm kapandıktan sonra dinlemek gerekmiyorsa abonelik kaldırılır. Event’i her servis çağrısının yerine koymak akışı görünmez hâle getirir.

PubSubEvent süreç içi iletişimdir; kalıcı mesaj kuyruğu veya veritabanı transaction’ı değildir. Başarısız bir kaydı başarılıymış gibi Publish etmek de onu düzeltmez. İstek/yanıt gereken davranışta servis çağrısı, gevşek bağlı bildirimde event kullanıyorum. EventAggregator ve modül parçaları açıklayıcı genişletmelerdir; çalıştırılabilir küçük örnekteki özellik listesine dahil değiller.

Asenkron iş ve sınanabilir sınırlar

Gerçek bir API kaydına geçildiğinde DelegateCommand içine async lambda yazıp bütün hata akışını unutmak doğru bir genişletme olmaz. Asenkron komut, IsBusy, tekrar basmayı engelleme, iptal ve hata geri bildirimi birlikte tasarlanmalı. UI koleksiyonunu arka plan thread’inden güncellemek de sadece await yazınca güvenli hâle gelmez. Bu örneğin senkron bellek deposu o sorunları çözmüş sayılmaz.

  • Boş/yalnız boşluk başlıkta CanExecute false; dolu başlıkta true olmalı.
  • Ekleme kırpılmış başlığı listeye/store’a yazmalı ve Title alanını temizlemeli.
  • Yeni ViewModel aynı singleton store’dan önceki notu okuyabilmeli.
  • XAML derlenmeli; Notes kaydı, MainRegion adı ve namespace convention’ı korunmalı.

Kod bloklarını, kurulum adımlarında oluşturduğun WPF projesindeki ilgili dosyalara yerleştirerek örneği çalıştırabilirsin. Kalıcı sürümde INoteStore bir API adapter’ına dönüşebilir; Supabase secret anahtarını veya SQL parolasını masaüstü istemciye dağıtmam. Prism’in kazancı daha çok sihirli sınıf eklemek değil, değişen davranışı hangi sınırdan değiştireceğimin belli olması.

Başvurular

✳ Bu yörüngenin sonuna geldin.
Diğer yazıları keşfet →
turgay.Biraz kod, biraz piksel, bolca merak.© 2026 · Dünya’dan sevgilerle ✳