WPF

Crie aplicativos compostos tolerantes a falhas

Ivan Krivyakov

Baixar o código de exemplo

Há uma necessidade generalizada de aplicativos compostos, mas os requisitos de tolerância a falhas variam. Em alguns casos, pode não haver problema em um plug-in de ponto de falha único desativar todo o aplicativo. Em outros casos, isso é inaceitável. Neste artigo, descrevo uma arquitetura de um aplicativo de área de trabalho composto tolerante a falhas. Esta arquitetura proposta fornecerá um alto nível de isolamento executando cada plug-in em seu próprio processo do Windows. Eu a construo com os seguintes objetivos de design em mente:

  • Forte isolamento entre o host e os plug-ins
  • Integração visual completa de controles de plug-ins na janela do host
  • Fácil desenvolvimento de novos plug-ins
  • Conversão razoavelmente fácil de aplicativos existentes em plug-ins
  • Possibilidade de os plug-ins usarem os serviços fornecidos pelo host e vice-versa
  • Adição razoavelmente fácil de novos serviços e interfaces

O código-fonte acompanhante (msdn.microsoft.com/magazine/msdnmag0114) contém duas soluções do Visual Studio 2012: WpfHost.sln e Plugins.sln. Compile e host primeiro e depois compile os plug-ins. O principal arquivo executável é o WpfHost.exe. Os assemblies dos plug-ins são carregados sob demanda. A Figura 1 mostra o aplicativo concluído.

The Host Window Seamlessly Integrates with the Out-of-Process Plug-Ins
Figura 1 A janela do host integra-se perfeitamente com os plug-ins fora do processo

Visão geral da arquitetura

O host exibe um controle de guias e um botão “+” no canto superior esquerdo que mostra uma lista de plug-ins disponíveis. A lista de plug-ins é lida do arquivo XML denominado plugins.xml, mas implementações do catálogo alternativo são possíveis. Cada plug-in é executado em seu próprio processo e nenhum assembly de plug-in é carregado no host. Uma exibição de alto nível da arquitetura é mostrada na Figura 2.

A High-Level View of the Application Architecture
Figura 2 Exibição de alto nível da arquitetura do aplicativo

Internamente, o host de plug-ins é um aplicativo WPF (Windows Presentation Foundation) comum que segue o paradigma MVVM (Model-View-ViewModel). A parte do modelo é representada pela classe PluginController, que tem uma coleção de plug-ins carregados. Cada plug-in carregado é representado por uma instância da classe Plugin, que tem um controle de plug-in e se comunica com um processo de plug-in.

O sistema de hospedagem consiste em quatro assemblies, organizados conforme ilustrado na Figura 3.

The Assemblies of the Hosting System
Figura 3 Os assemblies do sistema de hospedagem

WpfHost.exe é o aplicativo host. PluginProcess.exe é o processo de plug-in. Uma instância desse processo carrega um plug-in. Wpf­Host.Interfaces.dll contém interfaces comuns usadas pelo host, o processo de plug-in e os plug-ins. PluginHosting.dll contém os tipos usados pelo host e o processo de plug-in para a hospedagem de plug-ins.

Carregar um plug-in envolve algumas chamadas que devem ser executadas no thread da IU e algumas chamadas que podem ser executadas em qualquer thread. Para tornar o aplicativo responsivo, eu bloqueio o thread da IU quando estritamente necessário. Portanto, a interface de programação para a classe Plugin é dividida em dois métodos, Load e CreateView:

class Plugin
{
  public FrameworkElement View { get; private set; }
  public void Load(PluginInfo info); // Can be executed on any thread
  public void CreateView();          // Must execute on UI thread
}

O método Plugin.Load inicia um processo de plug-in e cria a infraestrutura no lado do processo de plug-in. Ele é executado em um thread de trabalho. O método Plugin.CreateView conecta a exibição local ao FrameworkElement remoto. Você precisará executá-lo no thread da IU para evitar exceções como InvalidOperationException.

Por fim, a classe Plugin chama uma classe de plug-in definida pelo usuário dentro do processo de plug-in. O único requisito para essa classe de usuário é que ela implemente a interface IPlugin do assembly WpfHost.Interfaces:

public interface IPlugin : IServiceProvider, IDisposable
{
  FrameworkElement CreateControl();
}

O elemento de estrutura retornado pelo plug-in pode ter uma complexidade arbitrária. Pode ser uma caixa de texto individual ou um controle de usuário elaborado que implementa algum aplicativo LOB (linha de negócios).

A necessidade de aplicativos compostos

Nos últimos anos, vários clientes meus expressaram a mesma necessidade comercial: aplicativos de área de trabalho que possam carregar plug-ins externos, combinando, assim, vários aplicativos LOB sob o mesmo “teto”. O motivo por trás desse requisito pode variar. Várias equipes podem desenvolver partes diferentes do aplicativo em cronogramas diferentes. Usuários comerciais diferentes podem precisar de conjuntos de recursos diferentes. Ou talvez os clientes queiram garantir a estabilidade do aplicativo "principal" e, ao mesmo tempo, manter a flexibilidade. De um jeito ou de outro, o requisito de hospedar plug-ins de terceiros surgiu mais de uma vez em organizações diferentes.

Há várias soluções tradicionais para esse problema: o clássico CAB (Composite Application Block – bloco de aplicativo composto), o MAF (Managed Add-In Framework – estrutura de suplemento gerenciada), o MEF (Managed Extensibility Framework – estrutura de extensibilidade gerenciada) e Prism. Outra solução foi publicada na edição de agosto de 2013 do MSDN por meus ex-colegas Gennady Slobodsky e Levi Haskell (consulte o artigo “Arquitetura para a hospedagem de plug-ins do .NET de terceiros” em msdn.microsoft.com/magazine/dn342875). Todas essas soluções são de grande valia, e muitos aplicativos úteis foram criados por meio delas. Também sou um usuário ativo dessas estruturas, mas há um problema que continuou me perseguindo por um bom tempo: estabilidade.

Os aplicativos falham. A realidade é essa. Referências nulas, exceções não tratadas, arquivos bloqueados e bancos de dados corrompidos não vão desaparecer tão cedo. Um bom aplicativo host deve ser capaz de sobreviver à falha de um plug-in e continuar. Um plug-in com defeito não pode ter a capacidade de prejudicar o host ou outros plug-ins. Essa proteção não precisa ser à prova de balas. Não estou tentando impedir ataques de hackers mal-intencionados. No entanto, os erros simples, como uma exceção não tratada em um segmento de trabalho, não devem prejudicar o host.

Níveis de isolamento

Os aplicativos do Microsoft NET Framework podem lidar com plug-ins de terceiros de pelo menos três maneiras diferentes:

  • Sem isolamento: executar o host e todos os plug-ins em um único processo com um único AppDomain.
  • Isolamento médio: carregar cada plug-em seu próprio AppDomain.
  • Isolamento forte: carregar cada plug-em seu próprio processo.

Sem isolamento implica o mínimo de proteção e menos controle. Todos os dados são globalmente acessíveis, não há proteção contra falhas e não há maneira de descarregar o código incorreto. A causa mais comum da falha de um aplicativo é uma exceção não tratada em um thread de trabalho criado por um plug-in.

Você pode tentar proteger threads do host com blocos try/catch, mas quando se trata de threads criados por plug-ins, todas as apostas são inúteis. A partir do NET Framework. 2.0, uma exceção não tratada em qualquer thread encerra o processo, e você não pode impedir isso. Há uma boa razão para essa crueldade aparente: uma exceção não tratada significa que o aplicativo provavelmente tornou-se instável, e deixá-lo continuar é perigoso.

Isolamento médio fornece mais controle sobre a segurança e a configuração de um plug-in. Você também pode descarregar plug-ins, pelo menos quando as coisas estão indo bem e nenhum thread está ocupado executando código não gerenciado. No entanto, o processo de host ainda não está protegido contra falhas de plug-ins, como demonstrado em meu artigo “AppDomains não protegem o host de um plug-in com falha” (bit.ly/1fO7spO). Elaborar uma estratégia de tratamento de erros confiável é difícil, se não impossível, e o descarregamento do AppDomain não é garantido.

