2017/7/25

Xamarin.Forms 教學系列文(二十.壹)async/await 非同步 & DisplayAlert

學習目標
  • 學會非同步寫法... 

圖形使用者介面 (GUI) 有一個特質,執行事件時都是 有順序性 的,每一件事情執行前都要等待另一件事情結束。

一開始,所有程式都會連結到一條執行緒內動作,這條執行緒也稱做 main thread、user-interface threadUI thread

但若 main thread 因為某個大量工作事件卡住,導致介面呈現 假當機 狀態,會讓使用者森氣氣的…

為了改進使用者經驗,我們開始想辦法將這些會卡住的大量工作,丟到其他執行緒去處理而不影響 UI thread 的執行,這些執行緒也稱做 work thread
可以把他們想像成在背景執行的工作,不會影響 UI thread 的運作。

近幾年來,非同步其實越來越好寫了,更幸運的是,在 C# 5.0 後支援 async/await,讓非同步寫法 超級簡化

底下先來介紹 Xamarin.Forms 內建的非同步功能 - DiplayAlert,同時帶著大家了解 async/await 的寫法

前面章節其實已有出現過一些非同步範例,
如 13 章 的 ImageBrowser 和 BitmapStreams,或是 19 章 RssFeed 內的 WebRequest 所搭配的 BeginGetResponse、EndGetResponse。

這邊只會介紹 async 和 await 的非同步寫法,
Xamarin.Forms 另有提供 Device.BeginInvokeOnMainThread 方法就不贅述,功用大同小異,若有興趣可以看原文書 p.626 An alert with callbacks


DiplayAlert 

[alert 視窗] (或稱 message box) 通常拿來 提醒使用者 或是 詢問問題
當 [alert 視窗] 顯示時,App 其他功能都是沒辦法動作的,一定要等到 [alert 視窗] 消失,使用者才能再次與應用程式互動。
因為上面描述的特性,Xamarin 將之定義成非同步方法,讓視窗彈出來時,主程式還是能繼續執行。

page 類別內定義了三個方法彈出 [alert 視窗]:
  • Task DisplayAlert (string title, string message, string cancel)
  • Task<bool> DisplayAlert (string title, string message, string accept, string cancel)
  • Task<string> DisplayActionSheet (string title, string cancel, string destruction, params string[] buttons)

差異在...
第一個只會顯示 一個 解除按鈕,第二個會顯示 是或否 兩個 按鈕,第三個方法則可以自訂 多個 按鈕...

備註:
這三個方法都是回傳 Task/Task<T> 物件,設計概念來自 Tak-based Asynchronous Pattern (TAP),TAP 也是.NET 比較推薦的非同步做法。
之前範例用 WebRequest 的 BeginGetResponse 和 EndGetResponse 方法,是屬於比較舊的非同步方法叫做 Asynchronous Programming Model (APM)。

底下來看範例,
功能很簡單,彈出一個 [alert 視窗] 並詢問使用者,當使用者按下 yes 時回傳 true,反之回傳 false,等關閉視窗後再更改 Label 的文字

XAML:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="AlertCallbacks.AlertCallbacksPage">
    <StackLayout>
        <Button Text="Invoke Alert" 
            FontSize="Large" 
            HorizontalOptions="Center" 
            VerticalOptions="CenterAndExpand" 
            Clicked="OnButtonClicked" />

        <Label x:Name="label" 
           Text="Tap button to invoke alert" 
           FontSize="Large" 
           HorizontalTextAlignment="Center" 
           VerticalOptions="CenterAndExpand" />
    </StackLayout>
</ContentPage>

.cs:
public partial class AlertAwaitPage : ContentPage
{
    public AlertAwaitPage()
    {
        InitializeComponent();
    }

    async void OnButtonClicked(object sender, EventArgs args)
    {
        Task task = DisplayAlert("Simple Alert", 
            "Decide on an option", 
            "yes or ok", "no or cancel");

        label.Text = "Alert is currently displayed";

        bool result = await task;
        label.Text = String.Format("Alert {0} button was pressed",
            result ? "OK" : "Cancel");
    }
}

關鍵就是 16 行
bool result = await task;

當我們使用 DisplayAlert 時,回傳的類別應該是 Task<bool>,但 await 這關鍵字似乎很神奇地幫我們將 bool 抽離出來!!


執行的步驟大家可以跟著文字想像一下:

1. 當 Button 按下去
執行到 DisplayAlert 會丟出一條 [彈出 alert 視窗] thread
但系統會繼續往下執行程式:Label.Text = "Alert is currently displayed"


2. 遇到關鍵 16 行 bool result = await task;
result 等待視窗回傳結果,但系統還是繼續執行其他和 task 無關 的程式碼

3. 等到視窗關閉回傳結果
系統再回來執行跟 task 有關 的程式碼:Label.Text =  result ? "OK" :"Cancel"



相當聰明且很好寫…感謝 .NET 讚嘆 .NET...

*使用 await 時,function 指示詞一定要設定為 async

也可以將以上程式碼合併成一行這樣:
async void OnButtonClicked(object sender, EventArgs args)
{
    label.Text = String.Format("Alert {0} button was pressed", 
        await DisplayAlert("Simple Alert", "Decide on an option", 
        "yes or ok", "no or cancel") ? "OK" : "Cancel");
}

*酷炫


小練習

練習寫一個非同步的 function 並呼叫,
功能很簡單,顯示目前時間並隨機放置顯示的位置:

XAML:
*OnPlatform 寫法已更改,請參考此篇文章

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="TaskDelayClock.TaskDelayClockPage">
    
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness" 
                    iOS="0, 20, 0, 0" />
    </ContentPage.Padding>
    
    <AbsoluteLayout>
        <Label x:Name="label" 
               FontSize="Large"
               AbsoluteLayout.LayoutFlags="PositionProportional" />
    </AbsoluteLayout>
    
</ContentPage>

.cs
public partial class TaskDelayClockPage : ContentPage
{
    Random random = new Random();
    public TaskDelayClockPage()
    {
        InitializeComponent();
        InfiniteLoop();
    }

    async void InfiniteLoop()
    {
        while (true)
        {
            label.Text = DateTime.Now.ToString("T");
            label.FontSize = random.Next(12, 49);
            AbsoluteLayout.SetLayoutBounds(label, new Rectangle(random.NextDouble(), 
                                                                random.NextDouble(), 
                                                                AbsoluteLayout.AutoSize, 
                                                                AbsoluteLayout.AutoSize));
            //將 Task 暫停 0.25 秒
            await Task.Delay(250);
        }
    }
}


為了不卡 main thread,這邊的 無窮迴圈 寫成非同步方法,

並利用 await Task.Delay(250),讓 Label 每隔 0.25 秒顯示當時時間並換位置...

執行結果:



*這邊給個建議,若是要寫非同步方法,方法命名上可以在最後加上 Async,

舉上面的例子來說,可以改命名為 InfiniteLoopAsync(),讓程式更好維護的啦




沒有留言:

張貼留言