Is it possible to Create different pojects for Views, ViewModels and Models in a single solution

Topics: Bootstrappers & IoC, Bugs, Conventions, Extensibility, Feature Requests, Framework Services, UI Architecture
Jan 18, 2013 at 12:27 PM

I am creating an application where ViewModels are kept in different classLibrary, Likewise Models in another. And I am using ViewModel first, It throws an exception saying

"Could not locate any instances of contract CMSample.ViewModels.ShellViewModel." 

As caliburn.Micro using name matching pattern, I am following the below pattern

View Namespace : CMSample.Views.ShellView.

View Namespace : CMSample.ViewModels.ShellViewModel.

My Bootstrapper look like this:

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Linq;
using System.Text;
using Caliburn.Micro;
using CMSample.ViewModels;

namespace CMSample
{
    public class AppBootstrapper : Bootstrapper<ShellViewModel>
    {
        CompositionContainer container;

        protected override void StartRuntime()
        {
            //LogManager.GetLog = type => new SimpleLog(type);
            base.StartRuntime();
        }

        protected override void Configure()
        {
            container = new CompositionContainer(
                new AggregateCatalog(AssemblySource.Instance.Select(x => new AssemblyCatalog(x)))
                );

            var batch = new CompositionBatch();

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

            container.Compose(batch);
        }

        protected override object GetInstance(Type serviceType, string key)
        {
            var contract = string.IsNullOrEmpty(key) ? AttributedModelServices.GetContractName(serviceType) : key;
            var exports = container.GetExportedValues<object>(contract);

            if (exports.Count() > 0)
                return exports.First();

            throw new Exception(string.Format("Could not locate any instances of contract {0}.", contract));
        }

        protected override IEnumerable<object> GetAllInstances(Type serviceType)
        {
            return container.GetExportedValues<object>(AttributedModelServices.GetContractName(serviceType));
        }
    }
}

Please help me to get out of this.

Jan 18, 2013 at 1:00 PM

Hi Antony,

I am fairly new to CM but hopefully I can point you in the right direction. If I am wrong, someone will surely correct me.

Check out the documentation "Customizing the Bootstrapper" written by Rob. Specifically the following paragraph.

All you have to do is return a list of searchable assemblies. By default, the base class returns the assembly that your Application exists in. So, if all your views are in the same assembly as your application, you don’t even need to worry about this. If you have multiple referenced assemblies that contain views, this is an extension point you need to remember. Also, if you are dynamically loading modules, you’ll need to make sure they get registered with your IoC container and the AssemblySoure.Instance when they are loaded.

You need to override the SelectAssemblies and add the assembly that contains your views to the container. Then when CM calls the GetInstance, it will find your assembly and apply convention to find the view.

Hope this gets you started,

Richard

Jan 19, 2013 at 7:32 AM

Thank you Richard, I will try it for sure. will let you know if there is any problem.

thanks again.

Antony

Jan 20, 2013 at 4:47 PM

Antony,

I was looking over your initial email and I realized that CM can't find your ShellViewModel and not some other ViewModel in another project. Are you also putting your ShellViewModel in a different project/assembly?

I was thinking that you have the ShellViewModel in the same project as your AppBootstrapper.  If that is the case then, you have to export your ShellViewModel by adding the attribute [Export] on your ShellViewModel class or something like the following if your shell is implementing an interface.

Export(typeof(IShell))]
public class ShellViewModel : PropertyChangedBase, IShell
{
   ...implementation is same as before...
}

If your using Visual Studio, put a stop after the line container.Compose(batch); Look into the catalogue of the container to make sure that the ShellViewModel is in the catalog. It has to be in there or else CM won't find it and give you the error message.

Richard

Jan 21, 2013 at 4:08 AM
Edited Jan 21, 2013 at 4:25 AM

Hi Richard,

Yes, shellViewModel is in another project with other ViewModels. And I am using the below code in ShellViewModel class. 

[Export(typeof(ShellViewModel))]    
public class ShellViewModel : Conductor<IScreen>.Collection.OneActive, IScreen
{
   ...implementation is same as before...
}