AppDomains foram inventados para hospedar aplicativos ASP.NET como alternativas leves para processos. Consulte a postagem no blog de Chris Brumme de 2003, “AppDomains (“domínios de aplicativos”)”, em bit.ly/PoIX1r. ASP.NET aplica uma abordagem relativamente prática à tolerância a falhas. Um aplicativo Web com falha pode facilmente prejudicar todo o processo de trabalho com vários aplicativos. Nesse caso, o ASP.NET simplesmente reinicia o processo de trabalho e emite novamente as solicitações da Web pendentes. Essa é uma decisão de design sensata para um processo de servidor sem janelas próprias voltadas para o usuário, mas pode não funcionar tão bem com um aplicativo de área de trabalho.

Isolamento forte fornece o nível máximo de proteção contra falhas. Como cada plug-in é executado em seu próprio processo, os plug-ins não podem prejudicar o host, e eles podem ser encerrados à vontade. Ao mesmo tempo, essa solução requer um design bastante complexo. O aplicativo tem de lidar com muita comunicação entre processos e sincronização. Ele também deve empacotar controles WPF entre limites de processos, o que não é trivial.

Assim como outros fatores no desenvolvimento de software, escolher um nível de isolamento é uma compensação. O isolamento mais forte lhe dá mais controle e mais flexibilidade, mas você paga por isso com o aumento da complexidade do aplicativo e desempenho mais lento.

Algumas estruturas optam por ignorar a tolerância a falhas e trabalham no nível "sem isolamento". MEF e Prism são bons exemplos dessa abordagem. Nos casos em que a tolerância a falhas e o ajuste preciso da configuração de plug-ins não são problemas, essa é a solução mais simples que funciona e é, portanto, a correta para usar.

Muitas arquiteturas de plug-in, inclusive a proposta por Slobodsky e Haskell, usam o isolamento médio. Elas obtêm isolamento via AppDomains. Os AppDomains dão aos desenvolvedores de host um grau de controle significativo sobre a configuração e segurança do plug-in. Eu, pessoalmente, criei uma série de soluções baseadas em AppDomain nos últimos anos. Se o aplicativo requer descarga de código, sandboxing e controle de configuração – e se a tolerância a falhas não é um problema – os AppDomains são definitivamente o caminho a seguir.

O MAF se destaca entre as estruturas complementares porque permite que os desenvolvedores de host escolham qualquer um dos três níveis de isolamento. Ele pode executar um suplemento em seu próprio processo usando a classe AddInProcess. Infelizmente, o AddInProcess não funciona para componentes visuais de imediato. Pode ser possível estender o MAF para empacotar componentes visuais entre processos, mas isso significaria a adição de mais uma camada a uma estrutura já complexa. Criar suplementos MAF não é fácil, e com outra camada sobre o MAF, a complexidade provavelmente se tornará incontrolável.

Minha arquitetura proposta visa preencher a lacuna e fornecer uma solução de hospedagem robusta que carregue plug-ins em seus próprios processos e forneça integração visual entre os plug-ins e o host.

Isolamento forte de componentes visuais

Quando é preciso carregar um plug-in, o processo de host gera um novo processo filho. Em seguida, o processo filho carrega uma classe de plug-in de usuário que cria um FrameworkElement exibido no host (veja a Figura 4).

Marshaling a FrameworkElement Between the Plug-In Process and the Host Process
Figura 4 Empacotamento de um FrameworkElement entre o processo de plug-In e o processo de host

O FrameworkElement não pode ser empacotado diretamente entre processos. Ele não herda do MarshalByRefObject, nem é marcado como [Serializable], então, a comunicação remota do .NET não o empacota. Ele não é marcado com o atributo [ServiceContract] e, portanto, o WCF (Windows Communication Foundation) não o empacota também. Para superar esse problema, eu uso a classe System.Addin.FrameworkElementAdapters do assembly System.Windows.Presentation que faz parte do MAF. Essa classe define dois métodos:

  • O método ViewToContractAdapter converte um FrameworkElement em uma interface INativeHandleContract, que pode ser empacotada com. a comunicação remota do .NET. Este método é chamado dentro do processo do plug-in.
  • O método ContractToViewAdapter reconverte uma instância INativeHandleContract para FrameworkElement. Esse método é chamado dentro do processo de host.

