示例项目的来源: https://github.com/AntwanReno/navi
我在WPF MVVM中做了一个项目。它有三个子项目:WPF app,ViewModels(PCL)和Domain(PCL)。 WPF只是一个Window
个Frame
和两个Pages
。我将展示代码,但建议在repo中克隆/分叉我准备好的样本。
以下是WPF客户端的代码:
App.xaml.cs
namespace NaviWPFApp
{
using System.Windows;
using NaviWPFApp.Views;
using NaviWPFApp.Views.Pages;
public partial class App : Application
{
public static NavigationService Navigation;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
MainWindow mainWindow = new MainWindow();
mainWindow.Show();
Navigation = new NavigationService(mainWindow.MyFrame);
Navigation.Navigate<FirstPage>();
}
}
}
App.xaml 它只是:
<Application x:Class="NaviWPFApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:NaviWPFApp"
x:Name="Application">
<Application.Resources>
<local:ViewModelLocator x:Key="ViewModelLocator"/>
</Application.Resources>
</Application>
我有一个带框架的主窗口和两个非常相似的页面(没有代码隐藏):
<Window x:Class="NaviWPFApp.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="NaviWPFApp" Height="300" Width="300">
<Grid>
<Frame x:Name="MyFrame" Margin="10" />
</Grid>
</Window>
<Page x:Class="NaviWPFApp.Views.FirstPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" Title="FirstPage"
DataContext="{Binding FirstPageViewModel, Source={StaticResource ViewModelLocator}}">
<Grid>
<Button Command="{Binding GoToSecondPageCommand}" Height="30" Content="Go to second page" />
</Grid>
</Page>
<Page x:Class="NaviWPFApp.Views.SecondPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Title="SecondPage"
DataContext="{Binding Path=SecondPageViewModel, Source={StaticResource ViewModelLocator}}">
<StackPanel Margin="0, 100, 0, 0">
<Button Command="{Binding CountSomethingCommand}" Content="Count something" Height="30" />
<Button Command="{Binding BackToFirstPageCommand}" Content="Go back to page 1" Height="30" />
</StackPanel>
</Page>
我的客户端我还有两个额外的类ViewModelLocator和NavigationService - 用于页面之间的导航:
namespace NaviWPFApp
{
using NaviWPFApp.ViewModels.Pages;
public class ViewModelLocator
{
public FirstPageViewModel FirstPageViewModel => new FirstPageViewModel(App.Navigation);
public SecondPageViewModel SecondPageViewModel => new SecondPageViewModel(App.Navigation);
}
}
namespace NaviWPFApp
{
using System;
using System.Linq;
using System.Reflection;
using System.Windows.Controls;
using NaviWPFApp.ViewModels.Common;
public class NavigationService : INavigationService
{
readonly Frame frame;
public NavigationService(Frame frame)
{
this.frame = frame;
}
public void GoBack()
{
frame.GoBack();
}
public void GoForward()
{
frame.GoForward();
}
public bool Navigate(string page)
{
var type = Assembly.GetExecutingAssembly().GetTypes().SingleOrDefault(a => a.Name.Equals(page));
if (type == null) return false;
var src = Activator.CreateInstance(type);
return frame.Navigate(src);
}
public bool Navigate<T>(object parameter = null)
{
var type = typeof(T);
return Navigate(type, parameter);
}
public bool Navigate(Type source, object parameter = null)
{
var src = Activator.CreateInstance(source);
return frame.Navigate(src, parameter);
}
}
}
这是我的 ViewModels(便携式)项目:
UI中每个页面只有两个ViewModel类,INavigationService(我不想知道有关NavigationService实现和UI客户端的任何信息),MyObservableObject和MyCommand。
MyObservableObject
和MyCommand
是INotifyPropertyChanged
和ICommand
接口的典型实现。
所以这是一个接口和两个视图模型:
public interface INavigationService
{
void GoForward();
void GoBack();
bool Navigate(string page);
}
namespace NaviWPFApp.ViewModels
{
public class FirstPageViewModel : MyObservableObject
{
private readonly INavigationService navigationService;
public FirstPageViewModel(INavigationService navigationService)
{
this.navigationService = navigationService;
}
public MyCommand GoToSecondPageCommand
{
get { return new MyCommand(x => navigationService.Navigate("SecondPage")); }
}
}
}
namespace NaviWPFApp.ViewModels
{
public class SecondPageViewModel : MyObservableObject
{
private readonly INavigationService navigationService;
private readonly BusinessLogic businessLogic;
public SecondPageViewModel(INavigationService navigationService, BusinessLogic businessLogic = null)
{
this.navigationService = navigationService;
this.businessLogic = businessLogic;
}
public MyCommand BackToFirstPageCommand
{
get { return new MyCommand(x => navigationService.Navigate("FirstPage")); }
}
public MyCommand CountSomethingCommand
{
get { return new MyCommand(x => businessLogic?.CountSomething()); }
}
}
}
我的业务逻辑,就是这样:
public class BusinessLogic
{
private int counter = 0;
public bool CountSomething()
{
return ++counter > 10;
}
}
依赖关系很简单:域除了自己的操作之外什么都不知道, ViewModel 知道域名,但没有关于View和查看的信息。嗯,这是我的问题 - 它知道ViewModel,但View应该知道Domain吗?我将解释我的担忧,但这就是我的意思:
首先关注:正如您所看到的,导航全部在ViewModel中完成,而Business逻辑仅在第二页面视图模型中使用。 SecondPage视图不需要知道逻辑。
第二个问题:因为我试图坚持依赖注入,我想在程序开始时创建我的域对象(我只需要一个)。所以在protected override void OnStartup(StartupEventArgs e)
中,在VIEW中也是如此。我不知道如何将其传递给第二个视图模型,该模型是在ViewModelLocator
中创建的。
所以我的问题是:如何转换此代码,以便更多ViewModel定向?我想将我的Domain对象注入ViewModel(它所属的位置),而不是注入View。
感谢您的任何建议!
答案 0 :(得分:2)
您无法避免这种依赖,因为App.OnStartup
是一个组合根,这意味着App.OnStartup
知道所有内容。
但是,你可以避免的是你的应用程序中的这个全球道具:public static NavigationService Navigation;
。您只需将其注入您需要它的对象即可。
首先关注:如您所见,导航全部完成 ViewModel和业务逻辑仅用于第二页面视图模型。 SecondPage视图不需要知道逻辑。
SecondPage不必了解业务对象。应用必须知道。因此,您可以将对象注入定位器,定位器可以在时间到来时将此对象注入特定的ViewModel。
第二个问题:因为我试图坚持依赖注入, 我想创建我的域对象(我只需要一个) 该计划的开始。所以在受保护的覆盖空虚 OnStartup(StartupEventArgs e),所以在VIEW中。我不知道,怎么样 将其传递给第二个视图模型,该模型在ViewModelLocator中创建。
依赖注入。
以下是我的表现:
public class ViewModelLocator
{
private NavigationService navigationService;
private BusinessLogic businessLogic;
public void InjectNavigationService(NavigationService navigation)
{
navigationService = navigation;
}
public void InjectBusinessLogic(BusinessLogic logic)
{
businessLogic = logic;
}
public FirstPageViewModel FirstPageViewModel => new FirstPageViewModel(navigationService);
public SecondPageViewModel SecondPageViewModel => new SecondPageViewModel(navigationService, businessLogic);
}
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Create/resolve all your objects in Comoposition Root:
var businessLogic = new BusinessLogic();
// Here you will have locator created already, but mainWindow has not been created yet
// Retrive your locator
ViewModelLocator locator = Resources.Values.OfType<ViewModelLocator>().FirstOrDefault();
if (locator == null)
throw new NoNullAllowedException("ViewModelLocator cannot be null.");
MainWindow mainWindow = new MainWindow();
var navigation = new NavigationService(mainWindow.MyFrame);
// Inject your logic and navigation into locator
locator.InjectBusinessLogic(businessLogic);
locator.InjectNavigationService(navigation);
// Set up first page
navigation.Navigate<FirstPage>();
// and show the window
mainWindow.Show();
}
}
答案 1 :(得分:0)
简短的回答是,您无法避免,尤其是如果您使用依赖注入/ IoC容器(建议将其解耦并提高可测试性)。
你无法避免它的原因是因为(当使用IoC时)你需要通过构造函数注入你的依赖项(或者不是最佳的,通过属性/方法)。大多数IoC容器要求这些都是公开的。
由于大多数以这种方式注入的类型不在您的ViewModel PCL中,而是在您的模型(域)中并且公开可见,因此即使您不手动初始化ViewModel,也需要引用该程序集。
你的困惑来自术语&#34; View&#34;。在这种情况下,您的WPF项目有多个角色。
NavigationServce
实现(INavigationService
接口但属于ViewModel程序集)虽然您的观点不需要对您的域类进行任何引用,但您的应用程序确实如此(组合根,DI / IoC容器)。