1. 程式人生 > WINDOWS開發 >在net Core3.1上基於winform實現依賴注入例項

在net Core3.1上基於winform實現依賴注入例項

在net Core3.1上基於winform實現依賴注入例項

目錄

1.背景

net core3.1是微軟LTS長期3年支援版本,正式釋出於2019-12-03,並且在windows平臺上支援了Winfrom跟WPF桌面應用。本文介紹了使用Winform時的第一步,將應用層以及ORM涉及到的DBconfig,倉儲層等依賴注入到容器中,並通過建構函式法從容器中呼叫例項,供給各窗體控制元件使用。
備註:本文的依賴注入講解基於微軟原生自帶的DI,通過Ninject或者AutoFac可自行仿照操作,原理相通。

2.依賴注入

2.1依賴注入是什麼?

依賴注入是通過反轉控制(IOC),設計模式屬於代理模式+工廠模式,由serviceProvider根據例項介面或者例項型別呼叫,注入時生命週期的設定,控制例項化及配置例項生命週期,並返回例項給程式設計師呼叫,從而達到解放程式設計師的生產力,不用再去new 一個個例項,也不用去考慮例項之間的依賴關係,也不用去考慮例項的生命週期。實現,分為三個階段,第一,程式設計師將服務注入服務容器階段,第二程式設計師DI例項呼叫階段,第三serviceProvider服務管理者根據注入時的配置返回給程式對應的例項以及配置好例項的生命週期。

一張圖就可以理解依賴注入例項呼叫過程
技術分享圖片

圖片來源出處,感謝作者。

這裡再向讀者做個說明ServiceCollection是服務容器,serviceProvider是服務管理者,管理著服務容器,當程式傳送抽象介面,或者型別時,serviceProvider會根據設定好的生命週期,返回需要的例項配置好例項的生命週期給程式設計師使用。

2.1依賴注入的目的

通過代理模式serviceProvider控制反轉,他將持有控制權,將所有需要用到的介面,型別,反射出對應的例項,例項化以及設定好例項的生命週期,然後將控制權返還給程式設計師,不用再去new 一個個例項,也不用去考慮例項之間的依賴關係,也不用去考慮例項的生命週期,最終目的就是解放程式設計師的生產力,讓程式設計師更輕鬆地寫程式。

2.2依賴注入帶來的好處

2.2.1生命週期的控制

在注入的同時可以設定如下三種生命週期:

  • Transient
    每次注入時,都重新 new 一個新的例項。
  • Scoped
    每個 Request 都重新 new 一個新的例項,同一個 Request 不管經過多少個 Pipeline 都是用同一個例項。
  • Singleton
    被例項化後就不會消失,程式執行期間只會有一個例項。

2.2.1.1 生命週期測試舉例

  • 定義同一個例子對應三個不同生命週期的介面
public interface ISample
{
    int Id { get; }
}

public interface ISampleTransient : ISample
{
}

public interface ISampleScoped : ISample
{
}

public interface ISampleSingleton : ISample
{
}

public class Sample : ISampleTransient,ISampleScoped,ISampleSingleton
{
    private static int _counter;
    private int _id;

    public Sample()
    {
        _id = ++_counter;
    }

    public int Id => _id;
}
  • 將對應的服務與介面註冊到容器中
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<ISampleTransient,Sample>();
        services.AddScoped<ISampleScoped,Sample>();
        services.AddSingleton<ISampleSingleton,Sample>();
        // Singleton 也可以用以下方法註冊
        // services.AddSingleton<ISampleSingleton>(new Sample());
    }
}
  • Controller中獲取對應DI例項的HashCode
public class HomeController : Controller
{
    private readonly ISample _transient;
    private readonly ISample _scoped;
    private readonly ISample _singleton;

    public HomeController(
        ISampleTransient transient,ISampleScoped scoped,ISampleSingleton singleton)
    {
        _transient = transient;
        _scoped = scoped;
        _singleton = singleton;
    }

