Ir para o conteúdo
Prism: Descubra e carregue dinamicamente módulos em tempo de execução

Prism: Descubra e carregue dinamicamente módulos em tempo de execução

Se você desenvolve aplicativos WPF com o Prism, provavelmente já está ciente das muitas maneiras pelas quais pode carregar um módulo.

14min read

O carregamento de um módulo começa com o que é chamado de ModuleCatalog.  Você não pode carregar um módulo a menos que ele tenha sido adicionado a um ModuleCatalog.  Depois que o módulo for adicionado a um ModuleCatalog, o Prism se encarregará de carregar o assembly do módulo para você.  O Prism ainda vem com um punhado de catálogos de módulos para oferecer flexibilidade na forma como você registra seus módulos com seu aplicativo Prism.  Você pode preencher um catálogo de módulos do código, do XAML, com XML em um app.config ou de um diretório.  Caramba, você pode até usar uma combinação de todas essas opções para preencher seu catálogo de módulos.

Quando estou dando uma palestra do Prism em um evento público ou um almoço interno e aprendo em uma empresa, tenho certeza de explicar todas as diferentes maneiras de carregar seus módulos e qual catálogo usar.  Este é o momento em que as perguntas realmente começam a ficar interessantes.  Dessas perguntas, a mais comum é sobre o DirectoryModuleCatalog.  Esse catálogo específico permite que você especifique um caminho de pasta para carregar seus módulos.  Agora a pergunta interessante ... "Mas, o que acontece quando um novo assembly de módulo é colocado na pasta?  Ele será carregado automaticamente no aplicativo enquanto estiver em execução?"  Essa é uma ótima pergunta, e a resposta é NÃO. O DirectoryModuleCatalog faz uma verificação única do diretório e, em seguida, carrega todos os módulos encontrados.  Se você soltar um novo assembly de módulo no diretório, ele não será carregado até que o aplicativo seja reiniciado.  Agora, a pergunta de acompanhamento... "Bem, é possível descobrir dinamicamente os módulos e carregá-los do diretório também?"  Responder; bem, claro que é.  Se você estiver usando o MEF, é fácil.  Se você estiver usando um contêiner como o Unity, precisará escrever o código para lidar com isso sozinho.  "Bem, nós não usamos MEF, então você pode nos mostrar como?"  É aqui que minha resposta é sempre a mesma, "uma simples pesquisa na web (Google ou Bing) deve ajudá-lo a encontrar o que procura".

Bem, acontece que esse não é o caso.  Parece que ninguém blogou ou compartilhou qualquer código que lide com a descoberta dinâmica e o carregamento de módulos usando um contêiner DI como o Unity.  Não que eu pudesse encontrar, nem qualquer um que estivesse me pedindo para mostrá-los pudesse encontrar.  O que me leva a este post.  Vou mostrar uma abordagem que usei para apoiar esse cenário.  Na verdade, vou dar-lhe duas abordagens.  Um é o caminho "rápido e sujo".  Basicamente, vou reunir a amostra mais simples para atingir o objetivo.  Em seguida, mostrarei "A Better Way", no qual encapsularemos essa funcionalidade em um ModuleCatalog personalizado que cuidará de tudo para nós.

Aqui está o aplicativo Prism que estamos usando para testar nosso código.

Descobrir e carregar módulos dinamicamente em tempo de execução - Solução

É um aplicativo Prism que contém um Shell com uma única região e um módulo que contém uma única visualização.  Quando o módulo for carregado corretamente, este será o resultado final.

Descobrir e carregar dinamicamente módulos em tempo de execução - Resultado

Feito nas coxas

O jeito "Rápido e Sujo", é bem.... Feito nas coxas.  Primeiro, precisamos determinar qual mecanismo usaremos para detectar quando um novo assembly de módulo foi adicionado ao nosso diretório de módulos.  Este é um acéfalo.  Usaremos a classe FileSystemWatcher.  O FileSystemWatcher monitora um determinado diretório em busca de alterações e nos notifica por meio de eventos que uma alteração ocorreu.  Como um arquivo sendo adicionado ao diretório.  Vamos criar uma instância dessa classe em nosso construtor Bootstrapper e ouvir seu evento Created.

public Bootstrapper()
{
    // we need to watch our folder for newly added modules
    FileSystemWatcher fileWatcher = new FileSystemWatcher(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "Modules"), "*.dll");
    fileWatcher.Created += fileWatcher_Created;
    fileWatcher.EnableRaisingEvents = true;
}

