Files
pos-system/microservices/.agent/skills/maui-branding-expert/guidelines/handler-customization.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

11 KiB

Handler Customization / Tùy Biến Handler

Overview / Tổng Quan

.NET MAUI sử dụng Handlers thay vì Custom Renderers (Xamarin.Forms legacy). Handlers cung cấp cách tiếp cận hiện đại, hiệu quả hơn để tùy chỉnh native controls.

┌─────────────────────────────────────────────────────────────┐
│                    HANDLER ARCHITECTURE                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌──────────────┐                  ┌──────────────────┐   │
│   │  MAUI View   │ ─── Handler ───► │   Native View    │   │
│   │   (Entry)    │                  │ (EditText/UITxt) │   │
│   └──────────────┘                  └──────────────────┘   │
│          │                                   │              │
│          │    PropertyMapper                 │              │
│          └──────────────────────────────────►│              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Customization Levels / Các Cấp Độ Tùy Biến

Level 1: XAML Styles (Ưu tiên cao nhất)

Dùng cho thay đổi properties cơ bản:

<Style TargetType="Entry" x:Key="BrandEntry">
    <Setter Property="BackgroundColor" Value="{StaticResource SurfaceCard}" />
    <Setter Property="TextColor" Value="{StaticResource TextPrimary}" />
    <Setter Property="PlaceholderColor" Value="{StaticResource TextSecondary}" />
</Style>

Level 2: ControlTemplate

Dùng khi cần thay đổi cấu trúc visual:

<ControlTemplate x:Key="RoundedEntryTemplate">
    <Border StrokeThickness="1"
            Stroke="{StaticResource BorderDefault}"
            StrokeShape="RoundRectangle 8">
        <ContentPresenter Padding="12,8" />
    </Border>
</ControlTemplate>

<!-- Usage -->
<Entry ControlTemplate="{StaticResource RoundedEntryTemplate}" />

Level 3: Handlers (Khi cần Native API)

Dùng khi XAML không đủ (bỏ underline, đổi cursor color, etc.):

// ⚠️ CHỈ dùng khi cần thiết
EntryHandler.Mapper.AppendToMapping("NoBorder", (handler, view) =>
{
#if ANDROID
    handler.PlatformView.SetBackgroundColor(Android.Graphics.Color.Transparent);
#elif IOS
    handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
#endif
});

Handler Mapping Methods

Thêm customization sau default mapping:

public static class EntryHandlerExtensions
{
    public static void ConfigureBrandEntry(this MauiAppBuilder builder)
    {
        EntryHandler.Mapper.AppendToMapping("BrandEntry", (handler, view) =>
        {
#if ANDROID
            // Bỏ underline mặc định
            handler.PlatformView.SetBackgroundColor(Android.Graphics.Color.Transparent);
            
            // Đổi màu cursor
            if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Q)
            {
                handler.PlatformView.SetTextCursorDrawable(null);
            }
#elif IOS
            // Bỏ border
            handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
#endif
        });
    }
}

// MauiProgram.cs
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder.ConfigureBrandEntry();
    return builder.Build();
}

PrependToMapping

Thêm customization trước default mapping:

// Ít dùng - chỉ khi cần override trước
EntryHandler.Mapper.PrependToMapping("BeforeDefault", (handler, view) =>
{
    // Code chạy TRƯỚC default mapping
});

ModifyMapping (Nguy hiểm)

Thay thế hoàn toàn một property mapping:

// ⚠️ NGUY HIỂM - có thể break default behavior
EntryHandler.Mapper.ModifyMapping(nameof(Entry.Text), (handler, view, action) =>
{
    // Thay thế hoàn toàn cách xử lý Text property
    action?.Invoke(handler, view);
});

Complete Handler Examples

BrandEntry Handler (Bỏ Underline)

// Handlers/BrandEntryHandler.cs
namespace MyApp.Handlers;

public static class BrandEntryHandler
{
    /// <summary>
    /// Configure Entry để bỏ underline mặc định trên Android
    /// và bỏ border trên iOS
    /// </summary>
    public static void Configure()
    {
        Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping(
            "BrandEntry", 
            (handler, view) =>
        {
#if ANDROID
            ConfigureAndroid(handler);
#elif IOS || MACCATALYST
            ConfigureIOS(handler);
#endif
        });
    }

#if ANDROID
    private static void ConfigureAndroid(
        Microsoft.Maui.Handlers.EntryHandler handler)
    {
        // Bỏ background/underline mặc định
        handler.PlatformView.SetBackgroundColor(
            Android.Graphics.Color.Transparent);
        
        // Optional: Select all on focus
        handler.PlatformView.SetSelectAllOnFocus(true);
    }
#endif

#if IOS || MACCATALYST
    private static void ConfigureIOS(
        Microsoft.Maui.Handlers.EntryHandler handler)
    {
        // Bỏ border
        handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
        
        // Optional: Clear button
        handler.PlatformView.ClearButtonMode = 
            UIKit.UITextFieldViewMode.WhileEditing;
    }
#endif
}