Infelizmente, a simples combinação desses dois métodos não funciona bem de imediato. Aparentemente, o MAF foi projetado para empacotar componentes WPF entre AppDomains e não entre processos. O método ContractToViewAdapter falha no lado do cliente com o seguinte erro:

System.Runtime.Remoting.RemotingException:
Permission denied: cannot call non-public or static methods remotely

A causa raiz é que o método ContractToViewAdapter chama o construtor da classe MS.Internal.Controls.AddInHost, que tenta converter o proxy de comunicação remota INativeHandleContract no tipo AddInHwndSourceWrapper. Se a conversão for bem-sucedida, isso chamará o método RegisterKeyboardInputSite interno sobre o proxy de comunicação remota. Não é permitido chamar métodos internos em proxies entre processos. Veja o que está acontecendo dentro do construtor da classe AddInHost:

// From Reflector
_addInHwndSourceWrapper = contract as AddInHwndSourceWrapper;
if (_addInHwndSourceWrapper != null)
{
  _addInHwndSourceWrapper.RegisterKeyboardInputSite(
    new AddInHostSite(this)); // Internal method call!
}

Para eliminar esse erro, criei a classe NativeContractInsulator. Essa classe reside na parte do servidor (plug-in). Ela implementa a interface INativeHandleContract encaminhando todas as chamadas para o INativeHandleContract original retornadas do método View­To­ContractAdapter. No entanto, ao contrário da implementação original, ela não pode ser convertida em AddInHwndSourceWrapper. Assim, a conversão na parte do cliente (host) não é bem-sucedida e a chamada de método interno proibida não ocorre.

Examinando a arquitetura de plug-ins com mais detalhes

Os métodos Plugin.Load e Plugin.CreateView criam todas as peças móveis necessárias para integração do plug-in.

A Figura 5 mostra o gráfico de objetos resultante. É um pouco complicado, mas cada parte é responsável por determinada função. Juntos, eles garantem o funcionamento perfeito e robusto do sistema de plug-ins host.

Object Diagram of a Loaded Plug-In
Figura 5 Diagrama de objetos de um plug-in carregado

A classe Plugin denota uma única instância de plug-in no host. Ela detém a propriedade View, que é a representação visual do plug-in dentro do processo de host. A classe Plugin cria uma instância de PluginProcessProxy e recupera dela um IRemotePlugin. IRemotePlugin contém um controle de plug-in remoto na forma de INativeHandleContract. Em seguida, a classe Plugin pega esse contrato e o converte em FrameworkElement conforme mostrado aqui (com parte do código elidida por brevidade):

public interface IRemotePlugin : IServiceProvider, IDisposable
{
  INativeHandleContract Contract { get; }
}
class Plugin
{
  public void CreateView()
  {
    View = FrameworkElementAdapters.ContractToViewAdapter(
      _remoteProcess.RemotePlugin.Contract);
  }}

A classe PluginProcessProxy controla o ciclo de vida do processo de plug-in dentro do host. Ela é responsável por iniciar o processo de plug-in, criando um canal de comunicação remota e monitorando a integridade do processo de plug-in. Também aciona o serviço PluginLoader, do qual recupera um IRemotePlugin.

A classe PluginLoader é executada dentro do processo de plug-in e implementa ciclo de vida do processo de plug-in. Ela estabelece um canal de comunicação remota, inicia um despachante de mensagem WPF, carrega um plug-in do usuário, cria uma instância RemotePlugin e entrega essa instância ao PluginProcessProxy no host.

A classe RemotePlugin torna o controle de plug-in do usuário empacotável entre os limites do processo. Ela converte o FrameworkElement do usuário em INativeHandleContract e envolve esse contrato com um NativeHandleContractInsulator para trabalhar nos problemas de chamadas de método ilegais descritos anteriormente.

