Xamarin.iOS 的 MonoTouch.Dialog 简介

MonoTouch.Dialog 简称 MT.D,是一个快速 UI 开发工具包,允许开发人员使用信息生成应用程序屏幕和导航,而不需要执行创建视图控制器、表等枯燥的工作。因此,它显著简化了 UI 开发并减少了代码。 例如,考虑以下屏幕截图:

For example, consider this screenshot

以下代码用于定义整个屏幕:

public enum Category
{
    Travel,
    Lodging,
    Books
}
        
public class Expense
{
    [Section("Expense Entry")]

    [Entry("Enter expense name")]
    public string Name;
    [Section("Expense Details")]
  
    [Caption("Description")]
    [Entry]
    public string Details;
        
    [Checkbox]
    public bool IsApproved = true;
    [Caption("Category")]
    public Category ExpenseCategory;
}

在 iOS 中使用表时,经常会出现大量重复代码。 例如,每次需要一个表时,都需要一个数据源来填充该表。 在具有两个通过导航控制器连接的基于表的屏幕的应用程序中,每个屏幕共享许多相同的代码。

MT.D 通过将所有代码封装到用于表创建的通用 API 中来简化这一过程。 然后,它在该 API 之上提供了一个抽象,允许使用声明性对象绑定语法,从而使其变得更加容易。 因此,MT.D 中有两个可用的 API:

  • 低级元素 API – 元素 API 基于创建表示屏幕及其组件的元素层次结构树。 元素 API 为开发人员创建 UI 提供了最大的灵活性和控制度。 此外,元素 API 还对通过 JSON 进行声明性定义提供了高级支持,从而实现令人难以置信的快速声明以及从服务器生成动态 UI。
  • 高级反射 API – 也称为绑定 API,其中使用 UI 提示对类进行注释,然后 MT.D 根据对象并提供屏幕上显示的内容(以及可选编辑的内容)和基础对象支持之间的绑定。 上面的示例演示了反射 API 的用法。 此 API 不提供元素 API 所提供的精细控制,但它通过根据类属性自动生成元素层次结构来进一步降低复杂性。

MT.D 包含大量用于屏幕创建的内置 UI 元素,但它也认识到对自定义元素和高级屏幕布局的需求。 因此,扩展性是 API 中的一流功能。 开发人员可以扩展现有元素或创建新元素,然后无缝集成。

此外,MT.D 还内置了许多常见的 iOS UX 功能,例如“拉取刷新”支持、异步图像加载和搜索支持。

本文将全面介绍如何使用 MT.D,包括:

  • MT.D 组件 – 重点是了解构成 MT.D 的类,以便能够快速上手
  • 元素引用 – MT.D 内置元素的完整列表
  • 高级用法 – 涵盖高级功能,例如拉取刷新、搜索、背景图像加载、使用 LINQ 生成元素层次结构以及创建与 MT.D 配合使用的自定义元素、单元格和控制器

设置 MT.D

MT.D 与 Xamarin.iOS 一起分发。 若要使用它,请右键单击 Visual Studio 2017 或 Visual Studio for Mac 中 Xamarin.iOS 项目的“引用”节点,然后添加对“MonoTouch.Dialog-1”程序集的引用。 然后,根据需要在源代码中添加 using MonoTouch.Dialog 语句。

了解 MT.D 的各个部分

即使使用反射 API,MT.D 也会在基础创建一个元素层次结构,就像直接通过元素 API 创建一样。 此外,上一部分中提到的 JSON 支持也会创建元素。 因此,对 MT.D 的组成部分有一个基本的了解非常重要。

MT.D 使用以下四个部分构建屏幕:

  • DialogViewController
  • RootElement
  • 元素

DialogViewController

DialogViewController(简称为 DVC)继承自 UITableViewController,因此代表包含表的屏幕。 DVC 可以像常规 UITableViewController 一样推送到导航控制器。

RootElement

RootElement 是进入 DVC 的项的顶级容器。 它包含部分,而后者又可以包含元素。 不呈现 RootElements;相反,它们只是实际呈现内容的容器。 将 RootElement 分配到 DVC,然后 DVC 呈现其子元素。

部分

部分是表中的一组单元格。 与普通表部分一样,它可以选择具有页眉和页脚,可以是文本,甚至可以是自定义视图,如以下屏幕截图所示:

As with a normal table section, it can optionally have a header and footer that can either be text, or even custom views, as in this screenshot

元素

元素代表表中的实际单元格。 MT.D 包含各种代表不同数据类型或不同输入的元素。 例如,以下屏幕截图演示了一些可用元素:

For example, this screenshots illustrate a few of the available elements

有关部分和 RootElements 的详细信息

现在让我们更详细地讨论 RootElements 和部分。

RootElements

启动 MonoTouch.Dialog 进程至少需要一个 RootElement。

如果使用部分/元素值初始化 RootElement,则该值用于定位将提供配置摘要的子元素,该配置摘要将呈现在显示屏的右侧。 例如,以下屏幕截图显示了左侧的表,其中一个单元格包含右侧详细信息屏幕的标题“Dessert”以及所选甜点的值。

This screenshot shows a table on the left with a cell containing the title of the detail screen on the right, Dessert, along with the value of the selected desertThis screenshot below shows a table on the left with a cell containing the title of the detail screen on the right, Dessert, along with the value of the selected desert

根元素也可以在部分内使用来触发加载新的嵌套配置页,如上所示。 在此模式下使用时,提供的标题将在部分内呈现时使用,并且也用作子页的标题。 例如:

var root = new RootElement ("Meals") {
    new Section ("Dinner") {
        new RootElement ("Dessert", new RadioGroup ("dessert", 2)) {
            new Section () {
                new RadioElement ("Ice Cream", "dessert"),
                new RadioElement ("Milkshake", "dessert"),
                new RadioElement ("Chocolate Cake", "dessert")
            }
        }
    }
};

在上面的示例中,当用户点击“Dessert”时,MonoTouch.Dialog 将创建新页并导航到该页,根目录为“Dessert”,并且具有包含三个值的单选按钮组。

在此特定示例中,单选按钮组将在“Dessert”部分中选择“Chocolate Cake”,因为我们将值“2”传递给了 RadioGroup。 这意味着选择列表中的第三项(零索引)。

调用 Add 方法或使用 C# 4 初始化语法添加部分。 提供插入方法来插入带有动画的部分。

如果使用 Group 实例(而不是 RadioGroup)创建 RootElement,则在部分显示时 RootElement 的汇总值将是与 Group.Key 值具有相同键的所有 BooleanElement 和 CheckboxElement 的累积计数。

部分

部分用于对屏幕中的元素进行分组,它们是 RootElement 唯一有效的直接子元素。 部分可以包含任何标准元素,包括新的 RootElements。

嵌入部分中的根元素用于导航到新的更深层次。

部分可以将页眉和页脚作为字符串或 UIViews。 通常只使用字符串,但要创建自定义 UI,可以使用任何 UIView 作为页眉或页脚。 可以使用字符串来创建它们,如下所示:

var section = new Section ("Header", "Footer");

若要使用视图,只需将视图传递给构造函数:

var header = new UIImageView (Image.FromFile ("sample.png"));
var section = new Section (header);

接收通知

处理 NSAction

MT.D 将 NSAction 显示为处理回调的委托。 例如,假设你想要处理 MT.D 创建的表单元格的触摸事件。 使用 MT.D 创建元素时,只需提供一个回调函数,如下所示:

new Section () {
    new StringElement ("Demo Callback", delegate { Console.WriteLine ("Handled"); })
}

检索元素值

Element.Value 属性相结合,回调可以检索其他元素中设置的值。 例如,考虑以下代码:

var element = new EntryElement (task.Name, "Enter task description", task.Description);
                
var taskElement = new RootElement (task.Name) {
    new Section () { element },
    new Section () { new DateElement ("Due Date", task.DueDate) },
    new Section ("Demo Retrieving Element Value") {
        new StringElement ("Output Task Description", delegate { Console.WriteLine (element.Value); })
    }
};

此代码创建一个 UI,如下所示。 有关此示例的完整演练,请参阅元素 API 演练教程。

Combined with the Element.Value property, the callback can retrieve the value set in other elements

当用户按下底部表单元格时,匿名函数中的代码就会执行,将 element 实例中的值写入 Visual Studio for Mac 中的“应用程序输出”面板

内置元素

MT.D 附带了许多称为“元素”的内置表单元格项。 这些元素用于在表单元格中显示各种不同类型,例如字符串、浮点数、日期甚至图像等等。 每个元素负责适当显示数据类型。 例如,布尔元素将显示一个开关来切换其值。 同样,浮点元素将显示一个滑块来更改浮点值。

还有更复杂的元素来支持更丰富的数据类型,例如图像和 html。 例如,一个 html 元素在选择时将打开 UIWebView 以加载网页,并在表单元格中显示标题。

使用元素值

