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());
}
- Sayfaya
IFmgLibHotReloadekleyin (tek birvoid Build()metodu ister). - Constructor'da
this.InitializeHotReload()çağırın. - 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:
- Editörünüz (ya da
dotnet watch) değişikliği derleyip .NET Hot Reload kanalından çalışan sürece gönderir. - Runtime, kayıtlı
MetadataUpdateHandler'ları uyarır. Kütüphanenin handler'ı bunlardan biridir. - Handler, kayıtlı her sayfanın
Build()metodunu ana iş parçacığında yeniden çağırır. Build()içindeContentyeniden 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ğrudanFmgLibContentPage<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. Assignreferansları yerel değişkende tutulur, alanda değil.- En güvenilir kanal
dotnet watch run; teşhiseupdate receivedsatı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.