Finalmente, a classe plug-in do usuário implementa a interface IPlugin. Sua principal tarefa é criar um controle de plug-in dentro do processo de plug-in. Normalmente, esse seria um UserControl WPF, mas pode ser qualquer FrameworkElement.

Quando é solicitado carregar o plug-in, a classe PluginProcessProxy gera um novo processo filho. O executável do processo filho é PluginProcess.exe ou PluginProcess64.exe, dependendo se o plug-in for de 32 bits ou de 64 bits. Cada processo de plug-in recebe um GUID exclusivo na linha de comando, bem como o diretório base de plug-ins:

PluginProcess.exe
  PluginProcess.0DAA530F-DCE4-4351-8D0F-36B0E334FF18
  c:\plug-in\assembly.dll

O processo de plug-in configura um serviço de comunicação remota do tipo IPluginLoader e lança um evento "ready" nomeado, neste caso, PluginProcess.0DAA530F-DCE4-4351-8D0F-36B0E334FF18.Ready. Em seguida, o host pode usar métodos IPluginLoader para carregar o plug-in.

Uma solução alternativa seria ter fazer o processo de plug-in chamar o host quando ele estiver pronto. Isso eliminaria a necessidade do evento ready, mas tornaria o tratamento de erros muito mais complicado. Se a operação "load plug-in" for oriunda do processo de plug-in, as informação do erro também serão retidas no processo de plug-in. Se algo der errado, talvez o host nunca descubra isso. Por isso, escolhi o design com o evento ready.

Outro problema de design era se eu deveria acomodar plug-ins não implantados no diretório host do WPF. Por um lado, no .NET Framework, carregar assemblies não situados dentro do diretório de aplicativos causa certas dificuldades. Por outro lado, reconheço que os plug-ins podem ter suas próprias preocupações de implantação, e nem sempre é possível implementar um plug-in no diretório host do WPF. Além disso, alguns aplicativos complexos não se comportam corretamente quando não são executados a partir de seus diretórios de base.

Devido a essas preocupações, o host do WPF permite carregar plug-ins de qualquer lugar no sistema de arquivos local. Para isso, o processo de plug-in executa praticamente todas as operações em um AppDomain secundário cujo diretório base de aplicativos é definido como o diretório base do plug-in. Isso gera o problema de carregamento de assemblies host do WPF nesse AppDomain. Isso pode ser conseguido pelo menos de quatro maneiras:

  • Colocar os assemblies host do WPF no GAC (Global Assembly Cache – cache de assembly global).
  • Usar redirecionamentos de assembly no arquivo app.config do processo de plug-in.
  • Carregar assemblies host do WPF usando uma das sobreposições de LoadFrom/CreateInstanceFrom.
  • Usar a API de hospedagem não gerenciada para iniciar o CLR no processo de plug-in com a configuração desejada.

Cada uma dessas soluções tem prós e contras. Colocar assemblies host do WPF no GAC requer acesso administrativo. Embora o GAC seja uma solução limpa, exigir direitos administrativos para instalação pode ser uma grande dor de cabeça em um ambiente corporativo, então, tentei evitar isso. Os redirecionamentos de assembly também são atraentes, mas os arquivos de configuração dependem da localização do host do WPF. Isso torna impossível uma instalação xcopy. Criar um projeto de hospedagem não gerenciado parecia ser um risco de grande manutenção.

Então, adotei a abordagem LoadFrom. A grande desvantagem dessa abordagem é que os assemblies host do WPF acabam no contexto LoadFrom (veja a postagem no blog “Como escolher o contexto de associação” de Suzanne Cook em bit.ly/cZmVuz). Para evitar problemas de associação, eu precisava substituir o evento AssemblyResolve no plug-in AppDomain para que o código do plug-in pudesse encontrar assemblies host do WPF mais facilmente.

Desenvolvendo plug-ins

