Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix dependency injection issue #339

Merged
merged 1 commit into from Aug 15, 2016
Merged

Fix dependency injection issue #339

merged 1 commit into from Aug 15, 2016

Conversation

ghost
Copy link

@ghost ghost commented Jun 13, 2016

I think it's a good idea to try to retrieve single instance when GetAllInstances returns empty collection.
See simpleinjector/SimpleInjector#251 (comment)

@nigel-sampson
Copy link
Contributor

Thanks for the pull request.

Can you clarify the issue that this pull request solves?

Currently the container will resolve single registrations when GetAllInstances is called so there shouldn't be any result from the latter call that wasn't returned by the former.

@ghost
Copy link
Author

ghost commented Jun 14, 2016

Some containers (like Simple Injector) do not create new instances implicitly when GetAllInstances is called and instead return empty collection. With this fix Caliburn.Micro will also try to get an instance via GetInstance

@TheBigRic
Copy link

I think the question here is, when you expect a single instance, which you do, hence this call:

var view = IoC.GetAllInstances(viewType).FirstOrDefault() as UIElement; 

why would you call GetAllInstances(). GetAllInstances is supposed to return multiple instances in every DI container I know. Some containers will return an IEnumerable<view> with a single item, others, like Simple Injector, will throw an exception if an 'IEnumerable' isn't registered explicitly and most containers will just return an empty list.

In other words I would recommend that the call is being changed to:

var view = IoC.GetInstance(viewType, null);

@nigel-sampson
Copy link
Contributor

I'm inclined to agree @TheBigRic , this code predates me and i can't think of a good reason why GetAllInstances is being called over GetInstance.

I'd like to switch this over, but depending on the container a developer is using this could be a breaking change and will be have to held off to a 4.0.0 release.

@ghost
Copy link
Author

ghost commented Jun 15, 2016

I think my solution can be used until 4.0.0 is released. It relies on current behavior, but tries to retrieve an instance the right way if GetAllInstances() returns empty collection. It is unlikely to break backward compatibility

@TheBigRic
Copy link

I agree with both of you. It is a breaking change an thus should be in a major version release. The fix in this pull request is very unlikely to break anybody's application I guess, although there are probably implementations depending on this exact implementation.

@nigel-sampson nigel-sampson merged commit ebf1b8d into Caliburn-Micro:master Aug 15, 2016
@nigel-sampson nigel-sampson added this to the v3.0.2 milestone Aug 15, 2016
@beachwalker
Copy link

beachwalker commented Dec 8, 2016

Hmm, it seems this might broke a lot more than just MEF. I used Autofac and this also throws an Exception because of trying to resolve an unregistered type now. Can be solved by:

protected override object GetInstance(Type service, string key)
{
     try
     {
           return string.IsNullOrWhiteSpace(key)
                      ? this.container.Resolve(service)
                      : this.container.ResolveNamed(key, service);
     }
     catch (ComponentNotRegisteredException) // this fixes the issue
     {
           return null;
     }
}

@nigel-sampson
Copy link
Contributor

Yeah, it would affect any container that throws an exception when unable to resolve a dependency (not sure of the full list here).

Apologies again.

@bluerobotch
Copy link

Same with Ninject. Checking for service type assignable to UIElement did the trick.

@laranjee
Copy link

laranjee commented Dec 15, 2016

This change also broke the Caliburn.Micro.Start that uses the SimpleContainer and all the apps that based their implementations on this one to integrate another container.

From Caliburn.Micro.Start 3.0.2, AppBootstrapper.cs

   protected override object GetInstance(Type service, string key) {
        var instance = container.GetInstance(service, key);
        if (instance != null)
            return instance;

        throw new InvalidOperationException("Could not locate any instances.");
    }

I've always assumed that if the service is not found it should throw, and from several implementations I've seen out there I'm not alone.

What should be the correct behavior, to throw or return null?

By the way, what is the point of calling IoC.GetInstance after the IoC.GetAllInstances returning an empty collection?

@ZoolWay
Copy link

ZoolWay commented Dec 20, 2016

Just run unexpectly into this issue when upgrading 3.0.1 to 3.0.2...

For SimpleContainer, this does the trick but is it right?

protected override object GetInstance(Type service, string key)
{
    var instance = container.GetInstance(service, key);
    if (instance != null)
         return instance;
    if (typeof(UIElement).IsAssignableFrom(service)) return null;
    throw new InvalidOperationException("Could not locate any instances.");
}