用于捕获用户输入的元素公开一个公共 Value 属性,该属性随时保存该元素的当前值。 当用户使用应用程序时,它会自动更新。

这是 MonoTouch.Dialog 中所有元素的行为,但用户创建的元素不需要这样做。

字符串元素

StringElement 在表单元格的左侧显示标题,在单元格的右侧显示字符串值。

A StringElement shows a caption on the left side of a table cell and the string value on the right side of the cell

若要将 StringElement 用作按钮,请提供委托。

new StringElement ("Click me", () => { 
    new UIAlertView("Tapped", "String Element Tapped", null, "ok", null).Show();
});

To use a StringElement as a button, provide a delegate

样式字符串元素

StyledStringElement 允许使用内置表单元格样式或自定义格式来显示字符串。

A StyledStringElement allows strings to be presented using either built-in table cell styles or with custom formatting

StyledStringElement 类派生自 StringElement,但允许开发人员自定义一些属性,例如字体、文本颜色、背景单元格颜色、换行模式、要显示的行数以及是否应显示附件。

多行元素

Multiline Element

输入元素

顾名思义,EntryElement 用于获取用户输入。 它支持常规字符串或隐藏字符的密码。

The EntryElement is used to get user input

使用三个值将其初始化:

  • 将向用户显示的条目的标题。
  • 占位符文本(这是向用户提供提示的灰色文本)。
  • 文本的值。

占位符和值可为 null。 不过,标题是必需的。

在任何时候,访问其 Value 属性都可以检索 EntryElement 的值。

此外,可以在创建时将 KeyboardType 属性设置为数据输入所需的键盘类型样式。 这可用于通过 UIKeyboardType 的值来配置键盘,如下所示:

  • Numeric
  • 手机
  • Url
  • 电子邮件

布尔元素

Boolean Element

复选框元素

Checkbox Element

单选按钮元素

RadioElement 要求在 RootElement 中指定 RadioGroup

mtRoot = new RootElement ("Demos", new RadioGroup("MyGroup", 0));

A RadioElement requires a RadioGroup to be specified in the RootElement

RootElements 还用于协调单选按钮元素。 RadioElement 成员可以跨越多个部分(例如,实现类似于铃声选择器的功能以及将自定义铃声与系统铃声分开)。 摘要视图将显示当前选择的单选按钮元素。 若要使用此功能,请使用组构造函数创建 RootElement,如下所示:

var root = new RootElement ("Meals", new RadioGroup ("myGroup", 0));

RadioGroup 中的组名称用于显示包含页中的所选值(如果有),该值(在本例中为零)是第一个所选项的索引。

徽章元素

Badge Element

浮点元素

Float Element

活动元素

Activity Element

日期元素

Date Element

选择 DateElement 对应的单元格时,会出现一个日期选取器,如下所示:

When the cell corresponding to the DateElement is selected, a date picker is presented as shown

Time 元素

Time Element

选择 TimeElement 对应的单元格时,会出现一个时间选取器,如下所示:

When the cell corresponding to the TimeElement is selected, a time picker is presented as shown

日期/时间元素

DateTime Element

选择 DateTimeElement 对应的单元格时,会出现一个日期/时间选取器,如下所示:

When the cell corresponding to the DateTimeElement is selected, a datetime picker is presented as shown

HTML 元素

HTML Element

HTMLElement 在表单元格中显示其 Caption 属性的值。 选择后,分配到该元素的 Url 将加载到 UIWebView 控件中,如下所示:

Whe selected, the Url assigned to the element is loaded in a UIWebView control as shown below

Message 元素

Message Element

加载更多元素

使用此元素可允许用户加载列表中的更多项。 可以自定义正常和加载中标题,以及字体和文本颜色。 UIActivity 指示器开始显示动画,当用户点击单元格时会显示加载中标题,然后执行传递给构造函数的 NSActionNSAction 中的代码完成后,UIActivity 指示器将停止显示动画并再次显示正常标题。

UIView 元素

此外,可以使用 UIViewElement 显示任何自定义的 UIView

Owner-Drawn 元素

此元素必须子类化,因为它是一个抽象类。 应重写 Height(RectangleF bounds) 方法(在该方法中应返回元素的高度)以及 Draw(RectangleF bounds, CGContext context, UIView view)(在该方法中应使用上下文和视图参数在给定的边界内执行所有自定义绘制)。 该元素执行对 UIView 进行子类化并将其放入要返回的单元格中的繁重工作,因此你只需实现两个简单的重写即可。 可以在 DemoOwnerDrawnElement.cs 文件的示例应用中看到更好的示例实现。

