原文:
zh.annas-archive.org/md5/2E6DA4A6D245D14BD719EE0F1D9AAED3
译者:飞龙
第五章:移动 Web 应用
在上一章中,我们看到了一个用于在 Windows 商店分发的本机桌面应用程序的创建。在本章中,我们将创建一个 Web 应用程序,让用户登录,并在地图上看到与自己在同一物理区域的其他用户。我们将使用以下技术:
-
ASP.NET MVC 4:这让你可以使用模型-视图-控制器设计模式和异步编程构建 Web 应用程序
-
SignalR:这是一个异步的双向通信框架
-
HTML5 GeoLocation:为应用程序提供真实世界的位置
-
使用 Google 进行客户端地图映射:这是为了可视化地理空间信息
这些技术共同让你创建非常强大的 Web 应用程序,并且借助与 C# 5 一同发布的 ASP.NET MVC 4,现在更容易创建可以轻松访问互联网的移动应用程序。在本章结束时,我们将拥有一个 Web 应用程序,它使用现代浏览器功能,如 WebSockets,让你与其他在你附近的 Web 用户连接。所有这些都使选择 C#技术栈成为创建 Web 应用程序的一个非常引人注目的选择。
使用 ASP.NET MVC 的移动 Web
ASP.NET 已经发展成为支持多种不同产品的服务器平台。在 Web 端,我们有 Web Forms 和 MVC。在服务端,我们有 ASMX Web 服务、Windows 通信框架(WCF)和 Web 服务,甚至一些开源技术,如 ServiceStack 也已经出现。
Web 开发可以被总结为技术的大熔炉。成功的 Web 开发人员应该精通 HTML、CSS、JavaScript 和 HTTP 协议。在这个意义上,Web 开发可以帮助你成为一名多语言程序员,可以在多种编程语言中工作。我们将在这个项目中使用 ASP.NET MVC,因为它在 Web 开发的背景下应用了模型-视图-控制器设计模式,同时允许每个贡献的技术有机会发挥其所长。它在下图中显示:
你的模型块将包含所有包含业务逻辑的代码,以及连接到远程服务和数据库的代码。控制器块将从模型层检索信息,并在用户与视图块交互时将信息传递给它。
关于使用 JavaScript 进行客户端开发的有趣观察是,许多应用程序的架构选择与开发任何其他本机应用程序时非常相似。从在内存中维护应用程序状态的方式到访问和缓存远程信息的方式,有许多相似之处。
构建一个 MeatSpace 跟踪器
接下来是我们要构建的应用程序!
正如术语CyberSpace指的是数字领域一样,术语MeatSpace在口语中用来指代在现实世界中发生的事物或互动。我们将在本章中创建的项目是一个移动应用程序,可以帮助你与 Web 应用程序的其他用户在你附近的物理位置进行连接。构建一个在真实世界中知道你位置的移动网站的对比非常吸引人,因为就在短短几年前,这类应用程序在 Web 上是不可能的。
这个应用程序将使用 HTML 5 地理位置 API 来让你在地图上看到应用程序的其他用户。当用户连接时,它将使用 SignalR 与服务器建立持久连接,这是一个由几名微软员工发起的开源项目。
迭代零
在我们开始编写代码之前,我们必须启动项目,迭代零。我们首先创建一个新的 ASP.NET MVC 4 项目,如下截图所示。在这个例子中,我正在使用 Visual Studio 2012 Express for Web,当然,完整版本的 Visual Studio 2012 也可以使用。
一旦选择了 MVC 4 项目,就会出现一个对话框,其中包含几种不同类型的项目模板。由于我们希望我们的 Web 应用程序可以从手机访问,所以我们选择 Visual Studio 2012 中包含的新项目模板之一,Mobile Application。该模板预装了一些有用的 JavaScript 库,列举如下:
-
jQuery和jQuery.UI:这是一个非常流行的库,用于简化对 HTML DOM 的访问。该库的 UI 部分提供了一个漂亮的小部件工具包,可在各种浏览器上使用,包括日期选择器等控件。
-
jQuery.Mobile:这提供了一个框架,用于创建移动友好的 Web 应用程序。
-
KnockoutJS:这是一个 JavaScript 绑定框架,可以让您实现 Model-View-ViewModel 模式。
-
Modernizr:这允许您进行丰富的功能检测,而不是查看浏览器的用户代理字符串来确定您可以依赖的功能。
我们将不会使用所有这些库,当然,您也可以选择不同的 JavaScript 库。但这些提供了一个方便的起点。您应该花一些时间熟悉项目模板创建的文件。
您应该首先查看主HomeController
类,因为这是(默认情况下)应用程序的入口点。默认情况下包含一些占位文本;您可以轻松更改此文本以适应您正在构建的应用程序。对于我们的目的,我们只需更改一些文本,以充当简单的信息,并鼓励用户注册。
修改Views/Home/Index.cshtml
文件如下:
<h2>@ViewBag.Message</h2>
<p>
Find like-minded individuals with JoinUp
</p>
注意@ViewBag.Message
标题,您可以按照以下方式更改HomeController
类的Index
操作方法中的特定值:
public ActionResult Index()
{
ViewBag.Message = "MeetUp. TalkUp. JoinUp";
return View();
}
还有其他视图,您可以更改以添加自己的信息,例如关于和联系页面,但对于这个特定的演示目的来说,它们并不是关键的。
进行异步操作
ASP.NET MVC 的最新版本中最强大的新增功能之一是能够使用 C# 5 中的新async
和await
关键字编写异步操作方法。要清楚,自 ASP.NET MVC 2 以来,您就已经有了创建异步操作方法的能力,但它们相当笨拙且难以使用。
您必须手动跟踪正在进行的异步操作的数量,然后让异步控制器知道它们何时完成,以便它可以完成响应。在 ASP.NET MVC 4 中,这不再是必要的。
例如,我们可以重写我们在上一节中讨论的Index
方法,使其成为异步的。假设我们希望在登陆页面的标题中打印的消息来自数据库。因为这可能需要与另一台机器上的数据库服务器通信,所以这是一个完美的异步方法候选者。
首先,创建一个可等待的方法,用作从数据库中检索消息的占位符,如下所示:
private async Task<string> GetSiteMessage()
{
await Task.Delay(1);
return "MeetUp. TalkUp. JoinUp";
}
当然,在您的实际代码中,这将连接到数据库,例如,它只是在返回字符串之前引入了一个非常小的延迟。现在,您可以按照以下方式重写Index
方法:
public async Task<ActionResult> Index()
{
ViewBag.Message = await GetSiteMessage();
return View();
}
您可以看到在先前代码中突出显示的方法的更改,您只需向方法添加async
关键字,将返回值设置为Task<ActionResult>
类,然后在方法体中使用await
。就是这样!现在,您的方法将允许 ASP.NET 运行时通过处理其他请求来最大程度地优化其资源,同时等待您的方法完成处理。
获取用户位置
一旦我们定义了初始着陆页面,我们就可以开始查看已登录的界面。请记住,我们应用程序的明确目标是帮助您在现实世界中与其他用户建立联系。为此,我们将使用包括移动浏览器在内的许多现代浏览器中包含的一个功能,以检索用户的位置。为了将所有人连接在一起,我们还将使用一个名为SignalR的库,它可以让您与用户的浏览器建立双向通信渠道。
该项目的网站简单地描述如下:
.NET 的异步库,用于帮助构建实时的、多用户交互式的 Web 应用程序。
使用 SignalR,您可以编写一个应用程序,让您可以双向与用户的浏览器进行通信。因此,您不必等待浏览器与服务器发起通信,实际上您可以从服务器调用并向浏览器发送信息。有趣的是,SignalR 是开源的,因此您可以深入了解其实现。但是对于我们的目的,我们将首先向我们的 Web 应用程序添加一个引用。您可以通过 Nuget 轻松实现这一点,只需在包管理控制台中运行以下命令:
install-package signalr
或者,如果您更喜欢使用 GUI 工具,可以右键单击项目的引用节点,然后选择管理 NuGet 包。从那里,您可以搜索 SignalR 包并单击安装按钮。
安装了该依赖项后,我们可以开始勾画用户在登录时将看到的界面,并为我们提供应用程序的主要功能。我们通过使用Empty MVC Controller
模板向Controllers
文件夹添加一个新的控制器来开始添加新屏幕的过程。将类命名为MapController
,如下所示:
public class MapController : Controller
{
public ActionResult Index()
{
return View();
}
}
默认情况下,您创建的文件将与先前代码中的文件相似;请注意控制器前缀(Map
)和操作方法名称(Index
)。创建控制器后,您可以添加视图,根据约定,使用控制器名称和操作方法名称。
首先,在Views
文件夹中添加一个名为Map
的文件夹,所有此控制器的视图都将放在这里。在该文件夹中,添加一个名为Index.cshtml
的视图。确保选择Razor
视图引擎,如果尚未选择。生成的 razor 文件非常简单,它只是设置页面的标题(使用 razor 代码块),然后输出一个带有操作名称的标题,如下所示:
@{
ViewBag.Title = "JoinUp Map";
}
<h2>Index</h2>
现在我们可以开始修改此视图并添加地理位置功能。将以下代码块添加到Views/map/Index.cshtml
的底部:
@section scripts {
@Scripts.Render("~/Scripts/map.js")
}
此脚本部分在站点范围模板中定义,并确保以正确的顺序呈现脚本引用,以便所有其他主要依赖项(例如 jQuery)已被引用。
接下来,我们创建了在先前代码中引用的map.js
文件,其中将保存我们所有的 JavaScript 代码。在我们的应用程序中,首先要做的是让我们的地理位置工作起来。将以下代码添加到map.js
中,以了解如何获取用户的位置:
$(function () {
var geo = navigator.geolocation;
if (geo) {
geo.getCurrentPosition(userAccepted, userDenied);
} else {
userDenied({message:'not supported'});
}
});
这从一个传递给 jQuery 的函数定义开始,当 DOM 加载完成时将执行该函数。在该方法中,我们获取对navigator.geolocation
属性的引用。如果该对象存在(例如,浏览器实现了地理位置),那么我们调用.getCurrentPosition
方法并传入两个我们定义的回调函数,如下所示:
function userAccepted(pos) {
alert("lat: " +
pos.coords.latitude +
", lon: " +
pos.coords.longitude);
}
function userDenied(msg) {
alert(msg.message);
}
保存了带有上述代码的map.js
后,您可以运行 Web 应用程序(F5)以查看其行为。如下截图所示,用户将被提示是否要允许 Web 应用程序跟踪他们的位置。如果他们点击允许,将执行userAccepted
方法。如果他们点击拒绝,将执行userDenied
消息。当未提供位置时,您可以使用此方法来相应地调整应用程序。
使用 SignalR 进行广播
用户的位置确定后,接下来的过程将涉及使用 SignalR 将每个连接的用户的位置广播给其他每个用户。
我们可以做的第一件事是通过在Views/Map/Index.cshtml
的脚本引用中添加以下两行来为 SignalR 添加脚本引用:
<ul id="messages"></ul>
@section scripts {
@Scripts.Render("~/Scripts/jquery.signalR-0.5.3.min.js")
@Scripts.Render("~/signalr/hubs")
@Scripts.Render("~/Scripts/map.js")
}
这将初始化 SignalR 基础设施,并允许我们在实现服务器之前构建应用程序的客户端部分。
提示
在撰写本文时,jQuery.signalR
库的版本 0.5.3 是最新版本。根据您阅读本书的时间,这个版本很可能已经改变。只需在通过 Nuget 添加 SignalR 依赖项后查看Scripts
目录,以查看您应该在此处使用哪个版本。
接下来,删除map.js
类的所有先前内容。为了保持组织,我们首先声明一个 JavaScript 类,其中包含一些方法,如下所示:
var app = {
geoAccepted: function(pos) {
var coord = JSON.stringify(pos.coords);
app.server.notifyNewPosition(coord);
},
initializeLocation: function() {
var geo = navigator.geolocation;
if (geo) {
geo.getCurrentPosition(this.geoAccepted);
} else {
error('not supported');
}
},
onNewPosition: function(name, coord) {
var pos = JSON.parse(coord);
$('#messages').append('<li>' + name + ', at '+ pos.latitude +', '+ pos.longitude +'</li>');
}
};
您将认出initializeLocation
方法,它与我们先前在其中初始化地理位置 API 的代码相同。在此版本中,初始化函数传递了另一个函数geoAccepted
,作为用户接受位置提示时执行的回调。最终函数onNewPosition
旨在在有人通知服务器有新位置时执行。SignalR 将广播位置并执行此函数,以让此脚本知道用户的名称和他们的新坐标。
页面加载时,我们希望初始化与 SignalR 的连接,并在此过程中使用我们刚刚在名为app
的变量中创建的对象,可以按如下方式完成:
$(function () {
var server = $.connection.serverHub;
server.onNewPosition = app.onNewPosition;
app.server = server;
$.connection.hub.start()
.done(function () {
app.initializeLocation();
});
});
Hubs,在 SignalR 中,是一种非常简单的方式,可以轻松地由客户端的 JavaScript 代码调用方法。在Models
文件夹中添加一个名为ServerHub
的新类,如下所示:
public class ServerHub : Hub
{
public void notifyNewPosition(string coord)
{
string name = HttpContext.Current.User.Identity.Name;
Clients.onNewPosition(name, coord);
}
}
在此 hub 中定义了一个方法notifyNewPosition
,它接受一个字符串。当我们从用户那里获得坐标时,此方法将将其广播给所有其他连接的用户。为此,代码首先获取用户的名称,然后调用.onNewPosition
方法将名称和坐标与所有连接的用户一起广播。
有趣的是,Clients
属性是一个动态类型,因此onNewPosition
实际上并不存在于该属性的方法中。该方法的名称用于自动生成从 JavaScript 代码调用的客户端方法。
为了确保用户在访问页面时已登录,我们只需在MapController
类的顶部添加[Authorize]
属性,如下所示:
[Authorize]
public class MapController : Controller
按下F5运行您的应用程序,看看我们的进展如何。如果一切正常,您将看到如下截图所示的屏幕:
当人们加入网站时,他们的位置被获取并推送给其他人。同时,在客户端,当收到新的位置时,我们会添加一个新的列表项元素,详细说明刚刚收到的名称和坐标。
我们正在逐步逐一地构建我们的功能,一旦我们验证了这一点,我们就可以开始完善下一个部分。
映射用户
随着位置信息被推送给每个人,我们可以开始在地图上显示他们的位置。对于这个示例,我们将使用 Google Maps,但您也可以轻松地使用 Bing、Nokia 或 OpenStreet 地图。但是,这个想法是为您提供一个空间参考,以查看谁还在查看相同的网页,以及他们相对于您在世界上的位置。
首先,在Views/Map/Index.cshtml
中添加一个 HTML 元素来保存地图,如下所示:
<div
id="map"
style="width:100%; height: 200px;">
</div>
这个<div>
将作为实际地图的容器,并将由 Google Maps API 管理。接下来在map.js
引用上面的脚本部分添加 JavaScript,如下所示:
@section scripts {
@Scripts.Render("~/Scripts/jquery.signalR-0.5.3.min.js")
@Scripts.Render("~/signalr/hubs")
@Scripts.Render("http://maps.google.com/maps/api/js?sensor=false");
@Scripts.Render("~/Scripts/map.js")
}
与 SignalR 脚本一样,我们只需要确保它在我们自己的脚本(map.js
)之前被引用,以便在我们的源中可用。接下来,我们添加代码来初始化地图,如下所示:
function initMap(coord) {
var googleCoord = new google.maps.LatLng(coord.latitude, coord.longitude);
if (!app.map) {
var mapElement = document.getElementById("map");
var map = new google.maps.Map(mapElement, {
zoom: 15,
center: googleCoord,
mapTypeControl: false,
navigationControlOptions: { style: google.maps.NavigationControlStyle.SMALL },
mapTypeId: google.maps.MapTypeId.ROADMAP
});
app.map = map;
}
else {
app.map.setCenter(googleCoord);
}
}
当获取位置时,将调用此函数。它通过获取用户最初报告的位置,并将对map
ID 的<div>
HTML 元素的引用传递给google.maps.Map
对象的新实例,将地图的中心设置为用户报告的位置。如果再次调用该函数,它将简单地将地图的中心设置为用户的坐标。
为了显示所有位置,我们将使用 Google Maps 的一个功能来在地图上放置一个标记。将以下函数添加到map.js
中:
function addMarker(name, coord) {
var googleCoord = new google.maps.LatLng(coord.latitude, coord.longitude);
if (!app.markers) app.markers = {};
if (!app.markers[name]) {
var marker = new google.maps.Marker({
position: googleCoord,
map: app.map,
title: name
});
app.markers[name] = marker;
}
else {
app.markers[name].setPosition(googleCoord);
}
}
这个方法通过使用一个关联的 JavaScript 数组来跟踪已添加的标记,类似于 C#中的Dictionary<string, object>
集合。当用户报告新位置时,它将获取现有的标记并将其移动到新位置。这意味着,对于每个登录的唯一用户,地图将显示一个标记,然后每次报告新位置时都会移动它。
最后,我们对应用对象中的现有函数进行了三个小的更改,以便与地图进行交互。首先在initializeLocation
中,我们从getCurrentPosition
更改为使用watchPosition
方法,如下所示:
initializeLocation: function() {
var geo = navigator.geolocation;
if (geo) {
geo.watchPosition(this.geoAccepted);
} else {
error('not supported');
}
},
watchPosition
方法将在用户位置发生变化时更新用户的位置,这应该导致所有位置的实时视图,因为它们将其报告给服务器。
接下来,我们更新geoAccepted
方法,该方法在用户获得新坐标时运行。我们可以利用这个事件在通知服务器新位置之前初始化地图,如下所示:
geoAccepted: function (pos) {
var coord = JSON.stringify(pos.coords);
initMap(pos.coords);
app.server.notifyNewPosition(coord);
},
最后,在通知我们的页面每当用户报告新位置时的方法中,我们添加一个调用addMarker
函数,如下所示:
onNewPosition: function(name, coord) {
var pos = JSON.parse(coord);
addMarker(name, pos);
$('#messages').append('<li>' + name + ', at '+ pos.latitude +', '+ pos.longitude +'</li>');
}
测试应用
当测试应用程序时,您可以在自己的计算机上进行一些初步测试。但这意味着您将始终只有一个标记位于地图的中心(即您)。为了进行更深入的测试,您需要将您的 Web 应用程序部署到可以从互联网访问的服务器上。
有许多可用的选项,从免费(用于测试)到需要付费的解决方案。当然,您也可以自己设置一个带有 IIS 的服务器并以这种方式进行管理。在 ASP.NET 网站的 URL www.asp.net/hosting
上可以找到一个寻找主机的好资源。
一旦应用程序上传到服务器,尝试从不同的设备和不同的地方访问它。接下来的三个屏幕截图证明了应用程序在桌面上的工作:
在 iPad 上,您将看到以下屏幕:
在 iPhone 上,您将看到以下屏幕:
总结
就是这样……一个 Web 应用程序,可以根据您的实际位置,实时连接您与该应用程序的其他用户。为此,我们探索了各种技术,任何现代 Web 开发人员,特别是 ASP.NET 开发人员都应该熟悉:ASP.NET MVC,SignalR,HTML5 GeoLocation 以及使用 Google Maps 进行客户端地图绘制。
以下是一些您可以用来扩展此示例的想法:
-
考虑将用户的最后已知位置持久化存储在诸如 SQL Server 或 MongoDB 之类的数据库中
-
考虑如何扩展这种应用程序以支持更多用户(查看
SignalR.Scaleout
库) -
将通知的用户限制为仅在一定距离内的用户(学习如何使用 haversine 公式计算地球上两点之间的距离)
-
展示用户附近的兴趣点,可以使用 Web 上可用的各种位置数据库,如 FourSquare Venus API 或 FaceBook Places API。
第六章:跨平台开发
微软平台并不是唯一可以执行 C#代码的平台。使用 Mono 框架,您可以针对其他平台进行开发,如 Linux、Mac OS、iOS 和 Android。在本章中,我们将探讨构建 Mac 应用程序所需的工具和框架。我们将在这里看到一些工具,例如:
-
MonoDevelop:这是一个 C# IDE,可以让您在其他非 Windows 平台上编写 C#
-
MonoMac:这提供了对 Mac 库的绑定,因此您可以从 C#使用本机 API
-
Cocoa:这是用于创建 Mac 应用程序的框架
我们将在本章中构建的应用程序是一个实用程序,您可以使用它来查找网站上的文本。给定一个 URL,应用程序将查找链接,并跟随它们查找特定的触发文本。我们将使用 Mac OS 的 UI SDK,AppKit 来显示结果。
构建网络爬虫
如果您有 C#经验并且需要构建应用程序或实用程序,Mono 可以让您快速创建它,利用现有的技能。假设您需要监视一个网站,以便在包含给定文本的新帖子出现时采取行动。与其整天手动刷新页面,不如构建一个自动化系统来完成这项任务。如果网站没有提供 RSS 订阅或其他 API 来提供程序化访问,您总是可以退而求其次,使用一种可靠的方法来获取远程数据——编写一个 HTTP 爬虫。
这听起来比实际复杂,这个实用程序将允许您输入一个 URL 和一些参数,以便应用程序知道要搜索什么。然后,它将负责访问网站,请求所有相关页面,并搜索您的目标文本。
从创建项目开始。打开 MonoDevelop 并从文件 | 新建 | 解决方案菜单项创建一个新项目,这将打开新解决方案对话框。在该对话框中,从左侧面板的C# | MonoMac列表中选择MonoMac 项目。创建解决方案时,项目模板将初始化为 Mac 应用程序的基础,如下面的屏幕截图所示:
与我们在上一章中构建的 Web 应用程序一样,Mac 应用程序使用模型-视图-控制器模式来组织自己。项目模板已经创建了控制器(MainWindowControl
)和视图(MainWindow.xib
);创建模型由您来完成。
构建模型
使用类似 MonoMac 这样的工具的主要好处之一是能够在不同平台之间共享代码,特别是如果您已经熟悉 C#。因为我们正在编写 C#,所以任何通用逻辑和数据结构都可以在需要为不同平台构建相同应用程序的情况下得到重用。例如,一个名为 iCircuit 的流行应用程序(icircuitapp.com
),它是使用 Mono 框架编写的,已经发布了 iOS、Android、Mac 和 Windows Phone 版本。iCircuit 应用程序在某些平台上实现了近 90%的代码重用。
这个数字之所以不是 100%是因为 Mono 框架最近一直专注于使用本机框架和接口构建应用程序的指导原则之一。过去跨平台工具包的主要争议点之一是它们从来没有特别本地化,因为它们被迫妥协以保持兼容性的最低公分母。使用 Mono,您被鼓励通过 C#使用平台的本机 API,以便您可以利用该平台的所有优势。
模型是您可以找到最多重用的地方,只要您尽量将所有特定于平台的依赖项排除在模型之外。为了保持组织,创建一个名为models
的文件夹,用于存储所有模型类。
访问网络
与我们在第四章中构建的 Windows 8 应用程序一样,创建 Windows Store 应用程序,我们想要做的第一件事是提供连接到 URL 并从远程服务器下载数据的能力。不过,在这种情况下,我们只想要访问 HTML 文本,以便我们可以解析它并查找各种属性。在/Models
目录中添加一个名为WebHelper
的类,如下所示:
using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;
namespace SiteWatcher
{
internal static class WebHelper
{
public static async Task<string> Get(string url)
{
var tcs = new TaskCompletionSource<string>();
var request = WebRequest.Create(url);
request.BeginGetResponse(o =>
{
var response = request.EndGetResponse(o);
using (var reader = new StreamReader(response.GetResponseStream()))
{
var result = reader.ReadToEnd();
tcs.SetResult(result);
}
}, null);
return await tcs.Task;
}
}
}
这与我们在第四章中构建的WebRequest
类非常相似,创建 Windows Store 应用程序,只是它只返回我们要解析的 HTML 字符串,而不是反序列化 JSON 对象;并且因为Get
方法将执行远程 I/O,我们使用async
关键字。作为一个经验法则,任何可能需要超过 50 毫秒才能完成的 I/O 绑定方法都应该是异步的。微软在决定哪些 OS 级 API 将是异步的时,使用了 50 毫秒的阈值。
现在,我们将为用户在用户界面中输入的数据构建后备存储模型。我们希望为用户做的一件事是保存他们的输入,这样他们下次启动应用程序时就不必重新输入。幸运的是,我们可以利用 Mac OS 上的一个内置类和 C# 5 的动态对象特性来轻松实现这一点。
NSUserDefaults
类是一个简单的键/值存储 API,它会在应用程序会话之间保留您放入其中的设置。但是,尽管针对“属性包”进行编程可以为您提供一个非常灵活的 API,但它可能会很冗长,并且难以一眼理解。为了减轻这一点,我们将在NSUserDefaults
周围构建一个很好的动态包装器,以便我们的代码至少看起来是强类型的。
首先,确保您的项目引用了Microsoft.CSharp.dll
程序集;如果没有,请添加。然后,在Models
文件夹中添加一个名为UserSettings.cs
的新类文件,并从DynamicObject
类继承。请注意,此类中使用了MonoMac.Foundation
命名空间,这是 Mono 绑定到 Mac 的 Core Foundation API 的位置。
using System;
using System.Dynamic;
using MonoMac.Foundation;
namespace SiteWatcher
{
public class UserSettings : DynamicObject
{
NSUserDefaults defaults = NSUserDefaults.StandardUserDefaults;
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
result = defaults.ValueForKey(new NSString(binder.Name));
if (result == null) result = string.Empty;
return result != null;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
defaults.SetValueForKey(NSObject.FromObject(value), new NSString(binder.Name));
return true;
}
}
}
我们只需要重写两个方法,TryGetMember
和TrySetMember
。在这些方法中,我们将使用NSUserDefaults
类,这是一个本地的 Mac API,来获取和设置给定的值。这是一个很好的例子,说明了我们如何在运行的本地平台上搭建桥梁,同时仍然具有一个 C#友好的 API 表面来进行编程。
当然,敏锐的读者会记得,在本章的开头,我说我们应该尽可能将特定于平台的代码从模型中分离出来。这通常是一个指导方针。如果我们想要将这个程序移植到另一个平台,我们只需将这个类的内部实现替换为适合该平台的内容,比如在 Android 上使用SharedSettings
,或者在 Windows RT 上使用ApplicationDataContainer
。
创建一个数据源
接下来,我们将构建一个类,该类将封装大部分我们的主要业务逻辑。当我们谈论跨平台开发时,这将是一个主要的候选代码,可以在所有平台上共享;并且您能够将代码抽象成这样的自包含类,它将更有可能被重复使用。
在Models
文件夹中创建一个名为WebDataSource.cs
的新文件。这个类将负责通过网络获取并解析结果。创建完类后,向类中添加以下两个成员:
private List<string> results = new List<string>();
public IEnumerable<string> Results
{
get { return this.results; }
}
这个字符串列表将在我们在网站源中找到匹配项时驱动用户界面。为了解析 HTML 以获得这些结果,我们可以利用一个名为HTML Agility Pack的优秀开源库,您可以在 CodePlex 网站上找到它(htmlagilitypack.codeplex.com/
)。
当您下载并解压缩包后,请在Net45
文件夹中查找名为HtmlAgilityPack.dll
的文件。这个程序集将在所有 CLR 平台上工作,因此您可以将其复制到您的项目中。通过右键单击解决方案资源管理器中的References
节点,并选择编辑引用 | .NET 程序集,将程序集添加为引用。从.NET 程序集表中浏览到HtmlAgilityPack.dll
程序集,然后单击确定。
现在我们已经添加了这个依赖项,我们可以开始编写应用程序的主要逻辑。记住,我们的目标是创建一个允许我们搜索网站特定文本的界面。将以下方法添加到WebDataSource
类中:
public async Task Retrieve()
{
dynamic settings = new UserSettings();
var htmlString = await WebHelper.Get(settings.Url);
HtmlDocument html = new HtmlDocument();
html.LoadHtml(htmlString);
foreach(var link in html.DocumentNode.SelectNodes(settings.LinkXPath))
{
string linkUrl = link.Attributes["href"].Value;
if (!linkUrl.StartsWith("http")) {
linkUrl = settings.Url + linkUrl;
}
// get this URL
string post = await WebHelper.Get (linkUrl);
ProcessPost(settings, link, post);
}
}
Retrieve
方法使用async
关键字启用您等待异步操作,首先实例化UserSettings
类作为动态对象,以便我们可以从 UI 中提取值。接下来,我们检索初始 URL 并将结果加载到HtmlDocument
类中,这样我们就可以解析出我们正在寻找的所有链接。在这里变得有趣,对于每个链接,我们异步检索该 URL 的内容并进行处理。
提示
您可能会认为,因为您在循环中等待(使用await
关键字),循环的每次迭代都会并发执行。但请记住,异步不一定意味着并发。在这种情况下,编译器将重写代码,以便主线程在等待 HTTP 调用完成时不被阻塞,但循环在等待时也不会继续迭代,因此循环的每次迭代将按正确的顺序完成。
最后,我们实现了ProcessPost
方法,该方法接收单个 URL 的内容,并使用用户提供的正则表达式进行搜索。
private void ProcessPost(dynamic settings, HtmlNode link, string postHtml)
{
// parse the doc to get the content area: settings.ContentXPath
HtmlDocument postDoc = new HtmlDocument();
postDoc.LoadHtml(postHtml);
var contentNode = postDoc.DocumentNode.SelectSingleNode(settings.ContentXPath);
if (contentNode == null) return;
// apply settings.TriggerRegex
string contentText = contentNode.InnerText;
if (string.IsNullOrWhiteSpace(contentText)) return;
Regex regex = new Regex(settings.TriggerRegex);
var match = regex.Match(contentText);
// if found, add to results
if (match.Success)
{
results.Add(link.InnerText);
}
}
完成WebDataSource
类后,我们拥有了开始工作于用户界面的一切所需。这表明了一些良好的抽象(WebHelper
和UserSettings
)以及async
和await
等新功能可以结合起来产生相对复杂的功能,同时保持良好的性能。
构建视图
接下来,我们将构建 MVC 三角形的第二和第三条腿,即视图和控制器。从视图开始是下一个逻辑步骤。在开发 Mac 应用程序时,构建 UI 的最简单方法是使用 Xcode 的界面构建器,您可以从 Mac App Store 安装该构建器。Mac 上的 MonoDevelop 专门与 Xcode 进行交互以构建 UI。
首先通过在 MonoDevelop 中双击MainWindow.xib
来打开它。它将自动在界面构建器编辑器中打开 XCode。表单最初只是一个空白窗口,但我们将开始添加视图。最初,对于任何使用过 Visual Studio 的 WinForms 或 XAML 的 WYSIWYG 编辑器的人来说,体验将非常熟悉,但这些相似之处很快就会分歧。
如果尚未显示,请通过单击屏幕右侧的按钮来显示实用程序面板,如下截图所示,您可以在 Xcode 的右上角找到该按钮。
找到对象库并浏览可用的用户界面元素列表。现在,从对象库中查找垂直分割视图,并将其拖到编辑器表面,确保将其拉伸到整个窗口,如下截图所示。
这样我们就可以构建一个简单的用户界面,让用户调整各种元素的大小,以适应他/她的需求。接下来,我们将把用户提供的选项作为文本字段元素添加到左侧面板,并附带标签。
-
URL:这是您想要抓取的网站的 URL。
-
Item Link XPath:这是在使用 URL 检索的页面上。这个 XPath 查询应该返回您感兴趣的扫描链接的列表。
-
内容 XPath:对于每个项目,我们将根据从Item Link XPath检索到的 URL 检索 HTML 内容。在新的 HTML 文档中,我们想要选择一个我们将查看的内容元素。
-
触发正则表达式:这是我们将用来指示匹配的正则表达式。
我们还希望有一种方法来显示任何匹配的结果。为此,从对象库中添加一个表视图到右侧第二个面板。这个表视图,类似于常规.NET/Windows 世界中的网格控件,将为我们提供一个以列表格式显示结果的地方。还添加一个推按钮,我们将用它来启动我们的网络调用。
完成后,您的界面应该看起来像下面的截图:
界面定义好后,我们开始查看控制器。如果您以前从未使用过 Xcode,将单独的视图元素暴露给控制器是独特的。其他平台的其他工具 tend to automatically generate code references to textboxes and buttons,但在 Xcode 中,您必须手动将它们链接到控制器中的属性。您将会接触到一些 Objective-C 代码,但只是很简短的,除了以下步骤外,您实际上不需要做任何事情。
-
显示助理编辑器,并确保
MainWindowController.h
显示在编辑器中。这是我们程序中将与视图交互的控制器的头文件。 -
您必须向控制器添加所谓的outlets并将它们与 UI 元素连接起来,这样您就可以从代码中获取对它们的引用。这是通过按住键盘上的Ctrl键,然后从控件文本框拖动到头文件中完成的。
在生成代码之前,将显示一个小对话框,如下截图所示,它让您在生成代码之前更改一些选项:
- 对于所有文本视图都这样做,并为它们赋予适当的名称,如
urlTextView
、linkXPathTextView
、contentXPathTextView
、regexTextView
和resultsTableView
。
当您添加按钮时,您会注意到您有一个选项可以将连接类型更改为Action连接,而不是Outlet连接。这是您可以连接按钮的单击事件的方法。完成后,头文件应该定义以下元素:
@property (assign) IBOutlet NSTextField *urlTextView;
@property (assign) IBOutlet NSTextField *linkXPathTextView;
@property (assign) IBOutlet NSTextField *contentXPathTextView;
@property (assign) IBOutlet NSTextField *regexTextView;
@property (assign) IBOutlet NSTableView *resultsTableView;
- (IBAction)buttonClicked:(NSButton *)sender;
- 关闭 Xcode,返回到 MonoDevelop,并查看
MainWindow.designer.cs
文件。
您会注意到您添加的所有 outlets 和 actions 都将在 C#代码中表示。MonoDevelop 会监视文件系统上的文件,当 Xcode 对其进行更改时,它会相应地重新生成此代码。
请记住,我们希望用户的设置在会话之间保持。因此,当窗口加载时,我们希望用先前输入的任何值初始化文本框。我们将使用我们在本章前面创建的UserSettings
类来提供这些值。覆盖WindowDidLoad
方法(如下面的代码所示),该方法在程序首次运行时执行,并将用户设置的值设置为文本视图。
public override void WindowDidLoad ()
{
base.WindowDidLoad ();
dynamic settings = new UserSettings();
urlTextView.StringValue = settings.Url;
linkXPathTextView.StringValue = settings.LinkXPath;
contentXPathTextView.StringValue = settings.ContentXPath;
regexTextView.StringValue = settings.TriggerRegex;
}
- 现在,我们将注意力转向数据的显示。我们应用程序中的主要输出是
NSTableView
,我们将使用它来显示目标 URL 中的任何匹配链接。为了将数据绑定到表格,我们创建一个从NSTableViewSource
继承的自定义类。
private class TableViewSource : NSTableViewSource
{
private string[] data;
public TableViewSource(string[] list)
{
data = list;
}
public override int GetRowCount (NSTableView tableView)
{
return data.Length;
}
public override NSObject GetObjectValue (NSTableView tableView, NSTableColumn tableColumn, int row)
{
return new NSString(data[row]);
}
}
每当表视图需要渲染给定的表格单元时,表视图将在GetObjectValue
方法中请求行数据。因此,当请求时,它只需获取一个字符串数组,并从数组中返回适当的索引。
- 现在我们定义了一个方法,它几乎可以将所有东西都整合在一起。
private async void GetData()
{
// retrieve data from UI
dynamic settings = new UserSettings();
settings.Url = urlTextView.StringValue;
settings.LinkXPath = linkXPathTextView.StringValue;
settings.ContentXPath = contentXPathTextView.StringValue;
settings.TriggerRegex = regexTextView.StringValue;
// initiate data retrieval
WebDataSource datasource = new WebDataSource();
await datasource.Retrieve();
// display data
TableViewSource source = new TableViewSource(datasource.Results.ToArray());
resultsTableView.Source = source;
}
在GetData
方法中,我们首先要做的是从文本框中获取值,并将其存储在UserSettings
对象中。接下来,我们异步地从WebDataSource
中检索数据。现在,将结果传递给TableViewSource
以便显示。
- 最后,实现在 Xcode 中连接的
buttonClicked
操作。
partial void buttonClicked (MonoMac.AppKit.NSButton sender)
{
GetData ();
}
现在运行程序,并输入一些要搜索的网页的值。您应该会看到类似于以下截图中显示的结果,您也可以尝试使用相同的值,但请注意,如果 Hacker News 已更新其 HTML 结构,则不起作用。
摘要
在本章中,我们使用 MonoMac 和 MonoDevelop 为 Mac OS 创建了一个小型实用程序应用程序。以下是一些可以用来扩展或改进此应用程序的想法:
-
跨应用程序会话保留结果(查看 Core Data)
-
通过在处理过程中向用户提供反馈来改善用户体验(查看
NSProgressIndicator
) -
通过并行化 URL 请求来提高应用程序的性能(查看
Parallel.ForEach
) -
尝试将应用程序移植到不同的平台。对于 iOS,查看 MonoTouch(
ios.xamarin.com
),对于 Android,查看 Mono for Android(android.xamarin.com
)
C#是一种非常表达力和强大的语言。能够针对每个主流计算平台,作为开发人员,您有着令人难以置信的机会,同时可以使用一种一致的编程语言,在不同平台上轻松重用代码。