Inside GetInstance Method

protected override object GetInstance(Type serviceType, string key)
        {
            var contract = string.IsNullOrEmpty(key) ? AttributedModelServices.GetContractName(serviceType) : key;
            var exports = container.GetExportedValues<object>(contract);

            if (exports.Count() > 0)
                return exports.First();

            throw new Exception(string.Format("Could not locate any instances of contract {0}.", contract));
        }

contract identifies the ShellViewModel string : like this "CMSample.ShellViewModel". But Exports.Count is Zero. then it throws the new exception. Is it related to the same problem, that you explained in you first reply.

Thanks,

Antony.

Jan 21, 2013 at 1:49 PM

Ok, I've created a test app and CM can now find the ShellViewModel that is in a separate project/dll.  Here's the extra line of codes in the SelectAssemblies method.

      protected override IEnumerable SelectAssemblies() {
         var assemblies = new List();
         assemblies.AddRange(base.SelectAssemblies());
         //Load new ViewModels here
         string[] fileEntries = Directory.GetFiles(Directory.GetCurrentDirectory());

         foreach (string fileName in fileEntries) {
            if (fileName.Contains("ViewModels.dll")) {
               assemblies.Add(Assembly.LoadFile(fileName));
            }
         }
         return assemblies;
      }

The foreach loop looks for files that contain ViewModels.dll and loads then in the assemblies list which is used by CM. When CM looks through the external library it will find the ShellViewModel as load as expected.  If you now put a stop on the container.Compose(batch) line in the Configure method you will see that the ShellViewModel is now in the list of parts in the catalogue.

So far so good. CM loads the ViewModel but can't find the View.  Not too sure how you can Export a xaml view.  I will try to figure that one out a bit later.

Richard


Jan 21, 2013 at 2:12 PM

Reread the original quote from Rob that I posted in a previous post and the answer is right there. Simply load the assembly with the views in the assemblies list. So here is the modified SelectAssemblies method.

      protected override IEnumerable SelectAssemblies() {
         var assemblies = new List();
         assemblies.AddRange(base.SelectAssemblies());
         //Load new ViewModels here
         string[] fileEntries = Directory.GetFiles(Directory.GetCurrentDirectory());

         foreach (string fileName in fileEntries) {
            if (fileName.Contains("ViewModels.dll") || fileName.Contains("Views.dll")) {
               assemblies.Add(Assembly.LoadFile(fileName));
            }
         }
         return assemblies;
      }

Now the method also looks for files that contains "Views.dll".

Now the ViewModel can create the View as expected...  This is too cool...

Just remember to copy your dll in the same folder as the main app since the main app does not have a reference to the project which contains the view, it won't automatically get copied.

Richard

 

Jan 22, 2013 at 1:18 PM

Thank you for your kind reply , Richard.

That's working. 

But I found some other code like  Adding catalog of referenced assemblies.

var catalog = new AggregateCatalog(AssemblySource.Instance.Select(x => new AssemblyCatalog(x)));
            catalog.Catalogs.Add(new AssemblyCatalog((typeof(CMSample.ViewModels.ShellViewModel)).Assembly));//.ViewModels.ShellViewModel)).Assembly));
            container = new CompositionContainer(catalog);

            var batch = new CompositionBatch();

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

            container.Compose(batch);

I am  not sure about the code, I got. will you please tell me, Which one suits best in Caliburn.Micro standards, the above mentioned code or one described in your reply.

Thanks

Antony

Jan 23, 2013 at 10:17 PM

Glad to hear you got your code to work.

Off the top of my head, it looks like the typeof() method returns the type CMSample.ViewModels.ShellViewModel in an assembly of the same name, the ShellViewModel, then its gets added to an Assembly catalog which subsequently gets added to the AggregateCatalog.  Seems to make sense to me.

What I don't get is how CM can find the View if it's in a different assembly.

Anyhow, I don't think that CM cares how the classes are loaded in the catalog. This is more of a MEF concept.  Once the Views and ViewModels are in the catalogue MEF will simply give your application an instance when your code needs it.

Richard