XAML in Xamarin.Forms 基礎篇 電子書

XAML in Xamarin.Forms 基礎篇 電子書
XAML in Xamarin.Forms 基礎篇 電子書

Xamarin.Forms 快速入門 電子書

Xamarin.Forms 快速入門 電子書
Xamarin.Forms 快速入門 電子書

2019/06/10

如何在Xamarin.Forms 的 iOS App,需要即時變更回上一頁按鈕的文字

如何在 Xamarin.Forms 的 iOS App,需要即時變更回上一頁按鈕的文字

最近遇到上課學員提出一個問題,學員要設計一個頁面,這個頁面可以設定這個 App 要使用的多國語言選單功能,當從某個語系切換到另外一個語系的時候,這個頁面上的相關內容也可以同步的切換成為新的語言,可是,當這個 App 在 Android 平台下的時候,相關的設計都可以正常運作,不過,當在 iOS 平台下的時候,卻發生了問題;問題在於這個頁面的最上方有個導航工具列,在 Android 平台下這個導航工具列上僅會顯示這個頁面的名稱,此時,這個頁面的名稱是可以透過資料綁定的方式,在 ViewModel 程式碼中,即時變更成為任何文字,頁面的工具列上也就會即時變更與顯示出最新的頁面名稱,然而,在 iOS 平台下,導航工具列的左上方出現的卻是回上頁的頁面名稱或者是自訂的回上頁文字,若在設定頁面的 ViewModel 來變更回上頁的按鈕的文字,是無法正常更新的,這是因為回上頁的按鈕名稱是要在上一頁的頁面中來設定的。
這篇文章的範例專案原始碼,可以從 GitHub 取得
下面螢幕截圖為這個範例專案在 Android 平台上的執行結果左下圖為一開始啟動 App 的頁面,右下圖為按下 切換可動態變換按鈕文字 的按鈕,就會切換到右下角的頁面。
 
下面螢幕截圖為這個範例專案在 iOS 平台上的執行結果左下圖為一開始啟動 App 的頁面,右下圖為按下 切換可動態變換按鈕文字 的按鈕,就會切換到右下角的頁面。
 
而在首頁上,其宣告的 XAML 內容可以從底下看的出來,這裡並沒有使用 NavigationPage.BackButtonTitle="自訂回上頁按鈕文字" 這樣的宣告,所以,當從首頁切換到下一個頁面的時候,在 iOS 平台下預設將會顯示首頁頁面標題的文字在下一頁的回上頁按鈕文字上。不過,因為這裡的頁面已經過修正,可以自動更新回上頁按鈕文字,與平常看到的不太一樣;若在 Xamarin.Forms 開發的程式,在 iOS 平台下跑起來,其實將會看到如下圖的畫面,若在前一頁面中,沒有使用 NavigationPage.BackButtonTitle 來指定客製按鈕文字,原則上都會出現上一頁面的標題文字在這個頁面的回上頁按鈕上。
xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ChangeiOSBackButtonText.Views.MainPage"
             Title="上頁文字變換">

    <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
        <Label Text="Welcome to Xamarin Forms and Prism!" />
        <Button Text="切換可動態變換按鈕文字" Command="{Binding SwithChangePageCommand}"/>
        <Button Text="切換一般頁面" Command="{Binding SwithNextPageCommand}"/>
        <Button Text="連續兩個頁面" Command="{Binding SwithTwoPageCommand}"/>
    </StackLayout>

</ContentPage>
那麼,要如何做到這樣的客製化的回上頁按鈕功能,並且可以結合資料綁定,做到動態可以在 ViewModel 中,來設定要顯示的文字,例如,回上頁按鈕文字、頁面主題名稱,達到如下的效果。
在 Android 系統下,若點選 切換中文 按鈕,會出現左下圖畫面,若點選 Switch English 按鈕,則會出現右下圖畫面。
 
在 iOS 系統下,若點選 切換中文 按鈕,會出現左下圖畫面,若點選 Switch English 按鈕,則會出現右下圖畫面。
 
現在來看看這個頁面 XAML 宣告內容,在這裡若想要變更回上頁按鈕的文字,需要在這裡使用這裡自訂 附加屬性 DynamicBackButtonTextAttached.SetBackButtonText ,使用它來指定要顯示的回上頁按鈕文字內容,由於這個附加屬性值可以做到資料綁定的更新通知,所以,想要變更該頁面回上頁按鈕文字內容,就可以從 ViewModel 來修改 ThisBackText 這個屬性值即可。
xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="ChangeiOSBackButtonText.Views.CanChangePage"
             xmlns:DynBtn="clr-namespace:ChangeiOSBackButtonText.AttachedProperties"
             DynBtn:DynamicBackButtonTextAttached.SetBackButtonText="{Binding ThisBackText}"
             xmlns:localViews="clr-namespace:ChangeiOSBackButtonText.Views"
             Title="{Binding Title}">

    <StackLayout
        >
        <Entry Text="{Binding Message}"/>
        <Button Text="設定" Command="{Binding SetBackButtonTextCommand}"/>
        <Button Text="切換中文" Command="{Binding SetChineseCommand}"/>
        <Button Text="Switch English" Command="{Binding SetEnglishCommand}"/>
    </StackLayout>