    public IActionResult Index() {
        ViewBag.TransientId = _transient.Id;
        ViewBag.TransientHashCode = _transient.GetHashCode();

        ViewBag.ScopedId = _scoped.Id;
        ViewBag.ScopedHashCode = _scoped.GetHashCode();

        ViewBag.SingletonId = _singleton.Id;
        ViewBag.SingletonHashCode = _singleton.GetHashCode();
        return View();
    }
}
  • VewBag 顯示元件
<table border="1">
    <tr><td colspan="3">Cotroller</td></tr>
    <tr><td>Lifetimes</td><td>Id</td><td>Hash Code</td></tr>
    <tr><td>Transient</td><td>@ViewBag.TransientId</td><td>@ViewBag.TransientHashCode</td></tr>
    <tr><td>Scoped</td><td>@ViewBag.ScopedId</td><td>@ViewBag.ScopedHashCode</td></tr>
    <tr><td>Singleton</td><td>@ViewBag.SingletonId</td><td>@ViewBag.SingletonHashCode</td></tr>
</table>

可自行做測試,具體可參考此部落格

2.2.2 實現了展現層(呼叫者)與服務類之間的解耦

如上,例項是在HomeController中通過介面來呼叫例項的,因此修改程式只需要在例項中需改,而不需要在呼叫層修改。
這符合了6大程式設計原則中的依賴倒置原則:
1.高層模組不應該依賴於低層模組,兩者都應該依賴其抽象
展現層Controller沒有依賴Model層Sample類,兩者都依賴了Sample的介面抽象ISample,ISampleTransient,ISampleScoped,ISampleSingleton.
2.抽象不應該依賴於細節
介面層只定義規範,沒有定義細節。

public interface ISample
{
    int Id { get; }
}

public interface ISampleTransient : ISample
{
}

public interface ISampleScoped : ISample
{
}

public interface ISampleSingleton : ISample
{
}

3.細節應該依賴於抽象
DI中取例項依賴於介面:

ISampleTransient transient;

服務類的實現也依賴於介面:

public class Sample : ISampleTransient,ISampleSingleton
{
    private static int _counter;
    private int _id;

    public Sample()
    {
        _id = ++_counter;
    }

    public int Id => _id;
}

2.2.3 開發者不用再去考慮依賴之間的關係

使程式設計師不用再去考慮各個DI例項之間的依賴,以及new很多個相互依賴的例項。

2.3 依賴注入使用的設計模式

2.3.1 代理模式

在依賴注入的服務呼叫的地方,容器管理者serviceProvider從程式設計師手中取得控制權,控制所需服務例項化以及設定好他的生命週期,然後返回給程式設計師。

2.3.2 工廠模式

根據DI的生命週期設定,根據介面或者型別,生產出各種生命週期的例項,需要注意的是這裡有可能是同一例項(scope的單次請求中,或者Transient生命週期),Transient每次產生的都是新的例項。

3.在Net Core 3.1上基於winform實現依賴注入

3.1 Net Core 3.1中對winform的支援。

筆者發現在最新的VS發行版中,能建立winform工程,但卻無法開啟設計器,也無法開啟winform的工具箱。怎麼辦?
微軟官方部落格中提到在VS16.5預覽版中支援了winform設計器,根據部落格中提到,需要在此下載連結下載VS16.5預覽版。

NetCore3.1 winform截圖如下:

技術分享圖片

可以看到控制元件明顯比基於dot Net Framework的好看很多,同時,工具箱中的控制元件很少,微軟把一些老的已經有替代的控制元件刪除了,並且以後會慢慢加入一些必要的控制元件。

3.2 winform依賴注入與net core MVC的不同?

net core MVC容器是自動建立好的,只需要在ConfigureServices方法裡配置服務即可。而在Net Core3.1上建立了winform工程之後窗體是new例項,以單例的形式跑的。容器的配置建立,都需要自己來做。

    static class Program
    {
        /// <summary>
        ///  The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.SetHighDpiMode(HighDpiMode.SystemAware);
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }

那如果需要向Form窗體中注入服務就需要在new例項的時候就傳入實參。

[STAThread]
  static void Main()
  {
      Application.SetHighDpiMode(HighDpiMode.SystemAware);
      Application.EnableVisualStyles();
      Application.SetCompatibleTextRenderingDefault(false);
 
      var services = new ServiceCollection();
 
      ConfigureServices(services);
 
      using (ServiceProvider serviceProvider = services.BuildServiceProvider())
      {
          var logg = services.BuildServiceProvider().GetRequiredService<ILogger<Form1>>();
 
          var businessObject = services.BuildServiceProvider().GetRequiredService<IBusinessLayer>();
 
          Application.Run(new Form1(logg,businessObject));
      }
  }

呼叫的時候用窗體的建構函式呼叫服務介面即可。

public partial class Form1 : Form
    {
        private readonly ILogger _logger;
 
        private readonly IBusinessLayer _business;
        public Form1(ILogger<Form1> logger,IBusinessLayer business)
        {
            _logger = logger;
            _business = business;
            InitializeComponent();
        }
 
        private void button1_Click(object sender,EventArgs e)
        {
            try
            {
                _logger.LogInformation("Form1 {BusinessLayerEvent} at {dateTime}","Started",DateTime.UtcNow);
 
                // Perform Business Logic here 
                _business.PerformBusiness();
 
                MessageBox.Show("Hello .NET Core 3.0 . This is First Forms app in .NET Core");
 
                _logger.LogInformation("Form1 {BusinessLayerEvent} at {dateTime}","Ended",DateTime.UtcNow);
 
            }
            catch (Exception ex)
            {
                //Log technical exception 
                _logger.LogError(ex.Message);
                //Return exception repsponse here
                throw;
 
            }
 
        }
    }

本方法摘自此文

這樣至少有兩個缺點:

  1. Form1中建構函式的依賴注入例項呼叫洩露在了他的呼叫層,這不符合6大程式設計原則中的依賴倒置原則;
  2. 當Form1中需要從DI中增加介面例項呼叫時,也需要在如下呼叫程式碼中增加對應實參。而且實參多了,會很冗長。
    Application.Run(new Form1(logg,businessObject));

3.3 解決3.2的思路

把form的型別也以單例的形式注入到容器中,呼叫時,獲取MainForm型別的服務。這樣此服務例項依賴於其他的服務。ServiceProvider容器管理者會自動解決好服務之間的依賴關係,並將對應的服務例項化並根據生命週期設定好,交給程式設計師去使用。問題完美解決。

此思路有借鑑於以下兩篇文章
微軟MSDN
stackoverflow
這裡向大家重點推薦下stackoverflow,這個基於世界級的程式設計師論壇,在我遇到很多的疑難雜症,孤立無援的時候,他都會給予我解決問題的思路,方向甚至方案,再次致敬感謝stackoverflow,同時也感謝谷歌。

3.4程式碼實現

3.4.1 在Program.cs中建立服務註冊靜態方法

        private static void ConfigureServices(ServiceCollection services)
        {
            //App
            services.ApplicationServiceIoC();
            //Infra

            //Repo
            services.InfrastructureORM<DapperIoC>();


            //Presentation 其他的窗體也可以注入在此處
            services.AddSingleton(typeof(MainForm));
        }

這裡需要說明的是,筆者這裡的IoC是應用層,展現層,倉儲層分層注入了,每層都寫了ServiceCollection服務容器的靜態方法,所以服務可以在各層注入,讀者可以不去追究,將自己的服務注入在此即可。
分層注入:

技術分享圖片

