Здається, ця нитка дуже популярна, і буде сумно не згадувати тут, що існує альтернативний спосіб - ViewModel First Navigation
. Більшість фреймворків MVVM там використовуються, проте якщо ви хочете зрозуміти, про що йдеться, продовжуйте читати.
Вся офіційна документація Xamarin.Forms демонструє просте, але трохи не чисте рішення MVVM. Це тому, що Page
(Вид) нічого не повинен знати про 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
(Перегляди). Однією з переваг, крім чіткого розділення проблем, є те, що ви можете легко передати параметри наступномуViewModel
або виконати код ініціалізації асинхронізації відразу після навігації. Тепер до деталей.
(Я спробую максимально спростити всі приклади коду).
1. Насамперед нам потрібне місце, де ми могли б реєструвати всі наші об’єкти та необов'язково визначати їхнє життя. Для цього ми можемо використовувати контейнер МОК, ви можете вибрати його самостійно. У цьому прикладі я буду використовувати автофактор (це один з найшвидших доступних). Ми можемо зберегти посилання на нього, 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) - у 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.Зазвичай нам знадобиться 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
підхід, і є безкоштовні шаблони застосувань для підприємств, використовуючи електронну книгу Xamarin.Forms, яка детально пояснює це та багато інших цікавих тем.