Sembra che questo thread sia molto popolare e sarà triste non menzionare qui che esiste un modo alternativo - ViewModel First Navigation
. La maggior parte dei framework MVVM disponibili lo utilizzano, tuttavia se vuoi capire di cosa si tratta, continua a leggere.
Tutta la documentazione ufficiale di Xamarin.Forms mostra una soluzione semplice, ma leggermente non MVVM. Questo perché Page
(View) non dovrebbe sapere nulla di ViewModel
e viceversa. Ecco un ottimo esempio di questa violazione:
// 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>
Se hai un'applicazione di 2 pagine, questo approccio potrebbe essere utile. Tuttavia, se stai lavorando a una soluzione per grandi aziende, è meglio che tu scelga un fileViewModel First Navigation
approccio. È un approccio leggermente più complicato ma molto più pulito che ti consente di navigare tra ViewModels
le Pages
(Visualizzazioni) invece di navigare . Uno dei vantaggi, oltre alla chiara separazione delle preoccupazioni, è che potresti facilmente passare i parametri al successivoViewModel
o eseguire un codice di inizializzazione asincrono subito dopo la navigazione. Ora ai dettagli.
(Cercherò di semplificare il più possibile tutti gli esempi di codice).
1. Prima di tutto abbiamo bisogno di un luogo in cui registrare tutti i nostri oggetti e opzionalmente definire la loro durata. Per questo possiamo usare un contenitore IOC, puoi sceglierne uno tu stesso. In questo esempio userò Autofac (è uno dei più veloci disponibili). Possiamo mantenere un riferimento ad esso in App
modo che sia disponibile a livello globale (non una buona idea, ma necessaria per semplificazione):
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.Avremo bisogno di un oggetto responsabile del recupero di una Page
(Vista) per uno specifico ViewModel
e viceversa. Il secondo caso potrebbe essere utile in caso di impostazione della pagina principale / principale dell'app. Per questo dovremmo concordare una semplice convenzione secondo cui tutto ViewModels
dovrebbe essere nella ViewModels
directory e Pages
(Views) dovrebbe essere nella Views
directory. In altre parole, ViewModels
dovrebbe risiedere nello [MyApp].ViewModels
spazio dei nomi e Pages
(Visualizzazioni) nello [MyApp].Views
spazio dei nomi. In aggiunta a ciò dovremmo convenire che WelcomeView
(Pagina) dovrebbe avere un fileWelcomeViewModel
ed ecc. Ecco un esempio di codice di un mappatore:
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.Per il caso di impostazione di una pagina principale, avremo bisogno di un tipo ViewModelLocator
che imposterà l'estensioneBindingContext
automaticamente:
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.Infine avremo bisogno di un NavigationService
supporto che supportiViewModel First Navigation
approccio :
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);
}
}
Come puoi vedere, esiste una BaseViewModel
classe base astratta per tutti i casi in ViewModels
cui puoi definire metodi InitializeAsync
che verranno eseguiti subito dopo la navigazione. Ed ecco un esempio di navigazione:
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>());
}
}
Come capisci, questo approccio è più complicato, più difficile da eseguire il debug e potrebbe creare confusione. Tuttavia ci sono molti vantaggi e in realtà non è necessario implementarlo da soli poiché la maggior parte dei framework MVVM lo supporta immediatamente. L'esempio di codice mostrato qui è disponibile su GitHub .
Ci sono molti buoni articoli ViewModel First Navigation
sull'approccio e c'è un Enterprise Application Patterns gratuito che usa Xamarin.Forms eBook che spiega questo e molti altri argomenti interessanti in dettaglio.