reactiveui / ReactiveUI

An advanced, composable, functional reactive model-view-viewmodel framework for all .NET platforms that is inspired by functional reactive programming. ReactiveUI allows you to abstract mutable state away from your user interfaces, express the idea around a feature in one readable place and improve the testability of your application.
https://www.reactiveui.net
MIT License
8.08k stars 1.12k forks source link

Exception thrown when presenting view for viewmodel in assembly loaded from file and imported by MEF #1841

Closed kriewall closed 5 years ago

kriewall commented 5 years ago

Do you want to request a feature or report a bug?

This is a bug, possibly at the user-keyboard interface.

What is the current behavior?

An exception is thrown when ViewModelViewHost is used to present a view/viewmodel contained in an external assembly loaded from file and imported using MEF. The exception message reads: "Couldn't find view for 'MefImportTest.ExternalModule.DummyModule2ViewModel'". An examination of the Locator.Current registry fields reveals that the view does actually exist in the registry:

image

This exception occurs only when the assembly is loaded from file, e.g.: Observable.Return(Assembly.LoadFrom(@"..\..\..\..\MefImportTest.ExternalModule\bin\debug\net472\MefImportTest.ExternalModule.dll")).ObserveOnDispatcher().Select(RecomposeExternal).Subscribe();

If I add a project reference in my main project to the TestModule project and load the assembly, the view is successfully found and displayed and no exception is thrown: Observable.Return(typeof(DummyModule2ViewModel).Assembly).ObserveOnDispatcher().Select(RecomposeExternal).Subscribe();

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem

Here is a demo of the problem. To reproduce the issue:

  1. Build and run.
  2. Select the first dummy module from the drop-down. This works great - the view appears in the main window.
  3. Select the second dummy module from the drop-down. The exception is thrown.

What is the expected behavior?

View resolution should occur seamlessly regardless of how the assembly is loaded.

What is the motivation / use case for changing the behavior?

Doing so would allow modular desktop app design such that external modules could be created and loaded without redeployment.

Which versions of ReactiveUI, and which platform/OS are affected by this issue? Did this work in previous versions of ReactiveUI? Please also test with the latest stable and development snapshot.

Currently using:

Tried again using the latest versions, but I get the same issue:

Other information (e.g. stacktraces, related issues, suggestions how to fix)

