(я постараюсь максимально упростить все примеры кода).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
(просмотр) для определенного 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 , которая подробно объясняет эту и многие другие интересные темы.