BrandPicker Handler (Custom Arrow)

namespace MyApp.Handlers;

public static class BrandPickerHandler
{
    public static void Configure()
    {
        Microsoft.Maui.Handlers.PickerHandler.Mapper.AppendToMapping(
            "BrandPicker",
            (handler, view) =>
        {
#if ANDROID
            // Bỏ underline
            handler.PlatformView.SetBackgroundColor(
                Android.Graphics.Color.Transparent);
#elif IOS
            // Custom styling
            handler.PlatformView.BorderStyle = 
                UIKit.UITextBorderStyle.None;
#endif
        });
    }
}

BrandEditor Handler (Multi-line Text)

namespace MyApp.Handlers;

public static class BrandEditorHandler
{
    public static void Configure()
    {
        Microsoft.Maui.Handlers.EditorHandler.Mapper.AppendToMapping(
            "BrandEditor",
            (handler, view) =>
        {
#if ANDROID
            // Bỏ background
            handler.PlatformView.SetBackgroundColor(
                Android.Graphics.Color.Transparent);
            
            // Set max lines
            handler.PlatformView.SetMaxLines(5);
#elif IOS
            // Customize TextView
            handler.PlatformView.Layer.BorderWidth = 0;
#endif
        });
    }
}

Cross-Platform Partial Classes

Khi code native dài, tách thành Partial Classes:

Shared Interface

// Services/IBrandingService.cs
namespace MyApp.Services;

public interface IBrandingService
{
    void ConfigureEntry(object platformView);
    void ConfigurePicker(object platformView);
}

Partial Implementation

// Services/BrandingService.cs (shared)
namespace MyApp.Services;

public partial class BrandingService : IBrandingService
{
    public partial void ConfigureEntry(object platformView);
    public partial void ConfigurePicker(object platformView);
}
// Platforms/Android/Services/BrandingService.Android.cs
namespace MyApp.Services;

public partial class BrandingService
{
    public partial void ConfigureEntry(object platformView)
    {
        if (platformView is AndroidX.AppCompat.Widget.AppCompatEditText editText)
        {
            editText.SetBackgroundColor(Android.Graphics.Color.Transparent);
            editText.SetSelectAllOnFocus(true);
        }
    }
    
    public partial void ConfigurePicker(object platformView)
    {
        if (platformView is AndroidX.AppCompat.Widget.AppCompatEditText editText)
        {
            editText.SetBackgroundColor(Android.Graphics.Color.Transparent);
        }
    }
}
// Platforms/iOS/Services/BrandingService.iOS.cs
namespace MyApp.Services;

public partial class BrandingService
{
    public partial void ConfigureEntry(object platformView)
    {
        if (platformView is UIKit.UITextField textField)
        {
            textField.BorderStyle = UIKit.UITextBorderStyle.None;
            textField.ClearButtonMode = UIKit.UITextFieldViewMode.WhileEditing;
        }
    }
    
    public partial void ConfigurePicker(object platformView)
    {
        if (platformView is UIKit.UITextField textField)
        {
            textField.BorderStyle = UIKit.UITextBorderStyle.None;
        }
    }
}

MauiProgram.cs Integration

using MyApp.Handlers;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseMauiCommunityToolkit()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("Inter-Regular.ttf", "InterRegular");
                fonts.AddFont("Inter-Bold.ttf", "InterBold");
            });
        
        // ✅ Configure all brand handlers
        ConfigureBrandHandlers();
        
        return builder.Build();
    }
    
    private static void ConfigureBrandHandlers()
    {
        BrandEntryHandler.Configure();
        BrandPickerHandler.Configure();
        BrandEditorHandler.Configure();
    }
}

Common Mistakes / Lỗi Thường Gặp

DON'T: Use Custom Renderers

// ❌ LEGACY - Không dùng nữa
[assembly: ExportRenderer(typeof(Entry), typeof(CustomEntryRenderer))]
public class CustomEntryRenderer : EntryRenderer
{
    // Old Xamarin.Forms approach
}

DO: Use Handlers

// ✅ MODERN - .NET MAUI approach
EntryHandler.Mapper.AppendToMapping("Custom", (h, v) => { });

DON'T: Use #if everywhere

// ❌ Code pollution
public void Configure()
{
#if ANDROID
    // 50 lines of Android code
#elif IOS
    // 50 lines of iOS code
#elif WINDOWS
    // 50 lines of Windows code
#endif
}

DO: Use Partial Classes

// ✅ Clean separation
// Services/MyService.cs
public partial class MyService
{
    public partial void DoPlatformWork();
}

// Platforms/Android/Services/MyService.Android.cs
public partial class MyService
{
    public partial void DoPlatformWork()
    {
        // Android-specific
    }
}

Quick Reference

Task Method
Thay đổi màu/font XAML Styles
Thay đổi cấu trúc UI ControlTemplate
Bỏ underline Entry Handler + AppendToMapping
Đổi cursor color Handler + Native API
Thêm clear button (iOS) Handler + UITextField
Custom dropdown arrow Handler + Native Drawable