VodiSoft
"XAML'i bırakırsam hot reload'u kaybederim" itirazının cevabı: .NET Hot Reload kod seviyesinde çalıştığı için C# markup da bu döngünün içinde. Kurulumun üç adımını, kaydettiğinizde arka planda ne olduğunu, ve asıl önemlisi Build() metodunun defalarca çalışacağını varsayarak kod yazmayı — durum nerede yaşamalı, abonelik nereye konmalı — adım adım ele alıyoruz.

C# markup'a geçmeyi düşünen herkesin takıldığı aynı cümle: "XAML'in hot reload'ı var, C#'a geçersem her değişiklikte uygulamayı yeniden başlatmam gerekir."

Bu, yaygın ama yanlış bir varsayım. .NET Hot Reload, XAML'e değil kodun kendisine bakar — çalışan sürece derlenmiş kod güncellemesi gönderir. XAML Hot Reload zaten bunun üzerine kurulmuş ayrı bir katmandır. C# markup'ta ihtiyacınız olan tek eksik parça şudur: kod güncellendiğinde arayüzü yeniden kuran biri.

FmgLib.MauiMarkup bu parçayı sağlıyor. Bu yazıda kurulumu, kaydettiğinizde arka planda ne olduğunu ve — asıl önemlisi — Build() metodunun defalarca çalışacağını bilerek kod yazmayı ele alıyoruz.

Üç adımda kurulum

public class CounterPage : ContentPage, IFmgLibHotReload
{
    public CounterPage() => this.InitializeHotReload();

    public void Build() => this
        .Content(
            new Label().Text("Merhaba").FontSize(32).Center());
}
  1. Sayfaya IFmgLibHotReload ekleyin (tek bir void Build() metodu ister).
  2. Constructor'da this.InitializeHotReload() çağırın.
  3. Bütün arayüz kurulumunu Build() içine koyun.

Hazır taban sınıfları kullanırsanız bu plumbing de kalkar:

public class ProfilePage : FmgLibContentPage<ProfileViewModel>
{
    public ProfilePage(ProfileViewModel vm) : base(vm) { }

    public override void Build() => this
        .Content(
            new VerticalStackLayout()
            .Padding(20)
            .Spacing(12)
            .Children(
                new Label().Text(e => e.Getter(static (ProfileViewModel v) => v.UserName)),

                new Button()
                    .Text("Yenile")
                    .Command(BindingContext.RefreshCommand)   // tipli, cast yok
            )
        );
}

FmgLibContentPage<TViewModel> view model'i constructor'da alır, ilk Build() çalışmadan önce BindingContext'e atar ve BindingContext özelliğini yeniden tipler — yani ((ProfileViewModel)BindingContext) yazmazsınız. DI ile doğrudan uyuşur:

builder.Services.AddTransient<ProfileViewModel>();
builder.Services.AddTransient<ProfilePage>();

Kaydettiğinizde ne oluyor?

Zincir şöyle işliyor:

  1. Editörünüz (ya da dotnet watch) değişikliği derleyip .NET Hot Reload kanalından çalışan sürece gönderir.
  2. Runtime, kayıtlı MetadataUpdateHandler'ları uyarır. Kütüphanenin handler'ı bunlardan biridir.
  3. Handler, kayıtlı her sayfanın Build() metodunu ana iş parçacığında yeniden çağırır.
  4. Build() içinde Content yeniden atandığı için görsel ağaç baştan kurulur.

Her güncellemede debug çıktısına bir satır düşer:

FmgLib.MauiMarkup hot reload: update received (types: …) — rebuilding N registered target(s).

Bu satırı görüyorsanız zincir uçtan uca çalışıyor demektir; görmüyorsanız sorun kodunuzda değil, güncellemeyi ileten kanaldadır (aşağıda).

