Navigation trong 1 ung dung Enterprise
August 02, 2020
Xamarin form có hỗ trợ navigation để điều hướng các page. Thường là tương tác giữa người dùng với UI hoặc để nó thay đổi trạng thái của ứng dụng dựa vào cập nhật logic của ứng dụng. Khi sử dụng MVVM thì nó sẽ trở nên phức tạp, hãy cùng nghiên cứu các vấn đề dưới đây nhé:
- Làm sao để biết được page nào chúng ta sẽ điều hướng đến, trong trường hợp các liên kết ko chặt chẽ và ko có sự phụ thuộc giữa các view với nhau. ???
- Làm sao để biết được 1 view khi điều hướng ( navigate) tới, thì đã được khởi tạo hay chưa? Trong trường hợp sử dụng MVVM thì cả view & view model cần được khởi tạo và liên kết với nhau qua view binding context. Khi 1 app sử dụng dependence injection container thì việc khởi tạo view & view model cần 1 cơ chế khởi tạo riêng.
- Nên thực hiện điều hướng đến view đầu tiên hay là view-model đầu tiên. Khi
view
được điều hướng trước, thì page để điều hướng tới sẽ nhận dưới dạng type của view đó. Trong khi điều hướng, thì cái view được chọn sẽ được khởi tạo cùng với view-model tương ứng với view & các service sử dụng được init theo. Một cách tiếp cận khác: là view-model sẽ được điều hướng đến đầu tiên, và cái page sẽ được điều hướng tới là tên của view-model type. - Làm để nào để tách biệt hành vi điều hướng giữa view & view-model. MVVM thì nó tách biệt giữa layer UI & layer business logic. Tuy nhiên việc điều hướng trong ứng dụng thường được sử dụng ở layer hiển thị. Nhưng Navigation phải thường được khởi tạo và phối hợp với view-model…
- Làm sao để truyền parameter thông qua navigation cho mục đích khởi tạo ? Ví dụ: người dùng điều hướng đến view : order detail, thì lúc này order data cần phải truyền đến cái view để hiển thị lên chính xác dữ liệu.
- Làm sao phối hợp với điều hướng, để các qui tắc ( design của app) vẫn chạy đúng. Ví dụ : hiển thị lên popup để sửa lại những input ko hợp lệ, hoặc nhắc nhở bỏ qua nhũng thay đổi khi thoát khỏi view…
Trong phần này sẽ giới thiệu NavigationService
sử dụng điều hướng đến view-model trước & xử lý các vấn đề trên.
Lưu ý:
NavigationService
này chỉ áp dụng cho ContentPage, sử dụng trên các loại khác sẽ có những bug ko mong muốn.
Navigation giữa các page trong Xamarin App:
Navigation có thể viết ở code-behind của view, nó sẽ đơn giản nhưng sẽ khó để tạo các unit test. Đặt Navigation ở view-model thì logic để điều hướng có thể được test thông qua unit test. Ví dụ : 1 app có thể sẽ ko cho người dùng truy cập vào khi dữ liệu nhập vào ko hợp lệ.
1 NavigationService nên được gọi từ view-model, điều này giúp việc testing dễ dàng. Tuy nhiên, muốn điều hướng đến 1 view thông qua view-model thì bắt buộc phải có liên kết từ view-model đến view, mà trong 1 số view đặc biệt thì những view-model đang active ko có sự liên kết này, thì sẽ xử lý ntn ???. Lúc này NavigationService
sẽ giải quyết được vấn đề này.
NavigationSerive sẽ implement interface dưới:
public interface INavigationService
{
ViewModelBase PreviousPageViewModel { get; }
Task InitializeAsync();
Task NavigateToAsync<TViewModel>() where TViewModel : ViewModelBase;
Task NavigateToAsync<TViewModel>(object parameter) where TViewModel : ViewModelBase;
Task RemoveLastFromBackStackAsync();
Task RemoveBackStackAsync();
}
Interface này chỉ định những class implement nó phải có các method :
Update các method sau ...
Ngoài ra, INavigationService
interface bắt buộc phải implement thuộc tính PreviousPageViewModel
, thuộc tính này sẽ trả về view model type được liên kết với cái page trước trong stack điều hướng.
Note:
An INavigationService interface would usually also specify a GoBackAsync method, which is used to programmatically return to the previous page in the navigation stack. However, this method is missing from the eShopOnContainers mobile app because it’s not required.
Tạo instance NavigationService:
NavigationService
được implement từ interface INavigationService, và được đăng ký như 1 single instance với DI Container ( Autofac) như sau:
builder.RegisterType<NavigationService>().As<INavigationService>().SingleInstance();
Interface INavigationService được resolve trong ViewModelBase constructor như sau: xem thêm phần DI container implement trong Xamarin với unity container
NavigationService = ViewModelLocator.Resolve<INavigationService>();
Đoạn code trên lấy được liên kết đến đối tượng NavigationService được tạo ra và lưu trữ trong Autofac Denpendence Injection Container lúc gọi method InitNavigation
trong App.
Lớp ViewModelBase
lưu instance NavigationService
như 1 thuộc tính của nó với type INavigationService
. Mà mọi lớp view-model đều kế thừa từ ViewModelBase nên có thể dùng thuộc tính NavigationService
này để sử dụng 1 số phương thức của INavigationService
.
Mục đích của việc này để tránh việc inject đối tượng NavigationService
từ DI Container trong mỗi view-model class.
Xử lý các yêu cầu điều hướng:
Trong Xamarin Form thì sẽ cung cấp 1 lớp để xử lý điều hướng NavigationPage, cái này thực hiện 1 số điều hướng giúp người dùng chuyển qua lại giữa các page như họ mong muốn.
Thay vì sử dụng NavigationPage trực tiếp, thì eShopOnContainers wraps NavigationPage trong CustomNavigationView như sau:
public partial class CustomNavigationView : NavigationPage
{
public CustomNavigationView() : base()
{
InitializeComponent();
}
public CustomNavigationView(Page root) : base(root)
{
InitializeComponent();
}
}
Mục đích của điều này là để giảm bớt style NavigationPage trong XAML.
Navigation sẽ được thực hiện bên trong logic của view-model bằng cách gọi hàm như sau:
await NavigationService.NavigateToAsync<MainViewModel>();
Phía code NavigateToAsync
sẽ được impelement như sau:
public Task NavigateToAsync<TViewModel>() where TViewModel : ViewModelBase
{
return InternalNavigateToAsync(typeof(TViewModel), null);
}
public Task NavigateToAsync<TViewModel>(object parameter) where TViewModel : ViewModelBase
{
return InternalNavigateToAsync(typeof(TViewModel), parameter);
}
Mỗi method đều chấp nhận bất kỳ view-model nào mà kế thừa từ ViewModelBase
.
InternalNavigateToAsync
được implement:
private async Task InternalNavigateToAsync(Type viewModelType, object parameter)
{
Page page = CreatePage(viewModelType, parameter);
if (page is LoginView)
{
Application.Current.MainPage = new CustomNavigationView(page);
}
else
{
var navigationPage = Application.Current.MainPage as CustomNavigationView;
if (navigationPage != null)
{
await navigationPage.PushAsync(page);
}
else
{
Application.Current.MainPage = new CustomNavigationView(page);
}
}
await (page.BindingContext as ViewModelBase).InitializeAsync(parameter);
}
private Type GetPageTypeForViewModel(Type viewModelType)
{
var viewName = viewModelType.FullName.Replace("Model", string.Empty);
var viewModelAssemblyName = viewModelType.GetTypeInfo().Assembly.FullName;
var viewAssemblyName = string.Format(
CultureInfo.InvariantCulture, "{0}, {1}", viewName, viewModelAssemblyName);
var viewType = Type.GetType(viewAssemblyName);
return viewType;
}
private Page CreatePage(Type viewModelType, object parameter)
{
Type pageType = GetPageTypeForViewModel(viewModelType);
if (pageType == null)
{
throw new Exception($"Cannot locate page type for {viewModelType}");
}
Page page = Activator.CreateInstance(pageType) as Page;
return page;
}
InternalNavigateToAsync
để điều hướng đến view-model, nhưng đầu tiên thì nó gọi CreatePage
trước . Phương thức này tạo ra view
tương ứng với view-model
dựa trên cách thức như sau:
- Các view thì giống với các view-models type khi biên dịch.
- Views nằm trong .Views child namespace.
- View models thì nằm trong .ViewModels child namespace.
- Tên của View sẽ tương ứng với tên của view-model, sau khi remove text “Model”, giống như hàm
GetPageTypeForViewModel
phía trên.
Khi 1 view được khởi tạo, thì nó liên kết tương ứng vơi view-model. Xem thêm tự động tạo view model với ViewModelLocator MVVM
private static void OnAutoWireViewModelChanged(BindableObject bindable, object oldValue, object newValue)
{
var view = bindable as Element;
if (view == null)
{
return;
}
var viewType = view.GetType();
var viewName = viewType.FullName.Replace(".Views.", ".ViewModels.");
var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
var viewModelName = string.Format(
CultureInfo.InvariantCulture, "{0}Model, {1}", viewName, viewAssemblyName);
var viewModelType = Type.GetType(viewModelName);
if (viewModelType == null)
{
return;
}
var viewModel = _container.Resolve(viewModelType);
view.BindingContext = viewModel;
}
Xử lý của InternalNavigateToAsync
:
- Nếu view được tạo là
LoginView
thì tạo CustomNavigationView mới & gán cho MainPage của App. - Else : lấy MainPage (App) hiện tại ép về CustomNavigationView (navigatePage) nếu là navigatePage khác null thì sẽ đẩy page được tạo vào stack của navigatePage. Ko thì tạo ra MainPage mới là navigatePage.
Điều này đảm bảo trong suốt quá trình điều hướng page, thì các page được thêm đúng vào stack của navigation kể cả các page null hoặc có dữ liệu.
TIP
Consider caching pages. Page caching results in memory consumption for views that are not currently displayed. However, without page caching it does mean that XAML parsing and construction of the page and its view model will occur every time a new page is navigated to, which can have a performance impact for a complex page. For a well-designed page that does not use an excessive number of controls, the performance should be sufficient. However, page caching might help if slow page loading times are encountered.
Sau cùng khi tạo được view-model thì sẽ gọi hàm : InitializeAsync để khởi tạo view-model ( với tham số đầu vào)
Điều hướng khi app được khởi chạy ( App Lauched):
Khi app chạy lần đầu tiên thì sẽ gọi:
private Task InitNavigation()
{
var navigationService = ViewModelLocator.Resolve<INavigationService>();
return navigationService.InitializeAsync();
}
Phương thức này sẽ tạo ra 1 đối tượng NavigationService nằm trong DI Container trước, và trả về liên kết đến đối tượng đó và sau đó mới gọi InitializeAsync().
Phương thức InitializeAsync như sau
public Task InitializeAsync()
{
if (string.IsNullOrEmpty(Settings.AuthAccessToken))
return NavigateToAsync<LoginViewModel>();
else
return NavigateToAsync<MainViewModel>();
}
Truyền parameter thông qua Navigation:
Ví dụ như sau:
ProfileViewModel
sẽ chứa 1 OrderDetailCommand
nó sẽ thực hiện khi user select 1 order trong ProfilePage
. Và trong view-model nó sẽ gọi hàm : OrderDetailAsync
. Hàm này sẽ điều hướng đến OrderDetailViewModel
với tham số dữ liệu Order
được truyền vào như sau:
private async Task OrderDetailAsync(Order order)
{
await NavigationService.NavigateToAsync<OrderDetailViewModel>(order);
}
Khi NavigationService tạo OrderDetailView
thì view-model tương ứng cũng được tạo ra như phần xử lý điều hướng phía trên & nó sẽ BindingContext để liên kết view & view-model … và cuối cùng sẽ gọi hàm InitializeAsync
có truyền tham số …
Và view-model được kế thừa từ ViewModelBase nên có thể override lại InitializeAsync
để lấy được tham số truyền vào như sau:
public override async Task InitializeAsync(object navigationData)
{
if (navigationData is Order)
{
...
Order = await _ordersService.GetOrderAsync(
Convert.ToInt32(order.OrderNumber), authToken);
...
}
}
Gọi Navigation sử dụng behavior ( Command)
Navigation thường được gọi từ view, xử lý tương tác với người dùng, nên thường sẽ xử lý như sau ở xaml file:
<WebView ...>
<WebView.Behaviors>
<behaviors:EventToCommandBehavior
EventName="Navigating"
EventArgsConverter="{StaticResource WebNavigatingEventArgsConverter}"
Command="{Binding NavigateCommand}" />
</WebView.Behaviors>
</WebView>
Khi runtime, EventToCommandBehavior
sẽ tương tác với WebView. Khi WebView
điều hướng đến webpage sự kiện Navigating
sẽ được gọi. và sẽ thực hiện NavigateCommand
ở trong LoginViewModel
.
Mặc định thì tham số của sự kiện sẽ được truyền vào Command. Data này sẽ được chuyển đổi thông qua converter để lấy được URL thông qua WebNavigatingEventArgs. Cuối cùng, khi NavigationCommand
được thực hiện thì Url of the web page sẽ được truyền như 1 tham số để đăng ký Action.
private async Task NavigateAsync(string url)
{
...
await NavigationService.NavigateToAsync<MainViewModel>();
await NavigationService.RemoveLastFromBackStackAsync();
...
}
Điều hướng vào MainPAge & sẽ remove LoginPage khỏi stack.
Xác nhận hoặc hủy bỏ điều hướng.
1 App có thể cần người dùng nhập liệu trước khi điều hướng, ví dụ nhập user / pass correct thì mới được điều hướng vào trang tiếp theo. Điều này có thể thực hiện ở lớp model-view để xử lý ..
Tổng kết
Trong Xamarin form hỗ trợ sẵn Navigation & có thể hoạt động tốt, nhưng khi áp dụng với MVVM thì nó sẽ trở nên phức tạp hơn.
Phần này giới thiệu cách sử dụng NavigationService
để điều hướng đến view-model trước. Đặt logic điều hướng trong view-model để logic này có thể được kiểm tra tự động bởi unit test để đảm bảo việc hoạt động của ứng dụng là chính xác.
Written by Quilv Follow me on Facebook