2017年10月2日 星期一

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

大家經常都在詢問這樣的問題:如何在 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();
        }
    }
}

C# : 泛型 Generic 類別內動態產生指定泛型物件

我們分別宣告簡單的類別與結構型別,接著,我們宣告了一個客製化的泛型類別 MyGenericClass<T>,這個泛型類別只有一個方法 GetInstance,他會根據我們建立這個泛型類別所傳送過去的型別,幫我們傳回這個型別的預設物件。

了解更多關於 [泛型的使用方式
了解更多關於 [C# 程式設計手冊 



我們要建立一個預設的物件,可以使用 System.Activator.CreateInstance 方法,不過,當泛型參數型別為字串 string 的時候,就會有問題了,因為,這個字串型別,他沒有無參數的預設建構函式,因此,我們需要呼叫其他的建構函式,這樣才能夠幫我們產生一個字串物件出來;因此,對於字串型別,我們使用了 Activator.CreateInstance(type, "".ToCharArray() 表示式來建立字串型別的物件。
由於呼叫System.Activator.CreateInstance所得到的物件,我們還需要將其轉型成為我們指定的泛型參數型別,在這裡,我們可以使用 (T) 就可輕鬆達到轉型的需求。
public class MyClass
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public struct MyStruct
{
    public int ID { get; set; }
    public string Name { get; set; }
}


public class MyGenericClass<T> 
{
    public T MyObject { get; set; }
    public T GetInstance()
    {
        T myObject;
        var type = typeof(T);
        // 類別型別,使用 Activator.CreateInstance 動態來產生物件
        if(type.Name != "String")
        {
            myObject = (T)Activator.CreateInstance(type);
        }
        else
        {
            myObject = (T)Activator.CreateInstance(type, "".ToCharArray());
        }

        return myObject;
    }
}
測試用的程式碼
在這裡,我們使用泛型型別 MyGenericClass,請這個型別的 GetInstance 方法,幫助我們產生我們所指定的型別物件。
//var tmp = new string();
var tmp = new int();

MyGenericClass<int> myGenericObject1 = new MyGenericClass<int>();
MyGenericClass<MyClass> myGenericObject2 = new MyGenericClass<MyClass>();
MyGenericClass<MyStruct> myGenericObject3 = new MyGenericClass<MyStruct>();
MyGenericClass<string> myGenericObject4 = new MyGenericClass<string>();
Console.WriteLine($"MyGenericClass<int>() Object is : {myGenericObject1.GetInstance()}");
Console.WriteLine($"MyGenericClass<MyClass>() Object is : {myGenericObject2.GetInstance()}");
Console.WriteLine($"MyGenericClass<MyStruct>() Object is : {myGenericObject3.GetInstance()}");
Console.WriteLine($"MyGenericClass<string>() Object is : {myGenericObject4.GetInstance()}");

Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
Console.ReadKey();

執行結果
MyGenericClass<int>() Object is : 0
MyGenericClass<MyClass>() Object is : GenericDynObject.MyClass
MyGenericClass<MyStruct>() Object is : GenericDynObject.MyStruct
MyGenericClass<string>() Object is :
Press any key to Exist...



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

想要購買 XAML in Xamarin.Forms 基礎篇 電子書,請點選 這裡




C# : 使用迭代器 Iterators 產生自己的集合 Collection 清單

在這個練習中,我們來開發出一個類別,可以用於 foreach 陳述式中,也就是迭代器的能力,根據微軟的定義:迭代器 可用來逐步執行集合,例如清單和陣列。迭代器方法或 get 存取子會對集合執行自訂反覆運算
要客製化類別,使其具有迭代器能力,在 C# 中,我們僅需要實作 IEnumerable<T> 這個介面,在您的類別中,實作出 GetEnumerator 這個方法即可。
這裡,我們模擬一個情境,您需要透過網路抓取四個網頁內容,不過,你無法控制哪個網頁會先讀取完畢,因此,您將需要設您的類別,只要網頁內容讀取完成之後,就會回傳到用戶端;而在用戶端中,可以使用 foreach 陳述式來逐一讀取這些網頁內容。為了達到這樣的需求,我們是用 C# 工作的靜態方法 Task.WhenAny 來協助我們處理這樣需求。
public class DownloadWeb : IEnumerable<DownloadWeb.GetWebResult>
{
    public class GetWebResult
    {
        public int Length { get; set; }
        public string URL { get; set; }
        public int ms { get; set; }
    }

    public string[] Urls = new string[]
    {
        "https://tw.yahoo.com/",
        "http://www.msn.com/zh-tw/",
        "https://world.taobao.com/",
        "https://www.microsoft.com",
    }; 

    public IEnumerator<GetWebResult> GetEnumerator()
    {
        List<Task<GetWebResult>> tasks = new List<Task<GetWebResult>>();
        tasks.Add(GetWebContent(Urls[0]));
        tasks.Add(GetWebContent(Urls[1]));
        tasks.Add(GetWebContent(Urls[2]));
        tasks.Add(GetWebContent(Urls[3]));

        while (tasks.Count > 0)
        {
            var aTask = Task.WhenAny(tasks);
            tasks.Remove(aTask.Result);
            var result = aTask.Result;
            yield return result.Result;
        }
    }

    public async Task<GetWebResult> GetWebContent(string url)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        using (var client = new HttpClient())
        {
            var result = await client.GetStringAsync(url);
            sw.Stop();
            GetWebResult getWebResult = new GetWebResult()
            {
                Length = result.Length,
                URL = url,
                ms = sw.Elapsed.Milliseconds
            };
            return getWebResult;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        throw new NotImplementedException();
    }
}
在我們要進行測試的時候,首先,我們產生出一個 DownloadWeb 物件,使用 foreach 陳述式,逐一取得讀取到的網頁內容。你可以嘗試重複執行這個練習程式,將會發現到,每次進行 foreach 陳述式的時候,回傳結果順序都不進相同。
DownloadWeb downloadWeb = new DownloadWeb();

foreach (var item in downloadWeb)
{
    Console.WriteLine($"{item.URL} ({item.ms}) : {item.Length}");
}

Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
Console.ReadKey();
執行結果
http://www.msn.com/zh-tw/ (241) : 46965
https://tw.yahoo.com/ (327) : 201659
https://world.taobao.com/ (349) : 220947
https://www.microsoft.com (4) : 1020
Press any key to Exist...

2017年10月1日 星期日

C# : 傳遞參數 - ref / out 方法參數修飾詞 練習

這個練習相當的簡單,是要讓你可以孰悉 ref / out 這兩個方法參數修飾詞的用法與差異。
底下是微軟官方對於這兩個修飾詞的定義。
ref 關鍵字會導致引數由參考加以傳遞,而非透過值
out 關鍵字會導致引數由參考傳遞。 它類似於 ref 關鍵字,只是 ref 需要在傳遞之前,先初始化變數。
static void Main(string[] args)
{
    int normal = 20;
    int ref_int = 10;
    int out_int;
    Method_normal(normal);
    Method_ref(ref ref_int);
    Method_out(out out_int);

    Console.WriteLine($"normal的值為 {normal}");
    Console.WriteLine($"ref_int的值為 {ref_int}");
    Console.WriteLine($"out_int的值為 {out_int}");
    Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
    Console.ReadKey();
}

private static void Method_normal(int normal)
{
    normal = 999;
}

private static void Method_out(out int out_int)
{
    out_int = 999;
}

private static void Method_ref(ref int ref_int)
{
    ref_int = 999;
}

C# : 使用擴充方法 Extension Method,增加 DateTime 功能

在這個練習中,我們將會要來練習擴充方法 Extension Method 的使用方式,根據微軟官方的說明:擴充方法可讓您在現有類型中「加入」方法,而不需要建立新的衍生類型、重新編譯,或是修改原始類型

了解更多關於 [擴充方法
了解更多關於 [C# 程式設計手冊 


在這裡,我們將會要使用結構 DateTime 型別,進行擴充它原有的功能;我們在實際開發專案的時候,往往需要使用到很多的日期相關的方法,取得需要的特定日期,例如,想要取得下個星期日的日期是哪一天,可是,在原有的 DateTime 結構中,卻沒有提供這樣的功能,因此,很多時候,我們需要寫出一些支援方法,幫助我們進行這樣的日期計算;最後,透過呼叫這些支援方法,得到所需要的日期。
不過,透過擴充方法的使用,讓我們可以更容易地在 DateTime 物件上,直接使用這些擴充方法,得到我們的日期。
首先,我們需要宣告一個靜態類別,在這個靜態類別內,我們再進行更多的靜態方法的定義;在這些靜態方法中,他的第一個參數將會是需要加入 this 關鍵字與要套用的型別,例如:public static DateTime Next(this DateTime from, DayOfWeek dayOfWeek);若要使用這個擴充方法,我們僅需要 DateTime 物件之後,輸入 .,Visual Studio 的 Intellience 就會顯示出我們剛剛定義的擴充方法(此時,需要加入適當的命名空間宣告),例如: Now.Next(DayOfWeek.Sunday)
    public static class MyDateTimeExtension
    {
        public static DateTime Next(this DateTime from, DayOfWeek dayOfWeek)
        {
            int start = (int)from.DayOfWeek;
            int target = (int)dayOfWeek;
            if (target <= start)
                target += 7;
            return from.AddDays(target - start);
        }
    }
這是我們測試的程式碼
            var Now = DateTime.Now;
            Console.WriteLine($"下個星期日為 {Now.Next(DayOfWeek.Sunday)}");
            Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
            Console.ReadKey();

執行結果為
下個星期日為 2017/10/1 下午 06:05:22
Press any key for continuing...

了解更多關於 [擴充方法
了解更多關於 [C# 程式設計手冊 




2017年9月30日 星期六

C# : 網路下載檔案之下載進度事件 event 方法

在上一個 C# : 網路下載檔案之下載進度委派方法 練習中,我們使用的委派型別,來做為通知使用者最新的下載進度,在這裡,我們將會使用 C# 的事件 Event,來做出相同效果的程式碼。
由於我們需要透過事件來回報處理進度,因此,我們需要客製化一個類別,用來處理這件需求,在這裡,我們需要在新建立的類別,讓他繼承 EventArgs 類別,在這個新的類別,DownloadFileEventArgs,我們新增一個屬性,用來說明處理進度百分比。
另外,對於 Task.Factory.StartNew,我們有做些調整,在這裡,我們將修飾詞 async 移除了,所以,現在的現在傳入的委派 Lambda 將會是:Task.Factory.StartNew(() => {...};由於沒有了 async 修飾詞,所以,在 Task.Factory.StartNew 引數用的委派方法,我們就要進行調整,把原先這個委派方法內有使用到 await 的地方,全部修改成為同步的執行方法;不過,在這裡,你不用擔心,因為我們使用了 Task.Factory.StartNew 啟動一個全新的工作,這個工作會使用 ThreadPool 執行緒集區內的一個執行緒來執行這個工作,所以,也是多工處理的作業。
當要進行回報處理進度事件的時候,我們需要呼叫這個方法:onProgress(this, new DownloadFileEventArgs() { Percent = percent });,在第二個引數,我們需要透過 DownloadFileEventArgs 產生的物件,將現在處理進度傳送到訂閱者的綁定方法中。
public class DownloadFileEventArgs : EventArgs
{
    public int Percent { get; set; }
}

public class DownloadFile
{
    public event EventHandler<DownloadFileEventArgs> OnProgress;
    //public string Url { get; set; } = "http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/js_api_reference.pdf";
    public string Url { get; set; } = "https://www.tutorialspoint.com/csharp/csharp_tutorial.pdf";
    public Task Download()
    {
        var task = Task.Factory.StartNew(() =>
        {
            int offset = 5120;
            int percent = 0;
            int currentDonload = 0;
            int getBytes;
            int lastPercent = -1;
            using (var client = new HttpClient())
            {
                using (var response = client.GetAsync(Url).Result)
                {
                    var total = int.Parse(response.Content.Headers.First(h => h.Key.Equals("Content-Length")).Value.First());
                    byte[] content = new byte[total+offset];
                    using (var stream = response.Content.ReadAsStreamAsync().Result)
                    {
                        var onProgress = OnProgress;
                        while ((getBytes = stream.Read(content, currentDonload, offset)) > 0)
                        {
                            currentDonload += getBytes;
                            percent = (int)((1.0 * currentDonload / total) * 100);
                            if (lastPercent != percent)
                            {
                                if (onProgress != null)
                                {
                                    onProgress(this, new DownloadFileEventArgs() { Percent = percent });
                                }
                                lastPercent = percent;
                            }
                        }
                    }
                }
            }
        });
        return task;
    }
}
現在,讓我們開始進行測試,我們同樣需要訂閱 OnProgress 的事件,不過,您會看到輸出結果卻是不太相同,這並不是我們使用事件的方式,你可以知道發生了甚麼問題以及差異在哪裡?
static async Task Main(string[] args)
{
    DownloadFile downloadFile = new DownloadFile();
    downloadFile.OnProgress += DownloadStatus;
    Console.WriteLine($"開始進行非同步檔案檔案下載");
    await downloadFile.Download();
    Console.WriteLine($"{Environment.NewLine}Press any key to Exist...");
    Console.ReadKey();
}

private static void DownloadStatus(object sender, DownloadFileEventArgs e)
{
    if (e.Percent % 10 == 0)
    {
        Console.Write($" {e.Percent}% ");
    }
    else
    {
        Console.Write($".");
    }
}
底下是執行結果
開始進行非同步檔案檔案下載
 0% ......... 10% ......... 20% ......... 30% ......... 40% ......... 50% ......... 60% ......... 70% ......... 80% ......... 90% ......... 100%
Press any key to Exist...