simpleinjector / SimpleInjector

An easy, flexible, and fast Dependency Injection library that promotes best practice to steer developers towards the pit of success.
https://simpleinjector.org
MIT License
1.21k stars 154 forks source link

Get 2 Transient instances in a Singelton? #233

Closed AnderssonPeter closed 8 years ago

AnderssonPeter commented 8 years ago

I'm using WPF and the MVVM pattern.

When I'm executing anything that's long running i want to display a loading indicator so i created a BusyViewModel for that. Now i want to get a new instance of this for each long running task, so lets say I have 2 one for Saving and one for Loading.

mContainer.Register(Of BusyViewMode)(Lifestyle.Transient)
mContainer.RegisterSingleton(Of ShellViewModel)()
mContainer.Verify()

And the constructor for ShellViewModel has the following parameters BusyViewMode savingBusyViewMode, BusyViewMode loadingBusyViewMode.

When I call verify I get a exception that tells me my Lifestyles are mismatching, so my second attempt was to take a Func<BusyViewMode> as parameter to the constructor but that didn't work either.

So I my idea totally stupid or how would I solve this?

TheBigRic commented 8 years ago

Your attempt to use a Func(Of ...) should work. I just tried, but this registrations pass verification.

Public Class BusyViewMode

End Class

Public Class ShellViewModel
    Private ReadOnly busyViewModeFactory As Func(Of BusyViewMode)

    Public Sub New(busyViewModeFactory As Func(Of BusyViewMode))
        Me.busyViewModeFactory = busyViewModeFactory 
    End Sub

    Private ReadOnly Property busyViewMode As BusyViewMode
        Get
            Return Me.busyViewModeFactory.Invoke()
        End Get
    End Property
End Class

Public Module CompositionRoot

    Public Sub Initialize()

        Dim container = New Container()

        container.RegisterSingleton(Of ShellViewModel)()
        container.Register(Of BusyViewMode)()
        container.RegisterSingleton(Of Func(Of BusyViewMode)) _
                          (Function() container.GetInstance(Of BusyViewMode)())

        container.Verify()

    End Sub

End Module

Notice that however this will pass verification and will return a new BusyViewMode every time you make a call to the private property, you will have to use a background task (using the old threading model or the async-await) in order to successfully display the BusyIndicator. If you will do the long running task in the UI thread, WPF will not update the UI until the thread is free, which is at the end of the method in most cases.

TheBigRic commented 8 years ago

After rereading your question I saw I missed a part. You want also to inject the same type twice in the constructor. But the question is, will each type have a separate view?

If that is the case, how would you think this will work in practice? I would recommend to inject a single factory which binds to a single view in which you can differentiate by passing some object to a method in 'BusyViewMode'. 'BusyViewMode' can use this object to change the text, or other parts of the UI by using default MVVM bindings

AnderssonPeter commented 8 years ago

Yes each view model should get their own view i haven't managed to test it yet but that's the idea.

But using a single one might also work I just have to redesign it so it can have multiplie texts.

TheBigRic commented 8 years ago

Yes each view model should get their own view i haven't managed to test it yet but that's the idea.

This would result in making decisions in your ShellViewModel which injected BusyViewMode should bind to which view. With that your violating almost all 5 SOLID principles.

Besides that how do you think your composition root (if done by a container, like Simple Injector, but also using Pure DI) should decide which BusyViewMode should be injected in which parameter, beside looking at the parameter naming...? If these have distinct functionality they should be different abstractions..

AnderssonPeter commented 8 years ago

@TheBigRic every parameter should get its own instance, so if we have ShellViewModel with Parameter a, and FileSaveViewModel with parameter a those should not be the same instance.

But it seems like I'm missing the whole point..

But one simple question Is it normal to mark the ShellViewModel as Singelton and if your root element is a Singelton how would you ever get a Transient.

Are there any example projects that follow best practices that I could learn from?

TheBigRic commented 8 years ago

Sorry, I totally forgot about this question. I hope an answer is still usefull.

In most of my applications I also use an application root viewmodel like ShellViewModel or MainViewModel. And whether this is registered as Singleton or not, it will behave as Singleton because when this will close, the application closes.

So how would you then show other views which need to be newed every time they open, in other words transients?

Your root object, the ShellViewModel in this case should be part of the composition root and should have no other functionalities than opening other view(model)s. In the best scenario this root viewmodel doesn't have a view at all, other than an empty window which only acts as a container for the views of the transient viewmodels which contain the actual application interface.

The ShellViewModel itself can then callback in the container to get a fresh copy of transient viewmodel and bind it to a corresponding view or it delegates this to some dispatch class with a single method which takes the viewmodel type as a parameter and has the single responsibility to create the viewmodel (by calling again back into the container) and find the corresponding view of that viewmodel, normally by using some MVVM tool like Caliburn Micro.

So this could look like:

public class ShellViewModel
{
    private readonly IViewModelHandler viewModelHandler;

    public ShellViewModel(IViewModelHandler viewModelHandler)
    {
        this.viewModelHandler = viewModelHandler;
    }

    public void ShowCustomers()
    {
        this.viewModelHandler.HandleViewModel<CustomerViewModel>();
    }
}

public interface IViewModelHandler
{
    void HandleViewModel<TViewModel>() where TViewModel : class;
}

public class MvvmViewModelHandler : IViewModelHandler
{
    private readonly Container container;
    private readonly IWindowManager windowManager;

    public MvvmViewModelHandler(Container container, IWindowManager windowManager)
    {
        this.container = container;
        this.windowManager = windowManager;
    }

    public void HandleViewModel<TViewModel>() where TViewModel : class
    {
        var viewModel = this.container.GetInstance<TViewModel>();
        // windowManager is defined in Caliburn, caliburn will find the
        // correct view, create and initialize a window class from it
        // and bind the viewmodel to the datacontext of this window
        // amongst several other things
        this.windowManager.ShowDialog(viewModel);
    }
}

If don't use a Mvvm toolkit (which I can recommend!) you can also implement your own implementation of a very simple ConventionViewHandler like this very simplied example:

public class CustomByConventionViewModelHandler : IViewModelHandler
{
    private readonly Container container;

    public CustomByConventionViewModelHandler(Container container)
    {
        this.container = container;
    }

    public void HandleViewModel<TViewModel>() where TViewModel : class
    {
        var viewType = this.FindViewType<TViewModel>();
        var view = (Window) this.container.GetInstance(viewType);
        var viewModel = this.container.GetInstance<TViewModel>();
        view.DataContext = viewModel;
        view.ShowDialog();
    }

   public Type FindViewType<TViewModel>()
    {
        var viewModelName = typeof(TViewModel).FullName;
        var viewName = this.GetViewName(viewModelName);

        var viewType = this.GetViewType(viewName);
        if (viewType == null)
        {
            throw new InvalidOperationException(
                                    $"A view for {viewModelName} could not be located!");
        }
        return viewType;
    }

    private Type GetViewType(string viewName)
    {
        return (
            from type in Assembly.GetExecutingAssembly().GetExportedTypes()
            where type.FullName == viewName
            select type).SingleOrDefault();
    }

    private string GetViewName(string viewModelName)
    {
        if (!viewModelName.EndsWith("viewmodel", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException(
                                   "By convention viewmodels should be named xxxViewModel");
        }

        return viewModelName.Substring(0, viewModelName.Length - 5);
    }
}