[Prism] Load modules in specific order (even for modules loaded on demand) !

November 17th, 2011 | Posted by Tom in .NET | Article | Prism | Silverlight | WPF | Read: 11,296

Prism is a well-know framework to develop applications that support modularity, navigation, communication between loosely coupled components and more !

Each module is loaded automatically (OnDemand property of Module attribute set to false) or on demand (OnDemand set to true) but in all case, you can’t define in which order the modules are loaded. By default, they are loaded by using their name and you don’t have any way to change that.

In my case, I wanted to be able to load my modules in specific order. So, after taking a look on Internet, I’ve found this post on StackOverFlow: http://stackoverflow.com/questions/1296642/how-to-control-the-order-of-module-initialization-in-prism

The code work fine except for modules that are asked to be loaded on demand. So I needed to find a way to be able to specify the loading order of each module, even if they have to be started on demand) and, for that, I’ve been inspired by the previous code.

Indeed, I first create the Priority attribute:

/// <summary>
/// Allows the order of module loading to be controlled.  Where dependencies
/// allow, module loading order will be controlled by relative values of priority
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class PriorityAttribute : Attribute
{
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="priority">the priority to assign</param>
    public PriorityAttribute(int priority)
    {
        this.Priority = priority;
    }

    /// <summary>
    /// Gets or sets the priority of the module.
    /// </summary>
    /// <value>The priority of the module.</value>
    public int Priority { get; private set; }
}

Then, I created a dedicated ModuleCatalog, inspired by the PrioritizedDirectoryCatalog (from the post of StackOverFlow) and the DirectoryModuleCatalog (from the Prism’s sources):

public class PrioritizedDirectoryModuleCatalog : DirectoryModuleCatalog
{
    protected override void InnerLoad()
    {
        if (string.IsNullOrEmpty(this.ModulePath))
            throw new InvalidOperationException("The ModulePath cannot contain a null value or be empty.");

        if (!Directory.Exists(this.ModulePath))
            throw new InvalidOperationException(
                string.Format(CultureInfo.CurrentCulture, "Directory {0} was not found.", this.ModulePath));

        AppDomain childDomain = this.BuildChildDomain(AppDomain.CurrentDomain);

        try
        {
            List<string> loadedAssemblies = new List<string>();

            var assemblies = (
                                 from Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()
                                 where !(assembly is System.Reflection.Emit.AssemblyBuilder)
                                    && assembly.GetType().FullName != "System.Reflection.Emit.InternalAssemblyBuilder"
                                    && !String.IsNullOrEmpty(assembly.Location)
                                 select assembly.Location
                             );

            loadedAssemblies.AddRange(assemblies);

            Type loaderType = typeof(ModulePriorityLoader);

            if (loaderType.Assembly != null)
            {
                var loader =
                    (ModulePriorityLoader)
                    childDomain.CreateInstanceFrom(loaderType.Assembly.Location, loaderType.FullName).Unwrap();
                loader.LoadAssemblies(loadedAssemblies);
                this.Items.AddRange(this.Sort(loader.GetModuleInfos(this.ModulePath)));
            }
        }
        finally
        {
            AppDomain.Unload(childDomain);
        }
    }

    /// <summary>
    /// Sort modules according to dependencies and Priority
    /// </summary>
    /// <param name="modules">modules to sort</param>
    /// <returns>sorted modules</returns>
    protected override IEnumerable<ModuleInfo> Sort(IEnumerable<ModuleInfo> modules)
    {
        Dictionary<string, int> priorities = GetPriorities(modules);

        //call the base sort since it resolves dependencies, then re-sort 
        var result = new List<ModuleInfo>(base.Sort(modules));
        result.Sort((x, y) =>
        {
            string xModuleName = x.ModuleName;
            string yModuleName = y.ModuleName;

            //if one depends on other then non-dependent must come first
            //otherwise base on priority
            if (x.DependsOn.Contains(yModuleName))
                return 1; //x after y
            else if (y.DependsOn.Contains(xModuleName))
                return -1; //y after x
            else
                return priorities[xModuleName].CompareTo(priorities[yModuleName]);
        });

        return result;
    }

