博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
MvvmCross[翻译] 使用Xamarin与MvvmCross完成一个跨平台App
阅读量:4688 次
发布时间:2019-06-09

本文共 17687 字,大约阅读时间需要 58 分钟。

总览

原文:

我们所做的第一个Model-View-ViewModel(MVVM)程序需要为一个餐馆实现一个跨平台的小费计算器。

他的线框图大概是下面这个样子:

App线框图

本片文章大概会涉及到以下几个方面:

  • MvvmCorss应用程序的总体结构。
  • 构建一个MvvmCross程序所需要的基本代码。
  • 如何在Xamarin.iOS、Xamarin.Android、Windows 通用应用程序中使用MvvmCross以及数据绑定

本文只关注与MvvmCross,Xamarin相关的问题不会详细的说明。


一、核心库(PCL部分)

一个MvvmCross的App通常由以下几个部分组成:

  • 一个共享的核心(Core)跨平台类库(Protable Class Library,PCL)。包含view model、service、converters等。
  • 各 个平台的的UI工程。

一般来说我们从这个跨平台的Core库开始写。

本例使用Visual Studio 2013,Xamarin 3.11.386。iOS部分使用虚拟机、操作系统为10.10.3 xcode6。

1.创建一个新的Portable Class Library

首先在VS中新建一个PCL项目。名称为TipCalc.Core,解决方案名称为TipCalc(小费计算器):

新建项目

在新建PCL项目时,VS会询问PCL的目标平台,请按照下面图片设置:

PCL目标平台

确保PCL的Profile为Profile259,实在不想选可以通过编辑PCL工程的csproj文件将

Profile***

中的值改为Profile259

如果你的VS报错请参考

以及yzf的博客:

关于Profile259Profile259包括了大多数.net程序集,也可以通过Nuget获取第三方的库,通常跨平台的类库都是使用Profile259生成的。

2.安装MvvmCross

在 [工具] - [Nuget 程序包管理器] - [程序包管理器控制台] (Package Manager Console)中输入

Install-Package MvvmCross.HotTuna.MvvmCrossLibraries

这时Nuget会自动帮我们把MvvmCross引入到我们的工程中。

然后删掉自动生成的Class1.cs。

当然你也可以在项目右键,使用Nuget程序包管理器引入MvvmCross。不过根据我的经验,Nuget管理器通常会比较卡。推荐用控制台。

3.增加计算小费的Service

在项目中增加文件夹,名为Services。在小型App中我们可以把App的业务逻辑放在这里。

在文件夹中增加一个接口。这个接口用来抽象计算小费的逻辑。

namespace TipCalc.Core.Services{    public interface ICalculation    {        double TipAmount(double subTotal, int generosity);    }}

然后我们来实现它

namespace TipCalc.Core.Services{    public class Calculation : ICalculation    {        public double TipAmount(double subTotal, int generosity)        {            return subTotal * ((double)generosity)/100.0;        }    }}

到此为止我们App的业务逻辑已经实现了。

4.增加ViewModel

在添加ViewModel前我们先梳理下我们这个页面需要完成的任务

  • 用途:
    • 使用Calculation服务计算小费。
  • 需要用户输入:
    • 账单的总价(SubTotal)。
    • 我们想要留下小费的百分比 (Generosity)。
  • 输出:
    • 我们需要留下多少小费。

在MvvmCross中,所有的ViewModel都需要继承自MvxViewModel

在项目中建立ViewModels文件夹,专门用来放ViewModel。在其中建立新的ViewModel.

