如何在 WinForms 中使用依赖注入

Moh*_*rak 39 .net c# dependency-injection winforms

如何在 Winforms C# 中定义依赖注入?

接口IC类别:

public interface ICategory
{
    void Save();
}
Run Code Online (Sandbox Code Playgroud)

类类别存储库:

public class CategoryRepository : ICategory
{

    private readonly ApplicationDbContext _context;

    public CategoryRepository(ApplicationDbContext contex)
    {
        _context = contex;
    }
    public void Save()
    {
        _context.SaveChanges();
    }
}
Run Code Online (Sandbox Code Playgroud)

表格一:

public partial class Form1 : Form
{
    private readonly  ICategury _ic;
    public Form1(ICategury ic)
    {
        InitializeComponent();
    _ic=ic
    }

    private void button1_Click(object sender, EventArgs e)
    {
    Form2 frm= new Form2();
    frm.show();
}
 }
Run Code Online (Sandbox Code Playgroud)

表格2:

public partial class Form2 : Form
{
    private readonly  ICategury _ic;
    public Form2(ICategury ic)
    {
        InitializeComponent();
    _ic=ic
    }
 }
Run Code Online (Sandbox Code Playgroud)

问题?

  1. Program.cs中依赖注入的定义

    Application.Run(new Form1());
    
    Run Code Online (Sandbox Code Playgroud)
  2. Form 2 调用时依赖注入的定义

    Form2 frm= new Form2();
    frm.show();
    
    Run Code Online (Sandbox Code Playgroud)

Rez*_*aei 71

如何在 Windows 窗体 (WinForms) 中使用依赖注入 (DI)