İki tasarım detayı işinizi kolaylaştırıyor: kayıt zayıf referansla yapılır — hot reload hiçbir sayfanın ömrünü uzatmaz, leak dedektörleri sayfaları “pinned” göstermez. Ve reload sırasında Build() hata fırlatırsa uygulama çökmez; hata loglanır, ReloadFailed olayıyla bildirilir, siz düzeltip tekrar kaydedersiniz.

Asıl mesele: Build() defalarca çalışır

Kurulum beş dakika. Geri kalan her şey tek bir zihinsel modele bağlı:

Görsel ağaç harcanabilir, durum değildir.

Build() bir oturumda onlarca kez çalışacak. Her çalıştığında yepyeni kontroller üretilecek, eskiler çöpe gidecek. Dolayısıyla Build() içinde ürettiğiniz her şey geçici, dışında tuttuğunuz her şey kalıcıdır.

Bu ayrımı bir kez oturttuğunuzda hot reload sorunlarının neredeyse tamamı buharlaşıyor.

Durum: constructor'da, alanlarda

public class CounterPage : ContentPage, IFmgLibHotReload
{
    readonly CounterViewModel vm = new();   // reload'ları atlatır

    public CounterPage() => this.InitializeHotReload();

    public void Build() => this
        .BindingContext(vm)
        .Content(
            new VerticalStackLayout()
            .Spacing(16)
            .Padding(24)
            .Center()
            .Children(
                new Label()
                    .FontSize(48)
                    .Text(e => e
                        .Getter(static (CounterViewModel v) => v.Count)
                        .StringFormat("{0}")),

                new Button()
                    .Text("Artır")
                    .Command(e => e.Getter(static (CounterViewModel v) => v.IncrementCommand))
            )
        );
}

Sayacı 47'ye getirip bir rengi değiştirdiğinizde ekran yeniden çizilir ama sayaç 47'de kalır — çünkü değer view model'de, view model de bir alanda yaşıyor.

Aynı satırı Build() içine taşısaydınız:

public void Build()
{
    var vm = new CounterViewModel();   // ❌ her reload'da sayaç sıfırlanır
    ...
}

Her kaydedişte 47 kaybolurdu. Bu, “hot reload state'i sıfırlıyor” şikâyetinin bir numaralı sebebidir — ve aslında hot reload'la ilgisi yoktur, durumun yanlış yerde durmasıyla ilgilidir.

Abonelikler: uzun ömürlü olanlar constructor'a

public CounterPage()
{
    Application.Current!.RequestedThemeChanged += OnThemeChanged;   // ✅ bir kez
    this.InitializeHotReload();
}

Bunu Build() içine koysaydınız, her reload'da bir abonelik daha eklenirdi: onuncu kaydetmeden sonra tema değiştiğinde handler'ınız on kez çalışırdı. “Bir tık attım, olay iki kere tetiklendi” hikâyesinin kaynağı budur.

Kural basit: abone olduğunuz nesne sayfadan uzun yaşıyorsa (Application.Current, statik olaylar, singleton servisler) abonelik constructor'a gider. Kontrolün kendi olayları — .OnClicked(...), .OnTapped(...)Build() içinde kalabilir, çünkü kontrol de her seferinde yeniden üretiliyor.

Bir kez yapılacak işler: Build() içinde değil

Ağ çağrısı, animasyon, veri yükleme — bunları Build() içinde başlatırsanız her kaydedişte tekrar tetiklenirler:

public class FeedPage : ContentPage, IFmgLibHotReload
{
    readonly List<string> items = new();
    bool loaded;                                   // bayrak da bir alan

    public FeedPage() => this.InitializeHotReload();

    public void Build() => this
        .Content(
            new CollectionView()
                .ItemsSource(items)
                .OnLoaded(async c => await LoadOnceAsync()));

    async Task LoadOnceAsync()
    {
        if (loaded)
            return;

        loaded = true;
        // ... servisten çek, items'a ekle ...
    }
}

OnLoaded/OnAppearing doğru yer, bir de bayrağı alan olarak tutmak — reload sonrası loaded hâlâ true olduğu için veri ikinci kez çekilmez.

