官术网_书友最值得收藏!

Locating View Models

For this method, we need to create interfaces for each of our View Models. It's called View Model Location and it's fairly similar to the Dependency Injection example that we have already seen. In fact, we could even use our existing DependencyManager to achieve a similar result. Let's take a quick look at that first:

DependencyManager.Instance.Register<IUserViewModel, UserViewModel>();
     
...
     
public partial class UserView : UserControl 
{ 
  public UserView() 
  { 
    InitializeComponent(); 
    DataContext = DependencyManager.Instance.Resolve<IUserViewModel>(); 
  } 
} 
    
...
     
<Views:UsersView /> 

In this example, we associate the IUserViewModel interface with the UserViewModel concrete implementation of that interface in some initialization code and later, resolve the dependency, before setting it as the View's DataContext value. After declaring our Views in the XAML, they automatically hook themselves up to their related View Models at runtime.

This method of connecting Views to View Models works View first, where we declare the View and it instantiates its own View Model and sets its own DataContext. The downside with this method is that we have to create an interface for all of our View Models and register and resolve each of them using the DependencyManager.

The main difference between this implementation and that of a View Model Locator is that a locator provides a level of abstraction from our Singleton class, which enables us to indirectly instantiate our View Models from the XAML, without using the code behind. They also have a little extra specific functionality that enables dummy data to be used at design time. Let's take a look at the simplest possible example:

using CompanyName.ApplicationName.Managers; 
using CompanyName.ApplicationName.ViewModels; 
using CompanyName.ApplicationName.ViewModels.Interfaces; 
 
namespace CompanyName.ApplicationName.Views.ViewModelLocators 
{ 
  public class ViewModelLocator 
  { 
    public IUserViewModel UserViewModel 
    { 
      get { return DependencyManager.Instance.Resolve<IUserViewModel>(); } 
    } 
  } 
} 

Here, we have a very basic View Model Locator that simply locates a single View Model. It is important that this View Model class has an empty constructor so that it can be instantiated from the XAML. Let's see how we can do this:

<UserControl x:Class="CompanyName.ApplicationName.Views.UserView" 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
  xmlns:ViewModelLocators="clr-namespace:  
    CompanyName.ApplicationName.Views.ViewModelLocators" 
  Height="30" Width="300"> 
  <UserControl.Resources> 
    <ViewModelLocators:ViewModelLocator x:Key="ViewModelLocator" /> 
  </UserControl.Resources> 
  <UserControl.DataContext> 
    <Binding Path="UserViewModel" 
      Source="{StaticResource ViewModelLocator}" /> 
  </UserControl.DataContext> 
  <TextBlock Text="{Binding User.Name}" /> 
</UserControl> 

As a side note, you may have noticed that our ViewModelLocator class has been declared in the Views project. The location of this class is not very important, but it must have references to both the ViewModels and the Views projects, and this severely limits the number of projects in which it can reside. Typically, the only projects that will have access to the classes from both of these projects will be the Views project and the startup project.

Getting back to our example, an instance of the ViewModelLocator class is declared in the View's Resources section and this will only work if we have a parameterless constructor (including the default parameterless constructor that is declared for us if we do not explicitly declare a constructor). Without a parameterless constructor, we will receive an error in the Visual Studio designer.

Our View sets its own DataContext property in XAML this time, using a binding path to the UserViewModel property from our ViewModelLocator resource. The property then utilizes our DependencyManager to resolve the concrete implementation of the IUserViewModel interface and return it for us.

There are other benefits to using this pattern as well though. One problem often faced by WPF developers is that the Visual Studio WPF Designer cannot resolve the interfaces that are used to back their concrete implementations, nor can it access the application data sources during design time. The result of this is that the designer does not typically display data items that cannot be resolved.

One thing that we can do with our ViewModelLocator resource is to provide mock View Models that have dummy data returned from their properties that we can use to help visualize our Views as we construct them. To achieve this, we can make use of the IsInDesignMode Attached Property from the DesignerProperties .NET class:

public bool IsDesignTime 
{ 
  get { return 
    DesignerProperties.GetIsInDesignMode(new DependencyObject()); } 
} 

The DependencyObject object here is required by the Attached Property and, in fact, is the object that is being checked. As all objects supplied here would return the same value, we are free to use a new one each time. If we are concerned that this property will be called more frequently than the garbage collector, we could opt to use a single member instead, just for this purpose:

private DependencyObject dependencyObject = new DependencyObject(); 
 
public bool IsDesignTime 
{ 
  get { return DesignerProperties.GetIsInDesignMode(dependencyObject); } 
} 

However, if we need a DependencyObject object just for this purpose, then we could simplify things further by extending our ViewModelLocator class from the DependencyObject class and use itself as the required parameter. Of course, this would mean that our class would inherit unwanted properties, so some might prefer to avoid doing this. Let's see how we could use this property to provide the WPF Designer with mock data at design time:

using System.ComponentModel; 
using System.Windows; 
using CompanyName.ApplicationName.Managers; 
using CompanyName.ApplicationName.ViewModels; 
using CompanyName.ApplicationName.ViewModels.Interfaces; 
 
namespace CompanyName.ApplicationName.Views.ViewModelLocators 
{ 
  public class ViewModelLocator : DependencyObject 
  { 
    public bool IsDesignTime 
    { 
      get { return DesignerProperties.GetIsInDesignMode(this); } 
    } 
 
