Find View in external assemblies

Dec 16, 2010 at 9:34 PM
Edited Dec 17, 2010 at 2:52 PM

I am creating external modules as WPF DLL files (Modules) that have the full ViewModel and View in them  and they are being discovered into the catalog.  But the ViewModel can't seem to find the associated View in the host project.  Is there anything that needs to be added specifically so that the host project knows where to look for Views in the external assemblies?

 

Dec 17, 2010 at 12:27 PM

Generally speaking, you should register external assemblies in AssemblySource.Instance, usually overriding Bootstrap.SelectAssemblies, to have CM use them in conventions.
I'm not sure to understand what do you mean with "associated Model", though...

Dec 17, 2010 at 2:51 PM

Sorry, I used the wrong term.  I meant assocatied View.  Ok, lets say I have a Shared interface called IModule. My external ViewModels impliment IModule.   So all IModules are being imported fine.  However, their associated Views are not being discovered via convention.  I always get "Cannot find View....".  So I don't know if I have to set something else to tell MEF/CM to look in the external assemblies for the Views that go with the imported ViewModels.

Dec 17, 2010 at 3:19 PM

To avoid a complete AppDomain search, by default Caliburn.Micro.ViewLocator.LocateForModelType will search for Views only in the assemblies specified in the AssemblySource.Instance collection, as Marco said.

So, if you want your views to be discoverable, you need to register the Aseemblies in that collection.

Dec 17, 2010 at 6:24 PM

Interesting, because I am specifying that, from what I can tell, for both the project assembly and external assemblies

 container = new CompositionContainer(
                new AggregateCatalog(
                    AssemblySource.Instance.Select(x => new AssemblyCatalog(x)).OfType<ComposablePartCatalog>()
                        .Concat(new ComposablePartCatalog[] { new DirectoryCatalog(ModuleDirectory) }))
                );

                var batch = new CompositionBatch();

                batch.AddExportedValue<IWindowManager>(new WindowManager());
                batch.AddExportedValue<IEventAggregator>(new EventAggregator());
                batch.AddExportedValue(container);
                container.Compose(batch); 

 


Dec 17, 2010 at 8:05 PM

Well, from the code you posted I can see that your IoC is searching the assemblies located in the ModuleDirectory and the executing assembly, but I cannot see the registration of such assemblies inside AssemblySource.Instance.

If you are not exporting the views and you are relying on naming conventions (which is probable, since most examples do so), the Caliburn.Micro.ViewLocator.LocateForModelType will not be able to find the view types.

Try to add the following code before the container composition

foreach(string file in System.IO.Directory.GetFiles("*.dll"))
    AssemblySource.Instance.Add(Assembly.Load(file));

and remove the Concat part of the composition itself to avoid to add assemblies twice.

Note that I typed the above code without compiling/testing it, and I am unsure of the Assembly.Load override parameters. Moreover, it would be better to either use some code to check if the *..dll is truly an assembly, or enclose the foreach body in  a try/catch (just to swallow up errors).

Dec 17, 2010 at 8:42 PM

Hey, thanks for the help.  You mentioned that I was not Exporting the Views.  Should I be exporting the Views?  What is the way to do that?  So I did it like this, but I get an error in the bootstrapper.  I know what you said about the try/catch, but I made sure the only two DLLs in that folder are assemblies with exports

private string ModuleDirectory = @"C:\Projects\DMF\DMF.WPF\Modules";
        protected override void Configure()
        {
            container = new CompositionContainer(
                new AggregateCatalog(
                    AssemblySource.Instance.Select(x => new AssemblyCatalog(x)).OfType<ComposablePartCatalog>())
                );

            foreach (string file in System.IO.Directory.GetFiles(ModuleDirectory, "*.DLL", SearchOption.AllDirectories))
            {
                AssemblySource.Instance.Add(Assembly.Load(file));
            }


            var batch = new CompositionBatch();

                batch.AddExportedValue<IWindowManager>(new WindowManager());
                batch.AddExportedValue<IEventAggregator>(new EventAggregator());
                batch.AddExportedValue(container);
                container.Compose(batch);
        }

Dec 18, 2010 at 2:50 AM

To export a view you just need to put the [Export] attribute on the view class, but it is not required at all; if you want to make the view object persistent, or you want to re-use it, this approach can be useful, but I wouldn't say that this is the recommended way.

Regarding the error, can you be more precise? Is it an exception? If so, could you post its information?

Apr 3, 2011 at 9:08 PM

Bladewise/Marco,

I have a similar problem as @heavywoody

This is my first attempt at overriding SelectAssemblies:

protected overrideIEnumerable<Assembly> SelectAssemblies() { var assemblies= new List<Assembly> {Assembly.GetEntryAssembly()}; assemblies.Add(Assembly.LoadFrom("ToothFairyIII.Views.dll")); var referencedAssemblies = assemblies[0].GetReferencedAssemblies(); assemblies.AddRange(referencedAssemblies.Select(Assembly.Load)); return assemblies; }

The dll containing the Views (i.e. ToothFairyIII.Views.dll) isn't referenced from any of the other assemblies, so I copy it to the bin\debug directory on a post-build event. Still the same error message, "Can't find view for..."

Then I tried (explicitly) adding the assemblies collection to AssemblySource like this:

  
     var  assemblies= new List<Assembly> {Assembly.GetEntryAssembly()};
            assemblies.Add(Assembly.LoadFrom("ToothFairyIII.Views.dll"));
            var referencedAssemblies = assemblies[0].GetReferencedAssemblies();
            assemblies.AddRange(referencedAssemblies.Select(Assembly.Load));
            AssemblySource.Instance.AddRange(assemblies);
   
  

