.NET MAUI Handler Mimarisi: Özel Kontrol Geliştirme Rehberi

.NET MAUI handler mimarisinin temellerinden ileri düzey kullanımına kapsamlı bir rehber. PropertyMapper, CommandMapper ile kontrol özelleştirme, sıfırdan özel kontrol oluşturma ve Xamarin renderer geçiş stratejilerini öğrenin.

Giriş: Handler Mimarisi Neden Bu Kadar Önemli?

Xamarin.Forms ile mobil uygulama geliştiren herkes, bir noktada custom renderer yazmak zorunda kalmıştır. Bir Entry kontrolünün altındaki o sinir bozucu çizgiyi kaldırmak, bir Button'ın köşe yuvarlaklığını platform bazında ayarlamak ya da tamamen sıfırdan yeni bir kontrol oluşturmak istediğinizde renderer'lar kaçınılmaz hale geliyordu. Ama itiraf edelim, Xamarin.Forms renderer mimarisi yıllar içinde birçok sınırlamasıyla geliştiricileri oldukça zorladı. Sıkı bağımlılıklar, performans sorunları, test edilebilirlik eksiklikleri ve karmaşık yaşam döngüsü yönetimi... Bunlar hep aklımızın bir köşesinde birer sorun olarak duruyordu.

.NET MAUI ile birlikte Microsoft, bu sorunlara kökten bir çözüm sundu: Handler mimarisi.

Handler'lar, Xamarin.Forms'taki renderer'ların yerini alan, daha hafif, daha hızlı ve çok daha esnek bir yapı olarak tasarlandı. Bu yeni mimari, platformlar arası kontrol geliştirmeyi ciddi anlamda basitleştirirken performansı da önemli ölçüde artırdı.

Bu rehberde, .NET MAUI handler mimarisinin temellerinden ileri düzey kullanım senaryolarına kadar her şeyi ele alacağız. PropertyMapper ve CommandMapper kavramlarından mevcut kontrolleri özelleştirmeye, sıfırdan özel kontrol oluşturmaya ve geçiş stratejilerine kadar kapsamlı bir yolculuğa çıkacağız. Hazırsanız başlayalım.

Handler Mimarisi Nedir?

Handler mimarisi, .NET MAUI'nin temel yapı taşlarından biridir. Platformlar arası kontrollerin yerel platform kontrollerine nasıl dönüştürüleceğini tanımlar. Birbirine gevşek bağlı (loosely coupled) bileşenlerden oluşur ve her biri belirli bir sorumluluk taşır.

Temel Kavramlar

Handler mimarisi üç ana bileşen üzerine inşa edilmiştir:

  • Sanal Görünüm (Virtual View): Platformdan bağımsız, soyut bir arayüz tanımıdır. Kontrolün sahip olması gereken özellikleri ve davranışları tanımlayan bir interface'tir. Örneğin IButton, IEntry gibi arayüzler bu kategoriye girer.
  • Handler: Sanal görünümü yerel platform kontrolüne dönüştüren sınıftır. Her platform için ayrı bir handler implementasyonu bulunur ve sanal görünüm ile yerel kontrol arasındaki köprü görevini üstlenir.
  • Yerel Görünüm (Native/Platform View): Platformun kendi UI toolkit'inden gelen gerçek kontroldür. Android'de EditText, iOS'ta UITextField gibi yerel kontroller bu katmandadır.

PropertyMapper

PropertyMapper, sanal görünümdeki özelliklerin yerel kontrole nasıl yansıtılacağını tanımlayan bir sözlük (dictionary) yapısıdır. Her özellik değiştiğinde ilgili mapper metodu çağrılır ve yerel kontrol güncellenir. Açıkçası bu yapı, eski renderer'daki OnElementPropertyChanged karmaşasına kıyasla çok daha temiz bir yaklaşım.

// PropertyMapper tanımı örneği
public static IPropertyMapper<IEntry, IEntryHandler> Mapper =
    new PropertyMapper<IEntry, IEntryHandler>(ViewMapper)
    {
        // Her özellik için bir eşleme metodu tanımlanır
        [nameof(IEntry.Text)] = MapText,
        [nameof(IEntry.TextColor)] = MapTextColor,
        [nameof(IEntry.Placeholder)] = MapPlaceholder,
        [nameof(IEntry.MaxLength)] = MapMaxLength,
        [nameof(IEntry.IsPassword)] = MapIsPassword,
        [nameof(IEntry.IsReadOnly)] = MapIsReadOnly,
    };

// Eşleme metodu örneği
public static void MapText(IEntryHandler handler, IEntry entry)
{
    // Yerel kontrolün Text özelliğini güncelle
    handler.PlatformView.UpdateText(entry);
}

CommandMapper

CommandMapper, PropertyMapper'a benzer şekilde çalışır ama özellikler yerine komutları (yani eylemleri) eşler. Bir komut tetiklendiğinde ilgili platform kodunun çalıştırılmasını sağlar.

// CommandMapper tanımı örneği
public static CommandMapper<IEntry, IEntryHandler> CommandMapper =
    new(ViewCommandMapper)
    {
        // Animasyon veya platform eylemleri için komut eşlemeleri
        ["Animate"] = MapAnimate,
        ["Focus"] = MapFocus,
    };

// Komut eşleme metodu
public static void MapFocus(IEntryHandler handler, IEntry entry, object? args)
{
    // Yerel kontrole odaklanma isteğini ilet
    handler.PlatformView?.RequestFocus();
}

Ayrıştırılmış Mimari

Handler mimarisinin en güçlü yanı, her katmanın birbirinden bağımsız olmasıdır. Sanal görünüm handler'ın varlığından haberdar değildir; handler ise yalnızca bir arayüz üzerinden sanal görünümle iletişim kurar. Bu ayrıştırma sayesinde:

  • Her katman bağımsız olarak test edilebilir
  • Platform implementasyonları kolayca değiştirilebilir
  • Yeni platformlar minimum eforla desteklenebilir
  • Kontroller daha az bellek tüketir ve daha hızlı oluşturulur

Renderer vs Handler Karşılaştırması

Xamarin.Forms renderer mimarisinden .NET MAUI handler mimarisine geçişin neden bu kadar önemli olduğunu anlamak için iki yaklaşımı yan yana koymak gerekiyor. Aşağıdaki tablo temel farkları özetliyor:

Özellik Xamarin.Forms Renderer .NET MAUI Handler
Mimari Yaklaşım Sıkı bağımlılık (tight coupling) Gevşek bağımlılık (loose coupling)
Performans Daha yavaş, fazla soyutlama katmanı Daha hızlı, doğrudan platform erişimi
Özelleştirme Tüm sınıfın alt sınıflanması gerekir Mapper ile nokta atışı özelleştirme
Test Edilebilirlik Platform bağımlılığı nedeniyle zor Arayüz tabanlı, kolay mock'lanabilir
Global Değişiklikler Her yerde ayrı renderer gerekir AppendToMapping ile tek noktadan yönetim
Kontrol Erişimi Element ve Control özellikleri VirtualView ve PlatformView özellikleri
Yaşam Döngüsü OnElementChanged, OnElementPropertyChanged ConnectHandler, DisconnectHandler
Bellek Yönetimi Dispose kalıbına bağımlı Otomatik bağlantı kesme desteği

Neden Handler'lar Daha Üstün?

Performans: Xamarin.Forms renderer'ları, platformlar arası kontrol ile yerel kontrol arasında çok sayıda soyutlama katmanı içeriyordu. Hem bellek tüketimini artırıyordu hem de render süresini uzatıyordu. Handler'lar ise doğrudan yerel platforma erişim sağlayarak bu ek yükü ortadan kaldırır. Microsoft'un kendi testlerine göre, handler mimarisi ile kontrollerin oluşturulma süresi ortalama %40 oranında kısalmış. Bu gerçekten ciddi bir fark.

