65.9K
CodeProject 正在变化。 阅读更多。
Home

Property Finder – 跨平台 Xamarin MonoTouch 移动应用

starIconstarIconstarIconstarIconstarIcon

5.00/5 (17投票s)

2013年1月2日

CPOL

19分钟阅读

viewsIcon

87289

downloadIcon

1184

了解 Xamarin MonoTouch 如何让您创建跨平台应用程序,针对 Windows Phone 使用原生的 C# / Silverlight,以及针对 iOS 通过 Xamarin MonoTouch 使用 C#。

目录 

摘要

几个月前,我发表了一篇文章,演示了如何使用 HTML5 技术(Knockout、Cordova / PhoneGap 和少量的 jQuery Mobile)来创建跨平台移动应用程序。结果证明,两个应用程序版本(Windows Phone 和 iPhone)之间可以共享大量的代码。然而,使用 HTML 代替原生 UI 会导致妥协,并最终影响用户体验。

在本文中,我使用原生的 C# / Silverlight(针对 Windows Phone)和 Xamarin MonoTouch(针对 iOS)实现了完全相同的跨平台应用程序。与 HTML5 版本相比,生成的跨平台应用程序可以共享相似数量的代码,但提供了完全原生的 UI。最终结果是用户体验没有任何妥协。

注意:自我开始撰写本文以来,项目的范围已经大大扩展。本文仍然侧重于使用 Xamarin 在 Windows Phone 和 iOS 应用程序之间共享 C# 代码库。然而,它已经催生了一个名为 PropertyCross 的更大项目,该项目比较了各种跨平台移动框架。该项目是开源的,托管在 github 上。如果您对跨平台开发感兴趣,并想了解 jQuery Mobile、Sencha Touch、Xamarin、Adobe AIR、Titanium 和传统的原生开发的优缺点,我建议您看一下!

您可以在下面查看应用程序两个不同版本的屏幕截图

Xamarin MonoTouch 简介

跨平台 HTML5 应用程序依赖于 JavaScript、HTML 和 CSS 这三种语言在所有浏览器(从桌面到移动设备再到平板电脑,如果您忽略兼容性问题的话!)之间的通用性。HTML5 移动应用程序在全屏浏览器控件中执行,UI 完全使用 HTML 构建。Xamarin 提供了一种截然不同的方法,其中 C# 代码库可以跨越一系列移动平台共享,利用每个平台的原生 API,尤其是 UI 框架。

Xamarin 的历史可以追溯到 2001 年,当时启动了开源的 Mono 项目。该项目旨在为 Linux 操作系统创建一个 Microsoft .NET 框架的实现。Mono 现在是一个成熟的项目,涵盖 C# 4.0 语言、核心 .NET API、Linq 等。

MonoTouch 利用 Mono 框架背后的技术,允许开发人员创建运行在 iPhone 和 iPad 上的 C# 和 .NET 应用程序。MonoTouch 应用程序被编译为直接针对 iPhone 的机器码,即没有虚拟机/CLR。应用程序作者可以使用大部分 .NET 框架,但他们也可以访问原生 iOS UI 组件,这些组件也作为 C# API 公开。

Xamarin 还为 Android 开发提供了类似的解决方案,该方案原生使用 Java。Mono for Android(以前称为 MonoDroid)创建在设备上的虚拟机中运行的代码。同样,原生 UI 组件也作为 C# API 公开。此应用程序的 Android 版本可在 PropertyCross 网站上找到。

开发环境 

MonoTouch 仍然依赖许多用于原生 iOS 开发的构建和部署工具,因此您需要一台 Mac 才能开发 MonoTouch 应用程序。所需工具和硬件的完整列表如下:
  • 一台 Mac 电脑
  • 一个 Apple Developer 账户(99 美元)
  • MonoDevelop – MonoTouch IDE
  • 一部 iPhone、iPod 或 iPad 用于测试
本文的主题是跨平台的 Windows Phone 和 iPhone 应用程序,不幸的是,上面描述的 Mac 环境不能用于 Windows Phone 开发。可以双启动 Mac 安装 Windows,或者使用 VMWare 运行 Windows 镜像,但两者都无法满足 Windows Phone 模拟器的图形硬件要求。此外,在开发跨平台应用程序时,能够快速修改共享代码库并为所有平台构建/测试应用程序是很有益的。因此,双启动 Mac 并不是一个非常实用的解决方案。

我正在使用的开发环境如下图所示

我有一台运行 Windows 8 的台式电脑,连接了两个显示器。旁边放着一台 MacBook Air,因为它的外观比 Dell PC 好看多了,所以它自豪地摆在我的桌子上!最左边的显示器连接到 Mac。

