在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服务管理者根据注入时的配置返回给程序对应的实例以及配置好实例的生命周期。windows

一张图就能够理解依赖注入实例调用过程
设计模式

图片来源出处,感谢做者。mvc

这里再向读者作个说明ServiceCollection是服务容器,serviceProvider是服务管理者,管理着服务容器,当程序发送抽象接口,或者类型时,serviceProvider会根据设置好的生命周期,返回须要的实例配置好实例的生命周期给程序员使用。app

2.1依赖注入的目的

经过代理模式serviceProvider控制反转,他将持有控制权,将全部须要用到的接口,类型,反射出对应的实例,实例化以及设置好实例的生命周期,而后将控制权返还给程序员,不用再去new 一个个实例,也不用去考虑实例之间的依赖关系,也不用去考虑实例的生命周期,最终目的就是解放程序员的生产力,让程序员更轻松地写程序。async

2.2依赖注入带来的好处

2.2.1生命周期的控制

在注入的同时能够设置以下三种生命周期:ide

  • 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, ISampleScoped, 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号摄像头的安装时间。

4.最后

原本就想写篇短文,谁知道洋洋洒洒还写得有点长。本文若是你们读了有疑惑,请提出来,我会耐心解答;若是知识点上有不稳当不正确或者不一样看法的地方,也恳请指出,我同时也很渴望进步。最后祝你们冬至安康,阖家幸福。

相关文章
相关标签/搜索