Basitlik: Renderer yaklaşımında basit bir görsel değişiklik için bile tüm renderer sınıfını alt sınıflamak ve birçok sanal metodu override etmek gerekiyordu. Handler mimarisinde ise sadece değiştirmek istediğiniz özelliğin mapper'ını güncellemeniz yeterli. Kod miktarı dramatik biçimde azalıyor.

Test Edilebilirlik: Handler mimarisi tamamen arayüz (interface) tabanlı olduğu için birim testlerde platform bağımlılıkları kolayca mock'lanabiliyor. Sanal görünüm arayüzleri, gerçek platform kontrolleri olmadan test edilebilir hale geliyor. Bence bu, handler mimarisinin en az konuşulan ama en değerli avantajlarından biri.

Mevcut Kontrolleri Özelleştirme

.NET MAUI'nin en sevdiğim özelliklerinden biri, mevcut kontrolleri alt sınıflama yapmadan özelleştirebilmeniz. Bu, AppendToMapping, PrependToMapping ve ModifyMapping yöntemleri ile sağlanıyor.

AppendToMapping

AppendToMapping, mevcut bir özellik eşlemesinin sonrasında çalışacak ek bir davranış ekler. Orijinal davranış korunur ve eklenen kod ondan sonra çalışır. En yaygın kullanılan özelleştirme yöntemidir ve muhtemelen en çok ihtiyaç duyacağınız da bu olacak.

// MauiProgram.cs dosyasında Entry kontrolünün görünümünü özelleştirme
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .ConfigureMauiHandlers(handlers =>
        {
            // Tüm Entry kontrolleri için alt çizgiyi kaldır
            Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping(
                "AltCizgiKaldir", (handler, view) =>
            {
#if ANDROID
                // Android'de EditText'in arka plan çizimini kaldır
                handler.PlatformView.BackgroundTintList =
                    Android.Content.Res.ColorStateList.ValueOf(
                        Android.Graphics.Color.Transparent);
#elif IOS
                // iOS'ta UITextField'in kenarlık stilini kaldır
                handler.PlatformView.BorderStyle =
                    UIKit.UITextBorderStyle.None;
#endif
            });
        });

    return builder.Build();
}

PrependToMapping

PrependToMapping, mevcut bir özellik eşlemesinin öncesinde çalışacak bir davranış ekler. Orijinal davranıştan önce bazı ön hazırlıklar yapmak istediğinizde işe yarar.

// Button kontrolü için köşe yuvarlaklığını önceden ayarla
Microsoft.Maui.Handlers.ButtonHandler.Mapper.PrependToMapping(
    "KoseYuvarlaklik", (handler, view) =>
{
#if ANDROID
    // Android'de MaterialButton'un köşe yuvarlaklığını ayarla
    if (handler.PlatformView is Google.Android.Material.Button.MaterialButton materialButton)
    {
        materialButton.CornerRadius = 24;
    }
#elif IOS
    // iOS'ta UIButton'un katman özelliklerini ayarla
    handler.PlatformView.Layer.CornerRadius = 12f;
    handler.PlatformView.ClipsToBounds = true;
#endif
});

ModifyMapping

ModifyMapping, mevcut bir özellik eşlemesinin davranışını tamamen kontrol altına almanızı sağlar. Orijinal eşleme metoduna erişim sağladığı için onu çağırıp çağırmamayı siz seçebilirsiniz. En esnek yöntem bu, ama aynı zamanda en dikkatli kullanılması gereken de bu. Yanlış kullanımda varsayılan davranışları kırabilirsiniz.

// Editor kontrolünün arka plan rengini koşullu olarak değiştir
Microsoft.Maui.Handlers.EditorHandler.Mapper.ModifyMapping(
    nameof(IEditor.Background), (handler, view, originalMapper) =>
{
    // Belirli bir koşula göre orijinal davranışı geçersiz kıl
    if (view is Editor editor && editor.ClassId == "OzelEditor")
    {
#if ANDROID
        handler.PlatformView.SetBackgroundColor(
            Android.Graphics.Color.ParseColor("#F0F4F8"));
#elif IOS
        handler.PlatformView.BackgroundColor =
            UIKit.UIColor.FromRGB(240, 244, 248);
#endif
    }
    else
    {
        // Diğer tüm durumlar için orijinal davranışı uygula
        originalMapper?.Invoke(handler, view);
    }
});

Pratik Örnek: Tüm Entry Kontrollerini Özelleştirme

Şimdi gelin daha kapsamlı bir örneğe bakalım. Aşağıda, uygulamadaki tüm Entry kontrollerinin görünümünü baştan aşağı özelleştiriyoruz:

// MauiProgram.cs - Kapsamlı Entry özelleştirmesi
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureMauiHandlers(handlers =>
            {
                EntryOzellikleriniYapılandır();
            });

        return builder.Build();
    }

    private static void EntryOzellikleriniYapılandır()
    {
        // Tüm Entry kontrolleri için özelleştirmeler
        Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping(
            "OzelEntryStili", (handler, view) =>
        {
#if ANDROID
            // Alt çizgiyi kaldır
            handler.PlatformView.BackgroundTintList =
                Android.Content.Res.ColorStateList.ValueOf(
                    Android.Graphics.Color.Transparent);

            // İç boşluk (padding) ayarla
            handler.PlatformView.SetPadding(32, 16, 32, 16);

            // Arka plan şeklini ayarla
            var gradientDrawable = new Android.Graphics.Drawables.GradientDrawable();
            gradientDrawable.SetCornerRadius(24f);
            gradientDrawable.SetColor(Android.Graphics.Color.ParseColor("#F5F5F5"));
            gradientDrawable.SetStroke(2, Android.Graphics.Color.ParseColor("#E0E0E0"));
            handler.PlatformView.Background = gradientDrawable;

#elif IOS
            // Kenarlık stilini kaldır
            handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;

            // Arka plan rengi ve köşe yuvarlaklığı
            handler.PlatformView.BackgroundColor =
                UIKit.UIColor.FromRGB(245, 245, 245);
            handler.PlatformView.Layer.CornerRadius = 12f;
            handler.PlatformView.Layer.BorderWidth = 1f;
            handler.PlatformView.Layer.BorderColor =
                UIKit.UIColor.FromRGB(224, 224, 224).CGColor;

            // İç boşluk için sol ve sağ görünüm ekle
            handler.PlatformView.LeftView =
                new UIKit.UIView(new CoreGraphics.CGRect(0, 0, 16, 0));
            handler.PlatformView.LeftViewMode =
                UIKit.UITextFieldViewMode.Always;
            handler.PlatformView.RightView =
                new UIKit.UIView(new CoreGraphics.CGRect(0, 0, 16, 0));
            handler.PlatformView.RightViewMode =
                UIKit.UITextFieldViewMode.Always;
#endif
        });
    }
}

Sıfırdan Özel Kontrol Oluşturma

Handler mimarisinin gerçek gücü, tamamen yeni kontroller oluşturabilmenizde ortaya çıkıyor. Bu bölümde adım adım özel bir DerecelendirmeCubuğu (Rating Bar) kontrolü oluşturacağız. Kullanıcıların yıldız vererek derecelendirme yapmasını sağlayan, pratikte sıkça ihtiyaç duyulan bir kontrol bu.

Adım 1: Platformlar Arası Arayüz Tanımlama

