Mert Özen Her Satırda Daha İleri
Yazılım Mimarisi UI Mimari Pattern'leri — #05

Framework'lerde Karşılıkları: Angular MVC mi, MVVM mi? React, Blazor, WPF ve ASP.NET Core

Mert Özen 8 Kas 2026 11 dk 2 görüntülenme
Framework'lerde Karşılıkları: Angular MVC mi, MVVM mi? React, Blazor, WPF ve ASP.NET Core

Angular ne klasik MVC ne de saf MVVM'dir: component class bir ViewModel gibi view state ve view logic taşır, template ona bağlanır, signal'lar ise UI'ı React'teki gibi state'ten türetir. Aynı sipariş formunu React, Blazor ve Razor Pages ile de yazıp çalıştırıyor, framework'leri etiketle değil üç soruyla (state nerede, senkronizasyon nasıl, view'sız test mümkün mü) okumayı gösteriyoruz.

Sürüm notu: Bu yazıdaki örnekler Angular 22 ve TypeScript ile, karşılaştırma örnekleri .NET 10 ile denenmiştir.

Önce Tahmin Et

Angular öğrenen hemen herkes bir noktada bu soruyu sorar: "Angular MVC (Model-View-Controller) mi, MVVM (Model-View-ViewModel) mi?" Forumlardaki cevaplar üçe ayrılır. Bir grup "MVC; service'ler model, template view, component controller" der. Bir grup "MVVM; component class ViewModel, template view" der. Üçüncü grup "ikisi de değil, component-based" der.

Aynı soruyu beş framework için sor: Angular, React, Blazor, WPF (Windows Presentation Foundation) ve ASP.NET Core MVC. Her biri için kafandaki etiketi yaz. Sonra bir adım geri çekil ve şunu sor: bu etiketlerden biri yanlış çıkarsa, yazdığın kodda neyi farklı yaparsın? Bu yazının iddiası, cevabın çoğu zaman "hiçbir şeyi" olduğu, ama etiketin arkasındaki üç sorunun cevabının her şeyi değiştirdiği.

Etiket Neden Kafa Karıştırıyor?

Önceki yazılarda gördük ki MVC kelimesi bile iki farklı akışı anlatıyor: Smalltalk'taki, view'ın model'i observe ettiği MVC ve web'deki, her request'te bir kez render edilen Web MVC. MVVM ile Presentation Model aynı fikrin iki adı. MVP'nin (Model-View-Presenter) kendisi Fowler tarafından Passive View ve Supervising Controller diye ikiye bölündü. Pattern adları kesin sınırları olan kategoriler değil, bir aile benzerliği.

Framework'ler de bu etiketleri farklı dönemlerde farklı amaçlarla kullandı. AngularJS'in ilk yıllarında topluluk "MVC mi, MVVM mi?" tartışmasına o kadar gömüldü ki ekipten Igor Minar 2012'de AngularJS'i bir MVW (Model-View-Whatever) framework'ü ilan etti; "Whatever", yani "senin için ne işe yarıyorsa". Gerekçesi, geliştiricilerin MVC, MVP, MVVM gibi etiketler üzerine tartışmak yerine separation of concerns'e uyan iyi tasarlanmış uygulamalar yazmasını tercih etmesiydi. Bu yazı aynı sonuca varıyor, ama oraya bir araçla gitmek istiyor.

Üç Soruyla Bir Framework'ü Okumak

İlk dört yazının ortak ipliği üç soruydu. Bir framework'e baktığında etiketi bırakıp bunları sor:

  • View state ve view logic nerede yaşıyor? Event handler'da mı, presenter'da mı, ViewModel'de mi, component'te mi, yoksa request başına oluşturulan bir object'te mi?
  • View ile state nasıl senkron kalıyor? Observer ile mi, presenter'ın elle çağrılarıyla mı, binding ile mi, state değişince yeniden render ile mi, yoksa her request'te baştan render ile mi?
  • Presentation logic'i view olmadan test edebiliyor musun? Testin bir DOM (Document Object Model), bir tarayıcı ya da bir UI (User Interface) thread'i istiyor mu?

Bu üç sorunun cevabı, bir pattern adından çok daha fazla şey söylüyor: kodu nereye yazacağını, hataları nerede arayacağını ve testi nasıl kuracağını.

Angular: ViewModel Gibi Bir Component, Signal'larla Türeyen UI

Dördüncü yazıdaki OrderForm component'ini üç soruyla okuyalım. View state ve view logic component class'ında: quantityText ve saving signal'ları view state, total, error ve canPlaceOrder computed'ları view logic, placeOrder method'u command. Senkronizasyonu template binding'leri yapıyor; signal değişince Angular ilgili template'i yeniden değerlendiriyor ve Angular 22'de varsayılan olan OnPush stratejisi sayesinde yalnız etkilenen component'ler kontrol ediliyor. Test için TestBed.createComponent ile component'i oluşturup signal'ları DOM'a bakmadan okuyabildik.

Bu cevaplar Angular'ı Presentation Model ve MVVM ailesine yerleştiriyor: component class view'ı tanımıyor, template ona bağlanıyor. Ama iki fark var. Birincisi, Angular component'i aynı zamanda bir composition birimi: kendi input'ları, output'ları ve child component'leriyle bir ağacın parçası. WPF'teki ViewModel'in böyle bir rolü yok. İkincisi, signal'larla birlikte Angular'ın senkronizasyonu "bildirimi sen yap" modelinden "bağımlılığı framework izlesin" modeline geçti; bu, React'in "UI state'ten türer" yaklaşımına yakın. Change detection'ın eski hâli Angular serisinin "Angular'da Change Detection ve Zone.js: OnPush, ChangeDetectorRef ve NgZone" yazısında, yeni varsayılanlar "Angular 22: Varsayılan OnPush, Stable Signal Forms ve Angular 12 ile Proje Farkı" yazısında.

Peki "service'ler model" iddiası? Kısmen doğru. Dördüncü yazıdaki OrderApi service'i model tarafının, yani domain ve veri erişiminin, Angular'daki kapısı. Ama Angular'da service'ler başka işler de yapıyor: birden çok component'in paylaştığı view state'i tutan state service'ler aslında ViewModel'in bir parçası. Hangi service'in hangi role ait olduğunu ayırmak serinin yedinci ve dokuzuncu yazılarının konusu. Kısa cevap: Angular MVC değil, çünkü ortada input yorumlayan bir controller da, request karşılayan bir controller da yok. MVVM'e çok yakın, ama component ağacı ve signal'larla ondan fazlası.

React ve Blazor: UI = f(state)

React

React kendini bir pattern adıyla tanıtmıyor. Dokümantasyonu yaklaşımı deklaratif UI olarak anlatıyor: element'leri tek tek açıp kapatmak yerine component'in olabileceği durumları tarif ediyorsun ve React ekranı o duruma getiriyor. Toplulukta bu yaklaşım genelde UI = f(state) formülüyle anlatılıyor. Aynı sipariş formu, React 19.3 ile:

import { useState } from 'react';
import { catalog, parseQuantity, priceOrder } from '../domain/pricing';

export function OrderForm() {
  const [bookId, setBookId] = useState(1);
  const [quantityText, setQuantityText] = useState('1');

  // her render'da state'ten yeniden hesaplanır: UI = f(state)
  const check = parseQuantity(quantityText);
  const book = catalog.find(b => b.id === bookId)!;
  const summary = check.ok ? `Total: ${priceOrder(book, check.quantity).toFixed(2)}` : check.error;

  return (
    <form>
      <select value={bookId} onChange={e => setBookId(Number(e.target.value))}>
        {catalog.map(b => <option key={b.id} value={b.id}>{b.title}</option>)}
      </select>
      <input value={quantityText} onChange={e => setQuantityText(e.target.value)} />
      <p>{summary}</p>
      <button type="button" disabled={!check.ok}>Place order</button>
    </form>
  );
}
 ✓ src/p05/OrderForm.spec.tsx > React re-renders the form from state 9ms

 Test Files  1 passed (1)
      Tests  1 passed (1)

Üç soru: view state useState hook'larında, view logic ise her render'da yeniden çalışan düz kod. Ayrı bir ViewModel yok; component fonksiyonu hem view'ı hem view logic'i taşıyor. Senkronizasyon yeniden render: state değişince fonksiyon baştan çalışıyor ve React yeni sonucu öncekiyle karşılaştırıp DOM'da yalnız farkı uyguluyor. Test, view logic'i ayrıca çıkarmadıysan DOM üzerinden; bu yüzden testte input'un native setter'ı ve act gerekti. Logic büyüdüğünde React dünyasındaki cevap onu custom hook'lara ya da pricing.ts gibi düz fonksiyonlara taşımak; yani Separated Presentation'ı framework'ün içinden yeniden kurmak.

Blazor

Blazor aynı component modelini C# ile kuruyor. Markup ile @code bloğu aynı dosyada:

@using System.Globalization

<select @bind="BookId">
    @foreach (Book book in Catalog.Books)
    {
        <option value="@book.Id">@book.Title</option>
    }
</select>
<input @bind="QuantityText" @bind:event="oninput" />
<p>@Summary</p>
<button disabled="@(!IsValid)" @onclick="PlaceOrder">Place order</button>

@code {
    [Parameter] public string InitialQuantity { get; set; } = "1";

    private int BookId { get; set; } = 1;
    private string QuantityText { get; set; } = "1";

    private bool IsValid => int.TryParse(QuantityText, out int quantity) && quantity is >= 1 and <= 10;

    private string Summary => IsValid
        ? string.Create(CultureInfo.InvariantCulture,
            $"Total: {Pricing.PriceOrder(Catalog.Books.Single(b => b.Id == BookId), int.Parse(QuantityText)):0.00}")
        : "Quantity must be between 1 and 10";

    protected override void OnInitialized() => QuantityText = InitialQuantity;

    private void PlaceOrder()
    {
        // gerçek uygulamada inject edilmiş bir service çağrılır
    }
}

Component'i bir tarayıcı olmadan, .NET 8'den beri gelen HtmlRenderer ile console'dan render edebiliyoruz:

using Bookstore.Blazor;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.HtmlRendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

await using ServiceProvider services = new ServiceCollection().AddLogging().BuildServiceProvider();
await using HtmlRenderer renderer = new(services, services.GetRequiredService<ILoggerFactory>());

foreach (string quantity in new[] { "3", "abc" })
{
    string html = await renderer.Dispatcher.InvokeAsync(async () =>
    {
        ParameterView parameters = ParameterView.FromDictionary(
            new Dictionary<string, object?> { [nameof(OrderForm.InitialQuantity)] = quantity });
        HtmlRootComponent root = await renderer.RenderComponentAsync<OrderForm>(parameters);
        return root.ToHtmlString();
    });
    Console.WriteLine(html);
}
<select value="1"><option value="1" selected>The Hobbit</option><option value="2">Emma</option></select>
<input value="3" />
<p>Total: 37.80</p>
<button>Place order</button>
<select value="1"><option value="1" selected>The Hobbit</option><option value="2">Emma</option></select>
<input value="abc" />
<p>Quantity must be between 1 and 10</p>
<button disabled>Place order</button>

Üç soru: view state @code içindeki property'lerde, view logic hesaplanan property'lerde (IsValid, Summary). Senkronizasyon React'e benziyor: bir event handler çalıştıktan sonra Blazor component'i yeniden render ediyor ve render tree'leri karşılaştırıp yalnız farkı uyguluyor. @bind, WPF'teki two-way binding'in kısa yazımı gibi görünse de arkasında bir INotifyPropertyChanged yok; değeri yazıp yeniden render tetikliyor. Test için HtmlRenderer ya da bUnit gibi bir library ile render çıktısına bakıyorsun; mantık ayrı bir class'a çıkarılırsa onu düz unit test ile test edersin.

WPF ve .NET MAUI: Kitaptaki MVVM

WPF ve .NET MAUI (Multi-platform App UI), bu listede adı ve uygulaması birebir örtüşen tek örnek. Dördüncü yazıdaki OrderFormViewModel view'ı tanımıyor, XAML (Extensible Application Markup Language) ona binding ile bağlanıyor, senkronizasyonu INotifyPropertyChanged ve ICommand taşıyor ve ViewModel'i macOS'taki bir console uygulamasından, WPF olmadan test edebildik. Üç sorunun cevabı tam olarak Presentation Model'in tarifine oturuyor. Bu yüzden "MVVM nedir?" sorusuna örnek ararken WPF'e bakmak, Angular'a bakmaktan daha az kafa karıştırır.

ASP.NET Core MVC ve Razor Pages: Web MVC ve Page Model

ASP.NET Core MVC'yi ikinci yazıda gördük: Web MVC. View state request boyunca yaşayan bir view model'de, senkronizasyon her request'te baştan render, view logic'in test edilebilirliği ise onu controller'dan ve Razor'dan ne kadar dışarı taşıdığına bağlı.

Razor Pages aynı altyapının sayfa odaklı bir yüzü. Microsoft dokümantasyonu onu sayfa odaklı senaryolarda controller ve view'lardan daha kolay ve verimli diye tarif ediyor ve PageModel class'ının sayfanın mantığını sunumundan ayırdığını söylüyor. Aynı form, Razor Pages ile:

using Bookstore.Web.Domain;
using Bookstore.Web.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace Bookstore.Web.Pages.Orders;

public sealed class QuickModel : PageModel
{
    [BindProperty]
    public OrderForm Form { get; set; } = new();

    public OrderSummary? Summary { get; private set; }

    public void OnGet()
    {
    }

    public IActionResult OnPost()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        Book book = Catalog.Books.Single(b => b.Id == Form.BookId);
        Summary = new(book.Title, Form.Quantity, Pricing.PriceOrder(book, Form.Quantity));
        return Page();
    }
}
@page
@model Bookstore.Web.Pages.Orders.QuickModel
@{ Layout = null; }
@if (Model.Summary is { } summary)
{
    <p>Total: @summary.Total.ToString("0.00", CultureInfo.InvariantCulture)</p>
}
<form method="post">
    <input asp-for="Form.Quantity" />
    <span asp-validation-for="Form.Quantity"></span>
    <button type="submit">Place order</button>
