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 155 forks source link

Does not work at VS design-time using recommended MVVM practices #173

Closed will-vertigo closed 8 years ago

will-vertigo commented 8 years ago

Say I'm using MvvmLight, which is a Model-View-ViewModel framework commonly used by C#/XAML/WPF/Win10 apps. I can use MvvmLight's built-in "SimpleIoc" container at both run-time and design-time. If I replace SimpleIoc with SimpleInjector, then my app runs perfectly at run-time, but fails at design-time in the VS designer.

FYI, I typically create an "AppServices" class which builds up my IOC container in a static constructor and exposes my services and view models as instance properties. This class gets created as an application resource, so it's accessible by my views at design-time.

using My.Services;
using My.ViewModels;
using SimpleInjector;

namespace My
{
    public sealed class AppServices
    {
        private static readonly Container container = new Container();

        static AppServices()
        {
            // Register services
            if (DesignMode.DesignModeEnabled)
            {
                container.Register<IDataService, DesignDataService>(Lifestyle.Singleton);
            }
            else
            {
                container.Register<IDataService, DataService>(Lifestyle.Singleton);
            }
        }

        public static AppServices Current
        {
            get { return (AppServices)Application.Current.Resources["AppServices"]; }
        }

        public IDataService DataService
        {
            get { return container.GetInstance<IDataService>(); }
        }

        public HomePageModel HomePageModel
        {
            get { return container.GetInstance<HomePageModel>(); }
        }
    }
}

The AppServices class gets created in App.xaml:

<Application
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:my="using:My"
    x:Class="My.App">

    <Application.Resources>
        <ResourceDictionary>
            <my:AppServices x:Key="AppServices"/>

I can then access my view models from my view (e.g. HomePage.xaml):

<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    x:Class="My.Views.HomePage"
    DataContext="{Binding HomePageModel, Source={StaticResource AppServices}, Mode=OneTime}">
will-vertigo commented 8 years ago

I verified that Autofac works as a replacement for SimpleIoc, so there is definitely an issue with SimpleInjector.

dotnetjunkie commented 8 years ago

Can you describe in more detail how it fails at design time. Please post the exception and stack trace.

will-vertigo commented 8 years ago

Since this is design-time I cannot debug and there is no human-readable exception or stack trace to give other than the AppServices line in App.xaml (shown above) has a red squiggle underneath and if you hover it says something about a failed HCOMResult. The views open up blank in the designer . My guess is that the container fails to build in the static ctor for some reason.

It is worth mentioning that I'm using the very latest builds of Windows 10 and VS 2015.

TheBigRic commented 8 years ago

It is also possible with SimpleInjector. I just tested this using VS2015 on Windows 10 in a WPF application. Although I did not use MVVM light, it shouldn't make any difference. Not completely sure why you got this to work with other IOC containers and not with SimpleInjector, because I think I didn't do anything out of the ordinary to get this to work.

It is offcourse a POC project, so the setup is pretty simple. The code...

First of all, start the application from Main to get rid of all WPF magic:

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

            RunApplication(container);
        }

        private static void RunApplication(Container container)
        {
            try
            {
                var app = new App();
                var mainWindow = container.GetInstance<MainWindow>();

                app.Run(mainWindow);
            }
            catch (Exception ex)
            {
                // log message or something
                throw;
            }
        }

        public static Container Bootstrap(bool isInDesignMode)
        {
            var container = new Container();

            if (isInDesignMode)
            {
                container.RegisterSingleton<IUserRepository, DesignUserRepository>();
            }
            else
            {
                container.RegisterSingleton<IUserRepository, SqlUserRepository>();
            }

            container.Register<MainWindow>();
            container.Register<MainWindowViewModel>();

            container.Verify();

            return container;
        }
    }

Notice that I'm telling the Bootstrapper class if it needs to register dependencies for DesignMode or not. In the normal flow is should be false offcourse.

Secondly I added a ViewModelLocator class which is responsible for resolving the ViewModels at designtime.

public class ViewModelLocator
{
    private Container container;

    public ViewModelLocator()
    {
        this.container = Program.Bootstrap(isInDesignMode: true);
    }