代码 resides 在 PC 硬盘上,该硬盘通过网络共享到 Mac,MonoDevelop 将文件链接到 MonoTouch 项目中的共享文件夹。

为了避免使用多个鼠标/键盘,我使用了一个名为 Synergy 的软件,它通过其客户端/服务器软件在多台计算机之间共享外围设备。这个工具几乎神奇地允许您将鼠标指针移出 PC 屏幕边缘到 Mac 屏幕上。您甚至可以在多台计算机之间共享剪贴板。唯一不允许的是将 Mac 窗口拖到 Windows 桌面! 

有了这个设置,我就可以在 Windows Phone 和 iPhone 代码库之间无缝切换,并且对公共核心的更改会立即在两者之间同步。

顺便说一下,Android 和 iOS 之间的跨平台开发不会那么复杂,因为 Android 开发使用 Eclipse IDE,它可以在 Mac 上很好地运行。

设计模式和 MVP

Windows Phone 开发的事实上的模式是 Model-View-ViewModel (MVVM) 模式,其中视图逻辑驻留在 ViewModel 中,并通过内置的绑定框架实现与“哑巴”视图的同步。

iOS API 缺乏绑定框架,因此 MVVM 模式不适合用于 iOS 或跨平台开发。虽然有适用于 iOS 的开源 MVVM 实现,但在这篇文章中,我不想引入更多框架,因此我将使用另一种 UI 设计模式,一种不依赖数据绑定的模式。

在我看来,MVVM 模式是最简单的 UI 设计模式之一。它包含三个组件:模型,包含应用程序数据和服务;视图模型,包含应用程序逻辑;以及视图,它渲染视图模型。

MVVM 模式的一个关键原则是视图应该尽可能“哑巴”,以最大限度地提高测试覆盖率。有关此方法的更多详细信息,请参阅 Martin Fowler 关于“被动视图”的文章。

因此,视图模型包含特定于视图的状态,例如按钮是否启用,甚至按钮的颜色。因此,视图模型通常被认为是“视图的模型”。

在 MVVM 模式下,视图模型持有对模型的引用,反之则不然。同样,视图持有对视图模型的引用(通常通过 DataContext 属性),反之则不然。这会产生一个简单的线性图

视图模型状态的更改通过绑定反映在视图中。

在没有绑定框架的情况下,没有自动机制可以将视图模型状态的更改传输到视图。在这种情况下,视图模型必须通过直接调用视图上的方法来“推送”更改。这就产生了一种称为 Model-View-Presenter (MVP) 的模式。

采用 MVP 模式非常简单,您可以像使用 MVVM 模式一样组织您的模型。主要区别在于需要编写的代码稍微多一些!

应用程序结构

在接下来的部分,我将并行介绍应用程序两个版本的实现;这反映了实际的开发过程,在实际开发过程中,同时开发两个版本是有意义的。

模型层

iOS 和 Windows Phone 应用程序共享的代码是模型和 Presenter 层。Property Finder 使用 Nestoria JSON API 查询其英国房产数据库。模型层的主要职责是隐藏此 Web 服务的细节,并提供一个 C# API 来查询房产。

我不会详细描述模型层,它相当无趣!相反,我将展示一些类,以便您了解该层的整体“形状”。

Nestoria API 允许您按文本搜索字符串或地理位置进行搜索。模型层有一个描述此服务的接口

public interface IJsonPropertySearch
{
  void FindProperties(string location, int pageNumber, Action<string> callback);
 
  void FindProperties(double latitude, double longitude, int pageNumber, Action<string> callback);
}
此接口的实现相当直接,上面每个方法都使用不同的查询 URL,因此我们只查看其中一个方法的实现
public void FindProperties(string location, int pageNumber, Action<string> callback, Action<Exception> error)
{
  var parameters = new Dictionary<string,object>(_commonParams);
  parameters.Add("place_name", location);
  parameters.Add("page", pageNumber);
  string url = "http://api.nestoria.co.uk/api?" + ToQueryString(parameters);
  ExecuteWebRequest(url, callback, error);
}
ToQueryString 是一个实用方法,它创建一个 URL,并使用提供的字典添加查询字符串参数。
private string ToQueryString(Dictionary<string, object> parameters)
{
  var items = parameters.Keys.Select(
    key => String.Format("{0}={1}", key, parameters[key].ToString())).ToArray();
 
  return String.Join("&", items);
}

ExecuteWebRequest 方法使用 WebClient 类执行异步请求。但这就是事情变得有点复杂的地方

