XAML in Xamarin.Forms 基礎篇 電子書

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

Xamarin.Forms 快速入門 電子書

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

2017/10/07

Xamarin.Forms - 從網路下載檔案,並且儲存到手機中,接著,可以使用安裝在手機內的應用程式,開啟這個檔案

這是一份 Xamarin.Forms 高等進階開發技術筆記,我們在這裡將會針對如何設計一個 Xamarin.Forms 應用專案,可以下載網路上的圖片、音樂、影片、文件檔案(pdf, ppt, doc, xls等等)到手機上,並且使用手機上預設安裝的 App,來開啟這些已經下載的檔案。
想要完成這樣的需求程式設計,您需要了解如何在 Xamarin.Forms 使用底下的技術:
  • MVVM開發框架,在這裡,我們使用 Prism 作為我們開發與設計 MVVM 架構的框架
  • 安裝與使用 HttpClient 來下載網路資源
  • 使用 using 來操作 IDisposable 實作類別物件
  • 使用 PCLStorage 套件,將剛剛下載的網路資源,儲存到本機裝置記憶卡內
  • 可以指定儲存檔案的地方,不是該應用程式的沙箱,而是可以由使用者自行存取的目錄下。
  • 使用相依性服務注入技巧,注入可以開啟該檔案的物件與使用其相關方法

測試頁面

在這裡,我們設計一個頁面,在這個頁面上,僅有兩個控制項,一個是 Picker 另外一個是 Button;使用者可以透過 Picker 選擇要下載檔案的類型,接著按下按鈕,就會開始進行下載該檔案與開啟該檔案。
底下是這個測試頁面的 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"
             xmlns:behavior="clr-namespace:Prism.Behaviors;assembly=Prism.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XFFileDownload.Views.MainPage"
             Title="MainPage">
    <Grid
        BackgroundColor="White">
        <StackLayout
            HorizontalOptions="Center" VerticalOptions="Center">
            <Picker
            ItemsSource="{Binding FileSourceTypes}"
            SelectedItem="{Binding FileSourceTypeSelect}"
            />
            <Button Text="下載檔案"
                Command="{Binding DownloadCommand}"/>
        </StackLayout>

        <BoxView
            Color="Gray"
            IsVisible="{Binding ShowMask}"
            />
    </Grid>