    public MainWindowViewModel MainWindowViewModel
    {
        get { return this.container.GetInstance<MainWindowViewModel>(); }
    }
}

In this case I create the container with dependencies needed for usage at DesignTime by supplying true to the isInDesignMode. The ViewModelLocator is created in the app.xaml by added it as a resource:

<Application.Resources>
     <local:ViewModelLocator x:Key="ViewModelLocator"/>
</Application.Resources>

You can now bind to this instance properties for the DesignTimeDataContext as follows:

<Window x:Class="SIDesignTimeDataContextInWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:SIDesignTimeDataContextInWpf"
        mc:Ignorable="d"
        d:DataContext="{Binding MainWindowViewModel, Source={StaticResource ViewModelLocator}}"
        >
    <Grid>
        <TextBox Text="{Binding Path=User, Mode=OneWay}" />
    </Grid>
</Window>

For the sake of completeness, the implementations of the MainWindow code behind, MainWindowViewModel and DesignUserRepository.

public partial class MainWindow : Window
{
    public MainWindow(MainWindowViewModel viewModel)
    {
        InitializeComponent();

        this.DataContext = viewModel;
    }
}

public class MainWindowViewModel
{
    private IUserRepository repository;

    public MainWindowViewModel(IUserRepository repository)
    {
        this.repository = repository;
    }

    public string User
    {
        get { return this.repository.UserName; }
    }
}

internal class DesignUserRepository : IUserRepository
{
    public string UserName { get { return "DesignTimeUser is here"; } }
}

But a word of advice: First off all, the ViewModelLocator class will grow rapidly. Or you need to find a way to do this in smarter way using an IEnumerable or Dictionary.

Secondly, instead of using the container to build your mocked ViewModels, just make these ViewModels plain and simple and implement only the properties which need to be bound. These implementations would be typically very small considering that creating a SOLID design which would lead to small ViewModels. Such a viewmodel can be bound to the view far easier at designtime using a DesignInstance like this:

d:DataContext="{d:DesignInstance local:YourMockedViewModel, IsDesignTimeCreatable=True}"

Last but not least. You can leave the design time support all together and since your already using a MVVM toolkit to bind your controls using Convention Over Configuration the only thing that is missing is writing a unit test which checks if all properties are actually in the viewmodel and thus can bind at runtme. This will provide a great feeling of flexibility, speed of development and still ensures all binding will be there at runtime. How the view will look at runtime is missing in this setup, but from my experience it is hardly ever possibly to create testdata that will actually mimic the data you get to show in production.

will-vertigo commented 8 years ago

That is all well and good and SimpleInjector may work fine in WPF, but we are essentially doing the same thing as far as building up the container, however the original problem still remains.

Maybe my post was not clear that this is specifically a problem with Windows 10 apps (and more than likely Win 8/8.1). As you know these apps are quite different from WPF.

TheBigRic commented 8 years ago

Indeed that was unclear.

I never used universal apps, so I'm not an expert at all at this, but I just tested this with the exact same setup and it works just the same!

Only adjustment made is that I'm not using the void main as entry point as it seems this is impossible for universal development.

And for some reason we (us developers) are back to square one because the page behind does not support constructor arguments. It seems there is no way to let an IoC container create the page for you using the promoted Frame.Navigate() method. But this is lack of experience on my part, you probably use MVVM light to set the datacontext and let MVVM call back into your container to create only the viewmodel.

Conclusion I have a lot to learn about Universal Apps AND: the same setup as above works also in Universal Apps....!

Did you follow my exact setup?

TheBigRic commented 8 years ago

@will-vertigo Did you have any luck in fixing this issue. After rereading I found 2 major differences in your setup and mine:

  1. You use a static class. This won't affect anything probably, but it can be an instance class as the App.Resources will nevertheless create an instance, even at design time
  2. Your using the DataContext property instead of the DesignTimeDataContext. Using the DataContext at designtime is not supported IMO.

So I think the main difference is that MVVM light does some magic under the covers to redirect the DataContext to the DesignTimeDataContext when designing.