下面是实现该类的一个非常简单的示例:

public class SampleOwnerDrawnElement : OwnerDrawnElement
{
    public SampleOwnerDrawnElement (string text) : base(UITableViewCellStyle.Default, "sampleOwnerDrawnElement")
    {
        this.Text = text;
    }

    public string Text { get; set; }

    public override void Draw (RectangleF bounds, CGContext context, UIView view)
    {
        UIColor.White.SetFill();
        context.FillRect(bounds);

        UIColor.Black.SetColor();   
        view.DrawString(this.Text, new RectangleF(10, 15, bounds.Width - 20, bounds.Height - 30), UIFont.BoldSystemFontOfSize(14.0f), UILineBreakMode.TailTruncation);
    }

    public override float Height (RectangleF bounds)
    {
        return 44.0f;
    }
}

JSON 元素

JsonElementRootElement 的子类,它扩展了 RootElement,以便能够从本地或远程 URL 加载嵌套子项的内容。

JsonElement 是能够以两种形式实例化的 RootElement。 其中一个版本创建了一个按需加载内容的 RootElement。 这些是通过使用 JsonElement 构造函数创建的,该构造函数在末尾带有一个额外的参数,即加载内容的 URL:

var je = new JsonElement ("Dynamic Data", "https://tirania.org/tmp/demo.json");

另一种形式从本地文件或已分析的现有 System.Json.JsonObject 创建数据:

var je = JsonElement.FromFile ("json.sample");
using (var reader = File.OpenRead ("json.sample"))
    return JsonElement.FromJson (JsonObject.Load (reader) as JsonObject, arg);

有关将 JSON 与 MT.D 配合使用的详细信息,请参阅 JSON 元素演练教程。

其他功能

拉取刷新支持

“拉取刷新”是一个视觉对象,最初出现在 Tweetie2 应用程序中,后来成为许多应用程序中的流行效果

若要向对话框添加自动拉取刷新支持,只需执行两项操作:挂接一个事件处理程序,以便在用户拉取数据时收到通知,并在数据加载后通知 DialogViewController 返回其默认状态。

挂接通知很简单;只需连接到 DialogViewController 上的 RefreshRequested 事件即可,如下所示:

dvc.RefreshRequested += OnUserRequestedRefresh;

然后,在方法 OnUserRequestedRefresh 中,可以对一些数据加载进行排队,从网络请求一些数据,或者运转一个线程来计算数据。 加载数据后,必须通知 DialogViewController 新数据已存在,并且要将视图还原到默认状态,可以通过调用 ReloadComplete 来执行此操作:

dvc.ReloadComplete ();

搜索支持

若要支持搜索,请在 DialogViewController 上设置 EnableSearch 属性。 还可以设置 SearchPlaceholder 属性以用作搜索栏中的水印文本。

搜索将随着用户键入而更改视图的内容。 它会搜索可见字段并将其显示给用户。 DialogViewController 公开三个方法,用于以编程方式启动、终止或触发对结果的新筛选操作。 下面列出了这些方法:

  • StartSearch
  • FinishSearch
  • PerformFilter

系统可扩展,因此,如果需要,你可以更改此行为。

背景图像加载

MonoTouch.Dialog 合并了 TweetStation 应用程序的图像加载程序。 此图像加载程序可用于在后台加载图像,支持缓存,并且可以在加载图像时通知代码。

它还会限制传出网络连接的数量。

图像加载程序在 ImageLoader 类中实现,你只需调用 DefaultRequestImage 方法,需要提供要加载的图像的 URI,以及加载图像时要调用的 IImageUpdated 接口的实例。

例如,以下代码将图像从 URL 加载到 BadgeElement

string uriString = "http://some-server.com/some image url";

var rootElement = new RootElement("Image Loader") {
    new Section() {
        new BadgeElement( ImageLoader.DefaultRequestImage( new Uri(uriString), this), "Xamarin")
    }
};

ImageLoader 类公开一个 Purge 方法,当你想要释放当前缓存在内存中的所有图像时可以调用该方法。 当前代码有 50 个图像的缓存。 如果你想使用不同的缓存大小(例如,如果你预期图像太大,50 个图像就太多了),你可以创建 ImageLoader 的实例并传递你想要保留的图像数量缓存。

使用 LINQ 创建元素层次结构

通过巧妙地使用 LINQ 和 C# 的初始化语法,LINQ 可用于创建元素层次结构。 例如,以下代码从一些字符串数组创建一个屏幕,并通过传递给每个 StringElement 的匿名函数处理单元格选择:

var rootElement = new RootElement ("LINQ root element") {
    from x in new string [] { "one", "two", "three" }
    select new Section (x) {
        from y in "Hello:World".Split (':')
        select (Element) new StringElement (y, delegate { Debug.WriteLine("cell tapped"); })
    }
};

这可以轻松地与 XML 数据存储或数据库中的数据相结合,几乎完全根据数据创建复杂的应用程序。

扩展 MT.D

创建自定义元素

你可以通过从现有元素继承或从根类元素派生来创建自己的元素。

若要创建自己的元素,需要重写以下方法:

// To release any heavy resources that you might have
void Dispose (bool disposing);

// To retrieve the UITableViewCell for your element
// you would need to prepare the cell to be reused, in the
// same way that UITableView expects reusable cells to work
UITableViewCell GetCell (UITableView tv);

// To retrieve a "summary" that can be used with
// a root element to render a summary one level up.  
string Summary ();

// To detect when the user has tapped on the cell
void Selected (DialogViewController dvc, UITableView tableView, NSIndexPath path);

// If you support search, to probe if the cell matches the user input
bool Matches (string text);

如果元素可以有可变大小,则需要实现 IElementSizing 接口,其中包含一个方法:

// Returns the height for the cell at indexPath.Section, indexPath.Row
float GetHeight (UITableView tableView, NSIndexPath indexPath);

如果你打算通过调用 base.GetCell(tv) 并自定义返回的单元格来实现 GetCell 方法,则还需要重写 CellKey 属性以返回对元素唯一的键,如下所示:

static NSString MyKey = new NSString ("MyKey");
protected override NSString CellKey {
    get {
        return MyKey;
    }
}

这适用于大多数元素,但不适用于 StringElementStyledStringElement,因为它们针对各种渲染方案使用自己的一组键。 必须复制这些类中的代码。

DialogViewController (DVC)

反射和元素 API 使用相同的 DialogViewController。 有时,你可能想要自定义视图的外观,或者可能想要使用 UITableViewController 的某些超出 UI 基本创建功能的功能。

DialogViewController 只是 UITableViewController 的子类,你可以像自定义 UITableViewController 一样对其进行自定义。

例如,如果你要将列表样式更改为 GroupedPlain,则可以通过在创建控制器时更改属性来设置此值,如下所示:

var myController = new DialogViewController (root, true) {
    Style = UITableViewStyle.Grouped;
}

对于 DialogViewController 的更高级自定义(例如设置其背景),可以将其子类化并重写正确的方法,如以下示例所示:

class SpiffyDialogViewController : DialogViewController {
    UIImage image;

    public SpiffyDialogViewController (RootElement root, bool pushing, UIImage image) 
        : base (root, pushing) 
    {
        this.image = image;
    }

    public override LoadView ()
    {
        base.LoadView ();
        var color = UIColor.FromPatternImage(image);
        TableView.BackgroundColor = UIColor.Clear;
        ParentViewController.View.BackgroundColor = color;
    }
}

另一个自定义点是 DialogViewController 中的以下虚拟方法:

public override Source CreateSizingSource (bool unevenRows)

如果单元格大小均匀,此方法应返回 DialogViewController.Source 的子类;如果单元格大小不均匀,则此方法应返回 DialogViewController.SizingSource 的子类。

可以使用此重写来捕获任何 UITableViewSource 方法。 例如,TweetStation 使用此功能来跟踪用户何时滚动到顶部,并相应地更新未读推文的数量。

验证

元素本身不提供验证,因为非常适合网页和桌面应用程序的模型不会直接映射到 iPhone 交互模型。

如果你要执行数据验证,则应该在用户使用输入的数据触发操作时执行此操作。 例如,顶部工具栏上的“完成”或“下一步”按钮,或用作转到下一阶段的按钮的某个 StringElement

可在此处执行基本的输入验证,也许可以执行更复杂的验证,例如检查服务器的用户/密码组合的有效性。

向用户通知错误的方式特定于应用程序。 可以弹出 UIAlertView 或显示提示。

总结

本文涵盖了有关 MonoTouch.Dialog 的大量信息。 其中讨论了 MT.D 工作原理的基础知识,并涵盖了组成 MT.D 的各个组件。 其中还介绍了 MT.D 支持的各种元素和表自定义,并讨论了如何使用自定义元素扩展 MT.D。 此外,它还解释了 MT.D 中允许从 JSON 动态创建元素的 JSON 支持。