private void ExecuteWebRequest (string url, Action<string> callback, Action<Exception> error)
{
  WebClient webClient = new WebClient();
      
  // create a timeout timer
  Timer timer = null;
  TimerCallback timerCallback = state =>
  {
    timer.Dispose();
    webClient.CancelAsync();
    error(new TimeoutException());
  };
  timer = new Timer(timerCallback, null, 5000, 5000);
      
  // create a web client
  webClient.DownloadStringCompleted += (s, e) =>
  {
    timer.Dispose();
    try
    {
      string result = e.Result;
      _marshal.Invoke(() => callback(result));
    }
    catch (Exception ex)
    {
      _marshal.Invoke(() => error(ex));
    }
  };
      
  webClient.DownloadStringAsync(new Uri(url));
} 

上面代码有几个地方与您可能编写的代码不同,如果不是与 MonoTouch 应用程序共享的话……

上面的代码使用计时器来“超时”运行时间过长的 Web 请求。在 Windows Phone 应用程序中,最容易使用的计时器是 DispatcherTimer,但这个类是 Silverlight 框架的一部分,MonoTouch 不支持。相反,上面的代码使用 System.Threading.Timer,它在 MonoTouch 中可用。有关各种 .NET 计时器的全面回顾,请参阅此 MSDN 文章

第二个区别是 WebClient 的使用。在 Windows 平台(Silverlight、WPF、WinForms)上使用时,Web 请求在后台线程上执行,以保持 UI 响应,但在下载完成后,DownloadStringCompleted 事件会在 UI 线程上触发——以便于使用。我通过反复试验发现,在 MonoTouch 中,DownloadStringCompleted 不会在 UI 线程上触发。iOS 具有与 Windows Phone 类似的线程模型,其中 UI 控件具有线程亲和性,这意味着您应该仅从 UI 线程更改其状态。

为了解决这个问题,上面的代码使用以下接口的实例

/// <summary>
/// A service which marshals invocations onto the UI thread.
/// </summary>
public interface IMarshalInvokeService
{
  void Invoke(Action action);
} 

Windows Phone 实现除了立即调用操作外,什么也不做……

public class MarshalInvokeService : IMarshalInvokeService
{
  public void Invoke(Action action)
  {
    // there is no need to marshal to the UI thread with Windows Phone
    action();
  }
} 

而 iOS 版本使用 NSObject.InvokeOnMainThread 方法将事件重新调度到 UI 线程

 public class MarshalInvokeService : IMarshalInvokeService
{
  private NSObject _obj = new NSObject();
 
  public MarshalInvokeService ()
  {
  }
 
  public void Invoke (Action action)
  {
    _obj.InvokeOnMainThread(() => action());
  }
} 

我将在后面更详细地介绍这种解决影响模型和 Presenter 层且使用“服务”的框架差异的方法。现在,值得注意的是,在 Windows Phone 和 iOS 之间共享此代码的要求对代码结构产生了微小影响。

因为这是一个 C# 应用程序(而不是 JavaScript),IJsonPropertySearch 返回的基于字符串的 JSON 数据并不是在应用程序层之间传递的最自然格式。PropertyDataSource 类是模型层的首要接口,负责将 JSON 数据转换为等效的 C# 表示。

public class PropertyDataSource
{
  private IJsonPropertySearch _jsonPropertySearch;
 
  public PropertyDataSource(IJsonPropertySearch jsonPropertySearch)
  {
    _jsonPropertySearch = jsonPropertySearch;
  }
 
  /// <summary>
  /// Find properties by geolocation
  /// </summary>
  public void FindProperties(double latitude, double longitude, int pageNumber, Action<PropertyDataSourceResult> callback)
  {
    _jsonPropertySearch.FindProperties(latitude, longitude, pageNumber,
      response => HandleResponse(response, callback));
  }
 
  /// <summary>
  /// Find properties by search-term
  /// </summary>
  public void FindProperties(string searchText, int pageNumber, Action<PropertyDataSourceResult> callback)
  {
    _jsonPropertySearch.FindProperties(searchText, pageNumber,
      response => HandleResponse(response, callback));
  }
 
  /// <summary>
  /// Handles the JSON response, and creates the required model objects 
  /// </summary>
  private void HandleResponse(string jsonResponse, Action<PropertyDataSourceResult> callback)
  {
    JObject json = JObject.Parse(jsonResponse);
 
    string responseCode = (string)json["response"]["application_response_code"];
 
    if (responseCode == "100" || /* one unambiguous location */
        responseCode == "101" || /* best guess location */
        responseCode == "110"  /* large location, 1000 matches max */)
    {
      var result = new PropertyListingsResult(json);
      callback(result);
 
    }
    else if (responseCode == "200" || /* ambiguous location */
              responseCode == "202" /* mis-spelled location */)
    {
      var result = new PropertyLocationsResult(json);
      callback(result);
    }
    else
    {
      /*
      201 - unknown location
      210 - coordinate error
      */
      callback(new PropertyUnknownLocationResult());
    };
  }
}

