使用 Razor 模板生成 HTML 视图

在移动开发领域,术语“混合应用”通常指的是在托管 Web 查看器控件内将其部分(或全部)屏幕呈现为 HTML 页面的应用程序。

在某些开发环境中,可以完全使用 HTML 和 JavaScript 生成移动应用,但在尝试完成复杂的处理或 UI 效果时,这些应用可能会遇到性能问题,并且它们可以访问的平台功能也受到限制。

Xamarin 集两个领域的长处于一身,特别是在利用 Razor HTML 模板化引擎时。 借助 Xamarin,可以灵活地生成使用 JavaScript 和 CSS 的跨平台模板化 HTML 视图,而且还可以完全访问基础平台 API 并使用 C# 进行快速处理。

本文档介绍如何借助 Xamarin,使用 Razor 模板化引擎来生成可跨移动平台使用的 HTML+JavaScript+CSS 视图。

以编程方式使用 Web 视图

在了解 Razor 之前,请先查看此部分,其中介绍了如何使用 Web 视图直接显示 HTML 内容,特别是在应用内生成的 HTML 内容。

Xamarin 提供对 iOS 和 Android 上的基础平台 API 的完全访问权限,因此可以使用 C# 轻松创建和显示 HTML。 每个平台的基本语法如下所示。

iOS

在 Xamarin.iOS 中的 UIWebView 控件中显示 HTML 也只需几行代码:

var webView = new UIWebView (View.Bounds);
View.AddSubview(webView);
string contentDirectoryPath = Path.Combine (NSBundle.MainBundle.BundlePath, "Content/");
var html = "<html><h1>Hello</h1><p>World</p></html>";
webView.LoadHtmlString(html, NSBundle.MainBundle.BundleUrl);

有关使用 UIWebView 控件的更多详细信息,请参阅 iOS UIWebView 方案。

Android

使用 Xamarin.Android 在 WebView 控件中显示 HTML 只需几行代码即可完成:

// webView is declared in an AXML layout file
var webView = FindViewById<WebView> (Resource.Id.webView);

// enable JavaScript execution in your html view so you can provide "alerts" and other js
webView.SetWebChromeClient(new WebChromeClient());

var html = "<html><h1>Hello</h1><p>World</p></html>";
webView.LoadDataWithBaseURL("file:///android_asset/", html, "text/html", "UTF-8", null);

有关使用 WebView 控件的更多详细信息,请参阅 Android WebView 方案。

指定基目录

在这两个平台上,都有一个参数指定 HTML 页面的基目录。 这是设备文件系统上的位置,用于解析对图像和 CSS 文件等资源的相对引用。 例如,诸如以下标记

<link rel="stylesheet" href="style.css" />
<img src="monkey.jpg" />
<script type="text/javascript" src="jscript.js">

引用了以下文件:style.css、monkey.jpg 和 jscript.js。 基目录设置告知 Web 视图这些文件所在的位置,以便可以将这些文件加载到页面中。

iOS

模板输出在 iOS 中使用以下 C# 代码呈现:

webView.LoadHtmlString (page, NSBundle.MainBundle.BundleUrl);

基目录指定为 NSBundle.MainBundle.BundleUrl,指的是应用程序的安装目录。 Resources 文件夹中的所有文件都复制到此位置,例如下面显示的 style.css 文件

iPhoneHybrid solution

所有静态内容文件的生成操作都应为 BundleResource

iOS project build action: BundleResource

Android

当 html 字符串显示在 web 视图中时,Android 还需要将基目录作为参数传递。

webView.LoadDataWithBaseURL("file:///android_asset/", page, "text/html", "UTF-8", null);

特殊字符串 file:///android_asset/ 是指应用中的 Android Assets 文件夹,此处显示包含 style.css 文件

AndroidHybrid solution

所有静态内容文件的生成操作都应为 AndroidAsset

Android project build action: AndroidAsset

从 HTML 和 JavaScript 调用 C#

当 html 页面加载到 Web 视图中时,它会像从服务器加载页面一样处理链接和表单。 这意味着如果用户单击链接或提交表单,Web 视图会尝试导航到指定的目标。

如果链接指向外部服务器(例如 google.com),则 Web 视图会尝试加载外部网站(假设有 Internet 连接)。

<a href="http://google.com/">Google</a>

