: MSDN Magazine November 2007

November 2007
NOVEMBER 2007 Volume 21 Número 10

• Computação confiável: Lições aprendidas em 5 anos de criação de softwares mais seguros
Michael Howard

• Curso intensivo: Analise panes para encontrar vulnerabilidades de segurança nos seus aplicativos
A. Abouchaev, D. Hasse, S. Lambert e G. Wroblewski

• Revisões de código: Encontre e corrija vulnerabilidades antes de lançar seu aplicativo
M. Chmielewski, N. Clift, S. Fonrobert e T. Ostwald

• Teste de difusão: Crie um provedor de interface de teste personalizado para o Team System
Dan Griffin

Code Download (511 KB) HTMLHelp Format
COLUNAS

• Tudo sobre o CLR: Gerenciamento da duração dos objetos
Tim Fischer

• Instintos básicos: Métodos de extensão
Adrian “Spotty” Bowles

• Espaço do Office: Eventos no SharePoint 2007
Ted Pattison

• Cutting Edge: Gerenciando a experiência do usuário no AJAX
Dino Esposito

• Team System: Diretivas de check-in personalizadas
Brian A. Randell

• Fundamentos: Contextos de sincronização no WCF
Juval Lowy

• Bugslayer: Medindo o impacto do estado de exibição
John Robbins

• Assuntos concorrentes: APM simplificada com C#
Jeffrey Richter

• .Assuntos .NET: Depuração dos finalizadores
Stephen Toub

• { End Bracket }: O WPF também é para desenvolvedores
Kevin Moore

Em cada edição

• Nota do editor: Por que chegar a extremos?
Howard Dierking

• Toolbox: Controles de grade atualizados, ferramentas de comparação de código-fonte aberto e muito mais
Scott Mitchell


Computa&#231;&#227;o confi&#225;vel: Li&#231;&#245;es aprendidas em 5 anos de cria&#231;&#227;o de softwares mais seguros

Computação confiável

Lições aprendidas em 5 anos de criação de softwares mais seguros

Michael Howard
Este artigo aborda:
  • Priorizando o código pela idade
  • Usando a automação e as ferramentas de análise
  • Analisando ameaças de vários ângulos
  • A importância do ensino
Conteúdo
Não se trata apenas do código
Corrija o código antigo primeiro
Descarte! Elimine! Exclua!
As ferramentas são fundamentais... a um ponto
Automatize!
Você nunca atingirá vulnerabilidades de segurança zero
A segurança é uma batalha sem fim
Não há um boletim de segurança mágico
O mantra “muitos olhos” está certo!
A atual negação de serviço é a exploração de amanhã
Considerações finais

H
á cinco anos, Bill Gates enviou um memorando a todos os funcionários da Microsoft explicando a importância de criar um software mais seguro. Desde então, muitas pessoas na Microsoft trabalharam para aprimorar a segurança de seus produtos. Ao fazer isso, aprendemos muito sobre quais os requisitos para a criação de um software mais seguro.

A segurança não é um campo estático – ela evolui constantemente à medida que os invasores atacam, os defensores defendem e cada lado aprende mais sobre as técnicas do outro. A segurança é uma corrida de armamento. Para ficar à frente e prever as manobras dos invasores, nós, os defensores, devemos aprender com nossos erros e criar formas melhores de impedir que os usuários sejam comprometidos.

Sendo assim, o que aprendemos nos últimos cinco anos? A maioria dessas lições é praticamente óbvia, mas como muitas coisas aparentes, às vezes ajuda ter alguém indicando o caminho.

Não se trata apenas do código

A indústria de software, ou mais precisamente a indústria de qualidade de software, está concentrada em obter o código correto. Eu realmente não tenho problemas com isso, mas muitas vulnerabilidades de segurança não estão codificando esses problemas. Muitos problemas têm a ver com o projeto. Se você mantiver o foco somente na detecção de problemas de segurança no código, você perderá uma classe inteira de vulnerabilidades. Esse é um dos motivos pelos quais a Microsoft atribui a modelagem de ameaças e a análise de superfície de ataque como parte do Processo SDL (ciclo de vida do desenvolvimento da segurança). A modelagem de ameaças é uma técnica de análise que ajuda a identificar e reduzir os pontos fracos do projeto em um produto. A análise da superfície de ataque se concentra em quais partes de um produto de software estão expostas a usuários não confiáveis, sejam locais ou remotos. Um produto com uma ampla superfície de ataque tem mais código exposto a usuários não confiáveis do que um produto com uma pequena superfície de ataque. (Leia mais em msdn.microsoft.com/msdnmag/issues/04/11/AttackSurface.)

A vulnerabilidade de saturação do buffer RPC DNS documentada no Microsoft Security Bulletin MS07-029 (consulte microsoft.com/technet/security/Bulletin/MS07-029.mspx) permite que um invasor tenha total controle de um sistema afetado. Nesse caso, houve certamente um problema de código. No entanto, o código estava acessível a usuários anônimos e remotos quando deveria estar restrito aos administradores. Dessa forma, o problema foi uma combinação de vulnerabilidade de código e projeto. Analisei essa vulnerabilidade no blog do SDL (consulte blogs.msdn.com/sdl/archive/2007/06/28/lessons-learned-from-ms07-029-the-dns-rpc-interface-buffer-overrun.aspx).

Lição aprendida É fundamental criar modelos de ameaças para detectar pontos fracos em potencial do projeto e determinar a superfície de ataque do software. Você precisa se certificar de que todas as ameaças materiais serão atenuadas e que a superfície de ataque será a menor possível.

Corrija o código antigo primeiro

No que diz respeito à revisão de código, gosto de classificar o código pelo potencial de vulnerabilidade. Escrevi um artigo sobre Segurança e a privacidade da IEEE, intitulado "A Process for Performing Security Code Reviews" (em inglês), que explica as métricas que utilizei para priorizar a revisão de código (você pode encontrar um link para o artigo no meu blog em blogs.msdn.com/michael_howard/archive/2006/08/01/686029.aspx).

A primeira prioridade é o código antigo, pois é muito mais provável que ele tenha mais vulnerabilidades de segurança do que o código mais recente. As ameaças estão em constante evolução. O código antigo – até mesmo o código criado há alguns anos – foi criado quando as ameaças eram diferentes do que são atualmente. Além disso, as técnicas usadas para criar o código antigo não têm as técnicas de defesa e práticas recomendadas mais recentes. Da mesma forma, o código herdado foi criado usando bibliotecas mais antigas e menos seguras. E, finalmente, o código anterior foi criado com menos percepção situacional, em um momento em que os desenvolvedores tinham pouco ou nenhum conhecimento de segurança.

Meu conselho é simples: todo o código antigo deve ser revisado manualmente quanto a vulnerabilidades de segurança. Na verdade, essa é a finalidade da fase do esforço de segurança do SDL na Microsoft. Embora o objetivo principal do SDL seja reduzir a probabilidade de os desenvolvedores adicionarem novas vulnerabilidades a um produto, o esforço de segurança é desenvolvido para forçar a equipe de desenvolvimento a verificar se há problemas no código antigo.

Nenhuma outra métrica que avaliamos foi tão importante ao priorizar a revisão de código – não a complexidade do código, nem a contagem do número de linhas, nem a formação do código. O indicador número um da densidade potencial de vulnerabilidade é simplesmente a idade do código.

Lição aprendida Identifique todos os seus arquivos de código-fonte e classifique-os por idade, em que idade é a data de “nascimento” do código. Execute a análise estática e revise manualmente o código, começando primeiro com o código mais antigo.

Descarte! Elimine! Exclua!

Às vezes, um recurso pode não ser seguro o suficiente no atual cenário de ameaças. O recurso pode ter sido bom há alguns anos, mas não é seguro atualmente não devido a vulnerabilidades de código, mas sim às alterações no atual ambiente computacional.

Um bom exemplo disso é o serviço Alerter no Windows® que deveria mostrar o status de impressão e enviar breves mensagens que seriam exibidas como pop-up na tela do outro usuário. Isso se tornou rapidamente um mecanismo de spam. Portanto, tomamos a difícil decisão de desativar o serviço por padrão no Windows XP SP2 e então o removemos totalmente do Windows Vista®.

Outro bom exemplo são os protocolos herdados IPX e SPX. (Sim, sei que o IPv4 também é antigo e tem seus próprios problemas.) No Windows Vista, simplesmente removemos o suporte para o Microsoft® Client for NetWare Networks, pois o código era muito antigo e não era utilizado pela maioria dos usuários.

Com o tempo, você verá que a Microsoft remove cuidadosamente os recursos mais antigos. Como, alguns usuários contam com determinados recursos, é importante equilibrar o risco com a utilidade.

Lição aprendida Identifique os recursos antigos que podem ser problemas de segurança a longo prazo e elabore um plano para eliminar tais recursos. Talvez a primeira etapa seja continuar lançando o curso, mas desativá-lo por padrão. Então, na próxima versão do produto, o recurso pode ser totalmente removido, mas disponibilizado como um download da Web para aqueles que dependem absolutamente dele. Finalmente, interrompa o suporte para o recurso. Apenas lembre-se de manter os clientes informados.

As ferramentas são fundamentais... a um ponto

No passado, eu era extramente crítico em relação às ferramentas. Na verdade, não das próprias ferramentas, mas a confiança excessiva de alguns desenvolvedores em relação a essas ferramentas. Por ferramentas, quero dizer análise estática de código, análise binária e similares que possam ajudar a determinar vulnerabilidades de segurança. Em meus velhos tempos, eu era de certa forma menos rígido em relação a isso.

Se você tem muito código – digamos, um milhão de linhas – torna-se muito mais difícil analisar todo esse código manualmente. As ferramentas são práticas porque analisam rapidamente grandes faixas de código. No entanto, elas não substituem o intelecto humano. Além disso, muitas delas tendem a não detectar vulnerabilidades por estarem preocupadas em manter uma taxa de falso-positivos e falso-negativos o mais baixa possível. E, para ser totalmente honesto, muitas ferramentas de análise de segurança criam uma quantidade tão ampla de erros e avisos que pode ser muito difícil determinar quais bugs são reais.

É claro, se você vir uma grande quantidade de problemas, isso não significa que pode simplesmente ignorar o resultado da ferramenta! Quando realizamos uma análise da causa dos efeitos de uma vulnerabilidade de segurança, sempre temos que nos perguntar por que o problema não foi detectado pelas nossas ferramentas. Existem três motivos possíveis: a ferramenta não detectou a vulnerabilidade, a ferramenta a detectou mas separou erroneamente o problema como prioridade baixa e a ferramenta de fato detectou o problema e o revisor humano o separou erroneamente. Essa análise nos permite ajustar as nossas ferramentas e treinamento ao longo do tempo.

As ferramentas de análise também são muito boas para determinar o potencial de vulnerabilidades de segurança no código. Digamos que você tenha dois produtos, cada um com aproximadamente 100.000 linhas de código C++. Você executa suas ferramentas em cada base de código – para esse exemplo, vamos pressupor que você usa os comutadores de compilador /W4 e /analyze. A primeira base de código produz 121 avisos /W4 e 19 avisos /analyze, enquanto a segunda base de código tem 235 avisos /W4 e 65 avisos /analyze. Qual conjunto de código você acha que precisa de mais revisão?

Finalmente, as ferramentas são excelentes quando executadas em código novo ou modificado antes da verificação, pois podem atuar como protetores do código e detectar determinadas classes de bug no início do processo.

Lição aprendida As ferramentas de análise podem ajudá-lo a determinar qual o cuidado necessário ao seu código. Você também pode usar o resultado da análise para determinar o risco geral do código. Use as ferramentas no momento da verificação para detectar os bugs precocemente. Além disso, execute as ferramentas com freqüência, de forma que possa lidar rapidamente com novos problemas – se executar as ferramentas somente em intervalos de meses, poderá acabar tendo que lidar com centenas de avisos de uma só vez.

Automatize!

No início de um programa de melhoria de segurança, há uma quantidade significativa de trabalho manual – revisão manual de código, revisões manuais de projeto e assim por diante. Para realmente elevar seu trabalho, você precisa automatizar o máximo possível do processo.

Em nossa equipe, criamos muitas ferramentas personalizadas e criamos um site interno da Web para coletar dados de ferramentas, de forma que a equipe de segurança central pudesse revisar o resultado das ferramentas. Quando é proposta uma melhoria do SDL, essa proposta deve incluir uma forma de automatizar a melhoria. Os principais motivadores para a automação são a escalabilidade e o uso constante. Se você tiver uma quantidade significativa de código, será necessário automatizar. E, se você quiser que partes de um processo sejam repetidas constantemente, então a automação é obviamente a solução.

Lição aprendida Sempre que possível busque a automação. Crie ou compre ferramentas que verifiquem o código e carreguem os resultados em um local central para análise por especialistas de segurança.

Você nunca atingirá vulnerabilidades de segurança zero

É triste, mas é verdade, mas você nunca terá vulnerabilidades de segurança zero. Lembro quando lancei uma das primeiras atualizações de segurança para o Windows Vista. Alguns usuários ficaram surpresos porque pensaram que a Microsoft alegava ter resolvido o problema de segurança com o Windows Vista. Primeiro, eu não conheço alguém que tenha feito essa alegação e, segundo, simplesmente não é possível atingir vulnerabilidades de segurança zero.

Embora as vulnerabilidades de segurança zero sejam o ideal, é ilusório achar que é possível atingir essa condição. O fato é que o cenário da tecnologia está sempre em fluxo, as ameaças são um alvo em movimento e a pesquisa de segurança está em andamento. Disse anteriormente que a segurança é uma corrida de armamento. Adicionamos defesas aos nossos produtos e os invasores se adaptam.

O seu código pode parecer totalmente isento de vulnerabilidades hoje, mas isso pode mudar amanhã quando um novo tipo de vulnerabilidade for detectado. Por exemplo, em 15 de outubro de 2003, a Microsoft lançou um boletim de segurança que corrigia uma vulnerabilidade de script em diferentes locais no Outlook® Web Access, incluído com o Microsoft Exchange 5.5. Em 4 de março do ano seguinte, a Sanctum (adquirida pela Watchfire e agora pela IBM) divulgou um artigo que descrevia uma nova vulnerabilidade de script em diferentes locais denominada divisão de resposta de HTTP. Seis meses depois, a Microsoft divulgou outra atualização de segurança para o Outlook Web Access no Microsoft Exchange 5.5 para corrigir uma vulnerabilidade de divisão de resposta de HTTP. Então, o que aconteceu? Em síntese, no momento em que o primeiro boletim foi lançado, os problemas de divisão de resposta não eram conhecidos, mas o cenário mudou.

Lição aprendida Certifique-se de que as pessoas na organização saibam que a meta é reduzir as vulnerabilidades de segurança, mas que você nunca atingirá problemas de segurança zero enquanto houver invasores buscando novas técnicas e vulnerabilidades.

A segurança é uma batalha sem fim

Nunca haverá um momento que você dirá, “concluímos a parte de segurança; o problema agora está resolvido”. Essa é uma extensão do ponto anterior, mas com um ângulo levemente diferente. É extremamente importante que você mantenha um treinamento contínuo para seus engenheiros. Caso contrário, as habilidades podem ficar desatualizadas e a urgência se dissipar ao longo do tempo. A segurança é fundamental e proteger os seus sistemas é imprescindível. Como dito no ponto anterior, as novas ameaças e vulnerabilidades estão constantemente sendo detectadas. Portanto, você deve tratar sempre a segurança como uma tarefa que nunca é concluída.

Você também deve se conscientizar de que não há mais algo de especial em relação à segurança. A segurança é simplesmente parte da conclusão do trabalho.

Lição aprendida Continue fornecendo treinamento contínuo aos seus engenheiros e certifique-se de que eles sempre estejam cientes da importância de solucionar os problemas de segurança.

Não há um boletim de segurança mágico

As pessoas me perguntam com freqüência “qual é o elemento mais importante do SDL?”. A resposta é: tudo. Se uma parte do SDL comprovar não ser útil, ela será excluída do SDL. Cada parte do SDL leva a uma redução nas vulnerabilidades de segurança. Portanto, quando escuto alguém dizer que tem uma tarefa única e mágica para solucionar a segurança, como executar ferramentas de análise estática ou treinar pessoas, ele não está cobrindo todas as suas bases de segurança. Com certeza, as ferramentas de análise estática têm seu lugar e ensinar os usuários é fundamental, mas não por eles mesmos. A segurança deve ser completa, bem como deve ser parte do processo.

Lição aprendida Certifique-se de que você esteja solucionando a segurança de todos os ângulos disponíveis. Caso contrário, você precisará alterar o seu processo!

O mantra “muitos olhos” está certo!

Renomado defensor de código-fonte, Eric Raymond, disse “tendo olhos suficientes, todos os bugs são superficiais”. Ele está certo. Mas eu considero que essa afirmação simplificada peca em um ponto principal. Ela deveria continuar assim, “desde que os olhos sejam incentivados e instruídos”.

Na Microsoft, podemos exigir que as pessoas adotem melhorias de processo, pois esses requisitos fazem parte do trabalho. Esse é o incentivo. Oferecemos ensino de várias formas, como treinamentos dinâmicos, laboratórios e treinamento online. Portanto, há bastante material para garantir que os engenheiros estejam atualizados com o que está acontecendo no front de segurança.

Lição aprendida Quanto mais olhos houver no código, melhor, mas isso deve ser feito em uma determinada estrutura. Você deve garantir que os desenvolvedores fornecendo a revisão do código tenham um incentivo para que se preocupem em se adaptar ao seu atual processo, bem como que estejam treinados em relação às mais recentes ameaças de segurança.

A atual negação de serviço é a exploração de amanhã

Muitos fornecedores de software, incluindo a Microsoft, aprenderam essa lição arduamente. Os engenheiros podem analisar o problema de código e rapidamente descartá-lo por ser “apenas um DoS”. Lembre-se de que o mundo da pesquisa em segurança está em constante evolução e as pressuposições sobre determinadas classes de vulnerabilidade podem mudar da noite para o dia.

Lição aprendida Não seja rápido ao descartar um problema de negação de serviço. Com pouco trabalho, os usuários maliciosos podem transformar vulnerabilidades de DoS em verdadeiras vulnerabilidades de execução de código.

Considerações finais

Se há algo que aprendi nos últimos anos é que, no que diz respeito à segurança, você precisa estar preparado para ter suas idéias e ponto de vista constantemente desafiados, além de estar sempre atualizado sobre os problemas mais recentes. Se você seguir à risca as lições apresentadas neste artigo, você se sairá muito bem.

Michael Howard é um dos principais gerentes de programas de segurança na Microsoft e se concentra no aprimoramento e nas melhores práticas do processo seguro. Ele é o co-autor de muitos livros de segurança, incluindo Writing Secure Code for Windows Vista, The Security Development Lifecycle, Writing Secure Code e 19 Deadly Sins of Software Security (em inglês).

© 2007 Microsoft Corporation e CMP Media, LLC. Todos os direitos reservados. A reprodução parcial ou completa sem autorização é proibida..


Curso intensivo: Analise panes para encontrar vulnerabilidades de seguran&#231;a nos seus aplicativos

Curso intensivo

Analise panes para encontrar vulnerabilidades de segurança nos seus aplicativos

Adel Abouchaev and Damien Hasse and Scott Lambert and Greg Wroblewski
Este artigo aborda:
  • Causas de falhas do programa
  • Erros exploráveis versus não exploráveis
  • Práticas recomendadas de análise
  • Estratégias para separação de erros
Este artigo utiliza as seguintes tecnologias:
Depuração do Windows, C/C++
Conteúdo
Violações de acesso
Violação de acesso ao gravar dados
Exceções não exploráveis
Exceções /GS
Exceções NX
Colocando em prática

C
omo você pode ter certeza de que uma falha no seu programa não é explorável? A breve resposta é simples: suponha que cada falha é explorável e a corrija! Na pior das hipóteses, é uma questão de qualidade e freqüentemente é mais barato – e mais prático – corrigir o problema antes que o produto seja enviado aos clientes. Pode ser mais caro executar a análise necessária para determinar a capacidade de exploração.

Analisar as falhas do programa relacionadas à corrupção de memória, a fim de compreender as ramificações de segurança, pode ser uma tarefa complexa e com tendência a erros. Vários erros devem ser considerados, incluindo a localização do buffer na memória, os possíveis alvos para substituição, o tamanho da substituição, restrições em dados que podem ser usados durante a substituição, o estado do ambiente de execução do tempo de execução e a capacidade de desvio de mecanismos de atenuação instalados. Em poucas palavras, você deve compreender a causa original da falha, a fim de responder totalmente a essas dúvidas.

Lembre-se de que nem toda falha se manifestará de maneira observável. Um exemplo disso é o problema de execução de código remoto GDI, abordado no Microsoft Security Bulletin MS07-017. O software responsável pela invocação do código de análise vulnerável fez uso de um manipulador de exceções, a fim de recuperar-se de praticamente qualquer exceção que pudesse ser gerada e continuar a operação como se nada tivesse ocorrido. (Leia mais sobre isso em blogs.msdn.com/sdl/archive/2007/04/26/lessons-learned-from-the-animated-cursor-security-bug.aspx.) Outro exemplo menos óbvio pode ser encontrado em determinados tipos de corrupções de memória de heap e pilha, em que é possível que falhas tenham ocorrido, mas o estado atual do programa e seu ambiente de execução não mostram sinais óbvios.

Esse artigo oferece orientação sobre como analisar falhas do programa em relação às possíveis implicações de segurança – como o caso da corrupção de memória, permitindo a execução de código arbitrário ou, na pior das hipóteses, a negação do serviço. Vamos enumerar as exceções comuns de hardware e software que você poderá encontrar ao verificar esses tipos de problemas. Também oferecemos algumas diretrizes gerais que você pode usar durante uma investigação desse tipo. A Figura 1, por exemplo, lhe fornece um caminho gráfico do processo de investigação, a fim de auxiliá-lo a decidir se uma falha específica é explorável. É importante lembrar que essas são apenas diretrizes e que somente uma análise completa da causa original pode garantir que você tenha diagnosticado corretamente a falha como sendo não explorável. Novas técnicas ou variações das técnicas de ataque existente estão sempre sendo detectadas.

Figura 1 Caminho de análise da violação de acesso
Figura 1 Caminho de análise da violação de acesso(Clique na imagem para diminuir a exibição)
Figura 1 Caminho de análise da violação de acesso
Figura 1 Caminho de análise da violação de acesso (Clique na imagem para aumentar a exibição)

A causa mais comum de falhas são as exceções de hardware ou software. Um processador moderno característico pode gerar muitos tipos diferentes de exceções de hardware, mas no ambiente Windows® somente alguns desses criam problemas relacionados à segurança de software. A causa mais comum de exceção de hardware é uma violação de acesso. Primeiro, nós iremos abordar como analisar as exceções de hardware, depois as de software.

Violações de acesso

As exceções de violação de acesso (0xc0000005= STATUS_ACCESS_VIOLATION) são geradas pelos processadores modernos com um acesso de memória causado por uma execução de programa ou instrução que não satifaz determinadas condições definidas pela arquitetura do processador ou estruturas da unidade de gerenciamento de memória.

Embora uma falha pura possa resultar somente em uma condição de negação de serviço, não é seguro pressupor que uma falha não possa ser usada para atingir efeitos mais perigosos, incluindo a execução de código. Ao analisar falhas, você deve pressupor que todo o corpo da memória, com algumas poucas exceções, está sob o controle de um invasor potencial; portanto, uma violação de acesso pode, na maioria dos casos, levar ao controle dos dados pelo invasor. Essa instrução aplica-se a uma exceção que ocorreu enquanto uma instrução estava lendo ou gravando dados.

Se a violação de acesso pode fazer com que os seus dados sejam controlados pelo invasor, então cada violação de acesso causada por uma leitura da memória se transformará no carregamento dos dados controlados pelo invasor. Nem sempre é fácil determinar os efeitos de segurança desse tipo de operação. Você poderia executar uma análise completa de fluxo de dados no código-fonte ou binário, a fim de localizar o escopo do controle de endereço de origem e as conseqüências de alimentar o programa com dados aleatórios em determinados pontos da execução. Essa é uma tarefa demorada e desafiadora. Em resposta, desenvolvemos uma heurística simples para analisar rapidamente as falhas de violação de acesso de leitura para potencial execução de código.

Como o exemplo a seguir mostra, um ponteiro de memória inválido no eax do Registro causou uma falha. Nesse caso, o controle do conteúdo de memória fornece ao invasor total controle sobre o fluxo do programa:

Application!Function+0x133:
3036a384 eb32            call     [eax]           ds:0023:6c7d890d=??
0:000> ub
mov    eax, [ebx]  ->  eax = invalid memory pointer
...                         (instructions not affecting register eax)
call        [eax]  ->  crash

As situações em que o endereço lido não pode ser suficientemente controlado pelo invasor podem ser tratadas como condições de negação de serviço. Por exemplo, no ambiente do modo de usuário característico do Windows, uma falha que ocorre em um ponteiro NULL que foi inicializado e não pode ser afetado por um invasor não poderá levar à execução do código por si próprio.

No exemplo a seguir, você pode ver que uma falha foi causada pela referência de endereço zero através do valor no eax do Registro:

Application!Function+0x133:
3036a384 8b14            mov     ecx, [eax]           ds:0023:00000000=??
0:000> ub
xor    eax, eax
...                         (instructions not affecting register eax)
cmp    ebx, 2
jne        label123
mov    ecx, [eax]  ->  crash, eax = 0 (NULL)

Ao desmontar (com o comando ub no depurador da linha de comando do Visual Studio®), podemos controlar o fluxo de dados neste Registro até que seja confirmado que esse valor no registro não pode ser influenciado por entrada maliciosa. Na verdade, neste exemplo, o registro foi zerado pelo XORing por ele mesmo e não foi usado até que a instrução de falha fosse atingida.

Às vezes, a capacidade de exploração de um defeito não está imediatamente visível a partir da instrução de falha. Por exemplo, depois da desmontagem das instruções a seguir, você pode ver que isso é uma conseqüência do fluxo de controle quando a instrução de falha (descrita no parágrafo anterior) é seguida por uma instrução crítica:

(1258.1638): Access violation - code c0000005 (second chance)

Application!Function+0x123:
3036a384 8b12            mov    eax, [ebx]           ds:0023:6c7d890d=??
0:000> u
mov    eax, [ebx]  -> crash, ebx points to invalid memory
...             (instructions not affecting regist an example er eax)
call        [eax]  ->  possibility of code execution

Este exemplo é semelhante ao primeiro, ainda que dessa vez a falha tenha ocorrido nos dados de carregamento da instrução no eax do registro. Embora essa operação por si só não indique problemas de segurança, a desmontagem mostra claramente que o controle sobre o valor do ebx do registro implica controle sobre o valor eax do registro, que leva à possível execução do código.

Os casos restantes podem ser analisados simulando a injeção de dados maliciosos durante o tempo de execução do programa, seja manualmente em um depurador ou automaticamente por meio de uma ferramenta de depuração. Depois de atingir a instrução que causa a violação de acesso, podemos alterar o endereço de origem para apontar para um endereço de memória válido e executar a instrução novamente ou podemos inserir dados aleatórios no registro de destino e ignorar a instrução com falha, continuando a execução. Repetiremos esse processo para cada violação de acesso encontrada até que seja atingida uma condição explorável.

Vamos analisar dois exemplos. Primeiro, vejamos o fluxo de dados de controle ao alterar o endereço de origem:

Application!Function+0xa70:
3036a37e 8b4708          mov     eax,dword ptr [edi+8] ds:0023:040fd004=????????

A falha aqui é causada por um valor incorreto no edi do registro. Iremos alterá-lo para apontar para uma região válida da memória. Há muitas escolhas possíveis, mas na prática com freqüência usamos o valor atual do eip do registro. Isso garante que um bloco relativamente grande de memória em torno do novo valor do edi é válido e acrescenta a possibilidade de obter quaisquer gravações subseqüentes no bloco de memória contendo código, pois ele é sempre marcado como somente leitura:

0:000> r edi=eip
0:000> g

Depois de definir o edi do registro como o valor atual do eip do registro, continuamos a execução, atingindo outra exceção no esi do registro onde repetimos o processo:

(1258.1638): Access violation - code c0000005 (second chance)

Application!Function+0xa76:
3036a384 f60601          test    byte ptr [esi],1           ds:0023:6c7d890d=??
0:000> r esi=eip
0:000> g

Continuando a execução, atingimos uma exceção de violação de acesso em uma instrução tentando gravar dados no segmento de código. Isso significa que um invasor também pode gravar dados em qualquer endereço de memória:

 (1258.1638): Access violation - code c0000005 (second chance)

Application!Function+0xbef:
3036a4fd 894710          mov     dword ptr [edi+10h],eax ds:0023:3036a38e=0c46f60d

Como você pode ver, a violação de acesso de leitura inicial se tornou um problema de segurança real com possibilidades de execução de código.

Agora, vejamos o fluxo de dados de controle ao definir os dados de destino e as instruções que devem ser ignoradas. Analisando a mesma falha como antes, nós continuaremos alterando os dados de destino. Depois que a exceção inicial for atingida, definimos o registro do eax como qualquer valor fácil de controlar e aumentamos o eip de registro pelo tamanho da instrução atual para ignorá-lo:

Application!Function+0xa70:
3036a37e 8b4708          mov     eax,dword ptr [edi+8] ds:0023:03f93004=????????
0:000> r eax=deadbeef
0:000> r eip=eip+3
0:000> g

Continuando a execução, atingimos a próxima exceção, que não tem um destino. Portanto, podemos apenas ignorar a instrução:

(1258.1638): Access violation - code c0000005 (second chance)

Application!Function+0xa76:
3036a384 f60601          test    byte ptr [esi],1           ds:0023:deadbefb=??
0:000> r eip=eip+3
0:000> g

Executando ainda mais, atingimos a mesma exceção de violação de acesso de gravação, implicando a possibilidade de problema explorável:

(1258.1638): Access violation - code c0000005 (second chance)

Application!Function+0xbef:
3036a4fd 894710          mov     dword ptr [edi+10h],eax ds:0023:03f9300c=0c46f60d

Embora ambos os métodos de simulação de injeção de dados maliciosos com freqüência gere os mesmos resultados, a partir da prática podemos dizer que em muitos casos eles abrangem diferentes caminhos de código, com somente um método mostrando a capacidade de exploração de um problema. Todos os métodos abordados até agora permitem a detecção muito rápida em potencial de falhas de violação de acesso de leitura exploráveis, mas devemos lembrar que eles não fornecem uma validação final de que determinada falha não é um problema explorável.

Violação de acesso ao gravar dados
Recursos de análise de falha
  • MSDN Security Developer Center
  • Latest Microsoft Security Bulletins
  • Visual Studio Debugger Security
  • Debugging Tools for Windows

Uma violação de acesso durante a gravação de dados indica possível corrupção de memória, o que praticamente leva a uma condição explorável da execução de código. Com muita freqüência, tais gravações são indicadores de condições de estouro de buffer presentes no programa com falha. Na prática, algumas poucas falhas de violação de acesso de gravação podem se mostrar não exploráveis. Não obstante, essas situações requerem que você realize uma análise completa do fluxo de dados, a fim de compreender a causa original do problema. Você precisa garantir que a corrupção não permita que um invasor capaz de substituir dados influencie arbitrariamente o fluxo da execução.

A maioria das violações de acesso com gravações de dados podem levar a casos exploráveis que resultarão na execução do código malicioso. Geralmente, a execução do código é atingida pela substituição de uma parte (arbitrária) da memória, na pilha ou no heap.

No exemplo a seguir, a falha ocorreu em uma instrução de cópia de memória quando o registro do destino atingiu o limite superior da pilha e uma área não alocada da memória. O tamanho da operação de cópia surgiu da variável no endereço [ebp-8] e a falha indica que está sob o controle do invasor:

Application!Function+0x143:
3036a384 f3a4            rep     movsb           es:0023:00140000=??   ds:0023:0125432c=41414141
0:000> ub
mov    esi, [ebp-4]
mov    edi, [ebp+4]
mov    ecx, [ebp-8]
rep    movsb  ->  write access violation if value in ecx is big enough

Podemos dizer com segurança que uma violação de acesso de gravação não pode levar à execução de código somente se os endereços de destino controlados pelo invasor apontarem para memória inválida ou dados que não influenciam a execução do programa. Na prática, somente um pequeno conjunto de violações de acesso de gravação satisfaz essa condição. No ambiente do modo de usuário do Windows, uma gravação para um ponteiro NULL ou espaço de endereço do sistema (geralmente endereços acima de 2 GB) poderia ser um exemplo de um problema não explorável (pressupondo-se que a página com endereço 0x00000000 não pode ser alocada). Além disso, em um cenário de servidor, a instrução levaria a uma negação de serviço, que deveria ser considerada uma vulnerabilidade de segurança se for acionada por um usuário não administrador.

Este exemplo é semelhante ao caso anterior abordado relativo às violações de acesso de leitura. A desmontagem rápida mostra que o eax do registro foi inicializado como valor zero e não foi alterado até que a instrução de falha fosse atingida:

Application!Function+0x133:
3036a384 8d14            mov     [eax], ecx           ds:0023:00000000=??
0:000> ub
xor    eax, eax
...                       (instructions not affecting register eax)
cmp    ebx, 2
jne    label123
mov    [eax], ecx  ->  crash, eax = 0 (NULL)

Exceções não exploráveis

No modo de usuário – não no modo kernel – é improvável que esta classe de exceções seja usada em uma exploração de fase única. Geralmente, essas exceções resultam na negação de serviço. Somente em alguns casos, quando o manipulador de exceções for alterado como resultado de outra vulnerabilidade, essas exceções podem levar à execução de código malicioso. A negação de serviço é um bug de alta prioridade na plataforma do servidor e de prioridade média em estações de trabalho.

Um exemplo dessas exceções é a desreferência estática/global (leitura e gravação). O processo neste exemplo não tem acesso de leitura à página 0x310000 para que a violação de acesso de leitura ocorra ou acesso de gravação para a mesma página para acionar uma violação de acesso de gravação.

mov ebp, 310046h
mov eax, [ebp+4h]

inc eax
mov [ebp+8h], eax

As duas primeiras exceções precisam ser analisadas com atenção. Se o valor estático/global apontar para o endereço na memória em que o código malicioso pode gravar sem limites na extensão e pode substituir outras estruturas para a fase de exploração complexa, esse caso precisa ser marcado como explorável. Se ele somente permitir que você armazene uma única DWORD no endereço que não é parte de qualquer estrutura de controle, ele será mais provavelmente não explorável. No entanto, essa instrução deve ser verificada pela execução do programa e pela observação dos valores (também conhecida como análise do tempo de execução). Se o valor que estiver armazenado não for usado posteriormente no código, ele não pode levar a outra condição explorável e pode ser ignorado. Se o valor puder ser usado como um endereço de memória ou em uma operação de cópia de memória, o risco de exploração é superior.

A execução de código em um endereço estático/global em espaço de endereço não controlável (página 0 e casos semelhantes) é outra instância em que a capacidade de exploração depende se a página de memória no endereço a partir do operando é controlável para gravação:

0040137F B8 DE C0 AD DE    mov     eax, 0DEADC0DEh
00401384 FF D0             call    eax

A análise de código de execução em um endereço estático/global tem outras implicações. Se o endereço pertence às páginas que nunca estariam associadas à execução de processo normal (página 0 ou páginas com endereços acima de 0x80000000), então ela não é explorável.

A operação divisão por zero também acionaria uma exceção, mas ela não é diretamente explorável nesse ponto. Essa situação precisa de análise extra para decidir se o resultado dessa exceção pode definir a CPU para que execute o código, o que pode levar a uma exploração bem-sucedida:

004013D6 33 C9    xor    ecx, ecx
004013D8 8B C1    mov    eax, ecx
004013DA 40       inc    eax
004013DB F7 F1    div    ecx

As exceções do C++ não tratadas podem interromper a execução do processo. Durante o tempo de execução, elas causarão o encerramento do aplicativo. No depurador, é possível continuar depois da exceção não tratada. As exceções do C++ ocorrem quando as funções que acionam a exceção da biblioteca do tempo de execução são chamadas:

00401902    mov     [ebp+var_4], 1
00401909    push    offset __TI1H
0040190E    lea     eax, [ebp+var_4]
00401911    push    eax
00401912    call    __CxxThrowException@8 ; _CxxThrowException(x,x)

Esta condição pode ser explorada somente se o mecanismo do manipulador de exceções já tiver sido substituído. Caso contrário, as exceções do C++ não tratadas não são exploráveis. O rastreamento de pilha para essa exceção é mostrado na Figura 2.

Figure 2 Rastreamento de pilha para um erro C++ sem tratamento (Close Figure 2)
CommandLine: test.exe
Symbol search path is: srv*c:\Symbols*\\symbols\symbols
Executable search path is:
ModLoad: 00400000 00405000   test.exe
ModLoad: 7c900000 7c9b0000   ntdll.dll
ModLoad: 7c800000 7c8f5000   C:\WINDOWS\system32\kernel32.dll
ModLoad: 78130000 781cb000   C:\WINDOWS\WinSxS\x86_Microsoft.VC80.CRT_1fc8b3b9a1e18e3b_8.0.50727.762_x-ww_6b128700\MSVCR
80.dll
ModLoad: 77c10000 77c68000   C:\WINDOWS\system32\msvcrt.dll
(1494.14c4): Break instruction exception - code 80000003 (first chance)
eax=00251eb4 ebx=7ffda000 ecx=00000004 edx=00000010 esi=00251f48 edi=00251eb4
eip=7c901230 esp=0012fb20 ebp=0012fc94 iopl=0    nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
ntdll!DbgBreakPoint:
7c901230 cc              int     3
0:000> g
1
(1494.14c4): C++ EH exception - code e06d7363 (first chance)
(1494.14c4): C++ EH exception - code e06d7363 (!!! second chance !!!)
eax=0012fee0 ebx=00000000 ecx=00000000 edx=781c3c58 esi=0012ff68 edi=004033a4
eip=7c812a5b esp=0012fedc ebp=0012ff30 iopl=0    nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
kernel32!RaiseException+0x53:
7c812a5b 5e              pop     esi
0:000> kb
ChildEBP RetAddr  Args to Child
0012ff30 78158e69 e06d7363 00000001 00000003 kernel32!RaiseException+0x53
0012ff68 00401917 0012ff78 004022b8 00000001 MSVCR80!_CxxThrowException+0x46
0012ff7c 004011b2 00000001 00353098 003530d0 test!wmain+0x27
0012ffc0 7c816fd7 00011970 7c9118f1 7ffda000 test!__tmainCRTStartup+0x10f
0012fff0 00000000 004012fb 00000000 78746341 kernel32!BaseProcessStart+0x23

Observe que a exceção do estouro de pilha pode ocultar outros problemas no tempo de execução, desviar o fluxo de código para um caminho diferente, desativar os mecanismos de proteção gerados pelo compilador, como /GS, ou causar inconsistência no aplicativo ao liberar memória que ainda está em uso. Quando um espaço na pilha já estiver quase consumido, é possível que um aplicativo falhe dentro dos métodos C++ com uma exceção de estouro da pilha e partes importantes do código não sejam executadas na ordem. Em tais condições, será necessária uma separação adicional.

Exceções /GS

As exceções /GS (0xc0000409=STATUS_STACK_BUFFER_OVERRUN) são aquelas que o Windows lançará sempre que detectar que o cookie de segurança protegendo o endereço de retorno foi violado. Como o objetivo da /GS é transformar saturações de buffer que levam a uma execução de código em ataques de negação de serviço, sempre que uma falha for detectada você pode ter certeza de que tem um bug de segurança. (Infelizmente, devido à memória cheia de bugs, placas-mãe overclocked, hardware defeituoso e outros problemas, às vezes, o código que valida o cookie é ultrapassado sem ter uma saturação real de buffer.)

No Windows Vista®, o sistema operacional irá lançar uma exceção 3 int quando for detectado STATUS_STACK_BUFFER_OVERRUN (pressupondo-se que o depurador esteja presente). Em versões anteriores do Windows, um ponto de interrupção deve ser posicionado no kernel32!UnhandledExceptionFilter para detectar se um cookie de segurança foi violado (caso contrário, o processo seria encerrado e o usuário não seria notificado).

Na Figura 3, a função foo irá saturar um buffer ao copiar muitos dados para um buffer de pilha, o que fará com que o cookie /GS seja substituído.

Figure 3 Substituição do cookie /GS (Close Figure 3)
<the /GS cookie is being setup in the function prolog>
0:000:x86> u gs!foo
gs!foo:
010011b9 8bff            mov     edi,edi
010011bb 55              push    ebp
010011bc 8bec            mov     ebp,esp
010011be 83ec18          sub     esp,18h
<global cookie will be moved to eax register>
010011c1 a100200001      mov     eax,dword ptr [gs!__security_cookie (01002000)]
010011c6 53              push    ebx
010011c7 56              push    esi
010011c8 57              push    edi
010011c9 8b7d08          mov     edi,dword ptr [ebp+8]
<cookie will be placed on the stack (ebp-4)>
010011cc 8945fc          mov     dword ptr [ebp-4],eax
<content of the source (src) and destination buffers (dst) – before the overrun>
0:000:x86> dv /V
000bfefc @ebp+0x08             src = 0x009d16cb "123456789012345678901234567890"
000bfedc @ebp-0x18             dst = char [20] ""
<value of the security cookie on the stack; note that it is located right after the buffer, before saved ebp (0x000bff24) and the return address (0x0100124a)>
0:000:x86> dd 000bfedc+0n20 l3
000bfef0  0000b96f 000bff24 0100124a
<value of global cookie>
0:000:x86> dd gs!__security_cookie l1
01002000  0000b96f
... code runs ...
<after the overrun has happened, the cookie got overwritten (with 0x34333231) as well as the last two bytes of the return address (with (0x3039)>
0:000:x86> dd 000bfedc+0n20 l3
000bfef0  34333231 38373635 01003039
<in the function epilog before it returns the /GS cookie gets checked (i.e. the return address has not been used yet)>
0:000:x86> u gs!foo+0x54
gs!foo+0x54:
0100120d 59              pop     ecx
<cookie is placed in the ecx register>
0100120e 8b4dfc          mov     ecx,dword ptr [ebp-4]
01001211 5f              pop     edi
01001212 5e              pop     esi
01001213 5b              pop     ebx
01001214 e882020000      call    gs!__security_check_cookie (01002000)
01001219 c9              leave
0100121a c20400          ret     4
<the below functions compares the global cookie with the one that was stored in the stack (currently in ecx register)>
0:000:x86> r
eax=0000000c ebx=7efde000 ecx=34333231 edx=00000000 esi=00000000 edi=00000000
eip=0100149b esp=000bfed8 ebp=000bfef4 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
gs!__security_check_cookie:
0100149b 3b0d00200001    cmp     ecx,dword ptr [gs!__security_cookie (01002000)] ds:002b:01002000=0000b96f
<if a debugger was attached to the process, then an (int 3) will be issued (in Windows Vista) before the process gets terminated>
0:000:x86>
STATUS_STACK_BUFFER_OVERRUN encountered
 (15d0.1708): Break instruction exception - code 80000003 (first chance)
ntdll!DbgBreakPoint:
773e0004 cc              int     3

Exceções NX

As exceções NX (0xc0000005=STATUS_ACCESS_VIOLATION) são lançadas pelo Windows sempre que ele detectar a execução de código em uma página não marcada como executável (em outras palavras, a página não tem PAGE_EXECUTE, PAGE_EXECUTE_READ ou outros sinalizadores relevantes). A NX é aplicada em versões de 64 bits do sistema operacional. Alguns aplicativos, como desempacotadores e DRM (Gerenciamento de direitos digitais), dependem da execução do código no heap. Portanto, nem toda exceção NX deveria ser considerada uma vulnerabilidade de segurança. Ainda faz sentido compreender a causa original do bug para garantir que ele não tem implicações de segurança.

É importante salientar que esse tipo de exceção usa o código de erro 0xc0000005, que não é específico para NX; qualquer aplicativo que tenha um comportamento impróprio (ao, digamos, ler a partir de memória não alocada) pode lançar esse erro. A fim de compreender se a exceção/código de erro está de fato relacionada a NX, precisamos analisar o conjunto de proteção definido na página. Se a página não for marcada como executável, temos uma exceção NX; caso contrário, temos outro tipo de problema. Na Figura 4, por exemplo, uma exceção de primeira chance foi atingida, mas a instrução parece ser válida e faz referência aos dados válidos.

Figure 4 Exceção NX em potencial (Close Figure 4)
(1424.a78): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
00000000`003b0020 2801            sub     byte ptr [rcx],al ds:00000000`7702e6aa=c3
0:000> r
rax=0000000000000001 rbx=000000000021fd10 rcx=000000007702e6aa
rdx=0000000000000000 rsi=000000000000000a rdi=0000000000000000
rip=00000000003b0020 rsp=000000000021fca0 rbp=00000000ff130000
 r8=000000000021fc98  r9=00000000ff130000 r10=0000000000000000
r11=0000000000000244 r12=00000000ff131728 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
00000000`003b0020 2801            sub     byte ptr [rcx],al ds:00000000`7702e6aa=c3
<when we execute the actual instruction again, we hit a second chance access violation>
0:000> t
(1424.a78): Access violation - code c0000005 (!!! second chance !!!)
00000000`003b0020 2801            sub     byte ptr [rcx],al ds:00000000`7702e6aa=c3
<a closer look at the protection of the page, shows that it does not have PAGE_EXECUTE set, this explains why the code cannot be executed>
0:000> !address 00000000`003b0020
 ProcessParametrs 00000000003b25c0 in range 00000000003b0000 00000000003e0000
 Environment 00000000003b1370 in range 00000000003b0000 00000000003e0000
    00000000003b0000 : 00000000003b0000 - 0000000000030000
                    Type     00020000 MEM_PRIVATE
                    Protect  00000004 PAGE_READWRITE
                    State    00001000 MEM_COMMIT
                    Usage    RegionUsageHeap
                    Handle   00000000003b0000

Colocando em prática

Como você pode ver, analisar as falhas do programa em relação às implicações de segurança é uma tarefa complexa e com tendência a erros. Abordamos as exceções mais comuns que você tem probabilidade de encontrar ao analisar falhas, incluindo violações de acesso de leitura/gravação, divisão por zero, exceções do C++, exceções /GS e problemas relacionados a NX. O principal ponto a lembrar é que somente essa análise completa de causa original pode garantir que você tenha diagnosticado corretamente se determinada falha é explorável.

Em um esforço de lhe fornecer algumas diretrizes úteis para análise de seus próprios aplicativos, resumimos as informações em uma referência rápida para você manter em sua estação de trabalho. A Figura 5 captura as diretrizes abordadas no artigo na forma de tabela. Por exemplo, se uma violação de acesso de gravação tiver ocorrido como resultado de a CPU tentar gravar dados em uma página de memória sem ter acesso de gravação a ela, você precisa corrigir o problema.

Figure 5 Critérios de correção de separação (Close Figure 5)

Exceção | Comentários
Requer correção |
Violação de acesso de gravação | A violação de acesso ocorre quando a CPU tenta gravar dados na página de memória sem acesso de gravação a ela.
Violação de acesso de leitura no ponteiro da instrução (violação de acesso no EIP) | A violação de acesso ocorre quando a CPU tenta executar uma instrução na página de memória sem acesso de leitura a ela.
Violação de acesso de leitura | Um dos seguintes pode ocorrer: •A violação de acesso ocorre em uma instrução de montagem rep (em um processador Intel) em que o registro de conta (ecx) é amplo. •A violação de acesso ocorre em uma instrução mov em que o resultado é usado como o destino de uma chamada nas instruções imediatamente depois do mov. •A violação de acesso ocorre em uma instrução mov em que o resultado é usado posteriormente em uma instrução rep como a origem (esi), o destino (edi) ou contagem (ecx).
Requer separação |
Violação de acesso de leitura | Se a violação de acesso ocorrer ao ler de NULL (endereço 0x00000000) ou se ocorrer a partir da leitura do endereço da memória que não é controlado pela entrada e o valor não é gerenciável pelo invasor.
Geralmente não explorável |
Divisão por zero | Se a violação de acesso ocorrer como um problema autônomo e outras estruturas (manipuladores de exceção, por exemplo) não forem corrompidas antes dessa violação de acesso.
Exceção do C++ | Mesmo acima.

Da mesma forma, a Figura 1 (introduzida anteriormente neste artigo) tem o objetivo de fornecer outra exibição do processo usando uma estrutura de gráfico, a fim de ajudá-lo a decidir se uma falha específica é explorável. Inicie com o nó superior e pergunte-se qual o próximo nó a acessar. Continue esse processo até que atinja o nó não explorável ou explorável.

Espero que você considere essas diretrizes úteis ao separar as falhas do programa. Para obter mais informações, siga os links na barra lateral "Recursos de análise de falha". E, lembre-se, se você diagnosticar uma falha como explorável em um produto da Microsoft, envie essas informações para secure@microsoft.com.

Adel Abouchaev (CCIE#12037, MCSE, CISSP) é engenheiro de software de segurança da equipe SWI (Secure Windows Initiative) da Microsoft. Ele é responsável pelas ferramentas de análise do tempo de execução e difusão, e trabalha para fornecer às equipes da Microsoft métodos automatizados de análise e avaliação de segurança.

Damien Hasse, engenheiro líder de software de segurança na Microsoft, lidera uma equipe de pesquisadores de segurança que investigam vulnerabilidades e ameaças de segurança como parte do MSRC (Microsoft Security Response Center).

Scott Lambert é gerente de programas de segurança da equipe SWI (Secure Windows Initiative) da Microsoft. Ele é responsável pelo aperfeiçoamento das ferramentas de segurança internas, incluindo várias ferramentas de difusão. Aproveitando sua experiência no setor, Lambert trabalha para garantir que as ferramentas SWI identifiquem a maioria das classes de vulnerabilidade.

Greg Wroblewski é engenheiro de software de segurança na Microsoft e trabalha na equipe de resposta à segurança.


Revis&#245;es de c&#243;digo: Encontre e corrija vulnerabilidades antes de lan&#231;ar seu aplicativo

Revisões de código

Encontre e corrija vulnerabilidades antes de lançar seu aplicativo

Michal Chmielewski and Neill Clift and Sergiusz Fonrobert and Tomasz Ostwald
Este artigo aborda:
  • O processo de revisão de código
  • Priorizando o seu código para revisão
  • Tipos de vulnerabilidades de segurança
  • Usando as descobertas da revisão
Conteúdo
Vulnerabilidades de segurança de software
Detectando erros de código
O processo de revisão de código
Priorizando os esforços de revisão
Táticas de revisão de código
Tipos de vulnerabilidades
Processando os resultados
Resumindo

N
o desenvolvimento de software, um pequeno erro de código pode resultar em uma vulnerabilidade crítica que acaba comprometendo a segurança de todo um sistema ou rede. Muitas vezes, uma vulnerabilidade de segurança não é causada por um único erro, e sim por uma seqüência de erros que ocorrem durante o ciclo de desenvolvimento: um erro de código é introduzido, não sendo detectado durante as fases de teste e os mecanismos de defesa disponíveis não impedem um ataque bem-sucedido.

A segurança deve ser uma prioridade em todas as fases do desenvolvimento de software. Os esforços devem ter o objetivo de impedir as vulnerabilidades de software – detectá-las antes do lançamento, é claro, mas também limitar seu impacto prático (por exemplo, ao reduzir a superfície de ataque do produto). Na Microsoft, essa abordagem holística à segurança é implementada por meio do SDL (Ciclo de vida do desenvolvimento da segurança), que cobre todas as principais fases do desenvolvimento de software, incluindo instruções aos desenvolvedores, aperfeiçoamento do projeto, aplicação de código e teste de práticas, bem como preparo para respostas de emergência depois do lançamento de um produto, como você pode ver na Figura 1. O SDL não é a única abordagem à revisão de código, mas compõe a base de grande parte do que trataremos neste artigo.

Figura 1 O ciclo de vida do desenvolvimento da segurança
Figura 1 O ciclo de vida do desenvolvimento da segurança(Clique na imagem para diminuir a exibição)
Figura 1 O ciclo de vida do desenvolvimento da segurança
Figura 1 O ciclo de vida do desenvolvimento da segurança (Clique na imagem para aumentar a exibição)

Neste artigo, abordaremos as revisões manuais de código de segurança realizadas por desenvolvedores ou especialistas em segurança. Em um processo definido pelo SDL, essas tarefas geralmente são realizadas durante um esforço de segurança ou contrato de teste de penetração e estão associados a uma revisão de segurança final. Os erros de código podem ser detectados com o uso de diferentes abordagens, mas mesmo quando comparadas a ferramentas sofisticadas, as revisões manuais de código comprovaram claramente seu valor nas áreas de precisão e qualidade. Infelizmente, a execução de revisões manuais de código também é mais dispendiosa.

Além disso, pretendemos abordar em detalhes as vantagens e desvantagens das revisões de código de segurança no contexto de projetos de software de grande porte. Este artigo foi preparado com base em experiências obtidas ao longo do tempo, por meio de revisões dos principais produtos lançados pela Microsoft ao longo dos últimos anos.

Vulnerabilidades de segurança de software

Nunca deve se supor que um produto de software de tamanho e complexidade não triviais esteja livre de vulnerabilidades de segurança. Todas as possíveis etapas devem ser consideradas para limitar o número de erros de código e reduzir o impacto prático, mas algo sempre é despercebido. Os erros de software que afetam a segurança (chamados de vulnerabilidades) podem ocorrer em níveis diferentes no aplicativo e podem ser introduzidos durante diferentes fases do ciclo de desenvolvimento.

As vulnerabilidades não estão limitadas ao código. Elas podem ser introduzidas até mesmo na definição de requisitos, na forma de um requisito que não possa ser implementado de forma segura. O projeto básico de um produto também pode ter falhas. Por exemplo, podem ser selecionadas tecnologias inadequadas ou seu uso pode estar incorreto a partir de uma perspectiva da segurança. Idealmente, todos esses problemas serão identificados pelas revisões de projeto ou modelagem de ameaças durante as fases iniciais de desenvolvimento do produto.

As revisões de código de segurança têm principalmente o objetivo de detectar os problemas no nível de código que ainda causam a maioria das vulnerabilidades de segurança. Esses esforços também podem resultar na identificação dos problemas de projeto, mas tais problemas poderiam estar relacionados a melhorias necessárias na modelagem de ameaças e outros aspectos do processo de desenvolvimento. As revisões de código-fonte também podem ser realizadas com prioridades não relacionadas à segurança. Mas nesse contexto específico, o objetivo é detectar vulnerabilidades de código que poderiam ser usadas para violar garantias de segurança feitas pelo produto ou comprometer a segurança do sistema.

Um erro de código que pode causar potencialmente problemas de segurança (como problemas devido à ausência de validação) deve atender a condições específicas para que constitua uma vulnerabilidade de segurança. Deve haver um limite de segurança para o ataque, bem como o invasor precisa ter algum nível de controle em relação aos dados ou ao ambiente. Problemas que possam estar presentes no código executado no mesmo contexto de segurança do invasor não oferecem ganho potencial de privilégio ao explorar a vulnerabilidade. Em outros casos, pode haver vulnerabilidades, mas elas estão localizadas no código que não pode ser executado, pois não estão acessíveis ao invasor. Esses erros de código, embora possam afetar a confiabilidade do produto, não devem ser considerados vulnerabilidades reais. O objetivo final das revisões de código de segurança é detectar as vulnerabilidades de código que estão acessíveis a um invasor e que podem permitir que ele se desvie de um limite de segurança.

Observe que a acessibilidade de uma vulnerabilidade não é equivalente à sua capacidade de exploração; um ataque bem-sucedido ainda pode ser minimizado pelas melhorias de plataforma, como o sinalizador /GS, o sinalizador /SafeSEH ou ASRL (Address Space Layout Randomization).

A capacidade de exploração de uma vulnerabilidade de código não está no escopo das investigações realizadas durante as revisões de código, em especial por ser geralmente impossível comprovar que as atenuações de exploração disponíveis são suficientes. As responsabilidades de um revisor de código são encerradas com a confirmação de que há uma vulnerabilidade de código e de que essa foi adequadamente separada. A partir desse momento, o bug deve ser considerado simplesmente outro problema que requer correção.

A identificação de vulnerabilidades de código é o principal objetivo das revisões de código de segurança, mas o esforço pode apresentar outros resultados. Um revisor pode fornecer comentários sobre a qualidade geral do código, redundância, código inativo ou complexidade desnecessária. Os revisores também podem fornecer recomendações de melhorias na redução da superfície de ataque, validação de dados, limpeza de código ou legibilidade (como comentários de melhoria). No entanto, como a documentação de tais resultados é demorada, você deve decidir antes de iniciar se os resultados também devem incluir tais recomendações ou se os esforços devem ter o foco exclusivamente na identificação de problemas de segurança.

Detectando erros de código

Há diferentes abordagens para a detecção de erros de código. Como cada uma tem vantagens exclusivas e limitações práticas, é importante compreender a diferença entre as revisões de código e outras opções. Para os objetivos deste artigo, vamos pressupor que o código-fonte e a documentação do projeto estão disponíveis e que a chamada análise de “caixa branca” do produto será realizada (uma análise interna, ao contrário de uma abordagem de “caixa preta” que tem o foco somente no comportamento visível externamente).

A revisão de código pode ser descrita como uma abordagem manual e estática para a detecção de erros de código. Usando descrições semelhantes, outras duas abordagens conhecidas podem ser descritas como automatizadas estática e dinâmica. A abordagem automatizada estática geralmente ocorre na forma de ferramentas de análise estática de código, as quais operam no código-fonte na tentativa de identificar tipos conhecidos de problemas definidos usando padrões. PREfix e PREfast são exemplos desta abordagem. A abordagem automatizada dinâmica é representada pelas técnicas automatizadas de teste de código (como difusão), que têm como foco principal arquivos, protocolos e APIs. Embora essas soluções possam ser aplicadas também em uma abordagem de caixa preta, com freqüência resultados muito melhores podem ser obtidos se informações sobre elementos internos, como formatos de arquivo, estiverem disponíveis e forem usadas corretamente.

Cada abordagem tem determinadas vantagens e limitações práticas. As ferramentas de análise estática de código permitem que mais códigos sejam processados por meio da automação, mas as descobertas são rigorosamente limitadas pelo conjunto de padrões predefinidos de tipos conhecidos de problemas. Com freqüência, os resultados também podem conter vários falso-positivos que dificultam a solução de problemas com recursos limitados. O teste de difusão pode ser facilmente automatizado e realizado continuamente, mas opera, pelo menos, de forma parcialmente aleatória e pode ter problemas para atingir partes mais profundas do código. Na maioria dos casos, é relativamente fácil realizar a difusão básica, ainda que seja muito mais difícil atingir a cobertura completa de caminhos de código críticos.

Em comparação aos métodos automatizados, as vantagens e as desvantagens das revisões manuais de código-fonte estão principalmente associadas ao envolvimento direto de um revisor humano. Os resultados dos esforços podem ser significativamente diferentes, pois dependem da experiência do participante com tecnologias, arquiteturas e cenários específicos. Geralmente, é inteligente pressupor que um revisor humano ainda terá vantagens em relação a uma solução baseada em máquina. O revisor pode aprender durante o processo, compreender o contexto dos componentes de software e interagir com designers e desenvolvedores. Além disso, um revisor pode fornecer comentários que não estão limitados somente a um relatório detalhado do problema, mas também podem incluir recomendações de alto nível.

No entanto, ao mesmo tempo, o revisor está suscetível ao erro humano, fadiga ou tédio. O escopo de uma revisão é geralmente limitado, e pode ser difícil avaliar a qualidade da revisão, pois ela geralmente tem como base a confiança subjetiva do revisor.

Porém, essas desvantagens podem ser superadas. É possível desenvolver uma revisão específica por tarefa, com objetivo de analisar um bloco de código em várias passagens, visando um único tipo de erro por vez. Usando outra abordagem, vários revisores podem realizar revisões independentes de uma parte crítica do código, limitando assim a probabilidade de erro humano. As revisões de código são um dos casos específicos em que a redundância tem extremo valor potencial, pois permite a superação de limitações do envolvimento humano.

A revisão manual de código nunca deve ser considerada a solução definitiva para a detecção de vulnerabilidades de código ou como uma substituição a outras abordagens, mas sim como uma solução complementar. A revisão de código nunca deve ser introduzida para substituir a modelagem de ameaças, difusão ou aplicação das práticas recomendadas para códigos. Comparada com outras abordagens, a revisão de código geralmente é mais dispendiosa. Portanto, deveria ser usada principalmente no escopo de áreas e problemas mais críticos em que a eficácia de outras abordagens seja limitada.

O processo de revisão de código

A revisão de código de segurança terá mais êxito se for planejada e executada no contexto de outros esforços relacionados à segurança, como a modelagem de ameaças (veja a Figura 2). Além disso, os resultados das revisões de código podem agregar valor ao aprimorar outras tarefas de segurança, como teste e projeto.

Figura 2 Revisões de código
Figura 2 Revisões de código

O valor de bons modelos de ameaças para a revisão de código raramente pode ser supervalorizado. Para realizar uma revisão de código bem-sucedida, o revisor deve ter uma boa compreensão dos objetivos do produto, de seu projeto e das tecnologias usadas para sua implementação. As duas primeiras áreas também envolvem a modelagem de ameaças e, embora o seu foco seja a detecção de problemas de alto nível, elas incluem pesquisa também útil na revisão de código. Esses dois tipos de esforços de segurança são geralmente complementares; a modelagem de ameaças ajuda a identificar uma área crítica do código que se torna então um aspecto da revisão detalhada. Da mesma forma, os resultados da revisão de código podem validar (ou questionar) os pressupostos de segurança especificados em um modelo de ameaças.

Em uma situação ideal, uma revisão de código de segurança iniciaria com a revisão da qualidade dos modelos de ameaças e especificações de projeto para então passar para o código-fonte. Antes de iniciar, é necessário concluir todo o trabalho de desenvolvimento em código no escopo da revisão. Para detectar os problemas mais simples, as ferramentas de segurança disponíveis devem ser executadas no código antes de a revisão manual ser iniciada. Finalmente, para evitar a detecção de problemas já conhecidos, todos os bugs identificados anteriormente devem ser corrigidos.

A equipe de segurança deve planejar os esforços de revisão de código com antecedência suficiente no ciclo de vida de desenvolvimento, a fim de identificar o momento mais adequado para sua execução. Um plano real também deve abordar decisões organizacionais que possam afetar o retorno geral desse investimento. Por exemplo, há três opções prováveis para a seleção de participantes em uma revisão de código. Primeiro, os revisores podem ser externos ao código e ao produto (um especialista em revisão de código de uma equipe de teste de penetração). Segundo, um revisor pode ser externo ao código mas estar familiarizado com o produto (um desenvolvedor de outra equipe na mesma empresa). Finalmente, os revisores podem estar familiarizados com o código e o produto (como o desenvolvedor trabalhando no código).

Cada opção tem vantagens e limitações. No caso de revisores externos a um produto, geralmente são selecionados especialistas de segurança qualificados, podendo representar uma grande diferença graças às suas experiências e perspectivas exclusivas. No entanto, na maioria dos casos, eles terão um nível inferior de compreensão de detalhes de implementação e internos do produto em comparação aos membros da equipe do produto. Os desenvolvedores que escreveram os fragmentos específicos do código geralmente os compreendem melhor, mas eles também estão mais suscetíveis à influência de terem sido os criadores e, portanto, podem não detectar erros significativos de código.

Nenhuma dessas opções é uma solução definitiva que pode ser automaticamente aplicada a cada caso. A revisão de código depende mais de participantes humanos do que soluções técnicas. Portanto, precisa ser adotada pela equipe que foi designada para o esforço. Algumas equipes atingem os melhores resultados ao abordar e analisar o código em reuniões de grupo. Em outros casos, os desenvolvedores obtêm os melhores resultados examinando eles mesmos o código. A equipe deve escolher qual a melhor estratégia que permite aos participantes sejam eficazes na revisão de um produto. Essa geral regra não se aplica somente ao planejamento e à organização de uma revisão de código, mas a todos os desafios que precisam ser enfrentados em tal processo.

Priorizando os esforços de revisão

A regra mais importante para a revisão de código é saber que nunca haverá tempo suficiente para revisar todo o código que você gostaria de revisar. No entanto, um dos maiores desafios é selecionar e priorizar os componentes do produto ou fragmentos do código que pertencem ao escopo principal, ou pelo menos inicial, da análise. Para atingir esse objetivo, é necessário compreender o projeto de um produto e a função de tecnologias específicas utilizadas para implementá-lo. A priorização é uma análise de alto nível e define a estrutura de todo o contrato.

A priorização começa com a análise de modelos de ameaças e outras documentações disponíveis para um produto. Se adequadamente criado, um modelo de ameaças pode fornecer muitos dados úteis sobre as relações entre diferentes componentes, pontos de entrada, limites de segurança, dependências e pressupostos no projeto ou na implementação. Como o objetivo da priorização é obter a compreensão dos detalhes de implementação, as informações de um modelo de ameaças deve então ser comparada ao próprio código, bem como a análise das informações sobre a estrutura, a qualidade ou processo de desenvolvimento do código.

A priorização pode ser dividida em quatro tarefas principais, que são apresentadas na Figura 3 junto com exemplos de perguntas que podem ser úteis ao localizar as informações necessárias.

Figure 3 Tarefas e perguntas da priorização da revisão de código (Close Figure 3)

Compreenda o ambiente e as tecnologias usadas para implementar o produto
É um componente TCB (Trusted Computing Base): driver do kernel, subsistema ou serviço privilegiado?
É iniciado por padrão, co-hospedado ou executado como um processo dedicado?
É um plug-in, driver, codec, manipulador de protocolos ou analisador de arquivos reutilizável?
Ele é carregado e chama diretamente outro código (bibliotecas de terceiros)?
Quais tecnologias subjacentes são usadas (RPC, DCOM, ActiveX, WMI, SQL)?
Que linguagens (C/C++, C#, Visual Basic) e bibliotecas (STL, ATL, WPF) são usadas?
Enumere todas as fontes de dados de entrada não confiáveis (pontos de entrada)
Está disponível por meio da rede (TCP, UDP, SMB/NetBIOS, pipes nomeados)?
Usa comunicação IPC (LPC, seções compartilhadas, objetos globais)?
Pode ser orientada programaticamente (automação ou script)?
Que recursos utiliza (Registro, arquivos, bancos de dados)?
Tem uma IU, analisa argumentos da linha de comando ou usa variáveis do ambiente?
Comunica-se diretamente com hardware externo (driver USB)?
Determine quem pode acessar os pontos de entrada e em quais condições
É um limite de segurança que permite potencial elevação de privilégio?
Este ponto de entrada está limitado a acesso remoto, local ou físico?
Há restrições de acesso no nível de transporte, interface ou função?
Requer autenticação (anônima, autenticada, serviço, admin)?
Quais mecanismos são utilizados para autenticação, proteção de segredos e assim por diante?
Há outras restrições (listas de permissão/negação, certificados)?
Inclua contexto de código não técnico
É um código novo ou herdado? Ainda está no modo de desenvolvimento ou em manutenção?
O contexto de segurança foi alterado (código movido do modo de usuário para kernel)?
Qual é o histórico de esforços de segurança desta equipe de produto ou da organização?
Qual é o histórico da segurança do produto?
Qual é o nível de conscientização de segurança da equipe ou da organização?
Quais são os resultados de revisões de código anteriores?
Que tipo de problemas têm sido detectados usando ferramentas automatizadas? Onde?

A primeira tarefa está relacionada à compreensão do contexto tecnológico do código. Esse contexto sobre não somente as tecnologias específicas usadas em um produto, mas também o sistema operacional e dependências de terceiros, bem como as ferramentas utilizadas no desenvolvimento. O objetivo dessa tarefa é identificar as relações entre um produto e outros sistemas, aplicativos ou serviços. Com base nessas relações, é possível determinar de quais componentes um produto depende, bem como de qual outro software seu produto depende. No contexto de segurança, essas relações determinam como um produto afeta o restante do sistema e como ele pode ser afetado. Determinadas áreas de alto risco tornam-se visível por meio desse processo.

As áreas de alto risco estão geralmente associadas aos conceitos, como limites de segurança e vetores potenciais de ataque. Na prática, tudo isso pode ser reduzido a dados que podem ser controlados por um invasor, que deve ser considerado não confiável, e pontos de entrada a partir dos quais esses dados podem se originar. A equipe deve analisar cada um desses pontos de entrada em relação às garantias e pressupostos sobre os dados de entrada. Uma pergunta fundamental é quando e como os dados foram validados. Essa pergunta com freqüência pode estar relacionada às características reais dos dados (eles podem ser alterados assincronicamente ou ser enviados fora de ordem?). Ela também pode estar relacionada às características de um ponto de entrada, como se está disponível por padrão (um serviço de rede ou interface programática) ou se é criado como resultado da ação de um usuário (um clique ou a abertura de um arquivo de dados).

As características de um ponto de entrada também estão no escopo da próxima grande tarefa: análise das relações de confiança e controle de acesso. Um produto pode ter muitos pontos de entrada diferentes, mas sua superfície de ataque real é definida somente por aqueles que estão acessíveis a um invasor em um limite de segurança. Portanto, cada um dos pontos de entrada deve ser investigado para verificar quem pode acessá-lo e em quais condições. O acesso não autenticado à ampla funcionalidade é obviamente o mais interessante para um invasor do que os pontos de entrada acessíveis somente com privilégios administrativos. No último caso, a revisão pode estar limitada a apenas a parte do código responsável pela autenticação real (incluindo o código que a precede).

Por último, mas não menos importante, as fontes não técnicas também podem fornecer dados úteis para a priorização da revisão de código. O código é geralmente criado por equipes de desenvolvedores; as equipes em geral mudam ao longo do tempo à medida que novos membros entram e outros saem. O código também pode ter seu próprio histórico de segurança, às vezes não somente no escopo de um produto específico. Na maioria dos casos, é possível pressupor que o código mais recente é de melhor qualidade do que o código herdado, mas pode haver exceções. Conversar com os desenvolvedores, rever a documentação do código de revisões anteriores ou fazer amostragem de código também podem fornecer informações muito úteis. Para atingir a melhor utilização dos recursos disponíveis, a equipe de segurança deve usar quaisquer dados que possam ajudar os revisores a permanecerem eficazes durante o processo.

Táticas de revisão de código

Os desafios relacionados às revisões de código de aplicativos não triviais resultam principalmente do tamanho e da complexidade do software. Os produtos modernos são criados usando uma variedade de diferentes tecnologias, modelos de programação, estilos de código e diretivas de gerenciamento de código. Na prática, a documentação completa e atualizada raramente está disponível. Um revisor geralmente se depara com muito mais informações do que pode ser processado por uma pessoa em um período de tempo aceitável. Para lidar com a sobrecarga de informações durante a revisão de produtos complexos, foram desenvolvidas determinadas práticas, conhecidas como táticas. Abordaremos as duas mais comuns.

A primeira tática, que podemos chamar de análise contextual de código, é a conseqüência natural da priorização e esforços de análise de alto nível. Ela depende da análise precisa e com reconhecimento de contexto de fluxos de dados nas áreas de código diretamente envolvidas com o processamento de dados não confiáveis que foram passados em um limite de segurança. A vantagem dessa abordagem direcionada é a possibilidade de foco no código que tem mais probabilidade de ser atacado e, portanto, obter boa cobertura no escopo dos caminhos de código mais críticos.

A análise contextual de código pode ser dividida em duas passagens. A primeira é iniciada com um ponto de entrada e compreensão do nível de controle que um invasor tem em relação aos dados de entrada. A partir desse ponto, o desenvolvedor realiza uma análise de fluxo de dados, a fim de acompanhar como as variáveis e as estruturas de dados são alteradas com a execução do programa. A análise é executada em uma base por função, iniciando com argumentos de entrada e, em seguida, movendo todos os caminhos de execução, monitorando simultaneamente nos estados de variáveis e estruturas.

Cada função que é chamada e obtém os dados controlados por um invasor (surgindo de um ponto de entrada e ainda não validados) deve ser analisada da mesma forma. Da mesma forma, qualquer propagação de dados em estruturas de dados globais deve ser sinalizada, de forma que cada referência a ela possa ser posteriormente analisada. Nesse ponto, a navegação por meio do código tem o objetivo de aprimorar a compreensão do código, detectando sua estrutura e sinalizando locais para investigações detalhadas posteriores.

Obviamente, no caso das revisões de código de segurança, é necessário ter especial atenção às áreas de código com mais probabilidade de ter problemas de segurança. Os exemplos de tais locais são relacionados na Figura 4.

Figure 4 Caminhos de código de alto risco para revisão (Close Figure 4)

Identificação do usuário, autenticação e proteção de dados
Verificações de autorização e acesso
Código que pré-processa dados não confiáveis (analisadores de pacote de rede, leitores de formato)
Operações não seguras em buffers, cadeias de caracteres, ponteiros e memória dinâmica
As camadas de validação garantindo dados não confiáveis estão em formato válido
Código responsável pela conversão de dados não confiáveis em estruturas de dados internas
Lógica envolvida na interpretação de dados não confiáveis
Locais fazendo pressuposições sobre os próprios dados bem como comportamento de sua fonte
Código envolvido na manipulação de condições de erro
Uso de recursos de SO e rede (arquivos, Registro, objetos globais, soquetes)
Áreas problemáticas características ao ambiente no qual o código é executado
Uso de API problemática ou violações de contratos de API

Se o código especificado não processar dados que poderiam ter se originado de um ponto de entrada ou processar dados que já tenham sido validados, ele deixa de ser relevante de um ponto de vista de segurança e o revisor deve passar para outras áreas. No final do dia, o revisor deve ter uma compreensão geral do código do componente, bem como da lista de locais sinalizados como locais potencialmente interessantes no código. Usando essa lista, o revisor pode iniciar a segunda passagem de análise contextual, que tem foco em uma investigação mais profunda das áreas selecionadas de código.

Chamamos a segunda tática de revisão de código de análise de código como foco no padrão. Nesse caso, um revisor inicia em um ponto arbitrário do código e procura por tipos conhecidos de vulnerabilidades de segurança em potencial. Embora diferentes ferramentas de suporte possam ser aplicadas, “conhecidos” não precisam fazer referência a padrões formais, mas sim à experiência e à intuição individual do revisor. Para cada candidato à vulnerabilidade, um revisor segue todos os caminhos de código, a fim de determinar se o erro de código realmente representa uma vulnerabilidade – processando os dados que podem ser controlados por um invasor em um limite de segurança. Se a validação correta for identificada em qualquer nível, o erro não deve ser considerado uma vulnerabilidade de segurança, embora ainda possa ser identificado como um problema de defesa intensa ou que não seja de segurança que requer uma correção.

A análise de código com foco no padrão pode ser usada com compreensão limitada de um aplicativo, pois um revisor pode ter o foco na qualidade e exatidão dos fragmentos selecionados de código, em vez de sua função ou localização em um sistema. Isso permite a cobertura de uma quantidade significativa de código no escopo de padrões específicos como construções estranhas de código ou chamadas de API problemáticas. No entanto, a análise de erros de codificação em potencial isoladamente do contexto de um aplicativo inteiro geralmente não fornece informações suficientes para determinar qual é o problema real. Isso pode levar à não detecção de bugs ou à correção daqueles não relevantes, apresentando conseqüências a longo prazo ao aplicativo. Se um bug for descrito somente no contexto local, ele pode estar tentando introduzir uma correção local em vez de uma validação mais completa em um nível superior. Esse tipo de abordagem para a correção de erros de código pode resultar em validações redundantes, maior caos, desempenho reduzido e problemas gerais com o gerenciamento de código.

Tipos de vulnerabilidades
Recursos de segurança
  • Trustworthy Computing and SDL
  • MSDN Security Developer Center
  • The Security Development Lifecycle, de Michael Howard e Steve Lipner (Microsoft Press, 2006)
  • Modelagem de ameaças: Descoberta de falhas de design de segurança usando a abordagem STRIDE, de S. Hernan, S. Lambert, T. Ostwald e A. Shostack (MSDN Magazine, novembro de 2006)
  • 19 Deadly Sins of Software Security, de M. Howard, D. LeBlanc e J. Viega (McGraw-Hill Osborne Media, 2005)

Durante as revisões de código de segurança, tente manter uma perspectiva do invasor e verifique tudo que poderia estar sob o controle do invasor. A execução das revisões de código não deve estar limitada à verificação de operações de cópia de dados e APIs perigosas. Quando se trata de segurança, os detalhes são significativos e o histórico mostra que as menores falhas podem ser exploráveis. Esta seção contém exemplos de alguns tipos de erros de código, mas não há como abordar todos eles neste artigo. A barra lateral “Recursos de segurança” apresenta fontes mais abrangentes de informações sobre prováveis vulnerabilidades.

O conceito geral de vulnerabilidades do código de segurança está freqüentemente associado a estouros de buffer. No contexto do processamento de dados não confiáveis, os erros de código relacionados à variedade e ao tipo de dados ainda são uma fonte comum de problemas de segurança. No entanto, esse grupo de vulnerabilidades não está limitado somente a estouros de buffer. A condição de estouro de buffer está relacionada à cópia de dados em pilha, segmentos de dados ou heap sem preocupação com o seu tamanho, o que pode fazer com que a gravação vá além do intervalo definido e resulte em situações perigosas a partir de sobregravações de estruturas que controlam o fluxo do programa ou outros dados confidenciais.

Exemplos graves e igualmente comuns de erros de código relacionados à validação de intervalo são as leituras e gravações sem buffer. Esses problemas específicos com freqüência ocorrem quando usamos ponteiros recebidos de uma fonte não confiável (usando ponteiros como cookies ou valores de manipulação) ou índices e deslocamentos calculados com erro. Eles permitem que os invasores acessem locais arbitrários de memória do processo, como variáveis de dados ou ponteiros de função e causam alterações no comportamento do programa, incluindo execução de código arbitrário.

No escopo dos cálculos, muitas transformações em variáveis de tipo numérico (adição, subtração, multiplicação e assim por diante) podem fazer com que o cálculo deixe seu intervalo definido e se encapsule em segundo plano, levando a estouros ou estouros negativos de inteiro. Isso se torna um problema quando a variável é utilizada depois da transformação e, ao mesmo tempo, é usado em outro local não transformado ou transformado de forma diferente. Conseqüentemente, as verificações existentes podem ser insuficientes, levando a estouros de buffer ou leituras e gravações sem buffer.

Outros problemas estão relacionados ao sinal de variáveis de tipo numérico. Há muitas entidades abstratas que um programa trata e que não têm significado definido para valores negativos. Por exemplo, os comprimentos de byte das cadeias de caracteres ou contagens de elementos de matriz realmente não fazem sentido como valores negativos – e o uso de tipos assinados para manipular essas quantidades introduz vários casos problemáticos.

A conversão de tipo (explícita ou implícita) é outro exemplo de transformação potencialmente não segura de elementos de dados. A conversão pode tornar os itens maiores (em termos de bits) e, portanto, pode envolver a propagação de bits assinados em novos valores. Da mesma forma, a conversão de tipo também poderia truncar um valor. Novamente, os problemas geralmente surgem quando o item de dados é utilizado em um estado transformado em uma parte do programa e, ao mesmo tempo, é utilizado em um estado não transformado ou transformado diferentemente em outro.

Essas são vulnerabilidades de código de segurança específicas relacionadas ao uso de memória dinâmica. O comportamento do programa com elementos de dados não inicializados pode estar indefinido. Um invasor pode ser capaz de controlar algumas variáveis não inicializadas chamando APIs para obter ou definir as variáveis não inicializadas antes que a API as inicialize. O retorno de memória não inicializada a um invasor pode causar vários problemas. Como as alocações de heap podem se originar de um recurso que pode ser compartilhado por threads diferentes atendendo a diferentes contextos (clientes) de segurança, as informações confidenciais podem vazar de um cliente para outro. Isso também pode fornecer a um invasor informações úteis em uma exploração mais ampla de outras falhas de segurança, como auxiliar o invasor ao prever endereços. Outros problemas podem estar relacionados à liberação dupla de memória, ao uso da memória que já estava livre, a vazamentos ou exaustão de memória. As possíveis ramificações variam de negação de serviço e divulgação não autorizada de informações à execução de código malicioso.

Um grupo um pouco diferente de vulnerabilidades de código de segurança está relacionado aos erros de sincronização e tempo. Bons exemplos são as condições de corrida TOCTOU (time-of-check, time-of-use). As verificações de segurança e saneamento são inúteis se realizadas em dados ou recursos que podem ser alterados por um invasor depois da verificação mas antes do uso real. A validação precisa ser realizada em cópias particulares de dados que não podem ser alteradas assincronicamente. Os recursos precisam ser mencionados adequadamente, a fim de garantir que não serão excluídos ou substituídos.

Os ambientes multithread apresentam requisitos especialmente rígidos para código (componentes de kernel e serviços) em termos de sincronização de acesso a objetos e recursos compartilhados. As interfaces programáticas que permitem que os invasores acessem chamadas simultâneas de função ou que seu cancelamento assíncrono abra uma brecha para possíveis condições de corrida. Os problemas causados pela falta de bloqueios, cancelamento de bloqueios e pressuposição de que há um estado preservado, uso incorreto de operações interligadas ou uso de um conjunto desconectado de bloqueios podem levar a operações ou deadlocks de memória não seguros.

No caso de problemas de gerenciamento do tempo de vida do objeto, se o código disponibilizar um novo objeto a outros threads ao manipular dados globais (publicação), ele precisará estar em um estado consistente (em geral, totalmente inicializado). O gerenciamento adequado do tempo de vida de um objeto é determinado pelo mecanismo adequado de contagem de referência. A destruição de objeto danificado e lógica de limpeza podem fornecer ao invasor uma forma de descarregar o código precocemente (como um plug-in) e liberar memória que ainda esteja em uso.

Muitos erros de código de segurança não estão diretamente relacionados às manipulações de dados não confiáveis, mas sim à sua real interpretação ou influência sobre o comportamento e resultados do programa. Os exemplos clássicos são detectados em ataques de injeção que podem ocorrer quando os dados enviados por um invasor são utilizados para parametrizar algum outro conteúdo, como script, página da Web, linha de comando ou cadeia de caracteres de formato. A usar caracteres de escape ou seqüências de controle especiais, um invasor pode forçar dados não confiáveis a serem interpretados como parte de um script ou comando executado. Também surgem problemas quando dados não confiáveis são usados para construir nomes e caminhos a recursos que estão prestes a serem criados ou usados (arquivos, soquetes, Registro, seções compartilhadas, objetos globais). Os problemas de canonicalização ou navegação de diretório tornam o código vulnerável a todos os tipos de redirecionamentos que podem resultar no uso de recursos que estão sob o controle dos invasores ou divulgação não autorizada de informações confidenciais (por exemplo, segredos do usuário ou credenciais de rede).

As vulnerabilidades de segurança também podem resultar de pressuposições com falhas sobre a origem dos dados e destino (id de cliente, endereço de rede, id de sessão ou manipulação de contexto), a ordem na qual é originada (mensagens de rede, chamadas de API) e o valor (muitos ou poucos dados). Com freqüência, um invasor pode controlar, em determinada extensão, o ambiente no qual o código é executado criando objetos nomeados antes que o produto original os crie ou os utilize (nomeados ataques ilegais), preenchendo o espaço em disco ou bloqueando ou redirecionando a comunicação. A falha em lidar com essas situações pode tornar o código vulnerável à elevação de privilégios ou ataques de negação de serviço. Outros problemas comuns resultam do fato de se fazer pressuposições sobre as tecnologias usadas pelo código – seja sobre suas garantias de segurança (integridade do canal de comunicação) ou sobre os funcionamentos internos de suas APIs (fornecendo a combinação incorreta de parâmetros e não verificando os valores de retorno, em vez de honrar os contratos de API).

Os resultados das revisões de código com freqüência não estão limitados à detecção de problemas classificados como problemas de nível de código. Os revisores operam no nível da implementação real e, embora possam se beneficiar do uso da documentação de nível de projeto, incluindo especificações e modelos de ameaças, eles não devem confiar nesses dados. Portanto, as revisões de código ainda levam à identificação de problemas no nível do projeto ou inconsistências entre as especificações e a implementação. Os tipos de problemas comumente encontrados incluem a exposição de funcionalidade perigosa, implementação incorreta de protocolo, uso de mecanismos personalizados de pseudo-segurança (como esquemas de autenticação e verificações de acesso) ou identificação de formas de desvio das barreiras de segurança.

Processando os resultados

Uma revisão de código de segurança não poderá ser considerada bem-sucedida se a segurança do produto não for aprimorada. A revisão de código deve fornecer resultados que sejam úteis para que as equipes de desenvolvimento façam alterações significativas em um produto. A realização desse aperfeiçoamento com base na revisão está conectada a dois requisitos práticos de documentação completa e separação precisa.

A documentação de cada uma das vulnerabilidades de código de segurança identificadas deve conter todos os detalhes necessários para localizar e compreender o problema. Os detalhes devem incluir ponteiros para o código com falha, uma explicação do problema e a justificativa da existência dessa vulnerabilidade. Oferecer recomendações para uma correção é uma prática útil, mas a seleção e o preparo da solução real é responsabilidade dos proprietários do código. Se houver dados faltando ou não estiver claro por que um erro de código apresenta uma vulnerabilidade de segurança, será necessária pesquisa adicional da equipe do produto, o que nem sempre é possível devido a restrições de recursos e tempo.

Outro importante requisito está relacionado à separação precisa de vulnerabilidades de código. Se a gravidade de um problema for definida como baixa, ele poderá não ser corrigido pela equipe do produto. Por outro lado, se a gravidade for definida como alta, o problema pode ser selecionado para correção em vez de outro problema com impacto prático superior. O processo de separação depende muito da qualidade do limite do bug de segurança, mas também de uma compreensão das prioridades dos revisores de código (que fazem a separação) e dos desenvolvedores (que atuam nos resultados da separação). Como mencionado anteriormente, a separação dos erros do código de segurança não deve ser afetada pela disponibilidade dos mecanismos de prevenção de exploração.

Resumindo

Os esforços da revisão de código podem fornecer informações mais úteis do que apenas uma lista de problemas de segurança. Se possível, os revisores também devem documentar a cobertura do código, a segurança em áreas de código específicas e recomendações gerais para um novo projeto e limpeza do código. As revisões de código também são oportunidades únicas para enriquecer o conhecimento organizacional, aumentando a conscientização da segurança e aperfeiçoando a eficácia das ferramentas de segurança. Por fim, mas definitivamente não menos importante, as equipes de desenvolvimento podem usar os resultados das revisões de código para ajudar a priorizar futuros esforços de segurança de produto.

Garantimos que o software sempre terá vulnerabilidades de segurança, embora a natureza dessas vulnerabilidades e impacto prático mudem com o tempo. As ferramentas automatizadas de segurança podem identificar cada vez mais erros de código, mas algumas vulnerabilidades ainda passarão despercebidas (não detectadas ou ocultas sob grandes números de falso-positivos). A análise manual de código-fonte não substitui essas ferramentas testadas e aprovadas, mas com freqüência pode ser integrada a elas para se obter mais vantagens.

A abordagem de revisão manual de código é dispendiosa, difícil e extremamente dependente da experiência e do comprometimento dos participantes. No entanto, em muitas situações, o projeto requer esse investimento para obter certeza aceitável sobre a segurança de um produto ou de seus principais componentes. Um revisor humano experiente ainda será capaz de identificar problemas que não seriam detectados pelas ferramentas. Enquanto o ser humano for a causa dos problemas de segurança, ele também fará parte da solução.

Michal Chmielewski trabalha como engenheiro de software de segurança na equipe SWI (Secure Windows Initiative) da Microsoft. Ele é especialista em analisar vulnerabilidades de segurança em produtos.

Neill Clift trabalha como engenheiro de software de segurança na equipe SWI da Microsoft. Ele é especialista nas questões relacionadas a kernel e driver.

Sergiusz Fonrobert atualmente é engenheiro de software de segurança na equipe SWI, responsável pela execução de testes de penetração de vários produtos Microsoft.

Tomasz Ostwald trabalha atualmente como gerente de programas de segurança da equipe SWI, em que ele realiza Revisões finais de segurança de produtos lançados pela Microsoft.


Teste de difus&#227;o: Crie um provedor de interface de teste personalizado para o Team System

Teste de difusão

Crie um provedor de interface de teste personalizado para o Team System

Dan Griffin
Download do código disponível em: Fuzzing2007_11.exe (301KB)
Este artigo aborda:
  • Visual Studio Team Edition for Testers
  • Teste de difusão
  • Gravando um provedor de interface de teste
  • Criação, teste e implantação de um difusor
Este artigo utiliza as seguintes tecnologias:
Visual Studio 2005 SDK, VSTS
Conteúdo
O que é o Visual Studio Team System?
O que é o teste de difusão?
Gravando um provedor de interface de teste
Compilar
Implantar
Depurar
Temos um difusor...

D
urante minha participação na conferência da Microsoft® Tech•Ed 2007 em Orlando, tive o privilégio de trabalhar em um dos estandes do desenvolvedor no The Learning Center. O que me impressionou mais sobre a experiência foram os rumores sobre as mais recentes ferramentas de ALM (Application Life-Cycle Management - Gerenciamento do ciclo de vida de aplicativos). Também havia muitas conversas sobre as metodologias mais recentes, como Agile Programming e TDD (desenvolvimento controlado por testes). E, como resultado, houve tremendo interesse na mais recente plataforma ALM da Microsoft, os produtos Visual Studio® Team System (VSTS).

O VSTS oferece alguns excelentes recursos e oportunidades de extensibilidade para testadores; esse é o foco deste artigo. Tendo tido isso, devo salientar que esse não é um artigo sobre TDD! Em vez disso, mostrarei como estender os recursos de teste de software do VSTS. Utilizarei meu exemplo como um subconjunto dos testes de segurança, conhecido como “difusão” (mais detalhes posteriormente neste capítulo).

O que é o Visual Studio Team System?

O componente do lado do servidor do VSTS é denominado Team Foundation Server (TFS). O componente do lado do cliente pode estar em qualquer uma das team editions do Visual Studio. Para obter mais informações sobre VSTS e TFS, verifique msdn2.microsoft.com/teamsystem.

A partir de uma perspectiva técnica, os produtos Visual Studio Team Edition (VSTE) compreendem um conjunto de extensões para Visual Studio 2005 IDE. As diferentes edições fornecem extensões relevantes para uma disciplina específica – Architect, Developer, Tester, Database Professional. O objetivo é unir a funcionalidade específica de função a uma interface do usuário consistente.

A grande vantagem do Visual Studio é que ele se tornou prontamente mais extensível com cada nova versão. O VSTE é um exemplo disso; no entanto, um teste melhor da extensibilidade do produto é o quão facilmente ele pode ser realizado por qualquer um que não faça parte da empresa que o criou.

Para esse fim, minha discussão aqui da extensibilidade do Visual Studio 2005 Team Edition for Software Testers irá explorar a modificação do exemplo de TIP (provedor de interface de teste) existente, incluído no Visual Studio SDK mais recente. Também verei em mais detalhes o teste, a implantação e a depuração do plug-in. Minha meta final é um TIP que implemente uma estratégia de teste que se tornou recentemente popular, especialmente entre os testadores de segurança: o teste de difusão.

O que é o teste de difusão?

Uma boa forma de explicar o teste de difusão (difusão) é através do exemplo. Atualmente, a difusão é mais freqüentemente utilizada para validar dois tipos de analisadores de dados: arquivo e rede.

Um exemplo de um analisador de arquivo é o Microsoft Word. O que o teste de difusão de um analisador de arquivo do Word envolve? Imagine criar um milhão de documentos do Word de extensão aleatória contendo dados aleatórios. Suponha que esses documentos não são criados pelo próprio Word, mas em vez disso pela separação literalmente da origem aleatória de dados binários em arquivos de teste no disco. Abra cada um desses documentos aleatórios no Word e veja então o que acontece.

É provável que haja três resultados potenciais desse experimento. Primeiro, para a maioria dos arquivos de teste, o Word fornecerá algum erro de que o formato de arquivo não é reconhecido ou que o arquivo está corrompido e não pode ser aberto. Segundo, alguns poucos arquivos de um milhão podem de fato conter alguma combinação de controle reconhecível e caracteres imprimíveis. Esses arquivos simplesmente serão abertos sem quaisquer problemas. Terceiro – e mais interessante – pode ocorrer que alguns arquivos tenham dados que o analisador de documentos não espere. Nesse caso, o programa pode falhar.

Para um testador de segurança, essa terceira classe de documentos de teste é realmente interessante. Se o analisador de arquivos falhar, isso significa que há um bug nele. E, se houver um bug, ele pode ser explorável. Por exemplo, o bug pode resultar em uma corrupção de heap ou pilha levando à falha. Em geral, esse tipo de bug é de grande preocupação em um ambiente de rede, pois uma ampla variedade de tipos de arquivos pode ser distribuída como anexos de email. A experiência mostra que a maioria dos usuários abrirá um anexo, independentemente de sua natureza.

Um problema de escopo semelhante reside no território de analisadores de rede. Considere o número enorme de protocolos compreendidos por um PC, servidor ou dispositivo de rede incorporado característico: DHCP (Dynamic Host Configuration Protocol, Protocolo de configuração dinâmica de hosts), DNS, HTTP, SMB (Server Message Block, Bloco de mensagens do servidor), SMTP e assim por diante. Cada implementação de protocolo inclui um analisador para os dados do pacote com origem na rede, e cada analisador provavelmente envolve alguma lógica complexa. Esse tipo de código também tende a ter vários casos extremos, tornando difícil sua validação.

Uma abordagem para o teste de código de protocolo de rede é por meio da difusão. No entanto, nesse caso, proponho uma variação na abordagem “just-throw-random-data-at-it” (apenas lance dados aleatórios), que descrevi no contexto do analisador do Word anteriormente.

Suponhamos que eu queira testar um cliente DHCP. Suponha ainda que haja uma parte específica do código do cliente DHCP que é especialmente nociva, mas que essa parte do código está de certa forma protegida da rede por outra parte do analisador que é considerada mais robusta. Nesse caso, é provável que a difusão do cliente DHCP com dados puramente aleatórios não seja um uso eficiente de tempo, pois espero que a parte robusta do código filtre a maior parte dos dados malformados. Em vez disso, utilizo uma abordagem mais direcionada.

Na verdade, na minha experiência, a difusão é mais eficaz quando realizada dessa maneira. Continuando no exemplo DHCP, em vez de bombardear o cliente com dados puramente aleatórios, configurei meu teste de difusão (difusor) para usar uma combinação de dados válidos conhecidos, dados intencionalmente inadequados e dados aleatórios. A finalidade dos dados válidos conhecidos é desviar os componentes do analisador que não me interessa. A finalidade dos dados intencionalmente inadequados é aproveitar o que sei ou suspeito que seja uma falha específica no código. E, finalmente, o objetivo dos dados aleatórios é apenas ver o que ocorre.

Durante o curso da difusão, meu método evolui à medida que refino meu comportamento suspeito. Por exemplo, onde parte do meu pacote de DHCP de teste originariamente incluir dados aleatórios, posso subseqüentemente usar dados válidos conhecidos para me aprofundar no analisador. Se eu detectar um erro, posso enviar intencionalmente dados inadequados para tentar explorá-los. Essa abordagem heurística me fornece o melhor dos dois mundos: intuição humana mais automação com máquina.

A difusão é realmente uma ferramenta poderosa, mas ainda está no território relativamente não transformado em relação à cobertura do teste. Portanto, chamo a atenção para isso (a ser implementado depois da leitura do restante do artigo, é claro): escolha uma tecnologia com a qual você esteja familiarizado ou pelo menos interessado e escreva um difusor para ela. Mais provavelmente sim do que não, você encontrará bugs.

Veja um exemplo: participei de uma inteligente demonstração de difusão na conferência da RSA neste ano em São Francisco. Isso me inspirou a voltar correndo para o hotel e escrever um difusor para uma tecnologia com a qual trabalhei muito. Usando um difusor personalizado, apliquei a abordagem iterativa descrita anteriormente – iniciei com dados aleatórios e, em seguida, ajustei-a – e tive êxito em apenas algumas horas. Como usuário, pude causar uma violação de acesso na execução do código como LocalSystem.

Gravando um provedor de interface de teste

O código de exemplo que acompanha este artigo é uma implementação de um difusor simples. A intenção é dupla: mostrar o teste de difusão em ação e mostrar um exemplo da integração do teste de difusão com a estrutura de automação VSTE.

O código de amostra contém os quatro projetos seguintes :

  1. ConsoleApp – este é o aplicativo sendo testado. O código é breve e importante. Esse aplicativo responde por meio da geração de uma linha de texto ou lançamento de uma exceção não tratada.
  2. MyTest – este é o provedor de tipo de teste de personalizado. O código praticamente não é alterado a partir do exemplo, exceto que renomeie o MyTestAdapter como FuzzTestAdapter e adicionei todo o código para realizar o teste real.
  3. MyTestUI – o código C++ para a interface do usuário de teste personalizado. Não é alterado a partir do exemplo original.
  4. TestProject – o projeto inicial para a depuração. É fácil de recriar: basta criar um novo projeto de teste e, em seguida, adicionar o tipo de teste personalizado.

Abordarei esses projetos com mais detalhes posteriormente neste artigo.

O Visual Studio 2005 SDK é necessário para criar o exemplo. O SDK pode ser obtido por download de microsoft.com/downloads/details.aspx?familyid=51A5C65B-C020-4E08-8AC0-3EB9C06996F4. A versão atual do SDK é 4.0, com data de 28/02/2007. Ela inclui o VSTE TIP de exemplo original a partir do qual o meu é baseado; no entanto, tive problemas para obter o exemplo original para a criação, instalação e execução. Embora as alterações necessárias para transformar o meu exemplo existente "MyTest" no exemplo "FuzzTest", abordado neste artigo, tenham sido poucas, o esforço necessário para chegar ao ponto de depuração do plug-in de execução não o foi. A ajuda da equipe do produto, bem como várias tentativas e erros, ajudaram a resolver esses problemas. Descreverei as etapas mais detalhadamente a seguir.

Novamente, as alterações que fiz no exemplo original MyTest para transformá-lo no exemplo FuzzTest que acompanha este artigo foram fáceis de realizar. Na verdade, a execução do windiff.exe com o comutador –T para comparar as árvores do diretório de exemplo modificado e original mostra uma lista breve de alterações interessantes e instrutivas.

A primeira alteração necessária foi obter dois arquivos que estão faltando do exemplo base MyTest, pelo menos na versão do Visual Studio SDK mencionada anteriormente. Os arquivos são mytest.ico e mytestui.rc. Ambos os arquivos me foram fornecidos pela equipe de produto VSTS e estão incluídos no meu código de exemplo FuzzTest. Ambos vão para o subdiretório mytestui da solução.

A segunda alteração de código foi no arquivo do projeto mytestui. Especificamente, um comando pós-compilação estava usando um caminho relativo e foi predestinado a falhar quando tentei compilar o projeto de exemplo em um diretório que não era seu local padrão (ou seja, na árvore de instalação do SDK). O comando em questão inicia o compilador Command Table Configuration (ctc.exe), uma ferramenta incluída com o Visual Studio SDK. Para obter mais informações sobre arquivos CTC, consulte msdn2.microsoft.com/bb165048.

Para solucionar esse problema, modifiquei o projeto para iniciar o ctc.exe a partir de seu caminho absoluto padrão – uma abordagem que claramente tem seus problemas (por exemplo, se o SDK não estiver instalado na unidade C:) – mas, pelo menos, você saberá o que procurar!

Como uma exceção, menciono as alterações anteriores, pois elas foram relevantes não somente para o exemplo FuzzTest, mas também para obter a compilação de amostra original no caso de se desejar testes adicionais. Há algumas questões importantes sobre implantação e depuração que você deve saber também, mas tratarei disso posteriormente no artigo.

Ao transformar o exemplo base MyTest em um novo plug-in de teste, a próxima etapa foi substituir a classe MyTestAdapter original (em mytest\myhostadapter.cs, que removi) pela nova implementação FuzzTestAdapter em mytest\fuzztestadapter.cs. A cópia desses dois arquivos para um diretório temporário e a execução de windiff.exe em mytestadapter.cs e fuzztestadapter.cs mostra basicamente todas as alterações de código principal necessárias para criar o novo TIP. E, na verdade, a maioria das alterações interessantes estão na função Run (consulte a Figura 1).

Figure 1 Implementação do método IBaseAdapter::Run (Close Figure 1)
[SuppressMessage("Microsoft.Design", "CA1031")]
public void Run(ITestElement testElement, ITestContext testContext)
{
MyTestAssertHelper.ParameterNotNull(testElement, "testElement");
MyTest test = testElement as MyTest;
MyTestAssertHelper.ParameterNotNull(test, "testElement");
MyTestResult result = new MyTestResult(
       new ComputerInfo(Environment.MachineName), m_runId, test);
Stopwatch timer = null;

try
{
// Start the timer for this test.
timer = new Stopwatch();
timer.Start();

// This is a FUZZ Test: Loop until the test breaks, then report back
// loop through unicode chars and pass into method
bool ErrorFound = false;
char ch;
for (int i = 0; i < char.MaxValue; i++)
{
       ch = Convert.ToChar(i);
       string testParam = ch.ToString();
       string CommandLine = test.CommandLine.Replace(
              "<FuzzTestString>", "\"" + testParam + "\"");
       int spaceIndex = CommandLine.ToUpper().IndexOf(".EXE ") + 4;

       Process p = new Process();
       p.StartInfo.FileName = CommandLine.Substring(0,spaceIndex);
       p.StartInfo.Arguments = CommandLine.Substring(spaceIndex+1);
       p.StartInfo.UseShellExecute = false;
       p.StartInfo.CreateNoWindow = true;
       p.StartInfo.RedirectStandardInput = true;
       p.StartInfo.RedirectStandardOutput = true;
       p.StartInfo.RedirectStandardError = true;
       p.Start();

       StreamWriter sIn = p.StandardInput;
       StreamReader sOut = p.StandardOutput;
       StreamReader sErr = p.StandardError;
       sIn.AutoFlush = true;
       sIn.Write(Environment.NewLine);
       string OutputString = sOut.ReadToEnd();
       string ErrorString = sErr.ReadToEnd();
       p.WaitForExit();

       if (ErrorString != "")
       {
       // Record the process exit code.
       ErrorFound = true;
       result.ProcessExitCode2 = -1;//failed
       result.Outcome = TestOutcome.Failed;
       result.ErrorMessage =
              "Test failed with input: " +
              CommandLine +
              ", Error Message: " +
              ErrorString;
       timer.Stop();
       break;
       }
}

if (!ErrorFound)
{
       // all tests succeeded:
       result.ProcessExitCode2 = 0; // success
       result.Outcome = TestOutcome.Passed;
       timer.Stop();
}
}
catch (Exception ex)
{
       result.Outcome = TestOutcome.Failed;
       result.ErrorMessage = ex.ToString();
}
finally
{
       if (timer != null)
       {
       timer.Stop();
       result.Duration = timer.Elapsed;
       }
}
testContext.ResultSink.AddResult(result);
}

A análise do código para a função Run na Figura 1 revela como o novo FuzzTestAdapter funciona. Em resumo, quando um teste do tipo FuzzTestAdapter está para ser executado no VSTE, a primeira etapa (depois de certa inicialização de objeto logo no início) é iniciar um timer. A duração da execução do teste não é necessariamente a estatística mais interessante relacionada a um teste de difusão (em oposição a, digamos, um teste de desempenho), mas é compatível como um recurso interno da classe TestResultMessage. Portanto, a mantive.

A próxima etapa em Run é extrair a opção de configuração da linha de comando para a instância de teste de destino, que indica como executá-la. Observe que, seguindo o caminho completo para o binário de teste de destino, o plug-in deve localizar uma cadeia de caracteres de espaço reservado, "<FuzzTestString>". A cadeia de caracteres como um todo é recuperada pelo membro CommandLine da classe MyTest. A finalidade dessa cadeia de caracteres é expor ao usuário uma forma conveniente de reutilizar o TIP para diferentes testes. Isso se torna ainda mais conveniente no contexto do meu TIP difusor, pois a linha de comando é alterada sempre que o teste faz um loop.

Continuando com a descrição do método Run, a próxima etapa é instanciar um objeto System.Diagnostics.Process, a fim de iniciar o programa de teste na máquina local. Se, como resultado da entrada de teste (ou por qualquer outro motivo), o processo de teste lança uma exceção ou retorna um erro, uma falha no teste será sinalizada. Observe que adornei minha implementação do método Run com um atributo [SuppressMessage]. Esse atributo é necessário nesse contexto, pois a execução do processo de teste é incluída por um manipulador que detectará todas as exceções. Essa é geralmente considerada uma prática inadequada e, é por padrão, sinalizada como tal pelo analisador de código estático VSTS, mas é necessária nesse caso para garantir que todos os cenários de conclusão de teste não positivos sejam detectados e rastreados como falhas.

Para resumir a discussão de Run e para descrever o tipo específico de teste de difusão implementado pelo meu TIP, observe na Figura 1 que a maior parte da lógica descrita nos dois parágrafos anteriores é encapsulada em um loop que itera através de cada valor possível que pode ser obtido pelo tipo de caractere gerenciado. O resultado líquido é que o binário de teste (neste exemplo, ConsoleApp.exe) é chamado com cada valor de caractere possível, continuando até que o loop seja concluído ou programa de teste falhe para tratar corretamente um dos valores nesse intervalo. Naturalmente, por segurança do demonstrativo, garanti que o teste falhará; caso contrário, ele não seria um exemplo interessante!

O teste ConsoleApp demonstra como a difusão pode ser usada para expor erros mesmo nas interfaces programáticas simples. Nesse caso, a interface foi escrita com base em uma pressuposição implícita e de risco sobre o intervalo de valores de caractere a ser tratado. O trecho a seguir, adaptado do código ConsoleApp, mostra essa pressuposição:

Int32 arraySize = Math.Max(Math.Max('Z', 'A'), Math.Max('z', 'a')) + 1;
charCountArray = new Int32[arraySize];
char c = inputString[0];
charCountArray[c] += 1;

Nesse código, a variável inputString é o segundo argumento da linha de comando recebido do FuzzTestAdapter. Qualquer valor de caractere fora do intervalo de letras maiúsculas e minúsculas do alfabeto ASCII resultará um erro “índice de matriz fora da baixa”. Como o difusor faz loop através de seus valores de caractere, ele chegará rapidamente em um que faça com que o código falhe. Quando isso ocorrer, o processo ConsoleApp será concluído com uma exceção não tratada. Esse resultado é recuperado pelo difusor por meio de objeto p do processo e retornado para VSTE.

Assim que a classe MyTestAdapter no código de exemplo original for substituída pela classe FuzzTestAdapter, tudo que restou para fazer feito na minha implementação foi renomear as referências para o anterior pelas referências para o último.

Agora que expliquei as alterações necessárias para transformar o exemplo TIP original no meu FuzzTestAdapter, vejamos a compilação, implantação e depuração.

Compilar

Para compilar a mesma solução FuzzTestSample.sln, você deve abri-la no VSTE e, então selecionar Build | Build Solution do menu de nível superior. Se você quiser iniciar com a solução de amostra básica incluída com o SDK, você precisa primeiro obter os dois arquivos faltando que estão incluídos com o download do código que acompanha este artigo.

Implantar

O exemplo de TIP original e a versão do difusor atualizada incluem um script de lote de implantação. O script é a forma recomendada de instalar os binários TIP para teste. O script pressupõe que ele está sendo executado na mesma máquina na qual que os binários foram compilados a partir de uma janela de comando do Visual Studio.

Antes de executar o script (mas depois de compilar a solução, é claro), detectei que é mais seguro desligar o Visual Studio para a fase de implantação. Isso é claramente inconveniente, mas se eu não tivesse realizado essa etapa, algumas das operações de cópia de arquivo durante a implantação teriam falhado.

Para executar uma janela de comando do Visual Studio, selecione Iniciar | Todos os Programas | Microsoft Visual Studio 2005 | Visual Studio Tools (Ferramentas do Visual Studio) | Visual Studio 2005 Command Prompt (Prompt de Comando do Microsoft Visual Studio 2005). Em seguida, execute o script de instalação deploy.bat. Fiz poucas alterações no script; você pode achar que o original precisa de alguns ajustes também, dependendo de onde você executá-lo. Verifique os caminhos relativos utilizados nos comandos copy e xcopy para implantação dos binários. Além disso, observe que o último comando executado pelo script é uma chamada para regpkg.exe, com base em um caminho relativo para o diretório Visual Studio Integration Tools no SDK. Alterei isso para um caminho absoluto usando a variável de ambiente %ProgramFiles% do Windows, com base na pressuposição de que o diretório de instalação padrão para o SDK foi usado.

Finalmente, não esqueça dessa última etapa, pois não é parte do arquivo de lote! A partir da janela de comando do Visual Studio, execute o comando devenv /setup. Esse comando garante que os metadados de recursos do seu plug-in serão mesclados com os metadados de outros pacotes instalados do Visual Studio e concluirá a instalação do novo plug-in.

No preparo deste artigo, perguntei ao grupo do produto Visual Studio como o TIP é de fato utilizado. Cada tipo de teste deve definir um TIP, que será instanciado pela TMI (Test Management Interface). A TMI usará essa TIP para carregar os testes a partir do armazenamento, criar novos testes, salvar testes e interpretar resultados.

Depurar

Assim que a implantação for concluída, a próxima etapa será criar e executar um teste do novo tipo. Aqui, as etapas são usadas para realizar o seguinte:

  1. Reabrir a solução FuzzTestSample no VSTE.
  2. Certifique-se de que o TestProject seja definido como o projeto de início padrão. Para fazer isso, clique com o botão direito do mouse em TestProject na janela Solution Explorer e selecione Set as Startup Project (Configurar projeto inicial). Depois de fazer isso, o nome TestProject deve aparecer em negrito no Solution Explorer, como mostrado no lado direito da tela na Figura 2.
  3. Clique novamente com o botão direito do mouse em TestProject no Solution Explorer e selecione Add | New Test (Adicionar|Novo teste).
  4. Na janela New Test (Novo teste) que aparece, você deve ver um tipo de teste intitulado My Test. Esse é o novo tipo de teste do TIP de exemplo e sua presença implica que as etapas de registro tenham tido êxito. Selecione o ícone My Test e clique em OK.
  5. No campo Command Line (Linha de comando) do novo teste, substitua a entrada padrão com o caminho completo para o programa de teste ConsoleApp.exe, anexado com os valores da linha de comando esperados (como discutido na seção anterior em relação à rotina Run). Por exemplo, você pode inserir "c:\projects\FuzzTestSample\ConsoleApp\bin\Debug\ConsoleApp.exe ProcessString <FuzzTestString>". Veja o painel esquerdo central na Figura 2.
  6. Execute o teste pressionando a tecla F5.

Figura 2 VSTE com o resultado do teste de exemplo
Figura 2 VSTE com o resultado do teste de exemplo(Clique na imagem para diminuir a exibição)
Figura 2 VSTE com o resultado do teste de exemplo
Figura 2 VSTE com o resultado do teste de exemplo (Clique na imagem para aumentar a exibição)

Essa abordagem tem a vantagem de executar o TIP no depurador. Por exemplo, antes de pressionar F5, eu posso definir um ponto de interrupção na função Run em MyTest\MyTest\FuzzTestAdapter.cs e verificar se ele inicia o ConsoleApp.exe com os vários valores de caractere. É claro, em produção, eu não esperaria que o plug-in fosse executado no depurador, pois ele torna a execução do teste consideravelmente lenta.

Esse teste específico demora uns minutos para ser concluído. Assim que for concluído, o painel de falha de teste – mostrado na parte inferior da tela na Figura 2 – se torna visível. A partir do texto de erro mostrado na falha de teste, está claro que um caractere não alfabético causou uma exceção IndexOutOfRange, exatamente como esperado.

É válido mencionar o método que aprendi para fazer alterações de código no tipo de teste personalizado depois de uma implantação inicial bem-sucedida. Com base na experimentação, essa seqüência de etapas parece ser a mínima necessária para reimplantar o plug-in de forma que possa ser depurado novamente.

  1. Faça as alterações desejadas de código e recompile a solução (a incrementação é adequada).
  2. Desligue o Visual Studio.
  3. Copie o arquivo atualizado, Microsoft.VisualStudio.QualityTools.Samples.MyTestTIP.dll, do diretório de resultado da solução para seu local implantado. Por padrão, o último será: C:\Arquivos de programas\Microsoft Visual Studio 8\Common7\IDE\PrivateAssemblies.
  4. Reabra a solução no Visual Studio.
  5. Adicione um novo teste (por exemplo, MyTest2.mytest).

Temos um difusor...

A automação eficaz é considerada o principal objetivo da maioria das equipes de garantia de qualidade de software. E a detecção de muitas classes de defeitos, incluindo bugs de segurança, comprovou-se ser especialmente de difícil automação. Uma abordagem heurística, como o método de difusão adaptativo descrito no início do artigo, combinada com uma estrutura de automação, como o Visual Studio Team Edition, fornece o melhor dos dois mundos. Ou seja, a intuição humana, além da tolerância da máquina quanto à repetição entediante, é igual a uma boa cobertura de teste!

Gostaria de agradecer a Euan Garden e Jeff Wang da Microsoft, bem como a Juan Perez da Personify Design, por seus comentários sobre este artigo.

Dan Griffin é consultor de segurança de software em Seattle, WA. Ele atuou por sete anos na Microsoft, na equipe de desenvolvimento de segurança do Windows. Dan pode ser contatado pelo endereço jwsecure.com.


Nota do editor: Por que chegar a extremos?

Nota do editor
Por que chegar a extremos?
Howard Dierking

"O seu sistema é seguro?" Esta pergunta, assim como a pergunta "Qual é o grau de segurança do seu sistema?", parece ser uma das perguntas mais engraçadas normalmente feitas por empresários e profissionais da área de TI. A pergunta é engraçada, não que segurança não seja importante £ pelo contrário, atualmente segurança é mais importante do que nunca £ mas porque ela é muito subjetiva para ter uma resposta mensurável.

Nos dois casos, a resposta mais concreta que se pode oferecer é: "mais seguro que alguns, menos seguro que outros." Obviamente, esta resposta não satisfaz um empresário e, por isto, nunca é oferecida. Em vez disso, acabamos por definir segurança em termos extremamente absolutos. Em um extremo, focamos somente nas necessidades corporativas do sistema, confiando no departamento de TI para fornecer uma lista de tarefas relacionadas à segurança que executamos antes de implementar o sistema. Em outro extremo, perdemos incontáveis quantidades de tempo e energia pesquisando e garantindo a segurança contra qualquer exploração possível que tenha sido efetuada na história da indústria de softwares £ muito além das mais restritivas normas DoDs £ e muitas vezes subjugando as necessidades dos negócios para favorecer as necessidades de nossa paranóia.

Na edição sobre segurança deste ano da MSDN Magazine, estou muito contente em perceber a ênfase em uma abordagem pragmática para criar uma estratégia de segurança mensurável e eficiente. Michael Howard iniciará com uma discussão a respeito das melhores e mais práticas metodologias que aprendeu ao longo dos últimos cinco anos desenvolvendo softwares de segurança para a Miscrosoft. Depois da discussão de Howard sobre as práticas recomendadas, Michal Chmielewski, Neill Clift, Sergiusz Fonrobert, e Tomasz Ostwald debaterão sobre técnicas para integração de atividades relacionadas à segurança £ tanto automatizadas quanto manuais £ mais profundamente dentro do ciclo de desenvolvimento existente. Dan Griffin prolongará este debate com um exemplo mais concreto de integração automatizada com o desenvolvimento de um suplemento de teste de difusão para o Visual Studio Team System. Finalmente, para aqueles que não se sentem desenvolvedores plenos sem algum conhecimento de linguagem assembly, Adel Abouchaev, Damian Hasse, Scott Lambert e Greg Wroblewski mergulharão fundo £ bastante fundo £ nas diferentes condições que podem causar panes em seus aplicativos, e como cada uma destas condições podem também gerar vulnerabilidades na segurança.

Um dos temas mais recorrentes ao longo de muitos artigos neste mês é o fato que preocupações com segurança estão em constante desenvolvimento e são baseadas em avaliações de risco realistas £ é por isto que a ênfase desta edição é mais direcionada no sentido de criar uma estratégia de segurança sólida que em instruções de como “fazer a segurança” de um aplicativo. Quando observamos segurança sob esta perspectiva, preocupações com segurança assemelham-se mais a requisitos comerciais do que a preocupações estáticas com infra-estrutura £ e talvez se conseguirmos manter esta perspectiva, poderemos descobrir como criar uma estratégia de segurança mensurável e equilibrada, evitando, assim, tanto um extremo quanto o outro.

Por falar em medidas, mais uma nota rápida: estamos constantemente tentando aprimorar a qualidade tanto do conteúdo quanto do planejamento da MSDN Magazine. E, entre outras coisas, confiamos no sistema de avaliação da versão online de cada artigo da edição. Você pode exercer um impacto direto na revista simplesmente acessando a versão online dos artigos que lê aqui e deixando sua avaliação usando o controle de avaliações. Sua opinião é importante.

Trabalhe seguro! £ Howard

Thanks to aos seguintes especialistas técnicos da Microsoft por sua ajuda nesta edição: Eric Bidstrup, Joe Binder, Todd Brooks, Claudio Caldato, John Durant, Steve Fox, Matt Gibbs, Ed Hintz, Michael Howard, Richard Johnson, Ivan Medvedev, Dave Reed, Bruce Taimana, Chris Tavares, Mads Torgersen e James Whittaker.


Toolbox: Controles de grade atualizados, ferramentas de compara&#231;&#227;o de c&#243;digo-fonte aberto e muito mais

Toolbox
Controles de grade atualizados, ferramentas de comparação de código-fonte aberto e muito mais
Scott Mitchell

Conteúdo
Atualize a sua grade do Windows Forms
Adicione caixas de texto de dados com aparência profissional ao seu site
Compare e mescle arquivos rapidamente
Guia para o Microsoft ASP.NET AJAX

Atualize a sua grade do Windows Forms

Provavelmente, a grade é o elemento de interface de usuário avançado usado com mais freqüência, pois é a forma mais fácil e direta de exibir e editar dados em formato de tabela. Desde os primórdios do Visual C++® e do Visual Basic®, a Microsoft tem fornecido aos desenvolvedores um controle de grade simples e gratuito. Mas os desenvolvedores também podem aproveitar as grades disponibilizadas de fornecedores de terceiros.

Um fornecedor terceirizado da Microsoft dos controles de grade do® .NET Framework Windows® Forms é a FarPoint Technologies, Inc., que oferece um componente de grade chamado Spread para Windows Forms 3. Para adicionar o Spread para Windows Forms no aplicativo .NET basta arrastar uma instância do Visual Studio® Toolbox e soltar no seu formulário. Os dados iniciais podem ser especificados manualmente, por meio de programação ou por ligação de dados.

Adicionar um Spread para grade do Windows Forms no aplicativo é como adicionar uma instância do Microsoft Excel®. Como o Excel, o Spread para Windows Forms pode incluir várias planilhas e fórmulas, ter células mescladas e células para expandir colunas, fornecer classificação de nível de coluna e muito mais. Com uma linha de código, você pode importar uma planilha do Excel para uma grade. Da mesma forma, uma grade pode ser exportada para uma planilha do Excel ou para um arquivo XML.

Personalizar um Spread para grade do Windows Forms no Visual Studio é particularmente fácil graças ao Spread Designer. Este programa autônomo, que pode ser inicializado a partir da janela Properties (Propriedades) do Visual Studio, permite que os desenvolvedores especifiquem a aparência, layout e formato de suas grades.

As grades são um acessório rotineiro no design de interface do usuário, como botões e rótulos. E alguns produtos de grade de terceiros, como o Spread para Windows Forms, fornecem aos desenvolvedores funcionalidades além das que são oferecidas pela Microsoft.

Preço: $899,00 por licença de desenvolvedor.

www.fpoint.com

As grades do Spread para Windows Forms são personalizáveis
As grades do Spread para Windows Forms são personalizáveis (Clique na imagem para diminuir a exibição)
As grades do Spread para Windows Forms são personalizáveis
As grades do Spread para Windows Forms são personalizáveis (Clique na imagem para aumentar a exibição)

Adicione caixas de texto de dados com aparência profissional ao seu site

Sites grandes e comerciais que solicitam valores de dados aos usuários £ como sites de reserva de viagem £ geralmente fornecem uma caixa de texto com um ícone de calendário ao lado dele. Clicando neste ícone de calendário, será exibido o mês atual em um calendário pop-up e permite que o usuário selecione uma data simplesmente clicando nela.

Tais interfaces de usuário de entrada de dados sofisticadas são menos encontradas em sites de intranet e em sites menores, em parte porque o ASP.NET não inclui nenhum controle Web interno relacionado a datas. A boa notícia é que há muitos controles de datas de terceiros disponíveis. Um desses produtos é o Peter's Date Package (versão 2.0.4), criado por Peter Blum, que compõe 24 controles de Web do ASP.NET. O principal controle é o DateTextBox, que exibe uma caixa de texto com um ícone de calendário que, quando clicado, exibe uma interface de calendário em pop-up. O calendário exibe o mês atual com ícones para exibir as datas do mês anterior e do próximo. O pop-up também inclui um selecionador de mês e ano, que agiliza a inserção de datas no futuro ou no passado. Além disso, o controle emite automaticamente scripts do lado do cliente que evitam que o usuário digite valores incorretos de data (como letras ou um pontuação que não seja relacionada a datas).

Além do controle DateTextBox, o Peter's Date Package inclui um selecionador autônomo de mês e ano, uma AnniversaryTextBox que permite que o usuário especifique apenas o mês e o dia e um controle TimePicker. Há também uma matriz de controles de validação de data específica. O UnselectableDatesValidator permite que o desenvolvedor indique um conjunto de datas inválidas, enquanto o DateCompareValidator assegura que as datas inseridas em dois controles de data mantenham uma certa relação, como o valor do primeiro controle ser menor do que o do segundo.

E o melhor de tudo é que o Peter's Date Package é muito fácil de usar. Configurar um site novo ou já existente para usar os controles de data leva menos de um minuto e incluir esses controles de data sofisticados em uma página da Web é tão simples quanto arrastar e soltar. Se você tiver problemas, Peter Blum responde rapidamente as dúvidas postadas nos fóruns de suporte.

Preço: $90 por servidor Web.

www.peterblum.com/DateControls

O DateTextBox é o controle principal do Peter's Date Package
O DateTextBox é o controle principal do Peter's Date Package (Clique na imagem para diminuir a exibição)
O DateTextBox é o controle principal do Peter's Date Package
O DateTextBox é o controle principal do Peter's Date Package (Clique na imagem para aumentar a exibição)

Compare e mescle arquivos rapidamente

Como os desenvolvedores trabalham com o código-fonte para aplicativos como uma coleção de arquivos, nós geralmente precisamos comparar as diferentes versões de arquivos ou pastas e, talvez, sincronizá-los. Por exemplo, pode ser necessário determinar quais arquivos foram modificados desde a última compilação ou atualizar a produção dos sites com as modificações feitas no servidor intermediário. Programas que encontram diferenças em arquivos existem há décadas £ o Windows há muito incluiu um programa deste tipo, chamado WinDiff. Além disso, praticamente todos os fornecedores de controle de código-fonte empacotam algum tipo de programa como o WinDiff para facilitar a comparação entre diferentes versões de arquivo sob controle de origem. Mas alguns desenvolvedores querem mais recursos do que o WinDiff e os programas de controle de código-fonte oferecem.

O WinMerge (versão 2.6.8) é uma ferramenta de comparação de código-fonte e pastas e sincronização de código aberto gratuita, rápida e com recursos sofisticados. Ao ser inicializado, o WinMerge solicita que você selecione dois arquivos ou pastas para comparar. Você também pode especificar um filtro de arquivo e de linha neste momento. Ao incluir um conjunto de arquivos em uma pasta (ou subpastas), o filtro de arquivo indicará que arquivos devem ser incluídos ou excluídos. Por exemplo, os desenvolvedores da Web podem querer usar um filtro que inclua páginas da Web HTML e do ASP.NET, mas que exclua arquivos GIF, JPG, CSS e JavaScript. Você pode criar os seus próprios filtros de arquivo ou usar um dos vários que acompanham o WinMerge. Os filtros de linha podem ser usados para excluir linhas de comparação do texto que correspondam a uma expressão regular.

Se você estiver comparando arquivos de duas pastas diferentes, o WinMerge listará os arquivos de cada pasta e se eles são idênticos ou não. Clicando em um dos arquivos listados, será visualizada uma janela que exibirá o conteúdo dos dois arquivos, usando cores para indicar as diferenças.

O WinMerge também facilita a sincronização de arquivos. Você pode acompanhar as diferenças uma de cada vez e escolher se tornará a diferença do arquivo à esquerda igual ao da direita ou vice-versa. Também é possível tornar um arquivo exatamente igual a outro clicando em um botão.

Além dessa interface de usuário gráfico intuitiva, o WinMerge pode ser executado a partir da linha de comando e ser integrado com o shell do Windows.

Preço: gratuito.

www.winmerge.org

O WinMerge tem uma interface gráfica intuitiva
O WinMerge tem uma interface gráfica intuitiva (Clique na imagem para diminuir a exibição)
O WinMerge tem uma interface gráfica intuitiva
O WinMerge tem uma interface gráfica intuitiva (Clique na imagem para aumentar a exibição)

Guia para o Microsoft ASP.NET AJAX

O AJAX é usado por muitas pessoas para criar sites interativos que ofereçam uma experiência de usuário simplificada como um aplicativo de área de trabalho. Os sites habilitados por AJAX usam o JavaScript para se comunicar de forma assíncrona com o servidor Web e para atualizar diretamente a interface do navegador de forma apropriada. Há uma variedade de estruturas do AJAX de terceiros e de código-fonte aberto e agora a Microsoft lançou a sua própria estrutura AJAX, ASP.NET AJAX (ajax.asp.net).

A maioria dos livros relacionados ao AJAX oferece uma abordagem de nível muito baixo das tecnologias envolvidas. Mas este nível de detalhes é desnecessário para um desenvolvedor que deseja iniciar rapidamente a criação de aplicativos ASP.NET habilitados por AJAX. Esses desenvolvedores acharão o Professional ASP.NET 2.0 AJAX (Wrox Press, 2007) de Matt Gibbs e Dan Wahlin perfeito para o que eles precisam. O livro foca nos principais conceitos AJAX, como postbacks assíncronos, modelo de objeto de documento (Document Object Model) do navegador e solicitações HTTP por meio de script £ e cada um é explicado com termos familiares aos desenvolvedores do ASP.NET. O trabalho com a biblioteca AJAX do Microsoft ASP.NET também é explorado, assim como o uso do controle UpdatePanel, a biblioteca do cliente ASP.NET, o ScriptManager e os controles do AJAX Toolkit.

O Professional ASP.NET 2.0 AJAX também apresenta dicas e técnicas de depuração. As técnicas envolvidas na criação de aplicativos Web habilitados para AJAX complicam o processo de testes. Como muitas funções relacionadas ao AJAX ocorrem no navegador da Web, os desenvolvedores perderão o suporte sofisticado de depuração disponível com o código do lado do servidor. O capítulo sobre testes analisa as classes relacionadas à depuração do ASP.NET AJAX e ferramentas úteis como o Script Debugger da Microsoft, o Fiddler de Eric Lawrence e o Web Development Helper de Nikhil Kothari. Além disso, o livro explora os desafios envolvidos na implantação dos aplicativos ASP.NET habilitados para AJAX.

Preço: $39,99.

www.wrox.com

Envie suas dúvidas e comentários para Scott em toolsmm@microsoft.com.

Scott Mitchell, autor de diversos livros e fundador da 4GuysFromRolla.com, é um MVP que tem trabalhado com tecnologias Microsoft Web desde 1998. Scott é consultor autônomo, treinador e escritor. Para entrar em contato com ele, envie um e-mail para Mitchell@4guysfromrolla.com ou visite o blog ScottOnWriting.NET.


Tudo sobre o CLR: Gerenciamento do tempo de vida dos objetos

Tudo sobre o CLR
Gerenciamento do tempo de vida dos objetos
Tim Fischer

Conteúdo
Coleta de lixo e finalização
Gerenciamento do tempo de vida no COM
.Finalização determinística de NET
Finalização em uma biblioteca COM
Vazamentos de memória

E
mbora o Microsoft® .NET Framework forneça um ambiente de execução gerenciado, é importante usar os recursos com cuidado, considerar o gerenciamento do tempo de vida e escrever um código que solucione corretamente essas preocupações. Nesta coluna, pretendo ajudar os desenvolvedores que estão consumindo classes .NET ou COM a compreenderem melhor o gerenciamento do tempo de vida, a reconhecerem as situações em que podem cair nas armadilhas existentes nos recursos e a saberem como solucionar esses problemas.

Observe que, por motivos de clareza, o código de exemplo mostrado na coluna funciona somente para aplicativos de thread único. O código de thread seguro reutilizável para o gerenciamento de recursos .NET está disponível no projeto LifeTimeScope.net no CodePlex (consulte codeplex.com/lifetimescope).

Na programação .NET, um objeto é criado com a nova palavra-chave e alocado no heap gerenciado. Ele ficará ativo até que o coletor de lixo detecte que o objeto não está mais acessível em uma forte referência raiz, através de um caminho de referências fortes. Cada programa tem um método principal e uma classe associada, bem como classes estáticas, que podem todas conter variáveis locais, membros estáticos ou, até mesmo, eventos. Essas referências estáticas ou locais são consideradas referências raiz no programa (veja a Figura 1). As referências comuns ao .NET Framework são consideradas referências fortes. O tempo de vida do objeto é determinado pelo caminho de referências fortes, que apontam de uma referência raiz para um objeto.

Figura 1 Referências fortes a objetos no heap
Figura 1 Referências fortes a objetos no heap

Em alguns casos, como, por exemplo, na criação de um cache rápido ou hash em um gráfico do objeto, referências adicionais a objetos podem ser interessantes, desde que não alterem o tempo de vida do objeto. Para esse fim, o .NET Framework oferece uma classe adicional intitulada WeakReference, que permite que você implemente uma referência fraca entre os objetos. Essa referência fraca é ignorada quando se determina o tempo de vida de um objeto.

Os eventos também podem ser fortes referências raiz e, por esse motivo, podem contribuir para o caminho de referência forte, afetando, assim, o tempo de vida de um objeto. Os eventos comuns na versão 2.0 do CLR (Common Language Runtime) são referências fortes bidirecionais entre a origem do evento e a escuta e, por isso, podem manter ativo um objeto (a origem ou a escuta), que, em caso contrário, já teria se tornado inativo. Isso se deve ao fato de uma classe WeakEvent ter sido adicionada ao .NET Framework 3.0. É importante que você se familiarize com o padrão WeakEvent, que ainda não é bem conhecido, mas é necessário para implementar com êxito o padrão Observer. O padrão WeakEvent foi usado na implementação da associação de dados do Windows® Presentation Foundation (WPF) para impedir o vazamento devido à associação de dados.

Coleta de lixo e finalização

Vejamos como o CLR determina se um objeto ainda está ativo e o que ocorre quando não estiver. Na edição de novembro de 2000 da MSDN® Magazine (msdn.microsoft.com/msdnmag/issues/1100/GCI), Jeffrey Richter descreve o procedimento. Cada processo .NET executa um thread separado, que é usado pelo coletor de lixo. Quando o coletor de lixo é executado, todos os outros threads são pausados. O coletor de lixo analisa as referências e estruturas de memória para detectar quais objetos estão inativos. Esses objetos são, então, colocados em fila, para um processo denominado finalização. Em uma segunda fase, o processo de finalização é executado em um thread separado, usado pelo coletor de lixo, e o método Finalizer é chamado em cada objeto.

Há dois pontos importantes a serem observados neste processo. Primeiro, o Finalizer é chamado de um thread diferente do thread que criou o objeto. Segundo, o Finalizer é chamado em algum momento, mas não necessariamente na iminência do objeto tornar-se inativo (isso pode ser observado quando o sistema sofre estresse, mas a memória está disponível).

O coletor de lixo é exposto na classe System.GC estática. Você pode forçar a execução da coleta de lixo e permitir que ela identifique objetos inativos e coloque-os na fila para a finalização, chamando o GC.Collect. Também é possível forçar a finalização, para que ela ocorra em todos os objetos na fila, chamando WaitForPendingFinalizers. No entanto, há dois problemas ao chamar WaitForPendingFinalizers: este é um processo demorado e os Finalizers são chamados de outro thread. Isso pode gerar problemas de desempenho e de thread se, por exemplo, você precisar liberar uma referência a um recurso de sistemas e planejar fazer isso no seu Finalizer – alguns recursos não gerenciados são dependentes de thread. (Nesse caso, leia mais e aprenda sobre o padrão Dispose.)

Como a equipe do .NET Framework já identificou a necessidade de uma descrição, em tempo hábil, dos objetos, além da necessidade de permitir que o código de destruição seja executado em um thread específico, eles forneceram uma solução conhecida como o padrão Dispose. Para obter detalhes, consulte a coluna Tudo sobre o CLR, na edição de julho de 2007 da MSDN Magazine em msdn.microsoft.com/msdnmag/issues/07/07/CLRInsideOut.

O padrão Dispose é uma convenção que deve ser implementada por cada objeto que tenha recursos e precise de destruição em tempo hábil. Isso pode ser realizado por meio da implementação da interface IDisposable e da liberação de recursos na implementação de um método chamado Dispose, exposto pela interface. No entanto, tal como no mundo não gerenciado, ainda é responsabilidade do programador se certificar de que o método Dispose seja chamado no momento correto no código do cliente. Brian Harry tem uma postagem em seu fórum público que explica por que a equipe ofereceu essa solução e por que outras soluções interessantes não poderiam ser aplicadas com êxito (consulte discuss.develop.com/archives/wa.exe?A2=ind0010A&L=DOTNET&P=R28572). O padrão Dispose deve ser corretamente implementado em todos os objetos que tenham recursos.

Gerenciamento do tempo de vida no COM
Recursos do gerenciamento do tempo de vida
  • MSDN .NET Framework Developer Center
  • The WeakEvent Pattern
  • LifeTimeScope.net Helper Classes

No mundo COM, o gerenciamento de tempo de vida funciona por meio da contagem de referência. Cada objeto COM tem uma contagem interna de referências do cliente apontando para ele. Esses contadores são incrementados e reduzidos pelo desenvolvedor do cliente chamando IUnknown::AddRef e IUnknown::Release. Observe que, ao passo que o AddRef aumenta o contador e deve ser chamado quando uma referência ao componente for definida, o Release reduz o contador, devendo ser chamado quando a referência tiver sido destruída. Quando o contador voltar para 0, o objeto pode ser destruído.

As chamadas para AddRef e Release foram facilitadas pelo uso de SmartPointers na ATL (Active Template Library) ou com a geração do código necessário pelo compilador/tempo de execução, como no Visual Basic® 6.0. Os eventos foram somente ponteiros fortes em uma direção e ponteiros fracos na outra direção.

Os dois problemas principais com a contagem de referência são a perda de desempenho e o potencial de não conseguir detectar objetos inativos, que são parte de um cenário de referência cíclico. Em geral, a perda de desempenho é aceitável no COM, pois essa tecnologia é tipicamente usada para combinar componentes maiores. No entanto, o .NET Framework é usado no nível de classe e, por esse motivo, o impacto no desempenho seria muito grande se a contagem de referência fosse adicionada ao CLR.

Se você usar um componente COM a partir do código gerenciado, use o tlbimp.exe para gerar o tão conhecido RCW (Runtime Callable Wrapper), que expõe a funcionalidade do componente COM ao mundo gerenciado. Observe que o tlbimp.exe é executado em segundo plano quando você referencia um componente COM no Visual Studio®. É possível identificar facilmente os RCWs à medida que são exibidos como System.__ComObject no depurador. Há tipos .NET que retêm um ponteiro não gerenciado no objeto COM e chamam AddRef e Release no objeto. O AddRef é chamado assim que o componente é criado, já o Release, quando o RCW é finalizado.

Mas quando o RCW é finalizado? Você não sabe; portanto, precisa de um método para chamar Dispose no RCW. Infelizmente, os RCWs gerados não implementam IDisposable. Atualmente, o equivalente a chamar o método Dispose ausente é chamar:

System.Runtime.InteropServices.Marshal.ReleaseComObject(MyCOMObject)

.Finalização determinística de NET

Em muitos casos, você precisa se certificar de que os recursos usados serão liberados o mais rápido possível. O modo ideal é através do uso de palavra-chave em C#, para garantir que o método Dispose será chamado no objeto, assim que o fluxo do programa sair do escopo indicado entre chaves:

using (FileStream theFileStream = File.Create("C:\\hello.txt"))
{
    string s = theFileStream.Name;
}

Além disso, é válido observar que C++/CLI chama automaticamente o destruidor de variáveis locais ao sair de um método. Nesses cenários, pode-se presumir que o método Dispose é chamado automaticamente no final do bloco ou método.

No entanto, essas abordagens não funcionam quando o escopo de uso do recurso não for local. Suponha que você chame outro método, no código anterior, que use o objeto FileStream como um parâmetro de entrada. Esse método pode não chamar Dispose ou usar a palavra-chave. No contexto de um servidor multithread que compartilhe recursos entre threads, isso pode se tornar ainda mais complexo.

Infelizmente, o C# não fornece uma construção simples, equivalente à palavra-chave, que possa ser empregada em diversos escopos ou threads. Isso torna a reutilização de códigos e rotinas difícil, pois a responsabilidade de chamar Dispose deve ser negociada entre os componentes, a fim de garantir que isso seja feito somente uma vez e no momento certo.

Lembre-se: se você não chamar Dispose explicitamente, o Finalizer certamente o fará, caso o objeto implemente o padrão Dispose de forma correta. No entanto, você corre o risco de ficar sem recursos sob altas cargas, pois não sabe quando o Finalizer será chamado. Além disso, há o risco do Finalizer falhar quando for chamado de outro thread, que não seja o que criou o objeto. Isso pode se tornar um problema se você tentar liberar recursos não gerenciados ou recursos COM em um Finalizer.

Uma solução possível é criar uma classe de invólucro genérica em torno de qualquer tipo implementando IDisposable. A classe de invólucro retorna uma interface IDisposable, mas internamente define um contador de referência antes de delegar a chamada ao componente. O código mostrado na Figura 2 usa um novo método de extensão, chamado AddRef, para obter um objeto de invólucro que tenha um contador de referência interna para um objeto específico.

Figure 2 Um invólucro IDisposable (Close Figure 2)
static void Main(string[] args)
{
    FileStream theFileStream = File.Create("C:\\hello.txt");
    // Unscoped use of RefCounted
    using (theFileStream.AddRef())
    {
        a(theFileStream);
    }

    System.Console.Read();
}

static void a(FileStream inFileStream)
{
    // Unscoped use of RefCounted
    using (inFileStream.AddRef())
    {
        // do something
        string s = inFileStream.Name;
    }
}

Quando AddRef for chamado, o código verificará uma tabela de hash para ver se o objeto já tem um invólucro. Em caso positivo, ele retornará o invólucro e aumentará o contador; caso contrário, ele criará um novo invólucro e o adicionará à tabela de hash. Quando o programa atingir o final do uso do bloco, Dispose será chamado automaticamente e reduzirá o contador. No caso do contador chegar a zero, a chamada Dispose será propagada para o objeto subjacente.

A implementação do método de extensão e invólucro é mostrada na Figura 3. Para o seu esclarecimento, esse código não foi desenvolvido para ser seguro para thread.

Figure 3 Invólucro e extensão AddRef (Close Figure 3)
public static class ReferenceCountingExtensions
{
    public static System.Collections.Generic.Dictionary<object,object>
        theHashOfReferenceCountedClasses =
        new System.Collections.Generic.Dictionary<object,object>();
    public static RefCounted<T> AddRef<T>(this T resource)
        where T : IDisposable
    {
        object theObject=null;
        theHashOfReferenceCountedClasses.TryGetValue(resource,
            out theObject);
        if (theObject == null)
        {
            theObject = new RefCounted<T>(resource);
            theHashOfReferenceCountedClasses[resource] = theObject;
        }
        else
        {
            ((RefCounted<T>)theObject).AddRef();
        }
        return (RefCounted<T>) theObject;
    }
}

// Base Class for a RefCountedUnmanagedResourceProtectingClass
public class RefCounted<T> : IDisposable
{
    private T theResource;
    bool IsFinalDisposed = false;
    int RefCounter = 0;
    public RefCounted(T inResource)
    {
        theResource = inResource;
        System.Threading.Interlocked.Increment(ref RefCounter);
    }
    public RefCounted<T> AddRef()
    {
        System.Threading.Interlocked.Increment(ref RefCounter);
        return this;
    }
    public T Resource
    {
        get
        {
            return theResource;
        }
    }
    ~RefCounted()
    {
        if (!IsFinalDisposed)
        {
            Dispose();
        }
    }
     public void Dispose()
    {
        System.Threading.Interlocked.Decrement(ref RefCounter);
        if ((RefCounter == 0) && (!IsFinalDisposed)) FinalDispose();
    }
    virtual public void FinalDispose()
    {
        ((IDisposable)theResource).Dispose();
        IsFinalDisposed = true;
    }
}

Certifique-se de que os seus Finalizers sejam realmente executados até o fim. O tempo de execução do .NET Framework fornece um conceito denominado CER (Regiões de Execução Restrita), além do CriticalFinalizerObject, que podem ajudá-lo a realizar essa tarefa.

Finalização em uma biblioteca COM

Vejamos um cenário em que pode ser usado um modelo de objeto COM a partir de C#. É possível utilizar, com facilidade, qualquer componente COM, criando-se uma referência à sua biblioteca de tipo registrada. Vamos supor que um componente COM exponha uma TAPI (API de telefone) e a use nas seguintes linhas de código – veja se consegue localizar um bug relacionado ao gerenciamento do tempo de vida:

void foo (CTAPIApplication TAPIApplication){
    string s = TAPIApplication.Ports[0].SpeechListener.GetName();
}

A razão do bug é que a coleção Ports foi adquirida a partir do modelo de objetos, mas seu método ReleaseComObject não foi chamado. O mesmo é válido para SpeechListener. Você pode argumentar que não é seguro supor que o foo deva liberar o ComObject. E você está certo. Não está claro, a partir desse código, quem deve liberar o objeto e quando. Então, o que você pode fazer?

A solução, novamente, é usar o Método AddRef, mas você precisa modificar a classe RefCounted para compreender que ela deve chamar ReleaseComObject, no caso de ser um ComObject. A Figura 4 mostra o método FinalDispose aprimorado. Além disso, onde T : IDisposable foi removido da chamada para AddRef<T>, pois não há interface comum que os Objetos COM e IDisposable compartilhem:

Figure 4 Método FinalDispose (Close Figure 4)
virtual public void FinalDispose()
{
    if(theResource.GetType().IsCOMObject)
    {
        Marshal.ReleaseComObject(theResource);
    }

    else
    {
        ((IDisposable)theResource).Dispose();
    }

    IsFinalDisposed = true;
}

public static RefCounted<T> AddRef<T>(this T resource)

Agora é possível reescrever a chamada para a API de telefone, como mostra a Figura 5. Embora esse código ainda não seja muito útil, ele é muito seguro, pois o uso da instrução garante que o ReleaseComObject realmente seja chamado, mesmo se for lançada uma exceção em algum ponto. Para tornar o código ainda mais legível, adicione outra classe auxiliar denominada LifeTimeScope e altere o tipo de retorno de AddRef:

Figure 5 Chamando COM (Close Figure 5)
void foo(CTAPIApplication aTAPIApplication)
{
    using(PortsCollection aPortsCollection =
        aTAPIApplication.Ports.AddRef())
    {
        using (Port aPort = aPortsCollection[0].AddRef())
        {
            using(SpeechListener aListener =
                aPort.SpeechListener.AddRef())
            {
                string s  = aListener.GetName()
            }
        }
    }
}

void foo (CTAPIApplication TAPIApplication)
{
    using (new LifeTimeScope())
    {
        string s =
            TAPIApplication.Ports.AddRef()[0].AddRef()
            .SpeechListener.AddRef().GetName();
    }
}

A classe LifeTimeScope, mostrada na Figura 6, atua como auxiliar para chamar Dispose em todos os invólucros de objeto que foram tocados pelo AddRef no uso de bloco. O AddRef também sofreu algumas alterações (veja a Figura 7). Embora todas essas alterações sejam boas, você ainda precisa fazer a chamada para AddRef.

Figure 7 AddRef revisado (Close Figure 7)
public static class ReferenceCountingExtensions
{
    static public System.Collections.Generic.Dictionary<object, object>
        theHashOfReferenceCountedClasses =
        new System.Collections.Generic.Dictionary<object, object>();

    public static T AddRef<T>(this T resource)
    {
        object theObject=null;
        theHashOfReferenceCountedClasses.TryGetValue(resource,
            out theObject);
        if (theObject == null)
        {
            theObject = new RefCounted<T>(resource);
            theHashOfReferenceCountedClasses[resource] = theObject;
        }
        else
        {
            ((RefCounted<T>)theObject).AddRef();
        }
        System.LifeTimeScope.Current
            .theListOfWrapperObjectsToCallDisposeOn.Add(theObject);
        return (T) resource;
    }
}
Figure 6 LifeTimeScope (Close Figure 6)
public  class LifeTimeScope : IDisposable
{
    private static LifeTimeScope _Current = null;

    private static Stack<LifeTimeScope> theScopeStack =
        new Stack<LifeTimeScope>();

    public static LifeTimeScope Current
    {
        get{ return _Current; }
    }

    public System.Collections.ArrayList
        theListOfWrapperObjectsToCallDisposeOn =
        new System.Collections.ArrayList();

    public LifeTimeScope()
    {
        _Current = this;
        theScopeStack.Push(this);
    }

    public void Dispose()
    {
        foreach (IDisposable theObject in
            theListOfWrapperObjectsToCallDisposeOn)
        {
            theObject.Dispose();
        }
        _Current = theScopeStack.Pop();
    }
}

Vazamentos de memória

O problema mais comum que você terá é que o aplicativo ficará sem memória, em razão de assinaturas de eventos. Por exemplo, você poderia escrever um controle de usuário do Windows Forms, que inscreve o evento NetworkChange no seu constructor, da seguinte forma:

NetworkChange.NetworkAvailabilityChanged += new
    NetworkAvailabilityChangedEventHandler(ctrl_NetAvailChangedHandler);

O controle de usuário e a classe estática NetworkAvailabilityChanged retêm referências fortes entre si bidirecionalmente. Como NetworkAvailabilityChanged é uma referência com raiz estática, o controle de usuário não será liberado da memória até que o manipulador de eventos seja removido.

Se você não cancelar um evento a partir de um objeto que exceda o tempo de vida de seu objeto referenciado – nesse caso, NetworkAvailabilityChanged e seu controle de usuário referenciado – você criará automaticamente um vazamento de memória. Esse problema é comum e se aplica a todos os tipos de cenários do padrão Observer, incluindo a associação de dados.

A documentação do .NET Framework 2.0 afirma que você deve cancelar a inscrição do evento no método Dispose do objeto. Também é uma boa idéia se certificar de que Dispose seja chamado. Isso porque os eventos do mundo .NET Framework 2.0 são recursos com os quais você precisa se preocupar. No entanto, no .NET Framework 3.0, pode-se empregar uma solução com base no padrão WeakEvent.

Finalmente, outra forma de garantir a destruição de recursos não gerenciados é usar a classe HandleCollector, que ajuda a executar GC.Collect ocasionalmente. A Figura 8 fornece uma instância de exemplo, Create, do HandleCollector, que fornece três parâmetros: nome do identificador (cadeia de caracteres), limite inicial (int.) e limite máximo (int.). O limite inicial é o ponto em que o coletor de lixo pode iniciar a execução da coleta. O limite máximo é o ponto em que o coletor de lixo deve executá-la.

Figure 8 Usando o HandleCollector (Close Figure 8)
// Handle Collector offers an easy way to have garbage collection
// started once we have 30+ active (and possibly destroyed) objects

class MyClassThatForcesGarbageCollectionIf30HandlesAreActive
{
    // Create a new HandleCollector
    static readonly HandleCollector appHandleCollector =
        new HandleCollector("ApplicationUnmanagedHandles", 5, 30);

    public MyClassThatForcesGarbageCollectionIf30HandlesAreActive()
    {
        // Increment Handle Count
        appHandleCollector.Add();
    }
    ~MyClassThatForcesGarbageCollectionIf30HandlesAreActive()
    {
        // Decrement Handle Count
        appHandleCollector.Remove();
    }
}
}

Essa foi uma breve visão geral do gerenciamento do tempo de vida nas classes .NET ou COM. Espero que você possa reconhecer e solucionar esses problemas. Para obter mais informações, visite os links na barra lateral “Gerenciamento do tempo de vida”.

Envie suas perguntas e comentários para clrinout@microsoft.com.

Tim Fischer mora na Alemanha e é gerente de programas dos produtos Visual Studio e Expression na Microsoft. Ele é graduado em Ciência da Computação pela Universidade de Stuttgart e trabalhou por 12 anos como consultor de desenvolvimento orientado por modelos. Ele é o fundador da tangible engineering, uma empresa dedicada à criação de fábricas de software .NET de classe mundial.


Instintos b&#225;sicos: M&#233;todos de extens&#227;o

Instintos básicos
Métodos de extensão
Adrian "Spotty" Bowles

Conteúdo
Criando e usando métodos de extensão
A vantagem dos métodos de extensão
Preocupações com a segurança
Sobrecarregando com métodos de extensão
Quais tipos
Estendendo tipo de objeto
Métodos de extensão nos aplicativos .NET Framework 2.0

C
om o desenvolvimento orientado a objeto tradicional, as classes de extensão são geralmente obtidas pela derivação de uma classe base e aperfeiçoamento da funcionalidade na classe derivada. O Visual Basic® ainda oferece suporte a esses conceitos orientados a objeto, mas classes são algumas vezes marcadas com NotInheritable para evitar modificações de seu comportamento por meio de herança. Como resultado, não há como personalizar estas classes. Um exemplo disso é a classe System.String.

Um novo recurso disponível no Visual Basic 2008, entretanto, permite a extensão das funcionalidades de tipo existentes, mesmo quando um tipo não é hereditário. E estes métodos de exibição têm uma função crucial na implementação do LINQ.

Vários tipos que já existem não podem ser atualizados facilmente sem a quebra dos códigos existentes. Um exemplo disso é a interface IEnumerable(Of T). Para oferecer suporte ao LINQ, novos métodos tiveram que ser adicionados a esta interface. Contudo, a alteração da interface adicionando novos métodos a deixaria incompatível com as versões dos consumidores existentes. A adição de uma nova interface era uma possibilidade, mas a criação de uma nova interface para complementar a interface IEnumerable(Of T) existente seria um projeto um tanto estranho.

Era necessária uma maneira de estender os tipos existentes com a nova funcionalidade sem alterar o contrato existente.

Os métodos de extensão fornecem um mecanismo simples para estender os tipos no sistema (valor, referência e tipos de interface) com novos métodos. Esses métodos estendem o tipo original e podem ser chamados como métodos regulares, de instância definida, mas deixam o tipo original e seus métodos intocados. Os métodos de extensão criam a ilusão de que são definidos em um tipo real mas, na realidade, não são feitas alterações nos tipos originais.

Este não é um conceito orientado a objeto padrão; é um recurso de implementação específico do Microsoft® .NET Framework. Embora este recurso abra um novo conjunto de possibilidades, vale a pena observar que o código de linguagem intermediária (IL) subjacente gerado pelo compilador não faz nada de novo ou específico para o .NET Framework 3.5. Na realidade, ele está fazendo simplesmente uma chamada de método compartilhada.

Isso significa que você tem a capacidade de usar este recurso no Visual Basic 2008 para chegar a versões anteriores do .NET Framework. Ele não deve introduzir problemas de segurança adicionais, uma vez que este recurso não altera o tipo que está sendo estendido e realmente não faz nada que você já não fizesse com versões anteriores do Framework.

Criando e usando métodos de extensão

O código na Figura 1 mostra um método de extensão AlternateCase, que retorna uma cadeia de caracteres com maiúsculas e minúsculas alternadamente. Isso estenderá a classe System.String, a qual, conforme observei, está marcada como NotInheritable.

Figure 1 Método de extensão AlternateCase (Close Figure 1)
Imports System.Runtime.CompilerServices

Module Module1
    Sub Main()
        Dim s As String = "Hello World"

'Call AlternateCase method as an extension member on
'variable s
        Console.WriteLine(s.AlternateCase)

'Invoke if it was not an extension method or
'resolve conflicts
Console.WriteLine(Extensions.AlternateCase(s))

    End Sub
End Module

Module Extensions
<Extension()> _
Function AlternateCase(ByVal x As String) As String
        Dim bCase = False
        Dim SB as new System.Text.StringBuilder
        For Each c In x
            If bCase Then
                SB.Append(c.ToUpper)
            Else
                SB.Append(c.ToLower)
            End If
            bCase = Not bCase
        Next
        Return SB.ToString
    End Function
End Module

Conduzirei você por este exemplo. A declaração do método contém o atributo que identifica este método como um método de extensão. Os métodos de extensão são definidos pela aplicação do atributo de extensão encontrado no namespace System.Runtime.CompilerServices encontrado em System.Core.Dll. Para aqueles que não estão familiarizados com atributos, eles podem ser usados para fornecer metadados que são usados pelo compilador para disparar uma funcionalidade adicional. Nesse caso, o atributo de extensão oferece informações que mostram ao compilador que aquele método poderá ser chamado no tipo identificado como o primeiro argumento para o método de extensão £ String, no meu exemplo.

A linguagem do Visual Basic insiste que todas as declarações de métodos de extensão sejam posicionadas em um módulo. E, por definição, os métodos de extensão podem ser criados como sub-rotinas ou funções, mas tentar declará-los como qualquer outra coisa, como propriedades ou eventos, é proibido. Não há restrições sobre os tipos que podem ser estendidos £ é possível estender qualquer classe, estrutura ou interface.

Os métodos de extensão são restritos a módulos para garantir que eles sejam fáceis de usar. Este projeto pretende desenvolver o conceito de busca de novas APIs para o escopo dentro de um programa Visual Basic usando a declaração Imports para trazer todos os métodos em um namespace para o escopo.

Todos os métodos no Visual Basic devem ser declarados dentro de um tipo, sejam eles uma classe, estrutura, módulo ou alguma outra coisa. Os métodos de extensão não são exceção, ainda que apresentem pequenas diferenças por serem definidos em um tipo fora do tipo que estejam estendendo. Isso abre o potencial para nomear conflitos quando usar métodos de extensão em vários módulos ou namespaces.

A restrição da declaração de métodos de extensão para módulos simplifica dramaticamente o projeto, já que módulos não podem participar de cadeias herdadas e não há conceito de módulos parciais. O uso de uma declaração Imports com um Namespace/Módulo traz esses métodos compartilhados para o escopo e permite a chamada de métodos sem precisar ter que qualificá-los completamente.

Como exemplo, digamos que posicionei as Extensões de módulo dentro de um namespace diferente, como:

Namespace NewFeatures
Module Extensions
<Extension()> Function AlternateCase(ByVal x As String) As String
        Dim bCase = False
        Dim SB as new System.Text.StringBuilder
        For Each c In x
            If bCase Then
                SB.Append(c.ToUpper)
            Else
                SB.Append(c.ToLower)
            End If
            bCase = Not bCase
        Next
        Return SB.ToString
End Module
End Namespace

Para posicionar as declarações de método de extensão dentro deste namespace no escopo, eu usaria a seguinte declaração:

Imports NewFeatures
Imports System.Runtime.CompilerServices

Module Module1
    Sub Main()
        Dim s As String = "Hello World"

'Call AlternateCase method as an extension member on 'variable s
        Console.WriteLine(s.AlternateCase)

'Invoking if it was not an extension method or
'resolve conflicts
Console.WriteLine(Extensions.AlternateCase(s))
    End Sub
End Module

Os métodos de extensão não são exclusivos do Visual Basic; o C# tem uma implementação semelhante onde métodos de extensão têm que ser declarados em uma classe estática. Isso fornece uma funcionalidade semelhante a de um módulo. Se você trabalha com C# e Visual Basic, os métodos de extensão permitem interoperabilidade £ é possível criar métodos de extensão em uma linguagem e usá-los em outra.

Observe que se você atualizou um projeto de uma versão anterior, será necessário atualizar o destino do Framework para a versão 3.5 e adicionar manualmente uma referência a System.Core.Dll. Se não desejar atualizar o destino do Framework, ainda será possível aproveitar os métodos de extensão recriando o ExtensionAttribute £ mas informações posteriormente. Também será necessário importar o namespace System.Runtime.CompilerServices (que é onde ExtensionAttribute está) ou qualificar completamente o atributo quando você usá-lo.

A parte final do exemplo é a chamada. O IntelliSense® aperfeiçoado permite que se veja os métodos de extensão juntamente com todos os métodos de instância existentes. Você verá que os métodos de extensão têm um ícone um pouco diferente (uma seta azul para baixo, como mostrado na Figura 2) para diferenciá-los dos métodos de instância.

Figura 2 Ícones dos métodos de extensão e de instância
Figura 2 Ícones dos métodos de extensão e de instância(Clique na imagem para diminuir a exibição)
Figura 2 Ícones dos métodos de extensão e de instância
Figura 2 Ícones dos métodos de extensão e de instância (Clique na imagem para aumentar a exibição)

A funcionalidade IDE do Visual Studio® £ como Goto Definition, Rename, dentre outros £ funciona com os métodos de extensão e com os métodos de instância normais. Você observará que as dicas de ferramentas também exibem o atributo de extensão quando exibem a assinatura.

Você poderá observar outros métodos de extensão em IntelliSense, como First e FirstorDefault. Esses são alguns métodos de extensão que já foram fornecidos como parte do LINQ e estão incluídos nas referências padrão do .NET Framework 3.5. Eles estão estendendo uma interface genérica System.Collections.Generic.IEnumerable(Of T) a partir do namespaceSystem.Linq.

A funcionalidade LINQ está ativada por padrão nos projetos da versão 3.5, o que significa que você obtém todos os métodos de extensão definidos em System.Core.Dll. É possível confirmar isso analisando as referências de assemblies padrão e namespaces importados £ indo para Project Properties | References | Imported Namespaces (Propriedades do projeto | Referências | Namespaces importados) mostrará System.Linq já marcado. Também é possível usar uma declaração Imports de nível de arquivo (Imports System.Linq) para trazer esses métodos de volta ao escopo.

A vantagem dos métodos de extensão

Os métodos de extensão permitem adicionar funcionalidades a um tipo que você não deseja modificar, evitando assim o risco de quebra de código nos aplicativos existentes. É possível estender interfaces padrão com métodos adicionais sem alterar fisicamente as bibliotecas de classe existentes. É possível estender os tipos .NET e antigos tipos de controle COM/ActiveX® para novos códigos sem o risco de quebrar aplicativos antigos que usam estes tipos.

Antes dos métodos de extensão, para adicionar funcionalidade a classes e interfaces, você tinha poucas opções:

  • Você podia adicionar a funcionalidade ao código-fonte, mas isso exige acesso ao código-fonte que pode não estar disponível.
  • Você podia usar a herança para herdar a funcionalidade contida dentro de um tipo para um novo tipo derivado, mas nem todos os tipos são hereditários.
  • Você podia implementar novamente a funcionalidade a partir do zero.

Todas as três abordagens tornavam a extensão de tipos de terceiros extremamente difícil. Se for possível obter o código-fonte e recompilar o componente, também seria necessário garantir que você mantivesse compatibilidade com o componente original para evitar a quebra de aplicativos existentes que usaram o componente. Se você criou uma nova classe derivada ou implementou novamente a funcionalidade a partir do zero, seria necessário consertar todos os aplicativos existentes para usar o novo componente ou ramificação entre a funcionalidade original e a nova funcionalidade atualizada £ e isso exigiria duas versões do componente.

Nenhuma dessas soluções era a ideal. Com os métodos de extensão, é possível simplesmente adicionar funcionalidade a classes existentes. Todos os aplicativos existentes continuam a funcionar com os binários originais como se não tivessem sido alterados, mas os aplicativos agora poderão estender a funcionalidade em constante avanço.

O desempenho não é relativamente afetado. Ainda que seu código-fonte do Visual Basic possa parecer diferente, ele compila para uma chamada de método compartilhado simples e gera o mesmo IL como se você tivesse usado a chamada do método compartilhado. Assim, não há impacto no desempenho pelo seu uso.

Dito isso, os métodos de extensão não são uma substituição dos conceitos orientados a objeto. Se você estiver familiarizado com os conceitos orientados a objeto e tiver um aplicativo bem projetado, você provavelmente já está usando herança em algum lugar do aplicativo e deve continuar a fazer isso. Você provavelmente possui alguns dos tipos que está usando, o que significa que você tem acesso ao código-fonte e pode estender a funcionalidade por conceitos orientados a objeto normais. Os métodos de extensão realmente fornecem uma maneira de aperfeiçoar classes que você não podia estender anteriormente. Os métodos de extensão devem ser usados com moderação, quando outras abordagens não são possíveis ou apropriadas.

Preocupações com a segurança

Ouço constantemente a preocupação de que os métodos de extensão possam ser usados para seqüestrar ou subverter o comportamento desejado dos métodos existentes. O Visual Basic soluciona isso garantindo que, onde for possível, um método de instância é preferível a um método de extensão.

A linguagem permite que os métodos de extensão sejam usados para criar sobrecargas para os métodos de instância existentes com assinaturas diferentes. Isso permite que os métodos de extensão sejam usados para criar sobrecargas, além de evitar que os métodos de instância existentes sejam substituídos. Se um método de extensão existir com a mesma assinatura de um método de instância, as regras de sombreamento criadas no compilador preferirão o método de instância, eliminando, portanto, a possibilidade de um método de extensão substituir uma funcionalidade de instância de classe base existente (consulte a Figura 3).

Figure 3 Método de extensão Shadowing (Close Figure 3)
Imports System.Runtime.CompilerServices

Module Module1
    Sub Main()
        Dim x As New ClsFoo
        Dim s As String = x.Method01
    End Sub
End Module

Class ClsFoo
    Function Method01() As String
        Return "Instance"
    End Function
End Class

Module ExtMethods
    <Extension()> _
    Function Method01(ByVal a As clsFoo) As String
        Return "Extension"
    End Function
End Module

Como a implementação de métodos de extensão não altera o tipo original, esta funcionalidade adicional é conhecida apenas pelos aplicativos escritos para usar o método de extensão. Os aplicativos existentes que usam o tipo original não verão diferenças.

Se você olhar o IL subjacente gerado pelo compilador, verá que a chamada do método de extensão é transportada para uma chamada de método compartilhado simples. Portanto, não há riscos de segurança aumentados a partir do uso de métodos de extensão, uma vez que você não estará fazendo nada que já não fizesse por uma chamada de método compartilhada normal.

Sobrecarregando com métodos de extensão

Os métodos de extensão permitem que você sobrecarregue métodos de instância. Para ajudar a garantir que os aplicativos funcionem corretamente, há várias regras criadas no compilador para oferecer consistência ao comportamento de ligação. O objetivo era fazer com que os métodos de extensão parecessem ser métodos de instância, mas também fornecessem uma experiência consistente £ quando você insere um código herdado em um projeto usando métodos de extensão, o código deve continuar a trabalhar da mesma maneira.

Um método de instância com uma assinatura aceitável usando conversão ampliada será quase sempre preferível a um método de extensão com uma assinatura de correspondência exata. Se isso resultar em fazer a ligação com o método de instância quando você desejar realmente usar o método de extensão, será possível chamar explicitamente o método de extensão usando a convenção de chamada de método compartilhada. Essa é também a maneira de remover a ambigüidade de dois métodos quando nenhum deles for mais específico.

Os campos e propriedades sempre fazem sombreamento dos métodos de extensão pelo nome. A Figura 4 exibe um método de extensão e um campo público com o mesmo nome e várias chamadas. Embora o método de extensão contenha um segundo argumento, o campo faz sombreamento no método de extensão pelo nome e todas as chamadas usando esse nome acessam o campo. As várias chamadas sobrecarregadas serão compiladas, mas os resultados no tempo de execução poderão ser inesperados, uma vez que se ligam à propriedade e usam o comportamento de propriedade padrão para retornar um único caractere ou resultar em uma exceção de tempo de execução. É importante escolher os nome dos métodos de extensão para evitar conflitos com propriedades, campos e métodos de instância existentes.

Figure 4 Método de extensão e campo público com o mesmo nome (Close Figure 4)
Imports System.Runtime.CompilerServices

Module Module1
    Sub Main()
        Dim x As New Foo

        'Field - (Test)
        Console.WriteLine(x.TypeField)
        'Default Property Transformation on field resulting in T
        Console.WriteLine(x.TypeField(0))

        'Invalid Cast Exception Converting "fred" to integer
        'to use in default property transformation
        Console.WriteLine(x.TypeField("fred"))
    End Sub
End Module

Class Foo
    Public TypeField As String = "Test"
End Class

Module Extension
    <Extension()> _
    Function TypeField(ByVal a As Foo, ByVal x As String) As String
        Return "Extension"
    End Function
End Module

As regras do compilador com relação a com qual método o código de chamada se ligará são complexas. Estas regras são ponderadas em relação à seleção de um método de instância existente sempre que possível. Contudo, há algumas exceções. Os pontos gerais a serem considerados são os seguintes:

  1. Os métodos de instância sempre serão levados em consideração antes dos métodos de extensão. Os métodos de instância ampliados serão levados em consideração antes dos métodos de extensão. Os candidatos a método de instância não-aplicáveis, como os que estão fora do escopo, não serão considerados na resolução de sobrecarga.
  2. Os métodos de instância com a assinatura correspondente ao tipo correta serão levados em consideração antes dos métodos com conversões ampliadas.
  3. Os campos/propriedades de classe farão sombreamento dos métodos de extensão pelo Nome.

Deve estar claro que o nome e assinatura do método de extensão são fundamentais para determinar o método a ser escolhido pelo compilador, seja um método de extensão ou de instância.

Quais tipos

Outra consideração se relaciona com quais tipos são estendidos. Embora seja possível estender virtualmente qualquer tipo, isso não significa que você deva fazer isso. Você deve estar ciente da "religação silenciosa", que pode ser descrita como a alteração de um método ligado, mas esta alteração não é imediatamente óbvia para o desenvolvedor. Não é gerado nenhum aviso ou erro e a única indicação de que algo ocorreu é a alteração resultante no comportamento pelo uso de um método diferente.

Imagine que você criou um método de extensão que sobrecarrega um método em uma biblioteca de classe de terceiros. Seu aplicativo funciona como esperado. A ferramenta de terceiros oferece uma atualização, que você instala em sua máquina. O aplicativo compila bem, sem erros. Mas quando o aplicativo é executado, o comportamento é incorreto. Isso pode ocorrer se o componente de atualização incluiu sobrecargas adicionais ao método estendido, resultando na sobrecarga do método de instância adicional a ser chamado e não o seu método de extensão.

Os métodos de extensão são frágeis em cenários de versão. Esta é uma razão pela qual você deve usar nomes do método de extensão com muito cuidado£ especialmente quando usados com tipos que são freqüentemente atualizados com novas sobrecargas. O uso de Option Strict On (Opção estrita ativada) pode ajudar a evitar a religação silenciosa, já que alertará você para algumas condições nas quais podem ocorrer conversões implícitas.

Estendendo tipo de objeto

Será possível estender o tipo Object, o qual permitirá que o método seja visto em qualquer tipo derivado do tipo Object. Mas se você criar um tipo como objeto e tentar usar nele o método de extensão, não será possível ver o método de extensão. Isso porque métodos de extensão não oferecem suporte à ligação tardia.

Observe que na Figura 5, a primeira chamada funcionará porque a variável s é do tipo Integer e deriva de Object. Como resultado, você verá o método de extensão. A variável T, entretanto, é do tipo object e é de ligação tardia, resultando assim em um erro de compilação.

Figure 5 Suporte para herança (Close Figure 5)
Imports System.Runtime.CompilerServices

Module Module1
    Sub Main()
        Dim s As Integer = 1234
        Console.WriteLine(s.Foo) '<- This will work

        Dim T As Object
        T = 1234
        Console.WriteLine(T.Foo)  '<- This will fail
    End Sub
End Module

Module Extensions01
    <Extension()> _
    Function Foo(ByVal b As Object) As String
        Return "Bar01"
    End Function
End Module

O código pode ser corrigido sendo alterado para o seguinte:

Dim T = 1234
Console.WriteLine(T.Foo)

Isso agora funcionará por causa do recurso de inferência de tipos. A variável T é inferida como inteiro baseado no valor que lhe será atribuído. É diferente do Visual Studio 2005, onde T seria do tipo Object contendo um inteiro emoldurado.

Métodos de extensão nos aplicativos .NET Framework 2.0

Como observei anteriormente, os métodos de extensão não fazem nada no código IL subjacente que não pudesse ser feito usando o Visual Basic 2005; eles simplesmente fazem uma chamada de método compartilhada. O que foi alterado é a capacidade do compilador de identificar métodos específicos e permitir a alteração na sintaxe do método de chamada, o que dá a aparência de que o tipo usado tem uma nova funcionalidade e faz as extensões trabalharem como os métodos de instância existentes.

Outro recurso no Visual Basic 2008 é o multi-direcionamento, que permite usar o Visual Basic 2008 para gravar códigos que direcionam uma versão específica do Framework. Como o mesmo compilador é usado para o multi-direcionamento, é possível usar métodos de extensão para os aplicativos direcionados, por exemplo, para a versão 2.0 do Framework. Isso exige um pequeno artifício, já que o IDE evita a adição de referências exigidas a um aplicativo direcionado ao .NET Framework 2.0.

Se você criar um aplicativo 3.5 e alterar o Framework de destino para a versão 2.0 (usando Project Properties | Compiler | Advanced | Target Framework [Propriedades do projeto | Compilador | Avançado | Framework de destino]), você verá algumas das referências destacadas como ausentes, incluindo aquelas que contém a maioria das funcionalidades LINQ prontas. Tentar adicionar estas referências a um projeto 2.0 resultará em uma caixa de diálogo informando a você que será necessária uma versão diferente do Framework de destino (consulte a Figura 6).

Figura 6 Referências para destino alterado para o .NET Framework 2.0
Figura 6 Referências para destino alterado para o .NET Framework 2.0(Clique na imagem para diminuir a exibição)
Figura 6 Referências para destino alterado para o .NET Framework 2.0
Figura 6 Referências para destino alterado para o .NET Framework 2.0 (Clique na imagem para aumentar a exibição)

Os métodos de extensão poderão ser criados e usados nos aplicativos 2.0 depois da criação do seu próprio atributo System.Runtime.CompilerServices.Extension. Você deverá, primeiramente, criar um novo projeto com a versão 2.0 como destino. As referências padrão não incluirão System.Core, que contém o atributo de extensão. Nas Applications Project Properties (Propriedades do projeto de aplicativo), vá para a guia Application (Aplicativo), limpe o Root Namespace (Namespace da raiz) e crie seu próprio atributo de extensão no namespace System.Runtime.CompilerServices correto. O código na Figura 7 exibe a criação do atributo de extensão e seu uso para declarar e usar métodos de extensão. Depois de compilar o aplicativo, será possível chamar os métodos de extensão.

Figure 7 Criando uma classe ExtensionAttribute (Close Figure 7)
Imports System.Runtime.CompilerServices

Module Module1
    Sub Main()
        Dim i_x As Integer = 1
        Dim i_s As String = "Test"

        Dim x = i_x.IntegerExtension
        MsgBox(x)

        Dim y = i_s.StringExtension
        MsgBox(y.ToString)
    End Sub
End Module

'CREATE A USER-DEFINED ATTRIBUTE CALLED EXTENSION IN THE CORRECT NS
Namespace System.Runtime.CompilerServices
    <AttributeUsage(AttributeTargets.Assembly Or AttributeTargets.Class Or AttributeTargets.Method, AllowMultiple:=False, Inherited:=False)> Class ExtensionAttribute
        Inherits Attribute
    End Class
End Namespace

'DEFINED EXTENSION METHODS WITH USER-DEFINED ATTRIBUTE ON THEM
Module ExtMethods
    <Extension()> _
Function StringExtension(ByVal a As String) As String
        Return "Success"
    End Function

    <Extension()> _
Function IntegerExtension(ByVal a As Integer) As Integer
        Return 100
    End Function
End Module

Aqui estão algumas considerações importantes relacionadas aos aplicativos 2.0 como destino. Primeiramente, a inferência de tipos está desativada por padrão. Para ativá-la novamente, use Option Infer On. Você provavelmente desejará fazer isso quando migrar o aplicativo .NET Framework 2.0 para a versão 3.5.

Segundo, para usar os métodos de extensão, será necessário criar seu próprio atributo de extensão que simula aquele do System.Core. O IDE evitará que você use a referência System.Core em um projeto de 2.0 como destino.

Finalmente, esta técnica não funcionará para aplicativos ASP.NET com .NET Framework 2.0 como destino porque eles têm uma dependência de tempo de execução no compilador da linha de comando do 2.0. Quando esses aplicativos são implantados para os servidores Web que têm apenas o .NET Framework 2.0 instalado, eles não funcionarão porque o compilador da linha de comando 2.0 VBC.EXE não entende métodos de extensão.

Mesmo que você não queira começar a escrever aplicativos para o .NET Framework 3.5 ou se você tiver aplicativos existentes na versão 2.0 que deseja manter, você ainda poderá se beneficiar da atualização para o Visual Basic 2008. A funcionalidade do método de extensão é embutida no compilador e pode ser usada em qualquer versão do Framework que o compilador do Visual Basic 2008 seja capaz de atingir.

Envie suas perguntas e comentários para instinct@microsoft.com.

Adrian "Spotty" Bowles desenvolveu usando todas as versões do Visual Basic e conseguiu encontrar seu caminho para Redmond, WA, onde trabalha na equipe de produto do Visual Basic como Testador de engenharia de design de software com foco no compilador do Visual Basic. Durante a versão do Visual Basic 2008, ele trabalhou em vários recursos de linguagem, inclusive métodos de extensão. Você pode entrar em contato com Spotty pelo email Abowles@microsoft.com.


Espa&#231;o do Office: Eventos no SharePoint 2007

Espaço do Office
Eventos no SharePoint 2007
Ted Pattison

Download do código disponível em: OfficeSpace2007_11.exe (167KB)
Conteúdo
Classes receptoras de eventos
Eventos Before e After
Validando eventos Before
Manipuladores de eventos After
Ligando manipuladores a eventos
Usando o modelo de objeto do WSS

O
Windows® SharePoint® Services (WSS) 3.0 oferece aos desenvolvedores muitos aprimoramentos significativos para criar soluções empresariais personalizadas com base nos sites do SharePoint. Um dos maiores aperfeiçoamentos direcionados para desenvolvedores está relacionado com uma nova infra-estrutura para manipular eventos de servidores. Por exemplo, quando um usuário executar uma ação que modifica o conteúdo em um site do SharePoint (carregando um documento novo ou excluindo um item de uma lista) haverá a oportunidade de um desenvolvedor responder a esta ação do usuário com um manipulador de eventos capaz de executar lógica de servidor escrita em C# ou no Visual Basic®.

Apesar do WSS 2.0 também fornecer uma infra-estrutura de manipulação de eventos de servidor, as coisas foram amplamente aperfeiçoadas no WSS 3.0. A Figura 1 mostra uma visão geral das diferenças entre o suporte a manipuladores de evento no WSS 2.0 versus o WSS 3.0. Além disso, é importante entender que a nova infra-estrutura do manipulador de eventos do WSS 3.0 não é um mero refinamento do que já existia na versão anterior. A nova infra-estrutura do manipulador de eventos do WSS 3.0 representa uma substituição completa da infra-estrutura do manipulador de eventos do WSS 2.0. Uma das maiores alterações é que o manipulador de eventos será implementado no nível da lista e não no nível da biblioteca de documentos. Isso significa que eventos que lançam definições de listas e itens de lista funcionam em todos os tipos de listas, incluindo bibliotecas de documentos.

Figure 1 Manipulação de evento no WSS 2.0 e no WSS 3.0 (Close Figure 1)

Tipo de evento | Infra-estrutura do WSS 2.0 | Infra-estrutura do WSS 3.0
Eventos em bibliotecas de documentos | Com suporte | Com suporte
Vários manipuladores de eventos ligados a um único evento | Sem suporte | Com suporte
Eventos em listas | Sem suporte | Com suporte
Eventos em definições de listas | Sem suporte | Com suporte
Eventos em sites | Sem suporte | Com suporte
Manipuladores de eventos síncronos do tipo Before com cancelamento | Sem suporte | Com suporte
Manipuladores de evento assíncronos do tipo After | Com suporte | Com suporte
Classes de manipulador de eventos | A interface IListEventSink deve ser implementada | Devem herdar as classes base receptoras no modelo de objeto do WSS

A infra-estrutura antiga do WSS 2.0 tem um modelo de programação completamente diferente e exige uma abordagem diferente para conectar os manipuladores de eventos aos eventos. O WSS 3.0 oferece suporte à nova infra-estrutura do manipulador de eventos da versão 3.0 e à infra-estrutura antiga do WSS 2.0. No entanto, você deve entender que o suporte do WSS 3.0 para o antigo estilo do 2.0 de manipuladores de eventos foi incluído apenas para fornecer compatibilidade com versões anteriores ao integrar o código escrito das soluções do WSS 2.0 ao ambiente do WSS 3.0. Quando você começar a desenvolver soluções para o WSS 3.0 e para o Microsoft® Office SharePoint Server (MOSS) 2007, você deverá empregar a nova infra-estrutura de manipulador de eventos do WSS 3.0, que é baseada em conceitos novos e no modelo de programação que vou apresentar este mês na coluna Espaço do Office.

Classes receptoras de eventos

O manipulador de eventos do WSS 3.0 é baseado em classes receptoras de eventos. Mais especificamente, você cria uma nova classe receptora de eventos herdando uma das várias classes base receptoras de eventos que são oferecidas como classes públicas dentro do modelo de objeto do WSS. Para fazer isso, você deve criar um novo projeto de biblioteca de classe que faça referência ao assembly do WSS principal chamado Microsoft.SharePoint.dll.

A lista das classes bases receptoras disponíveis pelo modelo de objeto do WSS será exibida na Figura 2. Depois de criar uma definição de classe herdeira de uma dessas classes base receptoras do WSS, adicione os manipuladores de eventos substituindo os métodos selecionados.

Figure 2 Classes base receptoras de eventos do WSS (Close Figure 2)

Classe | Descrição
SPListEventReceiver | Fornece manipuladores de eventos quando os usuários adicionam, alteram e removem colunas de uma definição de lista.
SPItemEventReceiver | Fornece manipuladores de eventos quando os usuários modificam itens de uma lista ou quando modificam documentos de uma biblioteca de documentos.
SPWebEventReceiver | Fornece manipuladores de eventos quando os usuários movem ou excluem um conjunto de sites ou um site.
SPEmailEventReceiver | Fornece manipuladores de eventos quando os usuários enviam um e-mail para uma lista habilitada para e-mails.

Depois de implementar uma classe receptora personalizada com um ou mais manipuladores de eventos, você deverá compilá-los em um assembly com um nome forte e instalá-los no GAC (cache de assembly global). Em seguida, você poderá ligar os seus manipuladores de eventos aos eventos expostos por um objeto host, como um site, uma lista ou uma biblioteca de documentos. Como você verá posteriormente nesta coluna, há duas opções quando se trata da ligação de manipuladores de eventos. Você pode ligar manipuladores de eventos usando um elemento Feature com uma sintaxe XML declarativa ou ligá-los programaticamente usando o modelo de objeto do WSS. Depois de fazer a ligação de um método de manipulador de eventos com um evento específico, o WSS se encarregará de executar o seu manipulador em resposta às ações do usuário que acionam o evento.

Eventos Before e After

Os eventos do WSS 3.0 podem ser separados em duas categorias principais: Eventos Before e After. Os eventos Before são disparados em resposta a uma ação do usuário antes de o WSS ter gravado quaisquer dados de volta no banco de dados de conteúdo. Um ponto importante é que os eventos Before ocorrem tão cedo no ciclo de vida do processamento de solicitação que eles oferecem suporte ao cancelamento da ação do usuário. Portanto, os eventos Before oferecem uma grande oportunidade para realizar verificações de segurança e validação personalizada.

O WSS também oferece suporte a eventos After, cujos manipuladores de eventos são executados após o WSS 3.0 ter gravado de volta no banco de dados de conteúdo para confirmar a ação do usuário. Os eventos After não oferecem suporte ao cancelamento da ação do usuário. Em vez disso, os eventos After são usados para ativar a lógica do processamento personalizado criada para ser executada sempre que o conteúdo tiver sido modificado.

O manipulador de eventos para eventos Before é executados no mesmo thread que está processando a ação do usuário (como adicionar um item). Portanto, os eventos Before têm uma natureza bloqueadora e são normalmente chamados de eventos síncronos. O WSS executa os manipuladores de eventos para eventos After em um thread separado, pois assim seu processamento não bloqueia a resposta enviada de volta ao usuário. Por esta razão, os eventos After são normalmente chamados de eventos assíncronos. Observe também que os eventos Before são baseados em métodos substituíveis, cujos nomes terminam com "ing" (por exemplo, FieldDeleting), enquanto os nomes de método dos eventos After terminam com "ed" (por exemplo, FieldDeleted).

Vamos examinar um exemplo de autoria de uma nova classe receptora personalizada que define um manipulador de eventos para eventos Before e After. A Figura 3 exibe uma classe receptora personalizada que herda da classe SPListEventReceiver e substitui os métodos FieldDeleting e FieldDeleted. Você deverá observar que quando o WSS chamar um desses manipuladores de eventos, ele passará um único parâmetro de propriedades nomeadas do tipo SPListEventProperties.

Figure 3 Classe receptora personalizada (Close Figure 3)
Imports Microsoft.SharePoint

Public Class MyListEvents
    Inherits SPListEventReceiver

    Public Overrides Sub FieldDeleting ( _
        ByVal properties As SPListEventProperties)
    '*** your custom event handling code goes here
    End Sub

    Public Overrides Sub FieldDeleted ( _
        ByVal properties As SPListEventProperties)
    '*** your custom event handling code goes here
    End Sub

End Class

Como o método FieldDeleting manipula um evento Before, ele oferecerá suporte ao cancelamento da ação do usuário. Imagine um cenário onde você deseja proibir todos os usuários, incluindo o administrador do conjunto de sites, de serem capazes de excluir colunas de uma definição de lista específica. É possível fazer isso atribuindo um valor de True para a propriedade Cancel das propriedades nomeadas do parâmetro SPListEventProperties. Ao cancelar a ação do usuário, você também deverá enviar uma mensagem de erro personalizada atribuindo um valor de seqüência de caracteres à propriedade ErrorMessage. Esta mensagem de erro será exibida diretamente ao usuário:

Public Overrides Sub FieldDeleting( _
    ByVal properties As SPListEventProperties)
    '*** cancel user action
    properties.ErrorMessage = "Deleting not supported!"
    properties.Cancel = True
End Sub

Quando um manipulador de eventos para um evento Before atribuir um valor de True à propriedade Cancel, o WSS responderá por meio de curto circuito a solicitação e cancelará a ação do usuário. Em vez de excluir a coluna conforme solicitado pelo usuário, o WSS exibirá a mensagem de erro personalizada para o usuário. O ponto principal aqui é que os eventos Before fornecem uma camada de defesa contra modificações indesejadas.

Agora que você viu o básico sobre cancelamento da ação de usuário no manipulador de eventos para um evento Before, vamos criar outro manipulador de eventos para um evento Before com alguma lógica condicional. Imagine que você tenha um requisito onde você precise executar uma verificação de segurança para assegurar que os usuários atendam a certos critérios antes de permitir que eles excluam itens de uma lista ou documentos de uma biblioteca de documentos. Examine o manipulador de eventos ItemDeleting na definição de classe receptora exibida na Figura 4. Observe que a lógica condicional cancelará a ação do usuário quando ele determinar que o usuário não possui as permissões de administrador do site.

Figure 4 Verificação de segurança simples (Close Figure 4)
Public Class MyItemEvents
    Inherits SPItemEventReceiver

    Public Overrides Sub ItemDeleting( _
        ByVal properties As SPItemEventProperties)

        If (Not properties.OpenWeb().CurrentUser.IsSiteAdmin()) Then
            Dim ErrorMessage As String
            ErrorMessage = "Only site administrator can delete items"
            properties.ErrorMessage = ErrorMessage
            properties.Cancel = True
        End If

    End Sub

End Class

Como é possível observar, esta lógica de manipulador de eventos exige que o usuário atual tenha certas permissões antes de permitir que ocorra uma exclusão. Levando esta etapa um pouco além, é possível escrever um manipulador de eventos para determinar se os usuários poderão excluir itens dependendo de serem membros de um grupo do Active Directory®. Como o WSS se baseia no tempo de execução do ASP.NET é possível fazer isso com uma técnica de programação simples do ASP.NET e consultar o método IsInRole exposto pela propriedade User do objeto HttpContext:

If (Not HttpContext.Current.User.IsInRole( _
    "MyDomain\ContentManagers")) Then
    Dim ErrorMessage As String
    ErrorMessage = _
    "Only members of ContentManagers can delete items"
    properties.ErrorMessage = ErrorMessage
    properties.Cancel = True
End If

Validando eventos Before

Agora, vamos continuar e examinar as possibilidades para escrever a lógica de validação em um manipulador de eventos para um evento Before. Isso possibilita escrever manipuladores de eventos que fazem os usuários estarem em conformidade com as restrições específicas do aplicativo quando eles adicionarem ou modificarem os itens de uma lista. Por exemplo, você pode validar que a data de envio de uma nota fiscal não seja no sábado ou domingo ou que o número de telefone ou o endereço de e-mail corresponda a um tipo definido por uma expressão regular.

Um dos aspectos mais poderosos de escrever uma validação lógica para eventos Before é que você pode realizar uma validação que se estenda pelas colunas. No código de exemplo para este mês, eu uso uma lista z contendo uma coluna para número de telefone de trabalho e uma coluna para endereço de e-mail. Tenho um manipulador de eventos que realizará uma validação em cada item da lista Sales Leads para exigir que o número de telefone de trabalho ou o endereço de e-mail não sejam valores em branco. A idéia básica aqui é que os líderes de vendas não sejam válidos se alguém não tiver inserido alguma informação de contato.

Há duas formas para um usuário modificar um item da lista. Um usuário poderá adicionar um novo item ou atualizar um item existente. Quando escrever uma lógica de validação para eventos Before, você deverá, em geral, adicionar um código redundante por trás do manipulador de eventos ligado ao evento ItemAdding e ao evento ItemUpdating. Portanto, é melhor manter a lógica de validação em um método separado que possa ser chamado a partir de dois manipuladores de eventos diferentes. O método SalesLeadIsValid exibido na Figura 5 aceita dois parâmetros de seqüência de caracteres denominados Phone e E-mail e retornará True se um desses parâmetros não estiver em branco.

Figure 5 Validando entradas (Close Figure 5)
Private Function SalesLeadIsValid(
    ByVal Phone As String, ByVal Email As String) _
    As Boolean

    '*** make sure we have either phone or e-mail
    If (String.IsNullOrEmpty(Phone) And _
        String.IsNullOrEmpty(Email)) Then
        Return False
    Else
        Return True
    End If

End Function

Com a lógica de validação dentro do método SalesLeadIsValid, é possível chamar este método a partir de um manipulador de eventos ligado ao evento ItemAdding bem como ao evento ItemUpdating. Observe que dentro de um manipulador de eventos para um evento ItemAdding ou ItemUpdating, será necessário recuperar os valores da coluna na qual você deseja realizar as validações. O parâmetro SPItemEventProperties expõe duas coleções chamadas BeforeProperties e AfterProperties. A idéia é que você possa recuperar o valor de qualquer coluna no estado anterior ou posterior ao que o usuário alterou.

A Figura 6 exibe um exemplo da implementação de um manipulador de eventos ItemAdding que recupera os valores After de telefone e e-mail e os passa para o método SalesLeadIsValid para determinar se a ação do usuário será realizada com êxito.

Figure 6 Validando em relação a uma ação (Close Figure 6)
Public Overrides Sub ItemAdding( _
    ByVal properties As SPItemEventProperties)

    Dim Phone As String, Email As String
    Phone = _
        CType(properties.AfterProperties("WorkPhone"), _
        String)
    Email = _
        CType(properties.AfterProperties("EMail"), _
        String)

    If (Not SalesLeadIsValid(Phone, Email)) Then
        Dim ErrorMessage As String
        ErrorMessage = _
            "Sales lead must have valid phone or email"
        properties.ErrorMessage = ErrorMessage
        properties.Cancel = True
    End If
End Sub

Examine mais de perto como o manipulador de eventos recupera os valores das duas colunas a partir da coleção AfterProperties. A AfterProperties é uma coleção com uma propriedade padrão parametrizada (um indexador) que aceita o nome de uma coluna. Observe que o parâmetro passado ao AfterProperties é um valor de seqüência de caracteres que diferencia maiúsculas de minúsculas. Você também deve entender que este parâmetro representa o nome físico da coluna e não o nome de exibição.

Manipuladores de eventos After

Um evento After é diferente de um evento Before porque ele é disparado após uma ação ser concluída e não oferece ao desenvolvedor a oportunidade de cancelar a ação. Os eventos After oferecem aos desenvolvedores a oportunidade de ativar o processamento personalizado que deverá ser executado quando o usuário concluir com êxito uma ação que modificou o conteúdo de um site do SharePoint. Por exemplo, após um usuário adicionar um item novo à lista, um evento After poderá ser usado para formatar novamente uma das colunas a fim de manter a integridade dos dados ou enviar uma mensagem de e-mail de notificação a um usuário específico ou a uma lista de distribuição.

Vamos examinar um exemplo de escrita de um manipulador de eventos para a nossa lista Sales Lead que será ligada ao evento ItemAdded. Neste exemplo, há a exigência de formatar todos os nomes dos líderes de vendas com letras maiúsculas. O manipulador de eventos se encarregará de pegar o valor da coluna com o nome do líder de vendas e a formatará novamente usando letras maiúsculas.

Observe que você precisa tomar cuidado ao escrever um manipulador de eventos para um evento After e ao modificar o item no qual o evento é baseado. O problema é que a modificação do item fará com que o evento seja acionado novamente. Quando o evento for acionado novamente, ele desejará modificar o item, iniciando um loop recursivo. Para evitar isto, você deverá desabilitar o disparo de eventos no item atual enquanto ele estiver sendo modificado. Isso é feito chamando o método DisableEventFiring fornecido pela classe base subjacente. Após modificar o item, chame o EnableEventFiring. Veja como todas as partes se encaixam perfeitamente:

Public Overrides Sub ItemUpdated( _
    ByVal properties As SPItemEventProperties)

    Me.DisableEventFiring()
    Dim SalesLead As String = _
        CType(properties.ListItem("Title"), String)
    '*** reformat item column values as required
    properties.ListItem("Title") = SalesLead.ToUpper()
    properties.ListItem.Update()
    Me.EnableEventFiring()
End Sub

Você acaba de conhecer vários exemplos diferentes para escrever manipuladores de eventos para eventos Before e After. Agora, você se sentirá mais à vontade com por que e como escrever manipuladores de eventos. A questão principal que resta é como ligá-los aos eventos no objeto host dentro de um site do SharePoint; então, vamos examinar isso agora.

Ligando manipuladores a eventos

Conforme mencionei anteriormente, as classes receptoras devem ser compiladas em uma DLL de assembly com um nome forte e instalado no GAC. As suas opções para ligar manipuladores de eventos são usar um XML declarativo em um elemento Feature ou ligá-los programaticamente usando o modelo de objeto do WSS. No entanto, as duas abordagens ainda exigem que você saiba o nome de quatro partes do assembly completo e o nome qualificado por namespace da classe receptora contendo os manipuladores de eventos aos quais você deseja fazer a ligação.

Começarei discutindo como ligar manipuladores de eventos a um tipo de lista usando XML declarativo em um Feature. Isso pode ser feito adicionando um elemento Receivers com um ou mais elementos Receiver internos. No código de exemplo que acompanha esta coluna, há um Feature com um elemento Receivers que liga dois manipuladores de eventos diferentes ao tipo de lista do WSS Contacts (consulte a Figure 7). A lista Contacts tem um identificador de tipo de lista de 105. Isso significa que todas as listas criadas a partir da lista Contacts de um site que tenha o Feature ativado terão esse manipulador de eventos ligado automaticamente.

Figure 7 Receptor de lista de contatos (Close Figure 7)
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">

  <!-- Receivers element used only in feature where Scope=Web -->
  <Receivers ListTemplateId="105" >

    <Receiver>
      <Name>Field_Deleting_Event</Name>
      <Type>FieldDeleting</Type>
      <Assembly>OfficeSpaceEventDemo, [4-part assembly name]</Assembly>
      <Class>OfficeSpaceEventDemo.MyListEvents</Class>
      <SequenceNumber>1000</SequenceNumber>
      <Data>My Custom Data</Data>
    </Receiver>

    <Receiver>
      <Name>Field_Deleted_Event</Name>
      <Type>FieldDeleted</Type>
      <Assembly>OfficeSpaceEventDemo, [4-part assembly name]</Assembly>
      <Class>OfficeSpaceEventDemo.MyListEvents</Class>
      <SequenceNumber>1000</SequenceNumber>
      <Data>My Custom Data</Data>
    </Receiver>

  </Receivers>
</Elements>

Observe que dentro do elemento Receivers você deverá fornecer um elemento Receiver separado para cada manipulador de eventos que você desejar ligar. Não há técnicas que ofereçam suporte para ligar vários manipuladores de eventos de uma só vez com um único elemento Receiver.

Há duas partes obrigatórias para ligar informações a um elemento Receiver. Elas são o elemento Assembly e o elemento Class, que devem conter o nome de quatro partes de assembly completo e o nome da classe receptora qualificada por namespace, respectivamente. O elemento Name dentro do elemento Receiver poderá ser usado para atribuir um nome amigável ao manipulador de eventos com o objetivo de acompanhamento, mas não é obrigatório.

O elemento SequenceNumber dentro de um elemento Receiver é útil quando existem diversos manipuladores de eventos ligados a um único evento. O manipulador de eventos com a menor seqüência numérica sempre será acionado antes de um manipulador de eventos com uma seqüência numérica maior. Portanto, é possível adicionar vários manipuladores de eventos e garantir uma ordem de execução determinística.

O elemento Data dentro do elemento Receiver permite que você configure um manipulador de eventos específico para ser ligado com qualquer dado de seqüência de caracteres personalizado que você desejar. É possível acessar este valor da seqüência de caracteres a partir do elemento Data usando a propriedade ReceiverData do parâmetro SPListEventProperties e usá-lo de uma forma específica no aplicativo para fornecer números ou expressões regulares para validar rotinas ou personalizar mensagens de erro:

Public Overrides Sub FieldDeleting( _
    ByVal properties As SPListEventProperties)

    Dim BindingData As String
    BindingData = properties.ReceiverData
  '*** use binding data in application-specific fashion
End Sub

Usando o modelo de objeto do WSS

Embora a ligação de manipuladores de eventos aos eventos usando declarativamente um elemento Receivers em um Feature funciona em alguns cenários, ela não tem a flexibilidade necessária em muitos outros. Por exemplo, o elemento Receivers só poderá ser usado para ligar um manipulador de eventos a um tipo de lista em todo o site. Esta abordagem declarativa não fornece um modo para ligar um manipulador de eventos a uma única instância de lista em um site sem ligar também todas as outras listas do mesmo tipo.

Ao criar soluções de SharePoint personalizadas que utilizam manipuladores de eventos, você verá que ligar manipuladores de eventos com código oferece muito mais flexibilidade. Por exemplo, você poderá ligar um manipulador de eventos a um evento de apenas uma lista.

Vamos examinar um exemplo de ligação de um manipulador de eventos do evento ItemAdding à lista Sales Leads.

Dim SalesLeadsList As SPList = Me.Web.Lists("Sales Leads")
    '*** bind event handler to ItemAdding event
    SalesLeadsList.EventReceivers.Add( _
        SPEventReceiverType.ItemAdding, _
        GetType(MyItemEvents).Assembly.FullName, _
        GetType(MyItemEvents).FullName)

Cada objeto SPList expõe uma coleção de EventReceivers. A coleção EventReceivers expõe um método Add com três implementações sobrecarregadas diferentes. O método Add deste último exemplo representa a abordagem mais simples, que exige passar três parâmetros. Quando você desejar ligar um manipulador de eventos por meio de código, você deverá fornecer pelo menos o nome do assembly, o nome da classe qualificada por namespace e o tipo de evento, usando a enumeração SPEventReceiverType do modelo de objeto do WSS.

Observe que embora você possa embutir valores de seqüência de caracteres literais dentro do código contendo o nome de quatro partes do assembly e o nome da classe receptora qualificada por namespace, você também poderá usar o código exibido anteriormente para descobrir esses valores dinamicamente.

Chamar o método Add representa a abordagem mais fácil, mas não é flexível o suficiente para todos os cenários. A Figura 8 ilustra uma abordagem mais complicada na qual a ligação do manipulador de eventos é realizada por meio da programação direta em um objeto SPEventReceiverDefinition. Isso permite fornecer à ligação um identificador GUID específico e, assim, você poderá adicioná-los e removê-los mais facilmente. Isso também permite inicializar as propriedades SequenceNumber e Data.

Figure 8 Ligando-se a um GUID (Close Figure 8)
Dim SalesLeadsList As SPList = Me.Web.Lists("Sales Leads")

Dim receivers As SPEventReceiverDefinitionCollection
receivers = SalesLeadsList.EventReceivers
Dim ItemAddingDefId As Guid
ItemAddingDefId = New Guid("D6C452E5-95D4-45e8-934C-30F785867000")
If (Not receivers.EventReceiverDefinitionExist(ItemAddingDefId)) Then
    Dim ItemAddingDef As SPEventReceiverDefinition
    ItemAddingDef = receivers.Add(ItemAddingDefId)
    ItemAddingDef.Name = "ItemAddingHandler"
    ItemAddingDef.Type = SPEventReceiverType.ItemAdding
    ItemAddingDef.Assembly = GetType(MyItemEvents).Assembly.FullName
    ItemAddingDef.Class = GetType(MyItemEvents).FullName
    ItemAddingDef.Data = "My Custom Data"
    ItemAddingDef.SequenceNumber = 1001
    ItemAddingDef.Update()
End If

Isso é apenas uma pequena amostra do que é possível quando é feita a ligação de manipuladores de eventos por meio do modelo de objeto do WSS. Eu demonstrei o trabalho com a coleção EventReceivers de uma lista, mas você deve saber que vários outros tipos do modelo de objeto do WSS têm uma coleção EventReceivers e poderão agir como hosts de eventos. Os exemplos deste tipo incluem sites, tipos de conteúdo e arquivos.

Envie suas dúvidas e comentários para Ted pelo e-mail mmoffice@microsoft.com.

Ted Pattison é autor, instrutor e MVP de SharePoint estabelecido em Tampa, Flórida. Ted acabou de escrever um livro intitulado Inside Windows SharePoint Services 3.0 para a Microsoft Press. Ele também ministra um curso avançado de SharePoint para desenvolvedores profissionais por meio de sua empresa, a Ted Pattison Group (www.Ted Pattison.net).


Cutting Edge: Gerenciando a experi&#234;ncia do usu&#225;rio no AJAX

Cutting Edge
Gerenciando a experiência do usuário no AJAX
Dino Esposito

Download do código disponível em: CuttingEdge2007_11.exe (183KB)
Conteúdo
AJAX e a interface do usuário
Desativando a interface do usuário por meio dos IDs de elemento
Desativando elementos da interface do usuário em recipientes de nomenclatura
Usando o extensor UpdatePanelAnimation
Desativando a página inteira
Um postback AJAX por vez
Chamadas de alta prioridade

T
odos nós estamos acostumados com a natureza stop-and-go da Web. Para cada solicitação feita com o clique de um botão ou link, o navegador faz uma solicitação e aguarda pela resposta. Você trabalha um pouco nessa página e, em seguida, clica e aguarda novamente.

Como mencionei nas colunas recentes, estou muito empolgado com as possibilidades oferecidas pelo paradigma AJAX e seu conjunto relacionado de tecnologias. Os desenvolvedores e arquitetos podem criar uma nova geração de sites da Web nos quais o padrão stop-and-go será substituído por uma estrutura mais interativa. Sempre gostei de ver o sincero entusiasmo dos usuários quando eles experimentam pela primeira vez a aparência contínua de um aplicativo AJAX.

Nas colunas recentes, exaltei o poder do AJAX, apresentando vários benefícios diretos e indiretos. Este mês, quero me concentrar em algumas questões relacionadas diretamente à implementação do programa AJAX usando as Extensões do ASP.NET AJAX.

Inicialmente desenvolvidas com base no ASP.NET 2.0, as Extensões do ASP.NET AJAX foram totalmente integradas à plataforma ASP.NET com o lançamento do ASP.NET 3.5. Na verdade, o ASP.NET AJAX tem dois modelos de programação para sua escolha – processamento parcial e serviços de script. Para uma discussão mais aprofundada dos dois, recomendo que você leia as minhas colunas de setembro e de outubro de 2007 da MSDN® Magazine.

Um aplicativo AJAX tem o poder de fazer solicitações da Web diretamente do código de script, desviando-se do navegador. Quando você usa o navegador para solicitar uma página – por exemplo, por meio de um botão de envio padrão ou hiperlink – apenas uma solicitação é processada por vez para cada janela ativa do navegador. A solicitação de uma página por meio de script não tem essa limitação e várias solicitações podem ser feitas simultaneamente. Na edição de julho de 2007 da Cutting Edge, aproveitei esse recurso para implementar uma barra de progresso dinâmica que informa ao cliente o progresso real feito por uma tarefa em execução no servidor. A possibilidade de realizar chamadas simultâneas de uma única janela do navegador abre um novo mundo repleto de possibilidades para a solução de vários problemas – incluindo muitos relacionados à interface do usuário.

Na minha coluna deste mês, analisarei os problemas, soluções e ferramentas necessárias para implementar uma interface do usuário eficaz para aplicativos da Web com base no ASP.NET AJAX. Especificamente, o foco será nos aplicativos que usam o modelo de processamento parcial.

AJAX e a interface do usuário

Uma página da Web permanece ativa mesmo depois que o usuário tiver efetuado uma solicitação AJAX. Isso significa que o usuário pode clicar em elementos ativos e iniciar novas operações que poderiam interferir na solicitação atual. Portanto, como ocorre em aplicativos de área de trabalho, é preciso que, às vezes, os desenvolvedores considerem a desativação temporária de partes da interface do usuário.

Como você pode desativar elementos da página do cliente para a duração de uma operação do servidor? Há algumas formas de fazer isso. Você pode recuperar a identificação do elemento do DOM (modelo do objeto de documento) correspondente e ativar ou desativar seu estado – essa é uma operação do cliente que você realiza por meio do script usando o DOM ou talvez estilos CSS. Além disso, os controles do ASP.NET têm a propriedade Visible. Quando definida como false, a propriedade Visible impede que o controle do servidor adicione sua própria marcação à resposta do cliente. Conseqüentemente, a marcação esperada para esse controle não é exibida na página do cliente.

Para desativar e ativar uma porção da interface do usuário no ASP.NET AJAX, você precisa identificar primeiro os IDs de todos os elementos HTML que formam essa parte, como mostra o código a seguir:

// Get the reference to the DOM element
var button1 = $get("Button1");

// Disable the status of the element
button1.disabled = true;

Para desativar elementos, recupere a identificação e defina a propriedade desativada como o valor apropriado. Idealmente, você deve desativar a parte específica da interface do usuário assim que for fazer a solicitação e reativá-la logo depois que a solicitação for concluída. Em relação à implementação, isso irá variar dependendo do modelo de programação escolhido.

Se você estiver usando o processamento parcial, use o modelo de eventos do objeto PageRequestManager – essa classe é o cérebro do cliente por trás do processamento parcial. O nome completo da classe é Sys.WebForms.PageRequestManager e seu código-fonte é definido no MicrosoftAjaxWebForms.js. O PageRequestManager contém toda a lógica necessária para configurar e controlar postbacks AJAX. A classe é um singleton – que significa que ele garante que exatamente uma solicitação estará pendente por vez. Obtenha a referência para a instância exclusiva do gerenciador de solicitação de página por meio do seguinte código:

var manager = Sys.WebForms.PageRequestManager.getInstance();

O PageRequestManager expõe poucos eventos do cliente que permitem que você intercepte etapas fundamentais no processamento de um postback AJAX, como descrito na Figura 1. A ocorrência do evento BeginRequest é geralmente um bom momento para desativar elementos da interface do usuário, e o evento EndRequest é o momento certo para reativar elementos HTML.

Figure 1 Eventos expostos pelo PageRequestManager (Close Figure 1)

Evento | Argumento | Descrição
initializeRequest | InitializeRequestEventArgs | Disparado assim que for detectado que uma nova solicitação AJAX foi emitida.
beginRequest | BeginRequestEventArgs | Disparado pouco antes da solicitação ser enviada.
pageLoading | PageLoadingEventArgs | Disparado assim que a resposta foi totalmente baixada e processada, mas antes que qualquer conteúdo na página seja atualizado.
pageLoaded | PageLoadedEventArgs | Disparado assim que todo o conteúdo na página for atualizado.
endRequest | EndRequestEventArgs | Disparado assim que todo o trabalho for concluído.

Se você tiver optado por uma abordagem de serviços de script, então provavelmente terá um front-end com JavaScript que se comunica com uma fachada do servidor composta de Web services de script do ASP.NET AJAX. Cada chamada para serviços é realizada por meio de uma chamada de script assíncrona explícita. O desenvolvedor tem o controle total do início da chamada remota e pode desativar com segurança quaisquer partes da interface do usuário. Como cada chamada será assíncrona, pode ser especificado um retorno de chamada para êxito e falha da chamada. Então, a partir do retorno de chamada, você pode redefinir a interface do usuário de volta para o status adequado.

Dito tudo isso, há alguns problemas relativos à desativação e reativação dos elementos da interface do usuário. Investigarei isso em alguns cenários.

Desativando a interface do usuário por meio dos IDs de elemento

A Figura 2 mostra o código utilizado para desativar alguns elementos de uma página AJAX de exemplo (usando o processamento parcial) durante postbacks. Quando a primeira página for carregada, os manipuladores dos eventos BeginRequest e EndRequest serão registrados. A função pageLoad é um manipulador nomeado convencionalmente para o evento de carregamento do aplicativo acionado pela biblioteca cliente AJAX. A função pageLoad recebe dois argumentos – um é do tipo Sys.Application e o outro é um objeto ApplicationLoadEventArgs.

Figure 2 Página que desativa a interface do usuário durante postbacks (Close Figure 2)
<html>
<body>

<script type="text/javascript">
function pageLoad()
{
    var manager = Sys.WebForms.PageRequestManager.getInstance();
    manager.add_beginRequest(OnBeginRequest);
    manager.add_endRequest(OnEndRequest);
}
var lcPostbackElementID;
functionw OnBeginRequest(sender, args)
{
    lcPostbackElementID = args.get_postBackElement().id.toLowerCase();
    if (lcPostbackElementID === "button1")
    {
        $get("Button1").disabled = true;
    }
}

function OnEndRequest(sender, args)
{
    if (lcPostbackElementID === "button1")
    {
        $get("Button1").disabled = false;
    }
}
</script>

<form id="form1" runat="server">

    <asp:ScriptManager ID="ScriptManager1" runat="server" />

    <div id="pageContent">

        <b>Enter some data:</b><br />
        <asp:TextBox ID="TextBox1" runat="server" />
        <asp:Button ID="Button1" runat="server" Text="Post ..."
            onclick="Button1_Click" />
        <br /><br />

        <asp:UpdateProgress runat="server" ID="UpdateProgress1">
            <ProgressTemplate>
                <img src="images/loading.gif" />
            </ProgressTemplate>
        </asp:UpdateProgress>

        <asp:UpdatePanel ID="UpdatePanel2" runat="server"
            UpdateMode="Conditional">
            <ContentTemplate>
                <asp:Label runat="server" ID="Label1" />
            </ContentTemplate>
            <Triggers>
                <asp:AsyncPostBackTrigger ControlID="Button1" />
            </Triggers>
        </asp:UpdatePanel>
    </div>

</form>
</body>
</html>

O BeginRequest (e todos os outros eventos relacionados na Figura 1) ocorre para cada postback AJAX que surja da página. Tais eventos meramente notificam os desenvolvedores que uma solicitação está sendo realizada ou acabou de ser concluída. Esses eventos não são como, por exemplo, o evento de clique de um botão cliente ou do servidor. Você sabe que um postback está prestes a ocorrer, mas não sabe imediatamente o motivo ou que elemento de interface do usuário é responsável por ele. Mas para desativar a parte correta da interface do usuário, você precisa saber exatamente em qual elemento o clique foi feito.

A estrutura de dados que acompanha o evento BeginRequest contém uma referência ao elemento DOM que causou o postback. O membro é nomeado postBackElement e é acessado da seguinte maneira:

var lcPostbackElement = args.get_postBackElement();

Então, você pode usar essas informações para verificar o elemento no qual o usuário clicou e decidir quais elementos precisam ser desativados:

if (lcPostbackElement.id.toLowerCase() === "button1")
{
    $get("Button1").disabled = true;
    ...
}

Observe que, quando você restaura a interface do usuário original, as mesmas informações não são associadas ao evento EndRequest. Portanto, você precisa armazenar a referência, ou apenas a identificação do elemento, em cache, a fim de verificá-lo novamente no manipulador do evento EndRequest.

Para desativar e ativar um elemento DOM, você precisa recuperar uma referência DOM ao elemento e, então, atuar na propriedade booleana desativada. No ASP.NET AJAX, a função $get é equivalente à chamada document.getElementById do DOM. Mais precisamente, $get é equivalente a uma chamada parametrizada de getElementById que também aceita uma referência opcional para indicar a raiz da subárvore DOM a ser pesquisada.

É muito simples desativar temporariamente elementos da interface do usuário em uma chamada de serviço de script. A única alteração é o local a partir do qual você usa a função $get. Você faz a desativação antes da chamada e a reativação nos retornos de chamada de êxito e falha, como mostrado na Figura 3.

Figure 3 Desativando elementos da interface do usuário em uma chamada de serviço de script (Close Figure 3)
function startCall()
{
   $get("Button1").disabled = true;
   ...
   Samples.RemoteService.GetTime(param1, param2,
      onCompleted, onFailed);
}
function onCompleted(results)
{
   // Process response
   ...
   $get("Button1").disabled = false;
   ...
}
function onFailed(error)
{
   $get("Button1").disabled = false;
   ...
}

Desativando elementos da interface do usuário em recipientes de nomenclatura

O código abordado até o momento funciona bem, mas está longe de ser perfeito. E se você precisar desativar elementos da interface do usuário incorporados em uma página mestra ou controle de usuário?

Muitos controles do servidor ASP.NET usam um recipiente de nomenclatura para criar um namespace exclusivo para os valores da propriedade da identificação de controles filho. Isso é especialmente importante nos controles associados a dados iterativos e com base em modelos, como GridView e Repeater. Para cada controle, o recipiente de nomenclatura é o controle pai mais próximo que implementa a interface INamingContainer. Por exemplo, a classe UserControl atua como um recipiente de nomenclatura para todos os seus controles contidos. O mesmo ocorre para qualquer controle em uma página associada a uma página mestra.

Para qualquer controle ASP.NET sob a jurisdição de um recipiente de nomenclatura, sua identificação do cliente é alterada para que tenha um prefixo com a identificação do pai. Considere o trecho de código a seguir:

<form runat="server">
  <asp:Button runat="server" ID="Button1" />
  ...
</form>

A identificação do cliente do botão corresponde à a identificação do servidor e é sempre Button1. No entanto, esse aspecto da programação será alterado de repente se você mover o botão em um controle de usuário ou derivar sua página de conteúdo a partir de uma página mestra. Considere o controle de usuário a seguir:

<%@ Control Language="C#" CodeFile="test.ascx.cs" Inherits="test" %>
<asp:Button runat="server" ID="Button2" Text="Click" />

Depois de incorporar o controle de usuário em uma página ASP.NET, o HTML emitido refere-se ao Button2:

<input type="submit"
       name="UserControl1$Button2"
       value="Click"
       id="UserControl1_Button2" />

A identificação de cada controle contido é agora prefixado com a identificação do pai. Esse comportamento é essencialmente transparente para o desenvolvedor desde que você se limite à programação do servidor. No entanto, se você precisar executar script de Button2 do cliente, precisará ter conhecimento sobre os recipientes de nomenclatura e como eles alteram a identificação do cliente dos elementos HTML na página DOM.

Na Figura 4, você vê a mesma página como a mostrada na Figura 2, mas incorporada como uma página de conteúdo em uma página mestra. A identificação do cliente real de Button1 agora é:

Figure 4 Página de conteúdo que desativa a interface do usuário durante postbacks (Close Figure 4)
<%@ Page Language="C#" AutoEventWireup="true"
    CodeFile="DisableUIEmb.aspx.cs"
    MasterPageFile="~/Sample.master" Title="Disable UI Elements (Embedded)"
    Inherits="Samples_DisableUIEmb" Theme="ProgAjax1" %>

<asp:Content runat="server" ContentPlaceHolderID="ScriptPlaceHolder">
    <script type="text/javascript">
    function pageLoad()
    {
        var manager = Sys.WebForms.PageRequestManager.getInstance();
        manager.add_beginRequest(OnBeginRequest);
        manager.add_endRequest(OnEndRequest);
    }
    var lcPostbackElementID;
    function OnBeginRequest(sender, args)
    {
        lcPostbackElementID = args.get_postBackElement().id.toLowerCase();
        if (lcPostbackElementID === "<%= Button1.ClientID.ToLower() %>")
        {
            $get("<%= Button1.ClientID %>").disabled = true;
        }
    }

    function OnEndRequest(sender, args)
    {
        if (lcPostbackElementID === "<%= Button1.ClientID.ToLower() %>")
        {
            $get("<%= Button1.ClientID %>").disabled = false;
        }
    }
    </script>
</asp:Content>

<asp:Content runat="server" ContentPlaceHolderID="ContentPlaceHolder1">

    <div id="pageContent">

        <b>Enter some data:</b><br />
        <asp:TextBox ID="TextBox1" runat="server" />
        <asp:Button ID="Button1" runat="server" Text="Post ..."
            onclick="Button1_Click" />
        <br /><br />

        <asp:UpdateProgress runat="server" ID="UpdateProgress1">
            <ProgressTemplate>
                <img src="images/loading.gif" />
            </ProgressTemplate>
        </asp:UpdateProgress>

        <asp:UpdatePanel ID="UpdatePanel2" runat="server"
            UpdateMode="Conditional">
            <ContentTemplate>
                <asp:Label runat="server" ID="Label1" />
            </ContentTemplate>
            <Triggers>
                <asp:AsyncPostBackTrigger ControlID="Button1" />
            </Triggers>
        </asp:UpdatePanel>
    </div>

</asp:Content>

ctl00_ContentPlaceHolder1_Button1

Para desativar corretamente o Button1 (ou, em geral, executar script nele), você precisa ter conhecimento dessa nova identificação. A identificação exata é conhecida somente no momento do tempo de execução, mesmo que o nome possa ser descoberto e codificado em um arquivo JavaScript externo. A Figura 4 demonstra como você pode usar blocos de código de estilo ASP e a propriedade ClientID do servidor, a fim de emitir código de script paramétrico no navegador que contorne os recipientes de nomenclatura. A Figura 5 mostra a página de exemplo em ação.

Figura 5 Página de conteúdo usando script para ajustar a interface do usuário
Figura 5 Página de conteúdo usando script para ajustar a interface do usuário(Clique na imagem para diminuir a exibição)
Figura 5 Página de conteúdo usando script para ajustar a interface do usuário
Figura 5 Página de conteúdo usando script para ajustar a interface do usuário (Clique na imagem para aumentar a exibição)

Usando o extensor UpdatePanelAnimation

A parte da interface do usuário sendo desativada é específica de postback e, geralmente, você define isso usando o script sem formatação. Na maioria dos casos, tudo que é necessário é esmaecer os elementos; no entanto, você pode fazer até mesmo mais do que isso. Por exemplo, você poderia fazer desaparecer e, então, reaparecer as partes da interface.

Se você estiver usando a abordagem de serviços de script, a implementação desse tipo de interação avançada com o usuário é inteiramente de sua responsabilidade. No entanto, se você usar a abordagem de processamento parcial e instalar o AJAX Control Toolkit, poderá contar com um controle do extensor extremamente interessante: o extensor UpdatePanelAnimation.

Um controle do extensor é um controle de servidor ASP.NET que fornece comportamento adicional para uma variedade de controles de servidor existentes. Quando você precisar de um comportamento especial a partir de um controle, deve codificá-lo em um extensor e, em seguida, associar o extensor e o controle original na mesma página. Suponha que você queira filtrar caracteres não numéricos em uma caixa de texto. Você pode desenvolver um controle TextBox ad hoc ou apenas usar o controle TextBox antigo sem formatação ampliado com o comportamento adequado. A vantagem é que o mesmo comportamento pode ser reutilizado para vários tipos de controles e, ainda, que vários comportamentos podem ser livremente combinados para enriquecer uma instância específica de um controle de servidor.

O extensor UpdatePanelAnimation foi desenvolvido para acrescentar um pouco de animação a quaisquer regiões atualizáveis em uma página de processamento parcial do ASP.NET AJAX. Você associa declarativamente a animação que deseja que a página use enquanto o postback continua. O extensor trabalha em conjunto com a estrutura de animação com base em scripts disponível por meio do AJAX Control Toolkit. A Figura 6 mostra o código de exemplo que você pode usar para animar uma região atualizável.

Figure 6 Usando o extensor UpdatePanelAnimation (Close Figure 6)
<asp:UpdatePanel ID="UpdatePanel1" runat="server"
    UpdateMode="Conditional">
     <ContentTemplate>
         <asp:Button ID="Button1" runat="server" Text="Load" ... />
         <div id="Panel1">
            ...
         </div>
     </ContentTemplate>
     <Triggers>
         ...
     </Triggers>
</asp:UpdatePanel>

<act:UpdatePanelAnimationExtender ID="UpdatePanelAnimation1" runat="server"
    TargetControlID="UpdatePanel1">
    <Animations>
        <OnUpdating>
            <Sequence>
                <EnableAction AnimationTarget="Button1" Enabled="false" />
                <FadeOut AnimationTarget="Panel1" minimumOpacity=".3" />
            </Sequence>
        </OnUpdating>
        <OnUpdated>
            <Sequence>
                <FadeIn AnimationTarget="Panel1" minimumOpacity=".3" />
                <EnableAction AnimationTarget="Button1" Enabled="true" />
            </Sequence>
        </OnUpdated>
    </Animations>
</act:UpdatePanelAnimationExtender>

Você associa primeiro o extensor a um controle UpdatePanel específico na página e, em seguida, configura a animação. O código na Figura 6 define uma seqüência de duas ações antes que o postback ocorra (a marca OnUpdating) e outra seqüência de duas ações quando o postback for concluído (a marca OnUpdated). EnableAction, FadeOut e FadeIn são todas as animações predefinidas compatíveis com o the AJAX Control Toolkit. FadeOut e FadeIn usam estilos CSS para fazer desaparecer e na subárvore DOM a que se aplicam – neste exemplo, Panel1. A animação EnableAction simplesmente desativa ou ativa o elemento DOM de destino especificado.

Comparado com o script direto, o extensor da animação UpdatePanel é compatível com a programação declarativa e permite que você faça mais do que apenas esmaecer elementos. No entanto, o extensor está limitado às páginas que usam processamento parcial. Para obter mais informações, visite a página inicial do AJAX Control Toolkit em codeplex.com/AtlasControlToolkit.

Desativando a página inteira

Como mencionei, a parte da interface do usuário a ser desativada depende em qual controle o usuário clicou e, de forma mais geral, da ação necessária. No entanto, em um contexto de processamento parcial, somente uma solicitação por vez pode ser executada. A identificação e a desativação somente da parte da interface afetada pela operação pendente apresentariam muito trabalho e, mais importante, não protegeriam contra cliques indesejados. Abordarei isso mais a fundo em breve. Por enquanto, é suficiente dizer que desativar a página completa sem o trabalho de descobrir que controles deveriam ser esmaecidos é uma solução mais simples e mais eficaz.

A idéia é criar uma marca DIV tão abrangente quanto a área do cliente do navegador e usá-la para cobrir tudo na página. Dessa forma, qualquer clique será capturado por essa DIV e não atingirá os elementos HTML subjacentes. Você pode até mesmo associar a DIV com alguns estilos para esmaecer a página enquanto a operação estiver sendo processada. A Figura 7 mostra o código JavaScript usado para realizar essa tarefa.

Figure 7 Abrangendo a interface do usuário com uma DIV (Close Figure 7)
<script type="text/javascript">
    var _backgroundElement = document.createElement("div");

    function pageLoad()
    {
        var manager = Sys.WebForms.PageRequestManager.getInstance();
        manager.add_beginRequest(OnBeginRequest);
        manager.add_endRequest(OnEndRequest);

    // pageContent is the parent of the new DIV
        $get("pageContent").appendChild(_backgroundElement);
    }

    function OnBeginRequest(sender, args)
    {
        EnableUI(false);
    }

    function OnEndRequest(sender, args)
    {
        EnableUI(true);
    }

    function EnableUI(state)
    {
        if (!state)
        {
            _backgroundElement.style.display = '';
            _backgroundElement.style.position = 'fixed';
            _backgroundElement.style.left = '0px';
            _backgroundElement.style.top = '0px';

            var clientBounds = this._getClientBounds();
            var clientWidth = clientBounds.width;
            var clientHeight = clientBounds.height;
            _backgroundElement.style.width =
               Math.max(Math.max(document.documentElement.scrollWidth,
               document.body.scrollWidth), clientWidth)+'px';
            _backgroundElement.style.height =
               Math.max(Math.max(document.documentElement.scrollHeight,
               document.body.scrollHeight), clientHeight)+'px';
            _backgroundElement.style.zIndex = 10000;
            _backgroundElement.className = "modalBackground";
        }
        else
        {
            _backgroundElement.style.display = 'none';
        }
    }

    function _getClientBounds()
    {
        var clientWidth;
        var clientHeight;
        switch(Sys.Browser.agent) {
            case Sys.Browser.InternetExplorer:
                clientWidth = document.documentElement.clientWidth;
                clientHeight = document.documentElement.clientHeight;
                break;
            case Sys.Browser.Safari:
                clientWidth = window.innerWidth;
                clientHeight = window.innerHeight;
                break;
            case Sys.Browser.Opera:
                clientWidth = Math.min(window.innerWidth,
                              document.body.clientWidth);
                clientHeight = Math.min(window.innerHeight,
                               document.body.clientHeight);
                break;
            default:  // Sys.Browser.Firefox, etc.
                clientWidth = Math.min(window.innerWidth,
                              document.documentElement.clientWidth);
                clientHeight = Math.min(window.innerHeight,
                               document.documentElement.clientHeight);
                break;
        }
        return new Sys.UI.Bounds(0, 0, clientWidth, clientHeight);
    }
</script>

Uma variável JavaScript global é declarada para representar a marca DIV. Assim que for inicializada, a DIV será adicionada à árvore DOM na posição especificada. Nesse caso, ela é definida como o filho da marca DIV que envolve toda a interface visível. O manipulador BeginRequest configura e exibe a DIV, desativando portanto a interface visível. O manipulador EndRequest apenas oculta a DIV, restaurando portanto a interface do usuário:

function OnBeginRequest(sender, args)
{
    EnableUI(false);
}

function OnEndRequest(sender, args)
{
    EnableUI(true);
}

A marca DIV é fixada na posição 0,0 e sua largura e altura são calculadas dinamicamente de forma específica do navegador. O tamanho da DIV reflete o tamanho atual da área do cliente do navegador. A função getClientBounds mostra um exemplo de programação de script que contorna as diferenças no modelo de objeto de vários navegadores.

Como você pode ver, o código usa algumas classes incorporadas na biblioteca cliente Microsoft® AJAX. Tomei emprestado o trecho de código do código-fonte do extensor ModalPopup – um dos extensores mais interessantes no AJAX Control Toolkit. Especificamente, o extensor ModalPopup permite que uma página exiba conteúdo ao usuário com a modalidade – ou seja, impedindo que o usuário interaja com o restante da página. O efeito líquido é o mesmo que o window.alert, mas a interface – a hierarquia dos controles sendo exibidos – não depende totalmente de você.

A Figura 8 mostra uma página que usa uma DIV modal para desativar toda a interface do usuário durante uma operação de processamento parcial.

Figura 8 Desativando a página inteira durante o processamento parcial
Figura 8 Desativando a página inteira durante o processamento parcial(Clique na imagem para diminuir a exibição)
Figura 8 Desativando a página inteira durante o processamento parcial
Figura 8 Desativando a página inteira durante o processamento parcial (Clique na imagem para aumentar a exibição)

Um postback AJAX por vez

Como o processamento parcial permite que você acrescente funcionalidade AJAX às páginas ASP.NET 2.0 clássicas sem modificar o modelo de programação, o ciclo de vida do servidor da página é totalmente preservado. Isso se aplica aos eventos de postback, carregamento, processamento e estado de exibição. A quantidade de tráfego de uma chamada de processamento parcial característica é menor do que aquela para uma chamada ASP.NET clássica, mas o tamanho do estado de exibição é uma constante e pode ser reduzido somente por meio de técnicas de programação específicas de página.

Com o processamento parcial, uma página ASP.NET tem a aparência contínua de uma página AJAX, mas mantém a interface de usuário tradicional. Visto do lado do cliente, um processamento parcial se comporta como se fosse parte de uma interface de usuário única e seqüencial. Somente uma operação pode estar pendente por vez, mesmo quando duas operações simultâneas são possíveis a partir de um ponto de vista dos negócios.

Imagine se você tiver uma página que exiba qualquer tipo de informações sobre um cliente. Essa página tem dois botões – um para download de informações de pedidos e outro para exibir detalhes do cliente. A partir de uma perspectiva puramente de negócios, não há nada que impeça o usuário de clicar nos dois botões na seqüência para gerar duas solicitações sucessivas, com a segunda solicitação iniciando antes de a primeira ter sido concluída. As duas solicitações precisam da mesma entrada – a identificação do cliente – e ambas são somente leitura, significando que não há risco de interferir no status do banco de dados de back-end. Entretanto, essas duas chamadas não podem ser executadas simultaneamente em uma página de processamento parcial com base no modelo de programação clássico ASP.NET – cada solicitação retorna marcação que inclui um estado de exibição atualizado para toda a página. Se você permitir que duas solicitações sejam executadas simultaneamente, a consistência do estado de exibição da página estará em risco. Duas solicitações enviadas em seqüência rápida enviam o mesmo estado de exibição ao servidor, mas cada solicitação retorna um estado de exibição diferente, atualizado para os controles envolvidos com o processamento da solicitação. Isso significa que a última solicitação a ser retornada substitui as alterações feitas quando a primeira solicitação for retornada. Portanto, as páginas AJAX do processamento parcial ainda funcionam de acordo com o padrão stop-and-go.

Para evitar esses tipos de problemas, o objeto do gerenciador de solicitação de página elimina automaticamente qualquer solicitação que esteja pendente quando for feita uma nova solicitação. Sabendo desse comportamento, é extremamente importante que você desative toda a interface do usuário em páginas de processamento parcial. Isso tem o objetivo de garantir que os usuários não podem interagir com a página inteira em relação à duração da solicitação atual. Como você pode ver, essa instrução contradiz diretamente um dos benefícios do AJAX – a interação assíncrona.

Chamadas de alta prioridade

Ao examinar o processamento parcial, sugerem determinadas dúvidas, como se você não pode executar duas solicitações simultaneamente devido à necessidade de preservar a consistência do estado de exibição, então por que a equipe não implementou um algoritmo para mesclar os dois estados de exibição parcial no cliente? Isso parece uma questão razoável. No entanto, não é uma opção devido, em parte, à estrutura interna do estado de exibição, que inclui código codificado, valor de hash e um valor de segurança somente de servidor.

É tecnicamente possível combinar dois estados de exibição no cliente, mas isso exigiria alterações significativas na estrutura atual do estado de exibição e no tempo de execução do ASP.NET. Além disso, surgiriam problemas de segurança ao disponibilizar o código de script a praticamente todos, permitindo que preparassem o conteúdo do estado de exibição de ad hoc para reprodução e ataques de script nos sites.

Outro problema comum tem a ver com a diretiva last-win (o último vence) que o gerenciador de solicitação de página implementa quando ele eliminar a solicitação pendente sempre que uma nova solicitação é recebida. Descobri que os desenvolvedores tendem a se sentir constrangidos quando outra pessoa toma uma decisão sobre um ponto de programação. Então, parece que muitos desenvolvedores – inclusive eu mesmo – não gostam da política last-win imposta para postbacks AJAX. Que tal uma política first-win (o primeiro vence) em que basicamente a solicitação atual ganha prioridade e as solicitações sucessivas são canceladas ou colocadas em fila até que a primeira solicitação seja concluída?

A estrutura do ASP.NET AJAX fornece as ferramentas necessárias para implementar esse padrão, como você verá a seguir. Mas você tem certeza de que realmente precisa dessa opção? O fato é que se você usar o processamento parcial, deverá estar pronto para fornecer suas páginas com uma solicitação por vez. A forma mais segura e fácil de realizar isso é desativar toda a interface do usuário para impedir os usuários de clicarem.

Se, por algum motivo, você achar que não pode desativar toda a interface do usuário, então saber como implementar uma abordagem first-win pode fornecer a flexibilidade necessária. A idéia é escrever algum código de script que intercepte o início de cada solicitação de processamento parcial – o evento initializeRequest do gerenciador de solicitação de página. No manipulador, você verifica as condições do tempo de execução para determinar se realmente deseja cancelar a solicitação e então instruir corretamente o gerenciador.

function OnInitializeRequest(sender, args)
{
    var manager = sender;
    if (manager.get_isInAsyncPostBack() &&
        args.get_postBackElement().id === "Button1")
    {
        args.set_cancel(true);
    }
}

O código de exemplo verifica se outra solicitação está sendo realizada e se é uma solicitação com uma prioridade inferior. Em caso positivo, você pode cancelar a solicitação definindo a propriedade de cancelamento como true na estrutura de dados de evento. De acordo com o código anterior, a solicitação mais recente não existe mais. No entanto, você pode gravar código para implementar uma fila interna para solicitações pendentes. Para obter mais informações sobre chamadas de alta prioridade, consulte ajax.asp.net/docs/tutorials/ExclusiveAsyncPostback.aspx.

Envie suas dúvidas e seus comentários para Dino em cutting@microsoft.com.

Dino Esposito é um dos mentores da Solid Quality Learning e autor de Introducing ASP.NET AJAX (Microsoft Press, 2007). Residente na Itália, Dino é um palestrante sempre presente em eventos do setor no mundo inteiro. Entre em contato com Dino pelo e-mail cutting@microsoft.com ou assine seu blog weblogs.asp.net/despos.


Team System: Diretivas de check-in personalizadas

Team System
Diretivas de check-in personalizadas
Brian A. Randell

Download do código disponível em: TeamSystem2007_11.exe (166KB)
Conteúdo
Observações e diretivas de check-in
Criando sua própria diretiva
Depurando assemblies de diretiva
Aprimorando a diretiva personalizada
Adicionando suporte à edição

N
as últimas três edições desta coluna, explorei a versão de controle do Team Foundation Server (TFS) e o item de acompanhamento de APIs. Usei as APIs para construir um suplemento do Microsoft® Word 2003 que oferece suporte para check-ins e associações de itens de trabalho dos documentos do Word semelhantes ao disponível por meio do Team Explorer no Visual Studio® 2005. Nesta coluna, falarei sobre observações e diretivas de check-in. Você aprenderá como elas funcionam e como escrever suas próprias implementações de diretiva personalizada. Em uma próxima coluna, adicionarei este suporte ao suplemento do Word.

Observações e diretivas de check-in

As observações de check-in são campos de texto de forma livre que permitem adicionar dados de seqüências de caracteres categorizados a um check-in. As observações de check-in são definidas no nível do projeto de equipe e podem ser obrigatórias como parte de uma operação de check-in. Os modelos de projeto padrão fornecidos pela Microsoft definem três observações de check-in: Revisor de código, Revisor de segurança e Revisor de desempenho. Nenhuma delas é obrigatória. Você tem liberdade para definir suas próprias observações de check-in usando o Team Explorer e/ou APIs depois de criar um projeto de equipe ou modificando o arquivo VersionControl.xml em seu modelo de processo antes de criar um projeto de equipe. Você define uma observação de check-in definindo um rótulo de até 64 caracteres, especificando se a observação será ou não exigida no check-in e especificando a posição de exibição da observação no formulário de check-in. No momento do check-in, o valor de uma observação poderá ser de até 2048 + (230 – 1) caracteres. Os primeiros 2048 caracteres serão armazenados em uma coluna nvarchar no banco de dados £ o excedente será armazenado em uma coluna ntext.

As diretivas de check-in impõem regras definidas pelo administrador do projeto de equipe. Uma diretiva pode variar em complexidade de algo simples, como assegurar que você insira um comentário no check-in, a algo mais complicado, como desempenhar análises de código estático e executar testes. Na versão 2005 do TFS, três diretivas prontas foram incluídas. Posteriormente, como parte do Microsoft Visual Studio 2005 Team Foundation Server Power Tool, a Microsoft adicionou mais quatro diretivas de check-in. A Figura 1 oferece os detalhes sobre todas as sete. (E sim, é "Tool", e não "Tools". Consulte msdn2.microsoft.com/aa718351.)

Figure 1 Diretivas fornecidas pela Microsoft (Close Figure 1)

Diretiva | Versão | Requisitos da diretiva
Itens de trabalho | RTM | Associe ao menos um item de trabalho no check-in. Não há opções de configuração.
Análise de código | RTM | Execute análise de código estático (que implica uma compilação limpa) antes do check-in. Você controla se a diretiva se aplica ao código gerenciado e/ou código C++.
Teste | RTM | Executar os testes especificados pela lista de teste de diretiva antes do check-in. Você controla a lista de testes executados pela diretiva.
Associação de comentário | Power Tool | Insira um comentário no check-in. Não há opções de configuração.
Padrões proibidos | Power Tool | Permite que você defina um conjunto de arquivos que você não deseja que sejam devolvidos ao repositório. Você configura a diretiva especificando uma extensão de arquivo simples ou usa uma expressão regular.
Consulta de item de trabalho | Power Tool | Associa um item de trabalho de uma consulta especificada com o seu check-in. Você configura a diretiva selecionando uma consulta de equipe a partir de seu projeto de equipe.
Caminho personalizado | Power Tool | Trabalha em conjunto com outra diretiva. Permite que você aplique seletivamente a diretiva "buddy" para um subconjunto de código da sua árvore de controles de código de origem do projeto de equipe.

Você implementará uma diretiva criando uma biblioteca de classes gerenciada que apresenta uma classe pública serializável a qual implementa duas interfaces: IPolicyDefinition e IPolicyEvaluation. Você instalará o assembly da diretiva primeiramente copiando-a para uma estação de trabalho com os bits do Team Explorer instalados. Depois, você registrará o assembly adicionando uma entrada no registro do Windows® em HKEY_LOCAL_ MACHINE\SOFTWARE\Microsoft\VisualStudio\8.0\TeamFoundation\SourceControl\Checkin Policies. Você criará um valor de seqüência de caracteres onde o Name (Nome) será o nome do arquivo do assembly de diretiva, sem a extensão .dll e Data (Dados) será o caminho totalmente qualificado para o assembly na estação de trabalho local (consulte a Figura 2).

Figura 2 Chaves de registro
Figura 2 Chaves de registro(Clique na imagem para diminuir a exibição)
Figura 2 Chaves de registro
Figura 2 Chaves de registro (Clique na imagem para aumentar a exibição)

Depois que a diretiva estiver instalada, você a habilitará para cada projeto de equipe, geralmente usando a caixa de diálogo Source Control Settings (Configurações de controle de origem) do Team Explorer. Você acessa esta caixa de diálogo selecionando primeiramente um projeto de equipe válido na janela da ferramenta Team Explorer e navegando até Team | Team Project Settings | Source Control (Equipe | Configurações do projeto de equipe | Controle de origem) e escolhendo a guia Check-in Policy (Diretiva de check-in) (consulte a Figura 3).

Figura 3 Guia Check-In Policy (Diretiva de check-in)
Figura 3 Guia Check-In Policy (Diretiva de check-in)(Clique na imagem para diminuir a exibição)
Figura 3 Guia Check-In Policy (Diretiva de check-in)
Figura 3 Guia Check-In Policy (Diretiva de check-in) (Clique na imagem para aumentar a exibição)

A primeira vez que você escolher o botão Add (Adicionar) na caixa de diálogo Source Control Settings (Configurações de controle de origem), o Team Explorer enumerará a lista de assemblies registrados na estação de trabalho local. Depois de fazê-lo, ele na realidade enumerará a lista cada vez que você abrir a caixa de diálogo Source Control Settings (Configurações de controle de origem). Se você clicar no botão Add (Adicionar), o Team Explorer exibirá a lista de assemblies de diretiva instalados (consulte aFigura 4), além de uma descrição da diretiva selecionada.

Figura 4 Caixa de diálogo Add Check-In Policy (Adicionar diretiva de check-in)
Figura 4 Caixa de diálogo Add Check-In Policy (Adicionar diretiva de check-in)

Depois de selecionar uma diretiva de check-in na caixa de diálogo Add Check-in Policy (Adicionar diretiva de check-in), o Team Explorer consultará a propriedade CanEdit da implementação IPolicyDefinition da diretiva. Se CanEdit retornar verdadeiro, o Team Explorer chamará o método Edit da diretiva, permitindo que a diretiva exiba uma caixa de diálogo de configurações adicionais. Depois de ter configurado a diretiva e fechado a caixa de diálogo Source Control Settings (Configurações de controle de origem), o Team Explorer chamará o método SetCheckinPolicies em uma instância do objeto TeamProject, o qual serializa um bit de metadados sobre o assembly da diretiva, além de uma instância da classe de diretiva para o banco de dados do Team Foundation Server.

Observe que o TFS não sabe nada sobre o assembly da diretiva até que você o habilite para um projeto de equipe específico. Depois habilitar uma diretiva, no entanto, será necessário instalar o assembly da diretiva na estação de trabalho de cada desenvolvedor que executará um check-in. O TFS não fornece, atualmente, qualquer mecanismo de implementação automática.

Criando sua própria diretiva

Para criar sua própria diretiva, é necessário ter o Microsoft .NET Framework 2.0 e o Team Explorer instalados. Embora você possa criar um assembly sem instalar o Visual Studio 2005, presumirei que você está usando ao menos o Visual Studio 2005 Professional edition. Primeiramente, você criará um novo projeto de Biblioteca de Classe no Visual Basic®. Neste exemplo, você criará um assembly de diretiva que verificará arquivos .vb ou .cs para encontrar palavras "estranhas". Chamei o assembly de MSDNMag TeamSystem.CheckinPolicies. Depois de criar o projeto, renomeie Class1.vb para BadWords.vb e marque a classe Serializable (Serializável).

Será necessário adicionar uma referência ao assembly Microsoft.TeamFoundation.VersionControl.Client.dll. Este assembly é instalado como parte do Visual Studio 2005 SDK e contém as definições para IPolicyDefinition e IPolicyEvaluation. Além disso, você encontrará uma classe base abstrata, PolicyBase, que poderá ser usada para iniciar um bit mais rapidamente. Neste exemplo, no entanto, você implementará explicitamente as interfaces uma de cada vez em sua própria classe.

No arquivo de classe BadWords.vb, adicione uma instrução de importação para Microsoft.TeamFoundation.VersionControl.Client. Em seguida implemente a interface IPolicyDefinition. O Visual Studio 2005 criará um stub dos seis membros desta interface. A Figura 5 fornece a implementação desta interface.

Figure 5 IPolicyDefinition para a diretiva BadWords (Close Figure 5)
Imports Microsoft.TeamFoundation.VersionControl.Client

<Serializable()> _
Public Class BadWords
  Implements IPolicyDefinition

  Public ReadOnly Property CanEdit() As Boolean _
    Implements IPolicyDefinition.CanEdit
    Get
      Return False
    End Get
  End Property

  Public ReadOnly Property Description() As String _
    Implements IPolicyDefinition.Description
    Get
      Return "Checks code for bad words."
    End Get
  End Property

  Public Function Edit(ByVal policyEditArgs As IPolicyEditArgs) _
    As Boolean Implements IPolicyDefinition.Edit

    Throw New NotImplementedException("Edit is not implemented")
  End Function

  Public ReadOnly Property InstallationInstructions() As String _
    Implements IPolicyDefinition.InstallationInstructions
    Get
      Using sw As New IO.StringWriter()
        With sw
          .Write("You need to install ")
          .Write("BrianRandell.MSDNMag.CheckinPolicies.dll ")
          .Write("on your local workstation." & .NewLine())
          .WriteLine("Visit the Team Project for more info.")
        End With

        Return sw.ToString()
      End Using
    End Get
  End Property

  Public ReadOnly Property Type() As String _
      Implements IPolicyDefinition.Type
    Get
      Return "Brian Randell-MSDNMag Check for Bad Words""
    End Get
  End Property

  Public ReadOnly Property TypeDescription() As String _
    Implements IPolicyDefinition.TypeDescription
    Get
      Return "This policy checks .vb and .cs files for bad words."
    End Get
  End Property
End Class

A maioria do código na Figura 5 é auto-explicativa. A interface fornece valores de seqüência de caracteres que descrevem a diretiva e fornece os ganchos necessários, CanEdit e Edit, se você escolher uma diretiva que ofereça suporte à configuração adicional. Observe que a propriedade Type representa a seqüência de caracteres exibida apresentada na caixa de diálogo Add Check-in Policy (Adicionar diretiva de check-in). Para começar, será necessário embutir a lista de palavras estranhas. Posteriormente, você poderá implementar os métodos CanEdit e Edit.

Figure 6 IPolicyEvaluate para a diretiva BadWords (Close Figure 6)
Imports Microsoft.TeamFoundation.VersionControl.Client
Imports System.IO

<Serializable()> _
Public Class BadWords
  Implements IPolicyDefinition, IPolicyEvaluation

' IPolicyDefinition elided (see Figure 5)

  Private badWords() As String = {"Dag", "Darn", "Doh"}
  <NonSerialized()> _
  Private m_pendingCheckin As IPendingCheckin = Nothing

  Public Sub Activate(ByVal failure As PolicyFailure) _
    Implements IPolicyEvaluation.Activate
    ' Do Nothing for Now
  End Sub

  Public Sub DisplayHelp(ByVal failure As PolicyFailure) _
    Implements IPolicyEvaluation.DisplayHelp
    ' Do Nothing for Now
  End Sub

  Public Function Evaluate() As PolicyFailure() _
    Implements IPolicyEvaluation.Evaluate

    Dim fileContents As String = Nothing
    Dim failureMessage As String = Nothing
    Dim failures As New List(Of PolicyFailure)

    For Each pc As PendingChange In _
      m_pendingCheckin.PendingChanges.CheckedPendingChanges

      ' Only check .vb and .cs files
      If pc.FileName.EndsWith(".vb") OrElse _
        pc.FileName.EndsWith(".cs") Then

        Using sr As New StreamReader(pc.FileName)
          fileContents = sr.ReadToEnd()
        End Using
        For Each s As String In badWords
          If fileContents.Contains(s) Then
            failureMessage = pc.FileName & " has a bad word."
            Dim pf As New PolicyFailure(failureMessage, Me)
            failures.Add(pf)
            Exit For
          End If
        Next
      End If
    Next

    Return failures.ToArray()
  End Function

  Public Sub Initialize(ByVal pendingCheckin As IPendingCheckin) _
    Implements IPolicyEvaluation.Initialize

    If (Not Me.m_pendingCheckin Is Nothing) Then
      Throw New InvalidOperationException("Already Active")
    End If
    If disposedValue Then
      Throw New ObjectDisposedException(Nothing)
    End If

    Me.m_pendingCheckin = pendingCheckin
  End Sub

  Public Event PolicyStateChanged(ByVal sender As Object, _
    ByVal e As PolicyStateChangedEventArgs) _
    Implements IPolicyEvaluation.PolicyStateChanged

  ' IDisposable implementation elided

End Class

Depois de implementar IPolicyDefinition, será necessário implementar IPolicyEvaluation. Esta interface define quatro métodos (Activate, DisplayHelp, Evaluate e Initialize) e um único evento (PolicyStateChanged). A estrutura de avaliação da diretiva chamará Initialize depois da criação da instância de classe da diretiva. Este método aceita uma referência IPendingCheckin, a qual fornece o assembly da diretiva com acesso aos dados, tais como alterações pendentes e itens de trabalho, relacionados ao check-in pendente. A avaliação é a essência do assembly da diretiva. A estrutura de check-in executará este método quando o usuário clicar no botão Policies (Diretivas) ou tentar uma operação de check-in ou de prateleira. Se Evaluate retornar uma lista não-vazia de instâncias PolicyFailure, a estrutura cancelará o check-in e apresentará a lista de erros. Se você clicar duas vezes em uma mensagem de erro, a estrutura chamará Activate, permitindo que a classe da diretiva exiba detalhes sobre o erro. Finalmente, se você pressionar F1 com um erro selecionado, a estrutura executará o método DisplayHelp, que poderá exibir um arquivo de ajuda, navegar para uma página da Web ou apresentar uma interface do usuário personalizada. A classe da diretiva usa o evento PolicyStateChanged para notificar à estrutura que ocorreram alterações e que ela deve chamar Evaluate novamente. A Figura 6 fornece a implementação inicial de IPolicyEvaluation.

A implementação inicial é bem simples £ a instância da diretiva armazena uma referência para a referência IPendingCheckin fornecida pelo método Initialize. No método Evaluate, o código enumera a coleção CheckedPendingChanges, recuperando uma instância PendingChange para cada item verificado. Se o item for um arquivo que termina com extensão .vb ou .cs, o código carregará o conteúdo do arquivo em uma variável de seqüência de caracteres usando um StreamReader. Em seguida o código verificará o conteúdo do arquivo usando o método String.Contains para cada palavra estranha, uma por vez. Se o código encontrar uma palavra estranha, ele criará uma nova instância PolicyFailure, a adicionará à coleção de falhas List(Of T) e continuará até a próxima alteração pendente. Depois de verificar todos os arquivos, ele retornará as coleções de falha para a estrutura de diretiva que chamou Evaluate.

Depois de adicionar o código necessário para oferecer suporte a IPolicyEvaluation, será necessário compilar o assembly. Você terá que adicionar os dados necessários ao registro para que o Visual Studio carregue o assembly quando você clicar no botão Add (Adicionar) na caixa de diálogo Source Code Control Settings (Configurações de controle do código-fonte). Sempre que você modificar a lista de diretivas instaladas, será necessário reinicializar as instâncias em execução do Visual Studio, para que as alterações sejam vistas. Depois de configurar tudo, ative a diretiva e modifique um arquivo sob controle da origem, inserindo uma palavra estranha. Em seguida abra a caixa de diálogo Pending Changes (Alterações pendentes) e clique no botão Policy Warnings (Avisos de diretiva) ou tente um check-in. Deve aparecer algo similar à Figura 7.

Figura 7 Aviso de violação de diretiva
Figura 7 Aviso de violação de diretiva(Clique na imagem para diminuir a exibição)
Figura 7 Aviso de violação de diretiva
Figura 7 Aviso de violação de diretiva (Clique na imagem para aumentar a exibição)

Depurando assemblies de diretiva

Depurar a diretiva personalizada é realmente muito fácil. Primeiramente, se você ativou a diretiva personalizada, remova-a da lista do projeto de equipe. Você tornará sua vida muito mais fácil se trabalhar apenas com o assembly de diretiva em um projeto de equipe particular. Naturalmente, em algum momento você desejará testar seu assembly de diretiva com quaisquer outras diretivas escolhidas para habilitar, mas reduzir a área de superfície do problema quando você estiver depurando faz sentido. Depois de remover todas as diretivas, feche todas as instâncias em execução do Visual Studio 2005 e deixe os dados adicionados anteriormente no registro. Depois, reinicialize uma instância do Visual Studio 2005. Carregue seu projeto de diretiva personalizada e acesse o Project Designer (clique com o botão direito do mouse em seu projeto no Solution Explorer e selecione Properties [Propriedades]). Ative a guia Debug (Depurar) e altere a Start Action (Ação inicial) para Start External Program (Iniciar programa externo). Será necessário fornecer o caminho totalmente qualificado para o Visual Studio 2005, geralmente C:\Arquivos de Programa\Microsoft Visual Studio 8\Common7\IDE\devenv.exe. Salve as alterações e feche o designer.

Depois de fazer isso, configure pontos de interrupção em IPolicyDefinition.Type e IPolicyEvaluate.Evaluate. Comece a depuração pressionando F5 ou escolhendo Debug | Start Debugging (Depurar | Iniciar depuração) no menu. Uma segunda instância do Visual Studio será iniciada e, nesta instância, abra o projeto que você está usando para testar as palavras estranhas. Acesse a caixa de diálogo Source Control Settings (Configurações do controle de origem) e adicione sua diretiva personalizada. Quando você clicar no botão Add (Adicionar), a primeira instância do Visual Studio se tornará a janela ativa enquanto o Visual Studio acessa a propriedade Type da diretiva. Quando a caixa de diálogo de configurações for fechada, a estrutura da diretiva executará o método Evaluate de sua diretiva, o que poderá, por sua vez, reativar a primeira instância do Visual Studio e permitir o início da depuração. No final da sessão de depuração, volte para a caixa de diálogo Source Control Settings (Configurações do controle de origem) e remova sua diretiva. Você terá a chance de depurar a implementação IDisposable aqui.

Quando tiver terminado, feche a segunda instância do Visual Studio. Neste ponto, será possível modificar o assembly da diretiva e repetir o processo. Você só deverá ativar a diretiva na segunda instância do Visual Studio. Caso contrário, será necessário descarregar todas as instâncias para recompilar.

Aprimorando a diretiva personalizada

Embora a implementação atual da diretiva personalizada funcione, ela não tem muitos recursos sofisticados. Ela não fornece detalhes sobre onde estão localizadas as palavras estranhas. Além disso, você embutiu a lista de palavras estranhas. Para corrigir isso, você primeiramente precisará fornecer uma implementação mais elaborada para IPolicyEvaluate.Evaluate. Você poderá fornecer uma implementação real para o método Activate. Para finalizar, você precisará fornecer uma implementação para IPolicyDefinition.Edit. A Figura 8 fornece o novo código completo para Evaluate.

Figure 8 Implementação atualizada de Evaluate (Close Figure 8)
Imports Microsoft.TeamFoundation.VersionControl.Client
Imports System.IO

<Serializable()> _
Public Class BadWords
  Implements IPolicyDefinition, IPolicyEvaluation

  Private badWords() As String = {"Dag", "Darn", "Doh"}
  <NonSerialized()> _
  Private m_pendingCheckin As IPendingCheckin = Nothing
  <NonSerialized()> _
  Private badWordFound As Boolean = False

  ' IPolicyDefinition elided
  ' IPolicyEvaluation Activate and DisplayHelp elided

  Public Function Evaluate() As PolicyFailure() _
    Implements IPolicyEvaluation.Evaluate

    Dim fileContents As String = Nothing
    Dim failureMessage As String = Nothing
    Dim wordData As List(Of WordInfo)
    Dim failures As New List(Of PolicyFailure)

    For Each pc As PendingChange In _
      m_pendingCheckin.PendingChanges.CheckedPendingChanges

      ' Only check .vb and .cs files
      If pc.FileName.EndsWith(".vb") OrElse _
        pc.FileName.EndsWith(".cs") Then

        badWordFound = False
        wordData = GetWordInfo(pc.LocalItem)
        If badWordFound Then
          failureMessage = _
            String.Format("{0} has at least one bad word." & _
            " Double-click for details", pc.FileName)

          Dim bpf As New BadWordPolicyFailure(failureMessage, _
            Me, New CheckedFileInfo(pc.FileName, wordData, pc))

          failures.Add(bpf)
        End If
      End If
    Next

    Return failures.ToArray()
  End Function

  ' IPolicyEvaluation Initialize elided
  ' IDisposable implenation elided

  Friend Function GetWordInfo(ByVal fileName As String) _
    As List(Of WordInfo)

    Dim lines As String() = File.ReadAllLines(fileName)
    Dim wordData As New List(Of WordInfo)

    For Each word As String In badWords
      Dim item As WordInfo = New WordInfo(word)
      wordData.Add(item)

      For i As Integer = 0 To lines.Length - 1
        If lines(i).Contains(word) Then
          item.LineList.Add(i + 1)
          item.LineData.Add(lines(i))
        End If
      Next i
      If item.LineList.Count > 0 Then
        badWordFound = True
      End If
    Next

    Return wordData
  End Function
End Class

A nova implementação de Evaluate usa um conjunto de novas classes auxiliares, mostrado na Figura 9. O WordInfo armazena o número de linhas e a linha real de código que contém a palavra estranha. O código cria uma instância do WordInfo para cada palavra estranha. CheckedFileInfo contém o nome do arquivo verificado, uma coleção de objetos do WordInfo e uma referência para o item PendingChange. Finalmente, BadWordPolicyFailure, que deriva de PolicyFailure, armazena uma referência a uma instância de CheckedFileInfo. Esta classe existe porque a classe PolicyFailure padrão não fornece uma maneira fácil de associar uma falha de diretiva ao suporte de metadados necessário ao método Activate para fornecer uma experiência de usuário de alto nível.

Figure 10 Ativar implementação do método (Close Figure 10)
Public Sub Activate(ByVal failure As PolicyFailure) _
  Implements IPolicyEvaluation.Activate

  Dim bpf As BadWordPolicyFailure = _
    TryCast(failure, BadWordPolicyFailure)

  If bpf IsNot Nothing Then
    Using frm As New frmFileDetails(bpf)
      frm.ShowDialog()
    End Using
  Else
    MsgBox("The Policy Failure object provided is invalid.", _
      MsgBoxStyle.Exclamation, "Warning")
  End If
End Sub

Com estas novas classes, o novo método Evaluate funciona de modo ligeiramente diferente. O código ainda enumera a coleção CheckedPendingChanges, procurando apenas arquivos .vb e .cs. Entretanto, o método auxiliar GetWordInfo faz o trabalho de realmente carregar o arquivo de origem e procurar as palavras estranhas. Este método cria uma coleção que contém um WordInfo para cada palavra estranha nessa lista. Se GetWordInfo encontrar ao menos uma palavra estranha, o método Evaluate criará uma instância de CheckedFileInfo e a passará para uma nova instância de BadWordPolicyFailure, que será adicionada à coleção de falhas. E continuará a verificar cada arquivo.

Em seguida, será necessário fornecer uma implementação para o método Activate. Quando você clica duas vezes em uma mensagem de violação de diretiva, você deseja que a diretiva exiba uma caixa de diálogo listando as palavras estranhas, em quais linhas elas ocorrem e a linha real de código com a palavra estranha. O download do código de exemplo fornecerá um formulário simples, frmFileDetails, para você começar.

Na Figura 10 você poderá ver o novo código para o método Activate. A primeira coisa feita pelo código é TryCast a referência PolicyFailure fornecida para o tipo BadWordPolicyFailure derivado. Se isso tiver êxito, o código exibirá frmFileDetails como uma caixa de diálogo (consulte a Figura 11 para obter um exemplo).

Figura 11 Mensagem de falha de diretiva
Figura 11 Mensagem de falha de diretiva(Clique na imagem para diminuir a exibição)
Figura 11 Mensagem de falha de diretiva
Figura 11 Mensagem de falha de diretiva (Clique na imagem para aumentar a exibição)

Adicionando suporte à edição

O último recurso que você precisa adicionar é uma caixa de diálogo que permita ao Administrador do projeto definir a lista de palavras estranhas. Há três tarefas para você concluir aqui. Primeiramente, será necessário fornecer um mecanismo de armazenamento para a lista de palavras estranhas. Em seguida, será necessário fornecer uma interface de usuário para editar a lista de palavras estranhas. Finalmente, será necessário modificar a diretiva para carregar as palavras estranhas do local de armazenamento em vez de usar a matriz embutida. Nenhuma destas tarefas é específica para assemblies de diretiva £ elas são apenas códigos padrão. Forneci uma implementação no exemplo de código. O exemplo armazena a lista de palavras estranhas em um arquivo de texto localizado no mesmo diretório do suplemento. No momento da edição, o Administrador do projeto deverá ter acesso de leitura/gravação ao arquivo de palavras estranhas, embora os consumidores da diretiva só precisarão de acesso de leitura.

Neste momento, você deverá estar à vontade com as observações e diretivas de check-in. A estrutura que a Microsoft forneceu para as diretivas de check-in é elaborada e flexível. No futuro, espero que a Microsoft aperfeiçoe a experiência de instalação. Na próxima edição desta coluna, falarei sobre como trabalhar de forma programática com as observações e diretivas de check-in do código personalizado e o suplemento do Word.

Envie suas perguntas e comentários para mmvsts@microsoft.com.

Brian A. Randell é consultor sênior da MCW Technologies, LLC. Ele ocupa seu tempo falando, ensinando e escrevendo sobre tecnologias da Microsoft, é o autor do curso Applied Team System da Pluralsight e é um MVP da Microsoft. Entre em contato com Brian pelo seu blog em mcwtech.com/cs/blogs/brianr.


Fundamentos: Contextos de sincroniza&#231;&#227;o no WCF

Fundamentos
Contextos de sincronização no WCF
Juval Lowy

Download do código disponível em: Foundations2007_11.exe (241KB)
Conteúdo
O que são os contextos de sincronização .NET?
WCF e contextos de sincronização
Contexto personalizado de sincronização de serviço
Contexto personalizado de sincronização de afinidade de thread
Processamento de prioridade
Retornos de chamada e contexto de sincronização
Por que usar contextos de sincronização?

U
m dos recursos mais úteis do Windows® Communication Foundation (WCF) é sua dependência do contexto de sincronização do Microsoft® .NET Framework para controlar as chamadas à instância do servidor (ou a um objeto de retorno de chamada). Esse mecanismo fornece desenvolvimento orientado à produtividade e extensibilidade avançada. Nesta coluna, descrevo brevemente o que são os contextos de sincronização e como o WCF os usa. Em seguida, demonstro várias opções para estender o WCF para usar contextos personalizados de sincronização, de forma programática e declarativa. Além de conhecer os méritos dos contextos personalizados de sincronização, você também pode ver certa programação avançada .NET bem como técnicas de extensibilidade do WCF.

O que são os contextos de sincronização .NET?

O .NET Framework 2.0 introduziu um recurso pouco conhecido chamado de contexto de sincronização, definido pela classe SynchronizationContext no namespace System.Threading:

public delegate void SendOrPostCallback(object state);

public class SynchronizationContext
{
   public virtual void Post(SendOrPostCallback callback,object state);
   public virtual void Send(SendOrPostCallback callback,object state);
   public static void SetSynchronizationContext
(SynchronizationContext context);
   public static SynchronizationContext Current
   {get;}
   //More members
}

O contexto de sincronização é armazenado no TLS (armazenamento local de segmento). Cada thread criado com o .NET Framework 2.0 pode ter um contexto de sincronização (semelhante a uma transação de ambiente) obtida por meio da propriedade Current estática de SynchronizationContext. Current pode retornar null se o thread atual não tiver contexto de sincronização. O contexto de sincronização é usado para devolver uma chamada de método entre um thread de chamada e um ou mais threads de destino, no caso de o método não conseguir executar no thread de chamada original. O calling de chamada engloba o método que deseja que controle o outro thread (ou threads) com um delegado do tipo SendOrPostCallback, e fornece-o para os métodos Send ou Post, para execução síncrona ou assíncrona, respectivamente. Você pode associar um contexto de sincronização ao seu thread atual ao chamar o método estático SetSynchronizationContext.

De longe, o uso mais comum de um contexto de sincronização é com atualizações da interface do usuário. Com todas as tecnologias multithread do Windows, de MFC a Windows Forms a WPF, somente o thread que criou uma janela tem permissão de atualizá-la pelo processamento de suas mensagens. Essa restrição tem a ver com o uso subjacente do loop de mensagens do Windows e a arquitetura de mensagens de thread. A arquitetura de mensagens cria um problema ao desenvolver um aplicativo multithread da interface do usuário – você gostaria de evitar o bloqueio da interface do usuário ao executar operações demoradas ou receber retornos de chamada. Isso, é claro, requer o uso de threads de trabalho, ainda que esses threads não possam atualizar a interface do usuário diretamente, pois não são o thread da interface do usuário.

Para solucionar esse problema no .NET Framework 2.0, o construtor de qualquer controle com base no Windows Forms ou formulário verifica para ver se o thread em execução tem um contexto de sincronização e, caso não tenha, o construtor anexa um novo contexto de sincronização (chamado de WindowsFormsSynchronizationContext). Esse contexto dedicado de sincronização converte todas as chamadas em seus métodos Post ou Send nas mensagens do Windows e os publica na fila de mensagens do thread da interface do usuário a ser processada no thread correto. O contexto de sincronização do Windows Forms é a tecnologia subjacente através do controle auxiliar comumente utilizado BackgroundWorker.

WCF e contextos de sincronização

Por padrão, todas as chamadas de serviço (e retornos de chamada) do WCF são executadas a partir do pool threads de conclusão E/S. Por padrão, esse pool tem 1.000 threads, nenhum deles sob controle de seu aplicativo. Agora, imagine um serviço que precisa atualizar parte da interface do usuário. O serviço não acessaria o formulário, controle ou janela diretamente, pois está no thread errado. Para lidar com isso, o ServiceBehaviorAttribute oferece a propriedade UseSynchronizationContext, definida como:

[AttributeUsage(AttributeTargets.Class)]
public sealed class ServiceBehaviorAttribute : ...
{
   public bool UseSynchronizationContext
   {get;set;}
   //More members
}

O valor padrão de UseSynchronizationContext é definido como true. Para determinar qual contexto de sincronização o serviço deve usar, o WCF verifica o thread que abriu o host. Se esse thread tiver um contexto de sincronização e UseSynchronizationContext for definido como true, então o WCF controlará automaticamente todas as chamadas para o serviço nesse contexto de sincronização. Não há interação explícita com o contexto de sincronização necessário do desenvolvedor. O WCF faz isso ao fornecer ao dispatcher de cada ponto de extremidade uma referência ao contexto de sincronização, e o dispatcher usa esse contexto de sincronização para expedir todas as chamadas.

Por exemplo, suponha que o serviço MyService (definido na Figura 1) precisa ser atualizado com parte da interface do usuário no formulário MyForm. Como o host foi aberto depois da construção do formulário, o thread de aberto já tem um contexto de sincronização e, portanto, todas as chamadas ao serviço serão automaticamente controladas no thread correto da interface do usuário.

Figure 1 Usando o contexto de sincronização da interface do usuário (Close Figure 1)
[ServiceContract]
interface IMyContract
{...}

class MyService : IMyContract
{
   /* some code to update MyForm */
}

class MyForm : Form
{...}

static class Program
{
   static void Main()
   {
      Form form = new MyForm();//Sync context established here

      ServiceHost host = new ServiceHost(typeof(MyService));
      host.Open();

      Application.Run(form);

      host.Close();
   }
}

Se você fosse simplesmente abrir o host em Main e usar o código gerado pelo designer do Windows Forms, não se beneficiaria do contexto de sincronização, pois o host é aberto sem ele estar presente:

//No automatic use of synchronization context
static void Main()
{
   ServiceHost host = new ServiceHost(typeof(MyService));
   host.Open();

   Application.Run(new MyForm());//Synchronization context
                                 //established here

   host.Close();
}

Contexto personalizado de sincronização de serviço

A utilização de um contexto personalizado de sincronização pelo seu serviço ou recursos tem dois aspectos relacionados: a implementação do contexto de sincronização e, em seguida, a sua instalação. O código-fonte que acompanha esta coluna contém a classe ThreadPoolSynchronizer, que é definido como:

public class ThreadPoolSynchronizer : SynchronizationContext,IDisposable
{
   public ThreadPoolSynchronizer(uint poolSize);
   public ThreadPoolSynchronizer(uint poolSize,string poolName);

   public void Dispose();
   public void Close();
   public void Abort();
}

O ThreadPoolSynchronizer controla todas as chamadas para um pool personalizado de threads, no qual as chamadas são primeiro colocadas em fila e, em seguida, multiplexadas nos threads disponíveis. O tamanho do pool é fornecido como um parâmetro de construção. Se o pool for maximizado, a chamada ficará pendente na fila até que um thread esteja disponível. Você também pode fornecer um nome de pool (que será o prefixo do nome de threads no pool). Descartar ou fechar o ThreadPoolSynchronizer simplesmente eliminará todos os threads, ou seja, depois de permitir que os threads envolvidos concluam sua tarefa. O método Abort provoca um desligamento não tão simples, pois encerra todos os threads abruptamente. A implementação de ThreadPoolSynchronizer não tem a ver com o WCF. Portanto, escolhi não incluir o código nesta coluna. O uso clássico de um pool de threads personalizado é com um aplicativo de servidor que precisa maximizar seu resultado controlando os threads de trabalho subjacentes e suas atribuições.

Para associar seu serviço a um pool personalizado de threads, você pode anexar manualmente o ThreadPoolSynchronizer ao thread que abre o host:

SynchronizationContext syncContext = new ThreadPoolSynchronizer(3);

SynchronizationContext.SetSynchronizationContext(syncContext);

using(syncContext as IDisposable)
{
   ServiceHost host = new ServiceHost(typeof(MyService));
   host.Open();
   /* Some blocking operations */
   host.Close();
}

O pool de threads terá três threads. Usando a mesma definição de serviço como na Figura 1, MyService terá uma afinidade com esses threads. Todas as chamadas para o serviço serão canalizadas neles, independentemente de o modo de simultaneidade do serviço ou de instância, e em todos os pontos de extremidade e contratos compatíveis com o serviço.

O problema com o código acima é que o serviço está à mercê do código de hospedagem. E se, por projeto, o serviço fosse necessário para execução no pool? Seria melhor aplicar o pool de threads de forma declarativa, como parte da definição de serviço. Para esse fim, escrevi o ThreadPoolBehaviorAttribute:

[AttributeUsage(AttributeTargets.Class)]
public class ThreadPoolBehaviorAttribute : Attribute,
                                           IContractBehavior,
                                           IServiceBehavior
{
   public ThreadPoolBehaviorAttribute(uint poolSize,Type serviceType);
   public ThreadPoolBehaviorAttribute(uint poolSize,Type serviceType,
                                      string poolName);
}

Você aplica o atributo diretamente no serviço, fornecendo ao mesmo tempo o tipo de serviço como um parâmetro construtor:

[ThreadPoolBehavior(3,typeof(MyService))]
class MyService : IMyContract
{...}

O atributo fornece uma instância do ThreadPoolSynchronizer para os dispatchers dos pontos de extremidade do serviço. A chave na implementação do ThreadPoolBehaviorAttribute é saber como e quando acionar os dispatchers com o contexto de sincronização. Para facilitar isso, o ThreadPoolBehaviorAttribute é compatível com o IContractBehavior da interface de extensibilidade do WCF especial:

public interface IContractBehavior
{
   void ApplyDispatchBehavior(ContractDescription description,
                              ServiceEndpoint endpoint,
                              DispatchRuntime dispatch);
   // More members
}

Quando um serviço é decorado com um atributo compatível com IContractBehavior, depois de abrir o host (mas antes de encaminhar as chamadas para o serviço) e para cada ponto de extremidade de serviço, o WCF chama o método ApplyDispatchBehavior, fornecendo a ele o parâmetro DispatchRuntime permitindo que ele afete o dispatcher. A Figura 2 relaciona a implementação de ThreadPoolBehaviorAttribute.

Figure 2 Implementando ThreadPoolBehaviorAttribute (Close Figure 2)
[AttributeUsage(AttributeTargets.Class)]
public class ThreadPoolBehaviorAttribute : Attribute,IContractBehavior,
                                           IServiceBehavior
{
   //Store values in matching members
   protected string PoolName
   {get{...};set{...};}
   protected uint PoolSize
   {get{...};set{...};}
   protected Type ServiceType
   {get{...};set{...};}

   public ThreadPoolBehaviorAttribute(uint poolSize,Type serviceType)
: this(poolSize,serviceType,null)
   {}
   public ThreadPoolBehaviorAttribute(uint poolSize,Type serviceType,
       string poolName)
   {
      PoolName    = poolName;
      ServiceType = serviceType;
      PoolSize    = poolSize;
   }
   protected virtual ThreadPoolSynchronizer ProvideSynchronizer()
   {
      if(ThreadPoolHelper.HasSynchronizer(ServiceType) == false)
         return new ThreadPoolSynchronizer(PoolSize,PoolName);
      else
         return ThreadPoolHelper.GetSynchronizer(ServiceType);
   }

   void IContractBehavior.ApplyDispatchBehavior
(ContractDescription description,ServiceEndpoint endpoint,
            DispatchRuntime dispatch)
   {
      PoolName = PoolName ?? "Pool executing endpoints of " + ServiceType;

      ThreadPoolHelper.ApplyDispatchBehavior(ProvideSynchronizer(),
        PoolSize,ServiceType,PoolName,dispatchRuntime);
   }
   void IServiceBehavior.Validate(ServiceDescription description,
        ServiceHostBase serviceHostBase)
   {
      serviceHostBase.Closed += delegate
          {
              ThreadPoolHelper.CloseThreads(ServiceType);
          };
   }
   //Rest of the implementation
}
public static class ThreadPoolHelper
{
   static Dictionary<Type,ThreadPoolSynchronizer> m_Synchronizers =
       new Dictionary<Type,ThreadPoolSynchronizer>();

   [MethodImpl(MethodImplOptions.Synchronized)]
   internal static bool HasSynchronizer(Type type)
   {
      return m_Synchronizers.ContainsKey(type);
   }

   [MethodImpl(MethodImplOptions.Synchronized)]
   internal static ThreadPoolSynchronizer GetSynchronizer(Type type)
   {
      return m_Synchronizers[type];
   }
   [MethodImpl(MethodImplOptions.Synchronized)]
   internal static void ApplyDispatchBehavior(
ThreadPoolSynchronizer synchronizer,uint poolSize,Type type,
    string poolName,DispatchRuntime dispatch)
   {
      if(HasSynchronizer(type) == false)
      {
         m_Synchronizers[type] = synchronizer;
      }
      dispatch.SynchronizationContext = m_Synchronizers[type];
   }
   [MethodImpl(MethodImplOptions.Synchronized)]
   public static void CloseThreads(Type type)
   {
      if(m_Synchronizers.ContainsKey(type))
      {
         m_Synchronizers[type].Dispose();
         m_Synchronizers.Remove(type);
      }
   }
}

É uma prática recomendada separar a implementação de um atributo de comportamento personalizado do WCF a partir do comportamento real. Deixemos que o atributo decida meramente a seqüência de eventos e tenha uma classe auxiliar fornecendo o comportamento real. Fazer isso permite que você use o comportamento separadamente, por exemplo por um host personalizado. É por isso que o ThreadPoolBehaviorAttribute não faz muito – ele delega a maior parte de seu trabalho a uma classe auxiliar estática denominada ThreadPoolHelper. A classe ThreadPoolHelper fornece o método HasSynchronizer, que informa se o tipo de serviço especificado já tem um contexto de sincronização, e o método GetSynchronizer, que retorna o contexto de sincronização associado ao tipo. O ThreadPoolBehaviorAttribute usa esses dois métodos no método ProvideSynchronizer para garantir que ele criará o pool exatamente uma vez para o tipo de serviço. Essa verificação é necessária, pois ApplyDispatchBehavior pode ser chamado várias vezes – na verdade, uma vez por ponto de extremidade. O ThreadPoolBehaviorAttribute também é um comportamento de serviço personalizado, pois ele implementa IServiceBehavior:

public interface IServiceBehavior
{
   void Validate(ServiceDescription description,ServiceHostBase host);
   //More members
}

O método Validate do IServiceBehavior usa a instância de host do serviço para inscrever-se no evento Closed do host, no qual solicita que ThreadPoolHelper encerre todos os threads no pool.

O que o ThreadPoolHelper faz está associado a todos os dispatchers de todos os pontos de extremidade desse tipo de serviço com a mesma instância do ThreadPoolSynchronizer. Isso garante que todas as chamadas sejam roteadas para o mesmo pool. O ThreadPoolHelper precisa ser capaz de mapear um tipo de serviço para um ThreadPoolSynchronizer específico. Portanto, ele declara um dicionário estático denominado m_Synchronizers que usa tipos de serviço como chaves e as instâncias do ThreadPoolSynchronizer como valores.

No ApplyDispatchBehavior, o ThreadPoolHelper verifica para ver se o m_Synchronizers já contém o tipo de serviço fornecido. Se o tipo não for localizado, o ThreadPoolHelper adiciona o ThreadPoolSynchronizer fornecido para m_Synchronizers, associando-o ao tipo de serviço. Depois disso, ele simplesmente atribui a instância ThreadPoolSynchronizer ao dispatcher:

dispatch.SynchronizationContext = m_Synchronizers[type];

Essa linha única é tudo que é necessário para que o WCF use o contexto de sincronização personalizado a partir de agora. No método CloseThreads, ThreadPoolHelper verifica a partir do dicionário a instância ThreadPoolSynchronizer e a descarta (encerrando simplesmente assim todos os threads de trabalho no pool). Todo o acesso ao dicionário é automaticamente sincronizado por meio do atributo MethodImpl e sinalizador MethodImplOptions.Synchronized. O ThreadPoolHelper também verifica se o valor do tamanho de pool fornecido não excede o valor máximo de chamadas simultâneas do controle do dispatcher (não mostrado na Figura 2).

Contexto personalizado de sincronização de afinidade de thread

Um tamanho de pool, por sua vez, criará uma afinidade entre um thread específico e todas as chamadas de serviço, independentemente dos modos de simultaneidade de serviço e instância. Isso é especialmente útil se o serviço precisar criar parte da interface do usuário, como uma janela pop-up, e periodicamente mostrá-la, ocultá-la e atualizá-la. Sendo a parte que cria a janela, o serviço deve aplicar o fato de ser estar sendo chamado no mesmo thread. Outro uso da afinidade de thread é um serviço que acessa ou cria recursos que usam o TLS. Para formalizar tais requisitos, criei o AffinitySynchronizer especializado da seguinte forma:

public class AffinitySynchronizer : ThreadPoolSynchronizer
{
   public AffinitySynchronizer() :
         this("AffinitySynchronizer Worker Thread")
   {}
   public AffinitySynchronizer(string threadName): base(1,threadName)
   {}
}

Embora você possa instalar AffinitySynchronizer, como no código referenciado anteriormente, também defini o ThreadAffinityBehaviorAttribute na Figura 3. Esse atributo pode ser usado da seguinte forma:

Figure 3 ThreadAffinityBehaviorAttribute (Close Figure 3)
[AttributeUsage(AttributeTargets.Class)]
public class ThreadAffinityBehaviorAttribute :
                                       ThreadPoolBehaviorAttribute
{
   public ThreadAffinityBehaviorAttribute(Type serviceType)
          : this(serviceType,"Affinity Worker Thread")
   {}

   public ThreadAffinityBehaviorAttribute(
Type serviceType,string threadName):base(1,serviceType,
                                                   threadName)
   {}
}

[ThreadAffinityBehavior(typeof(MyService))]
class MyService : IMyContract
{...}

Ao depender da afinidade de thread, todas as instâncias de serviço são sempre seguras quanto a thread, pois somente um único thread (e o mesmo thread nele) pode acessá-los. Configure o serviço com quantidades ConcurrencyMode .Multiple para criar acesso de thread único. Além disso, lembre-se de que configurar com ConcurrencyMode.Reentrant não fornece de fato reentrância, pois não há reentrância possível enquanto o único thread estiver bloqueado na chamada. É melhor selecionar o valor padrão do ConcurrencyMode.Single ao depender da afinidade de thread.

Processamento de prioridade

Por padrão, todas as chamadas para o seu serviço WCF serão processadas na ordem em que chegarem. Isso é verdadeiro para ambos se você usar o pool de threads das portas de conclusão E/S ou o pool de threads personalizado. Geralmente, isso é exatamente o que você deseja. Mas, e se algumas chamadas tiverem prioridade superior e você quiser processá-las assim que chegarem, em vez de tratá-las na ordem? Ainda pior, e se a carga no seu serviço for tal que os recursos de serviço subjacentes sejam exauridos? E se o controlador for maximizado? Nesses casos, as suas chamadas de prioridade serão colocadas em fila da mesma forma que as outras chamadas, aguardando que o serviço ou seus recursos sejam disponibilizados. Os contextos de sincronização oferecem uma solução elegante para esse problema: atribua uma prioridade às chamadas e faça com que o contexto de sincronização classifique as chamadas à medida que elas são recebidas, antes de expedi-las para o pool de threads para execução. Isso é exatamente o que a minha classe PrioritySynchronizer (definida no Figura 4) faz.

Figure 4 PrioritySynchronizer (Close Figure 4)
public enum CallPriority
{
   Low,
   Normal,
   High
}
public class PrioritySynchronizer : ThreadPoolSynchronizer
{
   public PrioritySynchronizer(uint poolSize);
   public PrioritySynchronizer(uint poolSize,string poolName);

   public static CallPriority Priority
   {get;set;}
}

O PrioritySynchronizer é derivado do ThreadPoolSynchronizer e adiciona a classificação que acabei de mencionar. Como os métodos Send e Post do SynchronizationContext não utilizam um parâmetro de prioridade, o cliente do PrioritySynchronizer tem duas formas de passar a prioridade da chamada: a primeira é por meio da propriedade Priority, que armazena a prioridade (um valor de CallPriority do tipo enum) no TLS do thread de chamada. A segunda opção é por meio dos cabeçalhos de mensagem, que discutirei posteriormente nesta coluna. Se deixado não especificado, Priority será padrão como CallPriority.Normal.

Ele também fornece o PriorityCallsBehaviorAttribute correspondente, mostrado na Figura 5. O uso de PriorityCallsBehaviorAttribute é simples, como você pode ver aqui:

Figure 5 Implementando PriorityCallsBehaviorAttribute (Close Figure 5)
[AttributeUsage(AttributeTargets.Class)]
public class PriorityCallsBehaviorAttribute : ThreadPoolBehaviorAttribute
{
   public PriorityCallsBehaviorAttribute(uint poolSize,Type serviceType)
            : this(poolSize,serviceType,null)
   {}
   public PriorityCallsBehaviorAttribute(uint poolSize,Type serviceType,
       string poolName) : base(poolSize,serviceType,poolName)
   {}
   protected override ThreadPoolSynchronizer ProvideSynchronizer()
   {
      if(ThreadPoolHelper.HasSynchronizer(ServiceType) == false)
         return new PrioritySynchronizer(PoolSize,PoolName);
      else
         return ThreadPoolHelper.GetSynchronizer(ServiceType);
   }
}

[PriorityCallsBehavior(3,typeof(MyService))]
class MyService : IMyContract
{...}

O PriorityCallsBehaviorAttribute substitui o ProvideSynchronizer e fornece PrioritySynchronizer em vez de ThreadPoolSynchronizer. Como PrioritySynchronizer é derivado do ThreadPoolSynchronizer, isso é transparente no que diz respeito a ThreadPoolHelper.

O real desafio de implementar e oferecer suporte ao processamento de prioridade é fornecer a chamada de prioridade a partir do cliente ao serviço e, por fim, ao PrioritySynchronizer. O uso da propriedade Priority de PrioritySynchronizer é útil somente para clientes não WCF que interagem diretamente com o contexto de sincronização; ele não é útil para um cliente WCF, cujo thread nunca seja usado para acessar o serviço. Embora você pudesse fornecer a prioridade como um parâmetro explícito em todos os métodos, eu queria um mecanismo genérico que pudesse ser aplicado em qualquer contrato e serviço. Para esse fim, você precisa passar prioridade da chamada fora de banda por meio dos cabeçalhos de mensagem. Em minha coluna Fundamentos de fevereiro de 2007 intitulada “Criar um serviço de resposta do WCF em fila” (consulte msdn.microsoft.com/msdnmag/issues/07/02/Foundations), enfrentei um problema semelhante – passar o endereço do serviço de resposta fora de banda para o serviço – e expliquei em detalhes o uso dos cabeçalhos de entrada e saída. Embora pudesse usar exatamente a mesma técnica aqui, isso seria um trabalho repetitivo. Portanto, é melhor estender a técnica mostrada nessa coluna e incrementar o WCF com o gerenciamento de fins gerais de informações irrelevantes do cliente para o serviço, em vigor, um contexto personalizado específico de aplicativo e genérico ainda seguro quanto a tipo. Isso é o que a classe GenericContext<T> fornece. A Figura 6 mostra sua implementação com o tratamento de erros removido.

Figure 9 HeaderClientBase<T,H> (Close Figure 9)
public abstract partial class HeaderClientBase<T,H> :
ClientBase<T> where T : class
{
   protected H Header;

   public HeaderClientBase() : this(default(H))
   {}

   public HeaderClientBase(string endpointName)
                                   : this(default(H),endpointName)
   {}

   public HeaderClientBase(H header)
   {
      Header = header;
   }

   public HeaderClientBase(H header,string endpointName)
                                    : base(endpointName)
   {
      Header = header;
   }

   /* More constructors */

   protected virtual object Invoke(string operation,params object[] args)
   {
      using(OperationContextScope contextScope =
           new OperationContextScope(InnerChannel))
      {
         GenericContext<H>.Current = new GenericContext<H>(Header);

         Type contract = typeof(T);
         MethodInfo methodInfo = contract.GetMethod(operation);
         return methodInfo.Invoke(Channel,args);
     }
   }
}

GenericContext<T> completamente a interação com os cabeçalhos de mensagens. Tudo que um cliente precisa fazer é passar informações adicionais para o serviço é definir a propriedade Current dentro de um novo OperationContextScope. Por exemplo, para passar o número 4 nos cabeçalhos, use o código como mostrado na Figura 7. Você ainda pode encapsular o GenericContext<T> por meio de derivação específica de tipo, como IntContext mostrado na Figura 8.

Figure 8 Contexto de inteiro (Close Figure 8)
[DataContract]
public class IntContext : GenericContext<int>
{
   public IntContext()
   {}

   public IntContext(int number) : base(number)
   {}

   public static int Number
   {
      get
         return Current.Value;
      set
         Current = new IntContext(value);
   }
}
Figure 7 Usando GenericContext<T> (Close Figure 7)
MyContractClient proxy = new MyContractClient();

using(OperationContextScope contextScope =
    new OperationContextScope(proxy.InnerChannel))
{
   GenericContext<int>.Current = new GenericContext<int>(4);
   proxy.MyMethod();
}

Com o uso de IntContext, a Figura 7 teria uma linha como:

IntContext.Current = 4;

Literalmente, qualquer tipo de contrato de dados (ou serializável) pode ser usado para o parâmetro de tipo no contexto personalizado. No lado do serviço, para ler o valor dos cabeçalhos personalizados, qualquer parte downstream pode escrever:

int number = GenericContext<int>.Current.Value;

ou usar o contexto personalizado específico de tipo:

int number = IntContext.Number;

Isso é exatamente o que a PrioritySynchronizer faz ao verificar a prioridade de chamada. Ela espera que os clientes forneçam a prioridade no TLS (através da propriedade Priority) ou na forma de um contexto personalizado que armazena a prioridade nos cabeçalhos da mensagem, usando código semelhante ao que você já viu na Figura 7:

GenericContext<CallPriority>.Current =
        new GenericContext<CallPriority>(CallPriority.High);
proxy.MyMethod();

No entanto, eu não estava muito satisfeito com o código, pois ele está exposto, associa o cliente ao mecanismo de propriedade e força o cliente a definir a prioridade sempre antes de usar o proxy. Para lidar com esses problemas, criei a classe base de proxy HeaderClientBase<T,H> mostrada na Figura 9.

Como sua classe base, o HeaderClientBase<T,H> aceita o parâmetro T do tipo de contrato, mas também o tipo H do cabeçalho personalizado. HeaderClientBase<T,H> usa o valor padrão de H ou de um valor fornecido de tempo de construção para passar para todas as chamadas. Na falta de um gerador de código compatível, como SvcUtil, recorri à reflexão para invocar a operação de serviço real, encapsulando completamente a interação com o contexto genérico e o escopo de contexto da operação. Você precisa derivar a partir de HeaderClientBase<T,H> e fornecer seu tipo desejado de cabeçalho, como na Figura 10, fornecendo um inteiro.

Figure 10 Usando HeaderClientBase<T,H> (Close Figure 10)
[ServiceContract]
interface IMyContract
{
   [OperationContract]
   void MyMethod(string text);
}

class MyContractClient : HeaderClientBase<IMyContract,int>,IMyContract
{
   public MyContractClient(int number) : base(number)
   {}
   public void MyMethod(string text)
   {
      Invoke("MyMethod",text);
   }
}

Ao usar o código na Figura 10, você pode ver que essa Figura 7 está reduzida a meramente:

MyContractClient proxy = new MyContractClient(4);
proxy.MyMethod();

Embora você possa usar as mesmas técnicas, como mostrado na Figura 10 para passar a prioridade de chamada, para fins de generalidade é melhor definir um proxy habilitado para prioridade para fins gerais, a PriorityClientBase<T>, mostrada na Figura 11.

Figure 11 Definindo PriorityClientBase<T> (Close Figure 11)
public abstract partial class PriorityClientBase<T> :
     HeaderClientBase<T,CallPriority> where T : class
{
   public PriorityClientBase() : this(PrioritySynchronizer.Priority)
   {}

   public PriorityClientBase(string endpointName) :
       this(PrioritySynchronizer.Priority,endpointName)
   {}

   public PriorityClientBase(Binding binding,
EndpointAddress remoteAddress) :
       this(PrioritySynchronizer.Priority,binding,remoteAddress)
   {}

   public PriorityClientBase(CallPriority priority) : base(priority)
   {}

   public PriorityClientBase(CallPriority priority,string endpointName) :
       base(priority,endpointName)
   {}

   public PriorityClientBase(CallPriority priority,Binding binding,
       EndpointAddress remoteAddress) :
       base(priority,binding,remoteAddress)
   {}
   /* More constructors */
}

PriorityClientBase<T> codifica o uso de CallPriority para o parâmetro de tipo H. Além disso, o PriorityClientBase<T> é padrão para leitura da prioridade a partir de TLS (produzindo CallPriority.Normal quando não encontrado). Portanto, ele pode ser usado como qualquer outra classe de proxy. Com alterações muito menores nas suas classes de proxy existentes, você agora pode adicionar suporte ao processamento de prioridade, como mostrado na Figura 12.

Figure 12 Suporte ao processamento de prioridade (Close Figure 12)
class MyContractClient : PriorityClientBase<IMyContract>,IMyContract
{
   //Reads priority from TLS
   public MyContractClient()
   {}

   public MyContractClient(CallPriority priority) : base(priority)
   {}
   public void MyMethod(string text)
   {
      Invoke("MyMethod",text);
   }
}

MyContractClient proxy = new MyContractClient(CallPriority.High);
proxy.MyMethod();

Retornos de chamada e contexto de sincronização

No que diz respeito aos contextos de sincronização, o WCF oferece suporte semelhante aos objetos de retorno de chamada com o CallbackBehaviorAttribute aplicado no tipo de retorno de chamada:

[AttributeUsage(AttributeTargets.Class)]
public sealed class CallbackBehaviorAttribute : Attribute,...
{
   public bool UseSynchronizationContext
   {get;set;}
   //More members
}

Como com o ServiceBehaviorAttribute, o UseSynchronizationContext é definido por padrão como true. O aspecto interessante de um objeto de retorno de chamada é quando a afinidade com o contexto de sincronização está bloqueada. O WCF examinará o thread que abre o proxy que estabelece a comunicação dúplex. Se esse thread tiver um contexto de sincronização, então esse contexto será usado para todos os retornos de chamada. Embora você possa instalar manualmente um contexto personalizado de sincronização ao defini-lo antes de abrir o proxy, é melhor fazer isso de forma declarativa usando um atributo. Para afetar o dispatcher do ponto de extremidade do retorno de chamada, o atributo precisa implementar a interface IEndpointBehavior:

public interface IEndpointBehavior
{
   void ApplyClientBehavior(ServiceEndpoint endpoint,
                            ClientRuntime clientRuntime);
   //More members
}

No método ApplyClientBehavior, o parâmetro ClientRuntime contém uma referência ao dispatcher do ponto de extremidade com a propriedade CallbackDispatchRuntime:

public sealed class ClientRuntime
{
   public DispatchRuntime CallbackDispatchRuntime
   {get;}
   //More members
}

O restante é idêntico ao atributo do lado do serviço, como demonstrado pelo CallbackThreadPoolBehaviorAttribute na Figura 13.

Figure 13 CallbackThreadPoolBehaviorAttribute (Close Figure 13)
[AttributeUsage(AttributeTargets.Class)]
public class CallbackThreadPoolBehaviorAttribute :
 ThreadPoolBehaviorAttribute,IEndpointBehavior
{
   public CallbackThreadPoolBehaviorAttribute(uint poolSize,
Type clientType) : this(poolSize,clientType,null)
   {}
   public CallbackThreadPoolBehaviorAttribute(uint poolSize,
Type clientType,string poolName) :
 base(poolSize,clientType,poolName)
   {
      AppDomain.CurrentDomain.ProcessExit += delegate
      {
         ThreadPoolHelper.CloseThreads(ServiceType);
      };
   }
   void IEndpointBehavior.ApplyClientBehavior
      (ServiceEndpoint serviceEndpoint,ClientRuntime clientRuntime)
   {
      IContractBehavior contractBehavior = this;
      contractBehavior.ApplyDispatchBehavior(null,serviceEndpoint,
         clientRuntime.CallbackDispatchRuntime);
   }
   //Rest of the implementation
}

Na verdade, no atributo de retorno de chamada, quis reutilizar o máximo possível do atributo de serviço. Para esse fim, o CallbackThreadPoolBehaviorAttribute é derivado de ThreadPoolBehaviorAttribute. Seus construtores passam o tipo cliente para os construtores base como o tipo de serviço. A implementação CallbackThreadPoolBehaviorAttribute do ApplyClientBehavior consulta sua classe base para IContractBehavior (isso é como uma subclasse usa uma implementação de interface particular explícita de sua classe base) e delega a implementação para ApplyDispatchBehavior.

A grande diferença entre um atributo de retorno de chamada de cliente e um atributo de serviço é que o cenário de retorno de chamada não tem objeto host para inscrever seu evento Closed. Para compensar, o CallbackThreadPoolBehaviorAttribute monitora o evento de saída do processo para fechar todos os threads no pool. Se o cliente quiser agilizar o encerramento desses threads, ele pode usar o ThreadPoolBehavior.CloseThreads, como mostrado na Figura 14.

Figure 14 Usando CallbackThreadPoolBehaviorAttribute (Close Figure 14)
[ServiceContract(CallbackContract = typeof(IMyContractCallback))]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}
interface IMyContractCallback
{
   [OperationContract]
   void OnCallback();
}

[CallbackThreadPoolBehavior(3,typeof(MyClient))]
class MyClient : IMyContractCallback,IDisposable
{
   MyContractClient m_Proxy;

   public void CallService()
   {
      m_Proxy = new MyContractClient(new InstanceContext(this));

      m_Proxy.MyMethod();
   }

   //Called by threads from the custom pool
   public void OnCallback()
   {...}

   public void Dispose()
   {
      m_Proxy.Close();
      ThreadPoolHelper.CloseThreads(typeof(MyClient));
   }
}

Assim como o lado de serviço, para ter todos os retornos de chamada executados no mesmo thread (talvez para criar parte da interface de usuário no lado do retorno de chamada), você pode configurar os retornos de chamada para ter um tamanho de pool de 1. Ainda melhor, você pode definir um atributo de retorno de chamada dedicado para tal afinidade de mesmo thread, como na Figura 15.

Figure 15 CallbackThreadAffinityBehaviorAttribute (Close Figure 15)
[AttributeUsage(AttributeTargets.Class)]
public class CallbackThreadAffinityBehaviorAttribute :
                     CallbackThreadPoolBehaviorAttribute
{
   public CallbackThreadAffinityBehaviorAttribute(Type clientType) :
        this(clientType,"Callback Worker Thread")
   {}
   public CallbackThreadAffinityBehaviorAttribute(Type clientType,
        string threadName) : base(1,clientType,threadName)
   {}
}

Por que usar contextos de sincronização?

Os contextos de sincronização no .NET Framework 2.0 são uma construção avançada e, associados ao WCF, proporcionam um modelo de programação superior e mais produtivo. A extensibilidade inerente ao WCF torna essa combinação especialmente poderosa, realizando tarefas difíceis, como afinidade de thread e processamento de prioridade com relativamente poucas linhas de código e tudo isso fora do escopo do seu serviço. Os cabeçalhos personalizados e as técnicas genéricas de contexto que demonstrei nesta coluna são úteis em todo o cenário da programação do WCF. Sempre que os parâmetros fora de banda forem chamados, eles fornecerão uma solução elegante e concisa e de tipo segura. Também gostaria de compartilhar com você meus processos e compensações para que você os considere ao evoluir suas próprias extensões personalizadas do WCF.

Envie suas perguntas e comentários para mmnet30@microsoft.com.

Juval Lowy é arquiteto de software na IDesign e fornece treinamento em WCF e consultoria de arquitetura WCF. Seu livro recente é o Programming WCF Services (O'Reilly, 2007). Ele também é diretor regional da Microsoft no Vale do Silício. Entre em contato com Juval no endereço www.idesign.net.


Bugslayer: Medindo o impacto do estado de exibi&#231;&#227;o

Bugslayer
Medindo o impacto do estado de exibição
John Robbins

Download do código disponível em: Bugslayer2007_11.exe (209KB)
Conteúdo
Estado de exibição
Dicas de depuração
Objetivos
Rastreamento de estado de exibição
Destaques da implementação
Idéias adicionais

V
ocê não acha engraçado que o Microsoft® .NET Framework seja considerado um ambiente onde você não precisa pensar em memória? Qual é o problema número um que continua a atormentar os aplicativos gerenciados hoje em dia? Memória! Por quê? Quando o coletor de lixo é executado para recuperar memória, o CLR (common language runtime) suspende todos os threads do aplicativo £ e nenhum trabalho é executado. Quando você não consegue executar trabalho algum, você tem um problema de desempenho.

Independentemente da importância que você dá ao uso da memória do .NET Framework, dois grandes problemas interferem na obtenção de uma imagem real dessa memória. O primeiro é que ainda que o .NET Framework execute uma quantidade enorme de trabalho para você, a documentação não é necessariamente útil em descobrir qual trabalho está sendo executado e a quantidade de memória usada. Felizmente, o Lutz Roeder's Reflector para .NET (aisto.com/roeder/dotnet) ajuda consideravelmente.

O segundo problema em observar o uso de memória é que a principal ferramenta de desenvolvimento principal para .NET, o Visual Studio®, não exibe informações sobre a memória. Isso não é culpa da equipe do Visual Studio, mas sim uma falha da arquitetura original do CLR, a qual não expõe um meio pelo qual a API de depuração do CLR veja em qual geração está um objeto específico. Embora seja possível usar a determinação de perfil de API do CLR para controlar as gerações de objetos, isso demanda muito esforço e não funciona durante a depuração. Conforme escrevi em junho de 2003 (msdn.microsoft.com/msdnmag/issues/03/06/Bugslayer) e novamente em março de 2005 (msdn.microsoft.com/msdnmag/issues/05/03/Bugslayer) nas colunas Bugslayer, é possível usar a extensão WinDBG do SOS (Son of Strike) para ver todas as informações de memória desejadas, mas talvez esta seja a ferramenta mais difícil desde o depurador kernel original do Windows® 95.

No episódio do Bugslayer, desejo apresentar uma ferramenta que permita a você examinar um dos aplicativos do ASP.NET de melhor desempenho: estado de exibição. Como você verá, conseguir ver facilmente o uso do estado de exibição em um servidor de produção pode significar a diferença entre trinta minutos de depuração e seis horas de adivinhação.

Estado de exibição

Em uma página ASP.NET, o estado de exibição é um mecanismo extremamente inteligente, no qual o estado dos controles na página é armazenado em um valor oculto dentro do código HTML enviado ao cliente. Isso evita a manutenção do estado da página no processo de trabalho do ASP.NET, o que poderia colocar ainda mais pressão de memória no servidor. Se você já trabalhou em um filtro ISAPI gravado em um C++ nativo no período Jurássico da Internet (mais ou menos em 1996), você lembra a quantidade de esforço gasto para controlar uma página, sessão e estado do aplicativo. Se você não tiver muita experiência com ASP.NET, leia a coluna Cutting Edge de Dino Esposito, publicada em fevereiro de 2003, sobre o estado de exibição como instrução (msdn.microsoft.com/msdnmag/issues/03/02/cuttingedge).

O problema com o estado de exibição é que não podemos saber, em nenhum momento, seu tamanho. Isso é especialmente verdadeiro quando há um controle GridView na página. A facilidade de uso em obter as informações do banco de dados na página tem um custo. A boa notícia é que no ASP.NET 2.0, os desenvolvedores da Microsoft trabalharam muito para reduzir o tamanho do estado de exibição comparado ao ASP.NET 1.1. No entanto, nos últimos meses ajudei a depurar vários aplicativos ASP.NET 2.0 grandes, onde os problemas de desempenho foram causados por estados de exibição enormes em algumas páginas.

Dicas de depuração

Antes de abordarmos a ferramenta e o código, quero discutir alguns dos meus truques de depuração. Quando os desenvolvedores me chamam por causa de um aplicativo ASP.NET que está com um problema de desempenho, eles dizem, invariavelmente, que "O aplicativo estava funcionando bem, mas agora está realmente lento".

Minha primeira pergunta é, "Vocês estão usando GridViews no aplicativo"? Como a maioria dos aplicativos ASP.NET são front-ends para banco de dados, isso ocorre algumas vezes, mas nem sempre. Se eles não estiverem usando GridViews, preciso buscar um caminho diferente.

A segunda pergunta que faço é, "Posso olhar o seu histórico de procedimentos armazenado no banco de dados do controle de versão"? Infelizmente, nesse momento, muitas equipes de desenvolvedores engolem em seco e me dizem que não têm procedimentos armazenados no controle de versão. Na última vez que verifiquei, um procedimento SQL armazenado estava em código, e se seguirmos as regras de bom senso de desenvolvimento, todos os códigos devem ficar em um controle de versão. Na verdade, a Microsoft torna isso realmente fácil com o Visual Studio 2005 Team Edition for Database Professionals. Se você não tiver procedimentos armazenados no controle de versão, será necessário parar de ler agora e adicioná-los.

O motivo pelo qual verifiquei os procedimentos armazenados no controle da versão é que, quando o aplicativo está sendo bem executado e de repente começa a apresentar problemas, preciso encontrar as alterações entre as edições anterior e atual. Não posso começar a dizer a você quantas vezes procurei no controle de versão logs alterados nos procedimentos armazenados e vi onde alguém alterou uma consulta mais ou menos assim:

   select * from table

Quando você está colocando aquela saída em um controle GridView (o que depende muito do estado de exibição) e o banco de dados contém seis milhões de registros, você tem um sério problema com o estado de exibição!

Embora o estado de exibição seja fácil de usar, é quase impossível ver quando há um problema. A maioria dos desenvolvedores sabe que você pode ver o estado de exibição de uma página ativando o rastreamento do ASP.NET. Infelizmente, o rastreamento do ASP.NET armazena apenas um número limitado de rastreamento e pode ser extremamente lento em um servidor de produção. Sua alternativa seria fazer com que cada usuário clicasse com o botão direito do mouse no navegador, selecionasse View Source no menu de contexto e iniciasse manualmente a contagem de caracteres. Obviamente, precisamos de uma solução melhor: uma maneira simples de rastrear o maior estado de exibição de todas as páginas em um servidor de produção para que você possa ter seus dados em um único lugar e tomar decisões de depuração inteligentes.

Objetivos

Quando comecei a pensar sobre os desafios de uma ferramenta que permita a fácil visualização do uso do estado de exibição, o objetivo principal era que não importa o que eu criasse, ele tinha que ser útil em um ambiente de produção. Em outras palavras, o código deveria ser rápido e gastar pouca memória. Portanto, tomei a decisão de armazenar apenas os maiores estados de exibição para cada página específica. Se eu armazenasse todos os tamanhos de estado de exibição de cada página, eu seria facilmente forçado a armazenar milhões de registros. Ao armazenar apenas os maiores estados de exibição de uma página, reduzo drasticamente o requisito de armazenamento, garantindo que nunca haverá mais entradas de dados que o número de arquivos .aspx no aplicativo ASP.NET.

Eu também quis fazer com que fosse simples e compreensível olhar os dados do estado de exibição, mesmo para não-desenvolvedores. Embora saber o tamanho total do estado de exibição de uma página seja ótimo, adicionei um requisito adicional que era uma opção de armazenar e exibir os tamanhos dos estados de exibição de todos os controles de uma página. Isso tornaria muito mais fácil a identificação do controle que contribuiu para um problema.

Também quis ter a opção de ver o tamanho do estado de exibição nas solicitações GET HTTP . Embora a maioria dos problemas com o estado de exibição esteja relacionada com as solicitações POST HTTP, os usuários podem armazenar o URL completo de uma página específica e ir diretamente para a página. Isso ocorre pouco, mas achei interessante incluir essa opção para que a solução fosse completa.

Finalmente, eu quis garantir a implantação fácil da ferramenta. Como objetivo secundário, seria ideal se a minha solução não exigisse alterações no código-fonte.

O download do código-fonte da coluna deste mês inclui o assembly que contém toda a mágica: Bugslayer.Web. Também inclui os testes de unidade completos, para que você possa brincar com o assembly e ver como ele funciona. Nos servidores de produção, aconselhamos instalar o Bugslayer.Web.DLL no GAC (Cache de Assembly Global), uma vez que ele permitirá o carregamento do código de qualquer lugar da máquina.

Existem duas partes principais no Bugslayer.Web: HttpModule e HttpHandler. Se você leu já sobre o pipeline de processamento do ASP.NET, você provavelmente adivinhará que HttpModule é onde ocorre a coleta de dados e HttpHandler é onde os dados são exibidos. E isso significa que você não precisará fazer alterações de código para usar o Bugslayer.Web.

Para configurar o aplicativo ASP.NET para usar HttpModule, será necessário adicionar o elemento <httpModules> como o seguinte arquivo web.config:

<?xml version="1.0"?>
<configuration>
  <system.web>
    <httpModules>
      <add name="ViewStateStats"
          type="Bugslayer.Web.ViewStateStatisticsModule"/>
    </httpModules>
  </system.web>
</configuration>

A configuração de HttpHandler é bem fácil. No arquivo web.config, adicione o elemento <httpHandlers> como segue:

<?xml version="1.0"?>
<configuration>
  <system.web>
    <httpHandlers>
      <add verb="*"
           path="viewstatestats.axd"
           type="Bugslayer.Web.ViewStateStatisticsHandler"
           validate="false" />
    </httpHandlers>
  </system.web>
</configuration>

Observe que o Bugslayer.Web.ViewStateStatisticsHandler é mapeado para viewstatestats.axd. Escolhi usar a extensão .axd porque o IIS já o entende como sendo um tipo de extensão do ASP.NET. Ao configurar o atributo path para um nome de arquivo específico, o ASP.NET chamará o HttpHandler específico, mas as solicitações de outros nomes de arquivo são encaminhadas ao manipulador .axd interno ASP.NET.

Rastreamento de estado de exibição

Depois de instalar o Bugslayer.Web e adicionar as entradas a web.config, você estará gravando a maior contagem de bytes do estado de exibição por página. Embora a coleta de dados seja ótima, você precisa conseguir vê-la e, como foi discutido, é para isso que serve o ViewStateStats.axd. A Figura 1 mostra uma saída de exemplo. O formato poderá parecer familiar já que tomei emprestado o estilo da saída de rastreamento do ASP.NET. Como você poder ver, o padrão é do maior para o menor estado de exibição. Para classificar o URL ou a hora, ou os dois de forma ascendente, clique nos cabeçalhos da coluna.

Figura 1 Saída de exemplo ViewStateStats.axd
Figura 1 Saída de exemplo ViewStateStats.axd(Clique na imagem para diminuir a exibição)
Figura 1 Saída de exemplo ViewStateStats.axd
Figura 1 Saída de exemplo ViewStateStats.axd (Clique na imagem para aumentar a exibição)

É possível controlar a coleta de dados do estado de exibição na parte superior da página ViewStateStats.axd. A opção mais interessante é o segundo link, Collect Control View State Sizes (Coletar tamanhos do estado de exibição de controle). Se o estado estiver desativado (a opção padrão), serão coletados apenas os tamanhos totais da página. Clicar no link alternará o estado de coleta para manter a lista de controles filho e os estados de exibição individual dos maiores estados de exibição totais de cada página. Embora isso cause um impacto no desempenho e use mais memória, há alguns momentos em que será necessário descobrir qual controle está sobrecarregando o estado de exibição. A Figura 2 mostra um exemplo da página ViewStateStats.axd com os controles expandidos.

Figura 2 Saída de árvore de controles ViewStateStats.axd
Figura 2 Saída de árvore de controles ViewStateStats.axd(Clique na imagem para diminuir a exibição)
Figura 2 Saída de árvore de controles ViewStateStats.axd
Figura 2 Saída de árvore de controles ViewStateStats.axd (Clique na imagem para aumentar a exibição)

Como a saída de rastreamento do ASP.NET, a árvore de controles é exibida com o nome do controle e o tamanho do estado de exibição. Se você estiver imaginando onde estão os bytes do tamanho de processamento e os valores dos bytes do tamanho de ControlState, não há como obter os dados de processamento e acho que os cálculos do tamanho de ControlState seriam muito lentos. Discutirei o porquê na seção de implementação. No entanto, como o estado de exibição é o principal inibidor de desempenho, a contribuição de controle verdadeiramente importante está aqui. Observe que os tamanhos individuais de controle não serão adicionados ao tamanho total do estado de exibição mostrado próximo ao URL e à hora. Como o estado de exibição está codificado na Base64, a codificação adiciona mais bytes ao estado de exibição geral.

O primeiro link no título ViewStateStats.axd, Clear all view state statistics (Limpar todas as estatísticas de estado de exibição), descarrega todos os valores salvos existentes e inicia novos. O link final, Collect View State Sizes for HTTP GET Requests (Coletar tamanhos de estado de exibição para as solicitações GET HTTP), ativa o relatório das solicitações GET HTTP. Como quase toda a preocupação que você tem sobre estado de exibição é um HTTP POST, essa opção deverá ser raramente ativada. No entanto, se for necessário visualizar os tamanhos do estado de exibição de HTTP GETs e HTTP POSTs, você terá a capacidade. Se você alterar esta opção, depois de cada URL na página ViewStateStats.axd, você verá (GET) ou (POST) conforme apropriado. Quando eu discutir como calcular o tamanho do estado de exibição HTTP GET, você verá que há implicações de desempenho para registrar esses dados.

Destaques da implementação

Quando comecei a cogitar a idéia de gravar o tamanho do estado de exibição de todo o aplicativo, fiz o protótipo da classe derivada System.Web.UI.Page. É comum obter a seqüência de caracteres do estado de exibição com o seguinte código C#:

String viewState = Request.Params [ "__VIEWSTATE" ];

O problema com o uso de uma classe Page derivada é que, para muitos aplicativos ASP.NET existentes, ela solicitará que você altere seu código para ver o estado de exibição através do aplicativo. Como HttpModules permite que seu código participe do pipeline ASP.NET normal, essa era a solução perfeita. Como eu só quis inspecionar a página depois de estar pronta para ser enviada ao usuário, configurei um manipulador de evento EndRequest no método de implementação IHttpModule.Init.

Uma descoberta interessante feita por mim enquanto lia sobre o estado de exibição do ASP.NET foi a seguinte configuração web.config:

<pages maxPageStateFieldLength="XX"/>

Aqui, XX é o número máximo de bytes para dividir o valor __VIEWSTATE em partes. Você também pode configurá-lo com a propriedade System.Web.Ui.Page.MaxPageStateFieldLength.

O ASP.NET 2.0 introduziu esta configuração para ajudar a dividir grandes valores __VIEWSTATE porque alguns firewalls e proxies obstruem seqüências de caracteres gigantescas na página. Considerando que seqüências de caracteres gigantescas são coisas as quais se quer evitar, e todo o objetivo desta coluna, você poderá pensar que configurar maxPageStateFieldLength poderia ajudar a reduzir o tamanho. Isso não só ajuda, incha realmente suas páginas que vão para baixo no navegador.

Digamos que você tenha um estado de exibição como o seguinte na página:

<div>
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE"
value="/wEPDwULLTE1ODEyMjM1MTgPZBYCAgMPZBYCAgcPEGQPFgNmAgECAhYDEAUBYQUBYWcQBQFiBQFiZxAFAW
MFAWNnZGRkQIU7yEIZAT5uuif4cZcFHBJgZ48=" />
</div>

Configurar maxPageStateFieldLength para 50 produz o seguinte, que tem mais texto real na página:

<div>
<input type="hidden" name="__VIEWSTATEFIELDCOUNT"
       id="__VIEWSTATEFIELDCOUNT" value="3" />
<input type="hidden" name="__VIEWSTATE"
       id="__VIEWSTATE" value="/wEPDwULLTE1ODEyMjM1MTgPZBYCAgMPZBYCAgcPEGQPFgNmAg" />
<input type="hidden" name="__VIEWSTATE1"
       id="__VIEWSTATE1" value="ECAhYDEAUBYQUBYWcQBQFiBQFiZxAFAWMFAWNnZGRkQIU7yEIZ" />
<input type="hidden" name="__VIEWSTATE2"
       id="__VIEWSTATE2" value="AT5uuif4cZcFHBJgZ48=" />
</div>

Embora haja provavelmente alguns leitores que tenham configurado maxPageStateFieldLength, duvido que a maioria o esteja usando. Se você estiver dividindo o estado de exibição em partes para contornar problemas de firewall e proxy, sugiro veementemente que você considere reformular a página ou armazenar o estado de exibição no servidor para evitar o envio de grandes quantidades de dados pela conexão.

O HttpModule, que foi implementado no ViewStateStatisticsModule.cs, manipula adequadamente o caso onde maxPageStateFieldLength foi configurado. Como tenho que enumerar cada um dos parâmetros __VIEWSTATE*, o código que usei é um pouco menor que acessar um único parâmetro __VIEWSTATE.

Como a coleta e a exibição de dados estão logicamente conectadas, decidi agrupá-las atrás da interface IViewStateProcessor e desassociá-las daquela funcionalidade em HttpModule e HttpHandler. A interface IViewStateProcessor oferece métodos para processar as solicitações HTTP GET e HTTP POST e para exibir os dados. Minha idéia é facilitar para fornecer coletas adicionais de dados e exibir sem precisar tocar na maioria do código em Bugslayer.Web.dll.

A classe derivada de IViewStateProcessor deve estar na memória a todo o momento, então a armazenei em um campo estático dentro da classe ViewStateStatisticsModule. A classe ViewStateStatisticsHandler simplesmente acessa uma propriedade estática em ViewStateStatisticsModule. Agora, o código suporta apenas uma única classe derivada IViewStateProcessor. Uma boa adição seria adicionar suporte para o uso simultâneo de classes adicionais.

Para especificar sua própria classe IViewStateProcessor-derived personalizada, será necessário substituir o método HttpApplication.Init. Lá, você acessará ViewStateStatisticsModule por HttpApplication.Modules["ViewStateStats"]. Crie uma instância da classe IViewStateProcessor-derived personalizada e configure ViewStateStatisticsModule.ViewStateProcessor para sua instância.

A classe derivada IViewStateProcessor padrão, BiggestPageProcessor, é onde todo o trabalho real ocorre para coletar e exibir os dados vistos nas Figuras 1 e 2. Os dados de coleta são armazenados em uma classe Dictionary com o URL como a chave para a pesquisa rápida. A parte interessante do código de coleta era descobrir como obter os estados de exibição para controles individuais.

Como todos são usados para visualizar os tamanhos do estado de visualização para cada controle individual, eu quis assegurar que meus números erram correspondentes para manter a correção. Isso significava que eu precisava descobrir onde aconteceu a ação no código de rastreamento do ASP.NET. A primeira parada do Reflector foi System.Web.Handlers.TraceHandler, que era o HttpHandler registrado para trace.axd. Cerca de 15 minutos de exploração do código descompilado e de procura dos tipos de dados referenciados me apontaram para System.Web.Ui.Page.ProcessRequestMain, que desempenha o rastreamento real se Page.Context.TracingEnabled for verdadeiro. Depois de saltar por Page.BuildPageProfileTree, finalmente cheguei a Control.BuildProfileTree, onde ocorre toda a ação para obter os controles e os dados na saída de rastreamento. A Figura 3 mostra o código descompilado.

Figure 3 Método Control.BuildProfileTree descompilado (Close Figure 3)
protected void BuildProfileTree(string parentId, bool calcViewState)
{
    int viewStateSize;
    calcViewState = calcViewState && !this.flags[4];
    if (calcViewState)
    {
        viewStateSize = this.EstimateStateSize(this.SaveViewState());
    }
    else
    {
        viewStateSize = 0;
    }
    int controlStateSize = 0;
    if (((this.Page != null) &&
         (this.Page._registeredControlsRequiringControlState != null)) &&
         this.Page._registeredControlsRequiringControlState.Contains(
         this))
    {
        controlStateSize =
            this.EstimateStateSize(this.SaveControlStateInternal());
    }
    this.Page.Trace.AddNewControl(this.UniqueID, parentId,
        base.GetType().FullName,  viewStateSize, controlStateSize);
    if ((this._occasionalFields != null) &&
        (this._occasionalFields.Controls != null))
    {
        int count = this._occasionalFields.Controls.Count;
        for (int i = 0; i < count; i++)
        {
            this._occasionalFields.Controls[i].BuildProfileTree(
                this.UniqueID, calcViewState);
        }
    }
}

A parte interessante do código do estado de exibição é as 10 primeiras linhas do método. A primeira parte do código que realmente me derrubou foi a referência !this.flags[4] porque o Reflector mostra os campos de sinalizador como privados. Usando o Analyzer do Reflector, fui até o campo de sinalizadores e cliquei no elemento de árvore "Used By" (Usado por). A verificação de qualquer coisa relacionada ao estado de exibição mostrou que o getter de propriedade Control.EnableViewState estava na lista. Saltar pelo método de getter EnableViewState descompilado mostrou que o método realmente retorna !this.flags[4], então o mistério foi resolvido.

O EstimateStateSize apenas cria um ObjectStateFormatter, chama o método Serialize e retorna o tamanho dos bytes de serialização, para que não haja problema em duplicá-lo. As coisas são definitivamente mais interessantes com a chamada do método this.SaveViewState protegido e eu quis chamá-lo do HttpModule. Considerando que a reflexão .NET é o ingrediente secreto para alcançar e chamar algo, usei uma porção dela para garantir que eu iria obter os mesmos números do sistema de rastreamento ASP.NET. A Figura 4 mostra o código principal que escrevi para calcular os tamanhos individuais do estado de exibição de controle.

Figure 4 Controle central do código de cálculo do tamanho do estado de exibição (Close Figure 4)
private PageViewState CreatePageViewState (
    String url ,
    DateTime timeStamp ,
    Int32 viewStateLength ,
    String httpRequestType )
{
    PageViewState returnValue = new PageViewState (
        url ,
        timeStamp ,
        viewStateLength ,
        httpRequestType );
    if ( true == collectControlData )
    {
        Page currentPage = HttpContext.Current.Handler as Page;
        if ( null != currentPage )
        {
            CalculateControlSizes ( currentPage ,
                                    0 ,
                                    returnValue.ControlList );
        }
    }
    return ( returnValue );
}

private static void CalculateControlSizes ( Control ctl ,
                                            Int32 indentLevel ,
                                            List<ControlViewState> ctlList )
{
    Int32 viewStateSize = EstimateSize ( ctl );
    ControlViewState state = new ControlViewState ( indentLevel ,
                                                ctl.UniqueID ,
                                                ctl.GetType ( ).FullName ,
                                                viewStateSize );
    // Add it to the list.
    ctlList.Add ( state );
    for ( int i = 0 ; i < ctl.Controls.Count ; i++ )
    {
        CalculateControlSizes ( ctl.Controls [ i ] , indentLevel + 1 ,
                                ctlList );
    }
}

private static Int32 EstimateSize ( Control ctl )
{
    // In Control.BuildProfileTree, the check is done against flag[4],
    // which is the same as the implementation of EnableViewState.
    if ( false == ctl.EnableViewState )
    {
        return ( 0 );
    }
    Object saved = CallSaveViewStateMethod ( ctl );
    if ( null == saved )
    {
        return ( 0 );
    }
    ObjectStateFormatter osf = new ObjectStateFormatter ( );
    Int32 ret = osf.Serialize ( saved ).Length;
    return ( ret );
}

private static Object CallSaveViewStateMethod ( Control ctl )
{
    // This is way ugly, but the SaveViewState method is protected on
    // the Control class so in order to get the total size, I need to
    // call it through Reflection.
    Object ret = null;
    BindingFlags flags = BindingFlags.Instance | BindingFlags.Public |
                            BindingFlags.NonPublic;
    Type type = ctl.GetType ( );
    MethodInfo m = type.GetMethod ( "SaveViewState" , flags );
    if ( null != m )
    {
        ret = m.Invoke ( ctl , null );
    }
    return ( ret );
}

private Int32 SumControlSizes ( Control ctl )
{
    Int32 viewStateSize = EstimateSize ( ctl );
    for ( int i = 0 ; i < ctl.Controls.Count ; i++ )
    {
        viewStateSize += SumControlSizes ( ctl.Controls [ i ] );
    }
    return ( viewStateSize );
}

Depois que descobri o truque para calcular o estado de exibição como o código de rastreamento ASP.NET, precisei começar a testar para ver se os valores do estado de exibição eram os mesmos de quando os solicitei dentro do manipulador de evento HttpModule.EndRequest. A boa notícia foi que, depois de executar todos os tipos de páginas pelo Bugslayer.Web, não vi discrepância entre o que foi reportado pelo rastreamento e o que calculei.

Se você estiver curioso sobre por que não inclui os bytes do tamanho de processamento na exibição, é porque não há como calcular estes valores. Isso ocorre durante o processamento da página e o HttpModule está muito atrasado no pipeline. Quanto aos bytes do tamanho do ControlState, escolhi não calculá-los por motivos de desempenho. A Figura 3 exibe o trabalho necessário para fazer os cálculos. No entanto, tive que usar muitas reflexões lentas para fazer estas chamadas com pouco retorno.

Os leitores com olhos atentos poderão observar que discuti como a obtenção do estado de exibição por Request.Params["__VIEWSTATE"] está acessando uma variável de formulário que não existe em uma solicitação HTTP GET. No entanto, se você olhar o código fonte de uma página no navegador, a variável __VIEWSTATE estará certamente lá, então eu quis encontrar uma maneira de obter os dados no caso de um usuário ir para uma página diretamente na barra de endereços do navegador.

Depois de muitas horas de investigação, a melhor solução que pude encontrar foi fazer os cálculos sozinho fazendo loop pelos controles e obtendo o tamanho individual do estado de exibição. Embora não seja ideal, ele se aproxima do tamanho real do estado de exibição sem entrar na codificação completa BASE64 e sem a sobrecarga adicional. Por causa da limitação do processamento HTTP GET, deixei a opção de monitoração desativada por padrão.

Idéias adicionais

No geral, fiquei muito satisfeito com o resultado da ferramenta de monitoramento do estado de exibição. Ao torná-la fácil de se integrar em um aplicativo de produção existente, meu objetivo de pesquisar estes estados de exibição enormes se tornou um pouco mais fácil de ser realizado. Quando você tem um inibidor de desempenho silencioso no estado de exibição, poder saber, facilmente, os tamanhos do estado de exibição significa que você está realizando depurações mais rapidamente e que sobrará tempo para trabalhar em novos recursos e nas partes divertidas, que afinal são o motivo de estamos nesse negócio.

Como ocorre em todas as colunas Bugslayer, sempre há recursos que podem ser adicionados ao Bugslayer.Web para rastrear ainda mais informações sobre as situações do estado de exibição.

A versão da ferramenta disponível com esta coluna apenas rastreia estados de exibição que estejam na página na forma de um campo oculto __VIEWSTATE pois este é o pior inibidor de desempenho de todos. Você também poderá estender o Bugslayer.Web para rastrear informações do estado de exibição armazenadas no servidor. Isso exigirá mais trabalho, pois você precisará fornecer uma classe Page derivada personalizada para substituir o método Page.SaveViewState.

No exemplo, o armazenamento do estado de exibição é apenas em uma base HttpApplication. Uma ótima adição seria rastrear o maior estado de exibição por um Web farm.

Como eu quis fazer a árvore de controles parecida com que o rastreamento ASP.NET exibe, estou incluindo controles que não têm dados de estado de exibição. Uma otimização rápida seria coletar apenas os controles que contribuem para o estado de exibição. Melhor ainda, torná-lo uma opção capaz de alternar no manipulador ViewStateStats.axd.

Se você for um assistente real HTML e JavaScript , você certamente tornará a exibição da página ViewStateStats.axd ainda melhor. Escolhi a forma mais simples de exibição, mas usar os controles expansível e recolhível tornaria mais fácil a exibição de dados extensos do site.

Finalmente, uma observação pessoal: eu gostaria muito de agradecer todos os leitores por acompanharem 10 anos de colunas Bugslayer. Todas as suas dúvidas e comentários ao longo dos anos fizeram meu trabalho de escrever essa coluna muito mais divertido do que vocês possam imaginar!

Dica 80 Recentemente, eu estava trabalhando em um problema em que um pool de aplicativos no IIS estava se reciclando inesperadamente. Em minha busca desesperada na Internet, encontrei um ótimo artigo no blog de Johan Straarup, "Common reasons why your application pool may unexpectedly recycle" (Razões comuns pelas quais seu pool de aplicativos poderá se reciclar inesperadamente) em blogs.msdn.com/johan/archive/2007/05/16/common-reasons-why-your-application-pool-may-unexpectedly-recycle.aspx. Trata-se de um excelente resumo de todos os motivos que você esteja imaginando sobre quando W3WP.EXE saiu para dar uma volta no parque. O blog de Johan é um no qual você precisa se inscrever.

Dica 81 Você deseja se tornar um depurador melhor? Roberto Farah, o maior especialista em WinDBG, reuniu uma de suas melhores listas de livros de depuração que já vi em blogs.msdn.com/debuggingtoolbox/archive/2007/06/08/recommended-books-how-to-acquire-or-improve-debugging-skills.aspx. Não importa o tipo de desenvolvimento que você esteja fazendo no Windows, Roberto tem as dicas para você. Em seu blog, Roberto está fazendo coisas com o script WinDBG que o surpreenderão completamente! Meu favorito é seu script WinDBG para trapacear no Campo minado (blogs.msdn.com/debuggingtoolbox/archive/2007/03/28/windbg-script-playing-with-minesweeper.aspx). Agora eu ganho sempre!

Dica 82 Se você leu as colunas Bugslayer anteriores, você sabe que sou louco por tarefas do MSBuild (msdn.microsoft.com/msdnmag/issues/06/03/Bugslayer). Você pode automatizar o mundo pelo MSBuild. As tarefas SDC foram atualizadas para incluir mais de 300 tarefas, incluindo a criação de sites, pools de aplicativos e usuários do Active Directory, execução de FxCop, configuração de servidores virtuais, criação de arquivos zip, configuração de COM+, criação de compartilhamentos de pasta, instalação no GAC, configuração do SQL Server e configuração do BizTalk® Server 2004 e BizTalk Server 2006. Para controlar a vida como você a conhece, a partir de um build automatizado, baixe as novas tarefas em codeplex.com/sdctasks.

Envie suas dúvidas e seus comentários para John em slayer@microsoft.com.

John Robbins é co-fundador da Wintellect, uma empresa de consultoria, treinamento e desenvolvimento de software especializada nas plataformas .NET e Windows. Seu livro mais recente é Debugging Microsoft .NET 2.0 Applications (Microsoft Press, 2006). Você pode entrar em contato com John em wintellect.com.


Assuntos concorrentes: APM simplificada com C#

Assuntos concorrentes
APM simplificada com C#
Jeffrey Richter

Conteúdo
Métodos anônimos e expressões lambda
A instrução foreach
Iteradores
Programação assíncrona

T
enho investigado as vantagens do APM (modelo de programação assíncrona), enfatizando que a execução de operações ligadas à E/S assincronicamente é a chave para produzir aplicativos e componentes escalonáveis e responsivos. Atingir essas metas é possível, pois o APM permite que você use um número muito pequeno de threads para executar uma grande quantidade de trabalho sem bloquear qualquer um dos threads. Infelizmente, o uso do APM para criar um aplicativo ou componente característico é difícil. Conseqüentemente, muitos programadores simplesmente não o fazem.

Existem vários itens que dificultam a implementação do APM. Primeiro, você precisa evitar ter dados de estado residindo na pilha do thread, pois os dados na pilha do thread não podem migrar para outro thread. Evitar dados com base na pilha significa ter que evitar os argumentos de método e variáveis locais. Ao longo dos anos, os desenvolvedores passaram a adorar argumentos e variáveis locais, pois eles facilitam muito a programação.

Segundo, você precisa dividir seu código em métodos de retorno de chamada, às vezes chamados de continuações. Por exemplo, em um método você inicia uma operação de leitura ou gravação assíncrona e, então precisa implementar outro método que será chamado (possivelmente por um thread diferente) para processar o resultado da operação de E/S. Os programadores não estão acostumados a pensar sobre o processamento de dados dessa forma. Migrar o estado em vários métodos e threads significa que o estado deve ser empacotado, o que acaba complicando a implementação.

Terceiro, há muitas construções de programação úteis – como instruções try/catch/finally, a instrução lock, a instrução using e loops de evento (instruções for, while e foreach) – que não podem ser divididas em vários métodos. Evitar essas construções também complica a implementação.

Finalmente, tentar oferecer recursos, como coordenar o resultado de várias operações de sobreposição, cancelamento, tempo limite e empacotamento de modificações de controle da interface do usuário para o thread de GUI em um aplicativo do Windows® Forms ou Windows Presentation Foundation (WPF), acrescenta ainda mais complexidade ao uso de APM.

Nesta coluna, demonstrarei alguns acréscimos relativamente recentes à linguagem de programação C# que podem ser usados para facilitar significativamente o funcionamento do APM. Então, introduzirei a minha própria classe, denominada AsyncEnumerator, com base nesses recursos da linguagem C#, a fim de solucionar os problemas que acabei de mencionar. Minha classe AsyncEnumerator torna divertido e fácil o uso do APM no seu código. Com essa classe, você não terá motivos para não usar a programação assíncrona, permitindo que o seu código seja escalonável e responsivo. Observe que a classe AsyncEnumerator é parte da minha biblioteca Power Threading e depende de outro código, que é parte dessa biblioteca; a biblioteca pode ser obtida por download de Wintellect.com.

Métodos anônimos e expressões lambda

Na Figura 1, o método SynchronousPattern mostra como abrir um arquivo e fazer a leitura a partir dele sincronicamente. O método é simples; ele constrói um objeto FileStream, aloca Byte[], chama Read e, então, processa os dados retornados. O C# usando a instrução garante que o objeto FileStream será fechado depois que o processamento dos dados for concluído.

Figure 1 Trechos da classe ApmPatterns (Close Figure 1)
private static void SynchronousPattern() {
    using (FileStream fs =
        new FileStream(typeof(ApmPatterns).Assembly.Location,
        FileMode.Open, FileAccess.Read, FileShare.Read, 8192,
        FileOptions.Asynchronous)) {
    Byte[] data = new Byte[fs.Length];
    Int32 bytesRead = fs.Read(data, 0, data.Length);
    ProcessData(data);
    }
}

#region APM Pattern with Multiple Methods
private sealed class ApmData {
    public FileStream m_fs;
    public Byte[] m_data;
}

private static void ApmPatternWithMultipleMethods() {
    ApmData apmData = new ApmData();
    apmData.m_fs =
        new FileStream(typeof(ApmPatterns).Assembly.Location,
        FileMode.Open, FileAccess.Read, FileShare.Read, 8192,
        FileOptions.Asynchronous);
    apmData.m_data = new Byte[apmData.m_fs.Length];
    apmData.m_fs.BeginRead(apmData.m_data, 0, apmData.m_data.Length,
        ReadCompleted, apmData);
    // Method returns after issuing async I/O request
}

private static void ReadCompleted(IAsyncResult result) {
    ApmData apmData = (ApmData) result.AsyncState;
    Int32 bytesRead = apmData.m_fs.EndRead(result);
    ProcessData(apmData.m_data);
    apmData.m_fs.Close(); // No 'using'
}
#endregion

private static void ApmPatternWithAnonymousMethod() {
    FileStream fs =
        new FileStream(typeof(ApmPatterns).Assembly.Location,
        FileMode.Open, FileAccess.Read, FileShare.Read, 8192,
        FileOptions.Asynchronous);
    Byte[] data = new Byte[fs.Length];
    fs.BeginRead(data, 0, data.Length, delegate(IAsyncResult result) {
        Int32 bytesRead = fs.EndRead(result);
        ProcessData(data);
        fs.Close(); // No 'using'
    }, null);
    // Method returns after issuing async I/O request
}

private static void ApmPatternWithLambdaExpression() {
    FileStream fs =
        new FileStream(typeof(ApmPatterns).Assembly.Location,
        FileMode.Open, FileAccess.Read, FileShare.Read, 8192,
        FileOptions.Asynchronous);
    Byte[] data = new Byte[fs.Length];
    fs.BeginRead(data, 0, data.Length, result => {
        Int32 bytesRead = fs.EndRead(result);
        ProcessData(data);
        fs.Close(); // No 'using'
    }, null);
    // Method returns after issuing async I/O request
}

O método ApmPatternWithMultipleMethods mostra como executar a mesma operação como o método SynchronousPattern usando o APM do CLR (common language runtime). Imediatamente, você verá como essa implementação é muito mais complicada. Observe que o método ApmPatternWithMultipleMethods inicia a operação assíncrona de E/S e o método ReadCompleted é chamado quando a operação é concluída. Observe também que a passagem dos dados entre os dois métodos é realizada pelo empacotamento dos dados compartilhados em uma instância de classe ApmData, que precisei definir especificamente para ativar a passagem desses dados entre esses dois métodos. Você também deve observar que o C# usando a instrução não pode ser usado, pois FileStream é aberto em um método e, então, fechado em outro método. Para compensar isso, precisei escrever código para chamar explicitamente o método Close do FileStream logo antes de o método ReadCompleted retornar.

O método ApmPatternWithAnonymousMethod demonstra como esse código poderia ser reescrito usando um novo recurso do C# 2.0 denominado métodos assíncronos, o que permite que você passe o código como um argumento para um método. Ele permite efetivamente que você incorpore o código de um método no código de outro método. (Descrevo métodos anônimos detalhadamente no meu livro CLR via C#, Microsoft Press, 2006.) Observe que o método ApmPatternWithAnonymousMethod é muito mais breve e de compreensão muito mais fácil – depois que você se acostuma a trabalhar com métodos anônimos.

Primeiro, observe que o código é mais simples devido ao fato de tudo estar contido em um apenas um método. Nesse código, estou chamando o método BeginRead para iniciar uma operação assíncrona de E/S. Todos os métodos BeginXxx utilizam para seu penúltimo parâmetro um AsyncCallback, que é um delegado referindo-se a um método que deveria ser chamado por um thread do pool de threads quando a operação fosse concluída. Geralmente, ao usar o APM, você precisa escrever um método separado, nomeá-lo e passar dados adicionais para o método por meio do último argumento do método BeginXxx. No entanto, o recurso do método anônimo permite que eu escreva apenas o método separado embutido, de forma que todo o código para iniciar a solicitação e processar o resultado está no contexto. Na verdade, o código é de alguma forma semelhante ao método SynchronousPattern.

Segundo, observe que a classe ApmData não é mais necessária; você não precisa defini-la, construir uma instância dela e trabalhar com qualquer um de seus campos! Como isso é possível? Bem, os métodos anônimos fazem mais do que apenas permitir que você incorpore o código de um método no código de outro método. Quando o compilador C# detecta quaisquer argumentos ou variáveis locais declarados no método externo que também são usados no método interno, o compilador na verdade define uma classe automaticamente para você, e cada variável armazenada entre os dois métodos se torna um campo nessa classe definida pelo compilador. Então, dentro do método ApmPatternWithAnonymousMethod, o compilador gera código para construir uma instância dessa classe e qualquer código que se refira às variáveis é compilado no código que acessa os campos da classe definida pelo compilador. O compilador também torna o método interno um método de instância na nova classe, o que permite que seu código acesse os campos facilmente e agora os dois métodos podem compartilhar os dados.

Esse é um recurso excelente de métodos anônimos, pois permite que você escreva código que pareça que você está usando argumentos de método e variáveis locais, mas o compilador está na verdade reescrevendo o código, retirando essas variáveis da pilha e as incorporando como campos em um objeto. O objeto pode ser facilmente passado entre os métodos e pode facilmente migrar de um thread para outro, o que é perfeito ao usar o APM. E, como o compilador está fazendo todo esse trabalho automaticamente, você pode simplesmente passar nulo no último parâmetro para o método BeginRead, pois agora não há dados que precisem ser passados explicitamente entre os métodos e threads. No entanto, eu ainda não posso usar o C# usando a instrução, pois ainda há dois métodos diferentes aqui, embora pareça apenas um método.

Veja a seguir o resultado da execução do código representado na Figura 1:

Primary ThreadId=1
ThreadId=1: 4D-5A-90-00-03 (SynchronousPattern)
ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithMultipleMethods)
ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithAnonymousMethod)
ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithLambdaExpression)
ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithIterator)

Fiz com que o método Main exibisse a Id do thread gerenciado no thread principal do aplicativo. Então, fiz com que o método ProcessData exibisse a Id do thread gerenciado no thread que o está executando. Como você pode ver, o resultado mostra que todos os padrões assíncronos têm os resultados executados por um thread que não é o principal, enquanto o padrão assíncrono tem tudo executando através do thread principal do aplicativo.

Também devo indicar que o C# 3.0 introduz um novo recurso denominado expressões lambda. O recurso de expressão lambda é uma sintaxe mais sucinta para executar a mesma ação que o recurso do método anônimo do C#. Na verdade, isso é um problema para a equipe de C#, pois agora ela precisa documentar e oferecer suporte a duas sintaxes diferentes que produzam resultados idênticos. A modificação do método ApmPatternWithAnonymousMethod para usar o recurso de expressão lambda do C# 3.0 lhe fornece o método ApmPatternWithLambdaExpression mostrado na Figura 1. Aqui, você vê que a sintaxe é levemente simplificada, pois o compilador pode inferir automaticamente o tipo do argumento de resultado como IAsyncResult e "=>" tem menos tipificação do que "delegate".

A instrução foreach

O C# 2.0 introduz outro recurso na linguagem de programação C#: iteradores. A intenção original do recurso iterador foi facilitar para os desenvolvedores a escrita de código para percorrer uma estrutura de dados interna da coleção. Para compreender os iteradores, você deve primeiro verificar bem a instrução foreach do C#. Quando você escrever código como o apresentado a seguir, o compilador o transformará no que você vê na Figura 2:

Figure 2 Loop foreach no C# (Close Figure 2)
// Construct an object that knows how to traverse the
// internal data structure of the collection
IEnumerator<String> e = collectionObject.GetEnumerator();
try {
   String s;
   while (e.MoveNext()) { // Advance to the next item
      s = e.Current;      // Get the item to process
      DoSomething(s);
   }
}
finally {
   // If the IEnumerable implements IDisposable,
   // clean up the object
   IDisposable d = e as IDisposable;
   if (d != null) d.Dispose();
}

foreach (String s in collectionObject)
   DoSomething(s);

Você vê que a instrução foreach lhe fornece uma forma fácil de iterar em todos os itens em uma classe de coleção. No entanto, há vários tipos diferentes de classes de coleção – matrizes, listas, dicionários, árvores, listas associadas e assim por diante – e cada uma usa sua própria estrutura de dados interna para a representação da coleção de itens. Quando você usa a instrução foreach, está afirmando que deseja iterar todos os itens na coleção e não se preocupa com qual estrutura de dados a classe de coleção utilizará internamente para manter seus itens.

A instrução foreach faz com que o compilador gere uma chamada para o método GetEnumerator da classe de coleção. Esse método cria um objeto que implementa a interface IEnumerator<T>. Esse objeto sabe como percorrer a estrutura de dados interna da coleção. Cada iteração do loop while chama o método MoveNext do objeto IEnumerator<T>. Esse método informa ao objeto para avançar para o próximo item na coleção e retorna true se tiver êxito ou false se todos os itens tiverem sido enumerados. Internamente, MoveNext pode apenas incrementar um índice ou pode avançá-lo para o próximo nó de uma lista vinculada ou pode percorrer uma árvore acima ou abaixo. A idéia por trás do método MoveNext é abstrair as estruturas de dados interna da coleção do código que está executando o loop foreach.

Se MoveNext retornar true, então a chamada para o método do acessador get da propriedade Current retorna o valor do item que o método MoveNext acabou de avançar, de forma que o corpo da instrução foreach (o código no bloco try) possa processar o item. Quando MoveNext determinar que todos os itens da coleção foram processados, ele retornará false.

Nesse ponto, o loop while sai e o bloco finally é inserido. No bloco finally, é feita uma verificação para determinar se o objeto IEnumerator<T> implementa a interface IDisposable e, em caso positivo, seu método Dispose é chamado. Para o registro, a interface IEnumerator<T> é derivada de IDisposable, requerendo portanto que todos os objetos IEnumerator<T> implementem o método Dispose. Alguns objetos do enumerador requerem recursos adicionais durante a iteração. Talvez o objeto retorne linhas de texto de um arquivo de texto. Quando o loop foreach sair (o que poderia acontecer antes da iteração de todos os itens na coleção), então o bloco finally chama Dispose, permitindo que o objeto IEnumerator<T> libere esses recursos adicionais – fechando o arquivo de texto, por exemplo.

Iteradores

Agora que você pode ver que código a instrução foreach produz, veremos como um objeto implementa a interface IEnumerable<T> junto com seus membros MoveNext e Current. Vamos falar sobre matrizes, pois elas são provavelmente a forma mais simples de todas as estruturas de dados de coleção. A classe System.Array, que é a classe base de todas as matrizes, implementa a interface IEnumerable<T>. Seu método GetEnumerator retorna um objeto cujo tipo implementa a interface IEnumerator<T>. Portanto, você pode escrever facilmente código para iterar através de todos os elementos de uma matriz:

Int32[] numbers = new Int32[] { 1, 2, 3 };
foreach (Int32 num in numbers)
   Console.WriteLine(num);

Como uma observação adicional, se o compilador C# detectar que a coleção usada com foreach era uma matriz, o compilador produzirá código otimizado que tem um índice simples que avança de 0 para a extensão da matriz menos um. Mas, para essa discussão, vamos ignorar que o compilador executa essa otimização e fingir que esse compilador trata as matrizes da mesma forma como qualquer outra classe de coleção. Na verdade, você pode forçar o compilador para que trate uma matriz da mesma forma como qualquer outra classe de coleção ao convertê-la em IEnumerable<T> como a seguir:

foreach (Int32 num in (IEnumerable<Int32>) numbers)
   Console.WriteLine(num);

Minha classe ApmPatterns inclui uma classe ArrayEnumerator<T> que sabe como retornar cada um dos itens em uma matriz na ordem inversa (veja a Figura 3). Essa classe implementa a interface IEnumerator<T>, que requer que a classe também implemente as interfaces não genéricas IEnumerable e IDisposable. A classe AsyncEnumerator<T> oferece um construtor público que salva a matriz passada em um membro particular. Há outro membro particular, m_index, que indica qual item da matriz você está atualmente iterando; esse campo é inicializado para a extensão da matriz (o elemento anterior ao último elemento na matriz). Você pode ver isso na Figura 3.

Figure 3 Iteradores (Close Figure 3)
private static void Iterators() {
    Int32[] numbers = new Int32[] { 1, 2, 3 };

    // Use Array's built-in IEnumerable
    foreach (Int32 num in numbers)
    Console.WriteLine(num);

    // Use my ArrayEnumerable class
    foreach (Int32 num in new ArrayEnumerable<Int32>(numbers))
    Console.WriteLine(num);

    // Use my iterator
    foreach (Int32 num in ArrayIterator(numbers))
    Console.WriteLine(num);
}

private sealed class ArrayEnumerator<T>: IEnumerator<T> {
    private T[] m_array;
    private Int32 m_index;

    public ArrayEnumerator(T[] array) {
        m_array = array; m_index = array.Length;
    }
    public Boolean MoveNext() {
        if (m_index == 0) return false;
        m_index--; return true;
    }
    public T Current { get { return m_array[m_index]; } }
    Object IEnumerator.Current { get { return Current; } }
    public void Dispose() { }
    public void Reset() { }
}

private sealed class ArrayEnumerable<T>: IEnumerable<T> {
    private T[] m_array;
    public ArrayEnumerable(T[] array) { m_array = array; }
    public IEnumerator<T> GetEnumerator() {
        return new ArrayEnumerator<T>(m_array);
    }
    IEnumerator IEnumerable.GetEnumerator() {
        return GetEnumerator();
    }
}

private static IEnumerable<T> ArrayIterator<T>(T[] array) {
    try { yield return default(T); }
    finally {  }
    for (Int32 index = array.Length - 1; index >= 0; index--)
    yield return array[index];
}

private static IEnumerator<Int32>
    ApmPatternWithIterator(AsyncEnumerator ae) {

    using (FileStream fs =
        new FileStream(typeof(ApmPatterns).Assembly.Location,
        FileMode.Open, FileAccess.Read, FileShare.Read, 8192,
        FileOptions.Asynchronous)) {

        Byte[] data = new Byte[fs.Length];
        fs.BeginRead(data, 0, data.Length, ae.End(), null);
        yield return 1;

        Int32 bytesRead = fs.EndRead(ae.DequeueAsyncResult());
        ProcessData(data);
    }
}

Todos os objetos IEnumerator<T> são essencialmente máquinas de estado que controlam de qual item elas estão enumerando a partir da coleção. Quando o loop foreach chamar MoveNext, o método MoveNext avançará a máquina de estado para o próximo item. Na minha classe ArrayEnumerator, MoveNext retornará false se todos os itens tiverem sido enumerados (m_index já é 0, o último item). Se esse não for o caso, ele reduzirá o campo m_index e retornará true. A propriedade Current simplesmente retornará o item refletido pela posição atual da máquina do estado.

Como você pode ver, a implementação do código é maçante e deixei alguns métodos da interface vazios, a fim de simplificar a codificação. Também deixei algum código de verificação de erros. Além disso, uma matriz é uma estrutura de dados bem simples e, portanto, escrever um enumerador para ela é bem fácil. Outras estruturas de dados, como uma lista vinculada ou árvore, complicariam significativamente o código.

Também devo salientar que você não pode usar minha classe ArrayEnumerator<T> diretamente com a instrução foreach. Em outras palavras, simplesmente não compilaria:

foreach (Int32 num in new ArrayEnumerator<Int32>(numbers))
   Console.WriteLine(num);

A instrução foreach espera uma coleção (um objeto cujo tipo implementa IEnumerable<T>) depois da palavra-chave in, não IEnumerator<T>. Portanto, para usar minha classe ArrayEnumerator<T>, precisei definir também uma classe que implementasse IEnumerable<T>, minha classe ArrayEnumerator<T> também mostrada na Figura 3. Agora, com a classe ArrayEnumerable<T> definida, posso escrever o código a seguir, o qual será compilado e executado muito bem:

foreach (Int32 num in new ArrayEnumerable<Int32>(numbers))
   Console.WriteLine(num);

Uau! Essa é uma quantidade considerável de código a ser escrita para iterar por meio de uma coleção de itens. Quem dera tivéssemos uma tecnologia disponível que tornasse todo esse código mais simples de escrever. Felizmente, há – o recurso iterador C# 2.0. Um iterador permite que você escreva um membro único que retornará uma seqüência ordenada de valores. Você pode escrever isso usando uma sintaxe simples para expressar como os valores serão retornados e o compilador C# transformará seu bloco de código em uma classe completa que implementará para você as interfaces IEnumerable<T>, IEnumerable, IEnumerator<T>, IEnumerator e IDisposable.

Usando o recurso iterador C#, substitui minha classe AsyncEnumerable<T> e as classes AsyncEnumerator<T> com o seguinte membro único:

private static IEnumerable<T> ArrayIterator<T>(T[] array) {
   for (Int32 index = array.Length-1; index >= 0; index--)
      yield return array[index];
}

Além disso, posso invocar esse membro com uma instrução foreach da seguinte forma:

foreach (Int32 item in ArrayIterator(numbers))
   Console.WriteLine(item);

Como um bônus, como esse é um método genético (versus uma classe genérica), a inferência do tipo C# é acionada e não preciso escrever o ArrayIterator<Int32>, tornando o código foreach mais simples também.

Observe que, como meu membro iterador retorna IEnumerable<T>, o compilador gera código que implementa IEnumerable<T> e IEnumerable, bem como IEnumerator<T>, IEnumerator e IDisposable. No entanto, você pode escrever um membro iterador que retornará um IEnumerator<T>, sendo que nesse caso o compilador gera uma definição de classe que implementa somente os membros de interface IEnumerator<T>, IEnumerator e IDisposable.

Também salientarei que você pode definir um membro iterador com um tipo de retorno que seja a interface não-genérica IEnumerable ou IEnumerator, então o compilador definirá IEnumerable<Object>/ IEnumerable (se você tiver especificado IEnumerable como o tipo de retorno), IEnumerator<Object>/IEnumerator e IDisposable.

Em um iterador, a instrução yield return efetivamente instrui o compilador qual valor (item de coleção) retornar do método. No entanto, um iterador na verdade não encerra sua execução nesse ponto; em vez disso, digamos que sua execução é suspensa. Na próxima vez que MoveNext for chamado, o iterador retomará sua execução com a instrução imediatamente posterior à instrução yield return. Além de yield return, um iterador pode conter uma instrução yield break, que, quando executada, faz com que o método MoveNext retorne false, forçando o encerramento do loop foreach. A saída de um membro iterador também faz com que false seja retornado do método MoveNext, forçando o encerramento do loop foreach.

Programação assíncrona

Para programação assíncrona, os iteradores lhe proporcionam alguns benefícios realmente excelentes. Primeiro, em um iterador, você pode escrever código que acesse argumentos e variáveis locais. No entanto, o compilador está na verdade empacotando essas variáveis em campos de classe para você (da mesma forma como faria com métodos anônimos e expressões lambda).

Segundo, o compilador permite que você escreva código seqüencial, mas você tem a capacidade de suspender o fluxo intermediário do método e continuar sua execução posteriormente (usando potencialmente um thread diferente).

Terceiro, você pode usar construções de programação – como instruções try/catch/finally, instruções lock, instruções using e loops de evento (instruções for, while e foreach) – em um iterador. O compilador reescreverá automaticamente seu código de forma que essas construções mantenham sua semântica.

Para o registro, há limitações associadas aos iteradores:

  • Uma assinatura do membro do iterador não pode conter parâmetros out ou ref.
  • Um iterador não pode conter uma instrução return (yield return está OK).
  • Um iterador pode não conter código não seguro.
  • Um bloco finally pode não conter uma instrução yield return ou yield break.
  • Uma instrução yield return pode não aparecer em um bloco try que tenha um bloco catch (yield break está OK).
  • Uma instrução yield return pode não aparecer em um bloco catch (yield break está OK).

Embora seja bom considerar essas limitações, para a maioria dos cenários de programação, elas não são um problema ou podem ser solucionadas.

Portanto, agora, armado com esse conhecimento de iteradores, você pode criar um iterador que execute uma série de operações assíncronas. Na Figura 3, consulte o membro ApmPatternWithIterator. Esse membro é um iterador, pois o tipo de retorno do membro é um IEnumerator<T>. Observe o quão semelhante o código nesse membro é ao método SynchronousPattern abordado anteriormente. Especificamente, observe como o iterador contém uma instrução using e como ele usa um argumento (ae) e variáveis locais (fs e dados).

Você pode informar a esse membro que execute operações assíncronas, pois ele chama BeginRead e EndRead (em vez de chamar apenas Read). Mas ao usar o APM, você geralmente chama BeginRead e passa um método de retorno de chamada. Nesse caso, estou passando o resultado de chamar ae.End (explicarei isso na minha próxima coluna). Geralmente, o método retornaria depois da chamada de BeginRead, mas em um iterador que estou chamando yield return, que apenas suspende o código.

Em geral, ao usar o APM, a chamada para EndRead seria em um método diferente, mas com os iteradores a chamada para EndRead é imediatamente seguida da instrução yield return. Isso significa que ela será executada quando a execução do iterador for retomada. Além disso, outro thread pode executar esse código. Na verdade, se você verificar o resultado mostrado anteriormente, verá que a chamada do ApmPatternWithIterator para ProcessData é executada por meio de outro thread (ThreadId=3) que não o thread principal do aplicativo (ThreadId=1).

Se você realmente estudar esse código com atenção, verá que o que está acontecendo aqui é verdadeiramente excepcional e torna a programação assíncrona quase tão simples quanto executar a programação síncrona. Mas agora os seus threads nunca serão bloqueados, permitindo que o seu aplicativo permaneça responsivo ao usuário. Como os threads não são bloqueados, não há necessidade de criar mais threads, de forma que o aplicativo usa muito poucos recursos.

Você pode ter vários iteradores executando simultaneamente, o que aumenta a escalabilidade do seu aplicativo. Além disso, você pode adicionar facilmente o recurso para cancelar operações pendentes, relatar o andamento, definir timers, coordenar o resultado de várias operações sobrepostas e ter tudo isso em execução no thread de GUI se desejar atualizar a interface do usuário. Você também pode ter tudo isso com um modelo de programação muito semelhante ao modelo de programação síncrona mais conveniente.

O que torna tudo isso possível de fato são os iteradores C# e uma classe AsyncEnumerator que desenvolvi para acionar esses iteradores. Felizmente, me empolguei tanto com isso que você deve estar curioso para ver minha classe AsyncEnumerator e como ela funciona. Infelizmente, já gastei o meu espaço na coluna deste mês. Portanto, você terá que aguardar a minha próxima coluna, na qual explicarei a minha classe AsyncEnumerator em detalhes.

Envie suas dúvidas e comentários a Jeffrey para mmsync@microsoft.com.

Jeffrey Richter é co-fundador da Wintellect (www.Wintellect.com), uma empresa de análise, consultoria e treinamento em arquiteturas. Ele é autor de vários livros, incluindo CLR via C# (Microsoft Press, 2006). Jeffrey também contribui como editor para a revista MSDN Magazine e é consultor da Microsoft desde 1990.


.Assuntos .NET: Depura&#231;&#227;o dos finalizadores

.Assuntos .NET
Depuração dos finalizadores
Stephen Toub

P Tenho uma série de tipos personalizados que, por um motivo ou outro, precisam implementar IDisposable. Quero me certificar de que outros desenvolvedores na minha equipe usando essa classe sempre descartarão os meus tipos corretamente. O que posso fazer para alertar um dos meus colegas se ele esquecer de chamar Dispose?

R Para iniciantes, o Visual Studio® 2005 e o FxCop podem ajudar com isso se você executar uma análise estática no seu código. A regra CA2000, descrita em msdn2.microsoft.com/ms182289, é "Dispose objects before losing scope" (Descartar objetos antes de perder escopo), que verifica se há qualquer objeto IDisposable local criado e então não descartado antes de todas as referências ao objeto estarem fora do escopo. Embora útil, não é a solução perfeita, pois há somente muito pouco que pode ser detectado por meio da análise estática. O que você realmente precisa é de uma forma de alertar os desenvolvedores usando seus tipos quando esses tipos forem coletados pelo lixo sem precisarem ser descartados e para isso você precisa aproveitar os finalizadores.

Em geral, você deve evitar implementar os finalizadores em seus tipos, a não ser que seja absolutamente necessário. E com os SafeHandles no Microsoft® .NET Framework 2.0, há alguns motivos pelos quais é absolutamente necessário. No entanto, há muitos cenários nos quais você deve implementar IDisposable, incluindo qualquer cenário em que o seu tipo tenha um recurso gerenciado que ele próprio implemente IDisposable; em tal cenário, o seu tipo deve fornecer um método Dispose que, por sua vez, chama Dispose no recurso contido, como mostrado na Figura 1. (Para obter detalhes sobre a implementação de IDisposable, consulte a coluna Tudo sobre o CLR, de Shawn Farkas, na edição de julho de 2007 da MSDN Magazine em msdn.microsoft.com/msdnmag/issues/07/07/CLRInsideOut.)

Figure 1 Descartando recursos gerenciados contidos (Close Figure 1)
public class DisposableClass : IDisposable
{
    private SomeOtherDisposableClass _data = ...;

    public DisposableClass()
    {
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // if DisposableClass isn't sealed
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _data.Dispose();
        }
    }

    ...
}
Figure 1 Descartando recursos gerenciados contidos (Close Figure 1)
public class DisposableClass : IDisposable
{
    private SomeOtherDisposableClass _data = ...;

    public DisposableClass()
    {
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // if DisposableClass isn't sealed
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _data.Dispose();
        }
    }

    ...
}

Na Figura 1, DisposableClass não usa diretamente quaisquer recursos nativos e, portanto, não precisa fornecer um finalizador (seu método Dispose ainda chama GC.SuppressFinalize no caso de um tipo derivado implementar um finalizador). No entanto, ao adicionar um finalizador para fins de depuração, você pode introduzir uma forma de detectar quando uma classe não for corretamente descartada, como mostrado na Figura 2. Desde que todas as instâncias do tipo sejam descartadas corretamente, o finalizador nunca será chamado; se, no entanto, qualquer instância não for descartada, o thread do finalizador chamará seu método Finalizer (~DisposableClass) depois que for detectado que essa instância não é mais referenciada, o que, por sua vez, causará uma declaração no depurador.

Figure 2 Adicionando um finalizador para depuração (Close Figure 2)
public class DisposableClass : IDisposable
{
    private SomeOtherDisposableClass _data = ...;

    public DisposableClass()
    {
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            data.Dispose();
        }
    }

#if DEBUG
    ~DisposableClass()
    {
        Debug.Fail("You forgot to Dispose this instance.");
    }
#endif

    ...
}
Figure 2 Adicionando um finalizador para depuração (Close Figure 2)
public class DisposableClass : IDisposable
{
    private SomeOtherDisposableClass _data = ...;

    public DisposableClass()
    {
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            data.Dispose();
        }
    }

#if DEBUG
    ~DisposableClass()
    {
        Debug.Fail("You forgot to Dispose this instance.");
    }
#endif

    ...
}

Infelizmente, embora o desenvolvedor usando o seu tipo agora saberá que não descartou uma instância e saberá qual instância, ele pode não saber qual a origem dela. Se os seus tipos forem instanciados somente em um local, isso pode ser suficiente. Mas se os seus tipos estiverem sendo instanciados de toda a base do código, os desenvolvedores não terão como saber a origem dessa instância específica, tornando mais difícil para eles saber como essa instância falhou.

Para solucionar isso, você pode coletar informações adicionais para sua classe no momento da construção. Por exemplo, você poderia adicionar os seguintes membros para controlar o rastreamento de pilha, thread e o momento no qual e a partir do qual essa instância foi criada:

#if DEBUG
    private StackTrace _stack = new StackTrace(true);
    private int _threadId = Thread.CurrentThread.ManagedThreadId;
    private DateTime _time = DateTime.UtcNow;
    ...
#endif

Você também poderia modificar o Debug.Fail para incluir informações desses membros quando o finalizador for executado, fornecendo ao usuário uma chance melhor de localizar exatamente de onde essa instância surgiu e rastrear o motivo pelo qual ela não foi descartada corretamente. Observe que as diretivas pragma no trecho de código anterior (#if/#endif) são usadas para garantir que todo esse código será compilado somente em builds DEBUG, em vez de builds RELEASE. A recuperação de rastreamentos de pilha é uma operação relativamente cara e é algo que você deve evitar sempre que possível se o desempenho estiver envolvido.

Além disso, você observará que na Figura 2 tratei o finalizador da mesma forma. Isso é principalmente uma questão de estilo. Os finalizadores acrescentam sobrecarga ao sistema, mesmo se forem suprimidos. Portanto, não há vantagem em ter um em sua classe se não for necessário para a correta funcionalidade. (Quando alocados, os objetos finalizados são adicionados a uma lista de finalização. Quando essas instâncias não estiverem mais acessíveis e o GC for executado, elas serão movidas para a fila "FReachable", que é processada pelo thread finalizador. A supressão da finalização com GC.SuppressFinalize define um sinalizador "do not run my finalizer" (não executar meu finalizador) no cabeçalho do objeto, sendo que esse objeto não será movido para a fila FReachable pelo GC. Como resultado, apesar de mínimo, ainda haverá sobrecarga ao fornecer um objeto ao finalizador mesmo se o finalizador estiver vazio ou suprimido.)

Isso funciona bem para um tipo individual, mas se você estiver criando vários tipos que precisam de comportamento semelhante, a maioria das práticas de código dita que você deve fatorar esse código em uma classe reutilizável. Isso é na verdade um problema extremamente complicado e gastarei o restante dessa resposta examinando várias implementações diferentes, fornecendo os prós e contras de cada uma. Você pode selecioná-las e escolher cada implementação com base nas suas necessidades.

Algo que é comum a todas as implementações é uma necessidade de armazenar várias quantidades de informação no momento da construção. Para esse fim, escolhi criar uma classe personalizada de exceção. Essa classe de exceção, quando construída, recupera dados semelhantes aos mencionados anteriormente. Ela pode ser posteriormente lançada quando o finalizador for invocado, ou seu método ToString pode ser usado como um argumento para uma chamada para Debug.Fail ou algo semelhante. (Observe que com o .NET Framework 2.0 e posteriores, lançar uma exceção a partir de um thread do finalizador irá, por padrão, subdividir o aplicativo. Isso é diferente em relação às versões anteriores, em que as exceções do thread do finalizador seriam silenciosamente consumidas pelo tempo de execução. Escolhi não lançar a exceção, mas se você acreditar que não descartar um objeto seria um erro grave o suficiente para garantir a subdivisão do processo, sinta-se à vontade para retirar os comentários da linha relevante.) Meu InvokedFinalizerException é mostrado na Figura 3.

Figure 3 InvokedFinalizerException (Close Figure 3)
[Serializable]
public class InvokedFinalizerException : Exception
{
    public readonly DateTime InstantiationTime = DateTime.UtcNow;
    public readonly int InstantiationThreadId =
        Thread.CurrentThread.ManagedThreadId;
    public readonly StackTrace InstantiationStackTrace =
        new StackTrace(true);

    public InvokedFinalizerException() :
        base("A finalizer was invoked.") { }

    public InvokedFinalizerException(string message) : base(message) { }

    public InvokedFinalizerException(string message, Exception inner) :
        base(message, inner) { }

    protected InvokedFinalizerException(
        SerializationInfo info, StreamingContext context) :
        base(info, context) { }

    public override string ToString()
    {
        return
            "Time: " + InstantiationTime + Environment.NewLine +
            "Thread: " + InstantiationThreadId + Environment.NewLine +
            "Stack: " + InstantiationStackTrace;
    }
}
Figure 3 InvokedFinalizerException (Close Figure 3)
[Serializable]
public class InvokedFinalizerException : Exception
{
    public readonly DateTime InstantiationTime = DateTime.UtcNow;
    public readonly int InstantiationThreadId =
        Thread.CurrentThread.ManagedThreadId;
    public readonly StackTrace InstantiationStackTrace =
        new StackTrace(true);

    public InvokedFinalizerException() :
        base("A finalizer was invoked.") { }

    public InvokedFinalizerException(string message) : base(message) { }

    public InvokedFinalizerException(string message, Exception inner) :
        base(message, inner) { }

    protected InvokedFinalizerException(
        SerializationInfo info, StreamingContext context) :
        base(info, context) { }

    public override string ToString()
    {
        return
            "Time: " + InstantiationTime + Environment.NewLine +
            "Thread: " + InstantiationThreadId + Environment.NewLine +
            "Stack: " + InstantiationStackTrace;
    }
}

Com isso inserido, a primeira técnica envolve mover todo o código de controle para uma classe construível separada, FinalizationDebugger, como mostrado na Figura 4. Uma instância dessa classe é armazenada como um membro da sua classe descartável e é construída quando a sua classe for construída. Quando a sua classe for descartada, essa classe FinalizationDebugger separada também será descartada. Se a sua classe não for descartada, quando ela não referenciar mais FinalizationDebugger ela será futuramente coletada e seu finalizador fornecerá todas as informações relevantes necessárias para você detectar qual instância não foi descartada corretamente, sua origem e assim por diante. Observe que o FinalizationDebugger retém uma referência para o seu objeto, mas como seu objeto contém a referência somente para essa instância do FinalizationDebugger, sua referência para o seu objeto não impedirá que seja coletado (se o coletor de lixo do .NET Framework funcionar em um esquema de contagem de referência em vez no esquema mark-and-sweep que ele usa, esse ciclo poderia ser um problema). É claro, como você pode ver na Figura 4, usar isso na sua classe descartável ainda requer que você adicione um pouco de parafernália. Na verdade, isso não é muito melhor do que a sua versão original que estamos tentando refatorar em uma solução mais limpa.

Figure 4 Abordagem Member (Close Figure 4)

Refatorada em classes separadas

public sealed class FinalizationDebugger : IDisposable
{
    private InvokedFinalizerException _exc;
    private object _obj;

    public FinalizationDebugger(object obj)
    {
        _exc = new InvokedFinalizerException();
        _obj = obj;
    }

    public void Dispose()
    {
        GC.SuppressFinalize(this);
    }

    ~FinalizationDebugger()
    {
        Debug.Fail(_exc.ToString());
        // throw _exc;
    }
}

Usando FinalizationDebugger
public class DisposableClass : IDisposable
{
    private SomeOtherDisposableClass _data = ...;
#if DEBUG
    private FinalizationDebugger _fd;
#endif

    public DisposableClass()
    {
#if DEBUG
       _fd = new FinalizationDebugger(this);
#endif
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _data.Dispose();
#if DEBUG
            _fd.Dispose();
#endif
        }
    }
}

A próxima abordagem leva isso um passo diante ao mover todo o seu código em uma classe base da qual a sua classe descartável pode ser derivada. Essa abordagem é mostrada na Figura 5. Observe como isso torna limpa a sua classe descartável, pois as únicas alterações necessárias são adicionar FinalizationDebuggerBase como a classe base para seu tipo, excluindo seu método Dispose sem parâmetro (pois ele já foi implementado pela classe base) e alterando seu método Dispose(bool) para substituir a implementação base.

Figure 5 Abordagem de classe base (Close Figure 5)

Refatorada em classe base

public class FinalizationDebuggerBase : IDisposable
{
#if DEBUG
    private FinalizationDebugger _fd;
#endif

    public FinalizationDebuggerBase()
    {
#if DEBUG
        _fd = new FinalizationDebugger(this);
#endif
    }

    public void Dispose() { Dispose(true); }

    protected virtual void Dispose(bool disposing)
    {
#if DEBUG
        if (disposing) _fd.Dispose();
#endif
    }
}

Usando FinalizationDebuggerBase

public class DisposableClass : FinalizationDebuggerBase
{
    private SomeOtherDisposableClass _data = ...;

    public DisposableClass() {}

    protected override void Dispose(bool disposing)
    {
        try
        {
            if (disposing) _data.Dispose();
        }
        finally { base.Dispose(disposing); }
    }
}

Infelizmente, há vários problemas com essa abordagem. Primeiro, as diretivas pragma #if/#endif são avaliadas no momento da compilação quando FinalizationDebuggerBase for compilado. Se FinalizationDebuggerBase for parte do mesmo projeto do seu DisposableClass, ele sempre coletará os mesmos sinalizadores de compilação DEBUG/RELEASE da sua classe e será mantido em sincronização, o que é praticamente certo que você queira que aconteça. Mas se o FinalizationDebuggerBase estiver em outra montagem (por exemplo, se for compilado em uma montagem que é compartilhada por uma série de projetos na sua empresa), a montagem que você fizer referência já terá sido compilada, o que significa que você não respeitará a escolha DEBUG/RELEASE do seu próprio projeto. Como resultado, você deveria compilar o seu projeto no modo RELEASE e não esperar que esse código de finalização seja incluído. Mas se a classe FinalizationDebuggerBase for compilada no modo DEBUG, as suas expectativas não seriam atendidas. O segundo problema, que deveria ser mais evidente, é que o .NET não é compatível com a herança de várias classes, significando que uma classe pode ter somente uma classe base (embora ela possa implementar qualquer número de interfaces). Isso significa que se a sua classe já tiver uma classe base, você não pode usar essa abordagem (a menos que você possa de alguma forma enganar essa classe base em alguma parte da hierarquia).

Uma terceira opção envolve fatorar tudo em uma classe estática para a qual as suas instâncias chamarão. Minha implementação disso é mostrada na Figura 6. Nessa abordagem, FinalizationDebugger é uma classe estática que expõe três métodos estáticos: Constructor, Dispose e Finalizer. A idéia é que você chame esses métodos do local apropriado na sua classe, onde o local apropriado seria o evidente dos nomes desses métodos (consulte a Figura 6 para obter um exemplo). Isso é minimamente invasivo no seu código, pois geralmente envolve adicionar somente três linhas de código (embora se o seu tipo ainda não tiver um finalizador, você precisará adicionar essas linhas adicionais). Todos esses métodos são marcados com um ConditionalAttribute de forma que somente poderão ser chamados por sua classe quando você compilar a classe no modo DEBUG.

Figure 6 Abordagem de classe estática (Close Figure 6)

Refatorada em classe estática

public static class FinalizationDebugger<T> where T : class
{
    private static Dictionary<object, InvokedFinalizerException> _db =
        new Dictionary<object, InvokedFinalizerException>();

    [Conditional("DEBUG")]
    public static void Constructor(T obj)
    {
        if (obj == null) throw new ArgumentNullException("obj");
        ObjectEqualityWeakReference weakRef =
            new ObjectEqualityWeakReference(obj);
        InvokedFinalizerException exc = new InvokedFinalizerException();
        lock (_db) _db.Add(weakRef, exc);
    }

    [Conditional("DEBUG")]
    public static void Dispose(T obj)
    {
        if (obj == null) throw new ArgumentNullException("obj");
        lock (_db) _db.Remove(obj);
    }

    [Conditional("DEBUG")]
    public static void Finalizer(T obj)
    {
        if (obj == null) throw new ArgumentNullException("obj");
        InvokedFinalizerException exc;
        lock (_db)
        {
            if (_db.TryGetValue(obj, out exc)) _db.Remove(obj);
        }
        if (exc != null)
        {
            Debug.Fail(exc.ToString());
            // throw exc;
        }
    }
    ... // private class ObjectEqualityWeakReference
}

Usando FinalizationDebugger<T>

public class DisposableClass : IDisposable
{
    private SomeOtherDisposableClass _data = ...;

    public DisposableClass()
    {
        ...
        FinalizationDebugger<DisposableClass>.Constructor(this);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            FinalizationDebugger<DisposableClass>.Dispose(this);
            _data.Dispose();
            ...
        }
    }

    ~DisposableClass()
    {
        FinalizationDebugger<DisposableClass>.Finalizer(this);
        ...
    }
}

A implementação dessa classe requer um pouco de explicação. O FinalizationDebugger contém um dicionário estático que mapeia os objetos para a nossa classe de exceção personalizada, que contém todas as informações sobre quando e como o objeto associado foi construído. Quando um objeto é construído e chama o método Constructor, passando a si mesmo como um argumento, o objeto é inserido nessa tabela junto com a instância da exceção. Quando o método Dispose for chamado, o objeto e a exceção associada serão removidos da tabela. Quando o método Finalizer for chamado, a tabela será consultada para ver se o objeto existe; caso contrário, a exceção associada será recuperada e usada para emissão. (Ela também pode ser lançada, se desejado.) Há, é claro, alguns pormenores a essa implementação.

O primeiro é que, como esses métodos são estáticos e poderia ser concebível que fossem chamados de vários threads simultaneamente, é necessário proteger o acesso aos dados compartilhados. Para fazer isso, eu uso um monitor para proteger todos os acessos ao dicionário subjacente.

A segunda questão é mais interessante. O Dictionary é um membro estático da classe FinalizationDebugger, o que o torna uma raiz GC. Como tal, quaisquer objetos armazenados nesse Dictionary nunca serão liberados para coleção pelo coletor de lixo, pois serão sempre acessíveis. Se eu fosse simplesmente armazenar suas instâncias de classe descartáveis nesse Dictionary, elas nunca estarão disponíveis para coleta de lixo e, portanto, nunca seriam finalizadas, derrotando todo esse sistema (para não mencionar a causa de um grande vazamento de memória). Para solucionar isso, precisamos de referências fracas.

A classe System.WeakReference aproveita a funcionalidade fornecida pelo coletor de lixo e System.GCHandle. Quando você instancia WeakReference de um objeto, WeakReference aloca internamente um GCHandle fraco (GCHandleType.Weak ou GCHandleType.WeakTrackResurrection) para o objeto. Em vez de armazenar uma referência no seu objeto, ele simplesmente armazena esse GCHandle. Um GCHandle fraco é usado para controlar um objeto mas ainda permite que ele seja coletado (quando um objeto for coletado, o conteúdo de GCHandle será zerado). Portanto, WeakReference permite que você acesse o objeto subjacente, mas ainda não mantém uma referência forte a ele, como se o coletor de lixo fosse executado e o objeto pudesse, caso contrário, ser coletável, ele será coletado e WeakReference retornará null em qualquer tentativa futura de acessar o objeto. Como tal, WeakReferences solucionam o nosso problema de Dictionary estático. Ao armazenar o WeakReferences em vez do objeto real, os objetos ainda serão coletáveis.

Isso, no entanto, nos leva a outro problema. O Dictionary usa um código de hash do objeto, bem como seu método Equals para determinar onde no Dictionary armazenar o objeto e se o objeto já existe no Dictionary (para verificações e similares). No entanto, o tipo de referência WeakReferencetype não substitui GetHashCode ou Equals; sua semântica de igualdade é padrão à do System.Object e, portanto, verifica para ver se as duas instâncias WeakReferences sendo comparadas são a mesma instância exata. Como resultado, o código como o seguinte será impresso duas vezes como “False”:

object a = new object();
Dictionary<object, bool> dict = new Dictionary<object, bool>();
dict.Add(new WeakReference(a), true);
Console.WriteLine(dict.ContainsKey(a));
Console.WriteLine(dict.ContainsKey(new WeakReference(a)));

Para resolver isso, há duas soluções que podemos tentar. A primeira seria modificar o método FinalizationDebugger.Constructor para retornar WeakReference criado. O seu DisposableClass poderia então manter esse WeakReference e fornecê-lo (em vez de a si próprio) nas chamadas futuras para FinalizationDebugger.Dispose e FinalizationDebugger.Finalizer:

private WeakReference _weakRef;
...
_weakRef = FinalizationDebugger.Constructor(this);
...
FinalizationDebugger.Finalizer(_weakRef);

No entanto, isso nos leva a outro problema. A ordem na qual os objetos são finalizados é indefinida e, como resultado, é uma má idéia fazer referência a uma instância finalizável (que poderia já ter sido finalizada) no finalizador de outra. WeakReference implementa a si próprio como um finalizador e, portanto, o trecho do código anterior resulta na tentativa de usar um WeakReference que pode já ter sido finalizado pelo finalizador do seu tipo descartável.

A segunda solução, e a que escolhi implementar, resolve o problema no seu núcleo: implementing GetHashCode e Equals em WeakReference. Para fazer isso, criei a classe mostrada na Figura 7, ObjectEqualityWeakReference, que substitui os métodos Equals e GetHashCode de WeakReference para fornecer a semântica necessária. Quando ObjectEqualityWeakReference é construído, ele é armazenado em cache em uma variável de membro, o código de hash do objeto fornecido e é esse valor em cache que é retornado do GetHashCode substituído; dessa forma, o código de hash tem como base o objeto subjacente. E mesmo que o objeto seja coletado, o ObjectEqualityWeakReference continuará retornando o mesmo valor de código de hash. Um código de hash de objeto nunca deveria ser alterado e Dictionary usa o código de hash como a primeira etapa para localizar um objeto em sua tabela; se o código de hash for alterado depois que o objeto for adicionado, há uma chance de que o Dictionary não consiga localizá-lo.

Figure 7 ObjectEqualityWeakReference (Close Figure 7)
private class ObjectEqualityWeakReference : WeakReference
{
    private int _hashCode;

    public ObjectEqualityWeakReference(object obj) :
        base(obj, true)
    {
        _hashCode = RuntimeHelpers.GetHashCode(obj);
    }

    public override bool Equals(object obj)
    {
        WeakReference other = obj as WeakReference;

        if (other == null) return ReferenceEquals(obj, this.Target);

        return ReferenceEquals(other, this) ||
               ReferenceEquals(other.Target, this.Target);
    }

    public override int GetHashCode() { return _hashCode; }
}
Figure 7 ObjectEqualityWeakReference (Close Figure 7)
private class ObjectEqualityWeakReference : WeakReference
{
    private int _hashCode;

    public ObjectEqualityWeakReference(object obj) :
        base(obj, true)
    {
        _hashCode = RuntimeHelpers.GetHashCode(obj);
    }

    public override bool Equals(object obj)
    {
        WeakReference other = obj as WeakReference;

        if (other == null) return ReferenceEquals(obj, this.Target);

        return ReferenceEquals(other, this) ||
               ReferenceEquals(other.Target, this.Target);
    }

    public override int GetHashCode() { return _hashCode; }
}

ObjectEqualityWeakReference também substitui o método Equals. Eu queria que Equals funcionasse ao comparar uma instância de ObjectEqualityWeakReference com outra WeakReference fazendo referência ao mesmo objeto subjacente ou ao compará-lo diretamente com esse objeto. Como tal, Equals primeiro verifica para se o objeto sendo comparado é outro WeakReference. Se for, Equals retornará se as instâncias WeakReference são as mesmas ou se os objetos subjacentes são os mesmos. Se o objeto sendo comparado não for um WeakReference, Equals retornará se ele corresponde ao objeto subjacente. (Desejar oferecer suporte ao acréscimo de instâncias ObjectEqualityWeakReference mas consultar pelo tipo de objeto subjacente é o motivo pelo qual o parâmetro de tipo TKey do Dictionary é vinculado ao Object em vez de a ObjectEqualityWeakReference.)

Há algo mais importante a se considerar sobre ObjectEqualityWeakReference: seu construtor:

public ObjectEqualityWeakReference(object obj) : base(obj, true)

Esse construtor é delegado ao construtor de WeakReference, que aceita dois parâmetros, não um. O segundo parâmetro é um Booleano que indica para a WeakReference quando parar o controle do objeto. Se o valor for falso, o objeto somente será controlado até a finalização e, se o valor for true, o objeto será controlado até depois da finalização. Como precisamos que o objeto sobreviva por meio da chamada para FinalizationDebugger.Finalize, definimos isso como true. Observe que esse parâmetro Booleano simplesmente dita que tipo de GCHandle é criado. Se o valor for false, será criado um GCHandle.Weak, e se for true, será criado um GCHandle.WeakTrackResurrection. A nomenclatura surge do conceito de ressurreição, em que um objeto pode ser salvo durante a finalização. Se um finalizador do objeto criar um nova referência com raiz para o objeto (como ao armazenar o objeto em um campo estático em algum lugar), o objeto agora será novamente acessível e, portanto, não será coletado. Para que isso funcione com um WeakReference, o GCHandle deve não permitir que o objeto seja coletado até a fase de finalização e, portanto, nomeando o comportamento descrito anteriormente como "WeakTrackResurrection".

Isso conclui a solução de classe estática, que funciona bem. No entanto, apresenta desvantagens, da mesma forma que outras abordagens. Um dos motivos, o Dictionary estático que precisa ser protegido, essa abordagem requer bloqueios, que poderiam causar contenção se vários objetos estiverem sendo construídos e descartados. Isso é atenuado de certa forma em alguns métodos. Primeiro, o trabalho realizado nas regiões bloqueadas é mantido em um mínimo – eu simplesmente encapsulo a chamada Dictionary.Add em Constructor, uma chamada Dictionary.Remove em Dispose e as chamadas Dictionary.TryGetValue e Dictionary.Remove em Finalizer. Segundo, tornei FinalizationDebugger uma classe genérica com base no tipo de objetos sendo controlados. Isso, em vigor, cria um tipo por tipo FinalizationDebugger totalmente novo com o qual você está trabalhando (com bloqueios e dicionários separados) e, como tal, somente essas chamadas com base no mesmo tipo causarão contenção.

Outra desvantagem a essa abordagem é que esse bloqueio em um finalizador não é a melhor opção. Se por algum motivo o finalizador não puder obter o bloqueio em um período razoável de tempo,o CLR pode simplesmente abortar o finalizador, mas, devido ao código atual, isso é muito improvável.

A maior desvantagem que vejo nessa abordagem é que ela requer que sua classe descartável implemente um finalizador, de forma que possa chamar o método FinalizationDebugger.Finalizer. Se o método Finalizer nunca for chamado, a verificação nunca será executada para ver se o objeto não foi descartado corretamente. Isso não é uma grande vantagem se a sua classe já tiver implementado um finalizador, mas se ela não o tiver, você precisará adicionar um para esse fim.

Há outras variantes em todas essas abordagens e estou certo de que há outras abordagens que não considerei. Se você tiver uma nova abordagem que acha que solucionará todos os vários problemas de cada uma dessas abordagens, estou interessado em receber seus comentários. Enquanto isso, qualquer uma dessas técnicas pode ajudá-lo a capacitar seus desenvolvedores a usarem seus tipos descartáveis para controlar casos em que eles não descartaram corretamente seus objetos.

Envie suas dúvidas e seus comentários para Stephen em netqa@microsoft.com.

Stephen Toub é gerente sênior de programas da equipe da plataforma de computação paralela da Microsoft. Ele também é um dos editores colaboradores da MSDN Magazine.


{ End Bracket }: O WPF tamb&#233;m &#233; para desenvolvedores

{ End Bracket }
O WPF também é para desenvolvedores
Kevin Moore

Tem sido bem divertido ver a agitação em torno do Windows® Presentation Foundation (WPF) em seus primeiros meses no estado natural. A introdução de Expression Blend™ solidificou o WPF como a primeira plataforma de experiência de usuário da Microsoft construída com os designers em mente. Esta capacitação do designer se tornou possível pelo XAML, um modelo baseado em XML para descrever objetos. No caso do WPF, o XAML descreve controles, estilos e animações. Ser baseado em XML habilita o XAML a ser muito mais compatível com ferramentas que o código C# ou Visual Basic® gerado nos aplicativos Windows Forms, o que significa que os designers podem focar na aparência e interação de um aplicativo em vez dos detalhes de programação.

Porém, o futuro baseado em XAML não vem sem seus detratores. Conversei com mais de um desenvolvedor frustrado que lamentou, "Eu só quero escrever códigos”. A reação provavelmente resultou de uma combinação de medo do desconhecido e falta de conhecimento. Como alguém que se uniu ao WPF no meio do caminho de desenvolvimento, posso compreender algum ceticismo em relação ao XAML. “O quê? Outra maneira de instanciar objetos?" Acredite, o XAML deve ser adotado, não temido. Quando você quiser entender o layout de um aplicativo, é muito mais fácil entender uma página de elementos aninhados e atributos que uma página com chaves e ponto-e-vírgula sublinhados. Você também ganha portabilidade pois o XAML foi projetado para ser usado pelas diversas ferramentas.

Então, onde entra o desenvolvedor? Aos fãs do Visual Basic e do C# só resta escrever manipuladores de evento para botões? Não realmente. Na conferência Mix 2007 em Las Vegas, destaquei três funções que permitem que os desenvolvedores tragam benefícios reais, e não triviais, para os aplicativos WPF enquanto fornecem uma experiência "melhor" com o conteúdo XAML. Essas funções caracterizam o desenvolvedor como técnico, autor de widget e pioneiro.

O desenvolvedor como técnico é a idéia de que muitos desenvolvedores não estão interessados em interfaces de usuário e preferem habilitar designers com a construção de camadas de dados poderosas. Suponha que você deseje criar um aplicativo para rastrear um conjunto de estoques. Com a definição de casa objeto de estoque com propriedades públicas, é fácil definir os modelos de dados para visualizar cada estoque. Se os objetos implementarem INotifyPropertyChange, a visualização será atualizada de forma transparente à medida que os dados forem atualizados. É possível aproveitar a infra-estrutura do comando WPF pela interface ICommand para permitir o acesso declarativo aos comportamentos do objeto comercial. Por exemplo, em vez de apenas expor um método Refresh, expor um RefreshCommand. Isso poderá ser conectado a um botão ou menu sem escrever manipuladores de evento. Os comandos ainda desativarão a interface de usuário associada quando a ação não estiver disponível, o que oferece a quem usa as ferramentas de design a flexibilidade máxima no processo criativo enquanto isola a lógica comercial.

Um designer experiente que trabalhe com Expression Blend e um camada de dados bem definida pode ir muito longe. A maioria dos controles incluídos podem ser ajustados com modelos de estilos e controles. A ligação e os modelos de dados permitem uma rica visualização dos objetos comerciais. No final, porém, um designer está limitado aos controles e elementos £ os "widgets" £ entregues com o WPF. Mas os desenvolvedores podem melhorar drasticamente a eficácia de um aplicativo definido em XAML por meio da definição de novos widgets personalizados para atingir um certo cenário. Imagine um design que chama um número arbitrário de elementos a serem organizados em um círculo. A maneira ingênua de abordar isso seria criar os elementos usando APIs de desenho. Ainda que isso fosse perfeitamente bom para um cenário restrito, ele é limitado. Se a aparência de um elemento filho necessitar de alteração, o código necessitará de alteração. Uma abordagem melhor seria criar uma subclasse de painel. Agora você tem um elemento que pode ser usado em elementos visuais arbitrários, em uma ferramenta de design e com um ItemsControl para exibir itens de dados arbitrários. Este é apenas um exemplo. Há várias maneiras de abordar desafios de design no WPF. Ao entender como os vários componentes trabalham em conjunto, um desenvolvedor pode criar widgets com o máximo de flexibilidade.

Enquanto o "técnico" tenta se manter fora do caminho e o "autor de widget" quer mesmo é jogar bem com os outros, o "pioneiro" é aquele que rouba o show. Em seu núcleo, o WPF tem um mecanismo de gráficos 2D/3D poderoso. Embora partes importantes desse mecanismo tenham sido apresentadas pelos controles e outros recursos da estrutura para as ferramentas baseadas em XAML, há muito mais capacidade esperando por um codificador inteligente para ser liberada. Quer esteja aproveitando a interoperabilidade entre 2D e 3D para criar novas visualizações de dados ou implantando animações personalizadas para fornecer transições orgânicas, o desenvolvedor £ compilador à disposição £ tem mais poder e flexibilidade no mundo do WPF. Em combinação com uma visão forte e um conjunto de melhores práticas, não há limite para o que os designers e desenvolvedores podem realizar no WPF.

Kevin Moore é um Gerente de programas da equipe Windows Presentation Foundation na Microsoft, onde trabalha com dados e controles. Visite seu blog em work.j832.com.