BLAZMVVM0012: Command Pattern
Summary
Methods invoked from UI should be exposed as RelayCommands instead of direct method calls.
Severity
Info - Suggestion to follow command pattern best practices.
Why This Rule Exists
- Enables proper async command execution with cancellation
- Provides automatic CanExecute support
- Improves testability
- Supports command state management (IsRunning, etc.)
- Follows MVVM command pattern
- Better separation of concerns
✅ DO: Correct Usage
// ✅ Correct: Using [RelayCommand]
[ViewModelDefinition]
public partial class ProductViewModel : ViewModelBase
{
private readonly IProductService _productService;
public ProductViewModel(IProductService productService)
{
_productService = productService;
}
[RelayCommand]
private async Task SaveAsync()
{
await _productService.SaveAsync(Product);
}
[RelayCommand(CanExecute = nameof(CanDelete))]
private async Task DeleteAsync()
{
await _productService.DeleteAsync(Product.Id);
}
private bool CanDelete() => Product.Id > 0;
public Product Product { get; set; } = new();
}
// View usage
<button @onclick="ViewModel.SaveCommand">Save</button>
<button @onclick="ViewModel.DeleteCommand" disabled="@(!ViewModel.DeleteCommand.CanExecute(null))">
Delete
</button>
❌ DON'T: Incorrect Usage
// ❌ Wrong: Public methods called directly from UI
[ViewModelDefinition]
public class ProductViewModel : ViewModelBase
{
public async Task SaveAsync() // Called directly - no command support!
{
await _productService.SaveAsync(Product);
}
public async Task DeleteAsync() // No CanExecute support
{
await _productService.DeleteAsync(Product.Id);
}
}
// View - direct method calls
<button @onclick="ViewModel.SaveAsync">Save</button>
// ❌ Wrong: Event handlers in code-behind
@code {
private async Task OnSaveClicked()
{
await ViewModel.SaveAsync(); // Bypasses command infrastructure
}
}
<button @onclick="OnSaveClicked">Save</button>
// ❌ Wrong: Mixing patterns
[ViewModelDefinition]
public partial class ProductViewModel : ViewModelBase
{
[RelayCommand]
private async Task SaveAsync() { } // Command
public async Task DeleteAsync() { } // Direct method - inconsistent!
}
How to Fix
Convert Method to Command
// Before
public async Task SaveProductAsync()
{
await _productService.SaveAsync(Product);
}
// In View
<button @onclick="ViewModel.SaveProductAsync">Save</button>
// After
[RelayCommand]
private async Task SaveProductAsync()
{
await _productService.SaveAsync(Product);
}
// In View
<button @onclick="ViewModel.SaveProductCommand">Save</button>
Add CanExecute Logic
// Before
public async Task DeleteAsync()
{
if (Product.Id > 0)
{
await _productService.DeleteAsync(Product.Id);
}
}
// After
[RelayCommand(CanExecute = nameof(CanDelete))]
private async Task DeleteAsync()
{
await _productService.DeleteAsync(Product.Id);
}
private bool CanDelete() => Product.Id > 0;
// In View - button automatically disables
<button @onclick="ViewModel.DeleteCommand"
disabled="@(!ViewModel.DeleteCommand.CanExecute(null))">
Delete
</button>
Benefits
Automatic Command Infrastructure
[RelayCommand]
private async Task SaveAsync()
{
await _productService.SaveAsync(Product);
}
// Generated:
// public IAsyncRelayCommand SaveCommand { get; }
// - Automatic IsRunning support
// - Cancellation token support
// - Exception handling
// - CanExecute support
CanExecute Support
[RelayCommand(CanExecute = nameof(CanSave))]
private async Task SaveAsync()
{
await _productService.SaveAsync(Product);
}
private bool CanSave() => !string.IsNullOrWhiteSpace(Product.Name);
// View
<button @onclick="ViewModel.SaveCommand"
disabled="@(!ViewModel.SaveCommand.CanExecute(null))">
Save
</button>
Cancellation Support
[RelayCommand]
private async Task LoadDataAsync(CancellationToken cancellationToken)
{
await _productService.LoadAsync(cancellationToken);
}
// Can be cancelled
ViewModel.LoadDataCommand.Cancel();
IsRunning State
<button @onclick="ViewModel.LoadCommand" disabled="@ViewModel.LoadCommand.IsRunning">
@(ViewModel.LoadCommand.IsRunning ? "Loading..." : "Load Data")
</button>
Command Patterns
Simple Command
[RelayCommand]
private void DoSomething()
{
// Synchronous action
}
Async Command
[RelayCommand]
private async Task LoadAsync()
{
await Task.Delay(1000);
}
With Parameter
[RelayCommand]
private async Task DeleteAsync(int id)
{
await _productService.DeleteAsync(id);
}
// View
<button @onclick="() => ViewModel.DeleteCommand.Execute(productId)">Delete</button>
With CanExecute
[RelayCommand(CanExecute = nameof(CanExecute))]
private async Task SaveAsync()
{
await _productService.SaveAsync(Product);
}
private bool CanExecute() => Product.IsValid;
Code Fix
This analyzer provides an automatic code fix that adds the [RelayCommand] attribute and makes the method private.
Related Analyzers
- BLAZMVVM0017: RelayCommand Async Pattern
- BLAZMVVM0001: ViewModelBase Inheritance