Você pode implementar um plug-in como uma biblioteca de classes (DLL) ou um arquivo executável (EXE). No cenário DLL, as etapas são as seguintes:

  1. Criar um novo projeto de biblioteca de classes.
  2. Referenciar os assemblies PresentationCore, PresentationFramework, System.Xaml e WindowsBase do WPF.
  3. Adicionar uma referência ao assembly WpfHost.Interfaces. "copy local" deve estar definido como false.
  4. Criar um novo controle de usuário do WPF, como MainUserControl.
  5. Criar uma classe chamada Plugin derivada de IKriv.WpfHost.Interfaces.PluginBase.
  6. Adicionar uma entrada para o plug-in ao arquivo plugins.xml do host.
  7. Compilar o plug-in e executar o host.

Uma classe mínima de plug-in é semelhante a esta:

public class Plugin : PluginBase
{
  public override FrameworkElement CreateControl()
  {
    return new MainUserControl();
  }
}

Opcionalmente, um plug-in pode ser implementado como um executável. Nesse caso, as etapas são:

  1. Criar um aplicativo WPF.
  2. Criar um controle de usuário do WPF, por exemplo, MainUserControl.
  3. Adicionar MainUserControl à janela principal do aplicativo.
  4. Adicionar uma referência ao assembly WpfHost.Interfaces. "copy local" deve estar definido como false.
  5. Criar uma classe chamada Plugin derivada de IKriv.WpfHost.Interfaces.PluginBase.
  6. Adicionar uma entrada do plug-in ao arquivo plugins.xml do host.

A classe de plug-in seria exatamente igual à do exemplo anterior, e o XAML da janela principal deve conter nada além de uma referência a MainUserControl:

<Window x:Class="MyPlugin.MainWindow"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:MyProject"
  Title="My Plugin" Height="600" Width="766" >
  <Grid>
    <local:MainUserControl />
  </Grid>
</Window>

Um plug-in implementado como este pode ser executado como aplicativo autônomo ou dentro do host. Isso simplifica a depuração do código do plug-in não relacionado à integração com o host. O diagrama de classes para esse plug-in de “duas cabeças” é ilustrado na Figura 6.

The Class Diagram for a Dual-Head Plug-In
Figura 6 Diagrama de classes de um plug-in de duas cabeças

Essa técnica também fornece um meio para a conversão rápida de aplicativos existentes em plug-ins. A única coisa que você precisa fazer é converter a janela principal do aplicativo em um controle de usuário. Depois, instancie esse controle de usuário em uma classe de plug-in, como demonstrado anteriormente. O plug-in Solar System no download do código acompanhante é um exemplo dessa conversão. O processo de conversão inteiro levou menos de uma hora.

Como o plug-in não é um aplicativo independente, e sim iniciado pelo host, a depuração pode não ser simples. Você pode iniciar a depuração do host, mas o Visual Studio ainda não pode anexar a processos filhos automaticamente. Você pode anexar manualmente o depurador ao processo de plug-in quando ele estiver em execução ou fazer com que o processo de plug-in entre no depurador na inicialização, alterando a linha 4 do app.config do PluginProcess para:

<add key="BreakIntoDebugger" value="True" />

Outra alternativa é criar seu plug-in como aplicativo autônomo, conforme descrito anteriormente. Você pode, então, depurar a maior parte do plug-in como aplicativo autônomo, apenas verificando periodicamente se a integração com o host do WPF funciona corretamente.

Se o processo de plug-in entrar no depurador na inicialização, você pode aumentar o tempo limite do evento ready alterando a linha 4 do arquivo app.config do WpfHost da seguinte forma:

<add key="PluginProcess.ReadyTimeoutMs" value="500000" />

A Figura 7 mostra uma lista de exemplos de plug-ins disponíveis no download do código acompanhante e as descrições do que eles fazem.

Figura 7 Exemplos de plug-ins disponíveis no download do código acompanhante

Projeto de plug-in O que ele faz
BitnessCheck Demonstra como um plug-in pode ser executado como 32 bits ou 64 bits
SolarSystem Demonstra um antigo aplicativo de demonstração WPF convertido em plug-in
TestExceptions Demonstra o tratamento de exceção para thread de usuário e exceções de thread de trabalho
UseLogServices Demonstra o uso de serviços de host e serviços de plug-in

Serviços de host e de plug-in