Observe que no construtor do FileSystemWatcher, ele usa o local do diretório que queremos monitorar, bem como um segundo parâmetro que nos permite especificar um filtro.  Nesse caso, nos preocupamos apenas com DLLs.  Também precisamos definir o FileSystemWatcher.EnableRaisingEvents = true para iniciar o monitoramento do diretório.  Agora, sempre que uma nova DLL for adicionada ao nosso diretório, nosso manipulador de eventos será executado.  Hora de conferir nosso manipulador de eventos

void fileWatcher_Created(object sender, FileSystemEventArgs e)
{
    if (e.ChangeType == WatcherChangeTypes.Created)
    {
        //get the Prism assembly that IModule is defined in
        Assembly moduleAssembly = AppDomain.CurrentDomain.GetAssemblies().First(asm => asm.FullName == typeof(IModule).Assembly.FullName);
        Type IModuleType = moduleAssembly.GetType(typeof(IModule).FullName);

        //load our newly added assembly
        Assembly assembly = Assembly.LoadFile(e.FullPath);

        //look for all the classes that implement IModule in our assembly and create a ModuleInfo class from it
        var moduleInfos = assembly.GetExportedTypes()
            .Where(IModuleType.IsAssignableFrom)
            .Where(t => t != IModuleType)
            .Where(t => !t.IsAbstract).Select(t => CreateModuleInfo(t));


        //create an instance of our module manager
        var moduleManager = Container.Resolve<IModuleManager>();

        foreach (var moduleInfo in moduleInfos)
        {
            //add the ModuleInfo to the catalog so it can be loaded
            ModuleCatalog.AddModule(moduleInfo);

            //now load the module using the Dispatcher because the FileSystemWatcher.Created even occurs on a separate thread
            //and we need to load our module into the main thread.
            var d = Application.Current.Dispatcher;
            if (d.CheckAccess())
                moduleManager.LoadModule(moduleInfo.ModuleName);
            else
                d.BeginInvoke((Action)delegate { moduleManager.LoadModule(moduleInfo.ModuleName); });
        }
    }
}

private static ModuleInfo CreateModuleInfo(Type type)
{
    string moduleName = type.Name;

    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;
            if (argumentName == "ModuleName")
            {
                moduleName = (string)argument.TypedValue.Value;
                break;
            }
        }
    }

    ModuleInfo moduleInfo = new ModuleInfo(moduleName, type.AssemblyQualifiedName)
    {
        InitializationMode = InitializationMode.OnDemand,
        Ref = type.Assembly.CodeBase,
    };

    return moduleInfo;
}

Esse código pega o assembly recém-adicionado e o carrega em nosso aplicativo.  Em seguida, pesquisamos o assembly para todas as classes que implementam IModule, que é a interface que especifica que representa um módulo Prism.  Em seguida, percorremos todos os módulos encontrados e os adicionamos ao ModuleCatalog.  Temos que fazer isso porque não podemos carregar um módulo que não esteja registrado no catálogo de módulos.  Agora usamos o IModuleManager para carregar o módulo usando o Dispatcher.  Temos que usar o Dispatcher porque o evento FileSystemWatcher.Created escuta em um thread separado e precisamos carregar os módulos no thread principal.  Os Dispatchers nos permitem enviar os módulos de um thread diferente para o thread principal.  Agora vamos em frente e executar o aplicativo e copiar o ModuleA.DLL para o diretório da pasta Módulos do aplicativo e ver o que acontece.

Before:

Descubra e carregue módulos dinamicamente em tempo de execução - antes

Execute o aplicativo e abra o local do diretório /Modules do aplicativo e o local do arquivo Bin/Debug/ModuleA.dll do ModuleA.  Como você pode ver, não há módulos carregados para o aplicativo e o aplicativo Prism está mostrando um shell vazio.

After:

Descobrir e carregar módulos dinamicamente em tempo de execução - Depois

Agora, copie o ModuleA.dll do diretório Bin/Debug do módulo para o diretório /Modules do aplicativo Prism.  Assim que a operação de cópia for concluída, o assembly ModuleA.dll é carregado e o ModuleAView é injetado no Shell; .  Tudo isso enquanto o aplicativo está em execução.  Não há necessidade de desligar e reiniciar o aplicativo.

Então essa foi a maneira rápida e suja.  Agora vamos ver como podemos criar um ModuleCatalog personalizado que não apenas carregará módulos de um diretório exatamente como o Prism DirectoryModuleCatalog padrão faz, mas também nos permitirá monitorar o diretório para módulos recém-adicionados em tempo de execução.

A Better Way

