Похоже, эта ветка очень популярна, и будет грустно не упомянуть здесь, что есть альтернативный способ - 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>
Если у вас есть двухстраничное приложение, этот подход может вам подойти. Однако, если вы работаете над большим корпоративным решением, вам лучше выбрать ViewModel First Navigation
подход. Это немного более сложный, но гораздо более понятный подход, который позволяет вам перемещаться ViewModels
между Pages
(представлениями), а не между ними . Одним из преимуществ помимо четкого разделения задач является то, что вы можете легко передать параметры следующему 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. Нам понадобится объект, ответственный за получение Page
(View) для конкретного ViewModel
и наоборот. Второй случай может быть полезен в случае установки корневой / главной страницы приложения. Для этого мы должны договориться о простом соглашении, согласно которому все объекты ViewModels
должны находиться в ViewModels
каталоге, а Pages
(представления) должны находиться в Views
каталоге. Другими словами, он ViewModels
должен находиться в [MyApp].ViewModels
пространстве имен, а Pages
(Представления) - в [MyApp].Views
пространстве имен. В дополнение к этому мы должны согласиться, что WelcomeView
(Страница) должна иметь 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.Наконец, нам понадобится NavigationService
поддерживающий ViewModel First Navigation
подход:
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 с использованием Xamarin.Forms, в которой подробно объясняется эта и многие другие интересные темы.