Article Details                  
 
Programação COM em .Net - parte V

Apresenta, de uma forma resumida, dois conceitos importantes: exportação de tipos .Net para COM e processamento de eventos em COM

Programação COM em .Net - parte V


Autor: Luís Abreu

Conteúdo: Continuação do último item da série e apresentação de várias ideias relativas ao desenvolvimento de controlos ActiveX

Ferramentas: Visual Studio 2003


Na sequência desta saga (que têm sido estes artigos sobre COM), cá estou eu para uno novo episódio. Hoje o "prato do dia" consiste em aprofundarmos alguns conceitos fundamentais ao desenvolvimento de componentes .Net para serem consumidos em COM . Este artigo irá apresentar alguns aspectos importantes que nos permitirão construir um ActiveX utilizando C# (algo que será feito no próximo artigo da série). Bom, o caminho é longo, pelo que é melhor avançarmos...


Exportação de tipos


Como vimos no último artigo, a utilização de um componente .Net numa aplicação COM é feita através de um wrapper. Esse wrapper serve de ponte entre a aplicação COM e o componente .Net. Como seria de esperar, o interface do wrapper depende do interface definifo pelo componente .Net. Portanto, para construirmos componentes COm através de .Net é necessário termos pelo menos noções sobre as transformações que são efectuadas e encapsuladas no wrapper.


Em COM os tipos estão descritos numa type library. As type libs não são mais do que ficheiros que contémuma descrição (mais ou menos) completa dos vários interfaces/objectos contidos numa dll com. Por outro lado, em .Net a definição dos tipos está contida no próprio assembly. Por isso não deve ser supresa para ninguém, se eu disser que um assembly tem de ser convertidonuma type lib para ser consumido por um objecto COM (como vimos no último artigo, este tipo de transformação pode ser feito através da ferramenta regasm). Uma type library é identificada através de três itens: GUID também conhecido por Library Id ou LIBID), um LCID (Locale Identifier - este elemento é opcional) e versão. Estes valores são obtidos das definições existentes no assembly. Assim, o LIBID é obtido através da combinação do nome do assembly e da chave pública deste. Podemos personalizar este valor através aplicando oatributo GuidAttribute ao assembly.


Como é óbvio, a versão da type lib é obtida a partir da versão definida no assembly ( a única observação relevante aqui prende-se com o facto de apenas serem aproveitas os valores referentes às partes major e minor da versão - ex.: se o nosso assembly possuir a versão 1.1.2.3, então a type lib será anotada com a versão 1.1). Finalmente, o LCID, que é opcional, também é obtido a partir do assembly, caso este contenha essa informação. Se tal não acontecer, então será utilizado o LCID 0.


A exportação de namespaces poderá trazer alguma surpresa devido ao facto deste tipo de exportação...simplesmente não existir! Por outras palavras, se um tipo estiver contido no interior de um namespace, apenas o nome do tipo será importado para a type lib (ex.: o tipo namespace.class será apenas exportado como class para a type lib).


Por defeito, todas as classes públicas são exportadas para a type lib sob a forma de coclasses. Estas serão (como sempre acontece no COM) anotadas com um determinado GUID. Todas as classes abstractas e/ou sem construtor por defeito público serão também anotadas com o atributo noncreatable (de forma a que se saiba que estas classes não devem ser criadas). Eventuais heranças existentes não serão propagas para a type lib pois não são suportadas a nível das coclasses. Assim, as coclasses irão expor todos os interfaces por si implementados. Para além disso, no caso das heranças de classes, uma classe irá também expor eventuais interfaces implementadas pela classe base (se não houver interfaces explicitos, então será gerado um que irá conter todos os métodos/propriedades públicas - mais informações sobre esta geração automática podem ser encontradas no artigo anterior).


A exportação de interfaces é feita de forma (mais ou menos) transparente. Com isto quero dizer que os interfaces são transformados em interfaces COM na type lib. É possível controlar o tipo de interface através do atributo ComInterfaceType (que foi apresentado no último artigo). A herança de interfaces é exportada de forma ligeiramente diferente. Por exemplo, suponhamos o seguinte caso:



namespace CarNetLib
{

interface A
{

}
inteface B: A
{
}
[ClassInterface(ClassInterfaceType.None)]
public class Final: B
{
}
}

Ao contrário do que se possa pensar, este tipo de hierarqui irá ser traduzido no seguinte IDL:


 
[
....//atributos idl removidos
]
interface A:IDispatch
{
}

[
....//atributos idl removidos
]
interface B:IDispatch
{
}

coclass Final
{
interface _Object;
[default] interface IDerived;
interface IBase;
}