</ContentPage>

設計理念說明

為了要做到這樣的效果,這個時候需要建立一個名為 NaviCustomPage 的 NavigationPage 型別的頁面,並且整個 App 的頁面導航功能,需要透過這個新建立的導航頁面來切換頁面,這樣剛剛所使用的附加屬性 DynamicBackButtonTextAttached.SetBackButtonText 才會正常的運作。
而這個附加屬性的主要目的是要能夠指定當時這個頁面的回上頁按鈕文字內容,不過,這需要透過導航頁面中的 可綁定屬性 DynamicBackButtonText 來指定該導航工具列上的回上頁按鈕文字,該 DynamicBackButtonText 屬性將會透過 附加屬性 DynamicBackButtonTextAttached.SetBackButtonText 來變更,因為在這個附加屬性上,有訂閱 OnSetBackButtonTextChanged 這個屬性變更事件,所以,當該附加屬性有變動就會觸發這個事件,接著將會在這個事件中執行一個輔助支援方法 ChangeBackButtonTextHelper.ChangeBackButtonText,透過這個方法來取得當前導航工具列上的 可綁定屬性 ,也就 DynamicBackButtonText 屬性值。
最後,需要在 iOS 平台下來實作出 NaviCustomPage 的 NavigationRenderer,這裡將會在 iOS 平台下建立一個 NaviCustomPageRenderer 類別來做到這件事情,在這個類別中,將會覆寫方方法 OnElementPropertyChanged ,若該 NaviCustomPage 內的任何一個可綁定屬性有異動的時候,就將會觸發這個事件;在該事件中將會檢查是否是 DynamicBackButtonText 這個屬性有異動,若有變更的話,將會呼叫 UpdateBackButtonTitleText 方法來變更回上頁按鈕。
因此,就可以完成這樣的需求,現在來逐一檢視這樣的設計過程。
建立一個新的 NavigationPage,並且指定該頁面的名稱為 NaviCustomPage
xaml
<?xml version="1.0" encoding="utf-8" ?>
<NavigationPage xmlns="http://xamarin.com/schemas/2014/forms"
                xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
                prism:ViewModelLocator.AutowireViewModel="True"
                x:Class="ChangeiOSBackButtonText.Views.NaviCustomPage">

</NavigationPage>
打開 NaviCustomPage 的 Code Behind ,加入一個可綁定屬性 DynamicBackButtonText,在這裡並不需要做其他的設計,因為,這個屬性存在的目的僅僅是要能夠讓 iOS 下的 NaviCustomPageRenderer 類別,可以知道這個屬性值有異動,也就是需要更新回上頁按鈕了,而真正要變更回上頁按鈕的處理動作,需要 iOS 平台下的 NaviCustomPageRenderer 類別中來處理。
public partial class NaviCustomPage : NavigationPage
{
    #region DynamicBackButtonText 可綁定屬性 BindableProperty
    public static readonly BindableProperty DynamicBackButtonTextProperty =
        BindableProperty.Create("DynamicBackButtonText", // 屬性名稱 
            typeof(string), // 回傳類型 
            typeof(NaviCustomPage), // 宣告類型 
            "", // 預設值 
            propertyChanged: OnDynamicBackButtonTextChanged  // 屬性值異動時,要執行的事件委派方法
        );

    public string DynamicBackButtonText
    {
        set
        {
            SetValue(DynamicBackButtonTextProperty, value);
        }
        get
        {
            return (string)GetValue(DynamicBackButtonTextProperty);
        }
    }

    private static void OnDynamicBackButtonTextChanged(BindableObject bindable, object oldValue, object newValue)
    {
    }

    #endregion

    public NaviCustomPage()
    {
        InitializeComponent();
    }
}

指定使用來做頁面導航

在這個測試專案,將會在 App.xaml.cs 內,指定使用 NaviCustomPage 作為導航工具列
protected override async void OnInitialized()
{
    InitializeComponent();

    await NavigationService.NavigateAsync("MDPage/NaviCustomPage/MainPage");
}

設計附加屬性 DynamicBackButtonTextAttached