Kontrol referansları: Assign ile, yerel değişkende

new Entry().Assign(out var emailEntry).Placeholder("E-posta"),
new Button().OnClicked(b => Validate(emailEntry.Text))

Assign ile yakaladığınız referansı Build() içinde yerel değişkende tutun. Alan yaparsanız reload sonrası alan eski, artık ekranda olmayan kontrolü göstermeye devam eder — sessizce yanlış çalışan, bulması zor bir hata.

Geliştirme döngüsü

En güvenilir yol, IDE'den bağımsız olanı: dotnet watch.

dotnet watch run -f net10.0-ios

Debugger gerekmez, tüm kanallar içinde en sorunsuzu budur. VS Code kullanıyorsanız tek tuşa bağlayın:

// .vscode/tasks.json
{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "🔥 Hot Reload: iOS Simulator",
            "type": "shell",
            "command": "dotnet",
            "args": [ "watch", "run", "-f", "net10.0-ios" ],
            "isBackground": true,
            "problemMatcher": []
        }
    ]
}

fmglib-mauimarkup-app şablonuyla oluşturulan projelerde bu dosya hazır geliyor. Breakpoint gerektiğinde F5 ile ayrı bir oturum açarsınız.

Kanal desteği özetle:

Kanal Durum
dotnet watch run ✅ En güvenilir, IDE'den bağımsız
Visual Studio (F5, Windows) ✅ Tam destek
VS Code + C# Dev Kit "csharp.experimental.debug.hotReload": true ayarı şart
Rider (debugger) ❌ MAUI'ye .NET Hot Reload iletmiyor — içinden dotnet watch konfigürasyonu açın
Düz dotnet run / Release ❌ Güncelleme kanalı yok — tasarım gereği, sıfır maliyet

Son satır önemli: Release derlemesinde hiçbir ek maliyet yok. MetadataUpdater.IsSupported yanlış olduğu için handler hiçbir zaman devreye girmez; InitializeHotReload() yalnızca Build()'i bir kez çağırmış olur.

iOS/Mac Catalyst tarafında bir not: .NET Hot Reload, Mono interpreter'ına ihtiyaç duyar. MAUI bunu Debug'da varsayılan olarak açar; kapattıysanız Debug için geri açın.

Bir şey görünmüyorsa

Teşhise hep aynı yerden başlayın: debug çıktısında update received satırı var mı?

Satır yok. Sorun kodunuzda değil, güncelleme kanalında. Değişiklikler sürece hiç ulaşmıyor demektir. VS Code'da yukarıdaki ayarı kontrol edin; olmuyorsa dotnet watch run ile ilerleyin. Kütüphane ayrıca kanal kapalıysa tek seferlik bir uyarı basar (MetadataUpdater.IsSupported = false) — o mesajı gördüyseniz o oturumda ne yaparsanız yapın değişiklik uygulanmaz.

Satır var ama ekran değişmiyor. Düzenlediğiniz tip kendini yeniden kurmuyordur. Yalnızca IFmgLibHotReload uygulayan tipler rebuild olur; yardımcı bir ContentView düzenlediyseniz ona da aynı üç adımı uygulayın (o da kendini yeniden kurar).

Değişiklikler uygulanmış ama render edilmemiş. Bazı araçlar kod güncellemesini uygular ama runtime handler'larını uyarmaz. Kurtarma kolu için debug'a özel bir jest bırakın:

#if DEBUG
new Border()
    .Content(new Label().Text("dev"))
    .GestureRecognizers(
        new TapGestureRecognizer()
            .NumberOfTapsRequired(3)
            .OnTapped((s, e) => FmgLibHotReloadHandler.RebuildAll()));
#endif

“Rude edit” uyarısı. Metot imzası değiştirmek, bazı tiplere alan eklemek gibi düzenlemeler .NET Hot Reload'ın sınırlarını aşar; oturumu yeniden başlatmak gerekir. Bu bir runtime kısıtı, kütüphaneyle ilgisi yok.

