Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
managedcode avatar

Mvvm

  • 17 installs
  • 466 repo stars
  • Updated July 25, 2026
  • managedcode/dotnet-skills

Helps with ai & agent building tasks.

About

mvvm is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.

  • mvvm
  • AI & Agent Building
  • AI-coding skill

Mvvm by the numbers

  • 17 all-time installs (skills.sh)
  • +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
  • Ranked #10,861 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
  • Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill mvvm

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs17
repo stars466
Last updatedJuly 25, 2026
Repositorymanagedcode/dotnet-skills

What it does

Helps with ai & agent building tasks.

Files

SKILL.mdMarkdownGitHub ↗

MVVM Pattern for .NET

Trigger On

  • implementing UI separation with Model-View-ViewModel
  • using MVVM Toolkit (CommunityToolkit.Mvvm) for ViewModels
  • designing testable UI architecture
  • handling commands, property changes, and messaging
  • choosing between MVVM frameworks

Documentation

References

See detailed examples in the references/ folder:

  • `patterns.md` — ViewModel, command, navigation, and state patterns
  • `anti-patterns.md` — Common mistakes and how to fix them

Core Concepts

ComponentResponsibilityExample
ModelBusiness logic and dataProduct, Order, User
ViewUI presentation (XAML/Razor)ProductPage.xaml
ViewModelUI logic and stateProductViewModel

Workflow

1. Keep Views dumb — no business logic in code-behind 2. Use data binding — connect View to ViewModel properties 3. Commands for actions — handle user interactions via ICommand 4. Inject dependencies — services go into ViewModel constructors 5. Test ViewModels — they should be unit testable without UI

MVVM Toolkit Setup

<PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" />

ViewModel Patterns

Basic ViewModel with Source Generators

public partial class ProductViewModel(IProductService productService) : ObservableObject
{
    [ObservableProperty]
    private string _name = string.Empty;

    [ObservableProperty]
    private decimal _price;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    private bool _isValid;

    [RelayCommand(CanExecute = nameof(CanSave))]
    private async Task SaveAsync()
    {
        await productService.SaveAsync(new Product { Name = Name, Price = Price });
    }

    private bool CanSave() => IsValid && !string.IsNullOrEmpty(Name);
}

Property Changed Notifications

public partial class OrderViewModel : ObservableObject
{
    [ObservableProperty]
    private int _quantity;

    [ObservableProperty]
    private decimal _unitPrice;

    // Computed property - manually notify
    public decimal Total => Quantity * UnitPrice;

    partial void OnQuantityChanged(int value)
    {
        OnPropertyChanged(nameof(Total));
    }

    partial void OnUnitPriceChanged(decimal value)
    {
        OnPropertyChanged(nameof(Total));
    }
}

Collection ViewModel

public partial class ProductListViewModel(IProductService productService) : ObservableObject
{
    [ObservableProperty]
    private ObservableCollection<ProductViewModel> _products = [];

    [ObservableProperty]
    private ProductViewModel? _selectedProduct;

    [ObservableProperty]
    private bool _isLoading;

    [RelayCommand]
    private async Task LoadProductsAsync()
    {
        IsLoading = true;
        try
        {
            var items = await productService.GetAllAsync();
            Products = new ObservableCollection<ProductViewModel>(
                items.Select(p => new ProductViewModel(productService)
                {
                    Name = p.Name,
                    Price = p.Price
                }));
        }
        finally
        {
            IsLoading = false;
        }
    }

    [RelayCommand]
    private void DeleteProduct(ProductViewModel product)
    {
        Products.Remove(product);
    }
}

Commands

Async Commands with Cancellation

public partial class SearchViewModel : ObservableObject
{
    [ObservableProperty]
    private string _searchText = string.Empty;

    [RelayCommand(IncludeCancelCommand = true)]
    private async Task SearchAsync(CancellationToken token)
    {
        await Task.Delay(500, token); // Debounce
        // Search logic with cancellation support
    }
}

Command with Parameter

public partial class NavigationViewModel : ObservableObject
{
    [RelayCommand]
    private void NavigateTo(string page)
    {
        // Navigate to page
    }

    [RelayCommand]
    private async Task OpenItemAsync(int itemId)
    {
        // Load and open item
    }
}

Messenger Pattern

Sending Messages

// Define message
public record ProductSelectedMessage(Product Product);

// Send from one ViewModel
WeakReferenceMessenger.Default.Send(new ProductSelectedMessage(selectedProduct));

Receiving Messages

public partial class ProductDetailViewModel : ObservableRecipient
{
    public ProductDetailViewModel()
    {
        IsActive = true; // Enable message reception
    }

