MVVM is the de facto standard for building WPF applications. It promises a beautiful world of decoupled layers, testability, and clean architecture. The ideal is great.
But when you're in the trenches, shipping features, you start running into the painful parts, don't you?
- The dogma of "data-binding for everything" becomes a bottleneck. How do you cleanly show a dialog or trigger a complex animation?
- You can't seem to control the timing of View updates from your ViewModel without jumping through hoops.
- Your ViewModels get bloated with silly "trigger properties" like
IsTimeToUpdateTheViewTrigger
that exist only to signal the UI. - For performance-critical apps, like a CAD viewer, the "chattiness" of data-binding can kill your frame rate.
To solve these real-world problems, I'd like to share a hybrid, pragmatic approach that respects the spirit of MVVM while introducing a "ViewService" to handle the messy parts.
The Solution: A "ViewService" as an Interpreter
The core of this architecture is an interpreter that sits between the ViewModel and the View. Let's call it a ViewService
.
- The ViewModel remains ignorant of the concrete View (
MainWindow.xaml
, etc.). - Instead, it only knows about an abstract interface, like
IMyViewService
. - The concrete
ViewService
class takes on the responsibility of translating the ViewModel's commands into actual View manipulations.
This setup allows us to call View-specific logic safely, without sacrificing the testability of our ViewModel—the biggest win of MVVM.
A Real-World Example: IMainContentViewService
Here is a ViewService
interface I used in a real CAD-style application.
public interface IMainContentViewService
{
// Exposes essential data from the View to the ViewModel
IEnumerable<IProfileDataPointElement> SelectedVertices { get; }
// Exposes behaviors (as commands) for the ViewModel to invoke
RelayCommand FitCommand { get; }
RelayCommand SelectAllCommand { get; }
RelayCommand ShowSettingDialogCommand { get; }
RelayCommand ShowGridCommand { get; }
// ...and many more
}
Key Decision: Exposing ICommand
Properties
You'll notice it exposes ICommand
properties (RelayCommand
) instead of methods like void Fit()
. This is a deliberate choice to empower the UI layer. It allows us to wire up different UI components entirely in XAML.
For example, a menu item in a parent ShellView
can now directly bind to a command that controls our MainContentView
:
<MenuItem Header="Fit to Screen"
Command="{Binding MainContentService.FitCommand}" />
The ViewService
Implementation
The implementation of this service is beautifully simple. Its only job is to delegate.
public class MainContentViewService : ViewService<MainContentView>, IMainContentViewService
{
public MainContentViewService(MainContentView view) : base(view) { }
// SelectedVertices just gets the data from an inner control in the View
public IEnumerable<IProfileDataPointElement> SelectedVertices => this.View.ProfileDataTableView.SelectedItemsAsProfilePointsData;
private RelayCommand _fitCommand;
public RelayCommand FitCommand
{
get
{
if (this._fitCommand == null)
{
// The 'CanExecute' logic is simple because it can directly access the View's state
bool canExecute(object obj)
{
return this.View.ProfileDataTableView.Items.Count > 0;
}
// The 'Execute' logic just calls a method on a specific part of the View
void execute(object obj)
{
this.View.ProfileDataGraphView.InternalZoomViewer.Fit();
}
this._fitCommand = new RelayCommand(canExecute, execute);
}
return this._fitCommand;
}
}
// ...other command implementations
}
Note how the CanExecute
logic for FitCommand
directly checks this.View.ProfileDataTableView.Items.Count
. This is far more efficient than trying to sync that state back to the ViewModel.
Our 4 Rules for This Hybrid MVVM
To keep our codebase consistent and avoid confusion about "where do I put this logic?", we established four simple rules.
1. For State Synchronization → Use Data Binding
When a ViewModel property and a View property should always be in sync, use traditional data binding. This is the heart of MVVM.
(e.g., Text
property of a TextBox
, ItemsSource
of a ListBox
)
2. For Behavior Invocation → Use a ViewService
When the ViewModel needs to tell the View to perform a one-time action, use a method or command on the ViewService
.
(e.g., FitToScreen()
, ShowDialog()
)
3. For View-Internal Logic → Use Code-Behind
It's okay to write code in the code-behind (.xaml.cs
) for logic that is purely visual and has no impact on the ViewModel.
(e.g., handling the close button, decorative animations on mouse-over)
4. For Info Flowing to the VM → Keep It on a "Need-to-Know" Basis
Only pass information from the View to the ViewModel if the ViewModel truly needs it for its business logic. The ViewModel should not care about visual state (e.g., whether a panel is expanded or not).
Final Thoughts
MVVM is a powerful pattern, but being a dogmatic purist can lead to overly complex and inefficient code.
This hybrid ViewService
approach is a pragmatic compromise. It maintains the core benefits of MVVM—like testability and separation of concerns—while providing a clean, simple escape hatch for the realities of complex UI development.
I hope this gives you a useful tool for your next WPF project!
Top comments (0)