Dieser Thread scheint sehr beliebt zu sein und es wird traurig sein, hier nicht zu erwähnen, dass es einen alternativen Weg gibt - ViewModel First Navigation
. Die meisten MVVM-Frameworks verwenden es. Wenn Sie jedoch verstehen möchten, worum es geht, lesen Sie weiter.
Die gesamte offizielle Xamarin.Forms-Dokumentation zeigt eine einfache, aber etwas nicht MVVM-reine Lösung. Das liegt daran, dass die Page
(Ansicht) nichts über die wissen sollte ViewModel
und umgekehrt. Hier ist ein gutes Beispiel für diesen Verstoß:
// 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>
Wenn Sie eine 2-seitige Anwendung haben, ist dieser Ansatz möglicherweise gut für Sie. Wenn Sie jedoch an einer Lösung für große Unternehmen arbeiten, sollten Sie einen ViewModel First Navigation
Ansatz wählen. Es ist ein etwas komplizierterer, aber viel saubererer Ansatz, mit dem Sie ViewModels
zwischen Pages
(Ansichten) navigieren können . Einer der Vorteile neben der klaren Trennung von Bedenken besteht darin, dass Sie problemlos Parameter an den nächsten übergeben ViewModel
oder direkt nach der Navigation einen asynchronen Initialisierungscode ausführen können. Nun zu den Details.
(Ich werde versuchen, alle Codebeispiele so weit wie möglich zu vereinfachen).
1. Zunächst benötigen wir einen Ort, an dem wir alle unsere Objekte registrieren und optional ihre Lebensdauer definieren können. Für diese Angelegenheit können wir einen IOC-Container verwenden, Sie können selbst einen auswählen. In diesem Beispiel verwende ich Autofac (es ist eines der schnellsten verfügbaren). Wir können einen Verweis darauf behalten, App
damit er global verfügbar ist (keine gute Idee, aber zur Vereinfachung erforderlich):
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.Wir benötigen ein Objekt, das für das Abrufen einer Page
(Ansicht) für ein bestimmtes Objekt verantwortlich ist, ViewModel
und umgekehrt. Der zweite Fall kann hilfreich sein, wenn Sie die Stamm- / Hauptseite der App festlegen. Dafür sollten wir uns auf eine einfache Konvention einigen, dass sich alle ViewModels
im ViewModels
Verzeichnis und Pages
(Ansichten) im Views
Verzeichnis befinden sollten. Mit anderen Worten, ViewModels
sollte im [MyApp].ViewModels
Namespace und Pages
(Ansichten) im [MyApp].Views
Namespace leben. Darüber hinaus sollten wir uns einig sein, dass WelcomeView
(Seite) ein WelcomeViewModel
und usw. haben sollte . Hier ist ein Codebeispiel für einen Mapper:
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.Für den Fall des Einstellens einer Stammseite benötigen wir eine Art ViewModelLocator
, die BindingContext
automatisch Folgendes festlegt :
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. Schließlich werden wir einen Ansatz brauchen NavigationService
, der den ViewModel First Navigation
Ansatz unterstützt:
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);
}
}
Wie Sie vielleicht sehen, gibt es eine BaseViewModel
abstrakte Basisklasse für alle, in ViewModels
denen Sie InitializeAsync
solche Methoden definieren können , die direkt nach der Navigation ausgeführt werden. Und hier ist ein Beispiel für die 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>());
}
}
Wie Sie verstehen, ist dieser Ansatz komplizierter, schwieriger zu debuggen und möglicherweise verwirrend. Es gibt jedoch viele Vorteile und Sie müssen es nicht selbst implementieren, da die meisten MVVM-Frameworks es sofort unterstützen. Das hier gezeigte Codebeispiel ist auf github verfügbar .
Es gibt viele gute Artikel zum Thema ViewModel First Navigation
Ansatz und es gibt ein kostenloses Enterprise Application Patterns mit Xamarin.Forms eBook, in dem dieses und viele andere interessante Themen ausführlich erläutert werden.