VodiSoft
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.

Serinin önceki yazılarında binding'leri, listeleri, temayı ve hot reload döngüsünü kurduk. Hepsinde MAUI'nin kendi kontrollerini kullandık. Ama gerçek bir uygulamada ekranların yarısı MAUI'nin kutudan çıkan kontrolleri değildir: Syncfusion bir grafik, UraniumUI bir form alanı, SkiaSharp bir Lottie animasyonu, ZXing bir barkod okuyucu.

Bu noktada akla gelen soru haklı: "O kontrollerde fluent sözdizimini kaybediyor muyum? Yarısı new X().Y(...), yarısı object initializer olan bir kod tabanı mı çıkacak?"

Cevap: hayır. Ama nedenini anlamak için önce fluent metotların nereden geldiğini bilmek gerekiyor.

Fluent metotlar nereden geliyor?

FmgLib.MauiMarkup'ta .Text(...), .FontSize(...), .OnClicked(...) gibi binlerce metot elle yazılmadı. Bir Roslyn kaynak üreteci (source generator) MAUI'nin kontrollerini tarayıp her BindableProperty ve her event için bunları derleme anında üretiyor.

Bunun doğal sonucu şu: üreteç MAUI'ye özel değil. Girdisi “public bindable property'leri olan bir BindableObject” olan her tip için aynı şeyi yapabilir — hangi kütüphaneden geldiği fark etmez.

Üç yol var: tek satırlık opt-in, otomatik tarama, ve attached property'ler için ayrı bir öznitelik.

1. [MauiMarkup] — istediğin kontrolü seç

Öznitelik herhangi bir sınıfa konur; sınıfın kendisi önemli değil, sadece bir çapa görevi görür.

using FmgLib.MauiMarkup;
using Acme.Controls;

[MauiMarkup(typeof(FancyRating))]
public static class Markup { }

Bu kadar. Derledikten sonra o kontrol de diğerleri gibi davranır:

new FancyRating()
    .Value(4.5)
    .IsReadOnly(true)
    .StarColor(e => e.OnLight(Colors.Goldenrod).OnDark(Colors.Yellow))
    .OnValueChanged((s, v) => Analyze(v))

Dikkat edilecek nokta: StarColor yalnızca değer almıyor, property builder lambda'sını da alıyor. Yani üçüncü parti kontrolde de binding, OnLight/OnDark, DynamicResource, multibinding — hepsi çalışıyor. Üretilen metotlar birinci sınıf vatandaş; kütüphanenin kendi kontrollerinden farkları yok.

Öznitelik birden fazla tip alabilir ve bir sınıfa birden fazla öznitelik konabilir. Pratikte tek bir yerde toplamak en temizi — çoğu proje MauiProgram'ı çapa olarak kullanıyor:

[MauiMarkup(typeof(CameraView))]
[MauiMarkup(typeof(SKLottieView), typeof(SKConfettiView))]
[MauiMarkup(typeof(TextField), typeof(EditorField), typeof(DataGrid))]
public static class MauiProgram { /* ... */ }

Taban sınıflar da otomatik geliyor

Bir kontrolün bindable yüzeyinin çoğu genellikle taban sınıfında durur. Üreteç bunu biliyor: yaprak sınıfı işaretlemek, uygun üçüncü parti taban sınıfları için de uzantı üretir.

Örneğimizde FancyRating, Maximum property'sini RatingBase'den alıyor. Tek bir öznitelik yazmama rağmen iki uzantı sınıfı üretildi ve şu satır sorunsuz derlendi:

new FancyRating()
    .Value(4.5)      // FancyRating'in kendi property'si
    .Maximum(10)     // RatingBase'den — ayrıca işaretlemeye gerek yok

Syncfusion'ın SfButton'ı buna iyi bir örnek: Text, Command, FontSize gibi çoğu şey ButtonBase'de yaşar, ama [MauiMarkup(typeof(SfButton))] hepsini getirir. (MAUI'nin kendi taban sınıfları yeniden üretilmez — onların uzantıları zaten kütüphanenin içinde.)

Ne kadar şey üretiliyor?

Yukarıdaki örnekte üç property ve bir event'ten oluşan küçük bir kontrol için üreteç 20 metot çıkardı. Bir property'nin karşılığı dört overload:

Overload Ne sağlar
.StarColor(Color) doğrudan değer
.StarColor(e => ...) property builder: binding, tema, dinamik kaynak, multibinding
SettersContext versiyonu Style<FancyRating> içinde kullanım
SettersContext + builder stil içinde binding/tema

