似乎这个线程很受欢迎,如果不提这里还有另一种方法 - ViewModel First Navigation,那就太遗憾了。大多数 MVVM 框架都在使用它,但是如果您想了解它的含义,请继续阅读。
所有官方 Xamarin.Forms 文档都展示了一个简单但稍微不是 MVVM 纯解决方案的解决方案。那是因为Page(View) 应该对ViewModel 一无所知,反之亦然。这是这种违规的一个很好的例子:
// C# version
public partial class MyPage : ContentPage
{
public MyPage()
{
InitializeComponent();
// Violation
this.BindingContext = new MyViewModel();
}
}
// XAML version
<?xml version="1.0" encoding="utf-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:MyApp.ViewModel"
x:Class="MyApp.Views.MyPage">
<ContentPage.BindingContext>
<!-- Violation -->
<viewmodels:MyViewModel />
</ContentPage.BindingContext>
</ContentPage>
如果您有一个 2 页的应用程序,这种方法可能对您有好处。但是,如果您正在开发大型企业解决方案,您最好使用ViewModel First Navigation 方法。这是一种稍微复杂但更简洁的方法,它允许您在ViewModels 之间导航,而不是在Pages(Views) 之间导航。除了清晰的关注点分离之外,优势之一是您可以轻松地将参数传递给下一个ViewModel 或在导航后立即执行异步初始化代码。现在详细介绍。
(我会尽量简化所有代码示例)。
1. 首先,我们需要一个可以注册所有对象并可选择定义其生命周期的地方。对于这个问题我们可以使用一个IOC容器,你可以自己选择一个。在这个例子中,我将使用Autofac(它是最快的可用之一)。我们可以在App 中保留对它的引用,这样它就可以在全球范围内使用(这不是一个好主意,但需要简化):
public class DependencyResolver
{
static IContainer container;
public DependencyResolver(params Module[] modules)
{
var builder = new ContainerBuilder();
if (modules != null)
foreach (var module in modules)
builder.RegisterModule(module);
container = builder.Build();
}
public T Resolve<T>() => container.Resolve<T>();
public object Resolve(Type type) => container.Resolve(type);
}
public partial class App : Application
{
public DependencyResolver DependencyResolver { get; }
// Pass here platform specific dependencies
public App(Module platformIocModule)
{
InitializeComponent();
DependencyResolver = new DependencyResolver(platformIocModule, new IocModule());
MainPage = new WelcomeView();
}
/* The rest of the code ... */
}
2.我们需要一个对象来负责为特定的ViewModel 检索Page(视图),反之亦然。第二种情况在设置应用程序的根/主页面时可能很有用。为此,我们应该同意一个简单的约定,即所有ViewModels 都应该在ViewModels 目录中,Pages(Views) 应该在Views 目录中。换句话说,ViewModels 应该存在于 [MyApp].ViewModels 命名空间中,Pages(Views) 应该存在于 [MyApp].Views 命名空间中。除此之外,我们应该同意WelcomeView(Page) 应该有一个WelcomeViewModel 等等。这是一个映射器的代码示例:
public class TypeMapperService
{
public Type MapViewModelToView(Type viewModelType)
{
var viewName = viewModelType.FullName.Replace("Model", string.Empty);
var viewAssemblyName = GetTypeAssemblyName(viewModelType);
var viewTypeName = GenerateTypeName("{0}, {1}", viewName, viewAssemblyName);
return Type.GetType(viewTypeName);
}
public Type MapViewToViewModel(Type viewType)
{
var viewModelName = viewType.FullName.Replace(".Views.", ".ViewModels.");
var viewModelAssemblyName = GetTypeAssemblyName(viewType);
var viewTypeModelName = GenerateTypeName("{0}Model, {1}", viewModelName, viewModelAssemblyName);
return Type.GetType(viewTypeModelName);
}
string GetTypeAssemblyName(Type type) => type.GetTypeInfo().Assembly.FullName;
string GenerateTypeName(string format, string typeName, string assemblyName) =>
string.Format(CultureInfo.InvariantCulture, format, typeName, assemblyName);
}
3.对于设置根页面的情况,我们需要ViewModelLocator,它会自动设置BindingContext:
public static class ViewModelLocator
{
public static readonly BindableProperty AutoWireViewModelProperty =
BindableProperty.CreateAttached("AutoWireViewModel", typeof(bool), typeof(ViewModelLocator), default(bool), propertyChanged: OnAutoWireViewModelChanged);
public static bool GetAutoWireViewModel(BindableObject bindable) =>
(bool)bindable.GetValue(AutoWireViewModelProperty);
public static void SetAutoWireViewModel(BindableObject bindable, bool value) =>
bindable.SetValue(AutoWireViewModelProperty, value);
static ITypeMapperService mapper = (Application.Current as App).DependencyResolver.Resolve<ITypeMapperService>();
static void OnAutoWireViewModelChanged(BindableObject bindable, object oldValue, object newValue)
{
var view = bindable as Element;
var viewType = view.GetType();
var viewModelType = mapper.MapViewToViewModel(viewType);
var viewModel = (Application.Current as App).DependencyResolver.Resolve(viewModelType);
view.BindingContext = viewModel;
}
}
// Usage example
<?xml version="1.0" encoding="utf-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:MyApp.ViewModel"
viewmodels:ViewModelLocator.AutoWireViewModel="true"
x:Class="MyApp.Views.MyPage">
</ContentPage>
4.最后,我们需要一个支持ViewModel First Navigation 方法的NavigationService:
public class NavigationService
{
TypeMapperService mapperService { get; }
public NavigationService(TypeMapperService mapperService)
{
this.mapperService = mapperService;
}
protected Page CreatePage(Type viewModelType)
{
Type pageType = mapperService.MapViewModelToView(viewModelType);
if (pageType == null)
{
throw new Exception($"Cannot locate page type for {viewModelType}");
}
return Activator.CreateInstance(pageType) as Page;
}
protected Page GetCurrentPage()
{
var mainPage = Application.Current.MainPage;
if (mainPage is MasterDetailPage)
{
return ((MasterDetailPage)mainPage).Detail;
}
// TabbedPage : MultiPage<Page>
// CarouselPage : MultiPage<ContentPage>
if (mainPage is TabbedPage || mainPage is CarouselPage)
{
return ((MultiPage<Page>)mainPage).CurrentPage;
}
return mainPage;
}
public Task PushAsync(Page page, bool animated = true)
{
var navigationPage = Application.Current.MainPage as NavigationPage;
return navigationPage.PushAsync(page, animated);
}
public Task PopAsync(bool animated = true)
{
var mainPage = Application.Current.MainPage as NavigationPage;
return mainPage.Navigation.PopAsync(animated);
}
public Task PushModalAsync<TViewModel>(object parameter = null, bool animated = true) where TViewModel : BaseViewModel =>
InternalPushModalAsync(typeof(TViewModel), animated, parameter);
public Task PopModalAsync(bool animated = true)
{
var mainPage = GetCurrentPage();
if (mainPage != null)
return mainPage.Navigation.PopModalAsync(animated);
throw new Exception("Current page is null.");
}
async Task InternalPushModalAsync(Type viewModelType, bool animated, object parameter)
{
var page = CreatePage(viewModelType);
var currentNavigationPage = GetCurrentPage();
if (currentNavigationPage != null)
{
await currentNavigationPage.Navigation.PushModalAsync(page, animated);
}
else
{
throw new Exception("Current page is null.");
}
await (page.BindingContext as BaseViewModel).InitializeAsync(parameter);
}
}
您可能会看到BaseViewModel - 所有ViewModels 的抽象基类,您可以在其中定义InitializeAsync 之类的方法,这些方法将在导航后立即执行。下面是一个导航示例:
public class WelcomeViewModel : BaseViewModel
{
public ICommand NewGameCmd { get; }
public ICommand TopScoreCmd { get; }
public ICommand AboutCmd { get; }
public WelcomeViewModel(INavigationService navigation) : base(navigation)
{
NewGameCmd = new Command(async () => await Navigation.PushModalAsync<GameViewModel>());
TopScoreCmd = new Command(async () => await navigation.PushModalAsync<TopScoreViewModel>());
AboutCmd = new Command(async () => await navigation.PushModalAsync<AboutViewModel>());
}
}
据您了解,这种方法更复杂、更难调试并且可能会造成混淆。但是有很多优点,而且您实际上不必自己实现它,因为大多数 MVVM 框架都支持它开箱即用。 github 上提供了此处演示的代码示例。
有很多关于 ViewModel First Navigation 方法的好文章,还有一本免费的 Enterprise Application Patterns using Xamarin.Forms 电子书详细解释了这个和许多其他有趣的主题。