2018年8月3日 星期五

客製化 SynchronizationContext 來讓不同執行緒 Thread 可以指定特定執行緒來執行委派方法

SynchronizationContext 類別.aspx)可以提供在各種同步處理模式中傳播同步處理內容的基本功能。這通常會在我們進行 GUI 類型應用程式的時候會用到,例如: Win Forms / WPF / Xamarin.Forms 這些類型的專案,會有這樣的需求那是因為,在這裡 GUI 類型的專案中,若想要設定 UI 控制項相關的屬性或者要變更其設定值,此時,您僅能夠在 UI Thread (UI 執行緒,或稱為 Main Thread 主執行緒) 內來執行這些程式碼。不過,有些時候,我們會需要透過多執行緒的程式設計方式,讓許多需要花費比較多時間的方法,在其他執行緒中來執行,不過,當在其他執行緒執行的方法內,有需要設定 UI 相關的屬性,我們就需要讓這些程式碼能夠在 UI 執行緒內來執行,這個時候,我們就可以透過 SynchronizationContext 類別 來滿足這樣需求。

了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [SynchronizationContext Class] 的使用方式


了解更多關於 [同步處理原始物件概觀] 的使用方式
由於 Win Forms / WPF / Xamarin.Forms 這些類型的專案內,已經內建這樣的機制,提供您在不同執行緒內,可以指定某些程式碼可以在 UI 執行緒內來執行;而在這篇文章中,我們將會建立一個 Console 類型的專案,並且自訂一個 SynchronizationContext 類別,提供相同的功能。
這篇文章的專案原始碼,可以從這個資料夾 CustomSynchronizationContext 中找到
我們繼承了 SynchronizationContext 類別,建立一個新的 MySynchronizationContext 類別,我們需要 override 覆寫 Send & Post 這兩個方法,前者是負責 會將同步訊息分派至同步處理內容,後者是負責 會將非同步訊息分派至同步處理內容;在這個練習中,我們僅會覆寫 Post 方法內的敘述。另外,我們也需要在這個類別內,建立一個佇列欄位 Queue messagesQueue,這裡將會記錄別的執行緒需要在這個特定執行緒下要執行的委派方法敘述,由於採用佇列資料結構,因此,將會採取先進先出的設計方式。因此,當在別的執行緒內呼叫了 Post 方法後,在這個方法內會使用敘述 messagesQueue.Enqueue(() => codeToRun(state))將要執行的委派方法儲存到佇列欄位內。
當進行多執行緒程式設計的時候,特別需要注意執行緒安全上的考量,因此,我們將會透過 lock (syncHandle) 敘述,確保受到保護的程式碼區段,在同一個時間內,僅會有一個執行緒可以來執行,這點需要特別注意。
最重要的是這個方法 RunMessagePump(),這個方法裡面是一個無窮迴圈,會來檢查佇列欄位內是否有要執行的委派方法,若有的話,則將該委派方法取出來,並且執行這個委派方法。
C Sharp / C#
class MySynchronizationContext : SynchronizationContext
{
    /// <summary>
    /// 待執行的訊息工作佇列
    /// </summary>
    private readonly Queue<Action> messagesQueue = new Queue<Action>();
    /// <summary>
    /// 用於同步處理之鎖定的物件
    /// </summary>
    private readonly object syncHandle = new object();
    /// <summary>
    /// 是否正在執行中
    /// </summary>
    private bool isRunning = true;

    public override void Send(SendOrPostCallback codeToRun, object state)
    {
        throw new NotImplementedException();
    }

    public override void Post(SendOrPostCallback codeToRun, object state)
    {
        lock (syncHandle)
        {
            // 將要處理的訊息工作,加入佇列中
            messagesQueue.Enqueue(() => codeToRun(state));
            SignalContinue();
        }
    }

    /// <summary>
    /// 進入訊息處理的無窮迴圈
    /// </summary>
    public void RunMessagePump()
    {
        while (CanContinue())
        {
            Console.Write(".");
            Action nextToRun = RetriveItem();
            if (nextToRun != null)
                nextToRun();
        }
    }

    /// <summary>
    /// 取出待處理的訊息工作項目
    /// </summary>
    /// <returns></returns>
    private Action RetriveItem()
    {
        lock (syncHandle)
        {
            while (CanContinue() && messagesQueue.Count == 0)
            {
                Monitor.Wait(syncHandle);
            }
            if (isRunning == true)
            {
                return messagesQueue.Dequeue();
            }
            else
            {
                return null;
            }
        }
    }

