2017年10月1日 星期日

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...

C# : 網路下載檔案之下載進度委派 delegate 方法

現在,讓我們來使用委派方法的功能,在這個練習中,我們將會設計一個類別,他會下載一個網路 PDF 檔案,當在下載的過程之中,會將下載檔案的實際讀取到的檔案完成百分比,透過委派方法回報;這個委派方法扮演著一個類似事件,也就是可以接收到處理工作當時狀態的通知,所以,訂閱這個委派事件的用戶端,就可以即時下載進度顯示在螢幕上,讓使用者明瞭這個長時間的工作,現在處理到甚麼進度了。
在這個類別 DownloadFile,我們定義了一個 Download 函式,他使用了 HttpClient.GetAsync 方法,抓取指定 URL 的內容,並且使用 HTTP Header 的 Content-Length 得到要下載的檔案總大小,接著,取得 HttpResponseMessage 之後,使用 HttpResponseMessage.Content.ReadAsStreamAsync() 方法,取得 Stream 行別的物件,便可以開始每次讀取 5120 bytes 大小的內容到程式中。每次完成讀取 5120 bytes,就會呼叫委派方法 onProgress,將處理進度回報,而這個 onProgress 是個委派型別的變數 public delegate void ProgressDelegate(int percent);
    public class DownloadFile
    {
        public delegate void ProgressDelegate(int percent);
        public ProgressDelegate OnProgress;
        public string Url { get; set; } = "http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/js_api_reference.pdf";
        public async void Download()
        {
            var task = Task.Factory.StartNew(async () =>
              {
                  int offset = 5120;
                  int percent = 0;
                  int currentDonload = 0;
                  int getBytes;
                  int lastPercent = -1;
                  using (var client = new HttpClient())
                  {
                      using (var response = await client.GetAsync(Url))
                      {
                          var total = int.Parse(response.Content.Headers.First(h => h.Key.Equals("Content-Length")).Value.First());
                          byte[] content = new byte[total];
                          using (var stream = await response.Content.ReadAsStreamAsync())
                          {
                              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(percent);
                                      }
                                      lastPercent = percent;
                                  }
                              }
                          }
                      }
                  }
              });
        }
    }
在進行測試的時候,我們建立了這個類別 (DownloadFile) 物件,接者設定 OnProgress 委派欄位,使其綁定到 DownloadStatus 方法上;這個方法就會將現在的處理進度,即時顯示在螢幕上,讓使用者知道現在的處理最新情況。
static void Main(string[] args)
{
    DownloadFile downloadFile = new DownloadFile();
    downloadFile.OnProgress += DownloadStatus;
    Console.WriteLine($"開始進行非同步檔案檔案下載");
    downloadFile.Download();
    Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
    Console.ReadKey();
}

private static void DownloadStatus(int percent)
{
    if(percent%10==0)
    {
        Console.Write($" {percent}% ");
    }
    else
    {
        Console.Write($".");
    }
}
底下是執行結果
開始進行非同步檔案檔案下載
Press any key to Exist...

 0% ......... 10% ......... 20% ......... 30% ......... 40% ......... 50% ......... 60% ......... 70%

2017年9月29日 星期五

C# : 使用Async 和 Await 讀取多網站資料

