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ề ViewModel
và 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 Navigation
tiế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 ViewModels
thay 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 ViewModel
hoặ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ể ViewModel
và 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ứ ViewModels
phải có trong ViewModels
thư mục và Pages
(Chế độ xem) phải ở trong Views
thư mục. Nói cách khác ViewModels
nên sống trong [MyApp].ViewModels
không gian tên và Pages
(Chế độ xem) trong [MyApp].Views
không gian tên. Ngoài điều đó, chúng tôi nên đồng ý rằng WelcomeView
(Trang) phải có WelcomeViewModel
và 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 ViewModelLocator
sẽ thiết lập BindingContext
tự độ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 NavigationService
hỗ 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 ViewModels
nơi bạn có thể định nghĩa các phương thức như InitializeAsync
vậ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 Navigation
cá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.