    /// <summary>
    /// 是否可以繼續執行
    /// </summary>
    /// <returns></returns>
    private bool CanContinue()
    {
        lock (syncHandle)
        {
            return isRunning;
        }
    }

    /// <summary>
    /// 停止 訊息處理的無窮迴圈 執行
    /// </summary>
    public void Cancel()
    {
        lock (syncHandle)
        {
            isRunning = false;
            SignalContinue();
        }
    }

    /// <summary>
    /// 解除鎖定,可以繼續執行
    /// </summary>
    private void SignalContinue()
    {
        Monitor.Pulse(syncHandle);
    }
}
現在,我們來進行這個客製化的 MySynchronizationContext 測試,我們將建立一個這個類別的物件 static MySynchronizationContext ctx = new MySynchronizationContext(); ,並且會先顯示這個程式是在哪個執行緒下運行,將著,使用 MySynchronizationContext.SetSynchronizationContext(ctx); 來設定我們自己設計的 同步處理的內容 synchronisation context。在程式碼最後,使用這個敘述 ctx.RunMessagePump(); 開始進行 訊息處理的無窮迴圈。


在進入無窮迴圈之前,我們會建立兩個執行緒,一個執行緒 將會讀取使用者輸入指定 Console.ReadKey();,若使用輸入 q 則會結束這個程式,在這裡會使用 ctx.Cancel(); 來取消無窮訊息處理迴圈,這樣,程式就會結束了。另外一個執行緒則是會讀取網路上網頁內容;不過,再進行讀取網頁內容的時候,將要使用敘述 ctx.Post(RunDownloadBlogger, null); 指示需要使用主執行緒來進行執行檔案下載的工作。
C Sharp / C#
class Program
{
    static MySynchronizationContext ctx = new MySynchronizationContext();
    static void Main(string[] args)
    {
        Console.Out.WriteLine("Main Thread No {0}", Thread.CurrentThread.ManagedThreadId);

        Console.WriteLine($"設定自製 SynchronizationContext 之前的 SynchronizationContext.Current : {SynchronizationContext.Current?.ToString()}");

        // 設定我們自己設計的 同步處理的內容 synchronisation context
        MySynchronizationContext.SetSynchronizationContext(ctx);

        Console.WriteLine($"設定自製 SynchronizationContext 之後的 SynchronizationContext.Current : {SynchronizationContext.Current?.ToString()}");

        Thread workerThread = new Thread(new ThreadStart(Run));
        workerThread.Start();

        Thread againThread = new Thread(new ThreadStart(() =>
        {
            while (true)
            {
                var foo = Console.ReadKey();
                if (foo.KeyChar == 'a')
                {
                    Run();
                }
                else if (foo.KeyChar == 'q')
                {
                    ctx.Cancel();
                    break;
                };
            }
        }));
        againThread.Start();

        //  開始進行 訊息處理的無窮迴圈
        ctx.RunMessagePump();
    }

    private static void Run()
    {
        Console.Out.WriteLine("Current New Thread No {0}", Thread.CurrentThread.ManagedThreadId);

        // 將要處理的委派方法項目,送入 訊息處理的無窮迴圈 執行
        ctx.Post(RunDownloadBlogger, null);
    }

    private static void RunDownloadBlogger(object state)
    {
        DownloadBlogger();
    }

    static async void DownloadBlogger()
    {
        Console.Out.WriteLine("MainProgram 下載前 on Thread No {0}", Thread.CurrentThread.ManagedThreadId);
        var client = new System.Net.WebClient();
        var webContentHomePage = await client.DownloadStringTaskAsync("https://mylabtw.blogspot.com/");
        Console.Out.WriteLine("下載 {0} 字元", webContentHomePage.Length);
        Console.Out.WriteLine("MainProgram 下載完後 on Thread No {0} ", Thread.CurrentThread.ManagedThreadId);
    }
}

了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [SynchronizationContext Class] 的使用方式


了解更多關於 [同步處理原始物件概觀] 的使用方式



關於 Xamarin 在台灣的學習技術資源
Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程


2018年7月17日 星期二

存取應用程式設定和使用者設定 Settings 的資料