Acabamos de ver como, com algumas linhas de código, poderíamos descobrir e carregar módulos dinamicamente.  Agora, vamos criar uma classe ModuleCatalog personalizada que não apenas registrará e carregará módulos existentes de um diretório, mas também monitorará esse mesmo diretório para módulos recém-adicionados em tempo de execução.  Essa classe deve ser um pouco mais estável e fazer criações adequadas de evidências e domínio do aplicativo e reflexão na memória sem carregar os assemblies no domínio do aplicativo principal até que seja realmente necessário.  Também removeremos a dependência do Dispatcher e, em vez disso, usaremos a classe SynchronizationContext.  Não vou percorrer todo o código.  Vou apenas fornecer o código e você pode lê-lo.

public class DynamicDirectoryModuleCatalog : ModuleCatalog
{
    SynchronizationContext _context;

    /// <summary>
    /// Directory containing modules to search for.
    /// </summary>
    public string ModulePath { get; set; }

    public DynamicDirectoryModuleCatalog(string modulePath)
    {
        _context = SynchronizationContext.Current;

        ModulePath = modulePath;

        // we need to watch our folder for newly added modules
        FileSystemWatcher fileWatcher = new FileSystemWatcher(ModulePath, "*.dll");
        fileWatcher.Created += FileWatcher_Created;
        fileWatcher.EnableRaisingEvents = true;
    }

    /// <summary>
    /// Rasied when a new file is added to the ModulePath directory
    /// </summary>
    void FileWatcher_Created(object sender, FileSystemEventArgs e)
    {
        if (e.ChangeType == WatcherChangeTypes.Created)
        {
            LoadModuleCatalog(e.FullPath, true);
        }
    }

    /// <summary>
    /// Drives the main logic of building the child domain and searching for the assemblies.
    /// </summary>
    protected override void InnerLoad()
    {
        LoadModuleCatalog(ModulePath);
    }

    void LoadModuleCatalog(string path, bool isFile = false)
    {
        if (string.IsNullOrEmpty(path))
            throw new InvalidOperationException("Path cannot be null.");

        if (isFile)
        {
            if (!File.Exists(path))
                throw new InvalidOperationException(string.Format("File {0} could not be found.", path));
        }
        else
        {
            if (!Directory.Exists(path))
                throw new InvalidOperationException(string.Format("Directory {0} could not be found.", path));
        }

        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(InnerModuleInfoLoader);
            if (loaderType.Assembly != null)
            {
                var loader = (InnerModuleInfoLoader)childDomain.CreateInstanceFrom(loaderType.Assembly.Location, loaderType.FullName).Unwrap();
                loader.LoadAssemblies(loadedAssemblies);

                //get all the ModuleInfos
                ModuleInfo[] modules = loader.GetModuleInfos(path, isFile);

                //add modules to catalog
                this.Items.AddRange(modules);

                //we are dealing with a file from our file watcher, so let's notify that it needs to be loaded
                if (isFile)
                {
                    LoadModules(modules);
                }
            }
        }
        finally
        {
            AppDomain.Unload(childDomain);
        }
    }

    /// <summary>
    /// Uses the IModuleManager to load the modules into memory
    /// </summary>
    /// <param name="modules"></param>
    private void LoadModules(ModuleInfo[] modules)
    {
        if (_context == null)
            return;

        IModuleManager manager = ServiceLocator.Current.GetInstance<IModuleManager>();

        _context.Send(new SendOrPostCallback(delegate(object state)
        {
            foreach (var module in modules)
            {
                manager.LoadModule(module.ModuleName);
            }
        }), null);
    }

    /// <summary>
    /// Creates a new child domain and copies the evidence from a parent domain.
    /// </summary>
    /// <param name="parentDomain">The parent domain.</param>
    /// <returns>The new child domain.</returns>
    /// <remarks>
    /// Grabs the <paramref name="parentDomain"/> evidence and uses it to construct the new
    /// <see cref="AppDomain"/> because in a ClickOnce execution environment, creating an
    /// <see cref="AppDomain"/> will by default pick up the partial trust environment of
    /// the AppLaunch.exe, which was the root executable. The AppLaunch.exe does a
    /// create domain and applies the evidence from the ClickOnce manifests to
    /// create the domain that the application is actually executing in. This will
    /// need to be Full Trust for Composite Application Library applications.
    /// </remarks>
    /// <exception cref="ArgumentNullException">An <see cref="ArgumentNullException"/> is thrown if <paramref name="parentDomain"/> is null.</exception>
    protected virtual AppDomain BuildChildDomain(AppDomain parentDomain)
    {
        if (parentDomain == null) throw new System.ArgumentNullException("parentDomain");

        Evidence evidence = new Evidence(parentDomain.Evidence);
        AppDomainSetup setup = parentDomain.SetupInformation;
        return AppDomain.CreateDomain("DiscoveryRegion", evidence, setup);
    }

    private class InnerModuleInfoLoader : MarshalByRefObject
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        internal ModuleInfo[] GetModuleInfos(string path, bool isFile = false)
        {
            Assembly moduleReflectionOnlyAssembly =
                AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().First(
                    asm => asm.FullName == typeof(IModule).Assembly.FullName);

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

            FileSystemInfo info = null;
            if (isFile)
                info = new FileInfo(path);
            else
                info = new DirectoryInfo(path);

            ResolveEventHandler resolveEventHandler = delegate(object sender, ResolveEventArgs args) { return OnReflectionOnlyResolve(args, info); };
            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler;
            IEnumerable<ModuleInfo> modules = GetNotAllreadyLoadedModuleInfos(info, IModuleType);
            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveEventHandler;

            return modules.ToArray();
        }

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

            FileInfo fileInfo = info as FileInfo;
            if (fileInfo != null)
            {
                if (alreadyLoadedAssemblies.FirstOrDefault(assembly => String.Compare(Path.GetFileName(assembly.Location), fileInfo.Name, StringComparison.OrdinalIgnoreCase) == 0) == null)
                {
                    var moduleInfos = Assembly.ReflectionOnlyLoadFrom(fileInfo.FullName).GetExportedTypes()
                    .Where(IModuleType.IsAssignableFrom)
                    .Where(t => t != IModuleType)
                    .Where(t => !t.IsAbstract).Select(t => CreateModuleInfo(t));

                    return moduleInfos;
                }
            }

            DirectoryInfo directory = info as DirectoryInfo;

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

            foreach (FileInfo file in files)
            {
                try
                {
                    Assembly.ReflectionOnlyLoadFrom(file.FullName);
                    validAssemblies.Add(file);
                }
                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, FileSystemInfo info)
        {
            Assembly loadedAssembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault(
                asm => string.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase));
            if (loadedAssembly != null)
            {
                return loadedAssembly;
            }

            DirectoryInfo directory = info as DirectoryInfo;
            if (directory != null)
            {
                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);
        }

        [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
                }
            }
        }

        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;
        }
    }
}