Her şey bir arayüzle başlıyor. Kontrolümüzün platformdan bağımsız arayüzünü tanımlayarak hangi özellikleri ve olayları destekleyeceğini belirliyoruz.

// Controls/IDerecelendirmeCubugu.cs
namespace MauiUygulama.Controls;

public interface IDerecelendirmeCubugu : IView
{
    // Mevcut derecelendirme değeri (0 ile MaksimumDeger arasında)
    double Deger { get; set; }

    // Maksimum derecelendirme değeri (varsayılan: 5)
    int MaksimumDeger { get; }

    // Yıldız boyutu piksel cinsinden
    double YildizBoyutu { get; }

    // Seçili yıldız rengi
    Color SeciliRenk { get; }

    // Boş yıldız rengi
    Color BosRenk { get; }

    // Yıldızlar arası boşluk
    double AralikBosluk { get; }

    // Kullanıcının değer değiştirebilme durumu
    bool SaltOkunur { get; }
}

Adım 2: Platformlar Arası Kontrol Sınıfı Oluşturma

Arayüzü uygulayan kontrol sınıfını oluşturuyoruz. Bu sınıf bağlanabilir özellikleri (BindableProperty) içerir ve XAML'de kullanılabilir hale gelir. Kod biraz uzun görünebilir ama aslında her özellik için aynı kalıp tekrarlanıyor, o yüzden korkmayın.

// Controls/DerecelendirmeCubugu.cs
namespace MauiUygulama.Controls;

public class DerecelendirmeCubugu : View, IDerecelendirmeCubugu
{
    // Deger bağlanabilir özelliği
    public static readonly BindableProperty DegerProperty =
        BindableProperty.Create(
            nameof(Deger),
            typeof(double),
            typeof(DerecelendirmeCubugu),
            0.0,
            BindingMode.TwoWay,
            validateValue: (bindable, value) =>
            {
                var deger = (double)value;
                var kontrol = (DerecelendirmeCubugu)bindable;
                return deger >= 0 && deger <= kontrol.MaksimumDeger;
            });

    public double Deger
    {
        get => (double)GetValue(DegerProperty);
        set => SetValue(DegerProperty, value);
    }

    // MaksimumDeger bağlanabilir özelliği
    public static readonly BindableProperty MaksimumDegerProperty =
        BindableProperty.Create(
            nameof(MaksimumDeger),
            typeof(int),
            typeof(DerecelendirmeCubugu),
            5);

    public int MaksimumDeger
    {
        get => (int)GetValue(MaksimumDegerProperty);
        set => SetValue(MaksimumDegerProperty, value);
    }

    // YildizBoyutu bağlanabilir özelliği
    public static readonly BindableProperty YildizBoyutuProperty =
        BindableProperty.Create(
            nameof(YildizBoyutu),
            typeof(double),
            typeof(DerecelendirmeCubugu),
            32.0);

    public double YildizBoyutu
    {
        get => (double)GetValue(YildizBoyutuProperty);
        set => SetValue(YildizBoyutuProperty, value);
    }

    // SeciliRenk bağlanabilir özelliği
    public static readonly BindableProperty SeciliRenkProperty =
        BindableProperty.Create(
            nameof(SeciliRenk),
            typeof(Color),
            typeof(DerecelendirmeCubugu),
            Colors.Gold);

    public Color SeciliRenk
    {
        get => (Color)GetValue(SeciliRenkProperty);
        set => SetValue(SeciliRenkProperty, value);
    }

    // BosRenk bağlanabilir özelliği
    public static readonly BindableProperty BosRenkProperty =
        BindableProperty.Create(
            nameof(BosRenk),
            typeof(Color),
            typeof(DerecelendirmeCubugu),
            Colors.LightGray);

    public Color BosRenk
    {
        get => (Color)GetValue(BosRenkProperty);
        set => SetValue(BosRenkProperty, value);
    }

    // AralikBosluk bağlanabilir özelliği
    public static readonly BindableProperty AralikBoslukProperty =
        BindableProperty.Create(
            nameof(AralikBosluk),
            typeof(double),
            typeof(DerecelendirmeCubugu),
            4.0);

    public double AralikBosluk
    {
        get => (double)GetValue(AralikBoslukProperty);
        set => SetValue(AralikBoslukProperty, value);
    }

    // SaltOkunur bağlanabilir özelliği
    public static readonly BindableProperty SaltOkunurProperty =
        BindableProperty.Create(
            nameof(SaltOkunur),
            typeof(bool),
            typeof(DerecelendirmeCubugu),
            false);

    public bool SaltOkunur
    {
        get => (bool)GetValue(SaltOkunurProperty);
        set => SetValue(SaltOkunurProperty, value);
    }

    // Değer değiştiğinde tetiklenen olay
    public event EventHandler<double>? DegerDegisti;

    // Handler tarafından çağrılacak dahili metot
    internal void DegerDegistiginiBildir(double yeniDeger)
    {
        Deger = yeniDeger;
        DegerDegisti?.Invoke(this, yeniDeger);
    }
}

Adım 3: Platform Handler Uygulamaları

Şimdi işin en heyecanlı kısmına geliyoruz. Her platform için ayrı handler implementasyonu oluşturacağız. Önce tüm platformlar için ortak olan handler temelini tanımlayalım:

// Handlers/DerecelendirmeCubuguHandler.cs (Paylaşılan kısım)
using Microsoft.Maui.Handlers;

namespace MauiUygulama.Handlers;

public partial class DerecelendirmeCubuguHandler
{
    // Özellik eşleme sözlüğü
    public static IPropertyMapper<IDerecelendirmeCubugu, DerecelendirmeCubuguHandler>
        OzellikMapper = new PropertyMapper<IDerecelendirmeCubugu,
            DerecelendirmeCubuguHandler>(ViewHandler.ViewMapper)
    {
        [nameof(IDerecelendirmeCubugu.Deger)] = DegeriEsle,
        [nameof(IDerecelendirmeCubugu.MaksimumDeger)] = MaksimumDegeriEsle,
        [nameof(IDerecelendirmeCubugu.YildizBoyutu)] = YildizBoyutunuEsle,
        [nameof(IDerecelendirmeCubugu.SeciliRenk)] = SeciliRengiEsle,
        [nameof(IDerecelendirmeCubugu.BosRenk)] = BosRengiEsle,
        [nameof(IDerecelendirmeCubugu.SaltOkunur)] = SaltOkunurEsle,
    };

    // Komut eşleme sözlüğü
    public static CommandMapper<IDerecelendirmeCubugu, DerecelendirmeCubuguHandler>
        KomutMapper = new(ViewCommandMapper);

    public DerecelendirmeCubuguHandler()
        : base(OzellikMapper, KomutMapper)
    {
    }

    public DerecelendirmeCubuguHandler(
        IPropertyMapper? mapper = null,
        CommandMapper? commandMapper = null)
        : base(mapper ?? OzellikMapper, commandMapper ?? KomutMapper)
    {
    }
}

Android platformu için handler implementasyonu:

// Platforms/Android/Handlers/DerecelendirmeCubuguHandler.cs
using Android.Content;
using Android.Graphics;
using Android.Views;
using Android.Widget;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;

namespace MauiUygulama.Handlers;