</form>
$ curl -s -b jar.txt -d "Form.BookId=2&Form.Quantity=4&__RequestVerificationToken=$TOKEN" http://localhost:5391/Orders/Quick | head -2
    <p>Total: 34.20</p>
<form method="post">
$ curl -s -o /dev/null -w "%{http_code}\n" -d "Form.Quantity=4" http://localhost:5391/Orders/Quick
400

QuickModel aynı anda iki şey: request'i karşılayan bir controller (OnGet, OnPost handler'ları) ve view'ın okuduğu bir view model ([BindProperty] ile bağlanan Form, view'a giden Summary). PoEAA'daki (Patterns of Enterprise Application Architecture, Martin Fowler, 2002) adıyla bu bir Page Controller. Razor Pages'ta antiforgery doğrulaması varsayılan olarak açık; token'sız istek bu yüzden 400 dönüyor. Bazı yazarlar PageModel'i ViewModel'e benzetip Razor Pages'a MVVM der; ama binding request'te bir kez çalışıyor ve sonrasında view ile model arasında canlı bir bağ kalmıyor. Senkronizasyon sorusunun cevabı Web MVC ile aynı.

Hepsi Bir Arada

05-framework-spectrum-tr.png

FrameworkView state ve view logicSenkronizasyonEn yakın aile
ASP.NET Core MVCController action ve request başına view modelHer request'te renderWeb MVC
Razor PagesPageModel handler'ları ve bağlı property'lerHer request'te renderWeb MVC, Page Controller
WPF, .NET MAUIViewModelBinding ve INotifyPropertyChangedMVVM
AngularJS$scope ve controller fonksiyonuTwo-way binding, dirty checkingMVVM ("MVW")
AngularComponent class: signal ve computedTemplate binding, signal'larla yeniden değerlendirmeMVVM ile component arası
ReactComponent fonksiyonu ve hook stateState değişince yeniden renderComponent, UI = f(state)
Blazor@code bloğuEvent sonrası yeniden render, render tree farkıComponent, UI = f(state)