No mundo real, os plug-ins muitas vezes precisam usar os serviços fornecidos pelo host. Eu demonstrar esse caso no plug-in UseLogService no download do código. Uma classe de plug-in pode ter um construtor padrão ou um construtor que recebe um parâmetro do tipo IWpfHost. Nesse último caso, o carregador de plug-in passará uma instância do host do WPF para o plug-in. A interface IWpfHost é definida da seguinte forma:

public interface IWpfHost : IServiceProvider
{
  void ReportFatalError(string userMessage,
     string fullExceptionText);
  int HostProcessId { get; }
}

Eu uso a parte IServerProvider no meu plug-in. IServiceProvider é uma interface padrão do .NET Framework definida em mscorlib.dll:

public interface IServiceProvider
{
  object GetService(Type serviceType);
}

Vou usá-la no meu plug-in para obter o serviço ILog do host:

class Plugin : PluginBase
{
  private readonly ILog _log;
  private MainUserControl _control;
  public Plugin(IWpfHost host)
  {
    _log = host.GetService<ILog>();
  }
  public override FrameworkElement CreateControl()
  {
    return new MainUserControl { Log = _log };
  }
}

O controle pode usar o serviço de host ILog para gravar no arquivo log do host.

O host também pode usar os serviços fornecidos por plug-ins. Eu defini um desse serviço denominado IUnsavedData, que provou ser útil na vida real. Ao implementar essa interface, um plug-in pode definir uma lista de itens de trabalho não salvos. Se o plug-in ou todo o aplicativo host for fechado, o host perguntará ao usuário se ele quer abandonar os dados não salvos, como ilustrado na Figura 8.

Using the IUnsavedData Service
Figura 8 Usando o serviço IUnsavedData

A interface IUnsavedData é definida da seguinte forma:

public interface IUnsavedData
{
  string[] GetNamesOfUnsavedItems();
}

O autor de um plug-in não precisa implementar a interface IServiceProvider explicitamente. É o suficiente implementar a interface IUnsavedData no plug-in. O método PluginBase.GetService se encarregará de devolvê-lo ao host. Meu projeto UseLogService no download do código fornece uma implementação de exemplo de IUnsavedData, com o código relevante mostrado aqui:

class Plugin : PluginBase, IUnsavedData
{
  private MainUserControl _control;
  public string[] GetNamesOfUnsavedItems()
  {
    if (_control == null) return null;
    return _control.GetNamesOfUnsavedItems();
  }
}

Registro e tratamento de erros

Os processos de host e plug-in do WPF criam logs no diretório %TMP%\WpfHost. O host do WPF grava no WpfHost.log e cada processo de host de plug-in é gravado no PluginProcess.Guid.log (o "GUID" não faz parte do nome literal, mas é ampliado para o valor real de GUID). O serviço de log é feito sob medida. Evitei o uso de serviços de log populares, como log4net ou NLog, para tornar a amostra autossuficiente.

Um processo de plug-in também grava os resultados na janela do console, que você pode mostrar alterando a linha 3 do app.config do WpfHost para:

<add key="PluginProcess.ShowConsole" value="True" />

Tomei muito cuidado para relatar todos os erros ao host e tratá-los com inteligência. O host monitora os processos de plug-in e fecha a janela do plug-in quando um processo de plug-in morre. Da mesma forma, um processo de plug-in monitora seu host e fecha quando o host morre. Todos os erros são registrados em log. Então, examinar os arquivos de log ajuda muito na solução de problemas.

É importante lembrar que tudo que é passado entre o host e os plug-ins deve ser [Serializable] ou do tipo derivado de MarshalByRefObject. Caso contrário, a comunicação remota do .NET não poderá empacotar o objeto entre as partes. Os tipos e interfaces devem ser conhecidos por ambas as partes, de modo que normalmente só tipos internos e tipos de assemblies WpfHost.Interfaces ou PluginHosting são seguros para o empacotamento.

Controle de versão

WpfHost.exe, PluginProcess.exe e PluginHosting.dll são fortemente integrados e devem ser lançados simultaneamente. Felizmente, o código do plug-in não depende de qualquer um desses assemblies e, portanto, eles podem ser modificados de quase qualquer forma. Por exemplo, você pode facilmente alterar o mecanismo de sincronização ou o nome do evento ready sem afetar os plug-ins.