PropertyDataSource 返回的每个模型对象都将 JSON 响应作为构造函数参数,模型对象负责转换。例如,如果搜索词被识别并且返回了属性数组,则会构建一个 PropertyListingResult

public class PropertyListingsResult : PropertyDataSourceResult
{
  public PropertyListingsResult(JObject json)
  { 
    TotalResult = (int)json["response"]["total_results"];
    PageNumber = int.Parse((string)json["response"]["page"]);
    TotalPages = (int)json["response"]["total_pages"];
 
    Data = new List<Property>();
 
    JArray listings = (JArray)json["response"]["listings"];
    foreach (var listing in listings)
    {
      Data.Add(new Property(listing));
    }
  }
 
  public int TotalResult { get; private set; }
 
  public int PageNumber { get; private set; }
 
  public int TotalPages { get; private set; }
 
  public List<Property> Data { get; private set; }
} 

Property 模型对象执行类似的功能,从单个属性的 JSON 表示创建 C# 模型对象。

Nestoria JSON 响应非常冗长,包含许多 Property Finder 应用程序不需要的详细信息。.NET 框架内置支持 JSON 序列化,但这需要构建一个与整个 JSON 响应结构匹配的 C# 对象模型。相反,我选择使用流行的 Newtonsoft Json.NET 库,它提供了一个 Linq 风格的查询接口来处理 JSON 数据。这可以通过 NuGet 轻松添加到 Visual Studio 项目中。

由于 Json.NET 不是 Microsoft .NET 框架的一部分,因此 MonoTouch 开发人员无法使用它。幸运的是,有一个 Josn.NET 的 MonoTouch 版本可以在 github 上找到,这使得将其包含到应用程序的 iOS 版本中变得容易。

这就是模型层的内容! 

Presenter 层

在 MVP 模式中,Presenter 的作用与 MVVM 模式中的 ViewModel 相同。如果您熟悉 MVVM 模式,MVP 模式应该不会感到太陌生!

我们将从 PropertySearchPresenter 开始,它“支持”应用程序的主页。此屏幕为用户提供了一个界面,允许他们输入用于查询房产数据库的搜索词。

如果我们暂时忽略“我的位置”按钮,那么允许用户输入文本并单击“go”的 Presenter 就很简单了……

/// <summary>
/// A presenter for the front-page of this application. This presenter allows the
/// user to search by a text string or their current location.
/// </summary>
public class PropertyFinderPresenter
{
  /// <summary>
  /// The interface this presenter requires from the associated view.
  /// </summary>
  public interface View
  {
    /// <summary>
    /// Sets the text displayed in the search field.
    /// </summary>
    string SearchText { set; }
 
    /// <summary>
    /// Supplies a message to the user, typically to indicate an error or problem.
    /// </summary>
    void SetMessage(string message);
 
    /// <summary>
    /// Sets whether to display a loading indicator
    /// </summary>
    bool IsLoading { set; }
 
    event EventHandler SearchButtonClicked;
 
    event EventHandler<SearchTextChangedEventArgs> SearchTextChanged;
  }
 
  private View _view;
 
  private PropertyDataSource _propertyDataSource;
 
  private INavigationService _navigationService;
 
  private SearchItemBase _searchItem = new PlainTextSearchItem("");
 
  public PropertyFinderPresenter(
PropertyDataSource dataSource, INavigationService navigationService)
  {
    _propertyDataSource = dataSource;
    _navigationService = navigationService;
  }
 
  public void SetView(View view)
  {
    _view = view;
    _view.SearchButtonClicked += View_SearchButtonClicked;
    _view.SearchTextChanged += View_SearchTextChanged;
  }
    
  private void View_SearchTextChanged(object sender, SearchTextChangedEventArgs e)
  {
    if (e.Text != _searchItem.DisplayText)
    {
      _searchItem = new PlainTextSearchItem(e.Text);
    }
  }
 
  private void View_SearchButtonClicked(object sender, EventArgs e)
  {
    SearchForProperties();
  }
 
