如何在WinForms中使用依赖注入

36

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

接口ICategory:

public interface ICategory
{
    void Save();
}

类别存储库类:

public class CategoryRepository : ICategory
{

    private readonly ApplicationDbContext _context;

    public CategoryRepository(ApplicationDbContext contex)
    {
        _context = contex;
    }
    public void Save()
    {
        _context.SaveChanges();
    }
}

表单1:

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();
}
 }

表单2:

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

问题?

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

Application.Run(new Form1());
  • 在 Form 2 调用时的依赖注入定义

  • Form2 frm= new Form2();
    frm.show();
    

    2
    我不确定你的问题是什么? 如果你想让它自动工作,你可能需要使用一个 DI 框架。 或者你可以在 new Form1(new ConcreteCategory()) 的构造函数中手动传递具体类型。 - Charles
    3个回答

    67

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

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

    1. 创建一个WinForms .NET应用程序

    2. 安装Microsoft.Extensions.Hosting软件包(它提供了许多有用的功能,如DI、日志记录、配置等)

    3. 添加一个新接口,IHelloService.cs

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

    5. public class HelloService : IHelloService
      {
          public string SayHello()
          {
              return "Hello, world!";
          }
      }
      
    6. 修改 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>();
                  });
          }
      }
      

    现在您可以在 Form1 中注入 IHelloService 并使用它:

    //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());
        }
    }
    
    如果您想使用 DI 来显示 Form2,则需要先注册它 services.AddTransient<Form2>(),然后根据 Form2 的使用情况,可以使用以下任一选项:
    • 如果您只需要在 Form1 的整个生命周期中使用单个 Form2 实例,则可以将其注入为 Form1 的构造函数的依赖项,并存储实例,以便随时显示它。 但请注意:它仅会初始化一次,在打开 Form1 时进行初始化,不会再次进行初始化。您也不应该处理它,因为它是传递给 Form1 的唯一实例。
    public Form1(IHelloService helloService, Form2 form2)
    { 
         InitializeComponent();
             form2.ShowDialog();
    }
    
  • 如果您需要多个Form2的实例或需要多次初始化它,则可以像这样获取它的实例:

  • using (var form2 = Program.ServiceProvider.GetRequiredService<Form2>())
         form2.ShowDialog();
    

    1
    @Charles我期望它的工作方式类似于AddSingleton,因为它具有单个范围。 - Reza Aghaei
    1
    @WiktorZychla 感谢您的反馈。我认为这个答案对于那些有兴趣在Windows表单中使用DI的人来说是一个足够好的起点。对于真实世界的应用程序,未来的读者应该考虑更多的架构问题。我也有发布博客文章来扩展这个主题的想法,但我不确定是否很快就会发布它。 - Reza Aghaei
    1
    @DanFriedman,第二个要点是显示使用ShowDialog时应如何处理。但是,如果您将它们使用Show显示,则不需要(也不应该)处理实例。 - Reza Aghaei
    1
    @DanFriedman,您可能会发现我在这个主题上的帖子很有用:关闭窗体后是否需要Dispose窗体? - Reza Aghaei
    1
    @RezaAghaei:只是提供信息,我已经写了一篇博客文章。该方法涵盖了多种情况,特别是不仅服务,而且其他自由参数也可以在窗体构造函数中被要求。因此,这篇文章太长了,无法从中得出答案。对于OP-请参考博客文章。问候。 - Wiktor Zychla
    显示剩余9条评论

    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();
        }
    }
    

    从现在开始,这个工厂是主要客户端创建表单的接口。客户端代码不再仅仅是调用。

    var form1 = new Form1();
    

    不,这是被禁止的。相反,客户端应始终调用。
    var form1 = new FormFactory().CreateForm1();
    

    (以及其他形式的类似情况)。

    请注意,虽然工厂已经实现,但它本身并不会做任何事情!相反,它将创建委托给某种神秘的提供者,这个提供者必须被注入到工厂中。这个想法背后的原理是,提供者将在组合根中被注入一次,这是代码中靠近启动并且非常高的地方,这样所有依赖项都可以在那里解析。因此,表单工厂不需要知道最终将注入哪个提供者。

    这种方法有一个显着的优点-根据实际要求,可以注入不同的提供者,例如,可以为实际应用程序编写基于DI的提供者(我们将在下面编写它),并为单元测试编写存根提供者。

    无论如何,让我们来创建一个具有依赖项的表单:

    public partial class Form1 : Form
    {
        private IHelloWorldService _service;
    
        public Form1(IHelloWorldService service)
        {
            InitializeComponent();
    
            this._service = service;
        }
    }
    

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

    var form2 = new FormFactory().CreateForm2();
    

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

    public Form2( string something, int somethingElse ) ...
    

    但现在你需要更像这样的东西

    public Form2( ISomeService service1, IAnotherService service2, 
                  string something, int somethingElse ) ...
    

    这是我们真正需要仔细研究的内容。再看一遍,一个现实生活中的表单可能需要:

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

    我们如何处理这个问题?

    为了有一个完整的例子,让我们修改表单工厂。

    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);
        }
    }
    

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

    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;
        }
    }
    

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

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

    现在最后是组合根。让我们从服务开始。

    public interface IHelloWorldService
    {
        string DoWork();
    }
    
    public class HelloWorldServiceImpl : IHelloWorldService
    {
        public string DoWork()
        {
            return "hello world service::do work";
        }
    }
    

    请注意,虽然接口应该在堆栈的某个位置(以便被所有人识别),但实现可以在任何地方提供(表单不需要引用实现!)。接下来是起始代码,最终提供了表单工厂并设置了容器。
    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 );
        }
    }
    

    首先请注意如何使用Host.CreateDefaultBuilder创建容器,这是一项简单的任务。然后请注意如何注册服务以及如何在其他服务中注册表单。
    对于没有任何依赖关系的表单来说,这很直接,只需
    services.AddTransient<Form1>();
    

    然而,如果一个表单需要服务和自由参数,它将被注册为...表单创建函数,一个任意自由参数的Func,返回实际的表单。看一下这个。
    services.AddTransient<Func<string,Form2>>(
        container =>
            something =>
            {
                var helloWorldService = container.GetRequiredService<IHelloWorldService>();
                return new Form2(helloWorldService, something);
            });
    

    这很聪明。我们使用其中一种注册机制来注册一个表单工厂函数,该机制本身使用工厂函数(是的,一个使用另一个工厂的工厂,即Factception。如果你感到迷失,请随意休息一下)。我们注册的函数Func<string, Form2>有一个参数,即something(对应于表单构造函数的自由参数),但它的其他依赖项由容器解决...这正是我们想要的。
    这就是为什么实际的表单工厂需要注意它解析的内容。一个简单的表单解析如下:
    return _serviceProvider.GetRequiredService<Form1>();
    

    其中另一种方法需要分两步解决。我们首先解析工厂函数,然后使用创建方法的参数将其馈送到函数中:

    var _form2Factory = _serviceProvider.GetRequiredService<Func<string, Form2>>();
    return _form2Factory( something );
    

    就是这样。每当创建一个表单时,它会是以下两者之一:

    new FormFactory().CreateForm1();
    

    对于“简单”表单(仅具有服务依赖项)或只是

    new FormFactory().CreateForm2("foo");
    

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


    2
    这是一个我已经点赞的精彩答案。我即将尝试使用它,但我想提出一点观察 - 在工厂形式中,我不得不多次阅读才能理解的是这行代码:“static IFormFactory _provider;”这就是允许我们在启动时注册提供程序并使其可用于通过其他方式创建的FormFactory实例的东西,而不是DI。在我弄清楚这一点之前,我读了几遍。 - EGP
    1
    @EGP:谢谢,这是非常方便的模式,不知道为什么很少被提到。 - Wiktor Zychla
    1
    我认为可能之所以没有经常提到它,是因为有其他方法可以完成它所做的一切,而且这种模式有点难以遵循其每个部分。但是,它绝对是最干净和最符合我们在Web应用程序中进行DI的方式。我个人希望我们可以为Windows表单构建类似于MVC路由和ConfigureWebHostDefaults的东西。我怀疑除非第三方编写它,否则永远不会发生。 - EGP
    1
    @Bandito:假设你有4个参数,你需要一个Func<string,string,int,string,Form2> - Wiktor Zychla
    2
    @Bandito: container => ( p1, p2, p3, p4 ) => ... 这只是一个容器的函数,它创建了一个工厂函数来处理你的参数。 - Wiktor Zychla
    显示剩余7条评论

    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);
                    });
                });
        }
    }
    

    表单工厂

    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>();
        }
    } 
    

    表单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);
        }
    }
    

    表格 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();
        }
    }
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接