在 Xamarin.Forms 專案內,建立這個類別,在此建裡一個附加屬性。在建立這個 附加屬性 類別的時候,有指定這個 propertyChanged 事件,在此綁定到這個 OnSetBackButtonTextChanged 事件上。
當特定 ContentPage 頁面上有指定 DynamicBackButtonTextAttached.SetBackButtonText 屬性且屬性值有變動的時候,這個 OnSetBackButtonTextChanged 就會被觸發,不過,當時的頁面必須要有實作 IDynamicChangeBackText 介面,這樣,才能夠接下來做異動,這裡將會呼叫 ChangeBackButtonTextHelper.ChangeBackButtonText(newString); 方法,這裡將會變更導航頁面上的 DynamicBackButtonText 屬性
public class DynamicBackButtonTextAttached
{
    #region SetBackButtonText 附加屬性 Attached Property
    public static readonly BindableProperty SetBackButtonTextProperty =
           BindableProperty.CreateAttached(
               propertyName: "SetBackButtonText",   // 屬性名稱 
               returnType: typeof(string), // 回傳類型 
               declaringType: typeof(ContentPage), // 宣告類型 
               defaultValue: null, // 預設值 
               propertyChanged: OnSetBackButtonTextChanged  // 屬性值異動時,要執行的事件委派方法
           );

    public static void SetSetBackButtonText(BindableObject bindable, string entryType)
    {
        bindable.SetValue(SetBackButtonTextProperty, entryType);
    }
    public static string GetSetBackButtonText(BindableObject bindable)
    {
        return (string)bindable.GetValue(SetBackButtonTextProperty);
    }

    private static void OnSetBackButtonTextChanged(BindableObject bindable, object oldValue, object newValue)
    {
        ContentPage page = bindable as ContentPage;
        if (page == null) return;
        string oldString = oldValue as string;
        string newString = newValue as string;

        if (newString == null) return;

        if(page is IDynamicChangeBackText)
        {
            ChangeBackButtonTextHelper.ChangeBackButtonText(newString);
        }
    }
    #endregion
}

建立輔助支援方法,可以修改當前導航頁面上的 DynamicBackButtonText 屬性

在這裡將會使用 App.Current.MainPage 屬性來檢查當前的頁面是 MasterDetailPage 還是 NaviCustomPage ;若為 MasterDetailPage 頁面,對於 NaviCustomPage 可以透過 MasterDetailPage.Detail 取得;若當前頁面是 NaviCustomPage,則就可以直接取得這個物件。
有了 naviCustomPage 這個物件,就可以透過該物件來存取該導航頁面上剛剛建立的新可綁定屬性 naviCustomPage.DynamicBackButtonText,一旦變更這個屬性值,在 iOS 平台下的 NaviCustomPageRenderer 事件也會被觸發。
public class ChangeBackButtonTextHelper
{
    public static void ChangeBackButtonText(string newBackButtonText)
    {
        NaviCustomPage naviCustomPage = GetNaviCustomPage();
        if (naviCustomPage != null)
        {
            naviCustomPage.DynamicBackButtonText = newBackButtonText;
        };
    }
    public static string GetBackButtonText()
    {
        string result = "";
        NaviCustomPage naviCustomPage = GetNaviCustomPage();
        if (naviCustomPage != null)
        {
            result = naviCustomPage.DynamicBackButtonText;
        }
        return result;
    }
    public static NaviCustomPage GetNaviCustomPage()
    {
        NaviCustomPage naviCustomPage = null;
        if (App.Current.MainPage is MasterDetailPage)
        {
            MasterDetailPage masterDetailPage = App.Current.MainPage as MasterDetailPage;
            if (masterDetailPage.Detail is NaviCustomPage)
            {
                naviCustomPage = masterDetailPage.Detail as NaviCustomPage;
            }
        }
        else if (App.Current.MainPage is NaviCustomPage)
        {
            naviCustomPage = App.Current.MainPage as NaviCustomPage;
        }
        return naviCustomPage;
    }
}

對於要能夠變更回上頁按鈕文字的 ContentPage,需要實作 IDynamicChangeBackText

首先,對於 IDynamicChangeBackText 這個介面內,並沒有宣告什麼內容,只是為了要能夠分辨出該頁面是否可以透過剛剛設計的可綁定屬性來取得回上頁按鈕文字。
public interface IDynamicChangeBackText
{
}
對於要自動更新回上頁按鈕的 ContentPage,需要打開該頁面的 Code Behind C# 程式碼,實作這個 IDynamicChangeBackText 介面。
public partial class CanChangePage : ContentPage, IDynamicChangeBackText
{
    public CanChangePage()
    {
        InitializeComponent();
    }
}

