MonoTouch.Dialog简称MT.D,是Xamarin.iOS的一个RAD工具包。它提供易于使用的声明式API,不需要使用导航控制器、表格等ViewController来定义复杂的应用程序UI,使得快速开发应用程序UI成为可能。
MT.D的作者是Xamarin的CTO:Miguel de Icaza,MT.D基于表格来创建UI,它提供的API使得创建基于表格的UI变得更加简单。
MonoTouch.Dialog提供了两种API来定义用户界面:
MT.D内置了一套UI元素,开发人员也可以通过扩展现有UI元素或者创建新的元素来支持自定义布局。
此外,MT.D内置了一些增强用户体验的特性,比如”pull-to-refresh”下拉刷新、异步加载图片、和搜索的支持。
在使用MT.D之前,有必要对它的组成部分进行了解:
元素API和反射API都是使用DialogViewController来呈现,DVC继承自UITableViewController,所有UITableViewController的属性与方法都可以在DVC上使用。
DVC提供了多个构造函数来对它进行初始化,这里只看参数最多的一个构造函数:
class="brush: csharp; auto-links: true; collapse: false; first-line: 1; gutter: true; html-script: false; light: false; ruler: false; smart-tabs: true; tab-size: 2; toolbar: true;">DialogViewController(UITableViewStyle style, RootElement root, bool pushing)
style即列表样式,默认都是UITableViewStyle.Grouped分组显示,可以设置为UITableViewStyle.Plain不分组。
root即根元素,它下面的所有Section/Element都会被DVC呈现出来。
pushing参数用于是否显示返回按钮,一般用在有UINavigationController的时候。
例如,创建一个不分组显示的DVC:
var dvc = new DialogViewController(UITableViewStyle.Plain, root)
在实际应用开发中,一般很少会直接创建DVC的实例,而是通过继承的方法对每一个视图进行定制:
class LwmeViewController: DialogViewController { public LwmeViewController(): base(UITableViewStyle.Plain, null) { this.Root = new RootElement("囧月") { //...创建Section及Element }; } }
然后通过重写DVC的一些方法来定制自己的视图。
在完全使用MT.D开发的app中,可以把DVC做为根视图控制器:
[Register("AppDelegate")] public partial class AppDelegate : UIApplicationDelegate { UIWindow window; public override bool FinishedLaunching(UIApplication app, NSDictionary options) { window = new UIWindow(UIScreen.MainScreen.Bounds); window.RootViewController = new LwmeViewController(); window.MakeKeyAndVisible(); return true; } // ... }
假如app需要使用UINavigationController,可以把DVC作为UINavigationController的根视图控制器:
nav = new UINavigationController(new DialogViewController(root)); window.RootViewController = nav;
DialogViewController需要一个RootElement作为根节点,它的子节点只能是Section,各种Element必须作为Section的子节点来呈现。
// 在使用NavigationController的时候,RootElement的Caption会被呈现为NavigationItem的内容 var root = new RootElement ("囧月 - 博客园") { new Section("随笔") { // 分组的文字 new StringElement("MonoTouch.Dialog") // 元素 } new Section("评论") { new EntryElement("内容") } }
RootElement还可以作为Section的子元素,当这个RootElement被点击的时候,实际上会打开一个新的视图,如下(官方DEMO):
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") } } } }
此外,还可以通过LINQ(语句或表达式)和C# 3.0新增的对象和集合初始化语法来创建元素的层次结构:
var root = new RootElement("囧月-lwme.cnblogs.com") { new string[] {"随笔", "评论", "RSS"}.Select( x => new Section(x) { "内容1,内容2,内容3,内容4".Split(',').Select( s => new StringElement(s, delegate { Console.WriteLine("内容被点击"); }) ) } ) }
通过这种做法,可以很容易的结合XML或数据库,完全从数据创建复杂的应用程序。
Section用来对Element元素进行分组显示,它可以包含任何标准内容(Element/UIView/RootElement),但RootElement只能包含它。
可以把Section的Header/Footer设置为字符串或者UIView:
var section = new Section("Header", "Footer") // 使用字符串 var section = new Section(new UIImageView(Image.FromBundle("header.png"))); // 使用UIView
MT.D内置了这些元素:
官方也给出了一个元素的结构树:
Element
BadgeElement
BoolElement
BooleanElement - uses an on/off slider
BooleanImageElement - uses images for true/false
EntryElement
FloatElement
HtmlElement
ImageElement
MessageElement
MultilineElement
RootElement (container for Sections)
Section (only valid container for Elements)
StringElement
CheckboxElement
DateTimeElement
DateElement
TimeElement
ImageStringElement
RadioElement
StyleStringElement
UIViewElement
Element提供了NSAction类型的委托作为回调函数来处理动作(大部分Element都有一个NSAction类型的Tapped事件),比如处理一个触摸事件:
new Section () { new StringElement ("点我 - 囧月", delegate { Console.WriteLine ("元素被点击"); }) }
继承自Element的元素默认有Caption属性,用来在单元格左边显示标题;大部分Element都有一个Value属性,用来显示在单元格右边。
在回调函数中通过Element的属性来获取对应的值:
var element = new EntryElement ("评论", "输入评论内容", null); var taskElement = new RootElement ("囧月-博客-评论"){ new Section () { element }, new Section ("获取评论内容") { new StringElement ("获取", delegate { Console.WriteLine (element.Value); }) } };
如果元素的属性是可操作的,如EntryElement.Value,可以直接通过属性设置它的值。
不可操作的如EntryElement.Caption,或者StringElement.Value/StringElement.Caption属性,直接设置元素的值不会反映在界面上,需要通过RootElement.Reload方法来重新加载才可以更新内容:
var ee = new EntryElement ("评论", "输入评论内容", null); var se = new StringElement("时间", DateTime.Now.ToString()); var root = new RootElement ("囧月-博客-评论"){ new Section () { ee, se }, new Section ("获取评论内容") { new StringElement ("获取", delegate { Console.WriteLine (element.Value); // 直接设置元素内容 ee.Value = DateTime.Now.ToString(); // 不可直接设置的属性 se.Caption = "新标题"; se.Value = DateTime.Now.ToString(); root.Reload(se, UITableViewRowAnimation.None); }) } };
反射API通过使得创建UI界面变得非常简单:
先来一个简单的例子:
class Blogger { [Section("登录博客"), Entry("输入用户名"), Caption("用户名")] public string Username; [Password("输入密码"), Caption("密码")] public string Password; [Checkbox, Caption("下次自动登录")] public bool Remember; [Section("开始登录", "请确认你输入的信息"), Caption("登录"), OnTap("Login")] public string DoLogin; } public class LwmeViewController: DialogViewController { BindingContext context; Blogger blog; public LwmeViewController(): base(UITableViewStyle.Grouped, null) { blog = new Blogger { Username = "囧月" }; context = new BindingContext(this, blog, null); this.Root = context.Root; } public void Login() { context.Fetch(); // 通过Fetch方法把文本框输入的信息反馈到blog实例上 if (string.IsNullOrWhiteSpace(blog.Username) || string.IsNullOrWhiteSpace(blog.Password)) { var tip = new UIAlertViewController( "出错提示", "用户名和密码必须填写", null, "确定", null); tip.Show(); } // 进行登录操作... } }
为了避免阻塞UI线程(用户界面假死),一般都会使用异步操作,比如上面的登录可能使用WebClient的UploadStringAsync异步方法,然后在相应事件中进行操作;这里需要注意,使用了异步方法之后,在相应的事件中可能就不是UI线程,将不能直接对UI相关元素进行操作,类似于Winform/Wpf,MonoTouch提供了两个方法用于在非UI线程操作UI元素:InvokeOnMainThread/BeginInvokeOnMainThread。
现在,来看一下MT.D为反射API提供了多少Attribute:
除了以上列出的,还有3个元素没有对应的Attribute:
再来一个例子:
class Blogger { public string Username = "囧月"; // 呈现为StringElement public bool Remember; // 呈现为BooleanElement public float Value; // 呈现为FloatElement [Multiline] public string Description; [Range(0, 100)] public float Value2; // 可以使用Range来标明范围 [Skip] public string ignoreField; // 不被呈现 }
另外,对于RadioElement类型的元素,除了可以使用RadioSelectionAttribute外,MT.D还提供了一个方法支持直接从Enum类型:
public enum Category { Blog, Post, Comment } class Blogger { public Category ContentCategory; } class Blogger2 { [RadioSelection("CategorySource")] // 设置数据源 public int ContentCategory; // 字段/属性必须是int类型 // 数据源只要实现IEnumerable接口,不限制类型 public List<string> CategorySource = new List<string>{ "Blog", "Post", "Comment" }; }
注意:字段/属性的类型必须与相应的Element的值类型对应,否则不会被呈现,比如:
反射API大大简化了UI界面的开发,但是它不能很好支持细粒度控制,如果对UI定制要求比较高,建议还是直接使用元素API。
当然,如果只是偶尔需要直接访问某个Element,可以通过DVC的Root属性来找到对应的Element,但是操作起来比较繁琐:
var section1 = this.Root[0]; var element1 = section1[0] as StringElement;
MT.D支持从本地/远程的json文件、或者已解析的JsonObject对象实例来创建JSON元素。
假如有这么一个简单的json文件:
{ "title": "囧月", "sections": [ { "elements" : [ { "id" : "lwme-username", "type": "entry", "caption": "用户名", "placeholder": "输入用户名" }, { "id" : "lwme-date", "type": "date", "caption": "日期", "value": "00:00" } ] } ] }
通过内置的方法来加载它:
var root = JsonElement.FromFile("lwme.json"); // 加载本地json var root = new JsonElement("load from json", "lwme.cnblogs.com/lwme.json"); // 加载远程json var dvc = new DialogViewController(root); // 可以直接把JsonElement作为根元素
另外,还可以通过json文件里设置的id来获得对应的Element:
var username = taskElement ["lwme-username"] as EntryElement; var date = taskElement ["lwme-date"] as DateElement;
通过json元素这种方式,可以创建非常灵活的界面,同时也能大大减小客户端的大小。
json文件里各种标记的属性对应元素的各种属性,完整的JSON格式见官方文档:http://docs.xamarin.com/guides/ios/user_interface/monotouch.dialog/monotouch.dialog_json_markup/。
DialogViewController提供了一个RefreshRequested事件,只需要实现它就可以为表格提供下拉刷新支持:
var dvc = new DialogViewController(root); dvc.RefreshRequested += (s, e) { // 处理数据... lwme.cnblogs.com dvc.ReloadComplete(); // 处理完成之后调用这个方法完成加载 };
另外,也有TriggerRefresh()方法来直接调用下拉刷新;还可以通过重写MakeRefreshTableHeaderView(RectangleF)方法来自定义刷新头部的内容。
DialogViewController提供了一些属性及方法用于搜索的支持:
一般情况下只需要通过EnableSearch属性来启用搜索即可,更多的定制可以通过以上的方法/事件来实现。
MT.D提供了一个ImageLoader用于在后台加载图片:
new BadgeElement( ImageLoader.DefaultRequestImage( new Uri("http://lwme.cnblogs.com/xx.png"), this), "囧月") // 等同于ImageLoader.DefaultLoader.RequestImage方法
下载的图片会被缓存在内存中(默认缓存50张图片),ImageLoader.Purge()方法可用于清理缓存。更多的自定义操作可以通过创建ImageLoader实例来实现。
可以通过继承Element或者更具体的类型来创建自定义的元素。创建自定义元素将需要重写以下方法:
// 为元素创建UITableViewCell,设置内容及样式并呈现在表格上 UITableViewCell GetCell (UITableView tv) // (可选)设置元素的高度,重写这个方法需要实现IElementSizing接口 float GetHeight (UITableView tableView, NSIndexPath indexPath); // (可选)释放资源 void Dispose (bool disposing); // (可选)为元素呈现摘要内容,比如StringElement就呈现为Caption string Summary () // (可选)元素被点击/触摸时,很多元素的Tapped事件就是在这个方法里实现 void Selected (DialogViewController dvc, UITableView tableView, NSIndexPath path) // (可选)如果需要支持搜索,需要在方法中检测用户输入是否匹配 bool Matches (string text)
如果重写了GetCell方法,并且在方法内部调用了base.GetCell(tv)方法来返回cell,那么还需要重写CellKey属性来返回一个唯一的key用于自定义元素:
static NSString MyKey = new NSString ("lwmeCustomElementKey"); protected override NSString CellKey { get { return MyKey; } }
MT.D没有为Element提供任何验证的方法,如果需要对用户输入进行验证,自己实现验证逻辑,比如元素的Tapped事件中进行数据验证:
var ee = new EntryElement ("评论", "输入评论内容", null); var root = new RootElement ("囧月-博客-评论"){ new Section () { ee }, new Section ("获取评论内容") { new StringElement ("获取", delegate { if (string.IsNullOrEmpty(ee.Value)) { var tip = new UIAlertViewController( "出错提示", "内容必须填写", null, "确定", null); tip.Show(); } }) } };
官方文档(本文内容主要来源):http://docs.xamarin.com/guides/ios/user_interface/monotouch.dialog/
Miguel de Icaza的文章(Simplified User Interfaces on the iPhone with MonoTouch.Dialog):http://tirania.org/blog/archive/2010/Feb-23.html
获取最新源码:https://github.com/migueldeicaza/MonoTouch.Dialog (Sample目录下也提供了不少例子)
完整的官方例子:https://github.com/migueldeicaza/TweetStation