MVVM Architecture Overview
MVVM (Model-View-ViewModel) is a structural design pattern that cleanly separates user interface logic from business logic.
Advantages of MVVM:
- Team Collaboration: Establishes a unified methodology and consistent development workflow.
- Architectural Stability: Promotes decoupling, ensuring that changes in one layer have minimal impact on others.
- Code Quality: Results in a codebase that is highly readable, testable, and component-replaceable.
Core Components:
- Model: Represents the abstraction of real-world data and business rules.
- View: The visual representation (UI) constructed using XAML.
- ViewModel: Acts as the data and command broker specifically tailored for the View.
Communication Mechanisms:
- Data Flow: Managed via Data Properties (DependencyProperties) and data binding.
- Action Flow: Handled via Command Properties (implementing
ICommand), bridging UI events to executable methods.
Core MVVM Implementation
Building an MVVM applicasion from scratch requires two foundational classes.
1. The Observable Base Class
This class implements INotifyPropertyChanged to notify the UI when a data property value changes.
public class ObservableEntity : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyStateChanged(string propName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
2. The Command Class
This class implements ICommand to bind UI interactions (like button clicks) directly to ViewModel methods.
public class ActionCommand : ICommand
{
public event EventHandler CanExecuteChanged;
private readonly Action<object> _executeHandler;
private readonly Func<object, bool> _canExecuteHandler;
public ActionCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_executeHandler = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecuteHandler = canExecute;
}
public bool CanExecute(object param) => _canExecuteHandler == null || _canExecuteHandler(param);
public void Execute(object param) => _executeHandler(param);
}
3. The ViewModel
Combining data properties and commands, the ViewModel encapsulates the presentation logic. Here is an example of a simple multiplier.
public class MultiplierViewModel : ObservableEntity
{
private double _factorOne;
public double FactorOne
{
get => _factorOne;
set { _factorOne = value; OnPropertyStateChanged(nameof(FactorOne)); }
}
private double _factorTwo;
public double FactorTwo
{
get => _factorTwo;
set { _factorTwo = value; OnPropertyStateChanged(nameof(FactorTwo)); }
}
private double _outcome;
public double Outcome
{
get => _outcome;
set { _outcome = value; OnPropertyStateChanged(nameof(Outcome)); }
}
public ICommand CalculateCommand { get; }
public ICommand ExportCommand { get; }
public MultiplierViewModel()
{
CalculateCommand = new ActionCommand(ExecuteCalculation);
ExportCommand = new ActionCommand(ExecuteExport);
}
private void ExecuteCalculation(object obj)
{
Outcome = FactorOne * FactorTwo;
}
private void ExecuteExport(object obj)
{
var dialog = new SaveFileDialog();
dialog.ShowDialog();
}
}
4. The View (XAML)
The user interface binds directly to the ViewModel properties and commands.
<Window x:Class="MvvmDemo.App.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Multiplier" Height="250" Width="250">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Button Grid.Row="0" Content="Export" Command="{Binding ExportCommand}" Margin="5"/>
<TextBox Grid.Row="1" Text="{Binding FactorOne}" Margin="5" TextAlignment="Center" FontSize="24"/>
<TextBox Grid.Row="2" Text="{Binding FactorTwo}" Margin="5" TextAlignment="Center" FontSize="24"/>
<TextBox Grid.Row="3" Text="{Binding Outcome}" Margin="5" TextAlignment="Center" FontSize="24" IsReadOnly="True"/>
<Button Grid.Row="4" Content="Multiply" Command="{Binding CalculateCommand}" Margin="5"/>
</Grid>
</Window>
5. Wiring the DataContext
The View's code-behind requires only a single line to establish the binding context.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MultiplierViewModel();
}
}
Development Enhancements & UI Design Principles
UI Design Principle: When designnig software interfaces, prioritize visual information over raw data—Images are superior to Tables, and Tables are superior to plain Text.
Code Folding Shortcut: To quickly collapse all definition and regions in Visual Studio, use the shortcut Ctrl + M, Ctrl + O.
Custom Code Snippets: Built-in snippets like prop speed up coding, but creating custom snippets optimizes MVVM development. You can define custom XML snippets via Visual Studio's Code Snippet Manager (Tools > Code Snippets Manager). Below is a custom snippet (propmvvm) that generates a property with INotifyPropertyChanged integration.
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>propmvvm</Title>
<Shortcut>propmvvm</Shortcut>
<Description>Property snippet with PropertyChanged invocation</Description>
<Author>Developer</Author>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Declarations>
<Literal>
<ID>valType</ID>
<ToolTip>Value Type</ToolTip>
<Default>double</Default>
</Literal>
<Literal>
<ID>propName</ID>
<ToolTip>Property name</ToolTip>
<Default>MyVal</Default>
</Literal>
<Literal>
<ID>backField</ID>
<ToolTip>Backing field name</ToolTip>
<Default>_myVal</Default>
</Literal>
</Declarations>
<Code Language="csharp">
<![CDATA[private $valType$ $backField$;
public $valType$ $propName$
{
get => $backField$;
set
{
$backField$ = value;
OnPropertyStateChanged(nameof($propName$));
}
}
$end$]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>