    public IUserViewModel UserViewModel 
    { 
      get 
      { 
        return IsDesignTime ? new MockUserViewModel() :  
          DependencyManager.Instance.Resolve<IUserViewModel>();  
      } 
    } 
  } 
} 

If you look at our UserViewModel property, you'll see the value that we return is now dependent upon the value of the IsDesignTime property. If we are in design time, for example, when the View file is open in the WPF Designer, then the MockUserViewModel class will be returned. At runtime, however, the concrete implementation of our IUserViewModel interface that we registered with the DependencyManager will be returned instead.

The MockUserViewModel class will typically hardcode some mock data and return it from its properties when requested. In this manner, the WPF Designer will be able to visualize the data for the developers or designers while they build the Views.

However, each View will require a new property in our locator class and we'll need to copy this conditional operator statement from the preceding code for each. As always in OOP, there is a further abstraction that we could make to hide that implementation away from the developers that will use our framework. We could create a generic base class for our View Model Locator:

using System.ComponentModel; 
using System.Windows; 
using CompanyName.ApplicationName.Managers; 
 
namespace CompanyName.ApplicationName.Views.ViewModelLocators 
{ 
  public abstract class BaseViewModelLocator<T> : DependencyObject  
    where T : class 
  { 
    private T runtimeViewModel, designTimeViewModel; 
 
    protected bool IsDesignTime 
    { 
      get { return DesignerProperties.GetIsInDesignMode(this); } 
    } 
 
    public T ViewModel 
    { 
      get { return IsDesignTime ?  
        DesignTimeViewModel : RuntimeViewModel; } 
    } 
 
    protected T RuntimeViewModel 
    { 
      get { return runtimeViewModel ??  
        (runtimeViewModel = DependencyManager.Instance.Resolve<T>()); } 
    } 
 
    protected T DesignTimeViewModel 
    { 
      set { designTimeViewModel = value; } 
      get { return designTimeViewModel; } 
    } 
  } 
} 

We start by declaring an abstract class that takes a generic type parameter, which represents the interface type of the View Model that we are trying to locate. Once again, note the generic type constraint declared on the generic type parameter that specifies that the type used must be a class. This is now required because this class calls the Resolve method of the DependencyManager class and that has the same constraint declared upon it.

We have two internal members of the relevant type of View Model interface that back the properties with the same names. There's one for our runtime View Model and one for our design time View Model. The third View Model property of the same type is the one that we will data-bind to from Views and it uses our IsDesignTime property to determine which View Model to return.

A nice touch in this class is that it does a lot of the connection work for the developers. They don't need to concern themselves with the implementation of the IsDesignTime property, and this base class will even attempt to automatically resolve the concrete View Model dependency for the runtime View Model property. Therefore, the developer need only declare the following code for each View Model to take advantage of this functionality:

using CompanyName.ApplicationName.ViewModels; 
using CompanyName.ApplicationName.ViewModels.Interfaces; 
 
namespace CompanyName.ApplicationName.Views.ViewModelLocators 
{ 
  public class UserViewModelLocator : BaseViewModelLocator<IUserViewModel> 
  { 
    public UserViewModelLocator() 
    { 
      DesignTimeViewModel = new MockUserViewModel(); 
    } 
  } 
} 

It could be set up in the UI with very little difference to our original locator version:

<UserControl x:Class="CompanyName.ApplicationName.Views.UserView" 
  ... 
  <UserControl.Resources> 
    <Locators:UserViewModelLocator x:Key="ViewModelLocator" /> 
  </UserControl.Resources> 
  <UserControl.DataContext> 
    <Binding Path="ViewModel" Source="{StaticResource ViewModelLocator}" /> 
  </UserControl.DataContext> 
  ... 
</UserControl> 

Note that although this should work automatically in newer versions of Visual Studio, you may need to provide a helping hand to the WPF Designer in older versions. The mc:Ignorable attribute specifies which XAML namespace prefixes encountered in a markup file may be ignored by an XAML processor and the d XAML namespace is used by the Designer, so we can specify a DataContext location to it directly at design time:

xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DataContext="{Binding ViewModel,
Source={StaticResource ViewModelLocator}}"

While there is a clear benefit to this arrangement, as always, we have to weigh up whether the cost of any such abstractions will be worth the benefits. For some, the cost of extracting an interface, declaring a mock version of it to use at design time, and creating a View Model Locator for each View Model will definitely be worth the benefit of designing Views that visualize their data.

For others, it simply won't be worth it. Each time we add a level of abstraction, we have more work to achieve to arrive at the same end goal. We need to decide whether each abstraction is viable in our own situations and build our application frameworks accordingly.

主站蜘蛛池模板: 澜沧| 营口市| 巴里| 隆化县| 应城市| 漯河市| 万州区| 静宁县| 土默特左旗| 泾川县| 榆中县| 宝鸡市| 邛崃市| 莒南县| 获嘉县| 招远市| 南通市| 连江县| 麟游县| 汝州市| 海伦市| 皋兰县| 德化县| 类乌齐县| 莱阳市| 澄城县| 遂川县| 杭锦旗| 沾益县| 普宁市| 东城区| 准格尔旗| 子洲县| 瑞昌市| 军事| 公安县| 芒康县| 元氏县| 九江县| 安阳市| 万州区|