O componente WpfHost.Interfaces.dll deve ter a versão controlada com extremo cuidado. Ele deve ser referenciado, mas não incluído no código do plug-in (CopyLocal = false), logo, o binário desse assembly sempre vem apenas do host. Eu não dei a esse assembly um nome forte porque eu não quero especificamente a execução lado a lado. Apenas uma versão de WpfHost.Interfaces.dll deve estar presente em todo o sistema.

Geralmente, você deve considerar os plug-ins como código de terceiros que não está sob o controle dos autores do host. Modificar ou até mesmo recompilar todos os plug-ins de uma vez pode ser difícil ou impossível. Por isso, as novas versões do assembly de interface deve ser binariamente compatível com as versões anteriores, com o número de alterações de quebra mantido a um mínimo absoluto.

A adição de novos tipos e interfaces ao assembly é geralmente segura. Quaisquer outras modificações, incluindo a adição de novos métodos a interfaces ou novos valores a enums, podem afetar a compatibilidade binária e devem ser evitadas.

Mesmo que os assemblies de hospedagem não tenham nomes fortes, é importante incrementar os números de versão após qualquer alteração, por menor que seja, para que não haja dois assemblies com o mesmo número de versão e código diferente.

Um bom ponto de partida

Minha arquitetura de referência fornecida aqui não é uma estrutura de qualidade de produção para a integração host/plug-in, mas chega bem perto e pode servir como um ponto de partida valioso para seu aplicativo.

A arquitetura se encarrega de considerações típicas, porém difíceis, como o ciclo de vida do processo de plug-in, empacotamento de controles de plug-in entre processos, mecanismo de troca e descoberta de serviços entre o host e os plug-ins, entre outros. A maioria das soluções de design e alternativas não são arbitrárias. Eles são baseadas na experiência real em criação de aplicativos para o WPF.

Muito provavelmente você vai querer modificar a aparência visual do host, substituir o mecanismo de log pelo padrão usado em sua empresa, adicionar novos serviços e, eventualmente, alterar a forma como os plug-ins são descobertos. Muitas outras modificações e melhorias são possíveis.

Mesmo se você não criar aplicativos compostos para o WPF, você ainda pode desfrutar da análise desta arquitetura como uma demonstração de quão poderoso e flexível o .NET Framework pode ser e como você pode combinar componentes conhecidos de uma maneira interessante, inesperada e produtiva.

Ivan Krivyakov é um líder técnico na Thomson Reuters. Ele é um desenvolvedor e arquiteto prático especializado na criação e melhoria de aplicativos LOB (linha de negócios) completos do Windows Presentation Foundation.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Dr. James McCaffrey, Daniel Plaisted e Kevin Ransom
Kevin Ransom trabalhou na Microsoft por 14 anos em inúmeros projetos, incluindo: Common Language Runtime, Microsoft Business Framework, Windows Vista e Windows 7, Managed Extensibility Framework e Base Class Libraries. Atualmente ele trabalha com linguagens gerenciadas na Visual FSharp.

Dr. James McCaffrey trabalha para a Microsoft no campus de Redmond, Washington. Ele trabalhou em vários produtos da Microsoft, incluindo Internet Explorer e MSN Search. Ele é o autor do livro “.NET Test Automation Recipes” (Apress, 2006) e pode ser contatado pelo email jammc@microsoft.com.

Desde que entrou para a Microsoft em 2008, Daniel Plaisted já trabalhou nos produtos Managed Extensibility Framework (MEF), Portable Class Libraries (PCL) e Microsoft .NET Framework para aplicativos da Windows Store. Ele já se apresentou no MS TechEd, BUILD e vários grupos locais, campos de códigos e conferências. Em seu tempo livre, ele adora jogos de computador, leitura, caminhadas, malabarismo e futebol (jogar uma pelada). O blog dele pode ser encontrado na página blogs.msdn.com/b/dsplaisted/ e ele pode ser contatado pelo email daplaist@microsoft.com."