Search code examples
c#dependency-injectioninversion-of-control

How to pass data to injected services


I want to code a small WinForms or WPF program that controls that only one person/computer a time uses a specific shared file through a network share. This file needs to be copied locally when working and copied back to the network share when the work is finished.

I want to create a small WPF application with only one button to flag the file as locked, copy it locally and open the application associated to that file extension. When this application is closed, the file is copied back to the network share and the lock is released. Each computer will use this application to access the file, so it should never be two computers editing the same file at the same time.

The application has to have some kind of configuration file with the path for local and remote folders, the name of the file and the path of the application to open that file. To ease setting up the application it will be done with a SettingsWindow.

I am trying to do it with IoC and some kind of lightweight DI container (i.e. SimpleInjector), but I am having some questions about how to do it properly:

Program.cs

static class Program
{
    [STAThread]
    static void Main()
    {
        var container = Bootstrap();

        // Any additional other configuration, e.g. of your desired MVVM toolkit.

        RunApplication(container);
    }

    private static Container Bootstrap()
    {
        // Create the container as usual.
        var container = new Container();

        // Register your types, for instance:
        container.Register<ILauncher, Launcher>();
        container.Register<IFileSyncService, FileSyncronizer>();
        container.Register<IAppRunnerService, AppRunnerService>();
        container.Register<ILockService, LockService>();

        // Register your windows and view models:
        container.Register<MainWindow>();
        container.Register<ConfigurationWindow>();

        container.Verify();

        return container;
    }

    private static void RunApplication(Container container)
    {
        try
        {
            var app = new App();
            var mainWindow = container.GetInstance<MainWindow>();
            app.Run(mainWindow);
        }
        catch (Exception ex)
        {
            //Log the exception and exit
        }
    }
}

MainWindow

I modified the constructor to receive an interface ILauncher that will be resolved by the DI container.

    public partial class MainWindow : Window
    {
        public MainWindow(ILauncher launcher)
        {
            InitializeComponent();
        }
…
}

ILauncher interface

public interface ILauncher
{
    void Run();
}

Launcher implementation

The launcher implementation will take care of coordinating every task needed to launch the application and edit the file. This consists of: checking and acquiring the lock, synchronizing the file, executing and monitoring that the application has been closed. To follow the Single Responsibility Principle (SRP) this is done through some services injected:

public class Launcher : ILauncher
{
    public Launcher(
        ILockService lockService,
        IAppRunnerService appRunnerService,
        IFileSyncService fileSyncService
        )
    {
        LockService = lockService;
        AppRunnerService = appRunnerService;
        FileSyncService = fileSyncService;
    }

    public ILockService LockService { get; }
    public IAppRunnerService AppRunnerService { get; }
    public IFileSyncService FileSyncService { get; }

    public void Run()
    {
        //TODO check lock + adquire lock
        //TODO Sinchronize file
        //TODO Subscribe to the IAppRunnerService.OnStop event 
        //TODO Start app through IAppRunnerService.Run method
    }
}

My questions:

  1. Creating new instances with the “new” keyword or with manual calls to the container inside classes or windows is a bad practice, so the entry point of the desktop application (usually some kind of a main window) should ask (via the constructor) for everything. It doesn’t seem a proper way when the application grows. Am I right? What is the solution?

  2. In my application, every service needs some kind of runtime data (the executable file path, the local or remote directory…). The container instantiates them early in the application execution, even before this data is known. How can the services receive this data? (Please note that data could be modified later by the SettingsWindow).

Sorry for the extension of this post but I wanted to make my problem and the context clear.


Solution

    1. As you've said, you have to ask for some services in the entry point, but what do you mean ask for everything? Your entry point should have only the services it directly uses speciifed as ctor parameters. If your application grows and you suddenly have a dozen services called directly by your entry point - that could indicate that some of the services could be consolidated into a bigger packet. That's opinion-based territory though, as long as you consider it maintainable it's okay.

    2. Many possible solutions here, one would be an ISettingsManager allowing for realtime access to the settings across any services that take it as a dependency.