有些時候,我們在進行 C# 程式設計的時候,需要產生一些物件,但是,需要由哪個具體實作類別來產生,並不希望在設計時期來指定,而是在應用程式執行的時候,依據當時應用程式的設定參數,來進行決定要由哪個類別來產生特定介面的物件。在這篇文章中,我們將會設計一個 靜態工廠模式 Static Factory Pattern 類別,建立起一個鬆散耦合的程式。
當我們呼叫靜態工廠方法的 StaticFactory.GetMessage() 方法的時候,會依據 應用程式設定和使用者設定 Settings.settings 定義的資料,產生出一個型別為 IMessage 的具體實作類別物件;在這個範例中,我們要產生一個可以發送訊息通知的物件,將所們指定的訊息發送出去,我們設計了介面 IMessage,並且有兩個類別 SMSMessage (使用簡訊方式發送) 與 EMailMessage (使用電子郵件方式發送) 皆實作這個介面,因此,我們在主程式端,透過靜態工廠模式類別,幫助我們產生一個 IMessage 的實作物件,讓我們可以傳送出訊息出去。
不過,我們在靜態工廠模式類別中,並沒有指定要由哪個類別來產生這個物件,而是我們會讀取 應用程式設定和使用者設定 Settings.settings 定義內容,找到 IMessage 的設定值 ( Properties.Settings.Default.IMessage ),這個設定文字的內容是會指定類別的在組件中定義名稱,我們使用這個字串與 Type.GetType 方法,幫助我們產生出指定的型別物件,透過 Activator.CreateInstance 方法,可以產生出該類別的執行個體了,更多資訊,您可以參考 使用應用程式設定和使用者設定
在這裡,我們建立一個 .NET Framework 主控制台應用專案 AccessUserSettings
C Sharp / C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AccessUserSettings
{
    public interface IMessage
    {
        void Send(string message);
    }
    public class SMSMessage : IMessage
    {
        public void Send(string message)
        {
            Console.WriteLine($"簡訊已經送出 : {message}");
        }
    }
    public class EMailMessage : IMessage
    {
        public void Send(string message)
        {
            Console.WriteLine($"郵件已經送出 : {message}");
        }
    }
    public class StaticFactory
    {
        public static IMessage GetMessage()
        {
            string fooValue = Properties.Settings.Default.IMessage;
            Type fooType = Type.GetType(fooValue);
            IMessage fooObject = Activator.CreateInstance(fooType) as IMessage;
            return fooObject;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            IMessage messageObject = StaticFactory.GetMessage();
            messageObject.Send("我已經動態產生具體實作物件了");

            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }
    }
}
緊接著,我們在該專案中,使用滑鼠雙擊 Properties 節點,或者查看該專案的屬性,此時,會看到如下圖畫面,請切換到 設定 標籤頁次‵,不過您會看到這樣的訊息: 此專案未包含預設的設定檔,請按一下這裡建立資源檔。請您點選這個藍色文字。
Settings.Settings
現在,您會看到在方案總管中該專案結構上,在 Properties 節點下,出現了一個 Settings.settings 檔案,這個就是我們準備要設定資料的地方。
Settings.Settings
請在該專案屬性頁面上的設定標籤頁次中,依據下圖填入 IMessage 的值為 AccessUserSettings.SMSMessage, AccessUserSettings
Settings.Settings
若想要得知您專案內的某個類別在組件中的表示字串,可以使用底下程式碼來得知。
C Sharp / C#
#region 取得指定完整的類型名稱
var foo = typeof(SMSMessage);
var bar = foo.AssemblyQualifiedName.ToString();
Console.WriteLine(bar);
#endregion
現在,我們執行之後,就會會看到我們產生了類別 SMSMessage 這個類別的執行個體
Console
簡訊已經送出 : 我已經動態產生具體實作物件了
Press any key for continuing...
現在,讓我們來切換當要取得一個 IMessage 介面之具體實作類別物件,不過,需要產生的是 EMailMessage,請在該專案屬性頁面上的設定標籤頁次中,依據下圖填入 IMessage 的值為 AccessUserSettings.EMailMessage, AccessUserSettings
Settings.Settings
現在,我們將會看到我們產生了類別 EMailMessage 這個類別的執行個體
Console
郵件已經送出 : 我已經動態產生具體實作物件了
Press any key for continuing...

關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程


存取目前應用程式預設組態 app.config 的 AppSettingsSection 資料