Üstüne bir de animasyon uzantısı geliyor — Color ve sayısal property'ler için AnimateStarColorTo(...) gibi. Yani “üçüncü parti kontrol ikinci sınıf vatandaş” diye bir durum yok; stil de yazabiliyorsunuz:

public static Style<FancyRating> ReadOnly { get; } = new(e => e
    .IsReadOnly(true)
    .StarColor(t => t.OnLight(Colors.Gray).OnDark(Colors.DimGray)));

2. Attached property'ler

Attached property'ler ayrı bir öznitelik ister, çünkü üretecin dört bilgiye ihtiyacı var: property'yi tanımlayan tip, BindableProperty alanının adı, değer tipi ve uzantının uygulanacağı tip.

[MauiMarkupAttachedProp(typeof(FancyRating),
                        nameof(FancyRating.HighlightProperty),
                        typeof(bool),
                        typeof(Label))]
public static class Markup { }

Üretilen metodun adı sahip sınıf + property adı biçiminde:

new Label()
    .Text("Öne çıkan")
    .FancyRatingHighlight(true)

InputKit'in FormView.IsSubmitButton'ını Button'a uygulamak da tam olarak bu kalıp.

3. Otomatik mod — hiç öznitelik yazmadan

Kontrolleri tek tek saymak istemiyorsanız uygulama projenizin .csproj dosyasına tek bir satır:

<PropertyGroup>
  <MauiMarkupSourceGenerator>true</MauiMarkupSourceGenerator>
</PropertyGroup>

Üreteç referans verilen üçüncü parti assembly'leri tarar ve uygun her public BindableObject için uzantı üretir. Gereken diğer MSBuild ayarları paketle birlikte otomatik geliyor.

Takas şu: otomatik mod rahat ama bulduğu her şey için kod üretir; büyük çözümlerde derleme süresini uzatabilir. Öznitelik yaklaşımı üretimi gerçekten kullandığınız kontrollerle sınırlar. Küçük/orta projede otomatik, büyük çözümde öznitelik iyi bir varsayılan.

Bilmezseniz vakit kaybettiren kural: yeniden tanımlanan property'ler

Kontroller bazen taban sınıfın property'sini yeniden tanımlar. Üreteç iki durumu ayırıyor ve bunu bilmek IntelliSense'te “metot kayboldu” paniğini önlüyor:

Aynı tiple yeniden tanım (yaygın olan). Örneğin SfButton.TextColor, ButtonBase.TextColor'ı aynı tiple yeniden açar. Bu durumda türetilmiş sınıf için ayrı metot üretilmez: taban sınıfın generic uzantısı zaten SfButton'a da uyar. İkisini birden üretmek her çağrıyı belirsiz hâle getirirdi (CS0121).

Sonuç: yaprak tipte bir metodu “göremiyorsanız” büyük ihtimalle taban sınıf uzantısı aynı isimle onu zaten sunuyordur.

Farklı tiple yeniden tanım (gerçek bir new). Örneğin SfAvatarView.Background, Brush olan property'yi Color'a çevirir. Burada türetilmiş metot New ekiyle üretilir, çünkü yalnızca generic argümanı farklı iki aynı isimli overload lambda çağrılarını bozar ve sessizce yanlış BindableProperty'yi hedefleyebilir:

new SfAvatarView()
    .BackgroundNew(Colors.LightBlue)   // SfAvatarView'ın kendi Color property'si
    .Background(someBrush)             // VisualElement'ten miras Brush property'si

Kendi sözlüğünüzü yazmak

Üçüncü parti kontroller çözüldü. Geriye bir şey kalıyor: projenizin kendi kelimeleri. Tasarım sisteminiz “başlık”, “kart”, “birincil buton” diyorsa kodunuz da öyle demeli.

Seviye 1 — kompozisyon kısayolları

En basit ve en çok işe yarayanı. Her fluent metot T döndürdüğü için hiçbir kütüphane makinesine ihtiyaç yok:

public static class MarkupHelpers
{
    public static T PrimaryText<T>(this T self) where T : Label
        => self
            .FontSize(16)
            .TextColor(e => e.OnLight(AppColors.Text).OnDark(AppColors.TextDark));

    public static T Card<T>(this T self) where T : Border
        => self
            .BackgroundColor(e => e.OnLight(Colors.White).OnDark(AppColors.SurfaceDark))
            .StrokeThickness(0)
            .Padding(16);
}
new Border().Card().Content(
    new Label().Text("Toplam").PrimaryText())

