# 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
```
```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(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
{
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
```
---
## 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
```
### 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)