On dirait que ce fil est très populaire et il sera triste de ne pas mentionner ici qu'il existe un moyen alternatif - ViewModel First Navigation
. La plupart des frameworks MVVM qui l'utilisent, mais si vous voulez comprendre de quoi il s'agit, continuez à lire.
Toute la documentation officielle de Xamarin.Forms montre une solution simple, mais légèrement pas pure MVVM. C'est parce que la Page
(Vue) ne doit rien savoir sur le ViewModel
et vice versa. Voici un excellent exemple de cette violation:
// 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>
Si vous avez une application de 2 pages, cette approche pourrait être bonne pour vous. Cependant, si vous travaillez sur une solution de grande entreprise, vous feriez mieux d'adopter une ViewModel First Navigation
approche. C'est une approche légèrement plus compliquée mais beaucoup plus propre qui vous permet de naviguer ViewModels
entre Pages
(Vues) au lieu de naviguer entre (Vues). L'un des avantages à côté de la séparation claire des préoccupations est que vous pouvez facilement passer des paramètres au suivant.ViewModel
ou exécuter un code d'initialisation asynchrone juste après la navigation. Passons maintenant aux détails.
(Je vais essayer de simplifier au maximum tous les exemples de code).
1. Tout d'abord, nous avons besoin d'un endroit où nous pourrions enregistrer tous nos objets et définir éventuellement leur durée de vie. Pour cela, nous pouvons utiliser un conteneur IOC, vous pouvez en choisir un vous-même. Dans cet exemple, j'utiliserai Autofac (c'est l'un des plus rapides disponibles). Nous pouvons en garder une référence dans le App
afin qu'il soit disponible dans le monde entier (ce n'est pas une bonne idée, mais nécessaire pour simplifier):
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 ... */
}
Nous aurons besoin d'un objet chargé de récupérer une Page
(Vue) pour un objet spécifique ViewModel
et vice versa. Le deuxième cas peut être utile en cas de configuration de la page racine / principale de l'application. Pour cela, nous devrions nous mettre d'accord sur une convention simple selon laquelle tous les ViewModels
devraient être dans le ViewModels
répertoire et Pages
(Views) devraient être dans le Views
répertoire. En d'autres termes, ViewModels
devrait vivre dans l' [MyApp].ViewModels
espace de noms et Pages
(Vues) dans l' [MyApp].Views
espace de noms. En plus de cela, nous devrions convenir que WelcomeView
(Page) devrait avoir un WelcomeViewModel
et etc. Voici un exemple de code d'un mappeur:
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);
}
Dans le cas de la configuration d'une page racine, nous aurons besoin d'une sorte de ViewModelLocator
paramètre qui définira BindingContext
automatiquement:
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.Enfin, nous aurons besoin d'une approche NavigationService
qui soutiendra 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);
}
}
Comme vous pouvez le voir, il existe une BaseViewModel
classe de base abstraite pour tous les ViewModels
domaines dans lesquels vous pouvez définir des méthodes comme InitializeAsync
celle-ci qui seront exécutées juste après la navigation. Et voici un exemple de navigation:
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>());
}
}
Comme vous le comprenez, cette approche est plus compliquée, plus difficile à déboguer et peut prêter à confusion. Cependant, il existe de nombreux avantages et vous n'avez pas à l'implémenter vous-même puisque la plupart des frameworks MVVM le prennent en charge dès la sortie de la boîte. L'exemple de code présenté ici est disponible sur github .
Il y a beaucoup de bons articles sur l' ViewModel First Navigation
approche et il existe un eBook gratuit pour les applications d'entreprise utilisant Xamarin.Forms eBook qui explique cela et de nombreux autres sujets intéressants en détail.