</ContentPage>
接下來,我們來看看這個頁面 ViewModel 的程式碼
首先,我們在 OnNavigatedTo 事件方法中,進行 Picker 下拉選單資料項目的初始化。
在建構函式內 public MainPageViewModel(INavigationService navigationService, IPublicFileSystem publicFileSystem, IOpenFileByName openFileByName) ,我們將會把我們等下要設計的兩個介面 IPublicFileSystem 與 IOpenFileByName,在這使用建構函式注入物件的方式,將這兩個介面的原生專案中所實作出來並且產生的物件,注入到這個頁面 ViewModel內,讓相關程式碼可以進行相關操作。
由於 PCLStorage 套件所提供的 FileSystem 類別,僅會提供該應用程式沙箱可以使用到的資料夾,因此,我們將會透過 _PublicFileSystem.PublicDownloadFolder 來取得指向該原生平台下的可以公開存取的 IFolder 資料夾。
我們使用 IFile 物件, file ,建立一個檔案,並且使用 file.OpenAsync(FileAccess.ReadAndWrite) 取得可以讀寫的 stream 物件。
此時,我們將會建立 HttpClient 物件,client,使用 client.GetStreamAsync(url) 方法,取得這個網路資源的 stream 物件,並且使用敘述 fooStream.CopyTo(fooFileStream); 將從網路取得的檔案內容,複製到本機檔案中;若您取得網路檔案的方法需要使用 Post 的方式來取得,您也是使用同樣的方式,取得 HttpResponseMessage.Content.ReadAsStreamAsync 的 stream 物件,就可以進行寫入到本機儲存空間內。
在上述的檔案與目錄操作的程式碼,我們都是使用 PCLStorage 套件所提供的類別與相關方法。
最後,我們使用 _OpenFileByName.OpenFile(file.Path); 將這個檔案,使用安裝在手機中的應用程式來開啟。
    public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private readonly INavigationService _navigationService;


        public ObservableCollection<string> FileSourceTypes { get; set; } = new ObservableCollection<string>();
        public string FileSourceTypeSelect { get; set; }
        public bool ShowMask { get; set; } = false;
        public DelegateCommand DownloadCommand { get; set; }

        IPublicFileSystem _PublicFileSystem;
        IOpenFileByName _OpenFileByName;
        public MainPageViewModel(INavigationService navigationService,
            IPublicFileSystem publicFileSystem, IOpenFileByName openFileByName)
        {
            _navigationService = navigationService;
            // 注入各平台的非應用程式專屬的沙箱資料夾
            _PublicFileSystem = publicFileSystem;
            // 使用手機內安裝的App,開啟指定的檔案
            _OpenFileByName = openFileByName;

            DownloadCommand = new DelegateCommand(async () =>
            {
                ShowMask = true;
                #region 依據所選擇的項目,設定下載來源與檔案名稱
                string filename = "";
                string url = "";
                if (FileSourceTypeSelect.ToLower() == "pdf")
                {
                    filename = "vulcan.pdf";
                    url = "https://www.tutorialspoint.com/csharp/csharp_tutorial.pdf";
                }
                else if (FileSourceTypeSelect.ToLower() == "image")
                {
                    filename = "vulcan.png";
                    url = "https://pluralsight.imgix.net/paths/path-icons/csharp-e7b8fcd4ce.png";
                }
                else if (FileSourceTypeSelect.ToLower() == "mp3")
                {
                    filename = "vulcan.mp3";
                    url = "http://video.ch9.ms/ch9/4855/ca67b144-e675-48a2-a0f2-706af9644855/DataTemplateSelector.mp3";
                }
                else if (FileSourceTypeSelect.ToLower() == "video")
                {
                    filename = "vulcan.mp4";
                    url = "http://video.ch9.ms/ch9/4855/ca67b144-e675-48a2-a0f2-706af9644855/DataTemplateSelector.mp4";
                }
                else if (FileSourceTypeSelect.ToLower() == "ppt")
                {
                    filename = "vulcan.ppt";
                    url = "http://people.csail.mit.edu/mrub/talks/small_world/Seminar07_rubinstein.ppt";
                }
                else if (FileSourceTypeSelect.ToLower() == "doc")
                {
                    filename = "vulcan.doc";
                    url = "http://im2.nhu.edu.tw/download.php?filename=270_2af7568a.doc&dir=personal_subject/&title=C%23-%E7%AC%AC%E4%B8%80%E7%AB%A0";
                }
                #endregion

                // 取得要存放該檔案的資料夾
                // FileSystem 為 PCLStorage 提供的應用程式沙箱的相關資料夾
                IFolder rootFolder = _PublicFileSystem.PublicDownloadFolder;
                try
                {
                    // 建立這個檔案
                    IFile file = await rootFolder.CreateFileAsync(filename,
                        CreationCollisionOption.OpenIfExists);
                    // 取得這個檔案的 Stream 物件
                    using (var fooFileStream = await file.OpenAsync(FileAccess.ReadAndWrite))
                    {
                        using (HttpClientHandler handle = new HttpClientHandler())
                        {
                            // 建立 HttpClient 物件
                            using (HttpClient client = new HttpClient(handle))
                            {
                                // 取得指定 URL 的 Stream
                                using (var fooStream = await client.GetStreamAsync(url))
                                {
                                    // 將網路的檔案 Stream 複製到本機檔案上
                                    fooStream.CopyTo(fooFileStream);
                                }
                            }
                        }
                    }

                    _OpenFileByName.OpenFile(file.Path);
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.Message);
                }
                ShowMask = false;
            });
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public void OnNavigatingTo(NavigationParameters parameters)
        {

        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {
            FileSourceTypes.Clear();
            FileSourceTypes.Add("PDF");
            FileSourceTypes.Add("Image");
            FileSourceTypes.Add("MP3");
            FileSourceTypes.Add("Video");
            FileSourceTypes.Add("PPT");
            FileSourceTypes.Add("Doc");

        }

    }

