Dependency injection is a technique for achieving Inversion of Control (IoC) between classes and their dependencies. Dependency injection in .NET is a built-in part of the framework.
Dependency injection achieves Inversion of Control through:
- The use of an interface or base class to abstract the dependency implementation.
- Registration of the dependency in a service container. .NET provides a built-in service container, IServiceProvider. Services are typically registered at the app’s start-up and appended to an IServiceCollection. Once all services are added, you use BuildServiceProvider to create the service container.
- Injection of the service into the constructor of the class where it’s used. The framework takes on the responsibility of creating an instance of the dependency and disposing of it when it’s no longer needed.
Sample Solution
Let’s create a sample solution that contains three projects:
- Windows Forms project: This project is a main application. It contains a Windows Form (Form1). There are two buttons on the form. Clicking the buttons will load the user profile from the injected profile service and populate the screen elements (First Name, Last Name, Email Address and Home Address).
- Class library project: This project contains an Interface for profile service and the implement of this interface. We will use dependency injection to inject this service into Form1 class.
- Unit test project: This project contains unit testing for the Windows Forms (Form1).
Profile Service
Interface
public interface IProfileService
{
Profile GetProfile(int profileID);
}
Implementation
private List<Profile> _profiles = new List<Profile>()
{
new Profile() { FirstName = "Foo", LastName = "Profile1", Email = "fooprofiile1@yahoo.com", HomeAddress = "1 King Street West, Toronto, ON", ProfileID = 1 },
new Profile() { FirstName = "Bar", LastName = "Profile2", Email = "barprofiile2@gmail.com", HomeAddress = "1 Hollywood Avenue, Los Angeles, CA", ProfileID = 2 },
};
public Profile GetProfile(int profileID)
{
return _profiles.FirstOrDefault(p => p.ProfileID == profileID);
}
Windows Forms App
Following code snippet is the Form1 class. Profile service is injected through Form1 constructor.
public partial class Form1 : Form
{
private IProfileService _profileService;
public Form1(IProfileService profileService)
{
_profileService = profileService;
InitializeComponent();
}
private void btnLoadProfile1_Click(object sender, EventArgs e)
{
Profile profile = _profileService.GetProfile(1);
AssignProfile(profile);
}
private void btnLoadProfile2_Click(object sender, EventArgs e)
{
Profile profile = _profileService.GetProfile(2);
AssignProfile(profile);
}
private void AssignProfile(Profile profile)
{
if (profile != null)
{
lblFirstNameValue.Text = profile.FirstName;
lblLastNameValue.Text = profile.LastName;
lblHomeAddressValue.Text = profile.HomeAddress;
lblEmailAddressValue.Text = profile.Email;
}
}
}
Since project service is injected into Form1 through contructor, we should avoid creating Form1 instance directly. We should let the dependency injection framework to handle the creation of the Form1 instance.
In Program.cs file, we need to initiate and register dependency injections.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ProfileService;
namespace UnitTestWithDIWinForm
{
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
var host = CreateHostBuilder().Build();
Application.Run(host.Services.GetRequiredService<Form1>());
}
static IHostBuilder CreateHostBuilder()
{
return Host.CreateDefaultBuilder()
.ConfigureServices((context, services) => {
services.AddSingleton<IProfileService, ProfileService.ProfileService>();
services.AddTransient<Form1>();
});
}
}
}
First, you need to install Microsoft.Extensions.Hosting package. After that, you can use Host.CreateDefaultBuilder() to create a hostbuilder and register dependency injection in its ConfigureServices() method.
When you need to create a Form1 instance, instead of using new
keyword to create it directly, you can call ServiceProvider.GetRequiredService<Form1>()
to let dependency injection framework handle the creation.
Unit Testing Project
In this unit test project, we need the similar code to above to create a hostbuilder and register dependency injection in its ConfigureServices() method.
However, since this is a unit testing, we should not rely on the real profile service. Therefore, we need to mock it using Moq.
Another thing we need to be aware of is most of the controls and event handlers in Windows Forms app are private. We need to use reflection to trigger the event handlers (in our case, button click event handlers) and to check the value of the controls (in our case, the Label text).
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NUnit.Framework.Interfaces;
using ProfileService;
using UnitTestWithDIWinForm;
using Moq;
using System.Reflection.Metadata.Ecma335;
using System.Windows.Forms;
using System.Reflection;
namespace TestWinFormsDI
{
public class Tests
{
private IHost _host;
[SetUp]
public void Setup()
{
_host = CreateHostBuilder().Build();
}
[Test]
public void Test1()
{
var form1 = _host.Services.GetRequiredService<Form1>();
InvokeNonPublicMethod(form1, "btnLoadProfile1_Click", this, EventArgs.Empty);
Assert.That(((Label)GetNonPublicField(form1, "lblFirstNameValue")).Text, Is.EqualTo("FooUnitTesting"));
InvokeNonPublicMethod(form1, "btnLoadProfile2_Click", this, EventArgs.Empty);
Assert.That(((Label)GetNonPublicField(form1, "lblFirstNameValue")).Text, Is.EqualTo("BarUnitTesting"));
}
private void InvokeNonPublicMethod(Form1 form1, string methodName, params object[] args)
{
MethodInfo method = form1.GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).FirstOrDefault(m => m.Name == methodName);
if (method != null)
{
method.Invoke(form1, args);
}
}
private object GetNonPublicField(Form1 form1, string fieldName)
{
FieldInfo field = form1.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance).FirstOrDefault(f => f.Name == fieldName);
return field?.GetValue(form1);
}
private object GetNonPublicProperty(Form1 form1, string propertyName)
{
PropertyInfo property = form1.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Instance).FirstOrDefault(p => p.Name == propertyName);
return property?.GetValue(form1);
}
private Mock CreateProfileServieMock()
{
var mock = new Mock<IProfileService>();
mock.Setup(s => s.GetProfile(1)).Returns(
new Profile()
{
FirstName = "FooUnitTesting",
LastName = "Profile1",
Email = "fooprofiile1@yahoo.com",
HomeAddress = "1 King Street West, Toronto, ON",
ProfileID = 1
});
mock.Setup(s => s.GetProfile(2)).Returns(
new Profile()
{
FirstName = "BarUnitTesting",
LastName = "Profile2",
Email = "barprofiile2@gmail.com",
HomeAddress = "1 Hollywood Avenue, Los Angeles, CA",
ProfileID = 2
});
return mock;
}
private IHostBuilder CreateHostBuilder()
{
var mock = CreateProfileServieMock();
return Host.CreateDefaultBuilder()
.ConfigureServices((context, services) => {
services.AddTransient(typeof(Form1));
services.AddSingleton(typeof(IProfileService), mock.Object);
});
}
}
}
Happy coding!