Figür ve tablo aynı şeyi söylüyor: "en yakın aile" sütununu belirleyen, senkronizasyon sütunu. View request başına bir kez çalışıyorsa Web MVC ailesindesin; view uzun yaşayan bir object'e binding ile bağlıysa MVVM ailesindesin; view state'ten her seferinde yeniden türetiliyorsa component ailesindesin. Angular'ın tabloda ikinci ve üçüncü aile arasında durması bir kararsızlık değil, son birkaç sürümde signal'larla gerçekten yön değiştirmesinin sonucu.

Sık Düşülen Tuzaklar

Etiketten kural çıkarmak. "Angular MVC'dir, o hâlde mantık controller'a yani component'e gider" diye düşünüp domain kurallarını component'e yazmak, ilk yazıdaki Smart UI'ı yeni bir framework'te yeniden kurmak demek. Hangi etiketi seçersen seç, domain kuralı pricing.ts gibi framework'ten habersiz bir yerde yaşamalı.

Başka bir framework'ün pattern'ini zorla taşımak. WPF'ten gelen bir ekibin Angular'da her component'in yanına ayrı bir ...ViewModel class'ı ve elle yazılmış bir PropertyChanged event'i kurması sık görülen bir durum. Angular'da component class zaten bu rolü oynuyor ve signal'lar bildirimi otomatik yapıyor; ikinci bir katman yalnız indirection ekliyor.