public partial class DerecelendirmeCubuguHandler
    : ViewHandler<IDerecelendirmeCubugu, LinearLayout>
{
    // Yerel Android görünümünü oluştur
    protected override LinearLayout CreatePlatformView()
    {
        var konteyner = new LinearLayout(Context)
        {
            Orientation = Orientation.Horizontal,
            Gravity = GravityFlags.CenterVertical
        };

        return konteyner;
    }

    // Handler bağlandığında çağrılır
    protected override void ConnectHandler(LinearLayout platformView)
    {
        base.ConnectHandler(platformView);
        platformView.Touch += PlatformGorunumDokunuldu;
        YildizlariOlustur();
    }

    // Handler bağlantısı kesildiğinde çağrılır
    protected override void DisconnectHandler(LinearLayout platformView)
    {
        // Olay dinleyicisini kaldır (bellek sızıntısını önle)
        platformView.Touch -= PlatformGorunumDokunuldu;
        base.DisconnectHandler(platformView);
    }

    // Dokunma olayını işle
    private void PlatformGorunumDokunuldu(object? sender, View.TouchEventArgs e)
    {
        if (VirtualView.SaltOkunur) return;

        if (e.Event?.Action == MotionEventActions.Down ||
            e.Event?.Action == MotionEventActions.Move)
        {
            var x = e.Event.GetX();
            var toplamGenislik = PlatformView.Width;
            var yildizGenisligi = toplamGenislik / (float)VirtualView.MaksimumDeger;
            var yeniDeger = Math.Ceiling(x / yildizGenisligi);
            yeniDeger = Math.Clamp(yeniDeger, 0, VirtualView.MaksimumDeger);

            if (VirtualView is DerecelendirmeCubugu kontrol)
            {
                kontrol.DegerDegistiginiBildir(yeniDeger);
            }
        }
    }

    // Yıldız görünümlerini oluştur
    private void YildizlariOlustur()
    {
        PlatformView.RemoveAllViews();

        for (int i = 0; i < VirtualView.MaksimumDeger; i++)
        {
            var yildizGorunumu = new TextView(Context)
            {
                Text = "★",
                TextSize = (float)VirtualView.YildizBoyutu,
                Gravity = GravityFlags.Center
            };

            var dolu = i < VirtualView.Deger;
            var renk = dolu ? VirtualView.SeciliRenk : VirtualView.BosRenk;
            yildizGorunumu.SetTextColor(renk.ToPlatform());

            var parametreler = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.WrapContent,
                LinearLayout.LayoutParams.WrapContent);
            parametreler.SetMargins(
                (int)VirtualView.AralikBosluk, 0,
                (int)VirtualView.AralikBosluk, 0);

            PlatformView.AddView(yildizGorunumu, parametreler);
        }
    }

    // Özellik eşleme metotları
    public static void DegeriEsle(
        DerecelendirmeCubuguHandler handler, IDerecelendirmeCubugu gorunum)
    {
        handler.YildizRenkleriniGuncelle();
    }

    public static void MaksimumDegeriEsle(
        DerecelendirmeCubuguHandler handler, IDerecelendirmeCubugu gorunum)
    {
        handler.YildizlariOlustur();
    }

    public static void YildizBoyutunuEsle(
        DerecelendirmeCubuguHandler handler, IDerecelendirmeCubugu gorunum)
    {
        handler.YildizlariOlustur();
    }

    public static void SeciliRengiEsle(
        DerecelendirmeCubuguHandler handler, IDerecelendirmeCubugu gorunum)
    {
        handler.YildizRenkleriniGuncelle();
    }

    public static void BosRengiEsle(
        DerecelendirmeCubuguHandler handler, IDerecelendirmeCubugu gorunum)
    {
        handler.YildizRenkleriniGuncelle();
    }

    public static void SaltOkunurEsle(
        DerecelendirmeCubuguHandler handler, IDerecelendirmeCubugu gorunum)
    {
        handler.PlatformView.Alpha = gorunum.SaltOkunur ? 0.6f : 1.0f;
    }

    private void YildizRenkleriniGuncelle()
    {
        for (int i = 0; i < PlatformView.ChildCount; i++)
        {
            if (PlatformView.GetChildAt(i) is TextView yildiz)
            {
                var dolu = i < VirtualView.Deger;
                var renk = dolu ? VirtualView.SeciliRenk : VirtualView.BosRenk;
                yildiz.SetTextColor(renk.ToPlatform());
            }
        }
    }
}

Ve iOS platformu için handler implementasyonu:

// Platforms/iOS/Handlers/DerecelendirmeCubuguHandler.cs
using UIKit;
using CoreGraphics;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;

namespace MauiUygulama.Handlers;

public partial class DerecelendirmeCubuguHandler
    : ViewHandler<IDerecelendirmeCubugu, UIStackView>
{
    protected override UIStackView CreatePlatformView()
    {
        var yiginGorunumu = new UIStackView
        {
            Axis = UILayoutConstraintAxis.Horizontal,
            Distribution = UIStackViewDistribution.FillEqually,
            Alignment = UIStackViewAlignment.Center,
            UserInteractionEnabled = true
        };

        return yiginGorunumu;
    }

    protected override void ConnectHandler(UIStackView platformView)
    {
        base.ConnectHandler(platformView);

        var dokunmaTaniyici = new UITapGestureRecognizer(DokunmaIslendi);
        platformView.AddGestureRecognizer(dokunmaTaniyici);

        YildizlariOlustur();
    }

    protected override void DisconnectHandler(UIStackView platformView)
    {
        if (platformView.GestureRecognizers != null)
        {
            foreach (var taniyici in platformView.GestureRecognizers)
            {
                platformView.RemoveGestureRecognizer(taniyici);
            }
        }

        base.DisconnectHandler(platformView);
    }

    private void DokunmaIslendi(UITapGestureRecognizer taniyici)
    {
        if (VirtualView.SaltOkunur) return;

        var konum = taniyici.LocationInView(PlatformView);
        var toplamGenislik = PlatformView.Bounds.Width;
        var yildizGenisligi = toplamGenislik / VirtualView.MaksimumDeger;
        var yeniDeger = Math.Ceiling(konum.X / yildizGenisligi);
        yeniDeger = Math.Clamp(yeniDeger, 0, VirtualView.MaksimumDeger);

        if (VirtualView is DerecelendirmeCubugu kontrol)
        {
            kontrol.DegerDegistiginiBildir(yeniDeger);
        }
    }

    private void YildizlariOlustur()
    {
        foreach (var altGorunum in PlatformView.ArrangedSubviews)
        {
            PlatformView.RemoveArrangedSubview(altGorunum);
            altGorunum.RemoveFromSuperview();
        }

        PlatformView.Spacing = (nfloat)VirtualView.AralikBosluk;

        for (int i = 0; i < VirtualView.MaksimumDeger; i++)
        {
            var yildizEtiketi = new UILabel
            {
                Text = "★",
                Font = UIFont.SystemFontOfSize((nfloat)VirtualView.YildizBoyutu),
                TextAlignment = UITextAlignment.Center
            };

            var dolu = i < VirtualView.Deger;
            var renk = dolu ? VirtualView.SeciliRenk : VirtualView.BosRenk;
            yildizEtiketi.TextColor = renk.ToPlatform();

            PlatformView.AddArrangedSubview(yildizEtiketi);
        }
    }

    public static void DegeriEsle(
        DerecelendirmeCubuguHandler handler, IDerecelendirmeCubugu gorunum)
    {
        handler.YildizRenkleriniGuncelle();
    }

    public static void MaksimumDegeriEsle(
        DerecelendirmeCubuguHandler handler, IDerecelendirmeCubugu gorunum)
    {
        handler.YildizlariOlustur();
    }

    public static void YildizBoyutunuEsle(
        DerecelendirmeCubuguHandler handler, IDerecelendirmeCubugu gorunum)
    {
        handler.YildizlariOlustur();
    }

    public static void SeciliRengiEsle(
        DerecelendirmeCubuguHandler handler, IDerecelendirmeCubugu gorunum)
    {
        handler.YildizRenkleriniGuncelle();
    }

    public static void BosRengiEsle(
        DerecelendirmeCubuguHandler handler, IDerecelendirmeCubugu gorunum)
    {
        handler.YildizRenkleriniGuncelle();
    }

    public static void SaltOkunurEsle(
        DerecelendirmeCubuguHandler handler, IDerecelendirmeCubugu gorunum)
    {
        handler.PlatformView.Alpha = gorunum.SaltOkunur ? 0.6f : 1.0f;
    }

    private void YildizRenkleriniGuncelle()
    {
        var altGorunumler = PlatformView.ArrangedSubviews;
        for (int i = 0; i < altGorunumler.Length; i++)
        {
            if (altGorunumler[i] is UILabel yildiz)
            {
                var dolu = i < VirtualView.Deger;
                var renk = dolu ? VirtualView.SeciliRenk : VirtualView.BosRenk;
                yildiz.TextColor = renk.ToPlatform();
            }
        }
    }
}

