• 使用MonoTouch.Dialog简化iOS界面开发


    MonoTouch.Dialog简称MT.D,是Xamarin.iOS的一个RAD工具包。它提供易于使用的声明式API,不需要使用导航控制器、表格等ViewController来定义复杂的应用程序UI,使得快速开发应用程序UI成为可能。

    MT.D的作者是Xamarin的CTO:Miguel de Icaza,MT.D基于表格来创建UI,它提供的API使得创建基于表格的UI变得更加简单。

    API介绍

    MonoTouch.Dialog提供了两种API来定义用户界面:

    • Low-level Elements API: 低级别的元素API,通过层次化的树型结构(类似于DOM)来表示UI及其部件。它提供了最大的灵活性用于创建和控制UI。此外,元素API支持通过定义JSON方式来动态生成UI。
    • High-Level Reflection API:高级反射API,也称为绑定API(Binding API),通过Attribute在实体类上标记元素类型等信息,然后基于对象提供的信息自动创建UI,并且提供对象与UI元素之间的自动绑定。注意:此API不提供细粒度的控制。

    MT.D内置了一套UI元素,开发人员也可以通过扩展现有UI元素或者创建新的元素来支持自定义布局。

    此外,MT.D内置了一些增强用户体验的特性,比如”pull-to-refresh”下拉刷新、异步加载图片、和搜索的支持。

    在使用MT.D之前,有必要对它的组成部分进行了解:

    • DialogViewController:简称DVC,继承自UITableViewController,所有MT.D的元素都需要通过它来显示到屏幕上。当然,也可以像普通的UITableViewController一样来使用它。
    • RootElement:是DVC的顶层容器,它包含多个Section,然后每个Section包含UI元素。RootElement不会被呈现在界面上,而是它们的子元素Section及Element被呈现。
    • Section:Section在表格中作为一个单元格的分组呈现(与UITableView的Section一样),它有Header和Footer属性,可以设置成文字或者是自定义视图。
    • Element:Element即元素作为最基本的控件,表示TableView的实际单元格,MT.D内置了各种不同类型的元素。

    DialogViewController(DVC)

    元素API和反射API都是使用DialogViewController来呈现,DVC继承自UITableViewController,所有UITableViewController的属性与方法都可以在DVC上使用。

    DVC提供了多个构造函数来对它进行初始化,这里只看参数最多的一个构造函数:

    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;

    RootElements

    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或数据库,完全从数据创建复杂的应用程序。

    Sections

    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内置了这些元素:

    • StringElement:呈现为普通的文本,左边为Caption右边为Value
    • StyledStringElement:继承自StringElement,使用内置的单元格样式或自定义格式,提供了字体、颜色、背景、换行方式、显示的行数等属性可供设置
    • MultilineElement:呈现为多行的文本
    • StyledMultilineElement:继承自MultilineElement,多了一些可以设置的属性(类似StyledStringElement)
    • EntryElement:文本框,用于输入普通字符串或者密码(isPassword参数),除了Caption/Value外,还有Placeholder属性用于设置文本框提示文本。除此之外,还可以设置KeyboardType属性,用来限制数据输入:
      • Numeric 数字
      • Phone 电话
      • Url 网址
      • Email 邮件地址
    • BooleanElement:呈现为UISwitch
    • CheckboxElement:呈现为复选框
    • RadioElement:呈现为单选框,需要放置在有RadioGroup的RootElement的Section中,使用起来显得有点麻烦
    • BadgeElement:呈现为垂直居中的文本左边一个图标 (57x57)
    • ImageElement:用于选取图片
    • ImageStringElement:继承自StringElement,类似于BadgeElement
    • FloatElement:呈现为UISlider
    • ActivityElement:呈现为loading加载动画
    • DateElement:日期选择
    • TimeElement:时间选择
    • DateTimeElement:日期时间选择
    • HtmlElement:呈现为一个普通的文本,通过Url属性设置网址,点击之后自动打开一个UIWebView加载网站
    • MessageElement:呈现为类似收件箱邮件的样式,有许多属性可以设置(Body/Caption/Date/Message/NewFlag/Sender/Subject)
    • LoadMoreElement:呈现为一个用于加载更多的普通文本,点击后显示加载动画,在相应的事件里进行一些逻辑处理
    • UIViewElement:所有类型的UIView都可以通过UIViewElement来呈现到表格上
    • OwnerDrawnElement:这是一个抽象类,可以通过继承它来创建自定义的视图
    • JsonElement:继承自RootElement,用于加载JSON内容来自动创建视图(从本地/网络上的json文件/字符串)

    官方也给出了一个元素的结构树:

        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

    反射API通过使得创建UI界面变得非常简单:

    • 创建一个类,并使用MT.D的Attribute来标记它的字段/属性
    • 创建BindingContext的实例,并把上一步类型的实例作为参数
    • 创建DialogViewController,并把它的Root设置为BindingContext的Root

    先来一个简单的例子:

    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:

    • EntryAttribute:文本框
    • PasswordAttribute:密码输入框,继承自EntryAttribute
    • CheckboxAttribute:复选框
    • DateAttribute:日期
    • TimeAttribute:时间
    • DateTimeAttribute:日期时间
    • HtmlAttribute:普通的文本,点击后打开一个UIWebView
    • MultilineAttribute:多行文本
    • RadioSelectionAttribute:呈现为RadioElement,字段/属性需要是int类型,数据源需要实现IEnumerable接口
    • CaptionAttribute:用于设置元素的Caption,如果不设置的话,将使用元素的属性/字段名
    • AlignmentAttribute:用于设置元素内容的对齐方式
    • OnTapAttribute:用于设置点击事件,参数为一个字符串对应执行的方法名
    • RangeAttribute:用于设置UISlider的值范围
    • SkipAttribute:使用此Attribute的属性/字段将不被用于作为UI元素呈现

    除了以上列出的,还有3个元素没有对应的Attribute:

    • StringElement:即普通文本,没有对应的Attribute,string类型的字段/属性默认会被呈现为StringElement
    • BooleanElement:即UISwitch,bool类型的字段/属性会被呈现为BooleanElement
    • FloatElement:即UISlider,float类型的字段/属性会被呈现为FloatElement

    再来一个例子:

    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的值类型对应,否则不会被呈现,比如:

    • EntryElement只能使用string类型,用int就不会被呈现
    • FloatElement只能使用float类型,double/decimal类型都无效
    • BooleanElement只能使用bool类型
    • RadioElement类型只能使用enum类型或者int类型并设置数据源
    • DateElement/TimeElement/DateTimeElement只能使用日期相关类型

    反射API大大简化了UI界面的开发,但是它不能很好支持细粒度控制,如果对UI定制要求比较高,建议还是直接使用元素API。

    当然,如果只是偶尔需要直接访问某个Element,可以通过DVC的Root属性来找到对应的Element,但是操作起来比较繁琐:

    var section1 = this.Root[0];
    var element1 = section1[0] as StringElement;

    JSON元素

    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/

    其他特性

    Pull-to-Refresh(下拉刷新)支持

    DialogViewController提供了一个RefreshRequested事件,只需要实现它就可以为表格提供下拉刷新支持:

    var dvc = new DialogViewController(root);
    dvc.RefreshRequested  += (s, e) {
      // 处理数据... lwme.cnblogs.com
      dvc.ReloadComplete(); // 处理完成之后调用这个方法完成加载
    };

    另外,也有TriggerRefresh()方法来直接调用下拉刷新;还可以通过重写MakeRefreshTableHeaderView(RectangleF)方法来自定义刷新头部的内容。

    搜索支持

    DialogViewController提供了一些属性及方法用于搜索的支持:

    • EnableSearch:启用搜索支持
    • SearchPlaceholder:搜索框提示文本
    • StartSearch():开始搜索
    • FinishSearch():完成搜索
    • PerformFilter():执行过滤
    • SearchButtonClicked():按下搜索按钮
    • OnSearchTextChanged():搜索文本框内容改变
    • SearchTextChanged:事件,同上

    一般情况下只需要通过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

  • 相关阅读:
    linux进程间通信之消息队列
    本地安装discuz x2.5(论坛站)程序
    缩小IO/CPU瓶颈:linux平台加速编译速度的几种方法
    php mcrypt
    Nginx工作原理和优化、漏洞。
    Linux下两种TCP网络服务器实现方式:循环服务&并发服务
    version `GLIBC_2.14' not found 解决方法.
    Flex Ant自动构建
    函数传指针和传引用
    JEECG 列表行编辑模式下实现文本的xheditor富文本框编辑器
  • 原文地址:https://www.cnblogs.com/lwme/p/Simplified-UI-on-the-iOS-with-MonoTouch-Dialog.html
Copyright © 2020-2023  润新知