Results 1 to 5 of 5

Thread: Windows forms dependency injection

  1. #1

    Thread Starter
    Karen Payne MVP kareninstructor's Avatar
    Join Date
    Jun 2008
    Location
    Oregon
    Posts
    6,671

    Windows forms dependency injection

    First off, don't see the following appropriate for a code bank submission.

    For those working with .NET Core Windows forms that are interested in an example for dependency injection check out the following project over on GitHub in the following project.

  2. #2
    Hyperactive Member
    Join Date
    Jun 2019
    Posts
    474

    Re: Windows forms dependency injection

    Actually in the current (at the time of writing) OP version on GitHub there is no injection in provided forms, just using ServiceCollection and accessing via call to global static method Program.GetService which is not good practice. (Note: for the whole project only UserPresenter.cs class requires dependency object to be set in the constructor, but it is normal class, not WinForms specific)

    Dependency injection is used to avoid the call to global objects and can be implemented via constructor injection. For WinForms there are some specific requirements when performing dependency injection in constructor as there is required to call InitializeComponent() as first call in the constructor.

    For VB.NET and WinForms the IDE automatically adds that call when constructor is added with some comments why it is required.

    C# and WinForms - the default form code already includes the constructor and the call to InitializeComponent().

    Here is how the sample Program.cs and Form1 from the OP (again - at the time of writing) can be changed to use dependency injection. Note that some code is omitted to reduce code length.

    Program.cs:
    C# Code:
    1. public static IServiceProvider ServiceProvider { get; set; }
    2.  
    3. static void ConfigureServices()
    4. {
    5.     var services = new ServiceCollection();
    6.  
    7.     services.AddScoped<IUserService, UserService>();
    8.     services.AddScoped<IDataConnection, DataConnectionService>();
    9.     ServiceProvider = services.BuildServiceProvider();
    10. }
    11.  
    12. public static T GetService<T>() where T : class
    13. {
    14.     return (T)ServiceProvider.GetService(typeof(T));
    15. }
    16.  
    17. [STAThread]
    18. static void Main()
    19. {
    20.     Application.SetHighDpiMode(HighDpiMode.SystemAware);
    21.     Application.EnableVisualStyles();
    22.     Application.SetCompatibleTextRenderingDefault(false);
    23.     ConfigureServices();
    24.  
    25.     // Inject IDataConnection implementation registered in the service collection
    26.     Application.Run(new Form1(GetService(IDataConnection)));
    27. }

    Form1:
    C# Code:
    1. private  IDataConnection dataConnection;
    2. public Form1(IDataConnection dataConn)
    3. {
    4.     InitializeComponent();
    5.     dataConnection = dataConn;
    6. }
    7.  
    8. private void UserViewButton_Click(object sender, EventArgs e)
    9. {
    10.     new UserView().ShowDialog();
    11. }
    12.  
    13. private void GetDataConnectionButton_Click(object sender, EventArgs e)
    14. {
    15.     //dataConnection = Program.GetService<IDataConnection>(); // Already injected in constructor
    16.     Debug.WriteLine(dataConnection.GetConnection());  // Directly use the injected object
    17. }

    With that code Form1 can be placed in separate project (WinForms library/DLL) where it will not rely on global object as all required objects are injected by the caller.

    There are many automatic dependency injection libraries but they are more suitable for ASP.NET type of projects where objects are created and initialized by the ASP.NET web app/api engine on external requests (e.g. coming from web requests) and the engine (web framework) has to create controller objects and inject all their dependent objects from their constructors. ASP.NET Core can do that not only for classes but also for lambda expressions where delegate parameters are defined by the developer - most notable at the moment are .NET 6 minimal web API with inline lambda expressions.
    Last edited by peterst; Jun 11th, 2022 at 04:04 AM.

  3. #3
    Hyperactive Member
    Join Date
    Jun 2019
    Posts
    474

    Re: Windows forms dependency injection

    I have to add some important information about WinForms and user controls:

    Dependency injection is not fine for user controls as the forms designer/editor will not know how to initialize dependent objects to call the constructor. It could be used with two constructors - one without parameters, another with dependences but it quickly becomes a mess and better to expose public function that setup the control and is called from our code.
    Last edited by peterst; Jun 12th, 2022 at 05:36 AM. Reason: Clarification that it is about user controls

  4. #4
    Hyperactive Member
    Join Date
    Jun 2019
    Posts
    474

    Re: Windows forms dependency injection

    I was thinking where to use the DI containers with automatic injection of constructor parameters in WinForm app and here is simple example app description:

    • The app contains 3 forms - MainForm, ProductsForm, OrdersForm
    • MainForm is showing (via button clicks) products and orders forms
    • There are interfaces for separate readers and writers for products and orders. These are the interfaces to data access layer for different objects and in the app in-memory implementation is added for products and orders
    • ProductsForm constructor expects IProductsReader and IProductsWriter parameters so the reader and writer can be used to perform the data operations
    • OrdersForm constructor expects IOrdersReader and IOrdersWriter parameters for data access purpose


    MainForm can directly inject readers and writers inside corresponding forms but this will require to pass all those ISomethingReader/ISomethingWriter objects to MainForm so it can inject to ProductsForm and OrdersForm.

    The example is using similar to OP approach Microsoft.Extensions.DependencyInjection package to create ServiceCollection which is used by the ServiceProvider which will be used to get object of specified type and inject dependent object.

    Again, some code (mainly usings, namespaces and comments) is omitted for shortness.

    Program.cs void Main for WinForms app - this one is new for .NET 6 where ApplicationConfiguration.Initialize() is called instead of all those Application.SetSomething calls:
    C# Code:
    1. [STAThread]
    2. static void Main()
    3. {
    4.     // To customize application configuration such as set high DPI settings or default font,
    5.     // see [url]https://aka.ms/applicationconfiguration[/url].
    6.     ApplicationConfiguration.Initialize();
    7.        
    8.     var services = new ServiceCollection();
    9.     ConfigureServices(services);
    10.  
    11.     IServiceProvider serviceProvider =  services.BuildServiceProvider();
    12.  
    13.     Application.Run(new MainForm(serviceProvider));
    14. }
    After WinForms is initialized, the services collection is created. It is passed to ConfigureServices method which will add all dependencies to that collection. After that the service provider is built and passed (injected to constructor) of MainForm.

    The interesting part here is ConfigureServices method:
    C# Code:
    1. private static void ConfigureServices(ServiceCollection services)
    2. {
    3.     services.AddSingleton<IOrdersReader, OrdersReaderWriterInMemory>();
    4.     services.AddSingleton<IOrdersWriter, OrdersReaderWriterInMemory>();
    5.     services.AddTransient<OrdersForm>();
    6.  
    7.     services.AddSingleton<IProductsReader, ProductsReaderWriterInMemory>();
    8.     services.AddSingleton<IProductsWriter, ProductsReaderWriterInMemory>();
    9.     services.AddTransient<ProductsForm>();
    10. }
    This needs more explanation. Objects definitions can be added to services collection directly as object itself (both ProductsForm and OrdersForm) or via specific interface implementation - all those ISomethingReader/Writer and their specific implementations that will be injected if constructor expects specific interface.

    Also objects are added by the scope where they are created:
    • Singleton means they are created once for the lifetime of the app - here (Orders/Products)ReaderWriterInMemory classes should be created once as it keeps all data in memory (Dictionary object in my simple implementation) and if the object is created each time it is injected, the data will be lost as the dictionary will be created again and again...
    • Scoped - the object is created once for the scope of the currently executed object (class) - e.g. getting ProductsForm object will return new Form object the first time and all next requests to get ProductsForm will return that created object. But if ProductsForm is disposed (see example below) - it will crash on second try to show it by clicking Products button.

      Sometimes in WinForms app some non-modal forms need to be created only once and developer should care if the form is not disposed for some reason. They are suitable for use case of scoped objects.

      Scoped object are more useful, e.g. on class that perform some processing and needs to create logger with output file name having current date/time. Create the object and it will generate output file name with current/date time. On next requests to logger object, the DI engine will return same object so the file name will be the same for the lifetime of that processing class.
    • Transient - the object is created each time it is requested from the DI container. This is how it is used in this project - on each click on Products or Orders button the form is created.


    After MainForm is initialized (and service provider is passed) it can do its job:
    C# Code:
    1. public partial class MainForm : Form
    2. {
    3.     private IServiceProvider _serviceProvider;
    4.  
    5.     public MainForm(IServiceProvider serviceProvider)
    6.     {
    7.         InitializeComponent();
    8.         _serviceProvider = serviceProvider;
    9.     }
    10.  
    11.     private void ProductsBtn_Click(object sender, EventArgs e)
    12.     {
    13.         using var frm = (ProductsForm)_serviceProvider.GetService(typeof(ProductsForm))!;
    14.         frm.ShowDialog();
    15.     }
    16.  
    17.     private void OrdersBtn_Click(object sender, EventArgs e)
    18.     {
    19.         using var frm = (OrdersForm)_serviceProvider.GetService(typeof(OrdersForm))!;
    20.         frm.ShowDialog();
    21.     }
    22. }
    Here can be seen how ProductsForm and OrderForms are requested from the service provider. MainForm does not know anything how they should be initialized via constructor parameters. This is job of the Dependency Injection container.

    But first to show how ProductsForm is defined:
    C# Code:
    1. public partial class ProductsForm : Form
    2. {
    3.     private IProductsReader _reader;
    4.     private IProductsWriter _writer;
    5.  
    6.     public ProductsForm(IProductsReader reader, IProductsWriter writer)
    7.     {
    8.         InitializeComponent();
    9.  
    10.         _reader = reader;
    11.         _writer = writer;
    12.     }
    13. }

    And OrderForms:
    C# Code:
    1. public partial class OrdersForm : Form
    2. {
    3.     private IOrdersReader _reader;
    4.     private IOrdersWriter _writer;
    5.  
    6.     public OrdersForm(IOrdersReader reader, IOrdersWriter writer)
    7.     {
    8.         InitializeComponent();
    9.  
    10.         _reader = reader;
    11.         _writer = writer;
    12.     }
    13. }

    If we don't use DI, MainForm should know (receive by initializing routine from Main sub) about all those IProductsReader, IProductsWriter, IOrdersReader, IOrdersWriter and MainForm constructor will be something like:
    Code:
    public MainForm(IProductsReader reader, IProductsWriter writer, IOrdersReader reader, IOrdersWriter writer)
    {
    ... save all those params to module vars ...
    }
    And code to show ProductsForm for example:
    C# Code:
    1. private void ProductsBtn_Click(object sender, EventArgs e)
    2. {
    3.     using var frm = new ProductsForm(_productsReader, _productsWriter);
    4.     frm.ShowDialog();
    5. }

    Imagine big application with hundreds of child forms, thousands of constructor injected parameters. Or split that big project solution into multiple smaller projects - each managing own domain (products, orders, users, accounts, etc.). MainForm (and the project it belongs to) should contain all those dependencies injected. Well, these could be put inside separate class so only one object will be passed to MainForm. But the project where MainForm resides will have references to all modules (products models, data management, UI - winforms, controllers, other logic).

    And imagine to add new parameter(s) to form, e.g. OrdersForm to become:
    C# Code:
    1. public OrdersForm(IOrdersReader reader, IOrdersWriter writer, IUsersReader usersReader, INotfier notifier)
    2. ...

    The developer should find all places where OrdersForm is created and update how it is initialized. And maybe add these missing objects (IUsersReader and INotifier) to the caller object, and its caller, and its caller...

    Hope this long description is more clear about dependency injection can be used in WinForms and other type of projects.
    Last edited by peterst; Jun 12th, 2022 at 05:43 PM. Reason: bugfixes in text and code :)

  5. #5
    Hyperactive Member
    Join Date
    Jun 2019
    Posts
    474

    Re: Windows forms dependency injection

    For those interested of my lame implementation of Orders reader/writer class, here it is:
    C# Code:
    1. public class OrdersReaderWriterInMemory : IOrdersReader, IOrdersWriter
    2. {
    3.     private Dictionary<string, Order> _orders = new();
    4.  
    5.     public Order? GetOrder(string orderNumber)
    6.     {
    7.         if (_orders.TryGetValue(orderNumber, out var order))
    8.             return order;
    9.        
    10.         return null;
    11.     }
    12.  
    13.     public IEnumerable<Order> GetOrders()
    14.     {
    15.         return _orders.Values.ToList();
    16.     }
    17.  
    18.     public void AddOrder(Order order)
    19.     {
    20.         _orders.Add(order.OrderNumber, order);
    21.     }
    22.  
    23.     public void UpdateOrder(Order order)
    24.     {
    25.         _orders[order.OrderNumber] = order;
    26.     }
    27.  
    28.     public void RemoveOrder(Order order)
    29.     {
    30.         _orders.Remove(order.OrderNumber);
    31.     }
    32. }
    Last edited by peterst; Jun 12th, 2022 at 03:10 PM.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width