Sepertinya utas ini sangat populer dan akan menyedihkan untuk tidak menyebutkan di sini bahwa ada cara alternatif - ViewModel First Navigation
. Sebagian besar kerangka kerja MVVM di luar sana menggunakannya, namun jika Anda ingin memahami tentang apa itu, lanjutkan membaca.
Semua dokumentasi resmi Xamarin.Forms mendemonstrasikan solusi murni MVVM yang sederhana, namun sedikit tidak. Itu karena Page
(View) seharusnya tidak tahu apa-apa tentang the ViewModel
dan sebaliknya. Berikut adalah contoh bagus dari pelanggaran ini:
// 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>
Jika Anda memiliki aplikasi 2 halaman, pendekatan ini mungkin baik untuk Anda. Namun jika Anda bekerja pada solusi perusahaan besar, Anda lebih baik menggunakan ViewModel First Navigation
pendekatan. Ini sedikit lebih rumit tetapi pendekatan yang jauh lebih bersih yang memungkinkan Anda untuk menavigasi di antara ViewModels
alih-alih navigasi di antara Pages
(Tampilan). Salah satu keuntungan di samping pemisahan perhatian yang jelas adalah Anda dapat dengan mudah meneruskan parameter ke parameter berikutnya ViewModel
atau menjalankan kode inisialisasi asinkron tepat setelah navigasi. Sekarang untuk detailnya.
(Saya akan mencoba menyederhanakan semua contoh kode sebanyak mungkin).
1. Pertama-tama kita membutuhkan tempat di mana kita bisa mendaftarkan semua objek kita dan secara opsional menentukan masa pakainya. Untuk masalah ini kita dapat menggunakan container IOC, Anda dapat memilihnya sendiri. Dalam contoh ini saya akan menggunakan Autofac (ini adalah salah satu yang tercepat yang tersedia). Kami dapat menyimpan referensi tentangnya App
agar tersedia secara global (bukan ide yang baik, tetapi diperlukan untuk penyederhanaan):
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. Kita akan membutuhkan objek yang bertanggung jawab untuk mengambil Page
(View) untuk spesifik ViewModel
dan sebaliknya. Kasus kedua mungkin berguna dalam kasus pengaturan root / halaman utama aplikasi. Untuk itu kita harus menyetujui konvensi sederhana bahwa semua ViewModels
harus ada di ViewModels
direktori dan Pages
(Views) harus ada di Views
direktori. Dengan kata lain ViewModels
harus hidup di [MyApp].ViewModels
namespace dan Pages
(Views) di [MyApp].Views
namespace. Selain itu, kita harus setuju bahwa WelcomeView
(Halaman) harus memiliki WelcomeViewModel
dan dll. Berikut adalah contoh kode 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. Untuk kasus pengaturan halaman root kita memerlukan semacam ViewModelLocator
yang akan mengatur BindingContext
secara otomatis:
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. Akhirnya kita membutuhkan pendekatan NavigationService
yang akan mendukung 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);
}
}
Seperti yang Anda lihat, ada BaseViewModel
- kelas dasar abstrak untuk semua ViewModels
tempat Anda dapat mendefinisikan metode seperti InitializeAsync
itu yang akan dieksekusi tepat setelah navigasi. Dan berikut adalah contoh navigasi:
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>());
}
}
Seperti yang Anda pahami, pendekatan ini lebih rumit, lebih sulit untuk di-debug, dan mungkin membingungkan. Namun ada banyak keuntungan plus Anda sebenarnya tidak harus menerapkannya sendiri karena sebagian besar kerangka kerja MVVM mendukungnya di luar kotak. Contoh kode yang ditunjukkan di sini tersedia di github .
Ada banyak artikel bagus tentang ViewModel First Navigation
pendekatan dan ada Pola Aplikasi Perusahaan gratis menggunakan eBook Xamarin.Forms yang menjelaskan hal ini dan banyak topik menarik lainnya secara rinci.