Wygląda na to, że ten wątek jest bardzo popularny i będzie smutno nie wspomnieć, że istnieje alternatywny sposób - ViewModel First Navigation
. Większość frameworków MVVM, które go używają, jednak jeśli chcesz zrozumieć, o co chodzi, kontynuuj czytanie.
Cała oficjalna dokumentacja platformy Xamarin.Forms przedstawia proste, ale nieco inne niż MVVM czyste rozwiązanie. Dzieje się tak dlatego, że Page
(Widok) nie powinien nic wiedzieć o tym ViewModel
i odwrotnie. Oto świetny przykład tego naruszenia:
// 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>
Jeśli masz dwustronicową aplikację, to podejście może być dla Ciebie dobre. Jeśli jednak pracujesz nad rozwiązaniem dla dużego przedsiębiorstwa, lepiej wybierz ViewModel First Navigation
podejście. Jest to nieco bardziej skomplikowane, ale znacznie czystsze podejście, które umożliwia nawigację między ( ViewModels
zamiast nawigacji) między Pages
(Widokami). Jedną z zalet oprócz wyraźnego oddzielenia obaw jest to, że można łatwo przekazywać parametry do kolejnychViewModel
lub wykonać kod inicjujący asynchroniczny zaraz po nawigacji. Teraz do szczegółów.
(Postaram się maksymalnie uprościć wszystkie przykłady kodu).
1. Przede wszystkim potrzebujemy miejsca, w którym moglibyśmy zarejestrować wszystkie nasze obiekty i opcjonalnie zdefiniować ich żywotność. W tym przypadku możemy użyć kontenera IOC, możesz sam wybrać. W tym przykładzie użyję Autofaca (jest to jeden z najszybszych dostępnych). Możemy zachować odniesienie do tego w sekcji, App
aby było dostępne globalnie (niezbyt dobry pomysł, ale potrzebny do uproszczenia):
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. Będziemy potrzebować obiektu odpowiedzialnego za pobranie Page
(View) dla konkretnego ViewModel
i odwrotnie. Drugi przypadek może być przydatny w przypadku ustawienia strony głównej / głównej aplikacji. W tym celu powinniśmy zgodzić się na prostą konwencję, że wszystko ViewModels
powinno znajdować się w ViewModels
katalogu, a Pages
(widoki) w Views
katalogu. Innymi słowy, ViewModels
powinien żyć w [MyApp].ViewModels
przestrzeni nazw i Pages
(Widoki) w [MyApp].Views
przestrzeni nazw. Oprócz tego powinniśmy się zgodzić, że WelcomeView
(Strona) powinna mieć a WelcomeViewModel
i itp. Oto przykład kodu mappera:
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);
}
W przypadku ustawienia strony głównej będziemy potrzebować czegoś takiego ViewModelLocator
, co ustawi BindingContext
automatycznie:
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>
W końcu będziemy potrzebować podejścia, NavigationService
które będzie wspierać 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);
}
}
Jak widać, istnieje BaseViewModel
- abstrakcyjna klasa bazowa dla wszystkich, w ViewModels
których można zdefiniować metody, takie jak InitializeAsync
ta, zostaną wykonane zaraz po nawigacji. A oto przykład nawigacji:
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>());
}
}
Jak rozumiesz, to podejście jest bardziej skomplikowane, trudniejsze do debugowania i może być mylące. Jednak jest wiele zalet, a w rzeczywistości nie musisz wdrażać go samodzielnie, ponieważ większość frameworków MVVM obsługuje go po wyjęciu z pudełka. Przykładowy kod pokazany tutaj jest dostępny na github .
Istnieje wiele dobrych artykułów na temat ViewModel First Navigation
podejścia, a także bezpłatny eBook wzorców aplikacji korporacyjnych wykorzystujący Xamarin.Forms, który szczegółowo wyjaśnia ten i wiele innych interesujących tematów.