@nigel-sampson
Copy link
Contributor

@ZoolWay that's pretty much right, I'm debating reverting this one till 4.0.0 since it's become more of a breaking change than expected.

@wannabeuk
Copy link

Turns out this one caught me out as well. Moving towards using some external modules with views and viewmodels. Spent the entire day pulling my hair out trying to work out why it just wouldn't find any instances of my views anymore. Turns out at some point I absent mindedly upgraded to 3.0.2 (Because the new DLL used the latest available on NUGET, so i upgraded the main app to match) Thankfully the above posted solutions solved my problems.

@chrisa23
Copy link

chrisa23 commented Jan 4, 2017

This bit me as well and wasted hours of my time. I started a new project and used my normal bootstrapper code and then kept getting errors saying couldn't find views. Figured I was missing something stupid on my part and ended up putting Export attributes on my view's as a quick fix so I could keep working until I figured out what happened. Finally thought of downgrading the package and then after more digging found this pull request through the release notes. Painful... I would vote for reverting this to help others save their sanity...

@nigel-sampson
Copy link
Contributor

nigel-sampson commented Jan 9, 2017

I've reverted this out in 3.0.3, it shouldn't have been included in 3.0.2. It may be included in 4.0.0.

Apologies to all.

@godrose
Copy link

godrose commented Jan 9, 2017 via email

@nigel-sampson
Copy link
Contributor

It's specific to containers that throw exceptions on failure to resolve the dependency such as Autofac. SimpleContainer included in the framework wasn't affected.

@TheBigRic
Copy link

If I understand correctly 4.0 will resolve views solely based on .GetInstance? Which I still think of as the only sensible thing to do here.

Notice that while Simple Injector started with throwing exceptions instead of retuning null for instances which can't be created, almost every container out there is now doing this. And this is offcourse a pretty sane approach. We as developers want things to fail fast with clear exceptions and stack traces instead of tricky and hard to debug issues with ArgumentNullExceptions etc.

In the dev branch for 4.0.0 I still see the same code, which uses .GetAllInstances. This is an open issue or are there any compelling reasons to leave this as is? I'm happy to send in a PR.

@Eternal21
Copy link

It would be great for this to be fixed. I hate having to use those ugly workarounds in bootsrapper to get SimpleInjector (and it seems like plenty other IoC containers these days) working.

@nigel-sampson
Copy link
Contributor

After the hiccups when this first rolled out, I'm a little hesitant to do it again. Especially as you said more frameworks throwing on dependency resolution failure.

Are there many people out there using the container for view construction? I almost feel like I want to go the other way and remove it if it's not being used.

@TheBigRic
Copy link

TheBigRic commented Jun 13, 2017

Especially as you said more frameworks throwing on dependency resolution failure.

Why do you consider this a problem? If a view can't be created by a DI container, how would caliburn be able to create the view. So in the end there will be an exception anyway, unless you consider showing a default view with a single Textblock, label or whatever no exception?

If you as a programmer made a mistake, also known as a bug, shouldn't it be fair to throw an exception? I certainly want that!!

But apart from the exception part. Caliburn clearly expects a single view returned from the call to GetAllInstances because it calls .FirstOrDefault() on the returned collection. Therefore a call to GetInstance would be far easier to understand for every developer out there.

With SimpleInjector for exampling you have to make a pretty nasty workaround to let the container resolve a view because SimpleInjector seperates registrations for collections from other registrations
.

Again I'm happy to provide a PR with my solution and I'm open for everybody seeing problems in this approach.

@Eternal21
Copy link

Eternal21 commented Jun 13, 2017

So in the end there will be an exception anyway, unless you consider showing a default view with a single Textblock, label or whatever no exception?

And even if that is a problem, a backwards compatibility option could be added. SimpleInjector had something to this effect (before they decided throwing an exception is always the right way to proceed):
ContainerOptions.ResolveUnregisteredCollections Property

@nigel-sampson
Copy link
Contributor

Why do you consider this a problem?

Any time a developer wants to integrate as DI container that has this behavior there's additional work to be done that's not immediately apparent.

If a view can't be created by a DI container, how would Caliburn be able to create the view.

