cYOUNES
cYOUNES

Reputation: 936

Resolve ViewModels in separated assembly with ViewModelLocator in Prism 6

I am trying to connect the DataContexts of my views to view models from another separated assembly.

Brian Lagunas wrote on his blog something to Getting Started with Prism’s new ViewModelLocator, However, his solution is specially to customize the conventions to allow the ViewModelLocator resolving the view models types.

My scenario:

I have the main project (MyApplication.exe) contains the Bootstrapper, Shell and the views In another separated assembly (MyApplication.Process.dll) i have all the view models.

Basing on Brian's explication, i tried the following solution:

protected override void ConfigureViewModelLocator()
    {
        base.ConfigureViewModelLocator();

        ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver(
            viewType =>
                {
                    var viewName = viewType.FullName;
                    var viewAssemblyName = viewType.Assembly.GetName().Name;

                    var viewModelNameSuffix = viewName.EndsWith("View") ? "Model" : "ViewModel";

                    var viewModelName = viewName.Replace("Views", "ViewModels") + viewModelNameSuffix;
                    viewModelName = viewModelName.Replace(viewAssemblyName, viewAssemblyName + ".Process");
                    var viewModelAssemblyName = viewAssemblyName + ".Process";
                    var viewModelTypeName = string.Format(
                        CultureInfo.InvariantCulture,
                        "{0}, {1}",
                        viewModelName,
                        viewModelAssemblyName);

                    return Type.GetType(viewModelTypeName);                        
                });
    }

The solution above works correctly, However, i don't know if this is the best way to do that?

All what i want, is to tell Prism ViewModelLocator in which Assemblies it has to find the view models, i mean the same approach of Caliburn.Micro (Looks for the view models in all registered assemblies).

The solution above will not work if my application support the Prism Modularity if the assembly name doesn't end with the word 'Process' for example?

What do you suggest for me ?

Upvotes: 3

Views: 3516

Answers (3)

shakram02
shakram02

Reputation: 11846

What about this workaround ? -I really gave up on editing the ViewModelLocator- make a ViewModule within the same project and let it inherit from another ViewModel in the other assembly, the core implementation is in the base ViewModel and you can still bind to it and do everything you want.

I marked all the functions in the Base class as virtual so I can extend their functionality if I want to use some platform specific components ex. IRegionManager
this is the code from the platform project ( WPF )

namespace PrismApplicationTest0.ViewModels
{
    public class ViewAViewModel : ViewAViewModelBase
    {
        private readonly IRegionManager _regionManager;

        public ViewAViewModel(IEventAggregator eventAggregator,IRegionManager regionManager) : base(eventAggregator)
        {
            _regionManager = regionManager;
        }

        protected override void UpdateMethod()
        {
            // After completing the core functionality
            base.UpdateMethod();

            // Switch to another page using platform specific region manager
            _regionManager.RequestNavigate(RegionNames.ContentRegion,"ViewB");
        }
      }
}

this is the code from the PCL ( portable class library )

using System;
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;

namespace MainModule.ViewModels
{
    public abstract class ViewAViewModelBase : BindableBase
    {
        private readonly IEventAggregator _eventAggregator;
        private string _firstName;
        private string _lastName;
        private DateTime? _lastUpdated;

        public string FirstName
        {
            get { return _firstName; }
            set { SetProperty(ref _firstName, value); }
        }

        public string LastName
        {
            get { return _lastName; }
            set { SetProperty(ref _lastName, value); }
        }

        public DateTime? LastUpdated
        {
            get { return _lastUpdated; }
            set { SetProperty(ref _lastUpdated, value); }
        }

        public DelegateCommand UpdateCommand { get; private set; }

        public ViewAViewModelBase(IEventAggregator eventAggregator)
        {

            _eventAggregator = eventAggregator;
            UpdateCommand =
            new DelegateCommand(UpdateMethod, CanUpdateMethod)
            .ObservesProperty(() => FirstName)
            .ObservesProperty(() => LastName);
        }

        protected bool CanUpdateMethod()
        {
            return !String.IsNullOrEmpty(_lastName) && !String.IsNullOrEmpty(_firstName);
        }

        protected virtual  void UpdateMethod()
        {

            LastUpdated = DateTime.Now;
            _eventAggregator.GetEvent<Events.UpdatedAggEvent>().Publish($"User {FirstName}");
        }
    }
}

it's working as a charm with me. I guess if you need another object from other assemblies you can create instances of them in the base class
Good luck

Upvotes: 1

cYOUNES
cYOUNES

Reputation: 936

I finally solved my issue by setting custom view model resolver to search for view models in all added assemblies catalogs.

Solution

First of all, i try to apply the default prism view model locator convention, if no viewmodel found then, i start applying my custom one.

