This commit is contained in:
Ho Ngoc Hai
2026-05-23 18:37:02 +07:00
parent f15d91ee29
commit 76d75c753b
3993 changed files with 403 additions and 0 deletions

View File

@@ -0,0 +1,446 @@
# 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:
```xml
<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:
```xml
<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.):
```csharp
// ⚠️ 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
### AppendToMapping (Recommended)
Thêm customization **sau** default mapping:
```csharp
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:
```csharp
// Í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:
```csharp
// ⚠️ 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)
```csharp
// 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)
```csharp
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)
```csharp
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
```csharp
// Services/IBrandingService.cs
namespace MyApp.Services;
public interface IBrandingService
{
void ConfigureEntry(object platformView);
void ConfigurePicker(object platformView);
}
```
### Partial Implementation
```csharp
// Services/BrandingService.cs (shared)
namespace MyApp.Services;
public partial class BrandingService : IBrandingService
{
public partial void ConfigureEntry(object platformView);
public partial void ConfigurePicker(object platformView);
}
```
```csharp
// 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);
}
}
}
```
```csharp
// 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
```csharp
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
```csharp
// ❌ LEGACY - Không dùng nữa
[assembly: ExportRenderer(typeof(Entry), typeof(CustomEntryRenderer))]
public class CustomEntryRenderer : EntryRenderer
{
// Old Xamarin.Forms approach
}
```
### ✅ DO: Use Handlers
```csharp
// ✅ MODERN - .NET MAUI approach
EntryHandler.Mapper.AppendToMapping("Custom", (h, v) => { });
```
### ❌ DON'T: Use `#if` everywhere
```csharp
// ❌ 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
```csharp
// ✅ 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 |
---
## Related Guidelines
- [Resource Architecture](./resource-architecture.md) - XAML Resources
- [Accessibility Rules](./accessibility-rules.md) - WCAG compliance