老司机学新平台 - Xamarin Forms开发框架之MvvmCross插件精选

在前两篇老司机学Xamarin系列中,简单介绍了Xamarin开发环境的搭建以及Prism和MvvmCross这两个开发框架。不同的框架,往往不仅仅使用不同的架构风格,同时社区活跃度不同,各种功能模块和插件数量也会有巨大差别。架构风格的好坏,属于仁者见仁,但功能模块和插件的好坏多寡,却实实在在体现了社区的力量,是可以实打实拿出来练一练的。今天我们就来一起玩一玩MvvmCross提供的各种功能插件,看看哪些轮子可以拿来直接就用的。

备注:本文主要关注兼容Xamarin Forms xaml并且至少支持Droid和iOS平台的插件。

官方模块和插件

在把视线放到社区贡献的插件之前,我们先来看看,MvvmCross框架官方提供了哪些模块、插件:

Localization

Localization支持,对任何一个需要支持多国语言的App都必不可少。这个组件其实谈不上MvvmCross特有,基本上是Xamarin官方文档推荐的最佳实践。不过MvvmCross的VS项目模版直接包含了这个实现,并且集成到了MvvmCross自己的IoC容器的服务注册。

在MvvmCross模版生成的项目中,资源文件和获取device当前语言的ILocalizeService接口定义在PCL Core项目中:

特定平台的LocalizeService实现,定义在各自平台项目的Services目录中。在Setup类中默认将ILocalizeService注册成为Singleton:

在任意xaml的View中,通过{res:Translate ResourceKey}这样的格式绑定到控件的属性:

DataBinding

数据绑定,是对任何App的View的显示和交互必不可少的功能,这方面,Xamarin Forms本身的支持,就已经非常强悍了。MvvmCross有一个页面专门介绍它对数据绑定的增强,然而,几乎所有的增强貌似都是针对非Xamarin Forms xaml时期的Xamarin UI的数据绑定支持的,随着Forms基于xaml的本身的数据绑定能力的出现和不断增强,MvvmCross的这部分扩展其实已经过时了。

需要重点提及的是,在上面的LocalizeService组件的介绍中提到的{res:Translate ResourceKey}这样的语法,其实是基于Xamarin Forms官方的IMarkupExtension接口的扩展。而另一个可以用来扩展数据绑定的是IValueConverter接口。自定义的value converter,一般是用来在进行某个字段的数据的存储和数据的显示之间的转换的,但是,应该也可以利用它的语法来实现一些巧妙的数据绑定扩展。未来对Xamarin Forms UI组件的数据绑定的真正扩展,我想应该是基于上面提到的这两个接口来实现。期望以后能看到各种有趣的插件。当然,自己增加扩展也很容易。

简单举一个自定义IValueConverter的例子。假设我实现了下面这个自定义converter,用来自定义显示某个model的长度属性:

上面这个Converter的意思是,如果一个字段在ViewModel里面的属性值为空,或者为0,显示到页面上时,显示为Default,否则,显示原来的值。假设ViewModel包含下面这个Length属性,默认值为0:

xaml页面的一个Label的Text,现在要绑定到上面这个Length属性,并且,使用上面这个converter:

xaml里稍微复杂一点,不过也不难理解。首先是定义一个名叫local的xmlns,然后需要将这个自定义的converter注册到resource中,最后,在Label的Text属性中指定绑定和converter。

Accelerometer

Accelerometer插件读取Accelerometer传感器的值。因为肯定要访问设备特定的API,它自然包含接口和对应不同设备的具体实现。在项目中启用一个MvvmCross的plugin,只需引用对应的nuget package。这个插件对应的package是MvvmCross.Plugin.Accelerometer,可以从nuget.org下载。如果为一个包含shared PCL项目和Droid,iOS项目的solution添加这个插件的nuget package后,会看到,shared项目只自动引用了包含IMvxAccelerometer接口的dll,而Droid项目和iOS项目则还自动引用了实现了IMvxAccelerometer接口的平台特定的dll,并且,在Droid和iOS项目中的Boostrap目录,会自动添加一个AccelerometerPluginBootstrap类,这个类是用于在App启动时自动注册插件的,在这里,特定来说,Bootstrap内部会自动注册IMvxAccelerometer接口的实现。然后,我们就可以在代码里访问传感器的数据了。比如:

当访问设备特定的api时,还需要留意是否需要申请特定的设备权限。比如安卓设备访问Accelerometer就需要在Droid项目\Properties\AndroidManifest.xml文件中添加一个users-feature如下:

可以看到,MvvmCross插件的注册机制非常巧妙,我一开始以为可能每个插件至少需要有shared,droid,ios这样多个nuget package,实际上,它只有一个nuget package,不同的项目类型自动引用了同一个package里的不同target的dll,而且还自动添加了Bootstrap类,在框架层面通过反射和命名约定自动载入了插件,使得整个插件启用过程不需手写任何代码,非常赞!另外,除了通过Mvx.Resolve()获取一个接口的实例之外,MvvmCross也支持构造函数注入,比如,可以为ViewModel的构造函数,添加一个IMvxAccelerometer参数,当ViewModel被IoC容器初始化时,构造函数的参数都会被自动注入。

Email

Email插件,自然是用来发Email了。安装nuget package:MvvmCross.Plugins.Email。它就包含了下面这个接口:

public interface IMvxComposeEmailTask
{
    void ComposeEmail(string to, string cc, string subject, string body, bool isHtml);
}

所以,发邮件至需要下面这样的代码,不能再简单了:

Mvx.Resolve<IMvxComposeEmailTask>()
   .ComposeEmail("me@slodge.com", 
                 string.Empty, 
                 "MvvmCross Email",
                 "I <3 MvvmCross",
                 false);

File

File插件,提供了读写设备本地文件的能力。nuget package:MvvmCross.Plugins.File。Droid和iOS读写文件时默认的当前目录有差异(更多其他平台的当前目录,请参见官方文档):

  • Android - Context.FilesDir
  • iOS - Environment.SpecialFolder.MyDocuments

实现的接口如下:

public interface IMvxFileStore
{
    bool TryReadTextFile(string path, out string contents);
    bool TryReadBinaryFile(string path, out Byte[] contents);
    bool TryReadBinaryFile(string path, Func<Stream, bool> readMethod);
    void WriteFile(string path, string contents);
    void WriteFile(string path, IEnumerable<Byte> contents);
    void WriteFile(string path, Action<Stream> writeMethod);
    bool TryMove(string from, string to, bool deleteExistingTo);
    bool Exists(string path);
    bool FolderExists(string folderPath);
    string PathCombine(string items0, string items1);
    string NativePath(string path);
    void EnsureFolderExists(string folderPath);
    IEnumerable<string> GetFilesIn(string folderPath);
    void DeleteFile(string path);
    void DeleteFolder(string folderPath, bool recursive);
}

具体使用方法类似其他插件,只需要通过Mvx.Resolve()方法拿到instance,调用相应方法即可,这里就不详述了。

Location

Location插件,用来获取用户当前的GPS位置信息。nuget package: MvvmCross.Plugins.Location。实现了IMvxLocationWatcher接口:

public interface IMvxLocationWatcher
{
    void Start(
        MvxLocationOptions options, 
        Action<MvxGeoLocation> success, 
        Action<MvxLocationError> error);
    void Stop();
    bool Started { get; }

    MvxGeoLocation CurrentLocation { get; }
    MvxGeoLocation LastSeenLocation { get; }

    event EventHandler<MvxValueEventArgs<MvxLocationPermission>> OnPermissionChanged;
}

使用方式和Accelerometer类似,就不详述了。

Messenger

Messenger插件提供了基于内存的组件间基于消息的pub/sub功能。非常有用的一个插件。nuget package:MvvmCross.Plugins.Messenger。这是一个纯PCL插件,没有平台特定代码。实现了IMvxMessenger接口:

public interface IMvxMessenger
{
    MvxSubscriptionToken Subscribe<TMessage>(Action<TMessage> deliveryAction, MvxReference reference = MvxReference.Weak, string tag = null) where TMessage : MvxMessage;
    MvxSubscriptionToken SubscribeOnMainThread<TMessage>(Action<TMessage> deliveryAction, MvxReference reference = MvxReference.Weak, string tag = null) where TMessage : MvxMessage;

    MvxSubscriptionToken SubscribeOnThreadPoolThread<TMessage>(Action<TMessage> deliveryAction, MvxReference reference = MvxReference.Weak, string tag = null) where TMessage : MvxMessage;

    void Unsubscribe<TMessage>(MvxSubscriptionToken mvxSubscriptionId) where TMessage : MvxMessage;

    bool HasSubscriptionsFor<TMessage>() where TMessage : MvxMessage;

    int CountSubscriptionsFor<TMessage>() where TMessage : MvxMessage;

    bool HasSubscriptionsForTag<TMessage>(string tag) where TMessage : MvxMessage;

    int CountSubscriptionsForTag<TMessage>(string tag) where TMessage : MvxMessage;

    IList<string> GetSubscriptionTagsFor<TMessage>() where TMessage : MvxMessage;

    void Publish<TMessage>(TMessage message) where TMessage : MvxMessage;

    void Publish(MvxMessage message);

    void Publish(MvxMessage message, Type messageType);

    void RequestPurge(Type messageType);

    void RequestPurgeAll();
}

Network

Network插件本来只是设计来检测某个网络主机是否能连通。结果,功能被热心网友不断扩充,就成了一个轻量级的REST Http Client。nuget package:MvvmCross.Plugins.Network。它提供了下面这些接口的实现:

public interface IMvxReachability
{
    bool IsHostReachable(string host);
}

public interface IMvxRestClient
{
    void ClearSetting(string key);

    void SetSetting(string key, object value);

    IMvxAbortable MakeRequest(MvxRestRequest restRequest, Action<MvxRestResponse> successAction, Action<Exception> errorAction);
    IMvxAbortable MakeRequest(MvxRestRequest restRequest, Action<MvxStreamRestResponse> successAction, Action<Exception> errorAction);

    Task<MvxRestResponse> MakeRequestAsync(MvxRestRequest restRequest, CancellationToken cancellationToken = default(CancellationToken));
    Task<MvxStreamRestResponse> MakeStreamRequestAsync(MvxRestRequest restRequest, CancellationToken cancellationToken = default(CancellationToken));
}

public interface IMvxJsonRestClient
{
    Func<IMvxJsonConverter> JsonConverterProvider { get; set; }

    IMvxAbortable MakeRequestFor<T>(MvxRestRequest restRequest, Action<MvxDecodedRestResponse<T>> successAction, Action<Exception> errorAction);

    Task<MvxDecodedRestResponse<T>> MakeRequestForAsync<T>(MvxRestRequest restRequest, CancellationToken cancellationToken = default(CancellationToken));
}

PhoneCall

PhoneCall插件自然是用来打电话的。nuget package:MvvmCross.Plugins.PhoneCall。实现了IMvxPhoneCallTask接口:

public interface IMvxPhoneCallTask
{
    void MakePhoneCall(string name, string number);
}

PictureChooser

PictureChooser用来从相册选照片,或者用相机拍照。nuget package:MvvmCross.Plugins.PictureChooser。实现了IMvxPictureChooserTask接口:

public interface IMvxPictureChooserTask
{
    void ChoosePictureFromLibrary(int maxPixelDimension, int percentQuality, Action<Stream> pictureAvailable, Action assumeCancelled);

    void TakePicture(int maxPixelDimension, int percentQuality, Action<Stream> pictureAvailable, Action assumeCancelled);
}

简单的使用代码如下:

var task = Mvx.Resolve<IMvxPictureChooserTask>();
task.ChoosePictureFromLibrary(500, 90,
       stream => {
           // use the stream
           // expect the stream to be disposed after immediately this method returns.
       },
       () => {
           // perform any cancelled operation
       });

Share

Share插件用来发表分享。nuget package:MvvmCross.Plugins.Share。实现了IMvxShareTask接口:

public interface IMvxShareTask
{
    void ShareShort(string message);
    void ShareLink(string title, string message, string link);
}

SQLite

SQLite插件是又一款必备插件,用来访问Sqlite数据库。nuget package:MvvmCross.Plugin.SQLitePCL。实现了IMvxSqliteConnectionFactory接口:

public interface IMvxSqliteConnectionFactory
{
    SQLiteConnection GetConnection(string databaseName, bool prefixPlatformPath = true);
    SQLiteConnection GetConnection(SqLiteConfig config, bool prefixPlatformPath = true);
    SQLiteAsyncConnection GetAsyncConnection(string databaseName, bool prefixPlatformPath = true);
    SQLiteAsyncConnection GetAsyncConnection(SqLiteConfig config, bool prefixPlatformPath = true);
}

WebBrowser

WebBrowser插件调用外部浏览器显示一个页面。nuget package:MvvmCross.Plugins.WebBrowser。实现了IMvxWebBrowserTask接口:

public interface IMvxWebBrowserTask
{
    void ShowWebPage(string url);
}

MvvmCross提供的官方插件,推荐的就这些了。貌似还有点不过瘾,好像很多期望的必备功能还没看到?下面来看看社区的贡献。

第三方插件

Settings

Settings插件提供了一个通用方案,用来持久化保存key/value键值对到各平台默认的App本地配置文件。nuget package:Cheesebaron.MvxPlugins.Settings。实现了ISettings接口:

public interface ISettings
{
    /// <param name="roaming">Roam settings (only for WindowsCommon)</param>
    
    T GetValue<T>(string key, T defaultValue = default(T), bool roaming = false);
    bool AddOrUpdateValue<T>(string key, T value = default(T), bool roaming = false);
    bool DeleteValue(string key, bool roaming = false);
    bool Contains(string key, bool roaming = false);
    bool ClearAllValues(bool roaming = false);
}

DeviceInfo

DeviceInfo插件支持获取包括设备分辨率,设备id,固件版本,内存大小等等设备信息。nuget package:Cheesebaron.MvxPlugins.DeviceInfo。实现了IDeviceInfo和IDisplay接口:

public interface IDeviceInfo
{
    string DeviceId { get; }
    string Name { get; }
    string FirmwareVersion { get; }
    string HardwareVersion { get; }
    string Manufacturer { get; }
    string LanguageCode { get; }
    double TimeZoneOffset { get; }
    string TimeZone { get; }
    Orientation Orientation { get; }
    long TotalMemory { get; }
    bool IsTablet { get; }
    DeviceType DeviceType { get; }
}

public interface IDisplay
{
    int Height { get; }
    int Width { get; }
    double Xdpi { get; }
    double Ydpi { get; }
    double Scale { get; }
}

Connectivity

Connectivity插件返回当前设备是否联网,当前使用WIFI还是移动网络。nuget package:Cheesebaron.MvxPlugins.Connectivity。实现了IConnectivity接口:

public interface IConnectivity: INotifyPropertyChanged
{
    bool IsConnected { get; }
    bool IsWifi { get; }
    bool IsCellular { get; }
    Task<bool> GetHostReachableAsync(string host, CancellationToken token = default(CancellationToken));
}

SMS

SMS插用来发送短信。nuget package:Cheesebaron.MvxPlugins.SMS。实现了ISmsTask接口:

public interface ISmsTask
{
    void SendSMS(string body, string phoneNumber);
}

SecureStorage

SecureStorage插用用户保存敏感数据key/value键值对到平台特定的安全数据存储,比如Keychain,Password Vault等。nuget package:Beezy.MvvmCross.Plugins.SecureStorage。实现了IMvxProtectedData接口:

public interface IMvxProtectedData
{
    void Protect(string key, string value);
    string Unprotect(string key);
    void Remove(string key);
}

InfiniteScrollPlugin

InfiniteScrollPlugin插用提供了一个能够根据指定的数据源,一直向下滚动分页显示数据的能力。nuget package:Sequence.Plugins.InfiniteScroll。实现了IIncrementalCollectionFactory接口。典型的示例可以参见这里

Fingerprint

Fingerprint提供指纹识别验证支持。nuget package:MvvmCross.Plugins.Fingerprint。实现了IFingerprint接口:

    Task<FingerprintAvailability> GetAvailabilityAsync();
    Task<bool> IsAvailableAsync();
    Task<FingerprintAuthenticationResult> AuthenticateAsync(string reason, CancellationToken cancellationToken = default(CancellationToken));
    Task<FingerprintAuthenticationResult> AuthenticateAsync(AuthenticationRequestConfiguration authRequestConfig, CancellationToken cancellationToken = default(CancellationToken));

调用IFingerprint接口的示例:

var fpService = Mvx.Resolve<IFingerprint>(); // or use dependency injection and inject IFingerprint

var result = await fpService.AuthenticateAsync("Prove you have mvx fingers!");
if (result.Authenticated)
{
    // do secret stuff :)
}
else
{
    // not allowed to do secret stuff :(
}

UserInteraction

UserInteraction插件实现了最常用的几个UserDialogs,包括:Alert,Confirm和Input。nuget package:Chance.MvvmCross.Plugins.UserInteraction。它实现了IUserInteraction接口:

public interface IUserInteraction
{
    void Confirm(string message, Action okClicked, string title = null, string okButton = "OK", string cancelButton = "Cancel");
    void Confirm(string message, Action<bool> answer, string title = null, string okButton = "OK", string cancelButton = "Cancel");
    Task<bool> ConfirmAsync(string message, string title = "", string okButton = "OK", string cancelButton = "Cancel");

    void Alert(string message, Action done = null, string title = "", string okButton = "OK");
    Task AlertAsync(string message, string title = "", string okButton = "OK");

    void Input(string message, Action<string> okClicked, string placeholder = null, string title = null, string okButton = "OK", string cancelButton = "Cancel", string initialText = null);
    void Input(string message, Action<bool, string> answer, string placeholder = null, string title = null, string okButton = "OK", string cancelButton = "Cancel", string initialText = null);
    Task<InputResponse> InputAsync(string message, string placeholder = null, string title = null, string okButton = "OK", string cancelButton = "Cancel", string initialText = null);

    void ConfirmThreeButtons(string message, Action<ConfirmThreeButtonsResponse> answer, string title = null, string positive = "Yes", string negative = "No", string neutral = "Maybe");
    Task<ConfirmThreeButtonsResponse> ConfirmThreeButtonsAsync(string message, string title = null, string positive = "Yes", string negative = "No", string neutral = "Maybe");
}

User Dialogs

User Dialogs严格来说并不是一个标准的MvvmCross插件。但是它提供了相比上面的UserInteraction更丰富的User Dialogs功能,所以,有需要的朋友也可以看一下。nuget package: Acr.UserDialogs。它可以和MvvmCrossy一起工作,但是没有follow MvvmCross插件自动初始化机制,因此,需要特别配置。引用package之后,需要在App类中注册IUserDialog到Mvx的IoC容器:

Mvx.RegisterSingleton<IUserDialogs>(() => UserDialogs.Instance);

同时,对于Droid项目(iOS项目不需要),还必须在MvxFormsApplicationActivity类的OnCreate()方法里,执行下面的初始化代码:

UserDialogs.Init(this);

然后,就可以在任意Command里调用IUserDialogs接口的方法弹出Dialogs了。IUserDialogs接口支持下面这些Dialogs方法:

public interface IUserDialogs
{
    IDisposable Alert(string message, string title = null, string okText = null);
    IDisposable Alert(AlertConfig config);
    Task AlertAsync(string message, string title = null, string okText = null, CancellationToken? cancelToken = null);
    Task AlertAsync(AlertConfig config, CancellationToken? cancelToken = null);

    IDisposable ActionSheet(ActionSheetConfig config);
    Task<string> ActionSheetAsync(string title, string cancel, string destructive, CancellationToken? cancelToken = null, params string[] buttons);

    IDisposable Confirm(ConfirmConfig config);
    Task<bool> ConfirmAsync(string message, string title = null, string okText = null, string cancelText = null, CancellationToken? cancelToken = null);
    Task<bool> ConfirmAsync(ConfirmConfig config, CancellationToken? cancelToken = null);

    IDisposable DatePrompt(DatePromptConfig config);
    Task<DatePromptResult> DatePromptAsync(DatePromptConfig config, CancellationToken? cancelToken = null);
    Task<DatePromptResult> DatePromptAsync(string title = null, DateTime? selectedDate = null, CancellationToken? cancelToken = null);

    IDisposable TimePrompt(TimePromptConfig config);
    Task<TimePromptResult> TimePromptAsync(TimePromptConfig config, CancellationToken? cancelToken = null);
    Task<TimePromptResult> TimePromptAsync(string title = null, TimeSpan? selectedTime = null, CancellationToken? cancelToken = null);

    IDisposable Prompt(PromptConfig config);
    Task<PromptResult> PromptAsync(string message, string title = null, string okText = null, string cancelText = null, string placeholder = "", InputType inputType = InputType.Default, CancellationToken? cancelToken = null);
    Task<PromptResult> PromptAsync(PromptConfig config, CancellationToken? cancelToken = null);

    IDisposable Login(LoginConfig config);
    Task<LoginResult> LoginAsync(string title = null, string message = null, CancellationToken? cancelToken = null);
    Task<LoginResult> LoginAsync(LoginConfig config, CancellationToken? cancelToken = null);

    IProgressDialog Progress(ProgressDialogConfig config);
    IProgressDialog Loading(string title = null, Action onCancel = null, string cancelText = null, bool show = true, MaskType? maskType = null);
    IProgressDialog Progress(string title = null, Action onCancel = null, string cancelText = null, bool show = true, MaskType? maskType = null);

    void ShowLoading(string title = null, MaskType? maskType = null);
    void HideLoading();

    void ShowImage(IBitmap image, string message, int timeoutMillis = 2000);
    void ShowSuccess(string message, int timeoutMillis = 2000);
    void ShowError(string message, int timeoutMillis = 2000);

    IDisposable Toast(string title, TimeSpan? dismissTimer = null);
    IDisposable Toast(ToastConfig cfg);
}

AudioPlay

比较遗憾的是没有看到支持音乐播放(比如播放在线或本地的mp3/wav)的插件。这个应该是大多数App都需要的功能。搜索了一些网上的实现方案,先给需要的朋友做个参考,要不下次咱把它改造成标准的MvvmCross插件?

2016-10-22 Update: 兑现设想,我的第一个MvvmCross跨平台插件:SimpleAudioPlayer

2016-10-23 Update: 新增一些非MvvmCross,但是Xamarin下的通用第三方组件:

  • Xamarin-Forms-Labs - 看上去是Xamarin界最大的一个第三方组件库,包含了大量UI和Service等类型的好东西,组件列表太长了,不贴在这里,大家可以点过去看。
  • DLToolkit.Forms.Controls - DL Toolkit提供的几个非常酷的组件,包括图片转换、增强的ListView和tag标签功能等。
  • DevExpress-Grid - 老牌大厂DevExpress出的Xamarin Forms数据表格控件,功能强大,竟然完全免费?!

暂时就这些了。如果您发现有别的优秀插件,欢迎推荐,我也会持续补充进来。


posted @ 2016-10-11 00:14 Teddy's Knowledge Base Views(...) Comments(...) Edit 收藏