Not sure if there's some conflict here between MEF and Splat that I should be aware of. Apologies if I should have listed this issue under Splat as opposed to ReactiveUI.

   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
   at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, Object[] index)
   at System.Reactive.AnonymousSafeObserver`1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\AnonymousSafeObserver.cs:line 46
   at System.Reactive.Linq.ObservableImpl.Where`1.Predicate._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Where.cs:line 57
   at System.Reactive.Linq.ObservableImpl.CombineLatest`3._.FirstObserver.OnNext(TFirst value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\CombineLatest.cs:line 101
   at System.Reactive.Linq.ObservableImpl.SelectMany`2.ObservableSelector._.InnerObserver.OnNext(TResult value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\SelectMany.cs:line 998
   at System.Reactive.Linq.ObservableImpl.Return`1._.Invoke() in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Return.cs:line 42
   at System.Reactive.Concurrency.Scheduler.Invoke(IScheduler scheduler, Action action) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Concurrency\Scheduler.Simple.cs:line 83
   at System.Reactive.Producer`2.SubscribeRaw(IObserver`1 observer, Boolean enableSafeguard) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Producer.cs:line 127
   at System.Reactive.Linq.ObservableImpl.SelectMany`2.ObservableSelector._.SubscribeInner(IObservable`1 inner) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\SelectMany.cs:line 982
   at System.Reactive.Linq.ObservableImpl.SelectMany`2.ObservableSelector._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\SelectMany.cs:line 938
   at System.Reactive.Linq.ObservableImpl.Switch`1._.InnerObserver.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Switch.cs:line 107
   at System.Reactive.Linq.ObservableImpl.Select`2.Selector._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Select.cs:line 49
   at System.Reactive.Linq.ObservableImpl.DistinctUntilChanged`2._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\DistinctUntilChanged.cs:line 79
   at System.Reactive.Linq.ObservableImpl.Select`2.Selector._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Select.cs:line 49
   at System.Reactive.Linq.ObservableImpl.Where`1.Predicate._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Where.cs:line 57
   at System.Reactive.Linq.ObservableImpl.Switch`1._.InnerObserver.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Switch.cs:line 107
   at System.Reactive.Linq.ObservableImpl.Select`2.Selector._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Select.cs:line 49
   at System.Reactive.Linq.ObservableImpl.Select`2.Selector._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Select.cs:line 49
   at System.Reactive.Linq.ObservableImpl.Where`1.Predicate._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Where.cs:line 57
   at System.Reactive.Linq.ObservableImpl.Cast`2._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Cast.cs:line 42
   at System.Reactive.Observer`1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Observers.cs:line 80
   at System.Reactive.Linq.ObservableImpl.SelectMany`2.EnumerableSelector._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\SelectMany.cs:line 1420
   at System.Reactive.Linq.ObservableImpl.Buffer`2.Boundaries._.BufferClosingObserver.OnNext(TBufferClosing value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Buffer.cs:line 651
   at System.Reactive.Linq.ObservableImpl.Merge`1.Observables._.InnerObserver.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Merge.cs:line 251
   at System.Reactive.Linq.ObservableImpl.Select`2.Selector._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Select.cs:line 49
   at System.Reactive.Linq.ObservableImpl.Where`1.Predicate._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Where.cs:line 57
   at System.Reactive.Observer`1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Observers.cs:line 80
   at ReactiveUI.IReactiveObjectExtensions.ExtensionState`1.NotifyObservable[T](TSender rxObj, T item, ISubject`1 subject) in D:\a\1\s\src\ReactiveUI\ReactiveObject\IReactiveObjectExtensions.cs:line 369
glennawatson commented 5 years ago

Whats the ReactiveUI Xaml package? It isn't anything officially supported from us.

glennawatson commented 5 years ago

Another thing to note, this is called once Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetCallingAssembly()); but there is no other View registrations after that point, and the calling assembly isn't in your externally loaded assembly.

kriewall commented 5 years ago

I got the ReactiveUI-XAML package from this link. I had learned of it from the answer to this Stack Overflow post by Paul Betts. Am I using the wrong Nuget package?

glennawatson commented 5 years ago

https://reactiveui.net/docs/getting-started/installation/

For WPF you'll want both the ReactiveUI and ReactiveUI.WPF nuget packages. If you use the eventing observables you can also use the ReactiveUI.Events.WPF package.

The xaml package you are using is deprecated.

Installation
ReactiveUI is a composable, cross-platform model-view-viewmodel framework for all .NET platforms that is inspired by functional reactive programming which is a paradigm that allows you to express the idea around a feature in one readable place, abstract mutable state away from your user interfaces and improve improve the testability of your application.
glennawatson commented 5 years ago

Also https://reactiveui.net/docs/getting-started/installation/windows-presentation-foundation is worth reading.

Windows Presentation Foundation
ReactiveUI is a composable, cross-platform model-view-viewmodel framework for all .NET platforms that is inspired by functional reactive programming which is a paradigm that allows you to express the idea around a feature in one readable place, abstract mutable state away from your user interfaces and improve improve the testability of your application.
kriewall commented 5 years ago

I see. Let me study those two links and see if I can figure out what I'm missing. I'm currently referencing ReactiveUI.Wpf, ReactiveUI is coming along by proxy.

To your other point, the second call to RegisterViewsForViewModels occurs in ModuleChangeMonitor in the RecomposeExternal method. In this method it targets the freshly loaded assembly. Is that a permissible use of RegisterViewsForViewModels?

glennawatson commented 5 years ago

Your packages looked good apart from that xaml project.

Should be a reasonable use. See if removing that xaml project helps your problem also since it might be conflicting with the ones in the WPF reactiveui project.

kriewall commented 5 years ago

I've removed all references to reactiveui-xaml from my solution, cleaned, blew away the bin and obj folders, rebuilt, reran - but still see the same issue. Also blew away reactiveui-xaml from my common .nuget folder and verified that it wasn't getting restored. Anything else I should try?

These are the only two package references in my .csproj now:

  <ItemGroup>
    <PackageReference Include="ReactiveUI.WPF" Version="9.2.2" />
    <PackageReference Include="MahApps.Metro" Version="1.6.5" />
  </ItemGroup>
DustinBryant commented 5 years ago

Experiencing the same issue here whereas we have a "shell-like" application that loads external "plugins" that contain their own view and view models. I'm fitting in ReactiveUI to the shell piece and eventually to each individual plugin. For now though, when the dynamic loading occurs we load in the ResourceDictionary which contains the Datatemplate's for ViewModel to View binding information.

IE:

<DataTemplate DataType="{x:Type SomeViewModel}">
        <SomeView />
</DataTemplate>

This used to work no problem until I hooked up the binding for the container of the plugin to use the control. Looked similiar to this:

<ViewModelViewHost x:Name="Container">
    <ContentPresenter x:Name="DynamicLoadedPlugin/>
</ViewModelViewHost>

I'd get the same error as OP now.

I have all latest stable NuGet's for ReactiveUI.WPF and its dependencies.

glennawatson commented 5 years ago

Have a potential solution, will require some testing first before I release.

glennawatson commented 5 years ago

The reason why this happens is because of the following:

https://stackoverflow.com/questions/4009382/type-gettype-fails-to-create-type-from-already-loaded-assembly

we use Type.GetType() which only returns by default types within assemblies loaded with Load (not LoadFrom).

You can use a custom Assembly loader to get around the issue currently which will allow your AddIn's to work with Assembly.Load() type syntax:

https://weblog.west-wind.com/posts/2016/Dec/12/Loading-NET-Assemblies-out-of-Seperate-Folders

The fix I am putting in place will search the current AppDomain (where both already Loaded assemblies and LoadFrom assemblies will be placed), and if that fails attempt to use the Assembly.Load method.

Stack Overflow
Type.GetType fails to create type from already loaded assembly
I have program which loads an assembly using Asssembly.LoadFrom method. Some time later I attempt to use Type.GetType to create a type from that assembly (using AssemblyQualifiedName), but the method
Loading .NET Assemblies out of Seperate Folders
In the process of updating the Addin manager in Markdown Monster I ran into a few snags with loading .NET assemblies out of separate folders. Assembly loading out of non base folders in .NET can be problematic and sure enough I ran into a few issues that took a while to find a work around for. In this post I describe some of the issues of folder based assembly loading and a brute force solution to deal with assembly resolution.
glennawatson commented 5 years ago

See https://reactiveui.net/docs/getting-started/installation/#development-packages to install 9.3.7 to test the functionality being fixed.

Installation
ReactiveUI is a composable, cross-platform model-view-viewmodel framework for all .NET platforms that is inspired by functional reactive programming which is a paradigm that allows you to express the idea around a feature in one readable place, abstract mutable state away from your user interfaces and improve improve the testability of your application.
kriewall commented 5 years ago

Thanks Glenn. Looks like the abstract ReactiveCommand class has been replaced with a static class sometime between the version I'm using (9.1.4) and 9.3.7, so I've got a bit of refactoring to do before I can test the fix. Will let you know the outcome once I've had the chance to do so.

kriewall commented 5 years ago

@glennawatson - Is removal of the abstract ReactiveCommand class definitely committed? The reason I ask is because this seems to be a breaking change associated with a minor version uprev. Just want to avoid making a bunch of changes if removal of the abstract ReactiveCommand is in any way under evaluation.

glennawatson commented 5 years ago

See https://reactiveui.net/blog/2018/11/reactive-command-abstract

Removing ReactiveCommand abstract base class
ReactiveUI is a composable, cross-platform model-view-viewmodel framework for all .NET platforms that is inspired by functional reactive programming which is a paradigm that allows you to express the idea around a feature in one readable place, abstract mutable state away from your user interfaces and improve improve the testability of your application.
kriewall commented 5 years ago

@glennawatson Tested out the fix, seems to work great. Thanks for your support!