Adım 4: Handler'ı Kaydetme

Oluşturduğumuz handler'ı uygulamanın DI konteynerine kaydetmemiz gerekiyor. Bu adım olmadan MAUI kontrolümüzü tanıyamaz.

// MauiProgram.cs
using MauiUygulama.Controls;
using MauiUygulama.Handlers;

namespace MauiUygulama;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureMauiHandlers(handlers =>
            {
                // Özel handler'ı kaydet
                handlers.AddHandler<DerecelendirmeCubugu,
                    DerecelendirmeCubuguHandler>();
            });

        return builder.Build();
    }
}

Adım 5: Özel Kontrolü Kullanma

Artık kontrolümüzü XAML ve C# kodunda rahatlıkla kullanabiliriz:

<!-- MainPage.xaml -->
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:kontroller="clr-namespace:MauiUygulama.Controls"
             x:Class="MauiUygulama.MainPage"
             Title="Derecelendirme Örneği">

    <VerticalStackLayout Padding="20" Spacing="16">

        <Label Text="Ürünü Değerlendirin:"
               FontSize="18"
               FontAttributes="Bold" />

        <!-- Etkileşimli derecelendirme çubuğu -->
        <kontroller:DerecelendirmeCubugu
            x:Name="derecelendirme"
            Deger="{Binding Puan, Mode=TwoWay}"
            MaksimumDeger="5"
            YildizBoyutu="36"
            SeciliRenk="Gold"
            BosRenk="LightGray"
            AralikBosluk="8"
            DegerDegisti="Derecelendirme_DegerDegisti" />

        <Label x:Name="puanEtiketi"
               Text="Puanınız: 0/5"
               FontSize="14"
               TextColor="Gray" />

        <!-- Salt okunur derecelendirme -->
        <Label Text="Ortalama Puan:"
               FontSize="16"
               Margin="0,20,0,0" />

        <kontroller:DerecelendirmeCubugu
            Deger="3.5"
            MaksimumDeger="5"
            YildizBoyutu="24"
            SeciliRenk="OrangeRed"
            SaltOkunur="True" />

    </VerticalStackLayout>
</ContentPage>
// MainPage.xaml.cs - Arka plan kodu
namespace MauiUygulama;

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    private void Derecelendirme_DegerDegisti(object? sender, double yeniDeger)
    {
        puanEtiketi.Text = $"Puanınız: {yeniDeger}/{derecelendirme.MaksimumDeger}";
    }
}

Handler Yaşam Döngüsü

Handler'ların yaşam döngüsünü anlamak, doğru kaynak yönetimi ve bellek sızıntılarının önlenmesi için gerçekten kritik bir konu. İhmal ettiğinizde uygulamanız yavaş yavaş bellek yemeye başlar ve sonunda crash olur. Kendi deneyimlerimden söylüyorum, bu konuyu hafife almayın.

.NET MAUI handler yaşam döngüsü dört temel aşamadan oluşur.

ConnectHandler

ConnectHandler, handler yerel platform görünümüne bağlandığında çağrılır. Bu metotta olay dinleyicilerini kaydetmeli, platform kaynaklarını başlatmalı ve başlangıç yapılandırmasını gerçekleştirmelisiniz.

protected override void ConnectHandler(PlatformView platformView)
{
    base.ConnectHandler(platformView);

    // Olay dinleyicilerini kaydet
    platformView.Click += PlatformGorunumTiklandi;
    platformView.FocusChange += OdakDegisti;

    // Platform kaynaklarını başlat
    _animasyon = new PlatformAnimasyon(platformView);

    // Başlangıç yapılandırmasını uygula
    PlatformGorunumuYapilandir();
}

DisconnectHandler

DisconnectHandler, handler bağlantısı kesildiğinde çağrılır. Tüm olay dinleyicilerini kaldırmalı, kaynakları serbest bırakmalı ve temizlik işlemlerini gerçekleştirmelisiniz. Bellek sızıntılarını önlemek için bu adım son derece önemli.

protected override void DisconnectHandler(PlatformView platformView)
{
    // Olay dinleyicilerini kaldır (bellek sızıntısını önle)
    platformView.Click -= PlatformGorunumTiklandi;
    platformView.FocusChange -= OdakDegisti;

    // Kaynakları serbest bırak
    _animasyon?.Dispose();
    _animasyon = null;

    // Temel sınıfın temizlik işlemini çağır
    base.DisconnectHandler(platformView);
}

HandlerChanging ve HandlerChanged

Kontrol düzeyinde handler değişikliklerini izlemek için HandlerChanging ve HandlerChanged olayları kullanılır. Bu olaylar, kontrolün yaşam döngüsünde handler'ın atanması veya kaldırılması sırasında tetiklenir.

// Kontrol sınıfında handler olaylarını dinleme
public class OzelKontrol : View
{
    public OzelKontrol()
    {
        HandlerChanging += OzelKontrol_HandlerDegisiyor;
        HandlerChanged += OzelKontrol_HandlerDegisti;
    }

    private void OzelKontrol_HandlerDegisiyor(
        object? sender, HandlerChangingEventArgs e)
    {
        if (e.OldHandler != null)
        {
            // Eski handler ile ilgili temizlik işlemleri
            System.Diagnostics.Debug.WriteLine(
                "Eski handler kaldırılıyor...");
        }

        if (e.NewHandler != null)
        {
            System.Diagnostics.Debug.WriteLine(
                "Yeni handler atanacak...");
        }
    }

    private void OzelKontrol_HandlerDegisti(
        object? sender, EventArgs e)
    {
        if (Handler != null)
        {
            System.Diagnostics.Debug.WriteLine(
                $"Handler türü: {Handler.GetType().Name}");

#if ANDROID
            if (Handler.PlatformView is Android.Views.View androidView)
            {
                androidView.SetBackgroundColor(
                    Android.Graphics.Color.Transparent);
            }
#elif IOS
            if (Handler.PlatformView is UIKit.UIView iosView)
            {
                iosView.BackgroundColor = UIKit.UIColor.Clear;
            }
#endif
        }
    }
}

Platform Bazlı Özelleştirme Stratejileri

.NET MAUI'de platform bazlı kod yazmak için iki temel strateji var: koşullu derleme (conditional compilation) ve kısmi sınıflar (partial classes). Her iki yaklaşımın da avantajları ve kullanım alanları farklı. Hangisini ne zaman kullanacağınızı bilmek gerçekten önemli.

Koşullu Derleme Yaklaşımı

Koşullu derleme, #if önişlemci direktifleri kullanarak platform kodunu aynı dosya içinde yazmayı sağlar. Küçük ve basit platform farklılıkları için ideal bir seçenek.