    /// <summary>
    /// Get the priorities
    /// </summary>
    /// <param name="modules"></param>
    /// <returns></returns>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom")]
    public Dictionary<string, int> GetPriorities(IEnumerable<ModuleInfo> modules)
    {
        //retrieve the priorities of each module, so that we can use them to override the 
        //sorting - but only so far as we don't mess up the dependencies
        var priorities = new Dictionary<string, int>();
        var assemblies = new Dictionary<string, Assembly>();

        foreach (ModuleInfo module in modules)
        {
            if (!assemblies.ContainsKey(module.Ref))
            {
                //LoadFrom should generally be avoided appently due to unexpected side effects,
                //but since we are doing all this in a separate AppDomain which is discarded
                //this needn't worry us
                assemblies.Add(module.Ref, Assembly.LoadFrom(module.Ref));
            }

            Type type = assemblies[module.Ref].GetExportedTypes()
                .Where(t => t.AssemblyQualifiedName.Equals(module.ModuleType, StringComparison.Ordinal))
                .First();

            var priorityAttribute =
                CustomAttributeData.GetCustomAttributes(type).FirstOrDefault(
                    cad => cad.Constructor.DeclaringType.FullName == typeof(PriorityAttribute).FullName);

            int priority;
            if (priorityAttribute != null)
            {
                priority = (int)priorityAttribute.ConstructorArguments[0].Value;
            }
            else
            {
                priority = 0;
            }

            priorities.Add(module.ModuleName, priority);
        }

        return priorities;
    }

    /// <summary>
    /// Local class to load assemblies into different appdomain which is then discarded
    /// </summary>
    private class ModulePriorityLoader : MarshalByRefObject
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        internal void LoadAssemblies(IEnumerable<string> assemblies)
        {
            foreach (string assemblyPath in assemblies)
            {
                try
                {
                    Assembly.ReflectionOnlyLoadFrom(assemblyPath);
                }
                catch (FileNotFoundException)
                {
                    // Continue loading assemblies even if an assembly can not be loaded in the new AppDomain
                }
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        internal ModuleInfo[] GetModuleInfos(string path)
        {
            DirectoryInfo directory = new DirectoryInfo(path);

            ResolveEventHandler resolveEventHandler =
                delegate(object sender, ResolveEventArgs args) { return OnReflectionOnlyResolve(args, directory); };

            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler;

            Assembly moduleReflectionOnlyAssembly =
                AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().First(
                    asm => asm.FullName == typeof(IModule).Assembly.FullName);

            Type IModuleType = moduleReflectionOnlyAssembly.GetType(typeof(IModule).FullName);

            IEnumerable<ModuleInfo> modules = GetNotAllreadyLoadedModuleInfos(directory, IModuleType);

            var array = modules.ToArray();
            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveEventHandler;

            return array;
        }

        private static IEnumerable<ModuleInfo> GetNotAllreadyLoadedModuleInfos(DirectoryInfo directory, Type IModuleType)
        {
            List<FileInfo> validAssemblies = new List<FileInfo>();
            Assembly[] alreadyLoadedAssemblies = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();

            var fileInfos = directory.GetFiles("*.dll")
                .Where(file => alreadyLoadedAssemblies
                                   .FirstOrDefault(
                                   assembly =>
                                   String.Compare(Path.GetFileName(assembly.Location), file.Name,
                                                  StringComparison.OrdinalIgnoreCase) == 0) == null);

            foreach (FileInfo fileInfo in fileInfos)
            {
                Assembly assembly = null;
                try
                {
                    assembly = Assembly.ReflectionOnlyLoadFrom(fileInfo.FullName);
                    validAssemblies.Add(fileInfo);
                }
                catch (BadImageFormatException)
                {
                    // skip non-.NET Dlls
                }
            }

            return validAssemblies.SelectMany(file => Assembly.ReflectionOnlyLoadFrom(file.FullName)
                                        .GetExportedTypes()
                                        .Where(IModuleType.IsAssignableFrom)
                                        .Where(t => t != IModuleType)
                                        .Where(t => !t.IsAbstract)
                                        .Select(type => CreateModuleInfo(type)));
        }

        private static Assembly OnReflectionOnlyResolve(ResolveEventArgs args, DirectoryInfo directory)
        {
            Assembly loadedAssembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault(
                asm => string.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase));
            if (loadedAssembly != null)
            {
                return loadedAssembly;
            }

            AssemblyName assemblyName = new AssemblyName(args.Name);
            string dependentAssemblyFilename = Path.Combine(directory.FullName, assemblyName.Name + ".dll");
            if (File.Exists(dependentAssemblyFilename))
            {
                return Assembly.ReflectionOnlyLoadFrom(dependentAssemblyFilename);
            }
            return Assembly.ReflectionOnlyLoad(args.Name);
        }

        private static ModuleInfo CreateModuleInfo(Type type)
        {
            string moduleName = type.Name;
            List<string> dependsOn = new List<string>();
            bool onDemand = false;
            var moduleAttribute =
                CustomAttributeData.GetCustomAttributes(type).FirstOrDefault(
                    cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleAttribute).FullName);

            if (moduleAttribute != null)
            {
                foreach (CustomAttributeNamedArgument argument in moduleAttribute.NamedArguments)
                {
                    string argumentName = argument.MemberInfo.Name;
                    switch (argumentName)
                    {
                        case "ModuleName":
                            moduleName = (string)argument.TypedValue.Value;
                            break;

                        case "OnDemand":
                            onDemand = (bool)argument.TypedValue.Value;
                            break;

                        case "StartupLoaded":
                            onDemand = !((bool)argument.TypedValue.Value);
                            break;
                    }
                }
            }

            var moduleDependencyAttributes =
                CustomAttributeData.GetCustomAttributes(type).Where(
                    cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleDependencyAttribute).FullName);