Still the same error message.

Sorry if the solution to this turns out to be "trivial", but I'm not able to see it right now, so any suggestions/help would be highly appreciated.

Coordinator
Apr 4, 2011 at 2:43 AM

My guess is that you need to customize the view locator. If you have ViewModels in different assemblies from their views, then the default type name pattern may not be working.

Apr 4, 2011 at 2:47 AM

Norgie,

My solution was that with each View, in the class file I have to put a [Export] decorator above the class declaration, and then it finds the View

 

Apr 4, 2011 at 7:12 AM
Edited Apr 4, 2011 at 1:44 PM

Hi, EisenbergEffect.

Is that because the LocateTypeForModelType relies on the ViewModel's full name and not just the name?

Yes, the simplest solution seems to be, as you suggest, to move the view(s) into the same assembly as the viewmodel(s).

<edit>

Well, I moved the view into my ToothFairyIII.ViewModels assembly and guess what? It still doesn't work.

Then I took a look at the ViewLocator class and the static method LocateTypeForModelType. There I found the following code snippet:

var viewTypeName = modelType.FullName.Substring(0, modelType.FullName.IndexOf("`") < 0
? modelType.FullName.Length
: modelType.FullName.IndexOf("`")
).Replace("Model", string.Empty);

After executing the codesnippet above the viewTypeName equals "ToothFairyIII.Views.ShellView"

Aha! Since Caliburn is relying on convention over configuration, naming your view model(s) assembly ViewModels is probably not a good idea. ;-)

The only thing I don't get is why Caliburn failed to find the View before I moved it to the same assembly as the view model. The view's original "home" was in an assembly called ToothFairyIII.Views.

Hmm, looks like I need to invastigate a bit more, unless someone else has a (different) solution to this.

</edit>

--norgie

Apr 4, 2011 at 7:14 AM

heavywoody,

Thanks for your suggestion, but I'm not using MEF and [Export] is a MEF specific attribute.

Btw, I'm using Ninject. :-)

--norgie

Apr 4, 2011 at 2:37 PM

Got it working :-)

The ShellViewModel's fullname is "ToothFairyIII.ViewModels.Shell.ShellViewModel", so I added a new project for the view(s) called ToothFairyIII.Views and made sure it contained a folder called Shell so that the ShellView's fullname is "ToothFairyIII.Views.Shell.ShellView".

I load the ToothFairyIII.Views assembly manually as per the code in a previous post (no need to explicitly call AssemblySource.Instance.AddRange(assemblies) though).

I'll consider using a different naming convention as well as using a different scheme for organising the view models and views (probably based on business function instead of type).

--norgie

Jun 1, 2011 at 3:13 AM

I am trying to do this same thing and am having an additional issue...

 

My solution has different projects for views and viewmodels... client.views and client.viewmodels

 

These additional assemblies are not being packaged in the xap file when it is copied to the web solution to be run... how do they get included in the xap so that the above techniques work?


Thanks

harold

Jun 1, 2011 at 6:07 AM

 

If you want to have it decoupled you need to

1. The "different projects" needs to be created as silverlight applications so a xap will be created

2. Then you need to use the LoadCatalog Result recipe which will download the xap and add it to the catalog and assembly source

With this approach you need to know the name of the xap though. I did some experimenting last weekend and I found 

http://www.wintellect.com/CS/blogs/jprosise/archive/2010/06/17/dynamic-xap-discovery-with-silverlight-mef.aspx

 

However, if you have added a reference for the "different projects" to you main silverlight app you may just override the SelectAssemblies and add them there. If the conventions are setup correct it should find them

 

Don't know if it helps you but it was a try :). 

 

/christoffer

Jun 1, 2011 at 1:57 PM

ok, the adding of the references to the main project makes sense... did that and then they appear in the xap... but the bootstrapper still can't find them. Since they are in the xap file, the path is relative to the startup dll i would assume?

 protected override IEnumerable<Assembly> SelectAssemblies()
        {
            return new[] {
                Assembly.GetExecutingAssembly(), Assembly.Load("Client.Views.dll"), Assembly.Load("Client.ViewModels.dll")
            };
        }

It is saying it can't find the Client.Views.dll file...

 

Thanks

Harold

Jul 12, 2011 at 2:03 PM
Edited Jul 12, 2011 at 2:04 PM

As I had an view in the same plug-in that wouldn't be loaded, I had to do this modification to the ViewLocator to get it to load the view.

 

 

            var viewType = (from assembly in AssemblySource.Instance
                            from type in assembly.GetExportedTypes()
                            where viewTypeList.Contains(type.FullName)
                            select type).FirstOrDefault();

            if(viewType == null) {
                viewType = (from type in modelType.Assembly.GetExportedTypes()
                            where viewTypeList.Contains(type.FullName)
                                select type).FirstOrDefault();

                if(viewType == null) {
                    Log.Warn("View not found. Searched: {0}.", string.Join(", ", viewTypeList.ToArray()));                
                }               
            }

 

As you can see, my convention is based on that fact that I always have the view in the same assembly as the model. I believe that this is true for 90% of all the people. I was not willing to write code to scan for assemblies in my plug-in folder as that is in my eyes a task that MEF's should do.

This is first time Rob hasn't provided a solution out-of-the-box, but I'm very happy with the Caliburn.Micro project as a whole.

Keep up the good work.

Benny/VirtueMe