要在 WinForms .NET 5 或 6 中使用 DI,您可以执行以下步骤:

  1. 创建 WinForms .NET 应用程序

  2. 安装 Microsoft.Extensions.Hosting 包(它为您提供了一系列有用的功能,例如 DI、日志记录、配置等)

  3. 添加新接口IHelloService.cs

    public interface IHelloService
    {
        string SayHello();
    }
    
    Run Code Online (Sandbox Code Playgroud)
  4. 为您的服务添加新的实现HelloService.cs

    public class HelloService : IHelloService
    {
        public string SayHello()
        {
            return "Hello, world!";
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  5. 修改Program.cs

    //using Microsoft.Extensions.DependencyInjection;
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.SetHighDpiMode(HighDpiMode.SystemAware);
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
    
            var host = CreateHostBuilder().Build();
            ServiceProvider = host.Services;
    
            Application.Run(ServiceProvider.GetRequiredService<Form1>());
        }
        public static IServiceProvider ServiceProvider { get; private set; }
        static IHostBuilder CreateHostBuilder()
        {
            return Host.CreateDefaultBuilder()
                .ConfigureServices((context, services)=>{
                    services.AddTransient<IHelloService, HelloService>();
                    services.AddTransient<Form1>();
                });
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

现在您可以注入IHelloServiceForm1使用它:

//using Microsoft.Extensions.DependencyInjection;
public partial class Form1 : Form
{
    private readonly IHelloService helloService;

    public Form1(IHelloService helloService)
    {
        InitializeComponent();
        this.helloService = helloService;
        MessageBox.Show(helloService.SayHello());
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您想Form2使用DI进行显示,您首先需要注册它 services.AddTransient<Form2>();,然后根据Form2的用途,您可以使用以下选项之一:

  • Form2如果您在 的整个生命周期中只需要一个 实例Form1,那么您可以将其作为依赖项注入到 的构造函数中Form1,并存储该实例并在需要时显示它。

    但请注意:它只会初始化一次,打开时Form1不会再次初始化。您也不应该处置它,因为它是传递给 的唯一实例Form1

    public Form1(IHelloService helloService, Form2 form2)
    { 
         InitializeComponent();
             form2.ShowDialog();
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 如果您需要多个实例Form2或者需要多次初始化它,那么您可能会像这样获得它的实例:

    using (var form2 = Program.ServiceProvider.GetRequiredService<Form2>())
         form2.ShowDialog();
    
    Run Code Online (Sandbox Code Playgroud)


Wik*_*hla 12

除了 Reza 在他的回答中描述的方法之外,还有另一种方法。我的这个答案最初发布在我的博客上,但是,由于 Stack Overflow 是我们大多数人的主要信息来源,因此 Reza 答案下面的评论中链接的博客文章很容易被错过。

那么就这样吧。该解决方案基于本地工厂模式。

我们将从表单工厂开始

public interface IFormFactory
{
    Form1 CreateForm1();
    Form2 CreateForm2();
}

public class FormFactory : IFormFactory
{
    static IFormFactory _provider;

    public static void SetProvider( IFormFactory provider )
    {
        _provider = provider;
    }

    public Form1 CreateForm1()
    {
        return _provider.CreateForm1();
    }

    public Form2 CreateForm2()
    {
        return _provider.CreateForm2();
    }
}
Run Code Online (Sandbox Code Playgroud)

从现在开始,该工厂是创建表单的主要客户端界面。客户端代码不再应该只调用

var form1 = new Form1();
Run Code Online (Sandbox Code Playgroud)

不,这是禁止的。相反,客户端应该始终调用

var form1 = new FormFactory().CreateForm1();
Run Code Online (Sandbox Code Playgroud)

(对于其他形式也类似)。

请注意,虽然工厂已实现,但它本身并不执行任何操作!相反,它将创建委托给一个神秘的提供者,该提供者必须注入到工厂中。这背后的想法是,提供程序将被注入到组合根中一次,组合根是代码中靠近启动的位置,并且在应用程序堆栈中位于非常高的位置,以便可以在那里解决所有依赖项。因此,表单工厂不需要知道最终将注入哪个提供程序。

这种方法有一个显着的优势 - 根据实际需求,可以注入不同的提供程序,例如,您可以为实际应用程序提供一个基于 DI 的提供程序(我们稍后会编写它),并为单元测试提供一个存根提供程序。

无论如何,让我们有一个具有依赖关系的表单:

public partial class Form1 : Form
{
    private IHelloWorldService _service;

    public Form1(IHelloWorldService service)
    {
        InitializeComponent();

        this._service = service;
    }
}
Run Code Online (Sandbox Code Playgroud)

这种形式依赖于一个服务,该服务将由构造函数提供。如果 Form1 需要创建另一个表单 Form2,它会按照我们已经讨论过的方式进行操作:

var form2 = new FormFactory().CreateForm2();
Run Code Online (Sandbox Code Playgroud)

然而,当表单不仅需要依赖服务还需要一些自由参数(字符串、整数等)时,事情就会变得更加复杂。通常,你会有一个构造函数

public Form2( string something, int somethingElse ) ...
Run Code Online (Sandbox Code Playgroud)

但现在你需要更多类似的东西

public Form2( ISomeService service1, IAnotherService service2, 
              string something, int somethingElse ) ...
Run Code Online (Sandbox Code Playgroud)

这是我们真正应该考虑的事情。再看一遍,现实生活中的形式可能需要

  • 容器解析的一些参数
  • 表单创建者应提供的其他参数(容器不知道!)。

我们该如何处理?

为了获得完整的示例,我们来修改表单工厂

public interface IFormFactory
{
    Form1 CreateForm1();
    Form2 CreateForm2(string something);
}

public class FormFactory : IFormFactory
{
    static IFormFactory _provider;

    public static void SetProvider( IFormFactory provider )
    {
        _provider = provider;
    }

    public Form1 CreateForm1()
    {
        return _provider.CreateForm1();
    }

    public Form2 CreateForm2(string something)
    {
        return _provider.CreateForm2(something);
    }
}
Run Code Online (Sandbox Code Playgroud)

让我们看看表单是如何定义的

public partial class Form1 : Form
{
    private IHelloWorldService _service;

    public Form1(IHelloWorldService service)
    {
        InitializeComponent();

        this._service = service;
    }

    private void button1_Click( object sender, EventArgs e )
    {
        var form2 = new FormFactory().CreateForm2("foo");
        form2.Show();
    }
}

public partial class Form2 : Form
{
    private IHelloWorldService _service;
    private string _something;

    public Form2(IHelloWorldService service, string something)
    {
        InitializeComponent();

        this._service = service;
        this._something = something;

        this.Text = something;
    }
}
Run Code Online (Sandbox Code Playgroud)

你能在这里看到一个模式吗?

  • 每当一个表单(例如 Form1)只需要依赖和服务时,它在 FormFactory 中的创建方法就是空的(依赖关系将由容器解析)。
  • 每当表单(例如 Form2)需要依赖服务和其他自由参数时,FormFactory 中的创建方法都会包含与这些自由参数对应的参数列表(服务依赖关系将由容器解析)

现在终于到了组合根。我们先从服务开始

public interface IHelloWorldService
{
    string DoWork();
}

public class HelloWorldServiceImpl : IHelloWorldService
{
    public string DoWork()
    {
        return "hello world service::do work";
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,虽然接口应该位于堆栈中的某个位置(每个人都可以识别),但实现可以在任何地方自由提供(表单不需要引用实现!)。然后,遵循起始代码,最终提供表单工厂并设置容器

internal static class Program
{
    [STAThread]
    static void Main()
    {
        var formFactory = CompositionRoot();

        ApplicationConfiguration.Initialize();
        Application.Run(formFactory.CreateForm1());
    }

    static IHostBuilder CreateHostBuilder()
    {
        return Host.CreateDefaultBuilder()
            .ConfigureServices((context, services) => {
                services.AddTransient<IHelloWorldService, HelloWorldServiceImpl>();
                services.AddTransient<Form1>();
                services.AddTransient<Func<string,Form2>>(
                    container =>
                        something =>
                        {
                            var helloWorldService = 
                                container.GetRequiredService<IHelloWorldService>();
                            return new Form2(helloWorldService, something);
                        });
            });
    }

    static IFormFactory CompositionRoot()
    {
        // host
        var hostBuilder = CreateHostBuilder();
        var host = hostBuilder.Build();

        // container
        var serviceProvider = host.Services;

        // form factory
        var formFactory = new FormFactoryImpl(serviceProvider);
        FormFactory.SetProvider(formFactory);

        return formFactory;
    }
}

public class FormFactoryImpl : IFormFactory
{
    private IServiceProvider _serviceProvider;

    public FormFactoryImpl(IServiceProvider serviceProvider)
    {
        this._serviceProvider = serviceProvider;
    }

    public Form1 CreateForm1()
    {
        return _serviceProvider.GetRequiredService<Form1>();
    }

    public Form2 CreateForm2(string something)
    {
        var _form2Factory = _serviceProvider.GetRequiredService<Func<string, Form2>>();
        return _form2Factory( something );
    }
}
Run Code Online (Sandbox Code Playgroud)

首先注意如何使用 Host.CreateDefaultBuilder 创建容器,这是一项简单的任务。然后注意如何注册服务以及如何在其他服务中注册表单。

这对于没有任何依赖项的表单来说很简单,只是

services.AddTransient<Form1>();
Run Code Online (Sandbox Code Playgroud)

但是,如果表单需要服务和自由参数,则将其注册为...表单创建函数,即返回实际表单的任何自由参数的 Func。看看这个

services.AddTransient<Func<string,Form2>>(
    container =>
        something =>
        {
            var helloWorldService = container.GetRequiredService<IHelloWorldService>();
            return new Form2(helloWorldService, something);
        });
Run Code Online (Sandbox Code Playgroud)

这很聪明。我们使用一种注册机制来注册表单工厂函数,该注册机制本身使用工厂函数(是的,一个工厂使用另一个工厂,Factception。如果您在这里感到迷茫,请随意休息一下)。我们注册的函数 Func<string, Form2> 有一个参数,即 some(对应于表单构造函数的自由参数),但它的其他依赖项由容器解析(这就是我们想要的)。

这就是为什么实际的表单工厂需要注意它所解析的内容。一个简单的表格解析如下

return _serviceProvider.GetRequiredService<Form1>();
Run Code Online (Sandbox Code Playgroud)

其中另一个分两步解决。我们首先解析工厂函数,然后使用创建的方法参数将其提供给函数:

var _form2Factory = _serviceProvider.GetRequiredService<Func<string, Form2>>();
return _form2Factory( something );
Run Code Online (Sandbox Code Playgroud)

就是这样。每当创建表单时,要么

new FormFactory().CreateForm1();
Run Code Online (Sandbox Code Playgroud)

对于“简单”形式(仅具有服务依赖性)或只是

new FormFactory().CreateForm2("foo");
Run Code Online (Sandbox Code Playgroud)

适用于同时需要服务依赖项和其他自由参数的表单。

  • 这是一个精彩的答案,我已投赞成票。我正准备尝试使用它,但我想做一个观察 - 我必须阅读多次才能得到的是表单工厂中的这一行代码:“static IFormFactory _provider;” 这使得我们能够在启动时注册提供程序,并使其可用于由各个表单通过 DI 以外的方式创建的 FormFactory 实例。我读了好几遍才明白那篇文章。 (2认同)
  • @Bandito:container =&gt; ( p1, p2, p3, p4 ) =&gt; ...这只是容器的一个函数,它创建参数的工厂函数 (2认同)

小智 6

只是想将其添加到此处作为 IFormFactory 的替代模式。这就是我通常的处理方式。

好处是您不需要为添加的每个表单和每组参数不断更改 IFormFactory 接口。

应用程序启动时加载所有表单,并将参数传递给 show 方法或您可以在表单上定义的其他基本方法。

internal static class Program
{
    public static IServiceProvider ServiceProvider { get; private set; }
  
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();
        ServiceProvider = CreateHostBuilder().Build().Services;
        Application.Run(ServiceProvider.GetService<Form1>());
    }
   
    static IHostBuilder CreateHostBuilder()
    {
        return Host.CreateDefaultBuilder()
            .ConfigureServices((context, services) => {
                services.AddSingleton<IFormFactory,FormFactory>();
                services.AddSingleton<IProductRepository, ProductRepository>();

                 //Add all forms
                var forms = typeof(Program).Assembly
                .GetTypes()
                .Where(t => t.BaseType ==  typeof(Form))
                .ToList();

                forms.ForEach(form =>
                {
                    services.AddTransient(form);
                });
            });
    }
}
Run Code Online (Sandbox Code Playgroud)

表格工厂

public interface IFormFactory
{
    T? Create<T>() where T : Form;
}

public class FormFactory : IFormFactory
{
    private readonly IServiceScope _scope;

    public FormFactory(IServiceScopeFactory scopeFactory) 
    {
        _scope = scopeFactory.CreateScope();
    }

    public T? Create<T>() where T : Form
    {
        return _scope.ServiceProvider.GetService<T>();
    }
} 
Run Code Online (Sandbox Code Playgroud)

表1

public partial class Form1 : Form
{
    private readonly IFormFactory _formFactory;
    public Form1(IFormFactory formFactory)
    {
        InitializeComponent();
        _formFactory = formFactory;
    }

    private void button_Click(object sender, EventArgs e)
    {
        var form2 = _formFactory.Create<Form2>();
        form2?.Show(99);
    }
}
Run Code Online (Sandbox Code Playgroud)

表2

public partial class Form2 : Form
{
    private readonly IProductRepository _productRepository;

    public Form2(IProductRepository productRepository)
    {
        InitializeComponent();
        _productRepository = productRepository;
    }

    public void Show(int recordId)
    {
        var product = _productRepository.GetProduct(recordId);

        //Bind your controls etc
        this.Show();
    }
}
Run Code Online (Sandbox Code Playgroud)