新文章 网摘 文章 随笔 日记

Xamarin与TinyIoC

在Xamarin中以TinyIoC为例使用IoC容器

介绍

本文基于ViewFactory中针对Xamarin应用程序Xamarin Master-Detail Page文章介绍的主题和解决方案它显示了一种通过Xamarin应用程序中的IoC轻松导航和使用服务的方法。
前面的示例中的知识不是必需的,但会使本文更容易理解。

对于IoC容器,它将使用TinyIoC,但不使用作者GitHub存储库(或Nuget)中的原始版本。我将使用我的个人fork,因为此版本可以在PCL项目中使用,这允许TinyIoC放置在共享项目中,这使它变得更加容易。否则,使用与PCL版本的.NET不兼容的原始版本,我们将被迫在每个平台项目中放置相同的TinyIoC代码。确实会使不必要的事情复杂化。使用启用PCL的版本,这不再是问题。

想法是使用IoC容器执行以下操作:

  1. 在视图之间创建无缝导航
  2. 在视图模型中解析应用程序服务

轻松导航

重要的是要注意,新的导航机制应适用于Xamarin Page类的后代的普通Xamarin视图,也应适用于该类的派生页面DetailPage细节视图是必须放置在母版页中的视图,如在“ 主-细节”文章中所述。借助这样的导航机制,我们可以使用其视图模型轻松地从视图模型(无论如何应保持这种逻辑)导航到另一个页面,而无需考虑视图实现的细节。

通过从IoC容器中解析模型类型来进行导航以查看模型类型,从而允许通过构造函数以非常简单,美观的方式注入此模型依赖项。您需要向用户显示GPS位置吗?您只需要添加新的构造函数参数IGSPService并使用获得的引用即可。

怎么做?首先,我们需要使用共享项目-创建Xamarin应用程序IoCSample在本文中,我将使用Android平台。其余平台项目不是必需的。创建项目之后,我们需要添加到Android项目很少引用:ViewFactoryViewFactory.AndroidCustomMasterDetailControl(均为提供XamarinSamples,在本文中的代码将被添加为好)。这些项目对于实现视图之间的更好导航是必需的。CustomMasterDetailControl程序集contains MasterDetailControl,它将允许我们创建详细视图并将其放入定义的主控件中。细节视图以及普通的Xamarin视图将允许显示导航如何独立于视图定义。ViewFactory导航将在内部使用项目创建视图。之后,我们可以添加带有IoC容器定义的TinyIoC文件。此类不需要任何初始化。使用staticTinyIoCContainer.Current是足够我们的需要。

现在我们可以更改默认应用程序。这是App.xaml的内容App.xaml.csMainActivity.cs文件(如果我们在没有IoC的情况下执行操作)。

<?xml version="1.0" encoding="utf-8" ?>
<Application xmlns="http://xamarin.com/schemas/2014/forms"

             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"

             x:Class="IoCSample.App">
</Application>
public partial class App
{
    public App ()
    {
        InitializeComponent();
        MainPage = new MainPageView { BindingContext = new MainPageViewModel() };
    }
}
[Activity(Label = "IoCSample", Icon = "@drawable/icon", 
MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsApplicationActivity
{
    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);

        global::Xamarin.Forms.Forms.Init(this, bundle);
        LoadApplication(new IoCSample.App());
    }
}

我们正在创建两个实例:视图和视图模型,以创建应用程序的主页。目标是用于ViewFactory根据视图模型类型创建页面。为此,我们需要先ViewFactory在IoC中注册服务,但是由于ViewFactory是在Android平台项目中定义的(因为很少有反射功能无法从PCL轻松访问,而在平台项目中从完整的.NET要容易得多;但是这是可能的)在PCL中,但超出了本文的范围),我们还需要在平台类-中注册此服务MainActivity

protected override void OnCreate(Bundle bundle)
{
    base.OnCreate(bundle);
    RegisterServices(TinyIoCContainer.Current);
    global::Xamarin.Forms.Forms.Init(this, bundle);
    LoadApplication(new IoCSample.App());
}

private void RegisterServices(TinyIoCContainer container)
{
    container.Register<IViewFactory, VFactory>();
}