A explicação para este tipo de comportamento prende-se com o facto do COM não suportar o conceito de hierarquia em runtime (ainda estão lembrados o QueryInterface???). Mesmo quando temos uma herança a nível do IDL, essa herança não tem o mesmo significado que geralmente lhe é associado nas linguagens orientadas a objectos.


A exportação de métodos segue um principio de funcionamento mais lógico. Assim, todos os parâmetros passados por valor são exportados como parâmetros de input (anotados na type lib com o valor in). Por outro lado, se os parâmetros forem passados como referência, então serão transformados em parâmetros in, out. O mesmo acontece com os apontadores (que continuam a ser utilizados em unsafe C# ou em Managed C++ - ou, se preferirem, C++/CLI na nova versão da framework).


Convém recordar que o tipo de retorno de um método (em COM) é sempre dado através do tipo HRESULT. Assim sendo, será necessário efectuar uma conversão de forma a que um método .Net que retorna um determinado valor possa ser exportado para uma type lib. A solução para este problema é simples (e não é novidade para os programadores de C++). O valor de retorno passa a ser definido como parâmetro do método em causa e é anotado com o valor out, retval de forma a indicar que esse valor contém o o resultado do processamento do método. Por exemplo:



//NET
long Process();

//COM
HRESULT Process( [out, retval]int64* aux );


É devido a esta anotação que é possível trabalhar em VB com métodos pertencentes a um objecto COM utilizando-os como se estes devolvessem um valor dum tipo diferente de HRESULT. Claro que o framework irá ter de modificar o código do wrapper. Assim, se tudo correr bem, o wrapper automaticamente retorna S_OK (um dos muitos HRESULT pré-definidos que é utilizado para indicar sucesso). Se houver uma excepção, então esta será mapeada num HRESULT que será utilizado como tipo de retorno.


Para os interessados, existe aqui uma tabela com o mapeamento entre as excepções e os respectivos HRESULTs. Se for necessário, podemos criar os nossos próprios HRESULT e associá-los a uma eventual excepção. Para tal, basta associarmos o valor por pretendido à propriedade protegida (protected) HResult definida na classe Exception (que, como é sabido, serve de base a todas as excepções). A criação de um valor adequado para ser utilizado como HRESULT requer alguns cuidados. Todos os HRESULTS não originários da Microsoft não podem começar com o valor 0x8004 e devem possuir um valor superior a 0x200 (num próximo artigo iremos abordar melhor este tema).



Nota: os HRESULTs personalizados podem ser reutilizados em interfaces diferentes.
Nesse caso, o significado de cada HRESULT depende do interface em que foi obtido o erro.

As propriedades públicas também são exportadas. Nestes casos, temos:



  • acessor get de uma propriedade é transformado num método anotado com o atributo idl propget;

  • acessor set de uma propriedade é convertido num método anotado com o atributo idl propput;

  • se o tipo de propriedade for um interface ou uma classe, então o método associado ao set será anotado com o idl proputref.



Nota: a utilização do atributo proputref permite ao editor do VB tratar a propriedade como objecto (ou seja, temos de utilizar a
propriedade em conjunto com o famoso Set).

Os tipos structs são exportados para o seu análogo em idl. Por outras palavras, uma estrutura será sempre transformado numa struct idl. Convém chamar a atenção para o facto de poder ser necessário fixar a definição da estrutura através do atributo LayouKindAttribute. Finalmente, falta referir ainda que as enumerações .Net são transformadas em...(já sei, advinharam!!!) enum na type lib correspondente. Após esta descrição pormenorizada, nada melhor do que passarmos a um tópico muito interessante: definição e processamento de eventos.


Definição e processamento de eventos


Como é do conhecimento geral, em .Net os eventos são baseados na definição de delegates. Contudo, e como seria de esperar, o COM recorre a uma aproximação diferente. Em COM os eventos são definidos através dos chamados connection points. Como não poderia deixar de ser, este mecanismo assenta na definição de interfaces.


A ideia é simples: definimos um interface com um determinado número de métodos. Este interface, ao contrário do que acontece com os interfaces normais (que são implementados pelo objecto servidor) será implementado pelo cliente. Claro que o cliente terá de sinalizar essa implementação ao servidor. Se tal não acontecer, o servidor não sabe que esse cliente está interessado em ser notificado dos acontecimentos descritos por esse interface. Na prática os eventos correspondem à evocação dos métodos definidos no interface. Esta evocação é sempre iniciada pelo servidor e processada pelos cliente (ou seja, é como se ambos trocassem os papeis que estão a desempenhar).


Este tipo de ligações é sempre definida através de duas partes: uma fonte, que efectua a evocação dos métodos definidos no interface (é sempre o servidor); e um cliente, que implementa os métodos definidos nesse interface. Tecnicamente, é normal encontrarmos o termo source ou connection point associado à fonte e o termo sink associado ao objecto que implementa o interface.


O cliente (sink) tem de indicar ao servidor (source) que está interessado em ser notificado desses eventos. Para tal, o cliente tem de estabelecer uma ligação ao servidor utilizando para tal o interface IConnectionPoint (que, como é óbvio, tem de ser implementado pelo servidor). Este interface contém a seguinte definição:



interface IConnectionPoint: IUnknown
{
HRESULT GetConnectionInterface( [out] IID* pIID );
HRESULT GetConnectionPointContainer( [out] IConnectionPointContainer** pp );
HRESULT Advise( [in] IUnknown* unkSink, [out]DWORD* cookie );
HRESULT Unadvise( [in]DWORD cookie);
HRESULT EnumConnections( [out]IEnumConnections* ppEnum );
}


Vamos lá então descrever estes métodos. O método GetConnectionInterface é utilizado por parte do cliente de forma a obter o IID do interface associado ao connection point (este valor não é mais do que um GUID que é associado ao interface que contém os métodos utilizados para notificar o cliente - por outras palavras, este método retorna o IID do interface que define os eventos). Como referi, o cliente deve possuir uma implementação do interface identificado por este IID. Se o cliente desejar ser notificado de eventuais eventos, então deverá indicar essa intenção ao objecto servidor, utilizando para tal o método Advise.


Ao evocar esse método, o cliente tem de enviar um apontador (IUnknow*) relativo ao objecto que implementa o sink interface e deve guardar o valor proveniente do parâmetro cookie (que é um parâmetro de output). Este parâmetro identifica a ligação estabelecida entre o cliente e o servidor e irá ser utilizado mais tarde, de forma a que o cliente seja capaz de indicar ao servidor que não está interessado em receber mais notificações relativas a esse evento (este mecanismo de cookies é necessário pois um servidor pode notificar vários clientes). Quando isso acontecer, então deverá evocar o método Unadvise e passar o cookie obtido aquando da evocação do método Advise.


O método EnumConnections permite obter uma enumeração de todas as ligações actuais mantidas por um connection point. Se o objecto source necessitar de evocar um método do interface, tem de percorrer esta lista e evocar sucessivamente todos os métodos dos objectos contidos nesta lista interna (portanto, através deste método obtemos uma lista de todos os clientes uqe desejam ser ntoficados acerca de um determinado evento).


Apesar da elegância desta solução, ainda temos um problema para resolver: e se o objecto necessitar de possuir vários connections points? A resposta a este problema vem sobre a forma de um novo interface: IConnectionPointContainer. Este interface apresenta os seguintes métodos:



interface IConnectionPointContainer:IUnknow
{
HRESULT EnumConnectionPoints( [out]IEnumConnectionPoints** ppEnum);
HRESULT FindConnectionPoint( [in]REFIID riid, [out] IConnectionPoint** ppCP );
}

Com este tipo de solução, um cliente deve sempre começar por obter este interface. Só em seguida deverá tentar obter o connection point desejado. O exemplo seguinte (em C++) apresenta um exemplo tipico da utilização destes interfaces (a partir do cliente ou sink):



CComPtr<IUnknown> src = //referencia ao IUnknown do objecto source
CComPtr<_IMyEvents> sink = //objecto que ira receber os eventos; neste caso implementa o interface
// _IMyEvents (este interface foi definido algures e encapsula os metodos
// relativos aos eventos COM)

DWORD cookie; //utilizado para desligar a ligacao ao connection
CComPtr<IConnectionPointContainer> pointContainer;
HRESULT hr = src.QueryInterface( &pointContainer ); //obter o IConnectionPointContainer

//metodo de verificacao de erro removido para simplificar o codigo
//agora vamos obter o connection point para o interface pretendido
CComPtr<IConnectionPoint> cnnPt;
pointContainer->FindConnectionPoint( __uuidof(_IMyEvents), &cnnPt );
//agora vamos estabelecer a ligacao
cnnPt->Advise( sink, &cookie );

//quando ja na quisermos ser notificados...
cnnPt->Unadvise( cookiew );

Vamos lá analisar o código. A classe CComPtr é um template que facilita a utilização de objectos COM. A vantagem da utilização desta classe reside na automatização da contagem relativa à referência dos objectos COM (ainda estão lembrados do AddRef e do Release, pertencentes ao interface IUnknown?) . O primeiro passo consiste em obtermos um apontador para o interface utilizado nas notificações (no exemplo esse interface tem o nome de _IMyEvents). Este interface pode ser implementado pela coclasse cliente ou então pode ser implementado por um objecto interno. Em seguida, começamos por pedir ao objecto servidor o interface IConnectionPointContainer. Como vimos, se o servidor suportar eventos, então terá de implementar este interface (convém relembrar que em COM temos sempre de utilizar o método QueryInterface para interrogar o objecto em relação a um interface - podem considerar isto como sendo um cast à COM).


O passo seguinte consiste em verificar se o objecto contém um connection point relativo ao interface em causa. Para tal, temos de passar o ID do interface (ou melhor, o IID, que é obtido no exemplo à custa do __uuidof - isto é mais um dos artíficios do C++ :) ). Se tal for verdade, então indicamos ao objecto servidor que estamos interessados em ser notificados, utilizando para tal o método Advise. Quando já não estivermos interessados nas notificações, podemos recorrer ao método Unadvise quebrar a ligação. Este tipo de mecanismo é muito utilizado pelos ActiveX (que será o alvo do próximo item da série) de forma a notificar eventuais clientes de determinadas situações.


