XAML in Xamarin.Forms 基礎篇 電子書

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

Xamarin.Forms 快速入門 電子書

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

2017/10/02

如何在 Xamarin.Forms 中執行結束應用程式的功能

大家經常都在詢問這樣的問題:如何在 Xamarin.Forms 中執行結束應用程式的功能

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

了解更多關於 [Xamarin.Forms 快速入門] 的使用方式

我個人認為,這個問題,不是技術上的問題,而是問問題的人,需要對於您開發的應用程式所在的平台上,了解這個平台提供的應用程式生命週期;一旦你對於 iOS & Android 平台的應用程式生命週期有了解之後,這個問題也就解決了;喔,你們沒有聽錯,可是,我們有看到任何的技術方法呀,可否告訴我究竟要呼叫甚麼 API 才能夠結束應用程式的執行嗎?
這個答案是很明確的,在行動裝置作業系統中,iOS / Android ,想要結束您開發的應用程式執行,在各自的應用程式生命週期中,只有兩個方法,一個就是當作業系統資源不足夠的時候,作業系統有權將任何應用程式立即結束執行;另外一個就是使用者可以透過手勢或者作業系統提供的操作介面,手動的結束特定應用程式的執行。
從應用程式的生命週期,每個應用程式是沒有透過 App 結束執行這件事情,只有在前景執行或者移到背景去等候再度回到前景來執行。所以,請不要逢人就詢問 如何在 Xamarin.Forms 中執行結束應用程式的功能,這不是 Xamarin.Forms 也不是 Xamarin 可以做到的事情,原則上,只要作業系統有提供這樣的 API,你就可以在 Xamarin.Forms 呼叫他來使用。
在這篇文章中 How do I programmatically quit my iOS application?,你會看到蘋果官方的回覆:There is no API provided for gracefully terminating an iOS application.。對於不死心的人,還是想要這麼做,那麼,請自行觀看蘋果相關的上架說明文件(不要沒看文件就到處用您的智慧來想像該怎麼做),強制這麼做的話,是無法上架App到公開的 App Store 市集上。
好的,若您還是堅持要這麼做,我們來看看在 Xamarin.Form 的開發環境中,該如何做到這樣的需求,在這裡,我們設計一個按鈕,當使用者點選這個按鈕之後,就會 ViewModel 內,執行一個注入原生平台的物件,執行結束應用程式的功能。

建立一個結束應用程式的介面

首先,我們在 Xamarin.Forms 專案內,建立一個介面,這個介面裡面只有定義一個方法 Exit(),當使用者呼叫這個方法之後,就會結束這個應用程式的執行。
    public interface IAppExit
    {
        void Exit();
    }

測試頁面 View / ViewModel

我們的測試 App 很簡單,只有一個按鈕
<?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="XFExit.Views.MainPage"
             Title="MainPage">
    <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
        <Label Text="{Binding Title}" />
        <Button
            Text="結束應用程式"
            Command="{Binding CloseAppCommand}"/>
    </StackLayout>