  private void SearchForProperties()
  {
    _view.IsLoading = true;
 
    _searchItem.FindProperties(_propertyDataSource, 1, response =>
    {
      if (response is PropertyListingsResult)
      {
        var propertiesResponse = (PropertyListingsResult)response;
        if (propertiesResponse.Data.Count == 0)
        {
          _view.SetMessage("There were no properties found for the given location.");
        }
        else
        {
          var listingsResponse = (PropertyListingsResult)response;
          _state.AddSearchToRecent(new RecentSearch(_searchItem, listingsResponse.TotalResult));
          var presenter = new SearchResultsPresenter(_navigationService, _state, listingsResponse,
                                                      _searchItem, _propertyDataSource);
          _navigationService.PushPresenter(presenter);
        }
      }
      else
      {
        _view.SetMessage("The location given was not recognised.");
      }
 
      _view.IsLoading = false;
    });
  }
}

此屏幕的视图模型可能会公开一个 SearchString 属性和一个 GoButtonClickedCommand,这些属性将使用 XAML 绑定到 UI。对于 PropertyFinderPresenter,这些被一个内部接口 PropertyFinderPresenter.View 所取代——注意,这当然不必是内部接口,但我的感觉是它与 Presenter 如此紧密耦合,以至于将其定义为此很有意义。

PropertyFinderPresenter.View 具有只写属性,即它们有 setter 但没有 getter,这允许 Presenter 通知视图它处于加载状态,或者设置搜索文本字段。此接口还定义了视图必须实现的事件,以便通知 Presenter 用户交互。

PropertyFinderPresenter 有一个 SetView 方法,该方法设置其 _view 字段(Presenter 只有一个视图),并添加对各种事件的处理程序。之后,一切都很直接。

您可能已经注意到 INavigationService,稍后将详细介绍……

View 层

应用程序的 iOS 和 Windows Phone 版本的视图层差异很大。我预计本文的大多数读者已经熟悉 Windows Phone(或 Silverlight)开发,因此唯一的真正区别是 MVVM 和 MVP 之间的区别。对于 iPhone,Xamarin 开发感觉非常接近原生 iOS 开发,应用程序有 AppDelegate,使用视图控制器,并且 UI 是使用 Xcode 的 Interface Builder 构建的。在本节中,我们将快速查看应用程序每个版本的 PropertyFinderPresenter 视图。

Windows Phone

对于应用程序的 Windows Phone 版本,相应的 PropertyFinderView 实现为一个常规页面,XAML 如下所示:

<Grid Margin="10">
  <Grid.RowDefinitions>
    ...
  </Grid.RowDefinitions>
 
  <TextBlock Text="UK Property Finder"
              FontSize="{StaticResource PhoneFontSizeLarge}"
              Margin="0,30,0,0"/>
 
  <TextBlock Text="Use the form below to search for houses ..."
              Grid.Row="1"
              TextWrapping="Wrap"
              Margin="0,30,0,0"/>
    
  <StackPanel Orientation="Horizontal"
              Grid.Row="2"
              Margin="0,10,0,0">
    <TextBox x:Name="searchText"
              Width="200"
              TextChanged="SearchText_TextChanged"/>
    <Button x:Name="buttonSearchGo"
            Content="Go"
            Click="ButtonSearchGo_Click"/>
    <Button x:Name="buttonMyLocation"
            Content="My location"
            Click="ButtonMyLocation_Click"/>
  </StackPanel>
      
  <ProgressBar x:Name="loadingIndicator"
                Grid.Row="3"
                IsIndeterminate="True"
                Visibility="Collapsed"/>
      
  <TextBlock x:Name="userMessage"
              Grid.Row="4"
              Margin="0,20,0,20"/>
</Grid> 

请注意,没有 XAML 绑定,并且元素都有一个 x:Name 属性以及各种事件处理程序。

此页面的代码隐藏实现了 PropertyFinderPresenter.View 接口,设置 UI 元素的状态并在事件发生时执行所需逻辑。Presenter 也在 OnNavigatedTo 方法中构建,视图设置为“this”。

public partial class PropertyFinderView : PhoneApplicationPage, PropertyFinderPresenter.View
{
  // Constructor
  public PropertyFnderView()
  {
    InitializeComponent();
  }
 
  protected override void OnNavigatedTo(NavigationEventArgs e)
  {
    base.OnNavigatedTo(e);
 
    if (e.NavigationMode != NavigationMode.Back)
    {
      var source = new PropertyDataSource(new JsonWebPropertySearch());
 
      var presenter = new PropertyFinderPresenter(source,
        new NavigationService(NavigationService));
      presenter.SetView(this);
    }
  }
 
#region PropertyFinderPresenter.View implementation
 