在 iOS 平台下實作 NaviCustomPageRenderer

為了要能夠做出可變動回上頁按鈕,這裡在 iOS 平台下,建立 NaviCustomPageRenderer 類別,該類別需要實作 NavigationRenderer
在這裡需要覆寫 OnPushAsync 與 OnPopViewAsync 這兩個方法,當要透過導航工具列切換到不同頁面的時候,這個 OnPushAsync 方法就會被執行,在此方法內,若該新頁面有實作 IDynamicChangeBackText 這個介面,將會呼叫 SetBackButtonOnPage(page) 方法,該方法將會呼叫 ChangeBackButtonTextHelper.GetBackButtonText() 方法,取得當時設定到的回上頁按鈕自訂文字內容。
[assembly: ExportRenderer(typeof(NaviCustomPage), typeof(NaviCustomPageRenderer))]
namespace ChangeiOSBackButtonText.iOS.Renderers
{
    public class NaviCustomPageRenderer : NavigationRenderer
    {
        UIBarButtonItem barButtonItem;
        NaviCustomPage oldMyNaviPage;
        NaviCustomPage newMyNaviPage;

        protected override Task<bool> OnPushAsync(Page page, bool animated)
        {
            var retVal = base.OnPushAsync(page, animated);

            if (page is IDynamicChangeBackText)
            {
                SetBackButtonOnPage(page);
            }

            return retVal;
        }
        protected override Task<bool> OnPopViewAsync(Page page, bool animated)
        {
            var retVal = base.OnPopViewAsync(page, animated);

            if (page is IDynamicChangeBackText)
            {
                var stack = page.Navigation.NavigationStack;

                var returnPage = stack[stack.Count - 2];

                if (returnPage != null)
                {
                    SetBackButtonOnPage(returnPage);
                }
            }
            else
            {
                //SetDefaultBackButton();
            }

            return retVal;
        }

        void SetBackButtonOnPage(Page page)
        {
            //var stack = page.Navigation.NavigationStack;

            //if(stack.Count == 1)
            //{
            //    //SetDefaultBackButton();
            //}

            if (page is IDynamicChangeBackText)
            {
                string backButtonText = ChangeBackButtonTextHelper.GetBackButtonText();
                SetImageTitleBackButton("Left2", backButtonText, -15);
            }
            else
            {
                //SetDefaultBackButton();
            }

        }

        void SetImageTitleBackButton(string imageBundleName, string buttonTitle, int horizontalOffset)
        {
            var topVC = this.TopViewController;

            // Create the image back button
            var backButtonImage = new UIBarButtonItem(
                    UIImage.FromBundle(imageBundleName),
                    UIBarButtonItemStyle.Plain,
                    (sender, args) =>
                    {
                        topVC.NavigationController.PopViewController(true);
                    });

            // Create the Text Back Button
            //var backLeftButtonText = new UIBarButtonItem(
            //    "<",
            //    UIBarButtonItemStyle.Plain,
            //    (sender, args) =>
            //    {
            //        topVC.NavigationController.PopViewController(true);
            //    });

            // Create the Text Back Button
            var backButtonText = new UIBarButtonItem(
                buttonTitle,
                UIBarButtonItemStyle.Plain,
                (sender, args) =>
                {
                    topVC.NavigationController.PopViewController(true);
                });

            backButtonText.SetTitlePositionAdjustment(new UIOffset(horizontalOffset, 0), UIBarMetrics.Default);

            // Add buttons to the Top Bar
            UIBarButtonItem[] buttons = new UIBarButtonItem[2];
            buttons[0] = backButtonImage;
            buttons[1] = backButtonText;

            topVC.NavigationItem.LeftBarButtonItems = buttons;
        }

        void SetDefaultBackButton()
        {
            this.TopViewController.NavigationItem.LeftBarButtonItems = null;
        }

        protected override void OnElementChanged(VisualElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            oldMyNaviPage = (NaviCustomPage)e.OldElement;
            newMyNaviPage = (NaviCustomPage)e.NewElement;
            if (oldMyNaviPage != null)
            {
                oldMyNaviPage.PropertyChanged -= OnElementPropertyChanged;
            }
            if (newMyNaviPage != null)
            {
                newMyNaviPage.PropertyChanged += OnElementPropertyChanged;
            }
        }

        private void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "DynamicBackButtonText")
            {
                UpdateBackButtonTitleText();
            }
        }

        private void UpdateBackButtonTitleText()
        {
            string backTitle = "";
            backTitle= ChangeBackButtonTextHelper.GetBackButtonText();
            if (this.NavigationBar.Items.Count() > 1)
            {
                this.TopViewController.NavigationItem.LeftBarButtonItems[1].Title = backTitle;
            }
        }
    }
}