如果链接是相对的,则 Web 视图会尝试从基目录加载该内容。 显然,此操作不需要网络连接,因为内容存储在设备上的应用中。

<a href="somepage.html">Local content</a>

表单操作遵循相同的规则。

<form method="get" action="http://google.com/"></form>
<form method="get" action="somepage.html"></form>

不会在客户端上托管 Web 服务器;但是,可以使用目前的响应式设计模式中使用的相同服务器通信技术通过 HTTP GET 来调用服务,并通过发出 JavaScript(或调用已托管在 Web 视图中的 JavaScript)来异步处理响应。 这使你能够轻松地将数据从 HTML 传递回 C# 代码进行处理,然后将结果显示在 HTML 页面上。

iOS 和 Android 都为应用代码提供了拦截这些导航事件的机制,以便应用程序代码可以做出响应(如果需要)。 此功能对于生成混合应用至关重要,因为它允许本机代码与 Web 视图交互。

iOS

可以重写 iOS 中 Web 视图上的 ShouldStartLoad 事件,以允许应用程序代码处理导航请求(例如点击链接)。 方法参数提供所有信息

bool HandleShouldStartLoad (UIWebView webView, NSUrlRequest request, UIWebViewNavigationType navigationType) {
    // return true if handled in code
    // return false to let the web view follow the link
}

然后分配事件处理程序:

webView.ShouldStartLoad += HandleShouldStartLoad;

Android

在 Android 上,只需子类化 WebViewClient,然后执行代码来响应导航请求。

class HybridWebViewClient : WebViewClient {
    public override bool ShouldOverrideUrlLoading (WebView webView, IWebResourceRequest request) {
        // return true if handled in code
        // return false to let the web view follow the link
    }
}

然后在 web 视图上设置客户端:

webView.SetWebViewClient (new HybridWebViewClient ());

从 C# 调用 JavaScript

除了告诉 Web 视图加载新的 HTML 页面之外,C# 代码还可以在当前显示的页面中运行 JavaScript。 可以使用 C# 字符串创建整个 JavaScript 代码块并执行,也可以通过 script 标记创建对页面上已有的 JavaScript 进行方法调用。

Android

创建要执行的 JavaScript 代码,然后在其前面加上“javascript:”前缀,并指示 Web 视图加载该字符串:

var js = "alert('test');";
webView.LoadUrl ("javascript:" + js);

iOS

iOS Web 视图提供了专用于调用 JavaScript 的方法:

var js = "alert('test');";
webView.EvaluateJavascript (js);

总结

本节介绍 Android 和 iOS 上的 Web 视图控件功能,这些功能使我们能够通过 Xamarin 生成混合应用,包括:

  • 能够从代码中生成的字符串加载 HTML,
  • 能够引用本地文件(CSS、JavaScript、图像或其他 HTML 文件),
  • 能够在 C# 代码中拦截导航请求,
  • 能够从 C# 代码调用 JavaScript。

下一节介绍 Razor,它可用于轻松创建要在混合应用中使用的 HTML。

什么是 Razor?

Razor 是随 ASP.NET MVC 引入的模板化引擎,它最初在服务器上运行并生成要提供给 Web 浏览器的 HTML。

Razor 模板化引擎用 C# 扩展了标准 HTML 语法,以便你可以轻松表达布局并合并 CSS 样式表和 JavaScript。 模板可以引用 Model 类,该类可以是任何自定义类型,并且可以直接从模板访问其属性。 它的主要优点之一是能够轻松混合 HTML 和 C# 语法。

Razor 模板不仅限于服务器端使用,它们还可以包含在 Xamarin 应用中。 使用 Razor 模板以及以编程方式处理 Web 视图的功能,可以使用 Xamarin 生成复杂的跨平台混合应用程序。

Razor 模板基础知识

Razor 模板文件的文件扩展名为 .cshtml。 可以从“新建文件”对话框中的“文本模板化”部分将它们添加到 Xamarin 项目

New File - Razor Template

下面显示了一个简单的 Razor 模板 (RazorView.cshtml)

@model string
<html>
    <body>
    <h1>@Model</h1>
    </body>
</html>