<T> generic'i ve where T : ... kısıtını koruyun; böylece zincirde somut tip akmaya devam eder ve .Card()'dan sonra Border'a özel metotlara erişmeyi kaybetmezsiniz.

Seviye 2 — tam property metodu

Bir kavramın gerçekten “property gibi” davranmasını istiyorsanız — yani binding alabilsin, OnLight/OnDark çalışsın, stil içinde kullanılabilsin — dört overload'ı yazarsınız. Üretecin ürettiğinin aynısı:

public static class SpacingExtensions
{
    public static readonly BindableProperty DensityProperty =
        BindableProperty.CreateAttached("Density", typeof(double), typeof(SpacingExtensions), 1d);

    // 1. Doğrudan değer
    public static T Density<T>(this T self, double density) where T : VisualElement
    {
        self.SetValue(DensityProperty, density);
        return self;
    }

    // 2. Property builder — binding, tema, dinamik kaynak
    public static T Density<T>(this T self, Func<PropertyContext<double>, IPropertyBuilder<double>> configure)
        where T : VisualElement
    {
        var context = new PropertyContext<double>(self, DensityProperty);
        configure(context).Build();
        return self;
    }

    // 3. Stil setter'ı
    public static SettersContext<T> Density<T>(this SettersContext<T> self, double density)
        where T : VisualElement
    {
        self.XamlSetters.Add(new Setter { Property = DensityProperty, Value = density });
        return self;
    }

    // 4. Stil setter'ı + builder
    public static SettersContext<T> Density<T>(this SettersContext<T> self,
        Func<PropertySettersContext<double>, IPropertySettersBuilder<double>> configure)
        where T : VisualElement
    {
        var context = new PropertySettersContext<double>(self.XamlSetters, DensityProperty);
        configure(context).Build();
        return self;
    }
}

Dördü birden yerindeyken her kullanım biçimi açılır:

new Label().Density(1.25)                              // değer
new Label().Density(e => e.OnPhone(1.0).OnDesktop(1.25))   // idiom
new Style<Label>(e => e.Density(0.8).FontSize(13))     // stil

İki kural: değeri her zaman BindableProperty üzerinden SetValue ile yazın (CLR property'si üzerinden değil) — yoksa stiller, binding'ler ve trigger'lar devre dışı kalır. Ve PropertyContext<TValue> ile SettersContext<T> ikilisini karıştırmayın: ilki canlı kontrole, ikincisi Style<T> içindeki setter listesine yazar.

Hangi seviye ne zaman?

Durum Yaklaşım
Üçüncü parti kontrolü fluent kullanmak [MauiMarkup(typeof(...))]
Üçüncü parti attached property [MauiMarkupAttachedProp(...)]
Çok sayıda üçüncü parti kontrol, küçük çözüm MauiMarkupSourceGenerator otomatik mod
Tekrarlayan stil zinciri (kart, başlık, rozet) Seviye 1 kısayol
Yeni bir “property” kavramı (yoğunluk, marka tipografisi) Seviye 2 dört overload
Kontrolün bindable property'si zaten var Hiçbiri — üreteç zaten üretmiş

Son satır bir uyarı: kendi metodunuzu yazmadan önce üretilmiş olanı arayın. Aynı isimde ikinci bir uzantı yazmak, aradığınız hatayı bulmakta zorlanacağınız belirsizlik hatalarına yol açar.

Özet

  • Fluent API bir kaynak üretecinin ürünü; üreteç MAUI'ye özel değil, herhangi bir BindableObject için çalışıyor.
  • [MauiMarkup(typeof(X))] tek satır: property'ler, event'ler, stil setter'ları ve animasyon uzantıları geliyor — taban sınıflar dâhil.
  • Attached property'ler için [MauiMarkupAttachedProp], dört parametresiyle.
  • Hepsini otomatikleştirmek isterseniz MauiMarkupSourceGenerator, derleme süresi takasıyla.
  • Aynı tiple yeniden tanımlanan property'ler taban sınıf uzantısıyla sunulur; tip değiştiren gerçek new tanımlar New ekini alır.
  • Kendi kelimeleriniz için: basit zincirlerde kompozisyon kısayolu, gerçek property davranışı gerekiyorsa dört overload'lı şablon.

Böylece kod tabanının tamamı — MAUI kontrolleri, üçüncü parti kontroller ve sizin tasarım sisteminiz — tek bir dil konuşur. C# markup'ın asıl kazancı da tek tek metotlar değil, bu bütünlük.

İ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

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ı