  public string SearchText
  {
    set
    {
      searchText.Text = value;
    }
  }
 
  public event EventHandler SearchButtonClicked = delegate { };
 
  public event EventHandler<SearchTextChangedEventArgs> SearchTextChanged = delegate { };
 
  public void SetMessage(string message)
  {
    userMessage.Text = message;
  }
 
  public bool IsLoading
  {
    set
    {
      loadingIndicator.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
      searchText.IsEnabled = !value;
      buttonMyLocation.IsEnabled = !value;
      buttonSearchGo.IsEnabled = !value;
    }
  }
 
#endregion
 
#region UI event handlers
 
  private void ButtonSearchGo_Click(object sender, RoutedEventArgs e)
  {
    SearchButtonClicked(this, EventArgs.Empty);
  }
 
  private void ButtonMyLocation_Click(object sender, RoutedEventArgs e)
  {
    MyLocationButtonClicked(this, EventArgs.Empty);
  }
 
  private void SearchText_TextChanged(object sender, TextChangedEventArgs e)
  {
    SearchTextChanged(this, new SearchTextChangedEventArgs(searchText.Text));
  }
 
#endregion
 
} 

我能听到 MVVM 纯粹主义者因代码隐藏的存在而愤怒地跳起来!

MVVM 模式的主要原则是 (1) 提高可测试性,(2) 促进开发人员/设计人员工作流程(即支持 Blend)。我个人对使此应用程序可 Blend 并不太感兴趣,但我关心编写可测试的代码。测试上面的 Presenter 只需要单元测试为 View 接口提供一个 mock。除此之外,它的工作方式与视图模型的单元测试相同,换句话说,您可以使用 MVP 设计模式完全测试应用程序,而无需提供“真实”视图(纯粹主义者可以停止跳了!)。

iOS

对于 iOS 开发,您大部分时间都在 MonoDevelop 中编写 C# 代码,这是一个感觉非常像 Visual Studio 的环境。但是,对于 UI 开发,您必须进入 Xcode 的世界,甚至必须创建一个 Objective-C 头文件!

主页的 iOS 视图是使用 Xcode Interface Builder 构建的,如下所示:

在 Windows Phone 开发中,会为希望在代码隐藏中生成字段的元素添加 x:Name 属性。通过 Interface Builder,您可以在相应的头文件中通过拖放方式创建“outlets”。

MonoDevelop 会自动生成一个 C# 部分类,其中包含对您在 Objective-C 头文件中创建了 outlets 的控件的引用。这听起来像是 Xcode 和 MonoDevelop 之间有些混乱的集成,但我确实发现它很可靠。然而,这确实突显了 Xamarin iOS 开发人员需要对 iOS 开发有相当的了解。

主页的视图控制器执行的任务与相应的 Windows Phone 页面相似,实现了 Presenter 所需的“视图”接口。

public partial class PropertyFinderViewController : UIViewController, PropertyFinderPresenter.View
{
  private PropertyFinderPresenter _presenter;
 
  public PropertyFinderViewController (PropertyFinderPresenter presenter)
    : base ("PropertyFinderViewController", null)
  {
    Title = "PropertyCross";
 
    _presenter = presenter;
  }
 
  public override void ViewDidLoad ()
  {
    base.ViewDidLoad ();
 
    // initial UI state
    searchActivityIndicator.Hidden = true;
    tableView.Hidden = true;
    tableView.SeparatorColor = UIColor.Clear;
 
    // handle the enter key, hiding the keyboard and initiating a search
    var enterDelegate = new CatchEnterDelegate();
    enterDelegate.EnterClicked += (s, e) => SearchButtonClicked(this, e);
    searchLocationText.Delegate = enterDelegate;
 
    // set the back button text
    NavigationItem.BackBarButtonItem = new UIBarButtonItem("Search",
                          UIBarButtonItemStyle.Bordered, BackButtonEventHandler);
 

    // associate with the presenter
    _presenter.SetView (this);
  }
 
  private void FavouriteButtonEventHandler (object sender, EventArgs args)
  {
    FavouritesClicked(this, EventArgs.Empty);
  }
 
  private void BackButtonEventHandler (object sender, EventArgs args)
  {
    NavigationController.PopViewControllerAnimated(true);
  }
    
  public override void ViewDidUnload ()
  {
    base.ViewDidUnload ();
 
    ReleaseDesignerOutlets ();
  }
    
 
  #region PropertyFinderPresenter.View implementation 
 
  public string SearchText
  {
    set
    {
      searchLocationText.Text = value;
    }
  }
 
  public event EventHandler SearchButtonClicked = delegate { };
 