</ContentPage>
當按下了 結束應用程式 按鈕之後,就會執行 ViewModel 內的 CloseAppCommand DelegateCommand 物件方法。
在底下的測試範例 ViewModel 中,我們透過相依性服務注入技術,將 IAppExit 實作物件注入到 ViewModel 內,並且執行 _AppExit.Exit(); 方法之後,就會結束應用程式的執行。
    public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private readonly INavigationService _navigationService;

        public DelegateCommand CloseAppCommand { get; set; }
        IAppExit _AppExit;
        public MainPageViewModel(INavigationService navigationService, IAppExit appExit)
        {
            _navigationService = navigationService;
            _AppExit = appExit;
            CloseAppCommand = new DelegateCommand(() =>
            {
                _AppExit.Exit();
            });
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public void OnNavigatingTo(NavigationParameters parameters)
        {

        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {

        }

    }

Android 平台實作 IAppExit

現在,我們需要在 Android 專案內,建立一個類別 AppExit,這個類別需要實作 IAppExit 介面的方法,我們在 Exit() 這個方法中,呼叫 Android.OS.Process.KillProcess(Android.OS.Process.MyPid());,這樣,當這個 Android 平台的 Xamarin.Forms 專案執行的時候,就會結束這個應用程式的執行。
[assembly: Xamarin.Forms.Dependency(typeof(AppExit))]
namespace XFExit.Droid.Servoces
{
    class AppExit : IAppExit
    {
        public void Exit()
        {
            Android.OS.Process.KillProcess(Android.OS.Process.MyPid());
        }
    }
}

iOS 平台實作 IAppExit

現在,我們需要在 iOS 專案內,建立一個類別 AppExit,這個類別需要實作 IAppExit 介面的方法,我們在 Exit() 這個方法中,呼叫 System.Diagnostics.Process.GetCurrentProcess().CloseMainWindow();,這樣,當這個 iOS 平台的 Xamarin.Forms 專案執行的時候,就會結束這個應用程式的執行。
當然,你也可以執行 .NET 平台的中止執行續的方法:Thread.CurrentThread.Abort(); 這樣,也會結束這個應用程式的執行。
[assembly: Xamarin.Forms.Dependency(typeof(AppExit))]
namespace XFExit.iOS.Services
{
    public class AppExit : IAppExit
    {
        public void Exit()
        {
            System.Diagnostics.Process.GetCurrentProcess().CloseMainWindow();
            //Thread.CurrentThread.Abort();
        }
    }
}

2017/09/29

Xamarin.Forms 頁面導航是否會造成頁面物件記憶體洩漏 Memory Leak

曾經有人問過我,在進行頁面導航的時候,使用 Xamarin.Forms 的專案樣板精靈做出來的專案,並且使用 call behind 後置程式碼的方式,呼叫 Navigation.PushAsync 方法進行頁面導航與使用 Prism 專案樣板精靈做出來的 Xamarin.Forms for Prism 專案,使用 ViewModel 的 INavigationService 來進行頁面導航;其中一個會造成頁面的記憶體洩漏,講白話一點,就是在進行頁面導航操作過程中,頁面無法被記憶體回收程序 Garbage Collection 進行回收,一直殘留在系統裡;若反覆進行這樣的操作,將會造成這個應用程式耗用大量的記憶體,最後終究會使得產生記憶體不足 Memory Overflow 的例外異常問題。
想要知道是否會造成這樣的現象,最簡單的方式,那就是寫個測試專案,並且來跑看看,不過,在 .NET 運作環境下,一個應用程式是無法自己手動來釋放掉變數持有的記憶體空間,而且,沒有被使用到的物件,何時會被釋放掉這些記憶體,也不是程式設計可以來決定的;若你想要更加清楚的瞭解這些機制如何運作,你需要深入去了解 .NET 通用語言執行階段 CLR Common Language Runtime 這個元件的核心運作機制,不過,我們在這裡並不會去介紹這些功能。

準備建立兩個測試專案

好的,在這裡,我們使用 Visual Studio 2017 建立兩個專案
  • Xamarin.Forms 跨平台專案
    檔案 > 新增 > 專案 > Visual C# > Cross-Platform > Cross Platrorm App (Xamarin)
    這裡所有的頁面導航等商業邏輯,都會使用 Call Behind 的方式來撰寫
  • Xamarin.Forms for Prism 專案
    檔案 > 新增 > 專案 > Visual C# > Prism > Prism Unity App (Xamarin.Forms)
    這裡所有的頁面導航等商業邏輯,都會使用 MVVM 的方式,在 ViewModel 來撰寫
在這兩個專案內,都會具有底下的頁面導航方式
MainPage > Pg1Page > Pg2Page
其中,在 Pg1Page 與 Pg2Page 這兩個頁面,都提供了導航工具列與頁面按鈕的返回上一頁功能。
下圖是這兩個專案所使用的首頁頁面,其中,GC 按鈕將會驅使 .NET CLR 進行記憶體沒有被參考到的物件之回收作業,而 RESET 按鈕,則是會使用絕對導航的方式,重新設定新的 MainPage 為這個應用程式的第一個首頁頁面。
NaviMemLeak1
底下的程式碼為 Xamarin.Forms 跨平台專案 的 MainPage 的 Call Behind 程式碼。
public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    private void GotoPage1_Clicked(object sender, EventArgs e)
    {
        Navigation.PushAsync(new Pg1Page());
    }

    private void NeedGC_Clicked(object sender, EventArgs e)
    {
        GC.Collect();           
    }

    private void Reset_Clicked(object sender, EventArgs e)
    {
        ((Application)App.Current).MainPage = new NavigationPage(new App1.MainPage());

    }
}
底下的程式碼為 Xamarin.Forms for Prism 專案 的 MainPage 頁面會使用到的 ViewModel 程式碼。
public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
{
    public event PropertyChangedEventHandler PropertyChanged;

    private readonly INavigationService _navigationService;

    public DelegateCommand GotoPage1Command { get; set; }
    public DelegateCommand GCCommand { get; set; }
    public DelegateCommand ResetCommand { get; set; }

    public MainPageViewModel(INavigationService navigationService)
    {
        _navigationService = navigationService;

        GotoPage1Command = new DelegateCommand(() =>
        {
            _navigationService.NavigateAsync("Pg1Page");
        });
        ResetCommand = new DelegateCommand(() =>
        {
            _navigationService.NavigateAsync("xf:///NavigationPage/MainPage?title=Hello%20from%20Xamarin.Forms");
        });
        GCCommand = new DelegateCommand(() =>
        {
            GC.Collect();
        });
    }

    public void OnNavigatedFrom(NavigationParameters parameters)
    {

    }

    public void OnNavigatingTo(NavigationParameters parameters)
    {

    }

    public void OnNavigatedTo(NavigationParameters parameters)
    {

    }

}
為了要能夠知道所這些葉面是否有被 CLR GC 將其物件所使用的記憶體回收,我們將會在 Pg1Page / Pg2Pag2 這兩個頁面類別內,分別使用解構函式,顯示出一段訊息,這個訊息,將會於 CLR 要釋放這個頁面物件之前,顯示出來,並且於顯示出來之後,該頁面物件就會隨即於記憶體釋放掉(相關運作方式與原理,請參考通用語言執行階段的相關文件)。
在底下的程式碼,為 Pg1Page 頁面的 Call Behind 的程式碼,我們在這裡宣告一個屬性 Index,該 Index 的屬性值將會於建構式中進行初始化,用來標示出這是第幾個產生的頁面指標。
public partial class Pg1Page : ContentPage
{
    public int Index { get; set; }
    ~Pg1Page()
    {
        Debug.WriteLine($"----------------- Release Pg1Page [{Index}]");
    }
    public Pg1Page()
    {
        InitializeComponent();

        Index = GlobalMember.Pg1Count;
        GlobalMember.Pg1Count++;
    }
}
而 GlobalMember.Pg1Count 這是個靜態變數,定義於底下 GlobalMember 類別中。
class GlobalMember
{
    public static int Pg1Count { get; set; } = 1;
    public static int Pg2Count { get; set; } = 901;
}