Acreditem ou não, este é o mecanismo utilizado pelo COM. Sempre que colocam um ActiveX num formulário de VB (ou mesmo VB.Net e C#) são efectuadas estas negociações. Ah, grande VB! Protegeu-nos durante anos e anos deste tipo de código :)


Felizmente para nós, a framework .Net implementa estes interfaces através dos wrappers gerados.


Implicações a nível de objectos .Net


Agora que já temos uma noção básica sobre o funcionamento de eventos em COM, convém referir quais as implicações deste mecanismo na construção de componentes .Net.


Tipicamente um evento é definido da seguinte forma em .Net:




public event ChangedHandler OnChange;

Infelizmente estes eventos não podem ser facilmente consumidos por um objecto COM. O problema reside na forma como o evento irá ser exportado. Durante a exportação para a type lib, o evento irá ser transformado em dois métodos (add_OnChange e remove_OnChange). Ambos estes métodos recebem um parâmetro do tipo ChangedHandler. O problema aqui reside no facto deste ser o mecanismo utilizado por objectos .Net e não por objectos COM! Para além disso, o parâmetro necessário tem ser um tipo managed! Devido a isso, não é possível ao objecto COM criar este tipo e passá-lo aos métodos.


A solução para este problema é simples: consiste em criar um interface (que irá funcionar de forma semelhante aos interfaces dos eventos em COM) e associar este interface ao evento utilizando para tal o atributo ComSourceInterfaces. O exemplo seguinte ilustra este principio:



//criar interface dos eventos
[
Guid( ... ),
InterfaceType(ComInterfaceType.IsDispatch)
]
public interface IMyEvents
{
void SomethingChanged(); //nome do metodo tem de ser igual ao nome do evento!
}

//definir o delegate
//com a mesma assinatura do metodo
delegate void ChangedDelegate();

//exportar a classe e expor o interface anterior como fonte de eventos
[
ComSourceInterfaces(typeof(IMyevents))]
]
class Test
{
public event ChangeDelegate SomethingChanged;

//definir um metodo geral que apenas dispara o evento
public void FireEvent()
{
OnChange();
}
}

