导航和依赖注入

4

我正在尝试制定一份标准代码,以便在我的xamarin.forms应用程序中实现。我想做的是有一种方法来在视图模型之间导航,以及正确实现依赖项注入。

我目前为导航所做的工作:

await Navigation.PushAsync(new SecondPageView());

对于DI:

 var test = DependencyService.Get<ITestService>();
 WelcomeMessage = test.GetSystemWelcome();

我知道正确实现Di的方法是创建一个接口并从那一步开始,但问题在于当我尝试时,我无法成功地拥有一个良好的导航系统(例如在文件中注册视图和视图模型)。
是否有示例可以提供给我参考?或者有一些指示可以帮助我继续进行吗?
附:我试图避免像MvvMcross这样的框架。
提前感谢!

我不太明白你想要什么。你是想像Prism一样实现自己的_ViewModelLocator_吗? - Diego Rafael Souza
@DiegoRafaelSouza 是的,我想知道实现 DI 和导航结构的最佳方法,就像 MvvmCross 一样,但不使用它(如果可能的话)。 - flaurens
请查看以下两篇文章 https://mallibone.com/post/xamarin.forms-navigation-with-mvvm-light https://alexdunn.org/2017/06/01/xamarin-tips-mvvm-light-and-dependency-injection/ 。我知道您不想使用框架,但MVVM Light非常简单且小巧,您可以看看它。 - Johannes
@Johannes 谢谢您的回复,我会检查这两个链接。 - flaurens
这是很有可能的,但我认为不值得。这些框架太轻了,已经处理了许多问题,一旦你开始开发和/或使用,就会让你很快头疼。 - Diego Rafael Souza
1个回答

7

首先,我们需要一个地方来注册所有的对象,并可选地定义它们的生命周期。 对于这个问题,我们可以使用一个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.我们需要一个对象负责检索特定ViewModelPage(View),反之亦然。第二种情况可能在设置应用程序的根/主页时有用。为此,我们应该约定一个简单的规则,即所有ViewModels都应该在ViewModels目录中,而Pages(Views)应该在Views目录中。换句话说,ViewModels应该位于[MyApp].ViewModels命名空间中,而Pages(Views)应该位于[MyApp].Views命名空间中。除此之外,我们还应该约定WelcomeView(Page)应该拥有一个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企业应用程序模式电子书,其中详细介绍了这个方法和许多其他有趣的主题。


2
做得好!我向你致敬,这是一个很棒的答案,这就是为什么我喜欢社区的原因 :) - Johannes
1
非常感谢!我已经查看了Xamarin.Forms电子书,但是当我尝试在我的代码中实现时,我迷失了方向。无论如何,非常感谢您的帮助。 - flaurens

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接