2019/06/05

Xamarin.Forms 的背景執行緒在 Android / iOS 背景模式下的執行情境測試

Xamarin.Forms 的背景執行緒在 Android / iOS 背景模式下的執行情境測試

對於已經具備擁有 .NET / C# 開發技能的開發者,可以使用 Xamarin.Forms Toolkit 開發工具,便可以立即開發出可以在 Android / iOS 平台上執行的 App;對於要學習如何使用 Xamarin.Forms & XAML 技能,現在已經推出兩本電子書來幫助大家學這這個開發技術。
這兩本電子書內包含了豐富的逐步開發教學內容與相關觀念、各種練習範例,歡迎各位購買。
Xamarin.Forms 電子書
想要購買 Xamarin.Forms 快速上手 電子書,請點選 這裡
想要購買 XAML in Xamarin.Forms 基礎篇 電子書,請點選 這裡


當在進行 Xamarin.Forms 專案開發的時候,必須要能夠了解 Android 與 iOS 應用程式生命週期 Application Life Cycle 的特性,最為重要的是,這兩個平台上對於生命週期的運作方式是不太相同的。原則上,所有的行動裝置應用程式都會分成前景、背景兩種模式,所謂的前景 Foreground 模式,就是該應用程式顯示在螢幕上,而背景 Background 模式,就是這個應用程式無法顯示在螢幕上,因為現在螢幕需要顯示其他應用程式的內容,關於這部分的詳細介紹,可以參考 Android 活動開發週期 與 iOS 中的背景處理簡介 這兩份文件。
當應用程式一起動的時候,此時這個應用程式將要顯示到螢幕上,就會觸發特定的事件,讓應用程式知道現在應用程式的已經進入到前景模式;而例如,當使用者按下手機上的 Home 按鍵,此時,這個應用程式就會切換到背景模式,當然,也會觸發特定的事件。
如同前面所說的,在 Android 與 iOS 系統下,會觸發的事件與可以觸發的事件項目都不相同,底下的圖片為 Android 作業系統下的 Activity 的生命週期狀態;當 Activity 建立後,就會觸發 OnCreate 事件,啟動之後,就會觸發 OnStart 事件;當應用程式按下了 Home 按鍵,就會觸發 OnPause的事件,使用者選擇切換到該應用程式,要讓該應用程式重新顯示到螢幕上,此時,將會觸發 OnRestart 與 OnStart 事件。
若現在的作業系統為 iOS ,此時對於應用程式生命週期相關會使用到的事件,將會如下圖。當應用程式啟動之後,將會觸發 OnActivated 事件,此時的狀態名稱為 Running 或者 Active;若使用者按下了 Home 按鍵,將會觸發 OnResignActivation 事件,此時,可以稱進入到 Inactive 狀態下,緊接著會在觸發 DidEnterBackground 事件,進入到 Background / Suspended 模式下;現在若使用者選擇要把這個應用程式讓他回到螢幕上,這個時候,就會觸發了 WillEnterForeground 事件,如下面流程圖。
然而,在 Xamarin.Forms 中,也會有一個應用程式生命週期的運作模式與特定的事件,只不過在 Xamarin.Forms App 生命週期內就簡單多了,在 Xamarin.Forms 內只有三種生命週期事件
  • OnStart - 會在應用程式啟動時呼叫。
  • OnSleep - 會在每次應用程式被移到背景時呼叫。
  • OnResume - 會在應用程式被傳送到背景後又再次繼續時呼叫。
這些事件可以從 Xamarin.Forms 專案內的 App 類別中來訂閱。

測試用的專案範例解說

這裡將會使用底下的 Xamarin.Forms 專案,進行 Android / iOS 兩個平台的不同生命週期狀態下的程式碼執行狀態來了解,在這裡,將會設計一個按鈕,當按下這個按鈕之後,將會執行 60 次的迴圈,每次迴圈將會休息兩秒鐘,並且將計數器變數加一,而該計數器屬性將會透過資料綁定的方式,將這個計數器值顯示到螢幕上。此時,也會使用 Console.WriteLine 方法,將現在迴圈的 Index 值顯示到螢幕上,所以,可以從 Visual Studio 2019 的輸出視窗中看到這個程式是否還有繼續在執行中;另外,這些相關執行日誌內容,也會寫到檔案中,以便當這個程式在實體手機上,不透過 Visual Studio 來執行,也可以看到這些 Log 執行過程內容。
這裡是這個應用程式的 XAML 設定內容。
xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XF5007.Views.MainPage"
             Title="背景執行緒與背景模式">

    <ScrollView
        Orientation="Both"
        >
        <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
            <Label Text="Welcome to Xamarin Forms and Prism!" />
            <Label Text="{Binding AppLifeStatusRecord}"
               FontSize="{OnPlatform 14, iOS=14}">
            </Label>

            <Label
            Text="{Binding Counter}"
            FontSize="30"
            TextColor="Red"/>
            <Button
            Text="開始定時執行"
            Command="{Binding StartCommand}"/>
            <StackLayout
            Orientation="Horizontal"
            >
                <Button Text="Read" Command="{Binding ReadCommand}"/>
                <Button Text="Reset" Command="{Binding ResetCommand}"/>
            </StackLayout>
        </StackLayout>
    </ScrollView>

