canton7 / Stylet

A very lightweight but powerful ViewModel-First MVVM framework for WPF for .NET Framework and .NET Core, inspired by Caliburn.Micro.
MIT License
988 stars 143 forks source link

[Question] Constructor-Injection with Variable Key Possible? #135

Closed uyaem closed 4 years ago

uyaem commented 4 years ago

Hello,

I'm relatively new to proper MVVM and greatly enjoyed this framework so far, the documentation is excellent in getting you started in no time. I've come across one point, where I'm not sure if it's not possible or if I understand the concept wrong. Here goes...

My application has a dialog MyDialog who displays items in a list on the left side. Selecting an item in the list opens a detail editor on the right side of the dialog. I want to empower the dialog to work with different types of data in order to reuse it throughout the application and would therefore like to inject functionality (a data source) into the dialog. I'm using an interface to abstract that behaviour:

public interface IMyDataSource { ... }
public class SourceOne : IMyDataSource { ... }
public class SourceTwo : IMyDataSource { ... }

The constructor of the dialog's ViewModel looks like this.

public MyDialogViewModel(IWindowManager windowManager, IMyDataSource source) { ... }

Instances of the dialog, with different data sources, would be created by the parent window in different Actions:

public class MainViewModel : Screen
{
    ...
    public void EditSourceOneAction()
    {
        // ???
    }

    public void EditSourceTwoAction()
    {
        // ???
    }
}

How can the dialog's parent (the "Main" window) get Stylet to instantiate appropriately?

I've tried an abstract factory and registering that with the IoC container as follows, but it didn't work, I guess the key does not get (or cannot be) propagated:

public interface IMyDialogFactory
{
    MyDialogViewModel CreateDialog(string key);
}

public class Strappy : Bootstrapper<MainViewModel>
{
    protected override void ConfigureIoC(IStyletIoCBuilder builder)
    {
        ... 
        builder.Bind<IMyDialogFactory>().ToAbstractFactory();
        builder.Bind<IMyDataSource>().To<SourceOne>().WithKey("one");
        builder.Bind<IMyDataSource>().To<SourceTwo>().WithKey("two");
    }
}

public class MyViewModel : Screen
{
    [Inject]
    public IMyDialogFactory DF { get; set; }

    public void EditSourceOneAction()
    {
        this.DF.CreateDialog("one");
    }
}

My workaround (or maybe this is the solution?) is to implement a factory myself, but it just doesn't feel as clean/loosely coupled because now I have ViewModels that are not instantiated through the IoC container and also I have to touch the factory for every new implementation of IMyDataSource that the dialog should support:

public MyRealDialogFactory()
{
    private IWindowManager wman;

    [Inject]
    public MyRealDialogFactory(IWindowManager wman)
    {
        this.wman = wman;
    }

    public MyDialogViewModel CreateWithSourceOne()
    {
        return new MyDialogViewModel(this.wman, new SourceOne());
    }

    public MyDialogViewModel CreateWithSourceTwo()
    {
        return new MyDialogViewModel(this.wman, new SourceTwo());
    }
}

Is what I want just not achievable, or not intended by StyletIoC, or am I missing something?

canton7 commented 4 years ago

The built-in ioc container is pretty simple, and doesn't support what you want out-of-the-box. You could use a more powerful ioc container, but my person preference would be to use hand-written factories (I don't like too much magic in my ioc container setup).

In more complex applications where types need to be created with factories, I tend to end up with a single factory per type that needs one. Where lots of decoupling is a priority, those factories are hidden behind interfaces and registered with the ioc container, and themselves only depend on abstractions: the only concrete type they know about is the type they're responsible for instantiating.

For your example, I might have MyRealDialogFactory's constructor take either (SourceOne sourceOne, SourceTwo sourceTwo) or (IDataSource sourceOne, IDataSource sourceTwo) or (Func<SourceOne> sourceOneFactory, Func<SourceTwo> sourceTwoFactory) or (Func<IDataSource> sourceOneFactory, Func<IDataSource> sourceTwoFactory), depending on needs (whether to depend on concrete types or abstractions, and whether you want factories or instances).

Does that help?

uyaem commented 4 years ago

Yes it does, thank you very much. I'll stick with my factory implementation then, which is basically your suggestion of "one factory per type".

I, too, don't like too much IoC setup magic - in fact, I've stayed completely clear of IoC containers for a long time because I felt that it can easily get out of hand. :)

canton7 commented 4 years ago

Yeah, my approach there is normally to plan everything as if there wasn't an ioc container, then use the ioc container to remove some of the repetitive boilerplate.