Có vẻ như chủ đề này rất phổ biến và sẽ rất buồn khi không đề cập ở đây rằng có một cách thay thế - ViewModel First Navigation. Hầu hết các khuôn khổ MVVM hiện có sử dụng nó, tuy nhiên nếu bạn muốn hiểu nội dung của nó, hãy tiếp tục đọc.
Tất cả tài liệu chính thức của Xamarin.Forms đang chứng minh một giải pháp MVVM đơn giản nhưng hơi không tinh khiết. Đó là bởi vì Page(Chế độ xem) không nên biết gì về ViewModelvà ngược lại. Đây là một ví dụ tuyệt vời về vi phạm này:
// 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>
Nếu bạn có một ứng dụng 2 trang, cách tiếp cận này có thể tốt cho bạn. Tuy nhiên, nếu bạn đang làm việc trên một giải pháp doanh nghiệp lớn, bạn tốt hơn nên ViewModel First Navigationtiếp cận. Đây là cách tiếp cận phức tạp hơn một chút nhưng rõ ràng hơn nhiều cho phép bạn điều hướng giữa ViewModelsthay vì điều hướng giữa Pages(Chế độ xem). Một trong những lợi thế bên cạnh việc tách biệt rõ ràng các mối quan tâm là bạn có thể dễ dàng chuyển các tham số cho phần tiếp theo ViewModelhoặc thực thi mã khởi tạo không đồng bộ ngay sau khi điều hướng. Bây giờ đến chi tiết.
(Tôi sẽ cố gắng đơn giản hóa tất cả các ví dụ mã càng nhiều càng tốt).
1. Trước hết, chúng ta cần một nơi mà chúng ta có thể đăng ký tất cả các đối tượng của mình và tùy chọn xác định thời gian tồn tại của chúng. Đối với vấn đề này, chúng ta có thể sử dụng một thùng chứa IOC, bạn có thể tự chọn một thùng chứa. Trong ví dụ này, tôi sẽ sử dụng Autofac (nó là một trong những cách nhanh nhất hiện có). Chúng tôi có thể giữ một tham chiếu đến nó Appđể nó sẽ có sẵn trên toàn cầu (không phải là một ý tưởng hay, nhưng cần thiết để đơn giản hóa):
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. Chúng ta sẽ cần một đối tượng chịu trách nhiệm truy xuất Page(View) cho một đối tượng cụ thể ViewModelvà ngược lại. Trường hợp thứ hai có thể hữu ích trong trường hợp đặt trang gốc / chính của ứng dụng. Đối với điều đó, chúng ta nên đồng ý về một quy ước đơn giản rằng tất cả những thứ ViewModelsphải có trong ViewModelsthư mục và Pages(Chế độ xem) phải ở trong Viewsthư mục. Nói cách khác ViewModelsnên sống trong [MyApp].ViewModelskhông gian tên và Pages(Chế độ xem) trong [MyApp].Viewskhông gian tên. Ngoài điều đó, chúng tôi nên đồng ý rằng WelcomeView(Trang) phải có WelcomeViewModelvà v.v. Đây là ví dụ mã của trình liên kết:
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.Đối với trường hợp thiết lập một trang gốc, chúng tôi sẽ cần loại ViewModelLocatorsẽ thiết lập BindingContexttự động:
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.Cuối cùng, chúng tôi sẽ cần một phương pháp NavigationServicehỗ trợ 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);
}
}
Như bạn có thể thấy, có một BaseViewModel- lớp cơ sở trừu tượng cho tất cả những ViewModelsnơi bạn có thể định nghĩa các phương thức như InitializeAsyncvậy sẽ được thực thi ngay sau điều hướng. Và đây là một ví dụ về điều hướng:
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>());
}
}
Như bạn hiểu, phương pháp này phức tạp hơn, khó gỡ lỗi hơn và có thể gây nhầm lẫn. Tuy nhiên, có rất nhiều lợi ích cộng với việc bạn thực sự không cần phải tự thực hiện vì hầu hết các khung công tác MVVM đều hỗ trợ nó. Ví dụ mã được trình bày ở đây có sẵn trên github .
Có rất nhiều bài viết hay về ViewModel First Navigationcách tiếp cận và có một Mẫu Ứng dụng Doanh nghiệp miễn phí sử dụng sách điện tử Xamarin.Forms giải thích chi tiết điều này và nhiều chủ đề thú vị khác.