分層注入簡單實現
CameraDM_Service註冊在了ApplicationServiceIoC,ApplicationServiceIoC註冊在了ConfigureServices。這就是我剛說的分層注入每層的依賴。

    public static class ServicesIoC
    {
        public static void ApplicationServiceIoC(this IServiceCollection services)
        {
            services.AddScoped(typeof(IServiceBase<>),typeof(ServiceBase<>));
            services.AddScoped<ICameraDM_Service,CameraDM_Service>();
        }
    }

重點關注
將窗體型別注入,當然後續加入其它窗體也可用同樣方法進行注入。

services.AddSingleton(typeof(MainForm));

3.4.2 建立服務容器物件

var services = new ServiceCollection();

3.4.3 新增服務註冊

 ConfigureServices(services);

此步驟呼叫的就是3.4.1中的方法。

3.4.4 構建ServiceProvider物件

  var serviceProvider = services.BuildServiceProvider();

3.4.5 執行MainForm服務

向服務管理者請求MainForm型別的例項服務,具體呼叫過程詳見2.1。

Application.Run(serviceProvider.GetService<MainForm>()); 

這一步是重點,也是winform跟MVC使用上的區別,但是本質卻是相同的,都是由serviceProvider管理著WPF,winform或者MVC這些例項以及他們對應的型別,只不過MVC容器已經建立好了,容器管理者serviceProvider也已經建立好了,直接往容器裡Add服務即可,而winform,WPF,net core控制檯程式需要我們自己去往容器裡添加註冊服務,並且建立容器管理者serviceProvider。因為ServiceCollection容器是死的,只有建立了serviceProvider容器管理者這個代理角色,容器才能體現出他的價值。而只有serviceProvider,沒有ServiceCollection裡的服務也是毫無意義的。

3.4.1到3.4.5整體程式碼如下:

    static class Program
    {
        /// <summary>
        ///  The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.SetHighDpiMode(HighDpiMode.SystemAware);
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            //建立服務容器物件
            var services = new ServiceCollection();

            //新增服務註冊
            ConfigureServices(services);
            //構建ServiceProvider物件
            var serviceProvider = services.BuildServiceProvider();
            //向服務管理者請求MainForm型別的例項服務
            Application.Run(serviceProvider.GetService<MainForm>());    
        }
        private static void ConfigureServices(ServiceCollection services)
        {
            //App
            services.ApplicationServiceIoC();
            //Infra

            //Repo
            services.InfrastructureORM<DapperIoC>();


            //Presentation 其他的窗體也可以注入在此處
            services.AddSingleton(typeof(MainForm));
        }
    }

3.4.6建構函式法呼叫DI例項

    public partial class MainForm : Form
    {
        ICameraDM_Service _cameraDM_Service;
        public MainForm(ICameraDM_Service cameraDM_Service)
        {
            _cameraDM_Service = cameraDM_Service;
            InitializeComponent();          
        }
        private async void button1_Click(object sender,EventArgs e)
        {
            MessageBox.Show(_cameraDM_Service.GetAllCameraInfo().ToList().Count().ToString());
            var _camera  =await _cameraDM_Service.GetAllIncludingTasksAsync();
            //textBox1.Text = _camera.ToList().Count().ToString();
            var _cameraNo3 = await _cameraDM_Service.GetByIdAsync(3);
            textBox1.Text = _cameraNo3.InstallTime.ToString();
        }
    }

3.5演示效果

點選按鈕之後從攝像頭服務中獲取到了攝像頭的數量。
技術分享圖片

點選確定之後從攝像頭服務中獲取到了3號攝像頭的安裝時間。

技術分享圖片

3.6 如何呼叫用依賴注入在母窗體中呼叫子窗體

3.6屬於12/23/13:33新增,為了答覆1樓網友 程式設計老油條所問問題。
問題如下:
假設在MainForm中的button1_Click,還需要開啟其他視窗,要如何實現?(只能往 MainForm 中傳遞serviceProvider嗎?)