進行 Xamarin.Forms 跨平台專案 測試

這時,我們執行 Xamarin.Forms 跨平台專案 專案,並且依照底下流程進行頁面切換
MainPage > Pg1Page > Pg2Page > Pg1Page > MainPage
當操作完成之後,Visual Studio 的輸出視窗內並沒有任何解構函式的輸出內容,此時,我們點選 GC 按鈕,這個時候,就會出現底下訊息;你會看到 Pg2Page頁面物件已經從記憶體中移除了。
----------------- Release Pg2Page [901]
現在,讓我們再度進行同樣的頁面切換
MainPage > Pg1Page > Pg2Page > Pg1Page > MainPage
接著,按下 GC 按鈕,會看到底下內容,這個時候,您將會看到第一次進行頁面切換的 Pg1Page 頁面物件因為沒有物件參考到他,所以,他被記憶體回收了,當然,Pg2Page頁面物件同樣的也被回收了,
----------------- Release Pg2Page [902]
----------------- Release Pg1Page [1]
現在,讓我們再度進行同樣的頁面切換
MainPage > Pg1Page > Pg2Page > Pg1Page > MainPage
接著,按下 RESET 按鈕,接著按下 GC 按鈕,會看到底下內容,此時,所有的子頁面物件就都全部被 GC 釋放掉了
----------------- Release Pg1Page [3]
----------------- Release Pg2Page [903]
----------------- Release Pg1Page [2]

進行 Xamarin.Forms for Prism 專案 測試