1- I start by getting all assemblies from the AggregateCatalog.

2- I Get all the non-abstract exported types inherit from Prism BindableBase.

3- I Apply the custom convention delegate to get the expected view model.

In my case, the custom convention is all types having the suffix "ViewModel" and the prefix is the view type name: Example: If the view name is "UsersView" the view model should be "UsersViewModel". If the view name is "Users" the view model should be also "UsersViewModel".

Code:

ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver(
            viewType =>
                {
                    // The default prism view model type resolver as Priority 
                    Type viewModelType = this.GetDefaultViewModelTypeFromViewType(viewType);
                    if (viewModelType != null)
                    {
                        return viewModelType;
                    }

                    // IF no view model found by the default prism view model resolver

                    // Get assembly catalogs
                    var assemblyCatalogs = this.AggregateCatalog.Catalogs.Where(c => c is AssemblyCatalog);

                    // Get all exported types inherit from BindableBase prism class
                    var bindableBases =
                        assemblyCatalogs.Select(
                            c =>
                            ((AssemblyCatalog)c).Assembly.GetExportedTypes()
                                .Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(BindableBase)))
                                .Select(t => t)).SelectMany(b =>
                                    {
                                        var types = b as IList<Type> ?? b.ToList();
                                        return types;
                                    }).Distinct() ;

                    // Get the type where the delegate is applied
                    var customConvention = new Func<Type, bool>(
                        (Type t) =>
                            {
                                const string ViewModelSuffix = "ViewModel";
                                var isTypeWithViewModelSuffix = t.Name.EndsWith(ViewModelSuffix);
                                return (isTypeWithViewModelSuffix)
                                       && ((viewType.Name.EndsWith("View") && viewType.Name + "Model" == t.Name)
                                           || (viewType.Name + "ViewModel" == t.Name));
                            });

                    var resolvedViewModelType = bindableBases.FirstOrDefault(customConvention);
                    return resolvedViewModelType;
                });

The method * GetDefaultViewModelTypeFromViewType * Is the default prism view model locator, its code is exactly the same as in Bart's answer. I hope this will be helpful for others.

Edit:

I finally solved the problem by creating a new custom MvvmTypeLocator:

public interface IMvvmTypeLocator
{
    #region Public Methods and Operators

    Type GetViewModelTypeFromViewType(Type viewType);

    Type GetViewTypeFromViewModelType(Type viewModelType);

    Type GetViewTypeFromViewName(string viewName);

    #endregion
}

The implementation :

public class MvvmTypeLocator: IMvvmTypeLocator
{
    private AggregateCatalog AggregateCatalog { get; set; }

    public MvvmTypeLocator(AggregateCatalog aggregateCatalog)
    {
        this.AggregateCatalog = aggregateCatalog;
    }

