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

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

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

在net Core3.1上基于winform实现依赖注入实例的更多相关文章

  1. spring3——IOC之基于XML的依赖注入(DI )

    我们知道spring容器的作用是负责对象的创建和对象间关系的维护,在上一篇博客中我们讲到spring容器会先调用对象的无参构造方法创建一个空值对象,那么接下来容器就会对对象的属性进行初始化,这个初始化 ...

  2. 07 Spring框架 依赖注入(四)基于注解的依赖注入

    前面几节我们都在使用xml进行依赖的注入,但是在实际的开发中我们往往偏爱于使用注解进行依赖注入,因为这样更符合我们人的思维,并且更加快捷,本节就来讲述Spring基于注解的依赖注入: 信息注入注解 @ ...

  3. 详解 Spring 3.0 基于 Annotation 的依赖注入实现(转)

    使用 @Repository.@Service.@Controller 和 @Component 将类标识为 Bean Spring 自 2.0 版本开始,陆续引入了一些注解用于简化 Spring 的 ...

  4. 详解 Spring 3.0 基于 Annotation 的依赖注入实现--转载

    使用 @Repository.@Service.@Controller 和 @Component 将类标识为 Bean Spring 自 2.0 版本开始,陆续引入了一些注解用于简化 Spring 的 ...

  5. 详解 Spring 3.0 基于 Annotation 的依赖注入实现

    Spring 的依赖配置方式与 Spring 框架的内核自身是松耦合设计的.然而,直到 Spring 3.0 以前,使用 XML 进行依赖配置几乎是唯一的选择.Spring 3.0 的出现改变了这一状 ...

  6. spring4——IOC之基于注解的依赖注入(DI )

    spring容器对于Bean的创建和对象属性的依赖注入提供了注解的支持,让我们在开发中能够更加便捷的实现对象的创建和对象属性的依赖注入.一,对于Bean的创建spring容器提供了以下四个注解的支持: ...

  7. Spring:基于注解的依赖注入的使用

    1.什么是pojo?什么是bean? 首先,在之前几篇Spring的介绍文章当中,自己都提到了一个名词叫做POJO类,但是在回顾Spring的注解的使用的时候,去形容java当中的对象还有一个名词是叫 ...

  8. 史上最好用的依赖注入框架Google Guice【转】

    Guice是Google开发的一个轻量级,基于Java5(主要运用泛型与注释特性)的依赖注入框架(IOC).Guice非常小而且快. (其他的依赖注入框架还有Dagger,Spring) Spring ...

  9. Spring 基于构造函数的依赖注入

    基于构造函数依赖注入 sprig通过bean创建对象时,会通过bean提供的参数来选择调用某个构造函数.上例中, <constructor-arg ref="spellChecker& ...

随机推荐

  1. centos--该虚拟机似乎正在使用中。 如果该虚拟机未在使用,请按“获取所有权(T)”按钮获取它的所有权。否则,请按“取消(C)”按钮以防损坏。

    centos非正常关机,导致无法正常启动的问题 该虚拟机似乎正在使用中. 如果该虚拟机未在使用,请按“获取所有权(T)”按钮获取它的所有权.否则,请按“取消(C)”按钮以防损坏. 解决方案: 1. 找 ...

  2. 知识图谱如何运用于RecomSys

    将知识图谱作为辅助信息引入到推荐系统中可以有效地解决传统推荐系统存在的稀疏性和冷启动问题,近几年有很多研究人员在做相关的工作.目前,将知识图谱特征学习应用到推荐系统中主要通过三种方式——依次学习.联合 ...

  3. Java设计模式:Factory Method(工厂方法)模式

    概念定义 工厂方法(Factory Method)模式,又称多态工厂(Polymorphic Factory)模式或虚拟构造器(Virtual Constructor)模式.工厂方法模式通过定义工厂抽 ...

  4. 使用 Powershell 远程连接 windows server

    使用 Powershell 远程连接 windows server Intro 最近我们的开发环境增加了一个 windows 服务器,没有界面的,不能直接远程桌面连上去管理,需要使用 Powershe ...

  5. Think in Speed (关于速度的一点思考)

    天下武功,无坚不摧,唯快不破!所以我们重视速度没毛病! 老话说:不要过早优化.赞同! 我们在写代码过程中,有时可能就是为了追求所谓的性能,然后,就给自己挖坑了. 关于开发速度,我有以下几点思考: 1. ...

  6. SQL Server 数据库备份语句

    ); --文件名 DECLARE @date DATETIME; --日期 ); --文件存放路径 SELECT @date = GETDATE(); --获取当前时间 --根据当前时间自动生成文件名 ...

  7. Vue-nodeJS环境搭建

    Node.jsNode.js是一个基于Chrome V8引擎的[JavaScript运行环境]. Node.js使用了一个事件驱动.非阻塞式I/O 的模型.Node.js是一个让JavaScript运 ...

  8. jdk api 1.6,1.7,1.8,1.9版本(中文)

    有需要的朋友,请自行到百度云下载 链接:https://pan.baidu.com/s/18WgEZ1WpBz5YexbbgikJcA 提取码:xry4

  9. Javase之内部类概述

    内部类概述 把类定义在其他类的内部就称为内部类 class A{ class B{ } } B就称为内部类,A称为外部类. 内部类的访问特点 内部类直接访问外部类成员,包括私有. 外部类要访问内部类要 ...

  10. python web框架Flask——csrf攻击

    CSRF是什么? (Cross Site Request Forgery, 跨站域请求伪造)是一种网络的攻击方式,它在 2007 年曾被列为互联网 20 大安全隐患之一,也被称为“One Click ...