/// <summary>
/// Class that provides extension methods to Collection
/// </summary>
public static class CollectionExtensions
{
    /// <summary>
    /// Add a range of items to a collection.
    /// </summary>
    /// <typeparam name="T">Type of objects within the collection.</typeparam>
    /// <param name="collection">The collection to add items to.</param>
    /// <param name="items">The items to add to the collection.</param>
    /// <returns>The collection.</returns>
    /// <exception cref="System.ArgumentNullException">An <see cref="System.ArgumentNullException"/> is thrown if <paramref name="collection"/> or <paramref name="items"/> is <see langword="null"/>.</exception>
    public static Collection<T> AddRange<T>(this Collection<T> collection, IEnumerable<T> items)
    {
        if (collection == null) throw new System.ArgumentNullException("collection");
        if (items == null) throw new System.ArgumentNullException("items");

        foreach (var each in items)
        {
            collection.Add(each);
        }

        return collection;
    }
}

É assim que você usaria o recém-criado DynamicDirectoryModuleCatalog em nosso Prism Bootstrapper.

protected override IModuleCatalog CreateModuleCatalog()
{
    DynamicDirectoryModuleCatalog catalog = new DynamicDirectoryModuleCatalog(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "Modules"));
    return catalog;
}
protected override IModuleCatalog CreateModuleCatalog()
{
    DynamicDirectoryModuleCatalog catalog = new DynamicDirectoryModuleCatalog(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, “Modules”));
    return catalog;
}

Você pode não saber disso, mas pode até fazer com que várias instâncias do seu aplicativo Prism monitorem o mesmo diretório e carreguem os mesmos módulos.

Descubra e carregue módulos dinamicamente em tempo de execução - vários aplicativos

Muito legal né?  Agora você pode descobrir e carregar dinamicamente seus módulos do Prism em tempo de execução.

Baixe o código-fonte

Como sempre, sinta-se à vontade para entrar em contato comigo no meu blog, conecte-se comigo no Twitter (@brianlagunas) ou deixe um comentário abaixo para quaisquer perguntas ou comentários que possa ter.

WPF
Solicite uma demonstração