    public Type GetViewModelTypeFromViewType(Type sourceType)
    {
        // The default prism view model type resolver as Priority 
        Type targetType = this.GetDefaultViewModelTypeFromViewType(sourceType);
        if (targetType != null)
        {
            return targetType;
        }

        // Get assembly catalogs
        var assemblyCatalogs = this.AggregateCatalog.Catalogs.Where(c => c is AssemblyCatalog);

        // Get all exported types inherit from BindableBase prism class
        var bindableBases =
            assemblyCatalogs.Select(
                c =>
                ((AssemblyCatalog)c).Assembly.GetExportedTypes()
                    .Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(BindableBase)))
                    .Select(t => t)).SelectMany(b =>
                    {
                        var types = b as IList<Type> ?? b.ToList();
                        return types;
                    }).Distinct();

        // Get the type where the delegate is applied
        var customConvention = new Func<Type, bool>(
            (Type t) =>
            {
                const string TargetTypeSuffix = "ViewModel";
                var isTypeWithTargetTypeSuffix = t.Name.EndsWith(TargetTypeSuffix);
                return (isTypeWithTargetTypeSuffix)
                       && ((sourceType.Name.EndsWith("View") && sourceType.Name + "Model" == t.Name)
                           || (sourceType.Name + "ViewModel" == t.Name));
            });

        var resolvedTargetType = bindableBases.FirstOrDefault(customConvention);
        return resolvedTargetType;
    }

    public Type GetViewTypeFromViewModelType(Type sourceType)
    { 
        // Get assembly catalogs
        var assemblyCatalogs = this.AggregateCatalog.Catalogs.Where(c => c is AssemblyCatalog);

        // Get all exported types inherit from BindableBase prism class
        var bindableBases =
            assemblyCatalogs.Select(
                c =>
                ((AssemblyCatalog)c).Assembly.GetExportedTypes()
                    .Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(IView)))
                    .Select(t => t)).SelectMany(b =>
                    {
                        var types = b as IList<Type> ?? b.ToList();
                        return types;
                    }).Distinct();

        // Get the type where the delegate is applied
        var customConvention = new Func<Type, bool>(
            (Type t) =>
            {
                const string SourceTypeSuffix = "ViewModel";
                var isTypeWithSourceTypeSuffix = t.Name.EndsWith(SourceTypeSuffix);
                return (isTypeWithSourceTypeSuffix)
                       && ((sourceType.Name.EndsWith("View") && t.Name + "Model" == sourceType.Name)
                           || (t.Name + "ViewModel" == sourceType.Name));
            });

        var resolvedTargetType = bindableBases.FirstOrDefault(customConvention);
        return resolvedTargetType;
    }

    public Type GetViewTypeFromViewName(string viewName)
    {
        // Get assembly catalogs
        var assemblyCatalogs = this.AggregateCatalog.Catalogs.Where(c => c is AssemblyCatalog);

        // Get all exported types inherit from BindableBase prism class
        var bindableBases =
            assemblyCatalogs.Select(
                c =>
                ((AssemblyCatalog)c).Assembly.GetExportedTypes()
                    .Where(t => !t.IsAbstract && typeof(IView).IsAssignableFrom(t) && t.Name.StartsWith(viewName))
                    .Select(t => t)).SelectMany(b =>
                    {
                        var types = b as IList<Type> ?? b.ToList();
                        return types;
                    }).Distinct();

        // Get the type where the delegate is applied
        var customConvention = new Func<Type, bool>(
            (Type t) =>
                {
                    return t.Name.EndsWith("View");
                });

        var resolvedTargetType = bindableBases.FirstOrDefault(customConvention);
        return resolvedTargetType;
    }

    private Type GetDefaultViewModelTypeFromViewType(Type viewType)
    {
        var viewName = viewType.FullName;
        viewName = viewName.Replace(".Views.", ".ViewModels.");
        var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
        var suffix = viewName.EndsWith("View") ? "Model" : "ViewModel";
        var viewModelName = String.Format(
            CultureInfo.InvariantCulture,
            "{0}{1}, {2}",
            viewName,
            suffix,
            viewAssemblyName);
        return Type.GetType(viewModelName);
    }
}

This custom type locator is using the AggregateCatalog to search the target types in all the assembly catalogs. Of course i create its instance on the Bootstrapper once the Bootstrapper's AggregateCatalog is configured:

protected override void ConfigureAggregateCatalog()
    {
        base.ConfigureAggregateCatalog();
        AggregateCatalog.Catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
        AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(LoginViewModel).Assembly));
        AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(LoginView).Assembly));
        this.mvvmTypeLocator = new MvvmTypeLocator(this.AggregateCatalog);
    }

At the end, i just configure the view model locator in the Bootstrapper like the following:

protected override void ConfigureViewModelLocator()
    {
        base.ConfigureViewModelLocator();

        ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver(
            viewType => this.mvvmTypeLocator.GetViewModelTypeFromViewType(viewType));
    }

Note that the methods GetViewTypeFromViewModelType and GetViewTypeFromViewName are searching all the views implementing the interface named IView. this just an empty interface i use to distinct my views from other classes inside the same assembly. If someone use this mvvmTypeLocator, then he has to create his own interface and implement all the views that should be discoverable by the mvvmTypeLocator.

Upvotes: 1

Bart
Bart

Reputation: 10015

Your code of resolving the viewmodel type is certainly ok. If you look to the codebase of Prism, you'll notice a quite similar way using minor reflection and string replacements.

static Func<Type, Type> _defaultViewTypeToViewModelTypeResolver =
        viewType =>
        {
            var viewName = viewType.FullName;
            viewName = viewName.Replace(".Views.", ".ViewModels.");
            var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
            var suffix = viewName.EndsWith("View") ? "Model" : "ViewModel";
            var viewModelName = String.Format(CultureInfo.InvariantCulture, "{0}{1}, {2}", viewName, suffix, viewAssemblyName);
            return Type.GetType(viewModelName);
        };

The thing with convention based type resolution is that you have to go with the default convention or create your own convention and stick to the chosen convention. Meaning that if in your first module you choose to have the viewmodels in a .Process assembly, that you should do this for all your other modules. Sticking to your convention is the easiest way to go.

The good news is: if you don't like convention based resolution, you can override the resolution like you already did and implement any kind of resolution you like, how complex you want it to be. Nothing keeps you from e.g. keeping a dictionary mapping views to viewmodels (which we actually did for one project). Filling up this dictionary (or setting up another way of resolution) would be done in the ModuleCatalog for each module.

Upvotes: 2

Related Questions