Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using abstraction and dependency injection, what if implementation-specific details need to be configurable in the UI?

I have an application that loads a list of client/matter numbers from an input file and displays them in a UI. These numbers are simple zero-padded numerical strings, like "02240/00106". Here is the ClientMatter class:

public class ClientMatter
{
    public string ClientNumber { get; set; }
    public string MatterNumber { get; set; }
}

I'm using MVVM, and it uses dependency injection with the composition root contained in the UI. There is an IMatterListLoader service interface where implementations represent mechanisms for loading the lists from different file types. For simplicity, let's say that only one implementation is used with the application, i.e. the application doesn't support more than one file type at present.

public interface IMatterListLoader
{
    IReadOnlyCollection<string> MatterListFileExtensions { get; }
    IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile);
}

Let's say in my initial version, I've chosen an MS Excel implementation to load the list of matters, like this:

enter image description here

I'd like to allow the user to configure at runtime the row and column numbers where the list starts, so the view might look like this:

enter image description here

And here's the MS Excel implementation of IMatterListLoader:

public sealed class ExcelMatterListLoader : IMatterListLoader
{
    public uint StartRowNum { get; set; }
    public uint StartColNum { get; set; }
    public IReadOnlyCollection<string> MatterListFileExtensions { get; set; }

    public IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile)
    {
        // load using StartRowNum and StartColNum
    }
}

The row and column numbers are an implementation detail specific to MS Excel implementations, and the view model doesn't know about it. Nevertheless, MVVM dictates that I control view properties in the view model, so if I were to do that, it would be like this:

public sealed class MainViewModel
{
    public string InputFilePath { get; set; }

    // These two properties really don't belong
    // here because they're implementation details
    // specific to an MS Excel implementation of IMatterListLoader.
    public uint StartRowNum { get; set; }
    public uint StartColNum { get; set; }

    public ICommandExecutor LoadClientMatterListCommand { get; }

    public MainViewModel(IMatterListLoader matterListLoader)
    {
        // blah blah
    }
}

Just for comparison, here's an ASCII text file based implementation that I might consider for the next version of the application:

enter image description here

public sealed class TextFileMatterListLoader : IMatterListLoader
{
    public bool HasHeaderLine { get; set; }
    public IReadOnlyCollection<string> MatterListFileExtensions { get; set; }

    public IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile)
    {
        // load tab-delimited client/matters from each line
        // optionally skipping the header line.
    }
}

Now I don't have the row and column numbers that the MS Excel implementation needed, but I have a Boolean flag indicating whether the client/matter numbers start on the first row (i.e. no header row) or start on the second row (i.e. with a header row).

I believe the view model should be unaware of the change between implementations of IMatterListLoader. How do I let the view model do its job controlling presentation concerns, but still keep certain implementation details unknown to it?


Here's the dependency diagram:

enter image description here

like image 267
rory.ap Avatar asked Aug 29 '18 14:08

rory.ap


1 Answers

You'd need a seperate viewmodel for each type of file you intend to load.

Each viewmodel does the setup for its particular loader.

These viewmodels can then be passed in as dependencies to the main viewmodel, which calls load on each viewmodel when needed;

public interface ILoaderViewModel
{
    IReadOnlyCollection<ClientMatter> Load();
}

public class ExcelMatterListLoaderViewModel : ILoaderViewModel
{
    private readonly ExcelMatterListLoader loader;

    public string InputFilePath { get; set; }

    public uint StartRowNum { get; set; }

    public uint StartColNum { get; set; }

    public ExcelMatterListLoaderViewModel(ExcelMatterListLoader loader)
    {
        this.loader = loader;
    }

    IReadOnlyCollection<ClientMatter> Load()
    {
        // Stuff

        loader.Load(fromFile);
    }
}

public sealed class MainViewModel
{
    private ExcelMatterListLoaderViewModel matterListLoaderViewModel;

    public ObservableCollection<ClientMatter> ClientMatters
        = new ObservableCollection<ClientMatter>();

    public MainViewModel(ExcelMatterListLoaderViewModel matterListLoaderViewModel)
    {
        this.matterListLoaderViewModel = matterListLoaderViewModel;
    }

    public void LoadCommand()
    {
        var clientMatters = matterListLoaderViewModel.Load();

        foreach (var matter in clientMatters)
        {
            ClientMatters.Add(matter)
        }
    }
}

As you add more types to the application, you'd create new view models and add those as dependencies.

like image 101
bic Avatar answered Sep 27 '22 16:09

bic