</ContentPage>
這裡是這個頁面的 ViewModel,定義了三個按鈕的命令行為,在這裡將會透過 AppLifeStatusRecord 類別內的 ReadAsync / WriteAsync 這兩個方法,進行日誌的檔案讀寫需求。
public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
{
    public event PropertyChangedEventHandler PropertyChanged;
    public DelegateCommand StartCommand { get; set; }
    public DelegateCommand ReadCommand { get; set; }
    public DelegateCommand ResetCommand { get; set; }
    public int Counter { get; set; }
    public string AppLifeStatusRecord { get; set; }
    private readonly INavigationService navigationService;

    public MainPageViewModel(INavigationService navigationService)
    {
        this.navigationService = navigationService;
        ReadCommand = new DelegateCommand(async () =>
        {
            AppLifeStatusRecord = await new AppLifeStatusService().ReadAsync();
        });
        ResetCommand = new DelegateCommand(async () =>
        {
            await new AppLifeStatusService().WriteAsync("", true);
            AppLifeStatusRecord = "";
        });
        StartCommand = new DelegateCommand(async () =>
        {
            for (int i = 0; i < 60; i++)
            {
                await Task.Delay(2000);
                Counter++;
                Console.WriteLine($"   === {i} ===");
                await new AppLifeStatusService().WriteAsync($"     Xamarin.Forms= {i} = > Timer - {DateTime.Now.Minute}:{DateTime.Now.Second}");
            }
        });
    }

    public void OnNavigatedFrom(INavigationParameters parameters)
    {
    }

    public void OnNavigatedTo(INavigationParameters parameters)
    {
    }

    public void OnNavigatingTo(INavigationParameters parameters)
    {
    }
}

如何訂用 Xamarin.Forms 的應用程式生命週期的相關事件

請在 Xamarin.Forms 專案中,找到 App.xaml.cs 節點,從這個節點內的 App 類別中,加入底下三個覆寫方法,所以,當應用程式在前景與背景模式下切換的時候,就會觸發這些事件。
protected override async void OnStart()
{
    IsAppInForeground = true;
    new AppLifeStatusService().WriteAsync($"Xamarin.Forms>OnStart - {DateTime.Now.Minute}:{DateTime.Now.Second} - 執行緒 {Thread.CurrentThread.ManagedThreadId}");
}
protected override async void OnSleep()
{
    IsAppInForeground = false;
    new AppLifeStatusService().WriteAsync($"Xamarin.Forms>OnSleep - {DateTime.Now.Minute}:{DateTime.Now.Second} - 執行緒 {Thread.CurrentThread.ManagedThreadId}");
}
protected override async void OnResume()
{
    IsAppInForeground = true;
    new AppLifeStatusService().WriteAsync($"Xamarin.Forms>OnResume - {DateTime.Now.Minute}:{DateTime.Now.Second} - 執行緒 {Thread.CurrentThread.ManagedThreadId}");
}

如何訂用 Xamarin.Android 的應用程式生命週期的相關事件