请注意与常规 HTML 文件的以下差异:

  • @ 符号在 Razor 模板中具有特殊含义 – 它表示其后的表达式是要计算的 C# 表达式。
  • @model 指令始终显示为 Razor 模板文件的第一行。
  • @model 指令应后跟一个类型。 在此示例中,向模板传递了一个简单的字符串,但这可以是任何自定义类。
  • 当在整个模板中引用 @Model 时,它提供对生成模板时传递给模板的对象(在本例中是一个字符串)的引用。
  • IDE 将自动生成模板(扩展名为 .cshtml 的文件)的分部类。 可以查看此代码,但不能对其进行编辑。 RazorView.cshtml 分部类命名为 RazorView,以便与 .cshtml 模板文件名匹配。 此名称用于在 C# 代码中引用模板。
  • 还可以将 @using 语句包含在 Razor 模板的顶部,以包含其他命名空间。

然后,可以使用以下 C# 代码生成最终的 HTML 输出。 请注意,我们将 Model 指定为字符串“Hello World”,它将合并到呈现的模板输出中。

var template = new RazorView () { Model = "Hello World" };
var page = template.GenerateString ();

以下是 iOS 模拟器和 Android Emulator 上的 Web 视图中显示的输出:

Hello World

更多 Razor 语法

在本节中,我们介绍一些基本 Razor 语法,以帮助你开始使用它。 本节中的示例使用数据填充以下类,并使用 Razor 显示它:

public class Monkey {
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
    public List<string> FavoriteFoods { get; set; }
}

所有示例都使用以下数据初始化代码

var animal = new Monkey {
    Name = "Rupert",
    Birthday=new DateTime(2011, 04, 01),
    FavoriteFoods = new List<string>
        {"Bananas", "Banana Split", "Banana Smoothie"}
};

显示 Model 属性

如果模型是具有属性的类,则可在 Razor 模板中轻松引用这些属性,如以下示例模板所示:

@model Monkey
<html>
    <body>
    <h1>@Model.Name</h1>
    <p>Birthday: @(Model.Birthday.ToString("d MMMM yyyy"))</p>
    </body>
</html>

可以使用以下代码将其呈现为字符串:

var template = new RazorView () { Model = animal };
var page = template.GenerateString ();

最终输出显示在 iOS 模拟器和 Android Emulator 的 Web 视图中,如下所示:

Rupert

C# 语句

模板中可以包含更复杂的 C#,例如本例中的 Model 属性更新和 Age 计算:

@model Monkey
<html>
    <body>
    @{
        Model.Name = "Rupert X. Monkey";
        Model.Birthday = new DateTime(2011,3,1);
    }
    <h1>@Model.Name</h1>
    <p>Birthday: @Model.Birthday.ToString("d MMMM yyyy")</p>
    <p>Age: @(Math.Floor(DateTime.Now.Date.Subtract (Model.Birthday.Date).TotalDays/365)) years old</p>
    </body>
</html>

可以通过用 @() 围绕代码来编写复杂的单行 C# 表达式(例如设置年龄的格式)。

要编写多个 C# 语句,可以使用 @{} 围绕这些语句。

If-else 语句

代码分支可以用 @if 表示,如以下模板示例所示。

@model Monkey
<html>
    <body>
    <h1>@Model.Name</h1>
    <p>Birthday: @(Model.Birthday.ToString("d MMMM yyyy"))</p>
    <p>Age: @(Math.Floor(DateTime.Now.Date.Subtract (Model.Birthday.Date).TotalDays/365)) years old</p>
    <p>Favorite Foods:</p>
    @if (Model.FavoriteFoods.Count == 0) {
        <p>No favorites</p>
    } else {
        <p>@Model.FavoriteFoods.Count favorites</p>
    }
    </body>
</html>

循环

还可以添加类似 foreach 这样的循环结构。 可以对循环变量使用 @ 前缀(在本例中为 @food),以将其呈现为 HTML。

@model Monkey
<html>
    <body>
    <h1>@Model.Name</h1>
    <p>Birthday: @Model.Birthday.ToString("d MMMM yyyy")</p>
    <p>Age: @(Math.Floor(DateTime.Now.Date.Subtract (Model.Birthday.Date).TotalDays/365)) years old</p>
    <p>Favorite Foods:</p>
    @if (Model.FavoriteFoods.Count == 0) {
        <p>No favorites</p>
    } else {
        <ul>
            @foreach (var food in @Model.FavoriteFoods) {
                <li>@food</li>
            }
        </ul>
    }
    </body>
</html>

