WPF, MVVM, Navigation, Keeping Dependency Injectio

2019-05-22 20:32发布

I have a simple WPF application which utilizes the Unity Framework for Dependency Injection. Currently, I am trying to simplify my method for navigation between views in my MVVM pattern implementation; however, many of the examples throughout Stack Overflow do not take into consideration the Dependency Injection caveat.

I have two entirely separate views.

One, Main acts as the main window into which content is loaded (pretty typical; unnecessary content eliminated):

<Window x:Class="Application.UI.Main">
    <Grid Background="White">
        <ContentControl Content="{Binding aProperty}"/>
    </Grid>
</Window>

The constructor receives a ViewModel via constructor injection (again, very simple):

    public partial class Main
    {
        private MainViewModel _mainViewModel;

        public Main (MainViewModel mainViewModel)
        {
            InitializeComponent();

           this.DataContext = _mainViewModel = mainViewModel;
        }
    }

I then have a UserControl, Home, to which I want to "navigate" the main window (i.e. set the ContentControl. It's constructor also receives a ViewModel via constructor injection in the same way Main does. It is equally simple:

public Home(HomeViewModel homeViewModel)
{
    InitializeComponent();

    // Set Data Context:
    this.DataContext = homeViewModel;
}

The main problem, here, is that I want to enable constructor-based injection while maintaining as pure an MVVM implementation as possible.

I am in the View-first camp of MVVM, about which you can find a good discussion in these comments.

I have seen some allusions to the idea of a navigation based service; however, am unsure if that maintains the separation of concerns strived for by MVVM. DataTemplates require View constructors that do not take arguments and I have read criticisms of DataTemplates that argue ViewModels should not participate in the instantiation of Views.

This solution (in my opinion) is just straight wrong as the ViewModel becomes aware of its View and relies on a service for ViewModel instantiaion which makes real Dependency Injection to resolve ViewModel and View dependencies all but impossible. This issue is very evident in the use of RelayCommand in this MSDN article.

Does a navigation service which maintains a global, singleton-like reference to the Main view make the most sense? Is it acceptable for the Main view to expose a method, e.g.:

public void SetContent(UserControl userControl) { //... }

That is then accessed by that service?

1条回答
Rolldiameter
2楼-- · 2019-05-22 21:03

This is my articulation of the motivations behind my implementation of the solution provided by another author. I do not provide code as great code examples are provided by the linked article. These are, of course, my opinions but also represent an amalgamation of my research into the topic

No-Container Needed Solution

Rachel Lim wrote a great article, Navigation with MVVM, which describes how to take full advantage of WPF's DataTemplates to solve the challenges presented by MVVM navigation. Lim's approach provides the "best" solution as it greatly reduces the need for any Framework dependencies; however, there really is no "great" way to solve the problem.

The greatest objection to Rachel's approach in general is that the View Model then becomes responsible for - or, "defines" - it's own relationship to the View. For Lim's solution, this is a minor objection for two reasons (and does nothing to further justify other bad architectural decisions, described later):

1.) The DataTemplate relationship is not enforced by anything other than an XAML file, i.e. the View Models are, themselves, never directly aware of their Views nor vice versa, so, even our View's constructor is further simplified, take for example the Home class constructor - now without need for a reference to a View Model:

public Home()
{
    InitializeComponent();
}

2.) As the relationship is expressed nowhere else, the association between a particular View and View Model is easy to change.

An application should be able to function (model the domain) sufficiently without a prescribed View. This ideal stems from the effort to best decouple the application's supporting architecture and to foster further application of SOLID programming principles, most specifically dependency injection.

The XAML file - not a third-party dependency container - becomes the pivotal point for resolution of the relationship between the View and the View Model (which, consequently, directly contradicts the OP).

Dependency Injection

An application should be designed to be entirely agnostic of its container and - even better - any implementation-specific information regarding service dependencies. What this allows us to do is to "assign" (inject) services that oblige by a certain contract (interface) to various classes that make up the meat of our applications' functionality.

This leaves us with two criteria for "good design":

  1. Application classes should be able to support a variety of services that perform the same task, as long as they oblige by a described contract.
  2. Application classes should never be aware of - or reference - their container, even if the container is Unity, PRISM, or Galasoft.

The second point is of utmost importance and is a "rule" that is broken, commonly. That "rule breaking" being the inspiration behind the original post.

The response to the navigation problem in many applications is to inject a wrapped dependency injection container which is then used to make calls out of the implementing class to resolve dependencies. The class now knows about the container and, worse, has even greater, more concrete knowledge of what specifics it needs in order to perform its operations (and some might argue more difficult to maintain).

Any knowledge of a dependency resolution container on the part of the View, View Model, or Model is an anti-pattern (you can read more about the justification for that statement elsewhere).

A well written application that relies on dependency injection could function without a dependency injection Framework, i.e. you could resolve the dependencies, manually, from handwritten bootstrapper (though that would require a great deal of careful work).

Lim's solution enables us to "not need" a reference to a container from within the implementation.

What we should be left with are constructors that look like:

// View:
public Home() { //... } 

// View Model 
public HomeViewModel (SomeModelClass someModel, MaybeAService someService)

If one goal is modularization and reusability, then the above achieves that, well. We could continue to abstract this out further by ensuring those passed-in dependencies are contract fulfillments via interfaces.

查看更多
登录 后发表回答