            foreach (CustomAttributeData cad in moduleDependencyAttributes)
            {
                dependsOn.Add((string)cad.ConstructorArguments[0].Value);
            }

            ModuleInfo moduleInfo = new ModuleInfo(moduleName, type.AssemblyQualifiedName)
            {
                InitializationMode =
                    onDemand
                        ? InitializationMode.OnDemand
                        : InitializationMode.WhenAvailable,
                Ref = type.Assembly.CodeBase,
            };
            moduleInfo.DependsOn.AddRange(dependsOn);

            return moduleInfo;
        }
    }
}

Now, in your Bootstrapper, you simply have to use your new ModuleCatalog:

protected override IModuleCatalog CreateModuleCatalog()
{
    return new PrioritizedDirectoryModuleCatalog { ModulePath = ".\Modules" };
}

Then, decorate each of your module with this new attribute:

[Module(ModuleName = "RootModule", OnDemand = false)]
[Priority(-1)]
public class RootModule : IModule

[Module(ModuleName = "ModuleA", OnDemand = true)]
[Priority(1)]
public class ModuleAModule : IModule

All that’s all ! If you enumerate all the modules, you will be able to see that they are ordered as you define it: RootModule will be the first, then, ModuleA (even if the sort on the name will produce a different result):

[Dependency]
public IModuleCatalog ModuleCatalog { get; set; }

[Dependency]
public IModuleManager ModuleManager { get; set; }

foreach (var module in this.ModuleCatalog.Modules.Where(m => m.InitializationMode != InitializationMode.WhenAvailable))
{
    this.ModuleManager.LoadModule(module.ModuleName);
}

 

Happy coding!

You can follow any responses to this entry through the RSS 2.0 You can leave a response, or trackback.

Add Comment Register



Leave a Reply