  public event EventHandler<SearchTextChangedEventArgs> SearchTextChanged = delegate { };
 

  public void SetMessage (string message)
  {
    userMessageLabel.Text = message;
  }
 
  public bool IsLoading
  {
    set
    {
      searchActivityIndicator.Hidden = !value;
      if (value)
      {
        searchActivityIndicator.StartAnimating();
      }
      else
      {
        searchActivityIndicator.StopAnimating();
      }
      goButton.Enabled = !value;
      myLocationButton.Enabled = !value;
      searchLocationText.Enabled = !value;
    }
  }
 
  #endregion
 
  partial void myLocationButtonTouched (NSObject sender)
  {
    MyLocationButtonClicked(this, EventArgs.Empty);
  }
 
  partial void goButtonTouched (NSObject sender)
  {
    // hide the keyboard
    searchLocationText.ResignFirstResponder();
 
    SearchButtonClicked(this, EventArgs.Empty);
  }
 
  partial void searchLocationTextChanged (NSObject sender)
  {
    SearchTextChanged(this, new SearchTextChangedEventArgs(searchLocationText.Text));
  }
 
  public class CatchEnterDelegate : UITextFieldDelegate
  {
    public override bool ShouldReturn (UITextField textField)
    {
      textField.ResignFirstResponder ();
      EnterClicked(this, EventArgs.Empty);
      return true;
    }
 
    public event EventHandler EnterClicked = delegate {};
  }
 
  // ...
}

上面的一些代码可能看起来有点陌生,UIColorsUITextFieldDelegate 等……但我相信您会发现它相当易读,毕竟都是 C#!

应用程序的所有其他 Presenter/View 都遵循类似的模式,因此我将不详细介绍它们。两个版本差异显著的其他方面是列表的渲染,Windows Phone 应用程序使用 ItemsControl 进行集合绑定,而 iOS 版本使用带有 dataSourceUITableView。两种截然不同的方法,但完全支持这里的 MVP 模式。有关这些主题的介绍,您可能想阅读我早些时候介绍 MonoTouch 的博客文章。

服务

PropertyFinderPresenterPropertyDataSource(模型层暴露的“接口”)以及它所需的各种其他“服务”作为构造函数参数。例如导航服务

/// <summary>
/// A service which provides navigation from page to page.
/// </summary>
public interface INavigationService
{
  /// <summary>
  /// Navigates to a view for the given presenter.
  /// </summary>
  void PushPresenter(object presenter);
}

Presenter 需要执行许多功能,例如导航、访问地理位置和状态持久化,而这些功能无法由核心 .NET API 提供。例如,iOS 的导航 API(由 MonoTouch API 公开)与 Windows Phone 的导航 API 有很大不同。

为了让 Presenter 执行导航(或使用地理位置、或状态持久化),我们需要一个抽象层。所需的服务可以通过接口提供给 Presenter,并为 Windows Phone 和 iOS 提供特定于平台的实现。

这导致了应用程序的整体架构如下

模型和 Presenter 层在所有平台之间共享,任何特定于平台的特性或功能都作为服务提供给 Presenter 层。视图层是每个平台独有的。

上面显示的 INavigationService 接口利用了 Windows Phone 和 iOS 都采用“堆栈”式导航方法的事实。Windows Phone 实现使用了特定于平台的 System.Windows.Navigation.NavigationService,将 Presenter 映射到页面。

using WPNavigationService = System.Windows.Navigation.NavigationService;
 
public class NavigationService : INavigationService
{
  private WPNavigationService _navigationService;
 
  public NavigationService(WPNavigationService navigationService)
  {
    _navigationService = navigationService;
  }
 
  public void PushPresenter(object presenter)
  {
    App.Instance.CurrentPresenter = presenter;
 
    if (presenter is SearchResultsPresenter)
    {
      _navigationService.Navigate(new Uri("/SearchResultsView.xaml", UriKind.Relative));
    }
    else if (presenter is PropertyPresenter)
    {
      _navigationService.Navigate(new Uri("/PropertyView.xaml", UriKind.Relative));
    }
    else if (presenter is FavouritesPresenter)
    {
      _navigationService.Navigate(new Uri("/FavouritesView.xaml", UriKind.Relative));
    }
  }
}<span style="font-size: 14px; white-space: normal; ">
</span>

而 iOS 版本创建了所需的视图控制器实例,这些实例被“推”到提供的 UINavigationController 实例。

public class NavigationService : INavigationService
{
 
  private UINavigationController _navigationController;
 
    public NavigationService (UINavigationController navigationController)
    {
    _navigationController = navigationController;
    }
 
