Migrate
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user