這時,我們執行 Xamarin.Forms for Prism 專案 專案,並且依照上面的操作過程,您將會得到一樣的結果,這表示您使用 Prims 框架來建立起來的跨平台行動應用程式,確實會將梅有被參考到的頁面物件予以釋放掉。而最前面所質疑的問題,就獲得解答了。

測試範例專案

在 Visual Studio 2017 上,安裝 .NET Standard 2.0 標準類別庫支援

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

  • 首先,請先將您的 Visual Studio 2107 升級到 15.3 以上的版本
  • 請安裝指定 SDK
  • 請依照指示,繼續安裝
    .NET Core 2.0 Install
    .NET Core 2.0 Install
    .NET Core 2.0 Install
  • 完成安裝

進行檢測

  • 開啟 Visual Studio 2017
  • 選擇功能表 檔案 > 新增 > 專案 > 已安裝 > Visual C# > .NET Standard > 類別庫 (.NET Standard)
    建立一個 .NET Standard 標準類別庫
    .NET Core 2.0 Install
  • 滑鼠右擊該專案的節點,選擇 屬性
    點選 目標 Framework 的下拉選單控制項,從這裡,你就可以切換使用 .NET Standard 2.0
    .NET Core 2.0 Install

2017/09/28

利用 UnhandledException 與 UnobservedTaskException 找出 Xamarin 的 "An unhandled exception occured" 錯誤問題

我們在進行 Xamarin.Forms 專案開發的時候,偶而會遇到這樣的例外錯誤訊息 An unhandled exception occured
unhandled exception1

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

可是,從這個畫面上,也看不出問題發生在哪裡,若我們點選 複製詳細資料 ,則會得到這樣的內容:An unhandled exception occured. 發生
結果,還是一樣,無法看到何有幫助的訊息。
在我們這裡,可以使用 C# 內建的類別 應用程式定義域 (Application Domain) 裡面的一個事件 UnhandledException;一旦你訂閱了這個事件,並且應用程式發生了沒有捕捉到的任何例外異常,這個訂閱的事件方法,將會被呼叫執行。
這個事件會接收一個客製化的事件引數類別 UnobservedTaskExceptionEventArgs 我們可以從這個參數中,得到真正發生例外異常的情況。
若想要使用這樣的功能,您需要在 Xamarin 原生專案的進入點加入訂閱這個事件,底下為在 Android 平台專案下的 MainActivity.cs的程式碼使用範例。
protected override void OnCreate(Bundle bundle)
{
    AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
    TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
    TabLayoutResource = Resource.Layout.tabs;
    ToolbarResource = Resource.Layout.toolbar;

    base.OnCreate(bundle);

    global::Xamarin.Forms.Forms.Init(this, bundle);
    LoadApplication(new App(new AndroidInitializer()));
}

private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
    throw new NotImplementedException();
}

private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
    throw new NotImplementedException();
}
所以,你可以在 CurrentDomain_UnhandledException 設定一個中斷點,並且執行這個有問題的專案,若看到 An unhandled exception occured 這個錯誤訊息出現,則按下 F5 繼續來執行,直到在你剛剛設定的中斷點停下來;這個時候,你就可以查看 e 這個參數值,看到問題原因說明了,在這個範例中,我們看到的是 Master and Detail must be set before using a MasterDetailPage
unhandled exception1
unhandled exception1
此時,我們開啟 MasterDetailPage.xaml 檔案,您就會知道問題出在哪裡了。
<?xml version="1.0" encoding="utf-8" ?>
<MasterDetailPage 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="PrismUnityApp4.Views.MDPage">

</MasterDetailPage>

2017/09/25

.NET Framework / PCL 可攜式類別庫 / .NET Standard 標準類別庫 之中繼套件與類型轉送 深入探究

在這裡,我們透過 IL 中繼語言的反組譯工具 ILSpy 來查看這三個 .NET 生態環境的內容。

.NET Framework

我們使用 ILSpy 工具,打開 C:\Windows\Microsoft.NET\Framework\v4.0.30319 目錄,找到 mscorlib.dll這個檔案
接著展開其 mscorlib 節點,接著再展開 System.Collections.Generic 節點,就會看到 List<T> 節點,點擊這個節點,就會看到這個類別的原始 C# 原始程式碼。

PCL 可攜式類別庫