// Tek dosyada koşullu derleme ile platform kodu
Microsoft.Maui.Handlers.SearchBarHandler.Mapper.AppendToMapping(
    "AramaBariniOzellestir", (handler, view) =>
{
#if ANDROID
    // Android: Arama çubuğunun arka plan rengini değiştir
    var aramaGorunumu = handler.PlatformView;
    aramaGorunumu.SetBackgroundColor(Android.Graphics.Color.White);

    // Arama ikonunun rengini ayarla
    var aramaIkonuId = aramaGorunumu.Context.Resources?.GetIdentifier(
        "android:id/search_mag_icon", null, null) ?? 0;
    if (aramaIkonuId > 0)
    {
        var ikon = aramaGorunumu.FindViewById<Android.Widget.ImageView>(
            aramaIkonuId);
        ikon?.SetColorFilter(Android.Graphics.Color.DarkGray,
            Android.Graphics.PorterDuff.Mode.SrcIn);
    }

#elif IOS
    // iOS: Arama çubuğunun stilini değiştir
    handler.PlatformView.SearchBarStyle = UIKit.UISearchBarStyle.Minimal;
    handler.PlatformView.BackgroundColor = UIKit.UIColor.White;
    handler.PlatformView.BarTintColor = UIKit.UIColor.White;

    // İptal düğmesinin başlığını Türkçe yap
    UIKit.UIBarButtonItem.Appearance
        .WhenContainedIn(typeof(UIKit.UISearchBar))
        .Title = "İptal";

#elif WINDOWS
    // Windows: Arama kutusunun görünümünü ayarla
    handler.PlatformView.QueryIcon.Foreground =
        new Microsoft.UI.Xaml.Media.SolidColorBrush(
            Microsoft.UI.Colors.DarkGray);
#endif
});

Kısmi Sınıflar Yaklaşımı

Kısmi sınıflar yaklaşımı, her platform için ayrı dosya oluşturarak platform kodunu organize eder. Karmaşık handler implementasyonları için çok daha temiz ve yönetilebilir bir yapı sunar. Özellikle takım halinde çalışıyorsanız bu yaklaşımı tercih etmenizi tavsiye ederim.

// Handlers/OzelGirisHandler.cs (Paylaşılan tanım)
using Microsoft.Maui.Handlers;

namespace MauiUygulama.Handlers;

public partial class OzelGirisHandler
{
    public static IPropertyMapper<IOzelGiris, OzelGirisHandler> OzelMapper =
        new PropertyMapper<IOzelGiris, OzelGirisHandler>(ViewMapper)
        {
            [nameof(IOzelGiris.MetinRengi)] = MetinRengiEsle,
            [nameof(IOzelGiris.YerTutucuRenk)] = YerTutucuRenkEsle,
            [nameof(IOzelGiris.KenarlıkRengi)] = KenarlıkRengiEsle,
        };

    public OzelGirisHandler() : base(OzelMapper) { }

    // Platform metotlarının imzaları (kısmi metotlar)
    public static partial void MetinRengiEsle(
        OzelGirisHandler handler, IOzelGiris gorunum);
    public static partial void YerTutucuRenkEsle(
        OzelGirisHandler handler, IOzelGiris gorunum);
    public static partial void KenarlıkRengiEsle(
        OzelGirisHandler handler, IOzelGiris gorunum);
}
// Platforms/Android/Handlers/OzelGirisHandler.cs
using Android.Graphics.Drawables;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;

namespace MauiUygulama.Handlers;

public partial class OzelGirisHandler
    : ViewHandler<IOzelGiris, Android.Widget.EditText>
{
    protected override Android.Widget.EditText CreatePlatformView()
    {
        return new Android.Widget.EditText(Context);
    }

    public static partial void MetinRengiEsle(
        OzelGirisHandler handler, IOzelGiris gorunum)
    {
        handler.PlatformView.SetTextColor(gorunum.MetinRengi.ToPlatform());
    }

    public static partial void YerTutucuRenkEsle(
        OzelGirisHandler handler, IOzelGiris gorunum)
    {
        handler.PlatformView.SetHintTextColor(
            gorunum.YerTutucuRenk.ToPlatform());
    }

    public static partial void KenarlıkRengiEsle(
        OzelGirisHandler handler, IOzelGiris gorunum)
    {
        var kenarlık = new GradientDrawable();
        kenarlık.SetCornerRadius(16f);
        kenarlık.SetStroke(2, gorunum.KenarlıkRengi.ToPlatform());
        handler.PlatformView.Background = kenarlık;
    }
}
// Platforms/iOS/Handlers/OzelGirisHandler.cs
using UIKit;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;

namespace MauiUygulama.Handlers;

public partial class OzelGirisHandler
    : ViewHandler<IOzelGiris, UITextField>
{
    protected override UITextField CreatePlatformView()
    {
        return new UITextField
        {
            BorderStyle = UITextBorderStyle.None
        };
    }

    public static partial void MetinRengiEsle(
        OzelGirisHandler handler, IOzelGiris gorunum)
    {
        handler.PlatformView.TextColor = gorunum.MetinRengi.ToPlatform();
    }

    public static partial void YerTutucuRenkEsle(
        OzelGirisHandler handler, IOzelGiris gorunum)
    {
        handler.PlatformView.AttributedPlaceholder =
            new Foundation.NSAttributedString(
                handler.PlatformView.Placeholder ?? "",
                new UIStringAttributes
                {
                    ForegroundColor = gorunum.YerTutucuRenk.ToPlatform()
                });
    }

    public static partial void KenarlıkRengiEsle(
        OzelGirisHandler handler, IOzelGiris gorunum)
    {
        handler.PlatformView.Layer.BorderColor =
            gorunum.KenarlıkRengi.ToPlatform().CGColor;
        handler.PlatformView.Layer.BorderWidth = 1f;
        handler.PlatformView.Layer.CornerRadius = 8f;
    }
}

Hangi Yaklaşımı Ne Zaman Kullanmalı?

  • Koşullu derleme, platform kodu birkaç satırdan ibaret olduğunda, hızlı prototipleme yaparken veya AppendToMapping gibi tek seferlik özelleştirmelerde idealdir.
  • Kısmi sınıflar, karmaşık handler implementasyonlarında, platform kodunun uzun ve detaylı olduğu durumlarda, takım çalışmasında ve sürdürülebilirliğin ön planda olduğu projelerde çok daha mantıklı bir tercih olur.

Xamarin Renderer'lardan Handler'lara Geçiş

Mevcut bir Xamarin.Forms projesini .NET MAUI'ye taşırken, özel renderer'ları handler'lara dönüştürmek en önemli adımlardan biridir. Bu geçiş ilk bakışta korkutucu görünebilir ama aslında sistematik bir yaklaşımla oldukça sorunsuz halledilebilir.

Xamarin.Forms Renderer Örneği (Eski Yaklaşım)

Önce tipik bir Xamarin.Forms özel renderer'ın nasıl göründüğünü hatırlayalım. Bu kodu görmek sizi nostaljik yapabilir (ya da kabus gibi hissettirebilir):

// Xamarin.Forms'daki eski renderer yaklaşımı
// NOT: Bu kod artık kullanılmıyor, sadece karşılaştırma amaçlıdır

// Özel kontrol tanımı
public class YuvarlakResim : Image
{
    public static readonly BindableProperty KenarlıkRenkiProperty =
        BindableProperty.Create(nameof(KenarlıkRenki), typeof(Color),
            typeof(YuvarlakResim), Colors.White);

    public Color KenarlıkRenki
    {
        get => (Color)GetValue(KenarlıkRenkiProperty);
        set => SetValue(KenarlıkRenkiProperty, value);
    }

