1. 程式人生 > >使用MonoTouch.Dialog簡化iOS介面開發

使用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元素這種方式,可以建立非常靈活的介面,同時也能大大減小客戶端的大小。

其他特性

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();
                          }
                        })
        }
};

參考