Reload sırasında Build() patlarsa uygulama ayakta kalır ama sebebi görmek isterseniz:

FmgLibHotReloadHandler.ReloadFailed += (target, ex) =>
    Debug.WriteLine($"Build() failed for {target.GetType().Name}: {ex.Message}");

Yan etki: hot reload dostu kod, zaten iyi kod

Bu kuralları uygulayınca fark edeceğiniz bir şey var: ortaya çıkan yapı, hot reload'dan bağımsız olarak daha temiz.

  • Durum view model'de toplanır, görsel ağaç sadece onu gösterir.
  • Build() saf bir tanım hâline gelir: “bu ekran şudur” — yan etkisi yoktur.
  • Yan etkiler (yükleme, animasyon, abonelik) ait oldukları yerlere, yaşam döngüsü metotlarına taşınır.

Bunlar zaten önerilen MVVM pratikleri. Hot reload sadece ihlal ettiğinizde size anında geri bildirim veriyor — sayaç sıfırlanıyorsa ya da olay iki kez tetikleniyorsa, kod size bir şey söylüyor demektir.

Özet

  • .NET Hot Reload koda bakar, XAML'e değil; C# markup bu döngünün dışında değil.
  • IFmgLibHotReload + InitializeHotReload() + Build() — ya da doğrudan FmgLibContentPage<TViewModel>.
  • Görsel ağaç harcanabilir, durum değildir: view model ve bayraklar alanlarda, arayüz Build() içinde.
  • Uzun ömürlü abonelikler constructor'a; kontrol olayları Build() içinde kalabilir.
  • Assign referansları yerel değişkende tutulur, alanda değil.
  • En güvenilir kanal dotnet watch run; teşhise update received satırından başlayın.
  • Release'de sıfır maliyet.

Sonuçta elde ettiğiniz şey şu: simülatör açık, kodu düzenliyorsunuz, kaydediyorsunuz, ekran bir iki saniyede güncelleniyor — üstelik uygulamanın durumu yerinde duruyor. Tasarım üzerinde çalışmanın en hızlı hâli bu ve XAML'e ihtiyaç duymuyor.

İlgili Yazılar

FmgLib.MauiMarkup mu, CommunityToolkit.Maui.Markup mı? Dürüst Bir Karşılaştırma

.NET MAUI arayüzünü XAML yerine C# ile yazmanın iki yolu var ve ikisi de akıcı (fluent) metotlar kullanıyor. Aradaki farkların neredeyse tamamı tek bir mimari tercihten doğuyor: elle seçilmiş uzantılar mı, yoksa kaynak üreteci mi? Bu yazıda farkları, her birinin hangi durumda daha iyi seçim olduğunu ve ikisinin aynı projede yan yana yaşayıp yaşayamayacağını ölçtüğümüz sonuçlarla ele alıyoruz.

Devamını Oku

Uygulamanın Tamamı Aynı Dili Konuşsun: .NET MAUI'de Üçüncü Parti Kontroller ve Kendi Fluent Metotlarınız

C# markup'ın gerçek projelerdeki en büyük sorusu: Syncfusion, UraniumUI, SkiaSharp gibi kütüphanelerin kontrolleri için fluent API kaybolur mu? Kaybolmuyor — aynı kaynak üreteci onlar için de çalışıyor. Tek satırlık opt-in'den otomatik taramaya, attached property'lerden yeniden tanımlanan property kurallarına, oradan kendi tasarım sistemi metotlarınızı yazmaya kadar tüm yolu ele alıyoruz.

Devamını Oku

VodiSoft

Yazılım projenizi gelir üreten bir ürüne dönüştürelim

Web, mobil, .NET, SaaS ve entegrasyon projeleri için teknik keşif yapalım; riskleri, takvimi ve en hızlı ticari kazanımı netleştirelim.

Teklif Al Toplantı