取得公開資料夾的介面與實作

首先,我們在 PCL 專案內,建立 IPublicFileSystem 這個介面,這個介面內僅有四個屬性需要在原生平台內實作出來。
    public interface IPublicFileSystem
    {
        IFolder PublicDownloadFolder { get; }
        IFolder PublicPictureFolder { get; }
        IFolder PublicMovieFolder { get; }
        IFolder PublicDCIMFolder { get; }
    }
在 Android 平台下,我們實作 IPublicFileSystem 介面,並且記得要使用 [assembly: Xamarin.Forms.Dependency(typeof(PublicFileSystem))] 來宣告這個介面實作,可以用於相依性服務注入之用。
我們在這個介面實作中,取得個公開資料夾的絕對路徑,並且建立 PCLStorage 套件的 FileSystemFolder類別物件,這就是我們要使用的 IFolder 實作物件。
有關 Android 平台下關於這些資料夾的說明,請參考 https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_DOWNLOADS
[assembly: Xamarin.Forms.Dependency(typeof(PublicFileSystem))]
namespace XFFileDownload.Droid.Services
{
    class PublicFileSystem : IPublicFileSystem
    {
        public IFolder PublicDownloadFolder
        {
            get
            {
                var localAppData = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDownloads).AbsolutePath;
                return new FileSystemFolder(localAppData);
            }
        }
        public IFolder PublicPictureFolder
        {
            get
            {
                var localAppData = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryPictures).AbsolutePath;
                return new FileSystemFolder(localAppData);
            }
        }
        public IFolder PublicMovieFolder
        {
            get
            {
                var localAppData = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryMovies).AbsolutePath;
                return new FileSystemFolder(localAppData);
            }
        }
        public IFolder PublicDCIMFolder
        {
            get
            {
                var localAppData = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDcim).AbsolutePath;
                return new FileSystemFolder(localAppData);
            }
        }
    }
}
在 iOS 平台下,我們實作 IPublicFileSystem 介面,並且記得要使用 [assembly: Xamarin.Forms.Dependency(typeof(PublicFileSystem))] 來宣告這個介面實作,可以用於相依性服務注入之用。
我們在這個介面實作中,取得個公開資料夾的絕對路徑,並且建立 PCLStorage 套件的 FileSystemFolder類別物件,這就是我們要使用的 IFolder 實作物件。
有關 iOS 平台下關於這些資料夾的說明,請參考 https://developer.xamarin.com/guides/ios/application_fundamentals/working_with_the_file_system/
[assembly: Xamarin.Forms.Dependency(typeof(PublicFileSystem))]
namespace XFFileDownload.iOS.Services
{
    class PublicFileSystem : IPublicFileSystem
    {
        public IFolder PublicDownloadFolder
        {
            get
            {
                var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
                return new FileSystemFolder(localAppData);
            }
        }
        public IFolder PublicPictureFolder
        {
            get
            {
                var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
                return new FileSystemFolder(localAppData);
            }
        }
        public IFolder PublicMovieFolder
        {
            get
            {
                var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
                return new FileSystemFolder(localAppData);
            }
        }
        public IFolder PublicDCIMFolder
        {
            get
            {
                var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
                return new FileSystemFolder(localAppData);
            }
        }
    }
}

取得公開資料夾的介面與實作

首先,我們在 PCL 專案內,建立 IOpenFileByName 這個介面,這個介面內僅有一個方法需要在原生平台內實作出來。
    public interface IOpenFileByName
    {
        void OpenFile(string fullFileName);
    }