IViewFactoryViewFactory类的新接口,它将与IoC一起更方便地使用它。

public interface IViewFactory
{
    UIPage CreateView<TViewModel>() where TViewModel : BaseViewModel;

    UIPage CreateView<TViewModel, TView>() where TViewModel : BaseViewModel
        where TView : UIPage;

    UIPage CreateView<TViewModel, TView>(TViewModel viewModel) where TViewModel : BaseViewModel
        where TView : UIPage;

    UIPage CreateView(Type viewModelType);
    void Init(Assembly appAssembly);
}

然后,我们可以使用来初始化MainPage应用程序ViewFactory

public IViewFactory viewFactory { get; set; }

public App()
{
    var container = TinyIoCContainer.Current;
    container.BuildUp(this);
    viewFactory.Init(Assembly.GetExecutingAssembly());
    InitializeComponent();
    MainPage = viewFactory.CreateView<MainPageViewModel>();
}

启动应用程序后,我们将看到MainPage,证明ViewFactoryIoC组合可以正常工作。

好的,但这仍然只是MainPage为应用程序属性创建新值如何实现更轻松的导航?如果您不打算单页应用程序,则需要Xamarin INavigation实例。获取NavigationPage实例的最简单方法是创建实例作为其值MainPage

MainPage = new NavigationPage(viewFactory.CreateView<MainPageViewModel>());

现在,让我们创建另一个页面,以测试新的导航是否有效。

<?xml version="1.0" encoding="UTF-8"?>
<customMasterDetailControl:UIPage xmlns="http://xamarin.com/schemas/2014/forms" 

                                  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"

                                  xmlns:customMasterDetailControl=
                                  "clr-namespace:CustomMasterDetailControl;
                                  assembly=CustomMasterDetailControl"

                                  x:Class="IoCSample.PageView">
  <Label Text="{Binding Label}" HorizontalOptions="Center" VerticalOptions="Center"/>
</customMasterDetailControl:UIPage>

下一步是在视图模型之间创建导航。我们可以为此创建staticNavigationHelper

public class NavigationHelper
{
    private static readonly INavigation _navigation;
    private static readonly IViewFactory _viewFactory;
    private static TinyIoCContainer _container;

    static NavigationHelper()
    {
        var container = TinyIoCContainer.Current;
        _container = container;
        _viewFactory = container.Resolve<IViewFactory>();
        _navigation = container.Resolve<INavigation>();
    }

    public static void NavigateTo<TViewModel>() where TViewModel : BaseViewModel
    {
        _navigation.PushAsync(_viewFactory.CreateView<TViewModel>());
    }

    public static void NavigateTo<TViewModel>(Action<TViewModel> init) where TViewModel : BaseViewModel
    {
        var viewModel = _container.Resolve<TViewModel>();
        init(viewModel);
        _navigation.PushAsync(_viewFactory.CreateView(viewModel));
    }
}

CreateViewViewFactory内部使用新方法

public UIPage CreateView<TViewModel>(TViewModel viewModel)
{
    var viewModelType = viewModel.GetType();
    if (Views.ContainsKey(viewModelType))
    {
        var viewData = Views[viewModelType];
        return CreateView(viewModel, viewData.Creator);
    }
    return null;
}

上面的方法允许我们创建具有给定视图模型的页面。但是,如果在导航到其页面之前需要以某种方式初始化视图模型,那将带来很多问题。例如,产品详细说明了产品的新实例或标识符。我们可以在MainPage其中创建新按钮,这将导致PageView使用初始化操作导航到新按钮以显示其工作原理。

新视图模型如下所示:

public class PageViewModel : BaseViewModel
{
    public void SetLabelText(string value)
    {
        Label = value;
    }

    public string Label { get; set; }
}

Initialization绑定到视图标签的方法和属性。导航到此页面是通过NavigationHelper名为MainPageView按钮的实现的

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
    </Grid.RowDefinitions>
    <Label Grid.Row="0" Text="{Binding MainText}" 

    VerticalOptions="Center" HorizontalOptions="Center" />
    <Button Grid.Row="1" Text="To Page" Command="{Binding ToPage}" />