    public static readonly BindableProperty KenarlıkKalinligiProperty =
        BindableProperty.Create(nameof(KenarlıkKalinligi), typeof(double),
            typeof(YuvarlakResim), 2.0);

    public double KenarlıkKalinligi
    {
        get => (double)GetValue(KenarlıkKalinligiProperty);
        set => SetValue(KenarlıkKalinligiProperty, value);
    }
}

// Android renderer
[assembly: ExportRenderer(typeof(YuvarlakResim), typeof(YuvarlakResimRenderer))]
public class YuvarlakResimRenderer : ViewRenderer<YuvarlakResim, ImageView>
{
    public YuvarlakResimRenderer(Context context) : base(context) { }

    protected override void OnElementChanged(ElementChangedEventArgs<YuvarlakResim> e)
    {
        base.OnElementChanged(e);

        if (e.NewElement != null)
        {
            GorunumuGuncelle();
        }
    }

    protected override void OnElementPropertyChanged(
        object sender, PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);

        // Her özellik değişikliğinde kontrol etmek gerekiyordu
        if (e.PropertyName == YuvarlakResim.KenarlıkRenkiProperty.PropertyName ||
            e.PropertyName == YuvarlakResim.KenarlıkKalinligiProperty.PropertyName)
        {
            GorunumuGuncelle();
        }
    }

    private void GorunumuGuncelle()
    {
        // Kontrol ve element null kontrolü zorunluydu
        if (Control == null || Element == null) return;
        // Görünüm güncelleme kodu...
    }
}

Handler Yaklaşımına Geçiş (Yeni Yaklaşım)

Şimdi aynı kontrolü handler mimarisiyle yeniden yazalım. Farkı göreceksiniz: daha az boilerplate kod, daha temiz yapı.

// .NET MAUI handler yaklaşımı ile YuvarlakResim kontrolü

// 1. Arayüz tanımı (yeni adım)
public interface IYuvarlakResim : IImage
{
    Color KenarlıkRenki { get; }
    double KenarlıkKalinligi { get; }
}

// 2. Kontrol sınıfı (değişiklik minimal)
public class YuvarlakResim : Image, IYuvarlakResim
{
    public static readonly BindableProperty KenarlıkRenkiProperty =
        BindableProperty.Create(nameof(KenarlıkRenki), typeof(Color),
            typeof(YuvarlakResim), Colors.White);

    public Color KenarlıkRenki
    {
        get => (Color)GetValue(KenarlıkRenkiProperty);
        set => SetValue(KenarlıkRenkiProperty, value);
    }

    public static readonly BindableProperty KenarlıkKalinligiProperty =
        BindableProperty.Create(nameof(KenarlıkKalinligi), typeof(double),
            typeof(YuvarlakResim), 2.0);

    public double KenarlıkKalinligi
    {
        get => (double)GetValue(KenarlıkKalinligiProperty);
        set => SetValue(KenarlıkKalinligiProperty, value);
    }
}

// 3. Handler tanımı (renderer yerine)
public partial class YuvarlakResimHandler
{
    public static IPropertyMapper<IYuvarlakResim, YuvarlakResimHandler>
        YuvarlakResimMapper =
            new PropertyMapper<IYuvarlakResim, YuvarlakResimHandler>(
                ImageHandler.Mapper)
            {
                // Her özellik için ayrı bir eşleme metodu
                [nameof(IYuvarlakResim.KenarlıkRenki)] = KenarlıkRengiEsle,
                [nameof(IYuvarlakResim.KenarlıkKalinligi)] = KenarlıkKalinligiEsle,
            };

    public YuvarlakResimHandler() : base(YuvarlakResimMapper) { }
}

// 4. Android handler implementasyonu
public partial class YuvarlakResimHandler
    : ImageHandler
{
    public static void KenarlıkRengiEsle(
        YuvarlakResimHandler handler, IYuvarlakResim gorunum)
    {
        // Null kontrolü artık gereksiz - framework tarafından yönetilir
        var platformGorunum = handler.PlatformView;
        // Kenarlık rengi güncelleme kodu...
    }

    public static void KenarlıkKalinligiEsle(
        YuvarlakResimHandler handler, IYuvarlakResim gorunum)
    {
        var platformGorunum = handler.PlatformView;
        // Kenarlık kalınlığı güncelleme kodu...
    }

    protected override void ConnectHandler(
        Android.Widget.ImageView platformView)
    {
        base.ConnectHandler(platformView);
        // OnElementChanged'deki başlatma kodu buraya taşınır
        platformView.SetScaleType(
            Android.Widget.ImageView.ScaleType.CenterCrop);
        platformView.SetClipToOutline(true);
    }
}

// 5. Kayıt (ExportRenderer attribute yerine)
// MauiProgram.cs içinde:
// handlers.AddHandler<YuvarlakResim, YuvarlakResimHandler>();

Geçiş Kontrol Listesi

Renderer'dan handler'a geçiş yaparken aşağıdaki adımları takip edin. Bu listeyi bir yere kaydetmenizi öneririm, çünkü her geçişte işinize yarayacak:

  1. Arayüz oluşturun: Kontrolünüz için bir IKontrolAdi arayüzü tanımlayın
  2. ExportRenderer attribute'ını kaldırın: Bunun yerine ConfigureMauiHandlers içinde kayıt yapın
  3. ViewRenderer yerine ViewHandler kullanın: Türetme sınıfını değiştirin
  4. OnElementChanged'i ConnectHandler'a taşıyın: Başlatma kodlarını aktarın
  5. OnElementPropertyChanged'i PropertyMapper'a dönüştürün: Her özellik için ayrı eşleme metodu yazın
  6. Element erişimini VirtualView ile değiştirin: Element.OzellikAdi yerine VirtualView.OzellikAdi kullanın
  7. Control erişimini PlatformView ile değiştirin: Control.OzellikAdi yerine PlatformView.OzellikAdi kullanın
  8. Dispose kalıbını DisconnectHandler ile değiştirin: Kaynak temizleme kodunu taşıyın

En İyi Uygulamalar ve İpuçları

Handler mimarisinden en iyi şekilde yararlanmak için dikkat etmeniz gereken bazı önemli noktalar var. Bunları projenizin başında öğrenmek, ileride sizi çok büyük baş ağrılarından kurtaracaktır.

Global ve Spesifik Özelleştirmeler

AppendToMapping ile yapılan değişiklikler tüm uygulama genelinde geçerli olur. Tutarlı bir görünüm sağlamak için harika ama bazen yalnızca belirli kontroller için özelleştirme yapmak istersiniz. Bu durumda alt sınıflama stratejisini kullanabilirsiniz:

// Yalnızca belirli kontroller için özelleştirme

// 1. Alt sınıf oluştur
public class KenarlıksızEntry : Entry { }

// 2. Sadece bu alt sınıf için handler özelleştirmesi yap
Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping(
    "KenarlıksızEntryStili", (handler, view) =>
{
    // Sadece KenarlıksızEntry türündeki kontroller için çalış
    if (view is not KenarlıksızEntry) return;

#if ANDROID
    handler.PlatformView.BackgroundTintList =
        Android.Content.Res.ColorStateList.ValueOf(
            Android.Graphics.Color.Transparent);
#elif IOS
    handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
#endif
});

// 3. XAML'de kullanım
// <kontroller:KenarlıksızEntry Placeholder="Kenarlıksız giriş..." />
// Normal Entry kontrolleri etkilenmez
// <Entry Placeholder="Normal giriş alanı" />