把serviceProvider設計成全域性靜態的,可設計成單例模式或直接放在Main的屬性中,供全域性任意子窗體訪問獲取DI例項即可,當然同時,其他窗體也需要注入到容器中。

3.6.1 注入子窗體

注入生命週期為瞬時的Form1型別。
services.AddTransient(typeof(Form1));

        private static void ConfigureServices(ServiceCollection services)
        {
            //App
            services.ApplicationServiceIoC();
            //Infra

            //Repo
            services.InfrastructureORM<DapperIoC>();

            //Presentation 其他的窗體也可以注入在此處
            services.AddSingleton(typeof(MainForm));
            services.AddTransient(typeof(Form1));
            
        }

因為Form1是MainForm的子窗體,而MainForm設定成了單例模式,所以在MainForm中開啟Form1是屬於同一次請求,姑不能用AddSingleton跟AddScope模式。如果使用以上兩種模式,會報如下異常:
比如設定Form1生命週期為單例模式

            services.AddSingleton(typeof(MainForm));
            services.AddSingleton(typeof(Form1));

第一次呼叫正常,

技術分享圖片

關閉Form1第二次點選MainForm的button1時,報如下異常:

技術分享圖片

因為是單例模式,我們關閉了Form1,MainForm還在,再次點選button1,會找不到生命週期是單例模式的Form1例項,就會報如上異常。
修改成

services.AddTransient(typeof(Form1));

問題得到完美解決,無論關閉多少次Form1,都能通過MainForm的button1呼叫開啟Form1。

3.6.2 設定全域性serviceProvider容器服務管理者

修改serviceProvider為Program靜態類的公用屬性(全域性),以給子窗體或其他winform中的元件來容器服務者獲取DI例項
public static IServiceProvider serviceProvider { get; set; }
Program.cs全部程式碼如下。

    static class Program
    {
        public static IServiceProvider serviceProvider { get; set; }
        /// <summary>
        ///  The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            
            Application.SetHighDpiMode(HighDpiMode.SystemAware);
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            //建立服務容器物件
            var services = new ServiceCollection();

            //新增服務註冊
            ConfigureServices(services);
            //構建ServiceProvider物件
            serviceProvider = services.BuildServiceProvider();

        //向服務管理者請求MainForm型別的例項服務
        Application.Run(serviceProvider.GetService<MainForm>());    
        }
        private static void ConfigureServices(ServiceCollection services)
        {
            //App
            services.ApplicationServiceIoC();
            //Infra

            //Repo
            services.InfrastructureORM<DapperIoC>();

            //Presentation 其他的窗體也可以注入在此處
            services.AddSingleton(typeof(MainForm));
            services.AddTransient(typeof(Form1));

        }
    }

3.6.3 MainForm中呼叫Form1

到Program服務管理者屬性手中拿到對應所需型別的設定好生命週期的Form1例項。顯示Form1。

    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();          
        }

        private async void button1_Click(object sender,EventArgs e)
        {
            var child = (Form)Program.serviceProvider.GetService((typeof(Form1)));
            child.Show();
        }
    }

3.6.4 建構函式法呼叫DI例項

Form1呼叫camera服務
此處同3.5

    public partial class Form1 : Form
    {

        ICameraDM_Service _cameraDM_Service;
        public Form1(ICameraDM_Service cameraDM_Service)
        {
            _cameraDM_Service = cameraDM_Service;
            InitializeComponent();

        }
        private async  void button1_Click(object sender,EventArgs e)
        {
            MessageBox.Show(_cameraDM_Service.GetAllCameraInfo().ToList().Count().ToString());
        }
    }

3.6.5效果

技術分享圖片

4.最後

本來就想寫篇短文,誰知道洋洋灑灑還寫得有點長。本文如果大家讀了有疑惑,請提出來,我會耐心解答;如果知識點上有不妥當不正確或者不同見解的地方,也懇請指出,我同時也很渴望進步。最後祝大家冬至安康,闔家幸福。