Most of the time (that I've seen) views aren't registered with the container, which means Caliburn falls back to Activator.CreateInstance which is fine because I have yet to see a good reason for dependency injection into the view.

Unless you consider showing a default view with a single Textblock, label or whatever no exception?

This is pretty much what happens.

In summary, I've yet to see anyone use dependency injection into views themselves and it feels like even trying to resolve the view out of the container adds a layer of complication that isn't required. Especially when in order to make it "correct" adds extra friction for developers who don't want it.

The extension point of ViewLocator.GetOrCreateViewType would let people add this behavior back if it was removed.

I'm happy to see a PR around a proposed solution though.

@TheBigRic
Copy link

TheBigRic commented Jun 15, 2017

In summary, I've yet to see anyone use dependency injection into views themselves and it feels like even trying to resolve the view out of the container adds a layer of complication that isn't required. Especially when in order to make it "correct" adds extra friction for developers who don't want it.

I think you definitively has seen more far more examples as I have, so you would know much better. However, this is a far greater breaking change than the one I'm suggesting.

The current design is IMO flawed in 2 ways:

  1. GetAllInstances is called, while a single instance is expected. And even though I understand why this was chosen in the past, the reasons for this are invalid at the moment because almost every DI container out there is becoming more strict, which is a good thing IMO. See below for a more extensive explanation on this.
  2. When a user in fact has dependencies on its view, probably because he is migrating towards MVVM, the current implementation uses Activator.CreateInstance and throws a hard to understand exception when this fails because the view actually has ctor parameters.

About DI container becoming more strict:
Since the beginning Simple Injector has been the most strict container out there. And some users hate this, while others see the benefits of this approach because it makes your applications fail fast
. And what I see is that containers who would return null in the past when a request was made for a missing registration, now throw an exception. And when you think about it, this is the most sensible thing to do! In the past I saw users writing endless guard clauses in the ctor and throw an ArgumentNullException anyway. The same yields for collections.

I'm not saying other DI maintainers are following Simple Injector, this move from failing silent or at a later point in time towards throwing exceptions I see in the complete industry. And this helps us all in creating better software. So I think users should become more and more accustomed to the fact that a library or framework could throw an exception. While the builders of these tools should invest in throwing exceptions with clear exception messages.

You'll see a PR in the next days.

@nigel-sampson
Copy link
Contributor

Definitely not going to argue the current design is flawed. especially for reason 1.

I haven't seen many DI containers inject null's when not resolving dependencies, they tend to return null if they can't resolve the entire tree.

My thought process is going something like this. There's two solutions we can head down:

  1. Have the framework resolve views via GetInstance, developers using a strict container will need to deal with this either by detecting view resolution and returning null or by registering their views with the container.
  2. Remove the resolution via GetInstance leaving it just using Activator.CreateInstance, developers who which to inject dependencies into their view would need to modify ViewLocator.GetOrCreateViewType to use their container.

Sadly I don't have numbers to back my gut feeling up, but I feel that the amount of developers affected by 2. is smaller than 1. A middle ground would be a setting on ViewLocator but given the customization required for 2 is small I'm not sure it's worth the effort.

If you do still want to create a PR for further discussion (always happy for that) then I'd probably target the dev/4.0.0 branch. No matter which way we go it's definitely a breaking change.

@TheBigRic
Copy link

When I'm trying to build the project I get this error.

Error CS2001 Source file 'D:\Git Development\Caliburn-Micro\src\Caliburn.Micro.Platform\obj\Debug\MonoAndroid10\Resource.Designer.cs' could not be found. Caliburn.Micro.Platform(Xamarin.iOS)

What I find suspoious here is that is for the Xamarin.IOS version of the project while the compiler is searching in the MonoAndroid10 object directory. The new csproj format rocks, when it works :-)

Any ideas?

@nigel-sampson
Copy link
Contributor

Yeah the new format is looking promising, I'll have to have a play at home with a clean build to see if I can recreate the above issue.

If you're truly blocked create the PR against master, we can discuss and if it goes ahead I can play silly buggers with git to get it to the right place.

@nigel-sampson
Copy link
Contributor

@TheBigRic I managed to recreate the compile error on my machine and pushed a fix, do you mind testing?

@TheBigRic
Copy link

Yep, that worked! Good work finding that!

Seems still a blackbox-a-like thingy this new csproj. The documentation from ms is not complete and to much spread out over different site/blogs/forums, etc. if you ask me. Anyways, will work on the PR tomorrow!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.