Files
pos-system/microservices/.agent/skills/maui-enterprise-architect/guidelines/mvvm-rules.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

295 lines
7.0 KiB
Markdown

# 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)