feat: Thêm các tài liệu hướng dẫn và tham khảo mới cho kỹ năng MAUI Branding Expert và MAUI Enterprise Architect.

This commit is contained in:
Ho Ngoc Hai
2026-01-15 23:07:55 +07:00
parent 76629ab7d3
commit 3d5a6cb218
11 changed files with 3194 additions and 0 deletions

View File

@@ -0,0 +1,281 @@
---
name: maui-branding-expert
description: Xây dựng hệ thống UI/UX chuẩn Enterprise cho .NET MAUI, bao gồm Resource Dictionary, Control Templates, Custom Handlers và Accessibility. Use for Design System, branding controls, hoặc khi cần standardized UI components.
compatibility: ".NET 9+ MAUI, CommunityToolkit.Maui 9+"
metadata:
author: Velik Ho
version: "1.0"
references: "Microsoft .NET MAUI Documentation, Enterprise Application Patterns"
---
# .NET MAUI Branding Controls Workflow
Bạn là một chuyên gia về UI/UX và XAML trong .NET MAUI. Nhiệm vụ của bạn là hiện thực hóa Design System thành code có khả năng tái sử dụng cao (Reusability) và dễ bảo trì (Maintainability). **Tuyệt đối không hard-code màu sắc hay kích thước trực tiếp trong View.**
## When to Use This Skill / Khi Nào Sử Dụng
Use this skill when:
- Xây dựng Design System / UI Kit cho app
- Tạo custom branded controls (BrandButton, BrandEntry, CardView...)
- Thiết lập Resource Dictionary architecture
- Tùy chỉnh native controls với Handlers
- Đảm bảo Accessibility (WCAG) compliance
**DO NOT use when:**
- Simple prototyping / Làm prototype nhanh
- Single-use UI components / Component dùng 1 lần
- Blazor Hybrid apps (use CSS/Blazor patterns)
## Overview / Tổng Quan
```
┌──────────────────────────────────────────────────────────────────┐
│ WORKFLOW XÂY DỰNG BRANDING CONTROLS │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ PHASE 1 │────►│ PHASE 2 │ │
│ │ RESOURCE │ │ CONTROL │ │
│ │ DICTIONARY │ │ CUSTOMIZATION │ │
│ │ - Colors │ │ - Styles (L1) │ │
│ │ - Typography│ │ - Templates(L2) │ │
│ │ - Theme │ │ - Handlers (L3) │ │
│ └─────────────┘ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ PHASE 4 │◄────│ PHASE 3 │ │
│ │ ACCESSIBILITY│ │ HANDLER │ │
│ │ │ │ MODIFICATION │ │
│ │ - Semantic │ │ - AppendMapping │ │
│ │ - HeadingLv │ │ - PlatformView │ │
│ │ - FontScale │ │ - Partial Class │ │
│ └─────────────┘ └─────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
```
---
## Phase 1: Resource Dictionary Architecture / Kiến Trúc Resource
**Goal**: Tổ chức tài nguyên UI để dễ dàng thay đổi thương hiệu (Re-branding).
**Detailed Guide**: [Resource Architecture](./guidelines/resource-architecture.md)
### Nguyên tắc cốt lõi
1. **Phân tách Resource** - Không gộp tất cả vào `App.xaml`
```
Resources/Styles/
├── Colors.xaml # Bảng màu (Color Palette)
├── Typography.xaml # Font, FontSize, LineHeight
└── Theme.xaml # Styles cho controls
```
2. **Merged Dictionaries** - Gộp files theo thứ tự ưu tiên trong `App.xaml`
3. **AppThemeBinding** - Luôn định nghĩa màu cho cả Light/Dark Mode
### Quick Pattern
```xml
<!-- Colors.xaml -->
<Color x:Key="BrandPrimary">{AppThemeBinding
Light=#2196F3, Dark=#64B5F6}</Color>
<Color x:Key="BrandSurface">{AppThemeBinding
Light=#FFFFFF, Dark=#1E1E1E}</Color>
```
---
## Phase 2: Control Customization Strategy / Chiến Lược Tùy Biến
**Goal**: Tùy chỉnh controls theo thứ tự ưu tiên phù hợp.
**Detailed Guide**: [Handler Customization](./guidelines/handler-customization.md)
### Thứ tự ưu tiên
| Level | Method | Use When |
|-------|--------|----------|
| **L1** | XAML Styles + VSM | Thay đổi cơ bản (màu, bo góc, hover) |
| **L2** | ControlTemplate | Thay đổi cấu trúc visual hoàn toàn |
| **L3** | Handlers | Can thiệp Native API (bỏ underline, đổi cursor) |
### Level 1: XAML Styles
```xml
<Style TargetType="Button" x:Key="BrandButton">
<Setter Property="BackgroundColor"
Value="{StaticResource BrandPrimary}" />
<Setter Property="TextColor" Value="White" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Property="Scale" Value="0.98" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
```
### Level 2: ControlTemplate
```xml
<ControlTemplate x:Key="CardTemplate">
<Frame CornerRadius="12"
BackgroundColor="{StaticResource BrandSurface}"
HasShadow="True">
<ContentPresenter />
</Frame>
</ControlTemplate>
```
### Level 3: Handlers (Advanced)
```csharp
// BẮT BUỘC: Sử dụng AppendToMapping, KHÔNG dùng Custom Renderer
EntryHandler.Mapper.AppendToMapping("BrandEntry", (handler, view) =>
{
#if ANDROID
handler.PlatformView.SetBackgroundColor(Android.Graphics.Color.Transparent);
#elif IOS
handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
#endif
});
```
---
## Phase 3: Handler Modification / Sửa Đổi Handler
**Goal**: Can thiệp sâu vào Native View một cách an toàn.
**Detailed Guide**: [Handler Customization](./guidelines/handler-customization.md)
### Key Patterns
1. **AppendToMapping** - Thêm tùy chỉnh vào cuối chuỗi xử lý mặc định
2. **PrependToMapping** - Thêm vào đầu chuỗi (ít dùng)
3. **ModifyMapping** - Thay thế hoàn toàn (nguy hiểm, tránh dùng)
### Cross-Platform Partial Classes
Khi code native quá dài, tách thành Partial Classes:
```csharp
// Services/BrandingService.cs (shared)
public partial class BrandingService
{
public partial void ConfigureEntry(IEntry entry);
}
// Platforms/Android/Services/BrandingService.cs
public partial class BrandingService
{
public partial void ConfigureEntry(IEntry entry)
{
// Android-specific implementation
}
}
```
---
## Phase 4: Accessibility (Enterprise Required) / Tính Năng Truy Cập
**Goal**: Tuân thủ chuẩn WCAG cho ứng dụng Enterprise.
**Detailed Guide**: [Accessibility Rules](./guidelines/accessibility-rules.md)
### Key Requirements
1. **SemanticProperties** - Thay thế AutomationProperties (đã cũ)
```xml
<Button Text="Submit"
SemanticProperties.Description="Submit form button"
SemanticProperties.Hint="Double tap to submit the form" />
```
2. **HeadingLevel** - Cho phép screen reader điều hướng đúng
```xml
<Label Text="Settings"
SemanticProperties.HeadingLevel="Level1" />
```
3. **Font Scaling** - UI không bị vỡ khi người dùng tăng font size
- Dùng Grid/FlexLayout thay vì HeightRequest cố định
- Test với System Font Scale 200%
---
## Image & Icon Resources
### Single Project Resources
Đặt file SVG/PNG vào `Resources/Images`, MAUI tự động resize:
```xml
<!-- .csproj -->
<MauiImage Include="Resources\Images\*" />
<MauiIcon Include="Resources\AppIcon\appicon.svg" />
```
**KHÔNG CẦN** tạo `drawable-xhdpi`, `drawable-xxhdpi` thủ công.
---
## Common Mistakes / Lỗi Thường Gặp
| Mistake | Problem | Solution |
|---------|---------|----------|
| Hard-coded colors | Không thể re-brand | Dùng `{StaticResource}` |
| All resources in App.xaml | Khó maintain | Tách thành files riêng |
| Custom Renderers | Legacy Xamarin.Forms | Dùng Handlers |
| Missing Dark Mode | Poor UX | Dùng AppThemeBinding |
| AutomationProperties | Deprecated | Dùng SemanticProperties |
| Fixed HeightRequest | Vỡ UI khi font scale | Dùng Grid/FlexLayout |
---
## Quick Reference / Tham Chiếu Nhanh
| Category | Standard |
|----------|----------|
| Resource Organization | Tách `Colors.xaml`, `Typography.xaml`, `Theme.xaml` |
| Theme Support | `{AppThemeBinding Light=X, Dark=Y}` |
| Style Level 1 | XAML Styles + VisualStateManager |
| Style Level 2 | ControlTemplate + ContentPresenter |
| Style Level 3 | Handlers + AppendToMapping |
| Accessibility | SemanticProperties (NOT AutomationProperties) |
| Images | SVG preferred, `MauiImage` build action |
| Platform Code | Partial Classes trong `Platforms/` |
---
## Resources / Tài Nguyên
### Guidelines
- [Resource Architecture](./guidelines/resource-architecture.md) - ResourceDictionary patterns
- [Handler Customization](./guidelines/handler-customization.md) - Native control customization
- [Accessibility Rules](./guidelines/accessibility-rules.md) - WCAG compliance
- [Typography & Theme](./guidelines/typography-theme.md) - Font & Dark/Light mode
### Related Skills
- [MAUI Enterprise Architect](../maui-enterprise-architect/SKILL.md) - MVVM, DI, Shell patterns
- [Project Rules](../project-rules/SKILL.md) - GoodGo coding standards
### External
- [Microsoft MAUI Handlers](https://learn.microsoft.com/en-us/dotnet/maui/user-interface/handlers/)
- [MAUI Styles & Themes](https://learn.microsoft.com/en-us/dotnet/maui/user-interface/styles/xaml)
- [MAUI Accessibility](https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/accessibility)

View File

@@ -0,0 +1,101 @@
# Accessibility Rules / Quy Tắc Truy Cập
## Overview
Ứng dụng Enterprise **BẮT BUỘC** tuân thủ chuẩn WCAG. .NET MAUI sử dụng `SemanticProperties` (thay thế `AutomationProperties` cũ).
---
## 1. SemanticProperties
```xml
<!-- ✅ MODERN -->
<Button Text="Submit"
SemanticProperties.Description="Submit button"
SemanticProperties.Hint="Double tap to submit form" />
<!-- ❌ DEPRECATED -->
<Button AutomationProperties.Name="Submit" />
```
| Property | Purpose |
|----------|---------|
| `Description` | Mô tả ngắn - đọc khi focus |
| `Hint` | Hướng dẫn chi tiết |
| `HeadingLevel` | Heading hierarchy (Level1-9) |
---
## 2. Heading Levels
```xml
<Label Text="Settings"
SemanticProperties.HeadingLevel="Level1" />
<Label Text="Account"
SemanticProperties.HeadingLevel="Level2" />
```
---
## 3. Interactive Elements
```xml
<!-- Image buttons PHẢI có Description -->
<ImageButton Source="cart.png"
SemanticProperties.Description="Shopping cart"
SemanticProperties.Hint="Tap to view cart" />
<!-- Entry với label -->
<Label Text="Email" x:Name="EmailLabel" />
<Entry SemanticProperties.LabeledBy="{x:Reference EmailLabel}" />
```
---
## 4. Font Scaling
### ❌ DON'T: Fixed Heights
```xml
<Label HeightRequest="50" />
```
### ✅ DO: Flexible Layouts
```xml
<Grid RowDefinitions="Auto,Auto,*">
<Label Grid.Row="0" LineBreakMode="WordWrap" />
</Grid>
```
---
## 5. Touch Target Size
| Platform | Minimum |
|----------|---------|
| iOS | 44x44 |
| Android | 48x48 |
```xml
<Button MinimumHeightRequest="48" MinimumWidthRequest="48" />
```
---
## 6. Programmatic Announcements
```csharp
SemanticScreenReader.Announce("Order placed successfully");
```
---
## Test Checklist
- [ ] Buttons có Description
- [ ] Images có Description (trừ decorative)
- [ ] Headings có HeadingLevel
- [ ] UI không vỡ ở font scale 200%
- [ ] Touch targets >= 48dp

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

View File

@@ -0,0 +1,344 @@
# Resource Dictionary Architecture / Kiến Trúc Resource Dictionary
## Overview / Tổng Quan
Resource Dictionary là nền tảng của Design System trong .NET MAUI. Việc tổ chức đúng cách giúp:
- **Re-branding dễ dàng**: Chỉ cần thay đổi 1 file
- **Maintainability**: Code UI sạch, không lặp lại
- **Consistency**: Đảm bảo UI đồng nhất toàn app
---
## Directory Structure / Cấu Trúc Thư Mục
```
Resources/
├── Styles/
│ ├── Colors.xaml # Bảng màu chính
│ ├── Typography.xaml # Font families, sizes
│ ├── Spacing.xaml # Margins, paddings
│ └── Theme.xaml # Styles cho controls
├── Fonts/
│ ├── Inter-Regular.ttf
│ └── Inter-Bold.ttf
└── Images/
├── logo.svg
└── icons/
```
---
## 1. Colors.xaml - Bảng Màu
### Nguyên tắc đặt tên
- **Brand Colors**: `Brand*` (Primary, Secondary, Accent)
- **Semantic Colors**: `Color*` (Success, Warning, Error, Info)
- **Surface Colors**: `Surface*` (Background, Card, Overlay)
- **Text Colors**: `Text*` (Primary, Secondary, Disabled)
### Complete Example
```xml
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Resources.Styles.Colors">
<!-- ==================== BRAND COLORS ==================== -->
<!-- Primary brand color - used for main CTAs -->
<Color x:Key="BrandPrimary">#2196F3</Color>
<Color x:Key="BrandPrimaryDark">#1976D2</Color>
<Color x:Key="BrandPrimaryLight">#64B5F6</Color>
<!-- Secondary brand color - used for secondary actions -->
<Color x:Key="BrandSecondary">#FF5722</Color>
<!-- ==================== SEMANTIC COLORS ==================== -->
<Color x:Key="ColorSuccess">#4CAF50</Color>
<Color x:Key="ColorWarning">#FF9800</Color>
<Color x:Key="ColorError">#F44336</Color>
<Color x:Key="ColorInfo">#2196F3</Color>
<!-- ==================== SURFACE COLORS (Theme-aware) ==================== -->
<Color x:Key="SurfaceBackground">
{AppThemeBinding Light=#FAFAFA, Dark=#121212}
</Color>
<Color x:Key="SurfaceCard">
{AppThemeBinding Light=#FFFFFF, Dark=#1E1E1E}
</Color>
<Color x:Key="SurfaceOverlay">
{AppThemeBinding Light=#00000033, Dark=#FFFFFF33}
</Color>
<!-- ==================== TEXT COLORS (Theme-aware) ==================== -->
<Color x:Key="TextPrimary">
{AppThemeBinding Light=#212121, Dark=#FFFFFF}
</Color>
<Color x:Key="TextSecondary">
{AppThemeBinding Light=#757575, Dark=#B0B0B0}
</Color>
<Color x:Key="TextDisabled">
{AppThemeBinding Light=#9E9E9E, Dark=#616161}
</Color>
<Color x:Key="TextOnPrimary">#FFFFFF</Color>
<!-- ==================== BORDER COLORS ==================== -->
<Color x:Key="BorderDefault">
{AppThemeBinding Light=#E0E0E0, Dark=#424242}
</Color>
<Color x:Key="BorderFocused">{StaticResource BrandPrimary}</Color>
</ResourceDictionary>
```
---
## 2. Typography.xaml - Font & Text Styles
### Font Registration (MauiProgram.cs)
```csharp
builder.ConfigureFonts(fonts =>
{
fonts.AddFont("Inter-Regular.ttf", "InterRegular");
fonts.AddFont("Inter-Medium.ttf", "InterMedium");
fonts.AddFont("Inter-Bold.ttf", "InterBold");
fonts.AddFont("Inter-SemiBold.ttf", "InterSemiBold");
});
```
### Typography Scale
```xml
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Resources.Styles.Typography">
<!-- ==================== FONT FAMILIES ==================== -->
<OnPlatform x:Key="FontFamilyRegular" x:TypeArguments="x:String">
<On Platform="Android, iOS" Value="InterRegular" />
<On Platform="WinUI" Value="Assets/Fonts/Inter-Regular.ttf#Inter" />
</OnPlatform>
<OnPlatform x:Key="FontFamilyBold" x:TypeArguments="x:String">
<On Platform="Android, iOS" Value="InterBold" />
<On Platform="WinUI" Value="Assets/Fonts/Inter-Bold.ttf#Inter" />
</OnPlatform>
<!-- ==================== FONT SIZES (Scalable) ==================== -->
<!-- Base size: 16 (Body) -->
<x:Double x:Key="FontSizeCaption">12</x:Double>
<x:Double x:Key="FontSizeBody">16</x:Double>
<x:Double x:Key="FontSizeSubtitle">18</x:Double>
<x:Double x:Key="FontSizeTitle">24</x:Double>
<x:Double x:Key="FontSizeHeadline">32</x:Double>
<x:Double x:Key="FontSizeDisplay">48</x:Double>
<!-- ==================== TEXT STYLES ==================== -->
<Style x:Key="TextCaption" TargetType="Label">
<Setter Property="FontFamily" Value="{StaticResource FontFamilyRegular}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeCaption}" />
<Setter Property="TextColor" Value="{StaticResource TextSecondary}" />
</Style>
<Style x:Key="TextBody" TargetType="Label">
<Setter Property="FontFamily" Value="{StaticResource FontFamilyRegular}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
<Setter Property="TextColor" Value="{StaticResource TextPrimary}" />
</Style>
<Style x:Key="TextTitle" TargetType="Label">
<Setter Property="FontFamily" Value="{StaticResource FontFamilyBold}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeTitle}" />
<Setter Property="TextColor" Value="{StaticResource TextPrimary}" />
</Style>
<Style x:Key="TextHeadline" TargetType="Label">
<Setter Property="FontFamily" Value="{StaticResource FontFamilyBold}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeHeadline}" />
<Setter Property="TextColor" Value="{StaticResource TextPrimary}" />
<Setter Property="SemanticProperties.HeadingLevel" Value="Level1" />
</Style>
</ResourceDictionary>
```
---
## 3. Theme.xaml - Control Styles
### Implicit vs Explicit Styles
| Type | Syntax | Use Case |
|------|--------|----------|
| **Implicit** | `<Style TargetType="Button">` | Default cho tất cả Button |
| **Explicit** | `<Style x:Key="PrimaryButton" TargetType="Button">` | Áp dụng có chọn lọc |
### Complete Example
```xml
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Resources.Styles.Theme">
<!-- ==================== BUTTON STYLES ==================== -->
<!-- Primary Button (Filled) -->
<Style x:Key="ButtonPrimary" TargetType="Button">
<Setter Property="BackgroundColor" Value="{StaticResource BrandPrimary}" />
<Setter Property="TextColor" Value="{StaticResource TextOnPrimary}" />
<Setter Property="FontFamily" Value="{StaticResource FontFamilyBold}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="24,12" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Property="BackgroundColor"
Value="{StaticResource BrandPrimaryDark}" />
<Setter Property="Scale" Value="0.98" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="BackgroundColor"
Value="{StaticResource TextDisabled}" />
<Setter Property="TextColor"
Value="{StaticResource SurfaceCard}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<!-- Secondary Button (Outlined) -->
<Style x:Key="ButtonSecondary" TargetType="Button">
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="TextColor" Value="{StaticResource BrandPrimary}" />
<Setter Property="BorderColor" Value="{StaticResource BrandPrimary}" />
<Setter Property="BorderWidth" Value="2" />
<Setter Property="FontFamily" Value="{StaticResource FontFamilyBold}" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="24,12" />
</Style>
<!-- ==================== ENTRY STYLES ==================== -->
<Style x:Key="EntryDefault" TargetType="Entry">
<Setter Property="BackgroundColor" Value="{StaticResource SurfaceCard}" />
<Setter Property="TextColor" Value="{StaticResource TextPrimary}" />
<Setter Property="PlaceholderColor" Value="{StaticResource TextSecondary}" />
<Setter Property="FontFamily" Value="{StaticResource FontFamilyRegular}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
</Style>
<!-- ==================== FRAME/CARD STYLES ==================== -->
<Style x:Key="CardDefault" TargetType="Frame">
<Setter Property="BackgroundColor" Value="{StaticResource SurfaceCard}" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="HasShadow" Value="True" />
<Setter Property="Padding" Value="16" />
<Setter Property="BorderColor" Value="Transparent" />
</Style>
</ResourceDictionary>
```
---
## 4. Merged Dictionaries trong App.xaml
### Thứ tự quan trọng
```xml
<!-- App.xaml -->
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- 1. Colors FIRST (base values) -->
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<!-- 2. Typography (depends on nothing) -->
<ResourceDictionary Source="Resources/Styles/Typography.xaml" />
<!-- 3. Spacing (if any) -->
<ResourceDictionary Source="Resources/Styles/Spacing.xaml" />
<!-- 4. Theme LAST (depends on Colors, Typography) -->
<ResourceDictionary Source="Resources/Styles/Theme.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
```
---
## Best Practices / Thực Hành Tốt Nhất
### ✅ DO
```xml
<!-- Dùng StaticResource cho performance -->
<Button BackgroundColor="{StaticResource BrandPrimary}" />
<!-- Dùng AppThemeBinding cho theme-aware colors -->
<Color x:Key="Background">{AppThemeBinding Light=#FFF, Dark=#000}</Color>
<!-- Đặt tên có prefix rõ ràng -->
<Color x:Key="BrandPrimary" />
<Color x:Key="TextPrimary" />
<Style x:Key="ButtonPrimary" />
```
### ❌ DON'T
```xml
<!-- KHÔNG hard-code màu -->
<Button BackgroundColor="#2196F3" />
<!-- KHÔNG đặt tên mơ hồ -->
<Color x:Key="Blue" />
<Color x:Key="MyColor" />
<!-- KHÔNG gộp tất cả vào App.xaml -->
<Application.Resources>
<Color x:Key="Primary" />
<Color x:Key="Secondary" />
<!-- 500 lines more... -->
</Application.Resources>
```
---
## Re-branding Workflow
Khi cần thay đổi thương hiệu:
1. **Chỉ sửa `Colors.xaml`** - Thay đổi BrandPrimary, BrandSecondary
2. **Cập nhật fonts** nếu cần trong `Typography.xaml`
3. **Rebuild app** - Tất cả UI tự động cập nhật
```diff
<!-- Colors.xaml -->
- <Color x:Key="BrandPrimary">#2196F3</Color>
+ <Color x:Key="BrandPrimary">#FF5733</Color>
```
---
## Related Guidelines
- [Typography & Theme](./typography-theme.md) - Chi tiết về Font và Dark/Light mode
- [Handler Customization](./handler-customization.md) - Tùy biến native controls

View File

@@ -0,0 +1,130 @@
# Typography & Theme / Font và Chủ Đề
## 1. Font Registration
```csharp
// MauiProgram.cs
builder.ConfigureFonts(fonts =>
{
fonts.AddFont("Inter-Regular.ttf", "InterRegular");
fonts.AddFont("Inter-Bold.ttf", "InterBold");
});
```
---
## 2. Typography Scale
```xml
<!-- Typography.xaml -->
<x:Double x:Key="FontSizeCaption">12</x:Double>
<x:Double x:Key="FontSizeBody">16</x:Double>
<x:Double x:Key="FontSizeTitle">24</x:Double>
<x:Double x:Key="FontSizeHeadline">32</x:Double>
<Style x:Key="TextBody" TargetType="Label">
<Setter Property="FontFamily" Value="InterRegular" />
<Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
<Setter Property="TextColor" Value="{StaticResource TextPrimary}" />
</Style>
<Style x:Key="TextHeadline" TargetType="Label">
<Setter Property="FontFamily" Value="InterBold" />
<Setter Property="FontSize" Value="{StaticResource FontSizeHeadline}" />
<Setter Property="SemanticProperties.HeadingLevel" Value="Level1" />
</Style>
```
---
## 3. Dark/Light Mode
### AppThemeBinding
```xml
<!-- Colors.xaml -->
<Color x:Key="SurfaceBackground">
{AppThemeBinding Light=#FAFAFA, Dark=#121212}
</Color>
<Color x:Key="TextPrimary">
{AppThemeBinding Light=#212121, Dark=#FFFFFF}
</Color>
```
### Programmatic Theme Change
```csharp
// Get current theme
var theme = Application.Current.RequestedTheme;
// Force theme
Application.Current.UserAppTheme = AppTheme.Dark;
// Reset to system
Application.Current.UserAppTheme = AppTheme.Unspecified;
```
---
## 4. OnPlatform Fonts
```xml
<OnPlatform x:Key="FontFamilyRegular" x:TypeArguments="x:String">
<On Platform="Android, iOS" Value="InterRegular" />
<On Platform="WinUI" Value="Assets/Fonts/Inter-Regular.ttf#Inter" />
</OnPlatform>
```
---
## 5. Complete Theme.xaml
```xml
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<!-- Button Styles -->
<Style x:Key="ButtonPrimary" TargetType="Button">
<Setter Property="BackgroundColor" Value="{StaticResource BrandPrimary}" />
<Setter Property="TextColor" Value="White" />
<Setter Property="FontFamily" Value="InterBold" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="24,12" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Property="Scale" Value="0.98" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style x:Key="ButtonSecondary" TargetType="Button">
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="TextColor" Value="{StaticResource BrandPrimary}" />
<Setter Property="BorderColor" Value="{StaticResource BrandPrimary}" />
<Setter Property="BorderWidth" Value="2" />
<Setter Property="CornerRadius" Value="8" />
</Style>
</ResourceDictionary>
```
---
## Quick Reference
| Category | Recommendation |
|----------|----------------|
| Primary Font | Inter, Roboto, SF Pro |
| Base Size | 16sp (Body) |
| Scale Ratio | 1.25x (Minor Third) |
| Theme | Always use AppThemeBinding |
| Accessibility | Include HeadingLevel in text styles |

View File

@@ -0,0 +1,189 @@
# MAUI Branding Expert Reference / Tham Chiếu
Complete code examples cho skill `maui-branding-expert`.
---
## 1. Complete Colors.xaml
```xml
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Resources.Styles.Colors">
<!-- Brand Colors -->
<Color x:Key="BrandPrimary">#FF5733</Color>
<Color x:Key="BrandPrimaryDark">#E64A2E</Color>
<Color x:Key="BrandSecondary">#3498DB</Color>
<!-- Semantic Colors -->
<Color x:Key="ColorSuccess">#27AE60</Color>
<Color x:Key="ColorError">#E74C3C</Color>
<Color x:Key="ColorWarning">#F39C12</Color>
<!-- Surface (Theme-aware) -->
<Color x:Key="SurfaceBackground">{AppThemeBinding Light=#FAFAFA, Dark=#121212}</Color>
<Color x:Key="SurfaceCard">{AppThemeBinding Light=#FFFFFF, Dark=#1E1E1E}</Color>
<!-- Text (Theme-aware) -->
<Color x:Key="TextPrimary">{AppThemeBinding Light=#212121, Dark=#FFFFFF}</Color>
<Color x:Key="TextSecondary">{AppThemeBinding Light=#757575, Dark=#B0B0B0}</Color>
</ResourceDictionary>
```
---
## 2. BrandEntry Handler
```csharp
// Handlers/BrandEntryHandler.cs
namespace MyApp.Handlers;
public static class BrandEntryHandler
{
public static void Configure()
{
Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping(
"BrandEntry", (handler, view) =>
{
#if ANDROID
handler.PlatformView.SetBackgroundColor(Android.Graphics.Color.Transparent);
#elif IOS
handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
#endif
});
}
}
```
---
## 3. BrandButton Style
```xml
<!-- Theme.xaml -->
<Style x:Key="BrandButton" TargetType="Button">
<Setter Property="BackgroundColor">
<Setter.Value>
<AppThemeBinding Light="{StaticResource BrandPrimary}"
Dark="#1E1E1E" />
</Setter.Value>
</Setter>
<Setter Property="TextColor" Value="White" />
<Setter Property="FontFamily" Value="InterBold" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="24,12" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup>
<VisualState x:Name="Normal" />
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Property="Scale" Value="0.98" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
```
---
## 4. CardView ControlTemplate
```xml
<!-- Controls/CardView.xaml -->
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Controls.CardView">
<ContentView.ControlTemplate>
<ControlTemplate>
<Border StrokeThickness="0"
StrokeShape="RoundRectangle 12"
BackgroundColor="{StaticResource SurfaceCard}">
<Border.Shadow>
<Shadow Brush="Black" Opacity="0.1" Radius="8" />
</Border.Shadow>
<ContentPresenter Padding="16" />
</Border>
</ControlTemplate>
</ContentView.ControlTemplate>
</ContentView>
```
---
## 5. MauiProgram.cs Setup
```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 brand handlers
BrandEntryHandler.Configure();
return builder.Build();
}
}
```
---
## 6. App.xaml MergedDictionaries
```xml
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Typography.xaml" />
<ResourceDictionary Source="Resources/Styles/Theme.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
```
---
## Usage Example
```xml
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:controls="clr-namespace:MyApp.Controls">
<VerticalStackLayout Padding="20" Spacing="16">
<Entry Style="{StaticResource EntryDefault}"
Placeholder="Email" />
<Button Style="{StaticResource BrandButton}"
Text="Sign In" />
<controls:CardView>
<Label Text="Card content here" />
</controls:CardView>
</VerticalStackLayout>
</ContentPage>
```

View File

@@ -0,0 +1,360 @@
---
name: maui-enterprise-architect
description: Thiết kế và xây dựng ứng dụng đa nền tảng .NET MAUI theo chuẩn Enterprise (MVVM, DI, Shell, Single Project). Use for mobile/desktop apps, cross-platform development, hoặc khi cần structured MAUI architecture.
compatibility: ".NET 9+ MAUI, CommunityToolkit.Mvvm 8+, CommunityToolkit.Maui 9+"
metadata:
author: Velik Ho
version: "1.0"
references: "Microsoft .NET MAUI Documentation, Enterprise Application Patterns"
---
# .NET MAUI Enterprise Development Workflow
Quy trình 4 giai đoạn để phát triển ứng dụng .NET MAUI đa nền tảng theo chuẩn Enterprise.
## When to Use This Skill / Khi Nào Sử Dụng
Use this skill when:
- Building cross-platform mobile/desktop apps / Xây dựng app đa nền tảng
- Creating enterprise MAUI applications / Tạo ứng dụng MAUI enterprise
- Need MVVM + DI architecture / Cần kiến trúc MVVM + DI
- Implementing Shell-based navigation / Triển khai điều hướng Shell
**DO NOT use when:**
- Simple single-platform apps / App đơn giản 1 nền tảng
- Prototyping / Làm prototype nhanh
- Blazor Hybrid apps (use Blazor patterns) / App Blazor Hybrid
## Overview / Tổng Quan
```
┌──────────────────────────────────────────────────────────────────┐
│ WORKFLOW 4 GIAI ĐOẠN (.NET MAUI) │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ PHASE 1 │────►│ PHASE 2 │ │
│ │ PROJECT │ │ ARCHITECTURE │ │
│ │ STRUCTURE │ │ │ │
│ │ - Single Prj│ │ - MVVM Pattern │ │
│ │ - Resources │ │ - DI Setup │ │
│ │ - Fonts │ │ - ViewModels │ │
│ └─────────────┘ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ PHASE 4 │◄────│ PHASE 3 │ │
│ │ PLATFORM │ │ UI & SHELL │ │
│ │ │ │ │ │
│ │ - Handlers │ │ - AppShell │ │
│ │ - Platform │ │ - Navigation │ │
│ │ - Native │ │ - XAML Perf │ │
│ └─────────────┘ └─────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
```
---
## Phase 1: Project Structure / Cấu Trúc Dự Án
**Goal**: Thiết lập Single Project MAUI với quản lý resources tối ưu
### Checklist
1. **Single Project Setup**
- Một codebase cho Android, iOS, macOS, Windows
- Platform-specific code trong `Platforms/` folder
2. **Resources Management**
- Đặt images, fonts, icons vào `Resources/`
- Sử dụng SVG cho icons (auto-resize)
3. **Font Registration**
- Đăng ký fonts trong `MauiProgram.cs`
### Project Structure
```
MyApp/
├── App.xaml # Application resources
├── AppShell.xaml # Navigation shell
├── MauiProgram.cs # DI & configuration
├── Platforms/ # Platform-specific code
│ ├── Android/
│ ├── iOS/
│ ├── MacCatalyst/
│ └── Windows/
├── Resources/
│ ├── Fonts/ # Custom fonts
│ ├── Images/ # App images (SVG preferred)
│ ├── Raw/ # Raw assets
│ └── Styles/ # XAML styles
├── ViewModels/ # MVVM ViewModels
├── Views/ # XAML Pages
├── Models/ # Data models
└── Services/ # Business services
```
---
## Phase 2: Architecture (MVVM + DI) / Kiến Trúc
**Goal**: Thiết lập MVVM pattern với Dependency Injection
**Detailed Guide**: [DI Setup Guidelines](./guidelines/di-setup.md) | [MVVM Rules](./guidelines/mvvm-rules.md)
### Key Patterns
```csharp
// MauiProgram.cs - DI Registration
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");
});
// Services (Singleton for stateful, Transient for stateless)
builder.Services.AddSingleton<IProductService, ProductService>();
builder.Services.AddSingleton<INavigationService, NavigationService>();
// ViewModels (Transient - new instance per navigation)
builder.Services.AddTransient<ProductListViewModel>();
builder.Services.AddTransient<ProductDetailViewModel>();
// Views (Transient to match ViewModel lifecycle)
builder.Services.AddTransient<ProductListPage>();
builder.Services.AddTransient<ProductDetailPage>();
return builder.Build();
}
```
```csharp
// ViewModel với CommunityToolkit.Mvvm
public partial class ProductListViewModel : ObservableObject
{
private readonly IProductService _productService;
[ObservableProperty]
private ObservableCollection<Product> _products = new();
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(RefreshCommand))]
private bool _isLoading;
public ProductListViewModel(IProductService productService)
{
_productService = productService;
}
[RelayCommand]
private async Task LoadProductsAsync()
{
IsLoading = true;
try
{
var items = await _productService.GetAllAsync();
Products = new ObservableCollection<Product>(items);
}
finally
{
IsLoading = false;
}
}
[RelayCommand]
private async Task NavigateToDetailAsync(Product product)
{
await Shell.Current.GoToAsync(
$"{nameof(ProductDetailPage)}",
new Dictionary<string, object> { ["Product"] = product });
}
}
```
---
## Phase 3: UI & Navigation (App Shell) / Giao Diện & Điều Hướng
**Goal**: Xây dựng UI với Shell navigation và XAML tối ưu
**Detailed Guide**: [Shell Navigation](./guidelines/shell-nav.md) | [XAML Performance](./guidelines/xaml-perf.md)
### AppShell Configuration
```xml
<!-- AppShell.xaml -->
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:MyApp.Views"
x:Class="MyApp.AppShell">
<!-- Tab-based navigation -->
<TabBar>
<ShellContent Title="Products"
Icon="products.svg"
ContentTemplate="{DataTemplate views:ProductListPage}" />
<ShellContent Title="Settings"
Icon="settings.svg"
ContentTemplate="{DataTemplate views:SettingsPage}" />
</TabBar>
</Shell>
```
```csharp
// AppShell.xaml.cs - Route Registration
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
// Register routes for navigation
Routing.RegisterRoute(nameof(ProductDetailPage), typeof(ProductDetailPage));
Routing.RegisterRoute(nameof(EditProductPage), typeof(EditProductPage));
}
}
```
### Compiled Bindings (BẮT BUỘC)
```xml
<!-- ✅ ĐÚNG: Compiled Bindings với x:DataType -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MyApp.ViewModels"
x:Class="MyApp.Views.ProductListPage"
x:DataType="vm:ProductListViewModel">
<CollectionView ItemsSource="{Binding Products}"
x:DataType="vm:ProductListViewModel">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Product">
<Grid Padding="10">
<Label Text="{Binding Name}" FontSize="16"/>
<Label Text="{Binding Price, StringFormat='{0:C}'}"
Grid.Column="1"/>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
```
### Navigation with Parameters
```csharp
// Implement IQueryAttributable for receiving parameters
public partial class ProductDetailViewModel : ObservableObject, IQueryAttributable
{
[ObservableProperty]
private Product _product;
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("Product", out var product))
{
Product = product as Product;
}
}
}
```
---
## Phase 4: Platform Integration / Tích Hợp Nền Tảng
**Goal**: Tùy chỉnh native controls và platform-specific code
### Handlers (NOT Custom Renderers)
```csharp
// Customize Entry control
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureMauiHandlers(handlers =>
{
#if ANDROID
handlers.AddHandler<Entry, CustomEntryHandler>();
#endif
});
return builder.Build();
}
```
### Platform-Specific Code (Partial Classes)
```csharp
// Services/DeviceService.cs (shared interface)
public partial class DeviceService : IDeviceService
{
public partial string GetDeviceId();
}
// Platforms/Android/Services/DeviceService.cs
public partial class DeviceService
{
public partial string GetDeviceId()
{
return Android.Provider.Settings.Secure.GetString(
Android.App.Application.Context.ContentResolver,
Android.Provider.Settings.Secure.AndroidId);
}
}
// Platforms/iOS/Services/DeviceService.cs
public partial class DeviceService
{
public partial string GetDeviceId()
{
return UIKit.UIDevice.CurrentDevice.IdentifierForVendor?.ToString() ?? "";
}
}
```
---
## Common Mistakes / Lỗi Thường Gặp
| Mistake | Problem | Solution |
|---------|---------|----------|
| Logic in Code-behind | Hard to test, violates MVVM | Move to ViewModel |
| No `x:DataType` | Runtime binding, slow | Always use Compiled Bindings |
| Nested StackLayout | Deep visual tree, slow | Use Grid instead |
| Custom Renderers | Legacy Xamarin.Forms | Use Handlers |
| Singleton ViewModels | State persists wrongly | Use Transient for pages |
| `#if ANDROID` everywhere | Code pollution | Use Partial Classes |
## Quick Reference / Tham Chiếu Nhanh
| Category | Standard |
|----------|----------|
| Project Type | Single Project MAUI |
| MVVM Toolkit | CommunityToolkit.Mvvm |
| Navigation | Shell + URI-based |
| Bindings | Compiled (`x:DataType`) |
| Images | SVG preferred |
| Platform Code | Partial Classes / `Platforms/` |
| Control Customization | Handlers (not Renderers) |
| Service Lifetime | Singleton (stateful), Transient (pages) |
## Resources / Tài Nguyên
- [MVVM Rules](./guidelines/mvvm-rules.md) - ViewModel patterns
- [Shell Navigation](./guidelines/shell-nav.md) - Navigation patterns
- [DI Setup](./guidelines/di-setup.md) - Dependency Injection
- [XAML Performance](./guidelines/xaml-perf.md) - Compiled Bindings & optimization
- [Microsoft MAUI Docs](https://learn.microsoft.com/en-us/dotnet/maui/)
- [CommunityToolkit.Mvvm](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/)
- [Project Rules](../project-rules/SKILL.md) - GoodGo coding standards

View File

@@ -0,0 +1,343 @@
# Dependency Injection Setup / Cấu Hình DI
Hướng dẫn chi tiết về Dependency Injection trong .NET MAUI với MauiProgram.cs.
## Core Concepts / Khái Niệm Cốt Lõi
1. **Built-in DI**: MAUI sử dụng `Microsoft.Extensions.DependencyInjection`
2. **Service Lifetimes**: Singleton, Scoped, Transient
3. **Constructor Injection**: Inject dependencies qua constructor
---
## MauiProgram.cs Structure
```csharp
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-SemiBold.ttf", "InterSemiBold");
fonts.AddFont("Inter-Bold.ttf", "InterBold");
});
// Register services
RegisterServices(builder.Services);
// Register ViewModels
RegisterViewModels(builder.Services);
// Register Views
RegisterViews(builder.Services);
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
private static void RegisterServices(IServiceCollection services)
{
// HTTP Client
services.AddHttpClient<IApiClient, ApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.Timeout = TimeSpan.FromSeconds(30);
});
// Singleton: Shared state, expensive to create
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IAuthService, AuthService>();
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<IConnectivityService, ConnectivityService>();
// Transient: Stateless, lightweight
services.AddTransient<IProductService, ProductService>();
services.AddTransient<IOrderService, OrderService>();
}
private static void RegisterViewModels(IServiceCollection services)
{
// Transient: New instance per navigation
services.AddTransient<MainViewModel>();
services.AddTransient<ProductListViewModel>();
services.AddTransient<ProductDetailViewModel>();
services.AddTransient<CartViewModel>();
services.AddTransient<CheckoutViewModel>();
services.AddTransient<SettingsViewModel>();
}
private static void RegisterViews(IServiceCollection services)
{
// Transient: Match ViewModel lifecycle
services.AddTransient<MainPage>();
services.AddTransient<ProductListPage>();
services.AddTransient<ProductDetailPage>();
services.AddTransient<CartPage>();
services.AddTransient<CheckoutPage>();
services.AddTransient<SettingsPage>();
}
}
```
---
## Service Lifetimes
### When to Use Each Lifetime
| Lifetime | When to Use | Examples |
|----------|-------------|----------|
| **Singleton** | Shared state across app | AuthService, SettingsService, Cache |
| **Scoped** | Per-request (rarely used in MAUI) | DbContext in Blazor Hybrid |
| **Transient** | Stateless, new each time | ViewModels, Pages, Repositories |
### Singleton Services
```csharp
// ✅ Singleton: Giữ state toàn cục
public interface IAuthService
{
User? CurrentUser { get; }
bool IsAuthenticated { get; }
Task<bool> LoginAsync(string email, string password);
Task LogoutAsync();
}
public class AuthService : IAuthService
{
private User? _currentUser;
public User? CurrentUser => _currentUser;
public bool IsAuthenticated => _currentUser != null;
public async Task<bool> LoginAsync(string email, string password)
{
_currentUser = await _apiClient.LoginAsync(email, password);
return _currentUser != null;
}
public Task LogoutAsync()
{
_currentUser = null;
return Task.CompletedTask;
}
}
// Registration
services.AddSingleton<IAuthService, AuthService>();
```
### Transient ViewModels
```csharp
// ✅ Transient: Mỗi lần navigate tạo instance mới
services.AddTransient<ProductDetailViewModel>();
// ❌ SAI: Singleton ViewModel sẽ giữ data cũ
services.AddSingleton<ProductDetailViewModel>(); // DON'T!
```
---
## View-ViewModel Injection
### Constructor Injection Pattern
```csharp
// View nhận ViewModel qua constructor
public partial class ProductListPage : ContentPage
{
public ProductListPage(ProductListViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}
// ViewModel nhận Services qua constructor
public partial class ProductListViewModel : ObservableObject
{
private readonly IProductService _productService;
private readonly INavigationService _navigation;
public ProductListViewModel(
IProductService productService,
INavigationService navigation)
{
_productService = productService;
_navigation = navigation;
}
}
```
### Shell Navigation with DI
```csharp
// AppShell.xaml - ContentTemplate cho lazy loading
<ShellContent Title="Products"
ContentTemplate="{DataTemplate views:ProductListPage}" />
```
```csharp
// AppShell.xaml.cs - Manual resolution nếu cần
public partial class AppShell : Shell
{
public AppShell(IServiceProvider serviceProvider)
{
InitializeComponent();
// Optional: Resolve services if Shell needs them
var authService = serviceProvider.GetRequiredService<IAuthService>();
}
}
```
---
## Platform-Specific Services
```csharp
// Interface
public interface IDeviceService
{
string GetDeviceId();
string GetPlatformName();
}
// Shared partial class
public partial class DeviceService : IDeviceService
{
public partial string GetDeviceId();
public string GetPlatformName()
{
return DeviceInfo.Platform.ToString();
}
}
// Platforms/Android/Services/DeviceService.cs
public partial class DeviceService
{
public partial string GetDeviceId()
{
return Android.Provider.Settings.Secure.GetString(
Android.App.Application.Context.ContentResolver,
Android.Provider.Settings.Secure.AndroidId) ?? "unknown";
}
}
// Platforms/iOS/Services/DeviceService.cs
public partial class DeviceService
{
public partial string GetDeviceId()
{
return UIKit.UIDevice.CurrentDevice.IdentifierForVendor?.ToString()
?? "unknown";
}
}
// Registration
services.AddSingleton<IDeviceService, DeviceService>();
```
---
## HttpClient Configuration
```csharp
// Named HttpClient với retry policy
services.AddHttpClient<IApiClient, ApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
#if ANDROID
return new Xamarin.Android.Net.AndroidMessageHandler
{
ServerCertificateCustomValidationCallback = (_, _, _, _) => true // Dev only!
};
#else
return new HttpClientHandler();
#endif
})
.AddTransientHttpErrorPolicy(policy =>
policy.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))));
```
---
## Common Mistakes / Lỗi Thường Gặp
### 1. Singleton ViewModel (State Leaking)
```csharp
// ❌ SAI: Product từ lần navigate trước vẫn còn
services.AddSingleton<ProductDetailViewModel>();
// ✅ ĐÚNG: Fresh instance mỗi lần
services.AddTransient<ProductDetailViewModel>();
```
### 2. Missing View Registration
```csharp
// ❌ SAI: Shell không thể resolve page
// Exception: Unable to resolve ProductListPage
// ✅ ĐÚNG: Đăng ký cả View và ViewModel
services.AddTransient<ProductListViewModel>();
services.AddTransient<ProductListPage>();
```
### 3. Circular Dependencies
```csharp
// ❌ SAI: ServiceA → ServiceB → ServiceA
public class ServiceA
{
public ServiceA(ServiceB b) { } // ServiceB cần ServiceA
}
// ✅ ĐÚNG: Dùng Lazy<T> hoặc refactor
public class ServiceA
{
private readonly Lazy<ServiceB> _serviceB;
public ServiceA(Lazy<ServiceB> serviceB)
{
_serviceB = serviceB;
}
}
```
---
## Best Practices Checklist
- [ ] Views và ViewModels đều đăng ký là Transient
- [ ] Services giữ state toàn cục đăng ký là Singleton
- [ ] Stateless services đăng ký là Transient
- [ ] Inject interfaces, không inject concrete classes
- [ ] Sử dụng `IHttpClientFactory` cho HttpClient
- [ ] Platform-specific code dùng Partial Classes
---
## Resources
- [.NET MAUI Dependency Injection](https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/dependency-injection)
- [MVVM Rules](./mvvm-rules.md)
- [Shell Navigation](./shell-nav.md)

View File

@@ -0,0 +1,294 @@
# MVVM Rules / Quy Tắc MVVM
Hướng dẫn chi tiết về Model-View-ViewModel pattern với CommunityToolkit.Mvvm.
## Core Principles / Nguyên Tắc Cốt Lõi
1. **Separation of Concerns**: View chỉ xử lý UI, ViewModel xử lý logic
2. **Testability**: ViewModel có thể unit test độc lập
3. **No Code-behind Logic**: Không viết business logic trong `.xaml.cs`
---
## CommunityToolkit.Mvvm Setup
```xml
<!-- .csproj -->
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
<PackageReference Include="CommunityToolkit.Maui" Version="9.1.0" />
```
```csharp
// MauiProgram.cs
builder.UseMauiCommunityToolkit();
```
---
## ViewModel Patterns
### 1. ObservableProperty (Auto-generate INotifyPropertyChanged)
```csharp
// ✅ ĐÚNG: Source Generator pattern
public partial class ProductViewModel : ObservableObject
{
[ObservableProperty]
private string _name = string.Empty;
[ObservableProperty]
private decimal _price;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullDescription))]
private string _category = string.Empty;
// Computed property
public string FullDescription => $"{Name} - {Category}";
}
```
```csharp
// ❌ SAI: Manual implementation (verbose)
public class ProductViewModel : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
// ... repetitive code
}
```
### 2. RelayCommand (Auto-generate ICommand)
```csharp
public partial class ProductListViewModel : ObservableObject
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private bool _hasChanges;
// Async command với cancellation
[RelayCommand]
private async Task LoadDataAsync(CancellationToken token)
{
var products = await _service.GetAllAsync(token);
Products = new ObservableCollection<Product>(products);
}
// Command with CanExecute
[RelayCommand(CanExecute = nameof(CanSave))]
private async Task SaveAsync()
{
await _service.SaveAsync(CurrentProduct);
HasChanges = false;
}
private bool CanSave() => HasChanges && CurrentProduct != null;
// Command with parameter
[RelayCommand]
private void SelectProduct(Product product)
{
SelectedProduct = product;
}
}
```
### 3. WeakReferenceMessenger (Event Aggregation)
```csharp
// Send message
WeakReferenceMessenger.Default.Send(new ProductUpdatedMessage(product));
// Receive message (register in constructor)
public partial class ProductListViewModel : ObservableObject, IRecipient<ProductUpdatedMessage>
{
public ProductListViewModel()
{
WeakReferenceMessenger.Default.Register(this);
}
public void Receive(ProductUpdatedMessage message)
{
// Handle product update
var product = Products.FirstOrDefault(p => p.Id == message.Product.Id);
if (product != null)
{
var index = Products.IndexOf(product);
Products[index] = message.Product;
}
}
}
// Message definition
public record ProductUpdatedMessage(Product Product);
```
---
## View-ViewModel Binding
### Constructor Injection Pattern
```csharp
// View (ProductListPage.xaml.cs)
public partial class ProductListPage : ContentPage
{
public ProductListPage(ProductListViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
protected override async void OnNavigatedTo(NavigatedToEventArgs args)
{
base.OnNavigatedTo(args);
// Trigger data loading
if (BindingContext is ProductListViewModel vm)
{
await vm.LoadDataCommand.ExecuteAsync(null);
}
}
}
```
### XAML Binding
```xml
<ContentPage x:DataType="vm:ProductListViewModel">
<!-- Property Binding -->
<Label Text="{Binding Title}" />
<!-- Command Binding -->
<Button Text="Save" Command="{Binding SaveCommand}" />
<!-- Command with Parameter -->
<CollectionView ItemsSource="{Binding Products}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Product">
<Grid>
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Source={RelativeSource AncestorType={x:Type vm:ProductListViewModel}}, Path=SelectProductCommand}"
CommandParameter="{Binding .}" />
</Grid.GestureRecognizers>
<Label Text="{Binding Name}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- IsRefreshing Binding -->
<RefreshView IsRefreshing="{Binding IsLoading}"
Command="{Binding RefreshCommand}">
<!-- Content -->
</RefreshView>
</ContentPage>
```
---
## Validation Pattern
```csharp
public partial class ProductFormViewModel : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
[Required(ErrorMessage = "Name is required")]
[MinLength(3, ErrorMessage = "Name must be at least 3 characters")]
private string _name = string.Empty;
[ObservableProperty]
[NotifyDataErrorInfo]
[Range(0.01, 999999, ErrorMessage = "Price must be positive")]
private decimal _price;
[RelayCommand]
private async Task SubmitAsync()
{
ValidateAllProperties();
if (HasErrors)
{
var errors = GetErrors();
// Handle validation errors
return;
}
await _service.CreateProductAsync(Name, Price);
}
}
```
---
## Common Mistakes / Lỗi Thường Gặp
### 1. Logic in Code-behind
```csharp
// ❌ SAI
public partial class ProductPage : ContentPage
{
private async void OnSaveClicked(object sender, EventArgs e)
{
var product = new Product { Name = NameEntry.Text };
await _service.SaveAsync(product);
}
}
// ✅ ĐÚNG: Logic trong ViewModel
// ViewModel
[RelayCommand]
private async Task SaveAsync() => await _service.SaveAsync(CurrentProduct);
// XAML
<Button Text="Save" Command="{Binding SaveCommand}" />
```
### 2. Missing [ObservableProperty] partial keyword
```csharp
// ❌ SAI: Missing partial
public class MyViewModel : ObservableObject
{
[ObservableProperty]
private string _name; // Won't generate property!
}
// ✅ ĐÚNG
public partial class MyViewModel : ObservableObject
{
[ObservableProperty]
private string _name;
}
```
### 3. Direct Collection Modification
```csharp
// ❌ SAI: UI won't update
Products.ToList().ForEach(p => p.IsSelected = false);
// ✅ ĐÚNG: Use ObservableCollection methods
foreach (var product in Products)
{
product.IsSelected = false;
}
OnPropertyChanged(nameof(Products));
```
---
## Resources
- [CommunityToolkit.Mvvm Docs](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/)
- [Shell Navigation](./shell-nav.md)
- [DI Setup](./di-setup.md)

View File

@@ -0,0 +1,308 @@
# Shell Navigation / Điều Hướng App Shell
Hướng dẫn chi tiết về App Shell và URI-based navigation trong .NET MAUI.
## Core Concepts / Khái Niệm Cốt Lõi
1. **Shell**: Container chính quản lý visual structure và navigation
2. **URI-based Navigation**: Điều hướng thông qua URI routes
3. **Query Parameters**: Truyền dữ liệu giữa pages
---
## AppShell Structure
### Basic Shell Configuration
```xml
<!-- AppShell.xaml -->
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:MyApp.Views"
x:Class="MyApp.AppShell"
FlyoutBehavior="Disabled">
<!-- Tab-based navigation -->
<TabBar>
<ShellContent Title="Home"
Icon="home.svg"
Route="home"
ContentTemplate="{DataTemplate views:HomePage}" />
<ShellContent Title="Products"
Icon="products.svg"
Route="products"
ContentTemplate="{DataTemplate views:ProductListPage}" />
<ShellContent Title="Settings"
Icon="settings.svg"
Route="settings"
ContentTemplate="{DataTemplate views:SettingsPage}" />
</TabBar>
</Shell>
```
### Flyout Navigation
```xml
<Shell FlyoutBehavior="Flyout">
<FlyoutItem Title="Dashboard" Icon="dashboard.svg">
<ShellContent ContentTemplate="{DataTemplate views:DashboardPage}" />
</FlyoutItem>
<FlyoutItem Title="Products" Icon="products.svg">
<Tab Title="All Products">
<ShellContent ContentTemplate="{DataTemplate views:ProductListPage}" />
</Tab>
<Tab Title="Categories">
<ShellContent ContentTemplate="{DataTemplate views:CategoryListPage}" />
</Tab>
</FlyoutItem>
<!-- Separator -->
<MenuItem Text="Logout"
IconImageSource="logout.svg"
Command="{Binding LogoutCommand}" />
</Shell>
```
---
## Route Registration
```csharp
// AppShell.xaml.cs
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
// Register detail pages (không có trong Shell visual hierarchy)
Routing.RegisterRoute(nameof(ProductDetailPage), typeof(ProductDetailPage));
Routing.RegisterRoute(nameof(EditProductPage), typeof(EditProductPage));
Routing.RegisterRoute(nameof(CheckoutPage), typeof(CheckoutPage));
// Nested routes (page within page)
Routing.RegisterRoute("products/detail", typeof(ProductDetailPage));
Routing.RegisterRoute("products/detail/edit", typeof(EditProductPage));
}
}
```
---
## Navigation Patterns
### 1. Basic Navigation
```csharp
// Absolute navigation (từ root)
await Shell.Current.GoToAsync("//products");
// Relative navigation (push onto stack)
await Shell.Current.GoToAsync(nameof(ProductDetailPage));
// Navigate back
await Shell.Current.GoToAsync("..");
// Navigate back to root
await Shell.Current.GoToAsync("//");
```
### 2. Navigation with Parameters
```csharp
// ✅ RECOMMENDED: Object-based parameters (NativeAOT safe)
await Shell.Current.GoToAsync(
nameof(ProductDetailPage),
new Dictionary<string, object>
{
["Product"] = selectedProduct,
["IsEditing"] = true
});
// Simple string parameters
await Shell.Current.GoToAsync($"{nameof(ProductDetailPage)}?productId={product.Id}");
```
### 3. Receiving Parameters (IQueryAttributable)
```csharp
// ✅ RECOMMENDED: IQueryAttributable (works with complex objects)
public partial class ProductDetailViewModel : ObservableObject, IQueryAttributable
{
[ObservableProperty]
private Product? _product;
[ObservableProperty]
private bool _isEditing;
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("Product", out var product))
{
Product = product as Product;
}
if (query.TryGetValue("IsEditing", out var isEditing))
{
IsEditing = isEditing is bool editing && editing;
}
}
}
```
```csharp
// Alternative: QueryProperty attribute (simple types only)
[QueryProperty(nameof(ProductId), "productId")]
public partial class ProductDetailViewModel : ObservableObject
{
[ObservableProperty]
private string? _productId;
partial void OnProductIdChanged(string? value)
{
if (!string.IsNullOrEmpty(value))
{
LoadProductAsync(value);
}
}
}
```
---
## Navigation Service Pattern
```csharp
// Interface
public interface INavigationService
{
Task GoToAsync(string route);
Task GoToAsync<TViewModel>(object? parameters = null) where TViewModel : class;
Task GoBackAsync();
Task GoToRootAsync();
}
// Implementation
public class NavigationService : INavigationService
{
public async Task GoToAsync(string route)
{
await Shell.Current.GoToAsync(route);
}
public async Task GoToAsync<TViewModel>(object? parameters = null)
where TViewModel : class
{
var route = typeof(TViewModel).Name.Replace("ViewModel", "Page");
if (parameters != null)
{
var dict = new Dictionary<string, object>();
foreach (var prop in parameters.GetType().GetProperties())
{
dict[prop.Name] = prop.GetValue(parameters)!;
}
await Shell.Current.GoToAsync(route, dict);
}
else
{
await Shell.Current.GoToAsync(route);
}
}
public async Task GoBackAsync()
{
await Shell.Current.GoToAsync("..");
}
public async Task GoToRootAsync()
{
await Shell.Current.GoToAsync("//");
}
}
// Usage in ViewModel
public partial class ProductListViewModel : ObservableObject
{
private readonly INavigationService _navigation;
public ProductListViewModel(INavigationService navigation)
{
_navigation = navigation;
}
[RelayCommand]
private async Task ViewProductAsync(Product product)
{
await _navigation.GoToAsync<ProductDetailViewModel>(new { Product = product });
}
}
```
---
## Modal Navigation
```csharp
// Push modal page
await Shell.Current.GoToAsync(nameof(EditProductPage), animate: true);
// Or using Navigation property
await Shell.Current.Navigation.PushModalAsync(new EditProductPage(viewModel));
// Close modal
await Shell.Current.Navigation.PopModalAsync();
```
---
## Common Mistakes / Lỗi Thường Gặp
### 1. Route Not Registered
```csharp
// ❌ SAI: Quên đăng ký route
await Shell.Current.GoToAsync(nameof(ProductDetailPage)); // Exception!
// ✅ ĐÚNG: Đăng ký trong AppShell constructor
Routing.RegisterRoute(nameof(ProductDetailPage), typeof(ProductDetailPage));
```
### 2. Wrong Route Syntax
```csharp
// ❌ SAI: Double slash cho relative
await Shell.Current.GoToAsync("//ProductDetailPage");
// ✅ ĐÚNG
// Relative (push): không có //
await Shell.Current.GoToAsync(nameof(ProductDetailPage));
// Absolute (to tab): có //
await Shell.Current.GoToAsync("//products");
```
### 3. Complex Objects with QueryProperty
```csharp
// ❌ SAI: QueryProperty không hỗ trợ complex objects
[QueryProperty(nameof(Product), "product")] // Won't work!
public Product Product { get; set; }
// ✅ ĐÚNG: Dùng IQueryAttributable
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("Product", out var product))
Product = product as Product;
}
```
---
## Resources
- [.NET MAUI Shell Navigation](https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/shell/navigation)
- [MVVM Rules](./mvvm-rules.md)
- [DI Setup](./di-setup.md)

View File

@@ -0,0 +1,398 @@
# XAML Performance / Tối Ưu XAML
Hướng dẫn chi tiết về Compiled Bindings, layout optimization và XAML best practices.
## Core Principles / Nguyên Tắc Cốt Lõi
1. **Compiled Bindings**: Luôn sử dụng `x:DataType` để binding tại compile-time
2. **Flat Layouts**: Tránh nested layouts, ưu tiên Grid
3. **Virtualization**: Sử dụng CollectionView cho danh sách lớn
---
## Compiled Bindings (BẮT BUỘC)
### Why Compiled Bindings?
| Aspect | Reflection Binding | Compiled Binding |
|--------|-------------------|------------------|
| Performance | Slow (runtime lookup) | Fast (compile-time) |
| Error Detection | Runtime exceptions | Compile-time errors |
| NativeAOT | May not work | Full support |
| Intellisense | No | Yes |
### Basic Pattern
```xml
<!-- ✅ ĐÚNG: Compiled Bindings -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MyApp.ViewModels"
xmlns:models="clr-namespace:MyApp.Models"
x:Class="MyApp.Views.ProductListPage"
x:DataType="vm:ProductListViewModel">
<!-- Binding được resolve tại compile-time -->
<Label Text="{Binding Title}" />
<Label Text="{Binding Products.Count, StringFormat='Total: {0}'}" />
<CollectionView ItemsSource="{Binding Products}">
<CollectionView.ItemTemplate>
<!-- x:DataType thay đổi cho ItemTemplate -->
<DataTemplate x:DataType="models:Product">
<Grid>
<Label Text="{Binding Name}" />
<Label Text="{Binding Price, StringFormat='{0:C}'}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
```
```xml
<!-- ❌ SAI: Không có x:DataType -->
<ContentPage>
<!-- Runtime reflection, chậm và không catch lỗi -->
<Label Text="{Binding Titlee}" /> <!-- Typo không bị phát hiện! -->
</ContentPage>
```
### Nested DataType Changes
```xml
<CollectionView ItemsSource="{Binding Categories}"
x:DataType="vm:CategoryListViewModel">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Category">
<StackLayout>
<Label Text="{Binding Name}" />
<!-- Nested collection -->
<CollectionView ItemsSource="{Binding Products}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Product">
<Label Text="{Binding Name}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
```
### Binding to Parent ViewModel from ItemTemplate
```xml
<CollectionView ItemsSource="{Binding Products}"
x:DataType="vm:ProductListViewModel">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Product">
<Grid>
<Label Text="{Binding Name}" />
<!-- Bind to parent ViewModel's command -->
<Button Text="Select"
Command="{Binding Source={RelativeSource AncestorType={x:Type vm:ProductListViewModel}}, Path=SelectProductCommand}"
CommandParameter="{Binding .}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
```
---
## Layout Optimization
### Grid vs Nested StackLayout
```xml
<!-- ❌ SAI: Nested StackLayout (deep visual tree) -->
<StackLayout>
<StackLayout Orientation="Horizontal">
<Image Source="{Binding ImageUrl}" />
<StackLayout>
<Label Text="{Binding Name}" />
<Label Text="{Binding Description}" />
<StackLayout Orientation="Horizontal">
<Label Text="{Binding Price}" />
<Button Text="Buy" />
</StackLayout>
</StackLayout>
</StackLayout>
</StackLayout>
<!-- Visual tree depth: 5 levels -->
<!-- ✅ ĐÚNG: Flat Grid -->
<Grid RowDefinitions="Auto,Auto,Auto"
ColumnDefinitions="80,*,Auto">
<Image Source="{Binding ImageUrl}"
Grid.RowSpan="3" />
<Label Text="{Binding Name}"
Grid.Column="1" />
<Label Text="{Binding Description}"
Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" />
<Label Text="{Binding Price}"
Grid.Row="2" Grid.Column="1" />
<Button Text="Buy"
Grid.Row="2" Grid.Column="2" />
</Grid>
<!-- Visual tree depth: 2 levels -->
```
### Layout Performance Tips
```xml
<!-- ✅ Fixed sizes when possible -->
<Image WidthRequest="80" HeightRequest="80" Aspect="AspectFill" />
<!-- ✅ Use Grid star sizing -->
<Grid ColumnDefinitions="*,2*,*">
<!-- Proportional sizing, efficient -->
</Grid>
<!-- ❌ Avoid: AbsoluteLayout for complex UIs -->
<!-- ❌ Avoid: Deep nesting (>4 levels) -->
```
---
## CollectionView Best Practices
### Virtualization (Auto-enabled)
```xml
<!-- CollectionView tự động virtualize -->
<CollectionView ItemsSource="{Binding LargeList}"
ItemsUpdatingScrollMode="KeepItemsInView">
<!-- Fixed item size improves virtualization -->
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical"
ItemSpacing="8" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<!-- Keep template simple for performance -->
<Grid HeightRequest="60" Padding="10">
<Label Text="{Binding Name}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
```
### EmptyView và Loading States
```xml
<CollectionView ItemsSource="{Binding Products}">
<!-- Empty state -->
<CollectionView.EmptyView>
<ContentView>
<StackLayout VerticalOptions="Center" HorizontalOptions="Center">
<Image Source="empty_box.svg" WidthRequest="100" />
<Label Text="No products found" />
</StackLayout>
</ContentView>
</CollectionView.EmptyView>
<CollectionView.ItemTemplate>
<!-- ... -->
</CollectionView.ItemTemplate>
</CollectionView>
<!-- Loading overlay -->
<ActivityIndicator IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"
VerticalOptions="Center"
HorizontalOptions="Center" />
```
---
## Resource Dictionary Patterns
### App-level Resources
```xml
<!-- App.xaml -->
<Application.Resources>
<ResourceDictionary>
<!-- Merged dictionaries -->
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
```
### Colors.xaml
```xml
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<!-- Primary palette -->
<Color x:Key="Primary">#6750A4</Color>
<Color x:Key="PrimaryDark">#381E72</Color>
<Color x:Key="Secondary">#625B71</Color>
<!-- Semantic colors -->
<Color x:Key="TextPrimary">#1C1B1F</Color>
<Color x:Key="TextSecondary">#49454F</Color>
<Color x:Key="Surface">#FFFBFE</Color>
<Color x:Key="SurfaceVariant">#E7E0EC</Color>
<!-- Status colors -->
<Color x:Key="Success">#4CAF50</Color>
<Color x:Key="Warning">#FF9800</Color>
<Color x:Key="Error">#B3261E</Color>
</ResourceDictionary>
```
### Styles.xaml
```xml
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<!-- Base styles -->
<Style TargetType="Label" x:Key="TitleLabel">
<Setter Property="FontSize" Value="24" />
<Setter Property="FontFamily" Value="InterBold" />
<Setter Property="TextColor" Value="{StaticResource TextPrimary}" />
</Style>
<Style TargetType="Label" x:Key="BodyLabel">
<Setter Property="FontSize" Value="16" />
<Setter Property="FontFamily" Value="InterRegular" />
<Setter Property="TextColor" Value="{StaticResource TextSecondary}" />
</Style>
<Style TargetType="Button" x:Key="PrimaryButton">
<Setter Property="BackgroundColor" Value="{StaticResource Primary}" />
<Setter Property="TextColor" Value="White" />
<Setter Property="FontFamily" Value="InterSemiBold" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="16,12" />
</Style>
<!-- Apply style globally -->
<Style TargetType="Entry" ApplyToDerivedTypes="True">
<Setter Property="FontFamily" Value="InterRegular" />
<Setter Property="FontSize" Value="16" />
</Style>
</ResourceDictionary>
```
### Usage in Pages
```xml
<Label Text="Welcome" Style="{StaticResource TitleLabel}" />
<Label Text="Description" Style="{StaticResource BodyLabel}" />
<Button Text="Submit" Style="{StaticResource PrimaryButton}" />
<!-- Dynamic resource (theme switching) -->
<Label TextColor="{DynamicResource TextPrimary}" />
```
---
## Hot Reload Tips
```xml
<!-- ✅ Hot Reload friendly -->
<Label Text="Static text" />
<Label Text="{Binding DynamicText}" />
<!-- ⚠️ May require restart -->
<!-- Changes to: -->
<!-- - x:DataType -->
<!-- - Namespace imports -->
<!-- - Resource dictionary structure -->
```
---
## Common Mistakes / Lỗi Thường Gặp
### 1. Missing x:DataType in ItemTemplate
```xml
<!-- ❌ SAI -->
<CollectionView ItemsSource="{Binding Products}" x:DataType="vm:ListViewModel">
<CollectionView.ItemTemplate>
<DataTemplate>
<!-- Binding uses reflection, slow! -->
<Label Text="{Binding Name}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- ✅ ĐÚNG -->
<DataTemplate x:DataType="models:Product">
<Label Text="{Binding Name}" />
</DataTemplate>
```
### 2. Image Not Caching
```xml
<!-- ❌ SAI: Reload mỗi lần -->
<Image Source="{Binding ImageUrl}" />
<!-- ✅ ĐÚNG: Enable caching -->
<Image>
<Image.Source>
<UriImageSource Uri="{Binding ImageUrl}"
CachingEnabled="True"
CacheValidity="1:00:00:00" />
</Image.Source>
</Image>
```
### 3. Heavy Work in Converters
```csharp
// ❌ SAI: Slow converter
public object Convert(object value, ...)
{
// Heavy computation on UI thread!
return ProcessData(value);
}
// ✅ ĐÚNG: Pre-compute in ViewModel
[ObservableProperty]
private string _processedValue; // Already computed
```
---
## Performance Checklist
- [ ] `x:DataType` declared on every page and DataTemplate
- [ ] No nested StackLayout deeper than 2 levels
- [ ] CollectionView used for lists (not StackLayout)
- [ ] Fixed sizes where possible (WidthRequest, HeightRequest)
- [ ] Image caching enabled for remote images
- [ ] Styles in ResourceDictionary, not inline
- [ ] StaticResource over DynamicResource when possible
---
## Resources
- [.NET MAUI Performance Best Practices](https://learn.microsoft.com/en-us/dotnet/maui/deployment/performance)
- [Compiled Bindings](https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/data-binding/compiled-bindings)
- [MVVM Rules](./mvvm-rules.md)
- [DI Setup](./di-setup.md)