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

7.0 KiB

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

<!-- .csproj -->
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
<PackageReference Include="CommunityToolkit.Maui" Version="9.1.0" />
// MauiProgram.cs
builder.UseMauiCommunityToolkit();

ViewModel Patterns

1. ObservableProperty (Auto-generate INotifyPropertyChanged)

// ✅ ĐÚ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}";
}
// ❌ 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)

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)

// 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

// 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

<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

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

// ❌ 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

// ❌ 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

// ❌ 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