</Grid>
public ICommand ToPage
{
    get
    {
        return _toPage ?? (_toPage = new Command(() =>
                {
                    NavigationHelper.NavigateTo<PageViewModel>(
                        vm => vm.SetLabelText("Value from MainPageViewModel"));
                }));
    }
}

生成并运行后,该应用程序可以正确地从导航MainPageViewPageView

图片1

相同的命令,但没有初始化视图模型,它的复杂程度较低,如下所示:

public ICommand ToPage
{
    get
    {
        return _toPage ?? (_toPage = new Command(NavigationHelper.NavigateTo<PageViewModel>));
    }
}

也有可能创建方法,即IViewModelInitInit()方法接口,但这会带来另一个问题。如何在此方法中定义参数?如果这将是单个对象类型的值,它将强制转换为正确的类型以在视图模型中设置必要的数据。如果我们不想强制转换,则需要BaseViewModel<>使用通用方法创建通用类进行初始化。但是,它仍然迫使我们将类型放在两个位置(类型定义和初始化方法),这使通常经常更改的代码变得复杂,并且仍然存在参数数量的问题(尽管我从未见过这样的方法具有两个以上的参数)。这就是为什么我决定离开它的原因。

尽管如此,我们可以通过创建用于在基本视图模型类内部进行导航的方法来进一步简化此过程。理想情况下,将其放置在内部,BaseViewModel但它是引用的装配,而无需了解NavigationHelper类。因此,我们需要NavigationViewModel使用这些方法创建新类型,然后更改应用程序视图模型的基本类型。

public class NavigationBaseViewModel : BaseViewModel
{
    public static void NavigateTo<TViewModel>() where TViewModel : BaseViewModel
    {
        NavigationHelper.NavigateTo<TViewModel>();
    }

    public static void NavigateTo<TViewModel>
    (Action<TViewModel> init) where TViewModel : BaseViewModel
    {
        NavigationHelper.NavigateTo(init);
    }
}

MainPageViewModel基类更改为上面的类型后,我们可以ToPage再次重写命令。

public ICommand ToPage
{
    get
    {
        return _toPage ?? (_toPage = new Command(() =>
        {
            NavigateTo<PageViewModel>
            (vm => vm.SetLabelText("Value from MainPageViewModel"));
        }));
    }
}

在一行中,我们现在可以订购应用程序以:

  1. 创建视图模型实例
  2. 使用来自先前视图的适当数据初始化视图模型实例
  3. 建立新检视
  4. 在新视图中设置视图模型
  5. 向用户显示新视图

与默认情况下Xamarin中所需的解决方案相比,这是一个非常优雅的解决方案。

public ICommand ToPageXamarin
{
    get
    {
        return _toPage ?? (_toPage = new Command(() =>
        {
            var newView = new PageView();
            var newViewModel = new PageViewModel();
            newViewModel.SetLabelText("Value from MainPageViewModel");
            newView.BindingContext = newViewModel;
            var navigation = App.Current.MainPage.Navigation;
            navigation.PushAsync(newView);
        }));
    }
}

如果上面的代码PageViewModel具有某些依赖关系(如依赖关系等),则上述代码将变得更加复杂

如何导航到详细信息页面?

下一步也将以相同的方式实现导航至详细页面-通过单次调用NavigateTo<>方法。这就要求我们NavigationHelper换班。static的问题是我们不能以任何方式强制指出应该使用哪个母版页。当然,您可以为所有应用程序使用一个文件,但是我对此表示高度怀疑。大多数情况下,它们会有所不同。因此,即使MasterPageControl是上一篇文章中的类也可能是不够的,因为它即无法将菜单放在应用程序的左侧(但可以通过修改MasterPageControl.xaml文件来轻松实现)。当然,可以创建一个static属性,比如说MasterPageViewModelType,这将指出,哪个主视图应用于详细信息页面,但这更容易出错。我更喜欢使代码尽可能清晰易用,然后在运行时抛出错误。大多数时候,我是自己代码的最终用户,我不想考虑如何实现某种“完成”的机制微笑它应该尽可能容易且显而易见。因此,一个更好的主意是将导航服务作为abstract具有abstract属性和母版页面视图模型类型的基类进行