上述模板的输出显示在 iOS 模拟器和 Android Emulator 上运行:

Rupert X Monkey

本节介绍了使用 Razor 模板呈现简单只读视图的基础知识。 下一节介绍如何使用 Razor 生成更完整的应用,这些应用可以接受用户输入并在 HTML 视图中的 JavaScript 和 C# 之间进行互操作。

配合使用 Razor 模板和 Xamarin

本节介绍如何使用 Visual Studio for Mac 中的解决方案模板生成自己的混合应用程序。 “文件”>“新建”>“解决方案...”窗口提供了三个模板

  • “Android”>“应用”>“Android WebView 应用程序”
  • “iOS”>“应用”>“WebView 应用程序”
  • ASP.NET MVC 项目

对于 iPhone 和 Android 项目,“新建解决方案”窗口如下所示 - 右侧的解决方案说明强调了对 Razor 模板化引擎的支持

Creating iPhone and Android solutions

请注意,可以轻松将 .cshtml Razor 模板添加到任何现有 Xamarin 项目,而无需使用这些解决方案模板。 iOS 项目不需要 Storyboard 即可使用 Razor;只需以编程方式将 UIWebView 控件添加到任何视图,就可以用 C# 代码呈现整个 Razor 模板。

iPhone 和 Android 项目的默认模板解决方案内容如下所示:

iPhone and Android templates

这些模板提供现成的应用程序基础结构,用于加载带有数据模型对象的 Razor 模板、处理用户输入并通过 JavaScript 与用户进行通信。

该解决方案的重要部分包括:

  • 静态内容,如 style.css 文件
  • Razor .cshtml 模板文件,如 RazorView.cshtml
  • 在 Razor 模板中引用的 Model 类(如 ExampleModel.cs)
  • 用于创建 Web 视图并呈现模板的特定于平台的类,例如 Android 上的 MainActivity 和 iOS 上的 iPhoneHybridViewController

下一节介绍项目的工作方式。

静态内容

静态内容包括 CSS 样式表、图像、JavaScript 文件或可从 Web 视图中显示的 HTML 文件链接或引用的其他内容。

模板项目包含一个最小样式表,用于演示如何在混合应用中包含静态内容。 CSS 样式表在模板中的引用方式如下所示:

<link rel="stylesheet" href="style.css" />

可以添加所需的任何样式表和 JavaScript 文件,包括 JQuery 等框架。

Razor cshtml 模板

该模板包含一个 Razor .cshtml 文件,其中包含预先编写的代码,以帮助在 HTML/JavaScript 和 C# 之间进行数据通信。 这样,便可以生成复杂的混合应用,这些应用不仅显示 Model 中的只读数据,而且还接受 HTML 中的用户输入,并将其传递回 C# 代码进行处理或存储。

呈现模板

通过在模板上调用 GenerateString,可呈现可在 Web 视图中显示的 HTML。 如果模板使用模型,则应在呈现之前提供该模型。 此图说明了呈现的工作方式,不是在运行时由 Web 视图解析静态资源,而是使用提供的基目录查找指定的文件。

Razor flowchart

从模板调用 C# 代码

从呈现的 Web 视图回调到 C# 的通信通过以下方式完成:设置 Web 视图的 URL,然后在 C# 中拦截请求以处理本机请求,而无需重新加载 Web 视图。

RazorView 按钮的处理方式就是一个示例。 该按钮具有以下 HTML:

<input type="button" name="UpdateLabel" value="Click" onclick="InvokeCSharpWithFormValues(this)" />

InvokeCSharpWithFormValues JavaScript 函数读取 HTML 表单中的所有值,并设置 Web 视图的 location.href

location.href = "hybrid:" + elm.name + "?" + qs;

这会尝试将 Web 视图导航到使用自定义方案(例如 hybrid:)的 URL

hybrid:UpdateLabel?textbox=SomeValue&UpdateLabel=Click

当本机 Web 视图处理此导航请求时,我们有机会拦截它。 在 iOS 中,这是通过处理 UIWebView 的 HandleShouldStartLoad 事件来完成的。 在 Android 中,我们只需对表单中使用的 WebViewClient 进行子类化,并替代 ShouldOverrideUrlLoading。

这两个导航拦截器的内部构造本质上是相同的。

