ดูเหมือนว่าหัวข้อนี้เป็นที่นิยมมากและมันจะเป็นเรื่องน่าเศร้าที่จะไม่พูดถึงที่นี่ว่ามีทางเลือก ViewModel First Navigation
- เฟรมเวิร์ก MVVM ส่วนใหญ่ใช้อยู่อย่างไรก็ตามหากคุณต้องการเข้าใจว่ามันเกี่ยวกับอะไรให้อ่านต่อ
เอกสาร Xamarin.Forms อย่างเป็นทางการทั้งหมดแสดงให้เห็นถึงโซลูชันที่เรียบง่าย แต่ไม่ใช่วิธีการแก้ปัญหาบริสุทธิ์ของ MVVM เล็กน้อย นั่นเป็นเพราะPage
(View) ไม่ควรรู้อะไรเกี่ยวกับViewModel
และในทางกลับกัน นี่คือตัวอย่างที่ดีของการละเมิดนี้:
// 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>
หากคุณมีแอปพลิเคชัน 2 หน้าวิธีนี้อาจดีสำหรับคุณ อย่างไรก็ตามหากคุณกำลังทำงานกับโซลูชันระดับองค์กรขนาดใหญ่คุณควรใช้ViewModel First Navigation
แนวทางนี้ มีความซับซ้อนกว่าเล็กน้อย แต่เป็นวิธีที่สะอาดกว่ามากซึ่งช่วยให้คุณสามารถนำทางไปมาระหว่างViewModels
การนำทางระหว่างPages
(มุมมอง) ข้อดีอย่างหนึ่งนอกเหนือจากการแยกข้อกังวลที่ชัดเจนคือคุณสามารถส่งผ่านพารามิเตอร์ไปยังหัวข้อถัดไปViewModel
หรือเรียกใช้รหัสเริ่มต้น async ได้ทันทีหลังจากการนำทาง ตอนนี้เพื่อดูรายละเอียด
(ฉันจะพยายามลดความซับซ้อนของตัวอย่างโค้ดทั้งหมดให้มากที่สุด)
1. ก่อนอื่นเราต้องมีสถานที่ที่เราสามารถลงทะเบียนวัตถุทั้งหมดของเราและกำหนดอายุการใช้งานได้ สำหรับเรื่องนี้เราสามารถใช้คอนเทนเนอร์ IOC คุณสามารถเลือกได้ด้วยตัวเอง ในตัวอย่างนี้ฉันจะใช้Autofac (เป็นหนึ่งในโปรแกรมที่เร็วที่สุด) เราสามารถเก็บข้อมูลอ้างอิงไว้App
เพื่อให้สามารถใช้งานได้ทั่วโลก (ไม่ใช่ความคิดที่ดี แต่จำเป็นสำหรับการทำให้เข้าใจง่าย):
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. เราจะต้องมีวัตถุที่รับผิดชอบในการดึงข้อมูลPage
(View) สำหรับเฉพาะViewModel
และในทางกลับกัน กรณีที่สองอาจมีประโยชน์ในกรณีของการตั้งค่ารูท / หน้าหลักของแอพ ด้วยเหตุนี้เราควรเห็นด้วยกับหลักการง่ายๆว่าViewModels
ควรอยู่ในViewModels
ไดเรกทอรีและPages
(มุมมอง) ควรอยู่ในViews
ไดเรกทอรี กล่าวอีกนัยหนึ่งViewModels
ควรอยู่ใน[MyApp].ViewModels
เนมสเปซและPages
(มุมมอง) ใน[MyApp].Views
เนมสเปซ นอกจากนั้นเราควรยอมรับว่าWelcomeView
(หน้า) ควรมีWelcomeViewModel
และอื่น ๆ นี่คือตัวอย่างโค้ดของผู้ทำแผนที่:
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. ในกรณีของการตั้งค่าหน้ารูทเราจะต้องมีการViewModelLocator
ตั้งค่าBindingContext
โดยอัตโนมัติ:
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. ในที่สุดเราจะต้องมีแนวทางNavigationService
ที่จะสนับสนุน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);
}
}
อย่างที่คุณเห็นมีBaseViewModel
- คลาสพื้นฐานที่เป็นนามธรรมสำหรับทุกViewModels
ที่ที่คุณสามารถกำหนดวิธีการเช่นInitializeAsync
นั้นจะได้รับการดำเนินการทันทีหลังจากการนำทาง และนี่คือตัวอย่างการนำทาง:
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>());
}
}
ตามที่คุณเข้าใจว่าแนวทางนี้ซับซ้อนกว่าแก้ไขจุดบกพร่องยากกว่าและอาจทำให้สับสนได้ อย่างไรก็ตามมีข้อดีมากมายและคุณไม่จำเป็นต้องใช้งานด้วยตัวเองเนื่องจากเฟรมเวิร์ก MVVM ส่วนใหญ่รองรับการใช้งานนอกกรอบ ตัวอย่างรหัสที่แสดงให้เห็นที่นี่เป็นที่ที่มีอยู่บนGitHub
มีบทความดีๆมากมายเกี่ยวกับViewModel First Navigation
แนวทางและมีรูปแบบแอปพลิเคชันสำหรับองค์กรฟรีโดยใช้ Xamarin.Forms eBook ซึ่งอธิบายเรื่องนี้และหัวข้อที่น่าสนใจอื่น ๆ อีกมากมายโดยละเอียด