因此,我们需要的基本实现NavigationService一个好主意是创建一个通用类。

public class NavigationService<TMasterViewModel> : INavigationService<TMasterViewModel>
        where TMasterViewModel : MasterDetailControlViewModel
{
}

大部分代码是从复制的NavigationHelper,但是要实现对细节视图的支持,我们需要添加一些新内容。

public TMasterViewModel MasterViewModel
{
    get
    {
        var page = _navigation.NavigationStack.LastOrDefault();
        return page?.BindingContext as TMasterViewModel;
    }
}

上面的属性以类定义中定义的类型返回主视图模型的实例。实例null不仅是堆栈顶部已经有母版页。为什么?因为Xamarin INavigation不允许同一页面在视图堆栈中出现两次(这很有意义),所以这就是为什么每次在普通的全屏页面之后添加详细信息页面时都需要创建新的母版页。认为是:

Master(Detail1) -> Full screen page -> Master(Detail2)

如果您想像上面一样在堆栈上推送新的详细信息页面,则可以重用最后一个母版页。但是,如果您想在顶部放上新的详细信息页面,则在“全屏页面”显示在顶部之后,您将无法重复使用前一个页面,因为它已经处于其他位置。因此,必须创建新实例。

有了这些知识,我们就可以实现新NavigateTo<>方法。

public void NavigateTo<TViewModel>() where TViewModel : BaseViewModel
{
    PushPage<TViewModel>(_viewFactory.CreateView<TViewModel>());
}

public void NavigateTo<TViewModel>(Action<TViewModel> init) where TViewModel : BaseViewModel
{
    var viewModel = _container.Resolve<TViewModel>();
    init(viewModel);
    PushPage<TViewModel>(_viewFactory.CreateView(viewModel));
}

public void PushPage<TViewModel>(Page page) where TViewModel : BaseViewModel
{
    if (!_viewFactory.IsDetailView<TViewModel>())
    {
        _navigation.PushAsync(page);
    }
    else
    {
        var masterViewModel = MasterViewModel;
        UIPage masterPage = null;
        if (masterViewModel == null)
        {
            masterPage = _viewFactory.CreateView<TMasterViewModel>();
            masterViewModel = (TMasterViewModel)masterPage.BindingContext;
        }
        masterViewModel.Detail = page;
        if (MasterViewModel == null)
        {
            _navigation.PushAsync(masterPage);
        }
    }
}

PushPage方法照顾了以前INavigation.PushAsync对普通(全屏)页面的调用

它使用新IsDetailViewIViewFactory界面方法来确定页面是否为详细视图。

public bool IsDetailView(Type viewModelType)
{
    return Views[viewModelType].IsDetail;
}

public bool IsDetailView<TViewModel>() where TViewModel : BaseViewModel
{
    return IsDetailView(typeof(TViewModel));
}

ViewFactory视图是否为局部视图,已经包含信息。我们只需要一种新方法即可从服务中提取此信息。

如果上述方法true针对某个视图模型返回,则意味着必须以不同的方式处理它,并且不能直接将其推入INavigation堆栈。这种不同的逻辑由方法中else子句处理INavigationService.PushPage如果顶部没有母版页,则会创建母版页(如上所述),并使用适当的视图ViewFactory之后,在母版中设置新的详细信息页面,最后将母版页推送到Xamarin的导航堆栈中(如果尚不存在)。这就说明了,这里整个类看起来像:

public class NavigationService<TMasterViewModel> : INavigationService<TMasterViewModel>
    where TMasterViewModel : MasterDetailControlViewModel
{
    private readonly TinyIoCContainer _container;

    private readonly INavigation _navigation;

    private readonly ViewSwitcher.IViewFactory _viewFactory;

    public NavigationService()
    {
        var container = TinyIoCContainer.Current;
        _container = container;
        _viewFactory = container.Resolve<ViewSwitcher.IViewFactory>();
        _navigation = container.Resolve<INavigation>();
    }

    public TMasterViewModel MasterViewModel
    {
        get
        {
            var firstOrDefault = _navigation.NavigationStack.FirstOrDefault();
            return firstOrDefault?.BindingContext as TMasterViewModel;
        }
    }

    public void NavigateTo<TViewModel>() where TViewModel : BaseViewModel
    {
        PushPage<TViewModel>(_viewFactory.CreateView<TViewModel>());
    }

    public void NavigateTo<TViewModel>(Action<TViewModel> init) where TViewModel : BaseViewModel
    {
        var viewModel = _container.Resolve<TViewModel>();
        init(viewModel);
        PushPage<TViewModel>(_viewFactory.CreateView(viewModel));
    }

    public void PushPage<TViewModel>(Page page) where TViewModel : BaseViewModel
    {
        if (!_viewFactory.IsDetailView<TViewModel>())
        {
            _navigation.PushAsync(page);
        }
        else
        {
            var masterViewModel = MasterViewModel;
            UIPage masterPage = null;
            if (masterViewModel == null)
            {
                masterPage = _viewFactory.CreateView<TMasterViewModel>();
                masterViewModel = (TMasterViewModel)masterPage.BindingContext;
            }
            masterViewModel.Detail = page;
            if (MasterViewModel == null)
            {
                _navigation.PushAsync(masterPage);
            }
        }
    }
}

现在我们终于可以使用它了。例如,PageView以类似in的方式添加的new按钮MainPageView将导航到new DetailView

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition />
    </Grid.RowDefinitions>
    <Label Grid.Row="0" 

    Text="{Binding Label}" HorizontalOptions="Center"

            VerticalOptions="Center"/>
    <Button Grid.Row="1" Text="To detail" 

    Command="{Binding ToDetailPage}" />
</Grid>

使用如下视图模型:

public class PageViewModel : NavigationBaseViewModel
{
    private Command _toDetailPage;

    public void SetLabelText(string value)
    {
        Label = value;
    }

    public string Label { get; set; }

    public ICommand ToDetailPage
    {
        get
        {
            return _toDetailPage ?? (_toDetailPage = new Command(OnToDetailPage));
        }
    }

    private void OnToDetailPage()
    {
        NavigateTo<DetailViewModel>();
    }
}

但是我们需要先定义母版页- MasterDetailControl

<masterDetail:MasterDetailControl 

    xmlns="http://xamarin.com/schemas/2014/forms"

    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"

    xmlns:masterDetail="clr-namespace:CustomMasterDetailControl;assembly=CustomMasterDetailControl"

    x:Class="IoCSample.Views.MasterDetailView">
    <masterDetail:MasterDetailControl.SideContent>
        <StackLayout>
            <Button Text="To detail page" Command="{Binding ToDetail}" />
        </StackLayout>
    </masterDetail:MasterDetailControl.SideContent>
</masterDetail:MasterDetailControl>

上面视图的视图模型如下所示:

public class MasterDetailViewModel : MasterDetailControlViewModel
{
    private ICommand _toDetai;
        
    public ICommand ToDetail
    {
        get { return _toDetai ?? (_toDetai = new Command(OnToDetail)); }
    }
        
    private void OnToDetail()
    {
        NavigationHelper.NavigateTo<Detail1ViewModel>();
    }
}

现在,我们只需要在App.xaml.cs文件中的容器中注册新服务,就可以尝试一下。

public void RegisterServices(TinyIoCContainer container)
{
    container.Register<INavigationService, NavigationService<MasterDetailViewModel>>();
}

运行此代码后,我们可以看到它运行良好,并且应提供类似于下图的结果:

图片3

现在,当我们实现了导航后,我们可以跳到第二个主题。

ViewModels依赖注入

视图应该没有构造函数依赖性。模型通常只是数据或数据容器,即数据逻辑(为客户添加订单等)。UI唯一需要的是视图模型中的应用程序逻辑。大多数时候,他们会获取新数据或向用户显示现有数据。但是有时候,需要一些特定的逻辑,例如,获取GPS位置,与蓝牙设备连接,生成PDF,显示模式对话框等。所有这些功能都比较通用,将它们作为服务来实现是一个好主意。可以在不同的视图和应用程序之间共享。可以通过IoC容器和依赖注入(通过构造函数)在视图模型中实现共享。

为了实现这一目标,我们需要在项目中做什么?两件事情:

  1. ViewFactory应该从IoC容器而不是Activator创建视图模型
  2. 在应用程序初始化期间注册服务。