    protected override void OnActivated()
    {
        Messenger.Register<ProductDetailViewModel, ProductSelectedMessage>(
            this, (r, m) => r.LoadProduct(m.Product));
    }

    private void LoadProduct(Product product)
    {
        // Update UI with product details
    }
}

Validation

Using ObservableValidator

public partial class RegistrationViewModel : ObservableValidator
{
    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required(ErrorMessage = "Email is required")]
    [EmailAddress(ErrorMessage = "Invalid email format")]
    private string _email = string.Empty;

    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required]
    [MinLength(8, ErrorMessage = "Password must be at least 8 characters")]
    private string _password = string.Empty;

    [RelayCommand(CanExecute = nameof(CanRegister))]
    private async Task RegisterAsync()
    {
        ValidateAllProperties();
        if (HasErrors) return;

        // Registration logic
    }

    private bool CanRegister() => !HasErrors;
}

Dependency Injection

Registration

// Services
services.AddSingleton<IProductService, ProductService>();
services.AddSingleton<INavigationService, NavigationService>();

// ViewModels
services.AddTransient<ProductListViewModel>();
services.AddTransient<ProductDetailViewModel>();

// Views (for View-first navigation)
services.AddTransient<ProductListPage>();
services.AddTransient<ProductDetailPage>();

ViewModel Locator Pattern

public class ViewModelLocator
{
    private static IServiceProvider _provider = null!;

    public static void Initialize(IServiceProvider provider) => _provider = provider;

    public ProductListViewModel ProductList => _provider.GetRequiredService<ProductListViewModel>();
    public ProductDetailViewModel ProductDetail => _provider.GetRequiredService<ProductDetailViewModel>();
}

View Binding

XAML Binding

<Page x:Class="MyApp.Views.ProductListPage"
      xmlns:vm="using:MyApp.ViewModels"
      x:DataType="vm:ProductListViewModel">

    <Grid>
        <ProgressRing IsActive="{x:Bind ViewModel.IsLoading, Mode=OneWay}"
                      Visibility="{x:Bind ViewModel.IsLoading, Mode=OneWay}" />

        <ListView ItemsSource="{x:Bind ViewModel.Products, Mode=OneWay}"
                  SelectedItem="{x:Bind ViewModel.SelectedProduct, Mode=TwoWay}">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="vm:ProductViewModel">
                    <StackPanel>
                        <TextBlock Text="{x:Bind Name, Mode=OneWay}" />
                        <TextBlock Text="{x:Bind Price, Mode=OneWay}" />
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

        <Button Content="Load"
                Command="{x:Bind ViewModel.LoadProductsCommand}" />
    </Grid>
</Page>

Anti-Patterns to Avoid

Anti-PatternWhy It's BadBetter Approach
Logic in code-behindNot testableMove to ViewModel
ViewModel knows ViewTight couplingUse interfaces/messaging
Manual INotifyPropertyChangedVerbose, error-proneUse source generators
God ViewModelUnmaintainableSplit responsibilities
Direct service calls in ViewViolates separationGo through ViewModel
Exposing Model directlyLeaks implementationCreate ViewModel properties

Testing ViewModels

public class ProductViewModelTests
{
    [Fact]
    public async Task LoadProducts_PopulatesCollection()
    {
        // Arrange
        var mockService = new Mock<IProductService>();
        mockService.Setup(s => s.GetAllAsync())
            .ReturnsAsync([new Product { Name = "Test", Price = 10 }]);

        var viewModel = new ProductListViewModel(mockService.Object);

        // Act
        await viewModel.LoadProductsCommand.ExecuteAsync(null);

        // Assert
        Assert.Single(viewModel.Products);
        Assert.Equal("Test", viewModel.Products[0].Name);
    }

    [Fact]
    public void SaveCommand_CannotExecute_WhenInvalid()
    {
        var viewModel = new ProductViewModel(Mock.Of<IProductService>())
        {
            Name = "",
            IsValid = false
        };

        Assert.False(viewModel.SaveCommand.CanExecute(null));
    }
}

Framework Comparison

FeatureMVVM ToolkitPrismMVVMLight
Source generatorsYesNoNo
MaintenanceActiveActiveDeprecated
DI built-inNoYesNo
NavigationNoYesNo
WeightLightHeavyLight

Deliver

  • ViewModels that are fully unit testable
  • Clean separation between UI and business logic
  • Proper use of commands and data binding
  • Messaging for loose coupling between components

Validate

  • No business logic in code-behind files
  • ViewModels don't reference View types
  • Commands are used for all user actions
  • Properties use ObservableProperty or equivalent
  • Dependencies are injected, not created
  • Unit tests cover ViewModel logic

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.