    #region INavigationService implementation
 
    public void PushPresenter (object presenter)
  {
    if (presenter is SearchResultsPresenter)
    {
      var viewController = new SearchResultsViewController(presenter as SearchResultsPresenter);
      _navigationController.PushViewController(viewController, true);
    }
 
    if (presenter is PropertyPresenter)
    {
      var viewController = new PropertyViewController(presenter as PropertyPresenter);
      _navigationController.PushViewController(viewController, true);
    }
 
    if (presenter is FavouritesPresenter)
    {
      var viewController = new FavouritesViewController(presenter as FavouritesPresenter);
      _navigationController.PushViewController(viewController, true);
    }
  }
 
  #endregion
 
} 

状态持久化和地理位置也采用相同的方法。

该应用程序有许多 Presenter 和 View 来渲染不同的应用程序屏幕。我不会一一详细介绍,它们几乎都遵循相同的模式。完整的源代码包含在本文章中——所以为什么不看一看呢?

结论

我写的那篇关于此应用程序的 HTML5 等效版本的文章有一个大篇幅介绍了用户体验,详细说明了由于选择此实现技术而做出的妥协。

对于这篇文章,没有这样的部分!使用 Xamarin,应用程序界面完全是原生的,提供了与等效原生应用程序相同的体验。

关于代码共享,使用 HTML5 或 Xamarin 等技术进行移动应用程序开发的主要驱动力之一是它允许代码共享。对于此应用程序的 HTML5 版本,大约 71% 的应用程序代码(即 JavaScript)在 Windows Phone 和 iOS 版本之间共享。

剩余的 29% 是特定于操作系统的,例如为 Windows Phone 版本添加后退按钮和应用程序栏支持。HTML5 具有增加共享代码量的潜力,如果您愿意创建一个在所有平台上看起来几乎相同的应用程序,忽略平台差异(即 Metro、Roboto、Apple 风格)。

使用 Xamarin,共享代码量有所下降

这是可以预期的,Windows Phone 和 iOS 的 UI 框架差异很大,这意味着视图层代码无法共享。有趣的是,大约三分之二的特定于操作系统的代码位于 iOS 版本中。这是因为 Windows Phone 的 UI 框架更加“高级”和强大,而 iOS 感觉更低级——您基本上需要编写更多代码。这并不一定意味着 Windows Phone 是一个更好的平台,我发现 iOS 开发的低级特性比我在 Windows Phone 上遇到过的问题更少。

HTML5 和 Xamarin 提供的开发体验截然不同。使用 HTML5,开发几乎是标准的 Web 开发,使用浏览器进行测试,以及您喜欢的编辑器或工具集。使用 Xamarin,事情会更复杂,需要一台 Mac 和一台 PC,以及对每个原生平台的了解。因此,尽管使用 C#,但我认为 Xamarin 在学习开发过程方面需要更大的前期投入。然而,这种投资小于学习原生 iOS 开发(涉及 Objective-C)所需的投资。

我在上一篇文章的结尾提供了一些要点,总结了我对跨平台开发的看法。我将在这里重复其中一些要点,并提供一些关于 Xamarin 在何处是合适技术的更详细信息。

  • HTML5 是一种可行的跨平台移动应用程序开发技术。不要被技术媒体上的一些文章所迷惑,它们声称“HMTL5 将在两年内准备就绪”。它现在就可以使用了!您可以编写共享大量代码的跨平台移动应用程序。
  • HTML5 是一条妥协之路。我之前的文章清楚地表明,HTML5 用户界面无法与原生应用程序相媲美。如果您选择 HTML5,请确保您了解并愿意接受这些妥协。
  • 避免使用 HTML5 来匹配原生外观和感觉。在我看来,如果您不尝试匹配每个平台的原生外观和感觉,您将最大限度地利用 HTML5。相反,创建一个具有自身视觉识别度的应用程序,并在所有平台上使用它。这将最大限度地提高每个平台之间的代码共享。
  • HTML5 不是唯一的跨平台技术,要实现无妥协的方法,请尝试 Xamarin!我个人觉得技术行业对 HTML5 的执着有点令人厌倦。对于跨平台移动应用程序开发,还有其他成熟的替代方案,在我看来,Xamarin 显然是一个可行的替代方案。

您可以在此处下载此应用程序的源代码: PropertyFinder.zip - 或者访问 github 项目以获取最新代码。 

为了扩展我上面最后一点,如果您想探索更多替代方案,包括 jQuery Mobile、Sencha Touch、Xamarin、Adobe AIR、Titanium 和原生开发——请查看 PropertyCross

© . All rights reserved.