在這裡,我們使用 PCL Profile259 這個版本
我們使用 ILSpy 工具,打開 C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.5\Profile\Profile259\ 目錄,這裡就是 Profile 259 會使用到的類別庫組件所在位置,找到 System.Collections.dll 這個檔案
這個位置,可以從 Visual Studio 中,打開任意一個 PCL 可攜式專案,點選 參考 > .NET 節點,從屬性視窗的 路徑 中,就可以查到,如下圖所示
接著展開其 System.Collections 節點,接著再展開 System.Collections.Generic 節點,就會看到 List<T> 節點,點擊這個節點,就會看到這個類別的原始 C# 原始程式碼。
不過,在這裡,似乎你只看到了這個 List 類別的成員定義,而成員的方法似乎都沒有實做出來。
讓我們繼續使用 ILSpy 工具,打開 C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.5\Profile\Profile259\ 目錄,這裡就是 Profile 259 會使用到的類別庫組件所在位置,找到 mscorlib.dll 這個檔案
當你點選剛剛開啟的 mscorlib.dll 的 mscorlib 節點,從右邊分割視窗中,您會看到這個敘述 [assembly: TypeForwardedTo(typeof(List<>))] 這表示,在執行時期,若你的專案中有使用這個 List 的泛型型別,則真正實作的組件,將不會在這個組件內定義,而是使用了 Type Forwarding 類型轉送 技術,需要到其他的組件上來找到這個類別的實作定義。根據微軟官方文件上的描述:型別轉送可讓您將某種型別移到其他組件,而不需重新編譯使用原始組件的應用程式

.NET Standard 標準類別庫

在這裡,請先下載 NETStandard.Library NuGet 套件到本機上,使用 zip 解壓縮工具,就可以解開這個 netstandard.library.2.0.0.nupkg 壓縮檔案。
當你在 Visual Studio 內,打開任意 .NET Standard 標準類別庫,就會看到如下圖
.NET Standard 標準類別庫
我們使用 ILSpy 工具,打開解壓縮 (netstandard.library.2.0.0.nupkg 壓縮檔案) 後的目錄 netstandard.library.2.0.0.nupkg\build\netstandard2.0\ref ,這裡就是 NETStandard.Library 會使用到的類別庫組件所在位置,找到 netstandard.dll 這個檔案
接著展開其 netstandard 節點,接著再展開 System.Collections.Generic 節點,就會看到 List<T> 節點,點擊這個節點,就會看到這個類別的原始 C# 原始程式碼。
不過,在這裡,似乎你只看到了這個 List 類別的成員定義,而成員的方法似乎都沒有實做出來。
讓我們繼續使用 ILSpy 工具,打開解壓縮 (netstandard.library.2.0.0.nupkg 壓縮檔案) 後的目錄 netstandard.library.2.0.0.nupkg\build\netstandard2.0\ref,這裡就是 NETStandard.Library 會使用到的類別庫組件所在位置,找到 mscorlib.dll 這個檔案
當你點選剛剛開啟的 mscorlib.dll 的 mscorlib 節點,從右邊分割視窗中,您會看到這個敘述 [assembly: TypeForwardedTo(typeof(List))] 這表示,在執行時期,若你的專案中有使用這個 List 的泛型型別,則真正實作的組件,將不會在這個組件內定義,而是使用了 Type Forwarding 類型轉送 技術,需要到其他的組件上來找到這個類別的實作定義。根據微軟官方文件上的描述:型別轉送可讓您將某種型別移到其他組件,而不需重新編譯使用原始組件的應用程式

總結

從上面的檢測過程,我們可以知道,不論 PCL 或者 .NET Standard 這兩個,他們使用的核心技術原則上是相同的,只不過對於中繼套件的使用方式與可以使用那些平台的 API 的規劃設定方式不同。
在 .NET Standard,中繼套件 (原始程式碼) 描述定義 (部分) 一個或多個 .NET 標準程式庫版本的程式庫集合,並且,以 NuGet 套件散發並由 NETStandard.Library 中繼套件參考的參考組件;而 PCL 的中繼套件則是存在於本機上的某個目錄中,這是隨著你的 Visual Studio 安裝的同時,也就會安裝進去的。
+

關於最後真正平台要使用各 API 實作,則是使用 Type Forwarding 類型轉送 技術,在執行階段,動態的進行使用真正實作的組件。

參考資料