通过这两个,我们可以通过服务接口向构造函数添加参数。

让我们先做第一点。这很容易,唯一的问题是我们必须virtualBaseViewFactory类中创建一个方法,并以与TinyIoC相同的程序集中定义的新类型覆盖该方法

private UIPage CreateView(Type viewModelType, Func<UIPage> creator)
{
    var viewModel = CreateViewModelInstance(viewModelType);
    return CreateView(viewModel, creator);
}

protected virtual object CreateViewModelInstance(Type viewModelType)
{
    var viewModel = Activator.CreateInstance(viewModelType);
    return viewModel;
}

以上方法已更改BaseViewFactory逻辑是相同的-创建类型via的实例Activator现在,在IoCSample项目中输入新类型

public class IoCViewFactory : ViewFactory.ViewFactory.ViewFactory
{
    private readonly TinyIoCContainer _container;

    public IoCViewFactory(TinyIoCContainer container)
    {
        _container = container;
    }

    protected override object CreateViewModelInstance(Type viewModelType)
    {
        return _container.Resolve(viewModelType);
    }
}

如您所见,我们只需要重写virtual方法即可使用IoC容器而不是Activator类。这就是我们要做的!微笑

现在,我们需要创建并注册一些服务以在视图模型中使用它。假设我们有某种机制可以将缓存数据存储在数据库或文件中。我们可以从任何视图模型中更改和/或读取它。为了简单起见,在我们的示例中,接口就足够了。

public class DataCacheService : IDataCacheService
{
    public Dictionary<string, object> DataCache { get; } = new Dictionary<string, object>();
}
public interface IDataCacheService
{
    Dictionary<string, object> DataCache { get; }
}

真正简单的机制,可以在运行时保存一些信息。我们必须在容器中注册此服务。我们可以在App.xaml.cs文件中执行此操作。

public void RegisterServices(TinyIoCContainer container)
{
    container.Register<INavigationService, NavigationService<MasterDetailViewModel>>();
    container.Register<IDataCacheService, DataCacheService>().AsSingleton();
}

现在,我们可以通过访问服务来共享缓存的数据。例如,我们可以将数据保存在一个视图模型中DetailViewModel,然后在另一个视图模型中读取Detail1ViewModel保存是通过Entry绑定到视图模型属性的简单控件完成的每次用户更改的值时Entry,都会保存数据。然后,导航到另一个视图后,它将在其标签中显示已保存的数据。下面是提到的视图模型的代码。

public class DetailViewModel : BaseViewModel
{
    private readonly IDataCacheService _dataCacheService;
    private string _text;

    public DetailViewModel(IDataCacheService dataCacheService)
    {
        _dataCacheService = dataCacheService;
        if (_dataCacheService.DataCache.ContainsKey(CacheKey))
        {
            _text = (string)_dataCacheService.DataCache[CacheKey];
        }
    }

    public const string CacheKey = "CacheKey";

    public string Text
    {
        get { return _text; }
        set
        {
            _text = value;
            _dataCacheService.DataCache[CacheKey] = value;
        }
    }
}
public class Detail1ViewModel : BaseViewModel
{
    public Detail1ViewModel(IDataCacheService dataCacheService)
    {
        if (dataCacheService.DataCache.ContainsKey(DetailViewModel.CacheKey))
        {
            Text = (string)dataCacheService.DataCache[DetailViewModel.CacheKey];
        }
    }

    public string Text { get; private set; }
}

真的很简单的例子。适当的视图模型视图甚至更简单,并且没有列出它们的代码的要点。

运行新的应用程序后,我们可以测试它是否真的有效。

图片5

如你看到的。在一个视图中输入的值可在第二个视图中访问。视图模型中的依赖项注入可以很好地与按视图模型类型自动导航到视图一起工作,而无需考虑视图定义的细节。微笑

您可以从此处(77.98 kB)或从github(以及该系列的其他Xamarin文章下载经过清理和重构的代码(在一个项目中的所有内容)。

执照

posted @ 2020-08-24 08:39  岭南春  阅读(159)  评论(0)    收藏  举报