Performans Konusunda Dikkat Edilecekler

  • Mapper metotlarını hafif tutun: Eşleme metotları her özellik değişikliğinde çağrılır. Bu metotlarda ağır işlemler yapmaktan kaçının. Yerel görünüm oluşturma veya karmaşık hesaplamalar ConnectHandler içinde yapılmalıdır.
  • PlatformView'a gereksiz erişimden kaçının: Yerel görünüme her erişim bir platform çağrısı tetikler. Birden fazla özelliği aynı anda güncellemeniz gerekiyorsa değerleri önce hesaplayıp sonra tek seferde uygulayın.
  • DisconnectHandler'ı ihmal etmeyin: Olay dinleyicilerini ve kaynakları mutlaka serbest bırakın. Aksi halde bellek sızıntıları meydana gelir ve uzun süre çalışan uygulamalarda ciddi sorunlara yol açar.
// İyi uygulama: Performanslı mapper metodu
public static void ArkaplanRengiEsle(
    OzelHandler handler, IOzelKontrol gorunum)
{
    // Gereksiz güncellemeyi önle
    var mevcutRenk = handler.PlatformView.BackgroundColor;
    var yeniRenk = gorunum.ArkaplanRengi.ToPlatform();

    // Sadece renk değiştiyse güncelle
    if (!mevcutRenk.Equals(yeniRenk))
    {
        handler.PlatformView.BackgroundColor = yeniRenk;
    }
}

// Kötü uygulama: Her çağrıda gereksiz yeniden oluşturma
public static void ArkaplanRengiEsleKotu(
    OzelHandler handler, IOzelKontrol gorunum)
{
    // Her çağrıda yeni GradientDrawable oluşturmak gereksiz yük yaratır
    var arkaplan = new GradientDrawable();
    arkaplan.SetColor(gorunum.ArkaplanRengi.ToPlatform());
    arkaplan.SetCornerRadius(16f);
    handler.PlatformView.Background = arkaplan;
}

Handler Testlerinde En İyi Uygulamalar

Handler mimarisinin test edilebilirlik avantajından yararlanmak için arayüz tabanlı testler yazın. Bu sayede platform bağımlılıkları olmadan birim testleri çalıştırabilirsiniz.

// Birim test örneği
[TestClass]
public class DerecelendirmeCubuguHandlerTestleri
{
    [TestMethod]
    public void DegerEsleme_DegerGuncellendiginde_YildizlarDogruRenkte()
    {
        // Arrange
        var sahteGorunum = new Mock<IDerecelendirmeCubugu>();
        sahteGorunum.Setup(g => g.Deger).Returns(3.0);
        sahteGorunum.Setup(g => g.MaksimumDeger).Returns(5);
        sahteGorunum.Setup(g => g.SeciliRenk).Returns(Colors.Gold);
        sahteGorunum.Setup(g => g.BosRenk).Returns(Colors.LightGray);

        // Act
        var sonuc = DegeriDogrula(sahteGorunum.Object);

        // Assert
        Assert.IsTrue(sonuc);
    }

    private bool DegeriDogrula(IDerecelendirmeCubugu gorunum)
    {
        return gorunum.Deger == 3.0 &&
               gorunum.MaksimumDeger == 5;
    }
}

// Entegrasyon test örneği
[TestMethod]
public async Task DerecelendirmeCubugu_VeriBaslama_DogruDegerGosterir()
{
    var kontrol = new DerecelendirmeCubugu
    {
        Deger = 4,
        MaksimumDeger = 5,
        SeciliRenk = Colors.Gold
    };

    Assert.AreEqual(4, kontrol.Deger);
    Assert.AreEqual(5, kontrol.MaksimumDeger);
}

Katmanlı Özelleştirme Stratejisi

Büyük projelerde handler özelleştirmelerini organize etmek için katmanlı bir strateji benimsemek işleri çok kolaylaştırır. Şöyle düşünün: temel stiller en altta, tema bazlı değişiklikler ortada, sayfa/modül bazlı özelleştirmeler en üstte.

// Katmanlı özelleştirme organizasyonu

// 1. Temel stil katmanı - tüm uygulamada geçerli
public static class TemelStiller
{
    public static void Uygula()
    {
        EntryHandler.Mapper.AppendToMapping("TemelEntryStili",
            (handler, view) =>
        {
#if ANDROID
            handler.PlatformView.SetPadding(24, 12, 24, 12);
#endif
        });

        ButtonHandler.Mapper.AppendToMapping("TemelButtonStili",
            (handler, view) =>
        {
#if ANDROID
            if (handler.PlatformView is
                Google.Android.Material.Button.MaterialButton btn)
            {
                btn.CornerRadius = 12;
            }
#endif
        });
    }
}

// 2. Tema katmanı - tema bazlı değişiklikler
public static class TemaStilleri
{
    public static void KoyuTemaUygula()
    {
        EntryHandler.Mapper.AppendToMapping("KoyuTemaEntry",
            (handler, view) =>
        {
#if ANDROID
            handler.PlatformView.SetTextColor(
                Android.Graphics.Color.White);
            handler.PlatformView.SetHintTextColor(
                Android.Graphics.Color.LightGray);
#endif
        });
    }
}

// 3. Uygulama katmanı - MauiProgram.cs'de birleştirme
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .ConfigureMauiHandlers(handlers =>
        {
            // Katmanları sırayla uygula
            TemelStiller.Uygula();
            TemaStilleri.KoyuTemaUygula();

            // Özel handler'ları kaydet
            handlers.AddHandler<DerecelendirmeCubugu,
                DerecelendirmeCubuguHandler>();
        });

    return builder.Build();
}

Sonuç

.NET MAUI'nin handler mimarisi, mobil ve masaüstü uygulama geliştirmede kontrol özelleştirme paradigmasını kökten değiştiren bir yenilik. Xamarin.Forms'un renderer yaklaşımına kıyasla sunduğu avantajlar -- daha yalın kod yapısı, üstün performans, kolay test edilebilirlik ve esnek özelleştirme mekanizmaları -- onu modern çapraz platform geliştirmenin vazgeçilmez bir parçası haline getirdi.

Bu rehberde ele aldığımız temel konuları özetleyelim:

  • PropertyMapper ve CommandMapper, özellik ve komut eşlemelerini bildirimsel bir şekilde yönetmenizi sağlar. Kod hem okunabilir hem de bakımı kolay olur.
  • AppendToMapping, PrependToMapping ve ModifyMapping, mevcut kontrolleri alt sınıflama yapmadan özelleştirmenize olanak tanır.
  • Sıfırdan özel kontrol oluşturma süreci, sistematik ve öngörülebilir bir akış izler: arayüz, kontrol sınıfı, platform handler ve kayıt.
  • ConnectHandler ve DisconnectHandler, yaşam döngüsü yönetimini basitleştirerek bellek sızıntılarını önlemeyi kolaylaştırır.
  • Koşullu derleme ve kısmi sınıflar, platform bazlı kod organizasyonu için birbirini tamamlayan iki strateji sunar.
  • Renderer'dan handler'a geçiş, sistematik bir kontrol listesi izleyerek sorunsuz biçimde gerçekleştirilebilir.

Handler mimarisiyle çalışmaya başladığınızda bazı kavramlar yabancı gelebilir, bu çok normal. Ama temel prensipleri kavradıktan sonra eski renderer yaklaşımına geri dönmek istemeyeceksiniz, bunu garanti edebilirim.

Önerim şu: küçük adımlarla başlayın. Önce AppendToMapping ile mevcut kontrollerin görünümünü uyarlayın, sonra zamanla kendi handler'larınızı oluşturarak deneyim kazanın. Bu rehberdeki kod örneklerini kendi projelerinize uyarlayarak handler mimarisinin tüm inceliklerini keşfedebilirsiniz.

Yazar Hakkında Editorial Team

Our team of expert writers and editors.