Xamarin的Forms的确挺神的,尤其是对已经熟悉WPF和Xaml语法的朋友。在Xamarin Forms里,Xaml还是那熟悉的Xaml(至少大体上是这样的),C#也还是那熟悉的C#,编译出来的东东却是苹果和安卓的App。在第三世界的Windows Phone和Windows Store里待久了,突然挤入了发达国家iOS和安卓的世界,感觉可不要太好了。
要说Xaml,大家最喜欢的铁定是它的绑定(Binding)功能。绑定功能实在是太强大,太省心。可绑定的内容多样不说,它可以彻底地实现用户节目层和业务逻辑层的分离,行云流水般地实现了model-view-viewmodel (MVVM)的构架设计。在Xaml的世界里,大伙儿怕是早就忘记了按钮儿还有一个叫Clicked的事件。
诸如这样的代码:
<Button Text="Click me" Clicked="OnButtonClicked" />或
button.Clicked += OnButtonClicked;
完全不需要!通过绑定按钮儿的命令(Command),如:
Xaml:
<Button Text="Click me" Command="{Binding ButtonClickCommand}" />
public class TryEventBindingPageViewModel { private readonly ICommand _buttonClickCommand = new Command(ButtonClickCommandExecute, CanButtonClickCommandExecute); private void ButtonClickCommandExecute(object args) { // 点击按钮后激活的业务逻辑 } private bool CanButtonClickCommandExecute(object args) { // 如果返回值为假,按钮被禁用无法点击 } }
>> 参看Xamarin官网上的更多MVVM的介绍
我们不但可以绑定上点击按钮后的逻辑还可以轻易地控制按钮的状态,比如可用(Enabled)和禁用(Disable的)。要知道命令的绑定实在是太方便了,方便到不管是控件的啥功能,只要是可以触发的,我们都想着通过命令绑定的方式来实现。
理想是美好的,现实是残酷的。并不是每个控件都有Command这个属性,事实上只有实现了ICommandSource的控件,比如按钮,才有Command。何况Command只能针对一个可激活的事件,比如按钮中的Command针对的就是Clicked这个事件而定的。难道对于广大的非ICommandSource的控件,我们就只能忍受传统的事件处理机制了吗?
No!方法还是有的。一个比较常用的解决方案,或者时髦一点儿说,设计模式(design pattern)叫EventToCommand。也就是说,我们可以把事件,也就是Event明修栈道暗渡陈仓地转化成Command,然后在按照命令绑定的方法来处理被激发的事件。开源MVVM框架MVVM Light里实现了EventToCommand的方法。但很遗憾的是MVVM Light的方法在Xamarin Forms里用不太上,因为Portable Library Class (PCL)不支持MVVM Light里需要的interactivity框架。这个事情让我苦恼了很一段时间。我的一个项目需要使用Xamarin Forms的Search Bar。我实在是太渴望能用命令绑定的方式来处理SearchBar里的TextChanged事件了,渴望到我决心一了百了的实现一个PCL下可以使用的CommandToEvent机制。设计的目的很简单,以下两条:
R.H在CodeProject上的文章《Command binding with Events - A way from simple to advanced》介绍了用Attached Property来实现EventToCommand的方式。Xamarin是支持Attached Property的,但是在它的官网上相关的信息实在是太少了。Google上也不多,但一阵努力后还是找到了店蛛丝马迹,比如pause.coffee的博文《Custom attached properties and behaviours》有提到Xamarin把传统WPF里的Attached Property的语法由
DependencyProperty someProperty = DependencyProperty.RegisterAttached(...)改成了
BindableProperty = BindableProperty.CreateAttached<TDeclarer, TPropertyType>(...)
无论如何把,pause.coffee的方法是一个很好的出发点。我是这样想的,从用户的角度出发的话,我希望我的Xaml会是这样的。。。
<SearchBar Text="Search" FromEvent="TextChanged" ToCommand="{Binding OnSearchChangedCommand}" />
FromEvent接受的是一个string,也就是要转化的事件的名字,比如TextChanged。故名思意,ToCommand则是目标命令,是我们的binding机制可以大显神通的地方。这样一来,处理事件的方法就顺其自然地演变成了普通命令绑定。
高记通用EventToCommand代码全文如下。
C#:
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Reactive.Linq; using System.Reflection; using System.Windows.Input; using Xamarin.Forms; namespace Xamarin.Portable.Extensions { public class EventToCommand { public static readonly BindableProperty ToCommandProperty = BindableProperty.CreateAttached<EventToCommand, ICommand>( bindable => EventToCommand.GetToCommand(bindable), null, BindingMode.OneWay, null, (bindable, oldValue, newValue) => EventToCommand.OnCommandChanged(bindable, oldValue, newValue), null, null); public static readonly BindableProperty FromEventProperty = BindableProperty.CreateAttached<EventToCommand, string>( bindable => EventToCommand.GetFromEvent(bindable), null, BindingMode.OneWay); private static readonly ICollection<IDisposable> Subscriptions = new Collection<IDisposable>(); public static ICommand GetToCommand(BindableObject obj) { return (ICommand)obj.GetValue(EventToCommand.ToCommandProperty); } public static void SetToCommand(BindableObject obj, ICommand value) { obj.SetValue(EventToCommand.ToCommandProperty, value); } public static string GetFromEvent(BindableObject obj) { return (string)obj.GetValue(EventToCommand.FromEventProperty); } public static void SetFromEvent(BindableObject obj, string value) { obj.SetValue(EventToCommand.FromEventProperty, value); } private static void OnCommandChanged(BindableObject obj, ICommand oldValue, ICommand newValue) { var eventName = GetFromEvent (obj); if (string.IsNullOrEmpty (eventName)) { throw new InvalidOperationException ("FromEvent property is null or empty"); } var subscription = Observable.FromEventPattern (obj, eventName).Subscribe (args => { var command = GetToCommand(obj); if (command != null && command.CanExecute(args)) { command.Execute(args); } }); Subscriptions.Add(subscription); } /// <summary> /// Unsubscribes all event subscription specifically /// </summary> internal static void UnsubscribeAll() { foreach (var subscription in Subscriptions) { subscription.Dispose (); } Subscriptions.Clear (); } } }
需要说明的是我的方法虽然需要使用微软的开源框架Rx,Reactive Extensions。Rx是一个很Cool的框架,基于 Observable模式,应用广泛,感兴趣的读者可以去它的官网看看。还有这个网站也很棒,它提供了无数Rx的使用案例。Rx的最新版本可以在Nuget找到,它支持Xamarin的PCL环境,使用不会有任何问题。附赠使用案例,欢迎惠顾,请多多包涵。
Xaml:
<?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:ex="clr-namespace:Xamarin.Try.Portable.Extensions;assembly=Xamarin.Try.Portable" xmlns:viewModels="clr-namespace:Xamarin.Try.Portable.Extensions;assembly=Xamarin.Try.Portable" x:Class="Xamarin.Try.Portable.Views.TryEventBindingPageView"> <ContentPage.BindingContext> <viewModels:TryEventBindingPageViewModel /> </ContentPage.BindingContext> <ContentPage.Content> <StackLayout Orientation="Vertical"> <SearchBar Text="Search" ex:EventToCommand.FromEvent="TextChanged" ex.EventToCommand.ToCommand="{Binding SearchTextChangedCommand}" /> </StackLayout> </ContentPage.Content> </ContentPage>
namespace Xamarin.Try.Portable.ViewModels { public class TryEventBindingPageViewModel { private readonly ICommand _searchTextChangedCommand = new Command(SearchTextChangedCommandExecute); public ICommand SearchTextChangedCommand { get { return _searchTextChangedCommand; } } private void SearchTextChangedCommandExecute(object args) { // Do something... } } }