Razor Pages'ı "küçük projeler için MVC" sanmak. İkisi aynı altyapıyı (routing, model binding, validation, filter'lar) kullanıyor; fark ölçek değil, organizasyon: Razor Pages sayfa başına, MVC controller başına gruplanıyor. Büyük bir uygulama Razor Pages ile, küçük bir uygulama MVC ile gayet iyi yazılabilir.

React'te her şeyi component'te bırakmak. Framework senkronizasyonu üstlendiği için view logic'i ayırma baskısı daha az hissediliyor; sonuç, test etmek için her seferinde DOM kurduğun büyük component'ler. Custom hook ya da düz fonksiyonlarla Separated Presentation'ı yine kendin kurmalısın.

Etiket Ne Zaman Önemli, Ne Zaman Değil?

Etiket, ekip içinde ortak bir kelime dağarcığı olarak işe yarıyor. "Bu ekranın ViewModel'i şişmiş" cümlesi Angular ekibinde de WPF ekibinde de anlaşılır. Bir framework'ü öğrenirken "bu, bildiğim X'e benziyor" demek de öğrenmeyi hızlandırır; WPF'ten gelen biri Angular component'ini ViewModel olarak düşünerek çok yol alır.

Etiket bir karar aracı olarak ise işe yaramıyor. "Bu kod nereye gider?", "Bu hatayı nerede ararım?", "Bunu nasıl test ederim?" sorularının cevabı etikette değil, yukarıdaki üç soruda. Mülakatta "Angular MVC mi MVVM mi?" sorusuyla karşılaşırsan iyi bir cevap bir etiket seçmek değil, bu üç soruyu Angular için cevaplamak.

Küçük Bir Deneme

★☆☆ Üç soruyu kendi projendeki bir ekran için cevapla. Cevaplar, kullandığın framework'ün tablodaki satırıyla örtüşüyor mu? Örtüşmüyorsa fark nereden geliyor?

★★☆ React sürümündeki view logic'i bir useOrderForm custom hook'una taşı ve hook'u DOM olmadan test et. Ortaya çıkan hook, dördüncü yazıdaki Angular component class'ına ne kadar benziyor?

★★★ Blazor component'indeki IsValid ve Summary mantığını ayrı bir OrderFormState class'ına çıkar ve hem Blazor component'ini hem de dördüncü yazıdaki WPF ViewModel'ini aynı class'ı kullanacak şekilde yeniden yaz. Hangi parçalar ortak kaldı, hangileri framework'e özgü?

Blok A burada bitiyor: Separated Presentation fikrinden MVC, MVP ve MVVM'e, oradan da bugünkü framework'lere geldik. Bir sonraki yazıda Blok B başlıyor ve two-way binding'in büyük uygulamalarda neden ölçeklenmediği sorusuyla Flux'a ve unidirectional data flow'a geçiyoruz.