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
BindableObjectiç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
newtanımlarNewekini 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.