DEV Community

NeoPotato
NeoPotato

Posted on

Beyond the Dogma: A Pragmatic Hybrid MVVM Architecture in WPF**

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
}
Enter fullscreen mode Exit fullscreen mode

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}" />
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)