請在 Xamarin.Android 專案中,找到 MainActivity.cs 節點,從這個節點內的 MainActivity 類別中,加入底下覆寫方法,所以,當一個 Android 平台下的應用程式在前景與背景模式下切換的時候,就會觸發這些事件。
protected override void OnStart()
{
    base.OnStart();
     new AppLifeStatusService().WriteAsync($"     Android>OnStart - {DateTime.Now.Minute}:{DateTime.Now.Second} - 執行緒 {Thread.CurrentThread.ManagedThreadId}");
}
protected override void OnResume()
{
    base.OnResume();
     new AppLifeStatusService().WriteAsync($"     Android>OnResume - {DateTime.Now.Minute}:{DateTime.Now.Second} - 執行緒 {Thread.CurrentThread.ManagedThreadId}"); 
}
protected override void OnPause()
{
    base.OnPause();
     new AppLifeStatusService().WriteAsync($"     Android>OnPause - {DateTime.Now.Minute}:{DateTime.Now.Second} - 執行緒 {Thread.CurrentThread.ManagedThreadId}"); 
}
protected override void OnStop()
{
    base.OnStop();
     new AppLifeStatusService().WriteAsync($"     Android>OnStop - {DateTime.Now.Minute}:{DateTime.Now.Second} - 執行緒 {Thread.CurrentThread.ManagedThreadId}");
}
protected override void OnRestart()
{
    base.OnRestart();
     new AppLifeStatusService().WriteAsync($"     Android>OnRestart - {DateTime.Now.Minute}:{DateTime.Now.Second} - 執行緒 {Thread.CurrentThread.ManagedThreadId}");
}
protected override void OnDestroy()
{
    base.OnDestroy();
     new AppLifeStatusService().WriteAsync($"     Android>OnDestroy - {DateTime.Now.Minute}:{DateTime.Now.Second} - 執行緒 {Thread.CurrentThread.ManagedThreadId}");
}

如何訂用 Xamarin.iOS 的應用程式生命週期的相關事件

請在 Xamarin.iOS 專案中,找到 AppDelegate.cs 節點,從這個節點內的 AppDelegate 類別中,加入底下覆寫方法,所以,當一個 iOS 平台下的應用程式在前景與背景模式下切換的時候,就會觸發這些事件。
public override void OnActivated(UIApplication application)
{
    base.OnActivated(application);
    new AppLifeStatusService().WriteAsync($"     iOS>OnActivated - {DateTime.Now.Minute}:{DateTime.Now.Second} - 執行緒 {Thread.CurrentThread.ManagedThreadId}");
}
public override void WillEnterForeground(UIApplication application)
{
    base.WillEnterForeground(application);
    new AppLifeStatusService().WriteAsync($"     iOS>WillEnterForeground - {DateTime.Now.Minute}:{DateTime.Now.Second} - 執行緒 {Thread.CurrentThread.ManagedThreadId}");
}
public override void OnResignActivation(UIApplication application)
{
    base.OnResignActivation(application);
    new AppLifeStatusService().WriteAsync($"     iOS>OnResignActivation - {DateTime.Now.Minute}:{DateTime.Now.Second} - 執行緒 {Thread.CurrentThread.ManagedThreadId}");
}
public override void DidEnterBackground(UIApplication application)
{
    base.DidEnterBackground(application);
    new AppLifeStatusService().WriteAsync($"     iOS>DidEnterBackground - {DateTime.Now.Minute}:{DateTime.Now.Second} - 執行緒 {Thread.CurrentThread.ManagedThreadId}");
}
// not guaranteed that this will run
public override void WillTerminate(UIApplication application)
{
    base.WillTerminate(application);
    new AppLifeStatusService().WriteAsync($"     iOS>WillTerminate - {DateTime.Now.Minute}:{DateTime.Now.Second} - 執行緒 {Thread.CurrentThread.ManagedThreadId}");
}

開始進行 Android 平台測試

當把這個測試範例專案在 Android 環境下啟動執行之後,可以從 Visual Studio 2019 的輸出視窗中看到底下的輸出日誌,這部分可以對照上面所提到的 應用程式生命週期 說明內容。
   --> Xamarin.Forms>OnStart - 46:53 - 執行緒 1
   -->      Android>OnStart - 46:53 - 執行緒 1
   -->      Android>OnResume - 46:53 - 執行緒 1
下面螢幕截圖將會是這個應用程式的執行結果
現在要點選螢幕上的 [開始定時執行] 這個按鈕,當紅色數字跑到 3 的時候,請點選到 Home 按鍵,這個時候應用程式將會被推到不可見的背景模式,請等候一分鐘左右的時間,將這個 App 切換到可見的前景模式。當應用程式回到可見前景模式,此時看到紅色數字已經變成 40 了。
 
當這個應用程式切換到背景不可見模式下的時候,可以從 Visual Studio 2019 的輸出視窗內,看到還是有不斷的輸出日誌顯示出來,這就表示了,雖然這個 Android 應用程式切換到不可見的背景模式下,可是,他還是會繼續的執行。
   === 0 ===
   -->      Xamarin.Forms= 0 = > Timer - 43:52
   === 1 ===
   -->      Xamarin.Forms= 1 = > Timer - 43:54
   === 2 ===
   -->      Xamarin.Forms= 2 = > Timer - 43:56
   -->      Android>OnPause - 43:57 - 執行緒 1
   --> Xamarin.Forms>OnSleep - 43:57 - 執行緒 1
   -->      Android>OnStop - 43:58 - 執行緒 1
   === 3 ===
   -->      Xamarin.Forms= 3 = > Timer - 43:58
   === 4 ===
   -->      Xamarin.Forms= 4 = > Timer - 44:00
   === 5 ===
   -->      Xamarin.Forms= 5 = > Timer - 44:20
   === 6 ===
   -->      Xamarin.Forms= 6 = > Timer - 44:40
   === 7 ===
   -->      Xamarin.Forms= 7 = > Timer - 44:60
   === 8 ===
   -->      Xamarin.Forms= 8 = > Timer - 44:80
   === 9 ===
   -->      Xamarin.Forms= 9 = > Timer - 44:10
   === 10 ===
   -->      Xamarin.Forms= 10 = > Timer - 44:12