有些時候,我們在進行 C# 程式設計的時候,需要產生一些物件,但是,需要由哪個具體實作類別來產生,並不希望在設計時期來指定,而是在應用程式執行的時候,依據當時應用程式的設定參數,來進行決定要由哪個類別來產生特定介面的物件。在這篇文章中,我們將會設計一個 靜態工廠模式 Static Factory Pattern 類別,建立起一個鬆散耦合的程式。
當我們呼叫靜態工廠方法的 StaticFactory.GetMessage() 方法的時候,會依據 app.config 這個設定檔案內容定義的資料,產生出一個型別為 IMessage 的具體實作類別物件;在這個範例中,我們要產生一個可以發送訊息通知的物件,將所們指定的訊息發送出去,我們設計了介面 IMessage,並且有兩個類別 SMSMessage (使用簡訊方式發送) 與 EMailMessage (使用電子郵件方式發送) 皆實作這個介面,因此,我們在主程式端,透過靜態工廠模式類別,幫助我們產生一個 IMessage 的實作物件,讓我們可以傳送出訊息出去。
不過,我們在靜態工廠模式類別中,並沒有指定要由哪個類別來產生這個物件,而是我們會讀取 app.config 這個 XML 檔案的 appSettings 裡面的定義內容,找到 IMessage 的設定值,這個設定文字的內容是會指定類別的在組件中定義名稱,我們使用這個字串與 Type.GetType 方法,幫助我們產生出指定的型別物件,透過 Activator.CreateInstance 方法,可以產生出該類別的執行個體了
在這裡,我們建立一個 .NET Framework 主控制台應用專案 AccessConfigurationManager
C Sharp / C#
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Linq;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AccessConfigurationManager
{
    public interface IMessage
    {
        void Send(string message);
    }
    public class SMSMessage : IMessage
    {
        public void Send(string message)
        {
            Console.WriteLine($"簡訊已經送出 : {message}");
        }
    }
    public class EMailMessage : IMessage
    {
        public void Send(string message)
        {
            Console.WriteLine($"郵件已經送出 : {message}");
        }
    }
    public class StaticFactory
    {
        public static IMessage GetMessage()
        {
            // 這裡要加入 System.Configuration 組件參考
            NameValueCollection appSettings = ConfigurationManager.AppSettings;
            string fooValue = appSettings["IMessage"] ;
            Type fooType = Type.GetType(fooValue);
            IMessage fooObject = Activator.CreateInstance(fooType) as IMessage;
            return fooObject;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            #region 取得指定完整的類型名稱
            var foo = typeof(SMSMessage);
            var bar = foo.AssemblyQualifiedName.ToString();
            Console.WriteLine(bar);
            #endregion

            IMessage messageObject = StaticFactory.GetMessage();
            messageObject.Send("我已經動態產生具體實作物件了");

            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }
    }
}
緊接著,我們在該專案中,找到根目錄下的 app.config 檔案,將其打開,並且在 Configuration 節點內,建立一個 appSettings 節點,我們將會在這個節點內,建立一個 IMessage 鍵值,他的值為 AccessConfigurationManager.SMSMessage, AccessConfigurationManager 。這個字串將會表示在 AccessConfigurationManager 組件內的 AccessConfigurationManager.SMSMessage 類別。若您想要得到某個類別的完整組件識別字串,可以參考這篇文章 指定完整的類型名稱 與底下程式碼用法。
C Sharp / C#
#region 取得指定完整的類型名稱
var foo = typeof(SMSMessage);
var bar = foo.AssemblyQualifiedName.ToString();
Console.WriteLine(bar);
#endregion
底下是我們修正後的 app.config 設定內容
app.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
  </startup>
  <appSettings>
    <add key="IMessage" value="AccessConfigurationManager.SMSMessage, AccessConfigurationManager"/>
  </appSettings>
</configuration>
現在,可以執行這個測試專案,我們將會看到我們產生了類別 SMSMessage 這個類別的執行個體
Console
AccessConfigurationManager.SMSMessage, AccessConfigurationManager, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
簡訊已經送出 : 我已經動態產生具體實作物件了
Press any key for continuing...
假設現在客戶有變更需求產生,所有的訊息通知,將不再使用簡訊方式發送,而要改用電子郵件方式來發送;在以往,我們需要修改我們的原始程式碼,但是這樣做,有可能會產生更多的 bug (尤其是在大型、多人開發的專案上)。不過,經過我們這樣的設計模式,我們僅需要修正 app.config 這個設定檔案中的 IMessage 值,重新執行一次
app.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
  </startup>
  <appSettings>
    <add key="IMessage" value="AccessConfigurationManager.EMailMessage, AccessConfigurationManager"/>
  </appSettings>
</configuration>
現在,我們將會看到我們產生了類別 EMailMessage 這個類別的執行個體
Console
AccessConfigurationManager.SMSMessage, AccessConfigurationManager, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
郵件已經送出 : 我已經動態產生具體實作物件了
Press any key for continuing...

關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程