using Cirrious.MvvmCross.ViewModels;using TipCalc.Core.Services;namespace TipCalc.Core.ViewModels{    public class TipViewModel : MvxViewModel    {        private readonly ICalculation _calculation;        private int _generosity;        private double _subTotal;        private double _tip;        ///         /// 构造函数注入ICalculation        ///         ///         public TipViewModel(ICalculation calculation)        {            _calculation = calculation;        }        ///         /// 总消费        ///         public double SubTotal        {            get { return _subTotal; }            set            {                _subTotal = value;                RaisePropertyChanged(() => SubTotal);                Recalcuate();            }        }        ///         /// 消费比例(百分比)        ///         public int Generosity        {            get { return _generosity; }            set            {                _generosity = value;                RaisePropertyChanged(() => Generosity);                Recalcuate();            }        }        ///         /// 小费        ///         public double Tip        {            get { return _tip; }            set            {                _tip = value;                RaisePropertyChanged(() => Tip);            }        }        ///         /// ViewModel初始化时执行        ///         public override void Start()        {            _subTotal = 100;            _generosity = 10;            Recalcuate();            base.Start();        }        ///         /// 调用ICalculation给我们的接口计算小费        ///         private void Recalcuate()        {            Tip = _calculation.TipAmount(SubTotal, Generosity);        }    }}

如果你之前在WPF、Sliverlight等平台中接触过MVVM设计模式应该对以上的代码并不陌生。

这个ViewModel中有3个被扩展过的属性SubTotalGenerosityTip
当他们被修改的时候会调用RaisePropertyChanged函数来通知其他的对象他们的属性被修改过了。
且当SubTotalGenerosity被修改时会重新计算小费。

5.增加启动配置代码(App)

在写好Calculation Service和TipViewModel之后,我们现在来添加App的启动配置代码,对于App类来说:

  • 他通常会待在在PCL项目的根目录下。
  • 他继承自MvxApplication
  • 一般来说他的名字就叫App
  • 他的主要功能是:
    • IoC容器注册接口以及相应的实现。以后我会专门写一篇关于MvvmCross的IoC容器的文章来介绍。
    • 设置App启动后第一个界面对应的的ViewModel。
    • 为整个App提供ViewModel的定位器(Locator)。定位器作用是通过ViewModel的Type以及以下参数来生成对应的ViewModel。通常情况下我们用默认的就行了。

对于我们的小费计算器来说,App的代码很简单:

using Cirrious.CrossCore;using Cirrious.MvvmCross.ViewModels;using TipCalc.Core.Services;using TipCalc.Core.ViewModels;namespace TipCalc.Core{    public class App : MvxApplication    {        public App()        {            //向IoC注册计算小费的服务            Mvx.RegisterType
(); //设置App的启动界面 Mvx.RegisterSingleton
(new MvxAppStart
()); } }}

6.完成!

到此我们完成了Core工程的全部代码。来看看我们具体都做了什么:

  • Profile 259新建了一个PCL项目。
  • 用Nuget向项目中添加了MvvmCross的程序集。
  • 添加了ICalculate接口和他的实现。
  • 添加了TipViewModel
    • 继承自MvxViewModel
    • 向框架请求了ICalculate服务。
    • 添加了一些会调用RaisePropertyChanged函数的公共属性。
  • 添加了App启动配置。
    • 继承自MvxApplication
    • ICalculate接口注册了他的具体实现。
    • 注册了应用程序的启动界面。

差不多每个使用MvvmCross的App都会有以上步骤。

接下来我们来看看每个平台该做些什么。

二、Android部分

Android部分y-z-f已经有写过相关的文章了:

1.创建Android项目

在解决方案中创建一个空的Android项目([Android] -- [Blank App]),命名为TipCalc.UI.Droid

2.安装MvvmCross并引用Core库

与Core一样使用Package Manager Console安装MvvmCross。记得切换Package Manager Console的对应项目。

Install-Package MvvmCross.HotTuna.MvvmCrossLibraries

因为需要在Android的axml文件中使用MvvmCross的相关命令,需要添加一个名为MvxBind.Xml文件到Resources/Values文件夹中。内容如下

3.添加Setup代码

我们需要一个Setup类来控制Android App的启动行为,在Android根目录下添加一个Setup类,内容如下:

using Android.Content;using Cirrious.MvvmCross.Droid.Platform;using Cirrious.MvvmCross.ViewModels; namespace TipCalc.UI.Droid{    public class Setup : MvxAndroidSetup    {        public Setup(Context applicationContext)            : base(applicationContext)        {        }        protected override IMvxApplication CreateApp()        {            return new Core.App();        }    }}

本文中使用默认的启动行为,所以简简单单的继承MvxAndroidSetup就行了。

4.添加View

Android的View分为2个部分:LayoutActivity数据绑定可以直接写在布局文件中。

①添加Android布局文件(AXML)

Resource\layout中添加View_Tip.axml文件,内容如下:

Note:Android的数据绑定

Android的数据绑定形如:

local:MvxBind="Text Tip"

这句话相当于WPF中的:

Text = "{Binding Tip}"

前一个属性表示需要绑定控件的哪个属性,后一个属性表示需要将前面指定的属性绑定到ViewModel的哪个属性。

MvvmCross提供了大部分控件属性的绑定模式,对于没有默认实现的属性我们可以自定义绑定。

②添加Activity代码

因为在Android中AXML文件需要经由Activity(Fragment)来呈现,而且也可以顺便在Activity写一些AXML文件里面无法写的后台代码,所以MVVM中的View在Android指的是Activity

本处于原文有一定差异,原文中MvvmCross官方将Activity文件命名为xxxView,容易和Android中的View混淆,所以在这里我将其命名为xxxViewActivity。

在项目中新建Views文件夹,并添加一个TipViewActivity类,内容如下:

using Android.App;using Cirrious.MvvmCross.Droid.Views;using TipCalc.Core.ViewModels;namespace TipCalc.UI.Droid.Views{    [Activity(Label = "Tip", MainLauncher = true)]     public class TipViewActivity        : MvxActivity
{ protected override void OnViewModelSet() { base.OnViewModelSet(); SetContentView(Resource.Layout.View_Tip); } } }

此处与原文有差异,原文是利用new关键字覆盖了父类中的ViewModel并没有使用泛型。这里推荐用使用泛型类。

5.完成!

Alt text


三、iOS部分

接下来看看如何实现iOS App

本文与原文有一定差异,原文中是用Xib的方式创建界面,在本文中为了方便直接使用代码创建界面。

MvvmCross不推荐使用Storyboard创建iOS界面,因为Storyboard包含有一定的逻辑成分,如导航的逻辑,况且在iOS编程中Storyboard、Xib、纯代码三种创建界面的方式也一直在争论。我的推荐是利用纯代码创建界面,至于原因我以后会详细说明。MvvmCross也可以使用Storyboard的,如何使用我也会在后续的文章中说明。

1.创建iOS项目

在解决方案中创建一个空的iOS项目([iOS]--[Universal]--[Blank App(iOS)]),名称为TipCalc.UI.Touch

2.安装MvvmCross并引用Core库

和Android一样,用Package Manager Console安装就行。

3.添加Setup代码

操作也和Android一样,不过构造函数有些许不同。

using Cirrious.MvvmCross.Touch.Platform;using Cirrious.MvvmCross.Touch.Views.Presenters;using Cirrious.MvvmCross.ViewModels;  namespace TioCalc.UI.Touch{    public class Setup : MvxTouchSetup    {         public Setup(IMvxApplicationDelegate applicationDelegate, IMvxTouchViewPresenter presenter)             : base(applicationDelegate, presenter)        {        }        protected override IMvxApplication CreateApp()        {            return new TipCalc.Core.App();        }    }}

4.在AppDelegate中启用Setup

首先我们需要将AppDelegate的基类改为MvxApplicationDelegate。

public partial class AppDelegate : MvxApplicationDelegate

修改FinishedLaunching函数,这个函数是在App启动初始化完成后被调用的。

public override bool FinishedLaunching(UIApplication app, NSDictionary options)        {            window = new UIWindow(UIScreen.MainScreen.Bounds);            //使用默认的呈现器            var presenter = new MvxTouchViewPresenter(this, window);            var setup = new Setup(this, presenter);            setup.Initialize();            //从IoC中获取启动界面            var startup = Mvx.Resolve
(); startup.Start(); window.MakeKeyAndVisible(); return true; }

修改后的AppDelegate.cs文件如下:

using Cirrious.CrossCore;using Cirrious.MvvmCross.Touch.Platform;using Cirrious.MvvmCross.Touch.Views.Presenters;using Cirrious.MvvmCross.ViewModels;using Foundation;using UIKit;namespace TioCalc.UI.Touch{    [Register("AppDelegate")]    public class AppDelegate : MvxApplicationDelegate    {        private UIWindow window;        public override bool FinishedLaunching(UIApplication app, NSDictionary options)        {            window = new UIWindow(UIScreen.MainScreen.Bounds);            //使用默认的呈现器            var presenter = new MvxTouchViewPresenter(this, window);            var setup = new Setup(this, presenter);            setup.Initialize();            //从IoC中获取启动界面            var startup = Mvx.Resolve
(); startup.Start(); window.MakeKeyAndVisible(); return true; } }}

5.添加View

因为iOS原本为MVC模式,UIView只是纯粹的界面,并不能添加逻辑代码,所以对于MvvmCross来说,iOS的View应该是ViewController。Xib与Storyboard的描述文件虽然是Xml但是可读性很差,苹果也不推荐修改Xml,所以数据绑定等代码需要写在ViewController里面。

原文中官方对ViewController的命名是直接命名成View的,但是我觉得会和UIView混淆,所以对MVVM中View在iOS中的命名写成xxxxViewController。

在项目中新建Views文件夹,并在其中添加一个类,名称为TipCalcViewController

using Cirrious.MvvmCross.Binding.BindingContext;using Cirrious.MvvmCross.Touch.Views;using CoreGraphics;using TipCalc.Core.ViewModels;using UIKit;namespace TioCalc.UI.Touch.Views{    public class TipViewController : MvxViewController
{ ///
/// 当View被加载完成后调用,此时View还没有被显示出来,详情请查看iOS ViewController的生命周期 /// public override void ViewDidLoad() { base.ViewDidLoad(); #region 创建6个控件 var SubTotalTextField = new UITextField(); var GenerositySlider = new UISlider(); var TipLabel = new UILabel(); var subTotalInfoLabel = new UILabel {Text = "总消费:"}; var generosityInfoLabel = new UILabel {Text = "比例:"}; var tipInfoLabel = new UILabel {Text = "小费:"}; #endregion #region 将6个控件加入到View中,并固定位置 View.AddSubview(subTotalInfoLabel); subTotalInfoLabel.Frame = new CGRect(60, 100, 200, 30); View.AddSubview(SubTotalTextField); SubTotalTextField.Frame = new CGRect(60, 140, 200, 30); SubTotalTextField.KeyboardType = UIKeyboardType.NumberPad; SubTotalTextField.BorderStyle = UITextBorderStyle.RoundedRect; View.AddSubview(generosityInfoLabel); generosityInfoLabel.Frame = new CGRect(60, 180, 200, 30); View.AddSubview(GenerositySlider); GenerositySlider.Frame = new CGRect(60, 220, 200, 30); GenerositySlider.MaxValue = 100; GenerositySlider.MinValue = 0; View.AddSubview(tipInfoLabel); tipInfoLabel.Frame = new CGRect(60, 260, 200, 30); View.AddSubview(TipLabel); TipLabel.Frame = new CGRect(60, 300, 200, 30); #endregion #region 数据绑定 this.CreateBinding(SubTotalTextField).To
(vm => vm.SubTotal).Apply(); this.CreateBinding(GenerositySlider).To
(vm => vm.Generosity).Apply(); this.CreateBinding(TipLabel).To
(vm => vm.Tip).Apply(); #endregion } }}

Note : 关于iOS的绑定

因为上面所说的原因,iOS的绑定需要在ViewController内使用代码进行绑定,虽然比Android和Windows平台复杂,但是总的来说还是比较简单的。

this.CreateBinding(TipLabel).To
(vm => vm.Tip).Apply();

等同于

this.CreateBinding(TipLabel).For(label => label.Text).To("Tip").Apply();

因为大部分控件都有一个默认的绑定属性,所以在大部分情况下可以省略指定属性的步骤。

目标属性指定时也可以直接用字符串,实际上利用表达式树的形式也是转换为字符串的,这样做的目的是为了在重命名时能够自动修改所有的绑定,避免重命名时少修改了一个字符串而导致的错误。

6.完成!

来看看iOS的效果吧:

Alt text


四、Windows 8 通用应用程序部分

前面我们已经完成了Android和iOS部分,接下来我们来看看MvvmCross如何在Windows通用程序使用。

本例以2013的Win8通用程序作为例子。Win10我还没看。

1.创建Windows通用应用程序

在VS中新建一个[应用商店]--[空白应用程序(通用应用程序)](Blank App Universal Apps),名称为TipCalc.UI.Win

一个通用应用程序包括了3个部分:

  • Shared库项目,这是一个Windows项目和WindowsPhone项目共用的部分。通常我们会把这2个平台的可以共用业务逻辑放在这,不过因为MvvmCross已经将业务逻辑移到Core库中,以供多个平台使用,所以在这个Share库里面不会有过多的代码。
  • Windows平台UI项目。运行Win8和Win10的设备将会使用这个项目。
  • WindowsPhone平台UI项目。运行WindowsPhone的设备将会使用这个项目。

2.安装MvvmCross与引用Core库

和Android、iOS一样,分别对2个平台的UI项目使用Package Manager Console

Install-Package MvvmCross.HotTuna.MvvmCrossLibraries

然后在分别引用Core库。并删掉各自的MainPage.xaml

3.增加Setup代码

Shared项目的根目录中新建一个Setup类,正如我们前面说的一样,每一个平台都需要对应的Setup类来控制程序的启动行为。对于WindowsWindowsPhone 我们可以共用一个Setup类。

using Cirrious.MvvmCross.ViewModels;using Cirrious.MvvmCross.WindowsCommon.Platform;using Cirrious.MvvmCross.WindowsCommon.Views;namespace TipCalc.UI.Win{    public class Setup : MvxWindowsSetup    {        public Setup(Frame rootFrame)            : base(rootFrame)        {        }        protected override IMvxApplication CreateApp()        {            return new Core.App();        }    }}

Setup本质上是返回一些用来控制程序运行的对象,当我们需要在WindowsWindowsPhone中实现不同的效果,可以用条件编译等方式让Setup返回不同的对象来达到控制程序不同运行效果的目的。

4.在App.xaml.cs启用Setup

修改App.xaml.cs文件的OnLaunch回调函数:

先删掉这几行:

if (!rootFrame.Navigate(typeof(MainPage), e.Arguments)) {     throw new Exception("Failed to create initial page"); }

然后在这几行的位置输入下面这段代码:

var setup = new Setup(rootFrame); setup.Initialize(); var start = Mvx.Resolve
(); start.Start();

5.增加View

这部分需要分别对WindowsWindowsPhone项目进行操作,但是操作的过程是一模一样的,除了WindowsPhone的布局有点不一样。这里我们只说如何在Windows项目操作。

①在TipCalc.UI.Win.Windows项目中新建Page

在TipCalc.UI.Win.Windows项目中创建Views文件夹。在其中添加一个基本页(Basic Page),名为TipView.xaml。

创建基本页的过程中VS会问我们是不是需要添加一些辅助类,选,这时VS会自动向项目中添加一些辅助类,不过在本文中我们用不着他们,不用管他们。

②将TipView修改为MvvmCross基本页

TipView.cs文件中将TipView的基类改为MvxWindowsPage

public sealed partial class TipView : Page

修改为:

public sealed partial class TipView : MvxWindowsPage

并修改OnNavigationToOnNavigationFrom回调函数,让其调用基类对应的函数。这样做的目的是为了让MvvmCross知道页面的状态,并执行相应操作,如利用IoC填充ViewModel属性。

protected override void OnNavigatedTo(NavigationEventArgs e){    base.OnNavigatedTo(e);    navigationHelper.OnNavigatedTo(e);}protected override void OnNavigatedFrom(NavigationEventArgs e){    base.OnNavigatedFrom(e);    navigationHelper.OnNavigatedFrom(e);}

③设置ViewModel

与iOS和Android不同的是我们需要用new关键字手动设置ViewModel。

这里与原文有点差异,原文3个平台都是用这个方法设置ViewModel的,但是因为可以利用泛型设置ViewModel,所以我们没有采用这种方法。通用应用程序与iOS、Android不同的是,他的View基类Xaml中定义了一次,而Xaml不能使用泛型(也可能是我不知道怎么设置),所以只能采用这种方式。

为什么一定要设置好View的ViewModel的类型,而不是使用一个公用的基类呢?因为MvvmCross是通过反射View的ViewModel属性来确定ViewViewModel对应关系的,如果你将2个View里面的ViewModel的类型设置为一样,MvvmCross启动的时候就会报错,因为他不知道ViewModel该如何对应这2个View。因为MvvmCross的导航系统是基于ViewModel的,所以ViewViewModel必须是一一对应的。

当然也有方法让不同的View对应相同的ViewModel,因为涉及到导航系统,所以在这里不展开讲,以后会有专门的文章来介绍的。

修改TipView.cs增加如下代码:

public new TipViewModel ViewModel{    get { return (TipViewModel)base.ViewModel; }    set { base.ViewModel = value; }}

这样后我们的TipView.cs文件大概是这样子的:(我删掉了注释)

using Windows.UI.Xaml.Navigation;using Cirrious.MvvmCross.WindowsCommon.Views;using TipCalc.Core.ViewModels;using TipCalc.UI.Win.Common; namespace TipCalc.UI.Win.Views{     public sealed partial class TipView : MvxWindowsPage    {        private NavigationHelper navigationHelper;        private ObservableDictionary defaultViewModel = new ObservableDictionary();        public new TipViewModel ViewModel        {            get { return (TipViewModel)base.ViewModel; }            set { base.ViewModel = value; }        }                public ObservableDictionary DefaultViewModel        {            get { return this.defaultViewModel; }        }         public NavigationHelper NavigationHelper        {            get { return this.navigationHelper; }        }        public TipView()        {            this.InitializeComponent();            this.navigationHelper = new NavigationHelper(this);            this.navigationHelper.LoadState += navigationHelper_LoadState;            this.navigationHelper.SaveState += navigationHelper_SaveState;        }                 private void navigationHelper_LoadState(object sender, LoadStateEventArgs e)        {        }              private void navigationHelper_SaveState(object sender, SaveStateEventArgs e)        {        }        #region NavigationHelper 注册                 protected override void OnNavigatedTo(NavigationEventArgs e)        {            base.OnNavigatedTo(e);            navigationHelper.OnNavigatedTo(e);        }        protected override void OnNavigatedFrom(NavigationEventArgs e)        {            base.OnNavigatedFrom(e);            navigationHelper.OnNavigatedFrom(e);        }        #endregion    }}

④修改Xaml布局文件

首先我们需要修改Page的基类为MvxWindowsPage,将:

修改为:

手动拖入控件,或者自己写Xaml,让Page看起来像这个样子:

Windows平台界面

Xaml代码如下:

⑥WindowsPhone项目与Windows的区别

首先是布局上的区别,很明显FontSize = 40在WP上有点大,还有StackPaanel的Margin也有点大,改到比较合适的值就行了。

然后是WindowsPhone的导航行为和其他平台有一定的差异,系统会缓存View来复用,所以我们需要在逻辑上创建一个新View时重置他的ViewModel。

我也没怎么研究过WindowsPhone,所以不了解这个复用机制。如果我说的有问题请在评论中指出。

在修改TipView.cs时需要将OnNavigationTo函数修改成下面这样:

protected override void OnNavigatedTo(NavigationEventArgs e)        {            if (e.NavigationMode == NavigationMode.New)            {                ViewModel = null;            }            base.OnNavigatedTo(e);            this.navigationHelper.OnNavigatedTo(e);        }

6.完成

让我们看看2个平台的运行效果:

Windows平台运行效果

Alt text

转载于:https://www.cnblogs.com/visualakari/p/4809448.html

你可能感兴趣的文章
MySQL 网络访问连接
查看>>
在aws ec2上使用root用户登录
查看>>
数据访问 投票习题
查看>>
CIO知识储备
查看>>
cnblog!i'm coming!
查看>>
使用点符号代替溢出的文本
查看>>
Axios 中文说明
查看>>
fatal: remote origin already exists.
查看>>
gridview 自定义value值
查看>>
2018二月实现计划成果及其三月规划
查看>>
封装springmvc处理ajax请求结果
查看>>
tyvj P2018 「Nescafé26」小猫爬山 解题报告
查看>>
类名.class和getClass()区别
查看>>
开发脚本自动部署及监控
查看>>
JavaScript--语句
查看>>
12/17面试题
查看>>
css 继承和层叠
查看>>
javascript实现图片轮播3D效果
查看>>
ssl初一组周六模拟赛【2018.3.17】
查看>>
[RxJS] Avoid mulit post requests by using shareReplay()
查看>>