Portanto, passos muito simples:



  • definir um interface com o método que deve ser evocado (este método tem a mesma assinatura do delegate e deve ter o mesmo nome do evento);

  • utilizar o atributo ComSourceInterfaces para indicar que o evento deve ser exposto a objectos COM através do interface indicado .


Se tivéssemos mais eventos, então deveríamos definir métodos adicionais (que estivessem de acordo com os vários delegates) de forma a que os clientes COM consigam responder aos vários eventos. Alternativamente, poderíamos definir esses eventos em interfaces diferentes. Actualmente podemos definir até quatro interfaces diferentes através do construtor da classe. Se desejarmos indicar mais interfaces, teremos de recorrer ao construtor que recebe uma string (neste caso, separamos os nomes dos tipos utilizando o caracter vírgula). Muito importante é dar o mesmo nome aos eventos e aos métodos associados aos interfaces que irão ser associados aos eventos COM.


Conclusões finais


Por hoje é tudo. Com o material discutido ao longo deste artigo já temos as bases necessárias à implementação de um componente COM com interface: estou a dalar dos ActiveXs. Isso será algo que iremos fazer no próximo artigo da série. Por favor enviem-me as vossas opiniões/sugestões/críticas/correcções para progC@netmadeira.com.Fiquem bem e boa programação!


Leiam o meu blog em: http://weblogs.pontonetpt.com/luisabreu


Written By: labreu
Date Posted: 4/14/2006
Number of Views: 423

Return