最後,讓我們使用 C# 5.0 的 存取修飾詞 Async / 運算子 Await,寫出一個具有非同步的應用程式;我們這裡一樣會需要去網路抓取四個網頁內容。
首先,我們寫個類別,這個類別裡面只有個方法,GetWebContent,這個方法的回傳值為 Task 物件,而且,我們看到了,在這個方法名稱前面,有用到存取修飾詞 Async,如果您在方法或運算式上使用這個修飾詞,則它是指非同步方法。
另外,在這個非同步方法裡面,我們使用 HttpClient 類別實例化出來的物件,讀取網頁資料,我們在這裡使用了 await client.GetStringAsync(url.ToString());,這裡使用到 await 運算子,而 await 運算子會套用至非同步方法中的工作以暫停執行方法,直到等候的工作完成為止,另外,在其中使用 await 的非同步方法必須由 async 關鍵字修改。當然,在這個方法內,我們還是一樣會進行了解當時使用的執行緒 ID 上哪個以及所耗費的時間。
    public class MyClass
    {
        public async Task GetWebContent(string url)
        {
            Console.WriteLine($"{url} 執行緒 ID {Thread.CurrentThread.ManagedThreadId}");

            Stopwatch sw = new Stopwatch();
            sw.Start();
            using (var client = new HttpClient())
            {
                string content = await client.GetStringAsync(url.ToString());
                Console.WriteLine($"{url} 的內容大小為 {content.Length} bytes");
            }
            sw.Stop();
            Console.WriteLine($"{url} 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
        }
    }
要使用剛剛寫好的類別,則是相當的簡單,我們需要使用 await 運算子來呼叫 GetWebContent 方法,如同 await myClass.GetWebContent(Urls[0]);
  • 我們對於程式進入點函式 Main,使用這樣方式來宣告 async Task Main(string[] args),並且,你的開發環境需要能夠支援 C# 7.1
  • 您可以嘗試將呼叫 myClass.GetWebContent 方法前面的 await 運算子移除,看看會有何結果。
    底下是移除 await 後的執行結果
https://www.microsoft.com 執行緒 ID 1
https://tw.yahoo.com/ 執行緒 ID 1
http://www.msn.com/zh-tw/ 執行緒 ID 1
https://world.taobao.com/ 執行緒 ID 1
抓取四個網站內容 花費時間為 109.2726 ms
Press any key for continuing...

https://www.microsoft.com 的內容大小為 1020 bytes
https://www.microsoft.com 花費時間為 295.5076 ms
https://world.taobao.com/ 的內容大小為 220947 bytes
https://world.taobao.com/ 花費時間為 283.5818 ms
http://www.msn.com/zh-tw/ 的內容大小為 46320 bytes
http://www.msn.com/zh-tw/ 花費時間為 389.8703 ms
https://tw.yahoo.com/ 的內容大小為 201849 bytes
https://tw.yahoo.com/ 花費時間為 635.2415 ms
底下測試用的程式碼
        static async Task Main(string[] args)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            MyClass myClass = new MyClass();
            await myClass.GetWebContent(Urls[0]);
            await myClass.GetWebContent(Urls[1]);
            await myClass.GetWebContent(Urls[2]);
            await myClass.GetWebContent(Urls[3]);

            sw.Stop();
            Console.WriteLine($"抓取四個網站內容 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
            Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
            Console.ReadKey();
       }
底下是執行結果內容
https://www.microsoft.com 執行緒 ID 1
https://www.microsoft.com 的內容大小為 1020 bytes
https://www.microsoft.com 花費時間為 525.7561 ms
https://tw.yahoo.com/ 執行緒 ID 9
https://tw.yahoo.com/ 的內容大小為 201849 bytes
https://tw.yahoo.com/ 花費時間為 962.6452 ms
http://www.msn.com/zh-tw/ 執行緒 ID 8
http://www.msn.com/zh-tw/ 的內容大小為 46320 bytes
http://www.msn.com/zh-tw/ 花費時間為 168.9015 ms
https://world.taobao.com/ 執行緒 ID 8
https://world.taobao.com/ 的內容大小為 220947 bytes
https://world.taobao.com/ 花費時間為 267.2545 ms
抓取四個網站內容 花費時間為 1947.116 ms
Press any key for continuing...

C# : 使用多個工作 task 讀取多網站資料

在這篇文章中,我們將會延續上一篇文章,需要同時讀取四個網頁上的資料,不過,在這裡,我們將會使用 C# 提供的 Task 類別來做到這樣需求。這裡是微軟官方對於 TAP 的需求說明:工作式非同步模式 (TAP) 是以 System.Threading.Tasks 命名空間中的 System.Threading.Tasks.Task 和 System.Threading.Tasks.Task<TResult> 類型為基礎,這兩種類別用於表示任意非同步作業。 在新開發非同步專案時建議使用TAP模式。
在這裡的練習中,我們使用了方法 Task.Factory.StartNew 來建立一個工作,這個靜態方法要接受一個 Action 委派方法作為引數,這個委派方法就是要來進行非同步執行的方法。
Task.WhenAll 方法會以非同步方式來等候多個 Task 或 Task 物件完成,因此,與執行緒的開發設計不同,使用工作可以有更多的方法與技巧,可以等候部分或者全部工作完成後,才會繼續進行處理其他程式碼。
這是執行結果
Press any key for continuing...

http://www.msn.com/zh-tw/ 執行緒 ID 5
https://world.taobao.com/ 執行緒 ID 6
https://tw.yahoo.com/ 執行緒 ID 3
https://www.microsoft.com 執行緒 ID 4
https://www.microsoft.com 的內容大小為 1020 bytes
https://world.taobao.com/ 的內容大小為 220949 bytes
https://www.microsoft.com 花費時間為 268.2176 ms
https://world.taobao.com/ 花費時間為 268.6948 ms
http://www.msn.com/zh-tw/ 的內容大小為 46066 bytes
http://www.msn.com/zh-tw/ 花費時間為 273.3024 ms
https://tw.yahoo.com/ 的內容大小為 192192 bytes
https://tw.yahoo.com/ 花費時間為 447.7144 ms
抓取四個網站內容 花費時間為 509.5968 ms
Press any key for continuing...
底下是完整的測試程式碼
class Program
{
    static string[] Urls = new string[]
    {
        "https://www.microsoft.com",
        "https://tw.yahoo.com/",
        "http://www.msn.com/zh-tw/",
        "https://world.taobao.com/"
    };

    static void Main(string[] args)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        Task t1 = Task.Factory.StartNew(() =>
        {
            GetWebContent(Urls[0]);
        });
        Task t2 = Task.Factory.StartNew(() =>
        {
            GetWebContent(Urls[1]);
        });
        Task t3 = Task.Factory.StartNew(() =>
        {
            GetWebContent(Urls[2]);
        });
        Task t4 = Task.Factory.StartNew(() =>
        {
            GetWebContent(Urls[3]);
        });

        Task.WhenAll(t1, t2, t3, t4).ContinueWith(t =>
        {
            sw.Stop();
            Console.WriteLine($"抓取四個網站內容 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
            Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
        });

        //// 這裡是另外一種做法
        //Task.WhenAll(t1, t2, t3, t4).ContinueWith(t =>
        //{
        //}).Wait();
        //sw.Stop();
        //Console.WriteLine($"抓取四個網站內容 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
        //Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");


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

    private static void GetWebContent(object url)
    {
        Console.WriteLine($"{url} 執行緒 ID {Thread.CurrentThread.ManagedThreadId}");

        Stopwatch sw = new Stopwatch();
        sw.Start();
        using (var client = new HttpClient())
        {
            string content = client.GetStringAsync(url.ToString()).Result;
            Console.WriteLine($"{url} 的內容大小為 {content.Length} bytes");
        }
        sw.Stop();
        Console.WriteLine($"{url} 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
    }
}

2017年9月28日 星期四

C# : 泛型 Generic 類別的物件序列化

當我們使用 C# 進行開發專案的時候,通常需要把當時物件的狀態記錄下來,也許是下次啟動應用程式時候,再把這個物件重新建立起來,並且回原成為原來的狀態值;或者是在這個程式的別的地方,需要再度重顯呈現出這個物件。
基於這樣的需求,我們可以把這個物件序列化成為別的格式,在這裡,我們會把這個測試物件序列化成為 JSON 格式內容;我們將會透過 Newtonsoft.Json 這個 NuGet 套件,把 C# 中的指定物件序列化成為 JSON 定義,也可以透過這個套件,將 JSON 內容,反序列化成為一個新的物件。
透過這樣的方法,您可以完成複製一個物件的功能
在這個練習中,我們將會使用到底下兩個類別,其中,我們將會建立 MyJSONClass<T> 這個泛型型別的物件,進行 JSON 序列與反序列化操作,而這個類別裡面會有兩個方法,分別將會提供這樣的功能。
public class MyClass
{
    public int ID { get; set; }
    public string Name { get; set; }
    public MyClass(int id, string name)
    {
        ID = id;
        Name = name;
    }
}

public class MyJSONClass<T>
{
    public T MyObject { get; set; }
    public string Content { get; set; }
    public MyJSONClass(T myObject)
    {
        MyObject = myObject;
    }
    public void Serialize()
    {
        Content = JsonConvert.SerializeObject(MyObject);
    }
    public T Deserialize()
    {
        return JsonConvert.DeserializeObject<T>(Content);
    }
}

進行測試

我們將使用底下程式碼進行測試
首先,我們建立一個 MyJSONClass<MyClass> 泛型類別物件 myJSONClassObject1,並且把這個物件內的屬性進行初始化設定。
接著,我們呼叫這個類別的序列化方法,就會得到物件序列化後的 JSON 內容,我們在這裡也將當時物件的 HashCode 也列印出來,這樣用來區分在進行反序列化動作之後,是否會得到另外一個記憶體空間的全新物件。
最後,我們使用該類別的反序列化方法,得到一個新的物件,並且,同樣的把這個物件的 HashCode 也列印出來,證實,我們確實得到一個新的物件。
MyJSONClass<MyClass> myJSONClassObject1 =
    new MyJSONClass<MyClass>(new MyClass(100, "Vulcan"));
myJSONClassObject1.Serialize();
MyClass myClassObject1 = myJSONClassObject1.MyObject;

Console.WriteLine($"JSON:{myJSONClassObject1.Content} / ID: {myClassObject1.ID} / HashCode: {myClassObject1.GetHashCode()}");
Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
Console.ReadKey();

MyClass myClassObject2 =
    myJSONClassObject1.Deserialize();
myClassObject1.ID = 999;
Console.WriteLine($"JSON:{myJSONClassObject1.Content} / ID: {myClassObject1.ID} / HashCode: {myClassObject1.GetHashCode()}");
Console.WriteLine($"ID={myClassObject2.ID}, Name={myClassObject2.Name} / HashCode: {myClassObject2.GetHashCode()}");
Console.ReadKey();
執行結果內容
JSON:{"ID":100,"Name":"Vulcan"} / ID: 100 / HashCode: 44223604
Press any key for continuing...

JSON:{"ID":100,"Name":"Vulcan"} / ID: 999 / HashCode: 44223604
ID=100, Name=Vulcan / HashCode: 62468121