首先,检查 Web 视图尝试加载的 URL,如果它不以自定义方案 (hybrid:) 开头,则允许导航按正常方式进行。

对于自定义 URL 方案,URL 中介于方案和“?” 之间的所有内容都是要处理的方法名称(本例中为“UpdateLabel”)。 查询字符串中的所有内容都被视为方法调用的参数:

var resources = url.Substring(scheme.Length).Split('?');
var method = resources [0];
var parameters = System.Web.HttpUtility.ParseQueryString(resources[1]);

此示例中的 UpdateLabel 对文本框参数执行最少量的字符串操作(在字符串前面添加“C# says”),然后回调到 Web 视图。

处理 URL 后,该方法会中止导航,以便 Web 视图不会尝试完成到自定义 URL 的导航。

从 C# 操作模板

从 C# 到呈现的 HTML Web 视图的通信是通过在 Web 视图中调用 JavaScript 来完成的。 在 iOS 上,这通过在 UIWebView 上调用 EvaluateJavascript 来完成:

webView.EvaluateJavascript (js);

在 Android 上,可以通过使用 "javascript:" URL 方案将 JavaScript 作为 URL 加载,在 Web 视图中调用 JavaScript:

webView.LoadUrl ("javascript:" + js);

打造真正的混合应用

这些模板不使用每个平台上的本机控件,整个屏幕都是单一的 Web 视图。

HTML 非常适合原型制作和显示 Web 最适合的内容,例如富文本和响应式布局。 但是,并非所有任务都适合 HTML 和 JavaScript,例如,滚动浏览较长的数据列表时,使用本机 UI 控件(例如 iOS 上的 UITableView 或 Android 上的 ListView)效果更佳。

可以使用特定于平台的控件轻松增强模板中的 Web 视图 - 只需在 Mac 上使用 Xcode 或在 Android 上使用 Resources/layout/Main.axml 编辑 MainStoryboard.storyboard 即可

RazorTodo 示例

RazorTodo 存储库包含两个单独的解决方案,以显示完全由 HTML 驱动的应用和将 HTML 与本机控件相结合的应用之间的差异:

  • RazorTodo - 使用 Razor 模板的完全由 HTML 驱动的应用
  • RazorNativeTodo - 使用 iOS 和 Android 的本机列表视图控件,但使用 HTML 和 Razor 显示编辑屏幕。

这些 Xamarin 应用在 iOS 和 Android 上运行,利用可移植类库 (PCL) 来共享公共代码,如数据库和模型类。 Razor .cshtml 模板也可以包含在 PCL 中,以便可以轻松地跨平台共享

这两个示例应用都包含来自本机平台的 Twitter 共享和文本转语音 API,演示了使用 Xamarin 的混合应用程序仍然可以从 HTML Razor 模板驱动的视图访问所有基础功能。

RazorTodo 应用对列表和编辑视图使用 HTML Razor 模板。 这意味着我们几乎可以完全在共享的可移植类库(包括数据库和 .cshtml Razor 模板)中生成应用。 下面的屏幕截图显示 iOS 和 Android 应用。

RazorTodo

RazorNativeTodo 应用对编辑视图使用 HTML Razor 模板,但在每个平台上实现本机滚动列表。 这带来了许多好处,包括:

  • 性能 - 本机滚动控件使用虚拟化来确保快速、平滑的滚动,即使数据列表很长。
  • 本机体验 - 可轻松启用特定于平台的 UI 元素,如 iOS 和 Android 中的快速滚动索引支持。

使用 Xamarin 生成混合应用的一个主要优点是,可以从完全由 HTML 驱动的用户界面(如第一个示例)开始,然后在需要时添加特定于平台的功能(如第二个示例所示)。 iOS 和 Android 上的本机列表屏幕和 HTML Razor 编辑屏幕如下所示。

RazorNativeTodo

总结

本文介绍了 iOS 和 Android 上可用的 Web 视图控件的功能,这些功能有助于简化生成混合应用程序。

然后,讨论了 Razor 模板化引擎以及可用于使用 .cshtml Razor 模板文件在 Xamarin 应用中轻松生成 HTML 的语法。 本文还介绍了 Visual Studio for Mac 解决方案模板,借助该模板可快速开始使用 Xamarin 生成混合应用程序。

最后,介绍了 RazorTodo 示例,这些示例演示如何将 Web 视图与本机用户界面和 API 组合在一起。