在 Android 平台下,我們實作 IOpenFileByName 介面,並且記得要使用 [assembly: Xamarin.Forms.Dependency(typeof(OpenFileByName))] 來宣告這個介面實作,可以用於相依性服務注入之用。
這個實作方法,首先,將這個檔案,複製到公開的資料夾內,也就是說,您下載的檔案,是可以儲存在應用程式沙箱資料夾內,也是可以正常使用手機內安裝的應用程式來開啟這個檔案的。
最後,我們使用 Intent intent = new Intent(Intent.ActionView); 來讓使用者選擇適合的應用程式,來開啟這個檔案。
[assembly: Xamarin.Forms.Dependency(typeof(OpenFileByName))]
namespace XFFileDownload.Droid.Services
{
    public class OpenFileByName : IOpenFileByName
    {
        public void OpenFile(string fullFileName)
        {
            try
            {
                var filePath = fullFileName;
                var fileName = Path.GetFileName(fullFileName);

                var bytes = File.ReadAllBytes(filePath);

                string externalStorageState = global::Android.OS.Environment.ExternalStorageState;
                var externalPath = global::Android.OS.Environment.ExternalStorageDirectory.Path + "/" + 
                    global::Android.OS.Environment.DirectoryDownloads + "/" + fileName;
                File.WriteAllBytes(externalPath, bytes);

                Java.IO.File file = new Java.IO.File(externalPath);
                file.SetReadable(true);

                string application = "";
                string extension = Path.GetExtension(filePath);

                // get mimeTye
                switch (extension.ToLower())
                {
                    case ".txt":
                        application = "text/plain";
                        break;
                    case ".doc":
                    case ".docx":
                        application = "application/msword";
                        break;
                    case ".pdf":
                        application = "application/pdf";
                        break;
                    case ".xls":
                    case ".xlsx":
                        application = "application/vnd.ms-excel";
                        break;
                    case ".jpg":
                    case ".jpeg":
                    case ".png":
                        application = "image/jpeg";
                        break;
                    default:
                        application = "*/*";
                        break;
                }

                //Android.Net.Uri uri = Android.Net.Uri.Parse("file://" + filePath);
                Android.Net.Uri uri = Android.Net.Uri.FromFile(file);
                Intent intent = new Intent(Intent.ActionView);
                intent.SetDataAndType(uri, application);
                intent.SetFlags(ActivityFlags.ClearWhenTaskReset | ActivityFlags.NewTask);

                Forms.Context.StartActivity(intent);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
}
在 iOS 平台下,我們實作 IOpenFileByName 介面,並且記得要使用 [assembly: Xamarin.Forms.Dependency(typeof(OpenFileByName))] 來宣告這個介面實作,可以用於相依性服務注入之用。
我們建立一個類別 UIDocumentInteractionControllerDelegateClass,用來顯示這個檔案。
[assembly: Xamarin.Forms.Dependency(typeof(OpenFileByName))]
namespace XFFileDownload.iOS.Services
{
    public class OpenFileByName : IOpenFileByName
    {
        public void OpenFile(string fullFileName)
        {
            var filePath = fullFileName;
            var fileName = Path.GetFileName(fullFileName);

            //Device.OpenUri(new Uri(filePath));
            var PreviewController = UIDocumentInteractionController.FromUrl(NSUrl.FromFilename(filePath));
            PreviewController.Delegate = new UIDocumentInteractionControllerDelegateClass(UIApplication.SharedApplication.KeyWindow.RootViewController);
            Device.BeginInvokeOnMainThread(() =>
            {
                PreviewController.PresentPreview(true);
            });
        }
    }

    public class UIDocumentInteractionControllerDelegateClass : UIDocumentInteractionControllerDelegate
    {
        UIViewController ownerVC;

        public UIDocumentInteractionControllerDelegateClass(UIViewController vc)
        {
            ownerVC = vc;
        }

        public override UIViewController ViewControllerForPreview(UIDocumentInteractionController controller)
        {
            return ownerVC;
        }

        public override UIView ViewForPreview(UIDocumentInteractionController controller)
        {
            return ownerVC.View;
        }
    }
}

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 框架來建立起來的跨平台行動應用程式,確實會將梅有被參考到的頁面物件予以釋放掉。而最前面所質疑的問題,就獲得解答了。

測試範例專案