...

   === 33 ===
   -->      Xamarin.Forms= 33 = > Timer - 44:58
   === 34 ===
   -->      Xamarin.Forms= 34 = > Timer - 45:0
   --> Xamarin.Forms>OnResume - 45:2 - 執行緒 1
   -->      Android>OnRestart - 45:2 - 執行緒 1
   === 35 ===
   -->      Xamarin.Forms= 35 = > Timer - 45:2
   === 36 ===
   -->      Xamarin.Forms= 36 = > Timer - 45:5
   === 37 ===
   -->      Xamarin.Forms= 37 = > Timer - 45:7
   === 38 ===
   -->      Xamarin.Forms= 38 = > Timer - 45:9

開始進行 iOS 平台測試

當把這個測試範例專案在 iOS 環境下啟動執行之後,可以從 Visual Studio 2019 的輸出視窗中看到底下的輸出日誌,這部分可以對照上面所提到的 應用程式生命週期 說明內容。
    --> Xamarin.Forms>OnStart - 45:55 - 執行緒 1
    -->      iOS>OnActivated - 45:55 - 執行緒 1
下面螢幕截圖將會是這個應用程式的執行結果
現在要點選螢幕上的 [開始定時執行] 這個按鈕,當紅色數字跑到 3 的時候,請點選到 Home 按鍵,這個時候應用程式將會被推到不可見的背景模式。現在,App 已經在不可見的背景模式下,請觀察 Visual Stuio 2019 的輸出視窗,應該不像是 Android 應用程式,此時輸出視窗內是沒有任何執行日誌輸出到輸出視窗內。
    === 0 ===
    -->      Xamarin.Forms= 0 = > Timer - 49:35
    === 1 ===
    -->      Xamarin.Forms= 1 = > Timer - 49:37
    === 2 ===
    -->      Xamarin.Forms= 2 = > Timer - 49:39
    --> Xamarin.Forms>OnSleep - 49:40 - 執行緒 1
    -->      iOS>OnResignActivation - 49:40 - 執行緒 1
    -->      iOS>DidEnterBackground - 49:41 - 執行緒 1
    === 3 ===
    -->      Xamarin.Forms= 3 = > Timer - 49:41
請等候一分鐘左右的時間,將這個 App 切換到可見的前景模式,當應用程式回到可見前景模式,此時看到紅色數字已經變成 6 了,這裡的紅色數字也與 Android 平台下運作不相同。
 
底下是在 iOS 平台下,當 App 從不可見背景模式切換到可見的模式下,在 Visual Studio 2019 輸出視窗內再度寫入的內容。因此,可以知道,對 iOS App,當應用程式切換到不可見的背景模式下,這個 App 的任何執行緒是沒有且無法做任何事情的。
+

    -->      iOS>WillEnterForeground - 54:31 - 執行緒 1
    === 4 ===
    -->      Xamarin.Forms= 4 = > Timer - 54:31
    --> Xamarin.Forms>OnResume - 54:31 - 執行緒 1
    -->      iOS>OnActivated - 54:31 - 執行緒 1
    === 5 ===
    -->      Xamarin.Forms= 5 = > Timer - 54:33
    === 6 ===
    -->      Xamarin.Forms= 6 = > Timer - 54:35
    === 7 ===
    -->      Xamarin.Forms= 7 = > Timer - 54:37
    === 8 ===
    -->      Xamarin.Forms= 8 = > Timer - 54:39
    === 9 ===
    -->      Xamarin.Forms= 9 = > Timer - 54:41


了解更多關於 [Xamarin.Android] 的使用方式
了解更多關於 [Xamarin.iOS] 的使用方式
了解更多關於 [Xamarin.Forms] 的使用方式
了解更多關於 [Hello, Android:快速入門] 的使用方式
了解更多關於 [Hello, iOS – 快速入門] 的使用方式
了解更多關於 [Xamarin.Forms 快速入門] 的使用方式