 MSDN Magazine Launch 2008

MSDN Magazine Launch 2008

WCF móvel: Escreva um aplicativo IM com o .NET Compact Framework

O .NET Compact Framework 3.5 fornece um subconjunto da funcionalidade do Windows Communication Foundation (WCF) que você pode utilizar para se comunicar entre os dispositivos Windows Mobile e computadores desktop. Mostraremos a você como fazer isso.

Andrew Arnott IIS 7.0: Construa soluções de servidor Web com extensibilidade ponta a ponta

Mike Volodarsky demonstra o modelo de extensibilidade do IIS 7.0 ao estender a Modificação de resposta a um módulo de servidor Web configurável e uma página de gerenciamento personalizado para o Gerenciador do IIS.

Mike Volodarsky VSTO 3.0: Desenvolvendo aplicativos comerciais de escritório com o Visual Studio 2008

Com as regiões de formulário personalizadas no Outlook, você pode receber dados de fontes de dados designadas e personalizar verdadeiramente a experiência do Outlook 2007 dos seus usuários.

Steve Fox Teste de unidade: Aplique o desenvolvimento controlado por testes aos projetos de banco de dados

Jamie Laflen exalta as vantagens do TDD quando aplicado ao desenvolvimento de banco de dados – e também fornece algumas técnicas úteis.

Jamie Laflen F# Primer: Uso de técnicas de programação funcionais no .NET Framework

Vamos apresentar aqui alguns dos conceitos por trás da nova linguagem F#, que combina elementos de linguagens .NET funcionais e orientadas a objeto. Em seguida, ajudaremos você a começar a escrever alguns programas simples.

Ted Neward Nota do Editor: Uma onda de grandes lançamentos está a caminho

Este mês, Howard Dierking, aborda a cobertura do Windows Server 2008, SQL Server 2008 e do Visual Studio 2008.

Howard Dierking Toolbox: Conclusão de código SQL, ferramentas do Subversion, desenvolvimento Agile e muito mais

Este mês: ajuda para a escrita de código SQL, modelagem de seu banco de dados com SubSonic e uma análise do Visual Studio Team System: melhor desenvolvimento de software para Agile Teams.

James Avery Cutting Edge: Caixas de diálogo modais com AJAX

Este mês, Dino Esposito mostra como obter caixas de diálogo modais do estilo do Windows em seus aplicativos Web graças ao Ajax Control Toolkit e a uma codificação inteligente.

Dino Esposito Execução de teste: Automação de teste personalizada com o Team System

Na coluna deste mês, o Dr. James McCaffrey descreve algumas formas de usar o Visual Studio 2005 Team System para gerenciar a automação de teste de software personalizada.

Dr. James McCaffrey Wicked Code: Dicas, truques e práticas recomendadas do Silverlight

: Jeff Prosise apresenta excelentes dicas para o desenvolvimento do Silverlight, que enquanto está ganhando ampla adoção, ainda precisa de mais documentação e práticas recomendadas para que os desenvolvedores possam obter o máximo dos incríveis recursos novos.

Jeff Prosise Estação de serviço: Web Service Software Factory Modeling Edition.

Web Service Software Factory: o Modeling Edition é uma coleção de recursos para modelagem e construção de serviços Web para Windows Communication Foundation e ASMX.

Gerardo de Geest and Gerben van Loon Fundamentos: Serviços de fluxo de trabalho

O Windows Workflow Foundation agora pode ser realmente integrado com o Windows Communication Foundation. Esta coluna descreve como a integração funciona e como usá-la efetivamente.

Matt Milner Windows com C++: Aprimoramentos nos serviços do Windows

O Windows Vista e o Windows Server 2008 apresentam algumas mudanças significativas que facilitam a produção de serviços mais seguros e receptivos.

Kenny Kerr Team System: Observações e diretivas de check-in.

Saiba como adicionar suporte a observações e diretivas de check-in a um suplemento do Word neste quinto e último artigo sobre uma discussão em andamento.

Brian A. Randell { End Bracket }: A declaração de independência digital

Bill Hill aborda sua declaração de independência digital.

Bill Hill


WCF m&#243;vel: Escreva um aplicativo IM com o .NET Compact Framework

WCF móvel

Escreva um aplicativo IM com o .NET Compact Framework

Andrew Arnott
Este artigo aborda:
  • Transportes de email no .NET Compact Framework
  • Escrita de aplicativos simples de mensagem
  • Por dentro do serviço de mensagens do WCF
  • Uso de um Web service do WCF
Este artigo utiliza as seguintes tecnologias:
.NET Compact Framework 3.5, Visual Studio 2008
Download do código disponível em: WCFCompactFramework2008_Launch.exe (185KB)
Conteúdo
Transportes de email
Escrita de um aplicativo do Windows Mobile
Serviço de mensagens do WCF
Criação e envio de mensagem
Escrita do aplicativo de desktop
Execução da amostra
Chamada de serviços do WCF
Criação de uma classe de proxy
Uso do serviço do WCF

U
m problema de capacidade de endereçamento com dispositivos móveis tornou tradicionalmente difícil escrever aplicativos Windows Mobile® que receberiam dados enviados dos servidores. Os pequenos dispositivos com freqüência não têm endereços IP estáticos ou entradas DNS dinâmicas que os sigam. Uma solução comum para o dispositivo tem sido enviar uma solicitação HTTP para o servidor quando o dispositivo estiver online e o servidor permite então que essa solicitação aguarde até que haja algo para enviar para o dispositivo. O servidor então responde a essa solicitação de longa espera com atualização e o dispositivo processa a atualização e envia outra solicitação para aguardar para a próxima atualização.

Essa solução pode causar problemas de escalabilidade no servidor, pois ele precisa suspender muitas solicitações de uma só vez em vez de respondê-las diretamente e fechar as conexões. Ela também pode reduzir a vida da bateria do dispositivo, que deve permanecer ligada para manter a conexão. Se o dispositivo não estiver ligado quando uma atualização do servidor estiver pronta, o servidor não terá como enviar a atualização e deverá ignorar a atualização ou manter o estado no qual o estado no qual os dispositivos têm atualizações. Finalmente, o aplicativo não terá como enviar nem receber mensagens se a rede não estiver disponível.

O Visual Studio® 2008 proporciona aos desenvolvedores dos aplicativos Windows Mobile a capacidade de acessar um subconjunto da funcionalidade do Windows® Communication Foundation (WCF) por meio do Microsoft® .NET Compact Framework 3.5, que soluciona muitos desses problemas com os dois novos elementos de ligação ao WCF, que facilitam o envio e o recebimento de mensagens usando o email como o transporte. Como muitos dispositivos já têm a funcionalidade de sincronização de email, esses transportes aproveitam a natureza inerente de enfileiramento de email e a disponibilidade dos servidores de email já configurados na Internet para criar uma fila de mensagem endereçável que permite a comunicação no nível de mensagem ponto a ponto, dispositivo a servidor e servidor a dispositivo em um verdadeiro método de envio de mensagens. Neste artigo, descreverei o subconjunto do WCF que é compatível com o .NET Compact Framework 3.5 e como você pode utilizar esses transportes e ferramentas nos seus aplicativos móveis.

Transportes de email
.Recursos do .NET Compact Framework
  • Blog da equipe do .NET Compact Framework
  • .NET Compact Framework 3.5 redistribuível e Power Toys
  • Alguns blogs individuais relacionados ao desenvolvimento do .NET Compact Framework:
    • blogs.msdn.com/andrewarnottms
    • blogs.msdn.com/markprenticems
    • blogs.msdn.com/davidklinems
    • jmpinline.nerdbank.net

O .NET Compact Framework 3.5 inclui o WindowsMobileMailTransport para mensagens sobre a plataforma Windows Mobile e o ExchangeWebServiceMailTransport para mensagens na plataforma desktop Windows. Os dois transportes interoperam de modo que a comunicação entre os dispositivos, computadores ou entre dispositivos e computadores ocorra de modo transparente. Como muitos aplicativos podem estar usando o transporte de email ao mesmo tempo para o mesmo destinatário, um nome de canal de email pode ser atribuído para diferenciar as mensagens destinadas ao seu aplicativo de mensagens destinadas a outros aplicativos. Esse nome de canal é então incluído na linha de assunto de cada mensagem. Como resultado, o transporte de email abrirá somente mensagens com o nome do assunto que corresponder ao nome do canal para o qual você está na escuta.

O transporte de email para dispositivos do Windows Mobile fornecido com o .NET Compact Framework 3.5 tem melhor desempenho se executado em um dispositivo do Windows Mobile compatível com DirectPush, um recurso que permite que o servidor Exchange associado envie mensagens para o Windows Mobile assim que o Exchange receber a mensagem. Para saber mais sobre o DirectPush (também conhecido como Always-Up-To-Date) ou instalá-lo no seu dispositivo móvel do Windows Mobile, acesse msexchangeteam.com/archive/2004/04/26/120520.aspx.

Se você estiver familiarizado com o Microsoft Message Queuing (MSMQ), o transporte de email é muito semelhante, exceto que ele não tem o problema de capacidade de endereçamento do dispositivo. O email é armazenado no dispositivo e o servidor Exchange atua como as filas de mensagens. Os endereços de email de cada dispositivo e servidor atuam como a URI da fila.

Observe que embora, teoricamente, dois dispositivos usando o WindowsMobileMailTransport possam ser capazes de usar quaisquer servidores SMTP/POP3 para comunicação, o único cenário que atualmente é compatível no .NET Compact Framework 3.5 usa o Exchange Server 2007 como servidor de email. Além disso, os dispositivos do Windows CE personalizados não são compatíveis.

Para ilustrar como os transportes funcionam, vou criar um par de pequenos aplicativos de amostra e usar o email para enviar mensagens do tipo mensagem instantânea entre um dispositivo do Windows Mobile e um computador. Os transportes de email são compatíveis com mensagem unidirecional (em oposição à solicitação-resposta), que é ideal para um aplicativo de mensagens instantâneas.

As primeiras duas ações que você deve fazer são instalar o Visual Studio 2008 e decidir qual o dispositivo de destino. Esse dispositivo de destino, seja físico ou emulado, precisará ser configurado para acessar a sua conta de email do Exchange Server 2007. Certifique-se de fazer isso antes de testar o seu aplicativo. Se você configurar um dispositivo emulado, deixe-o em execução ou salve seu estado antes de fechá-lo; caso contrário, ele será revertido como não tendo uma configuração de conta de email. Se você se esquecer, provavelmente, verá mensagens se acumulando sem destino na caixa de saída do dispositivo.

Certifique-se de que tenha o Windows Mobile Device Center 6.1 ou ActiveSync® 4.5 instalado antes de começar. Você pode fazer o download em microsoft.com/activesync.

Se você estiver executando o Windows Vista®, a forma mais fácil de configurar um email no seu emulador é abrir o Device Emulator Manager e conectar o emulador. O Windows Mobile Device Center (WMDC) deverá ser aberto quando o dispositivo for conectado. Se o WMDC for instalado mas não reconhecer um emulador conectado, use o menu Iniciar para iniciar o WMDC. Em Mobile Device Settings (Configurações do dispositivo móvel), marque Connection Settings (Configurações de conexão) e certifique-se de que a opção "Allow connections to one of the following:" (Permitir conexões para um dos seguintes) esteja marcada e que o DMA seja selecionado na área suspensa.

Clique em "Set up your device" (Configurar seu dispositivo) e mantenha pelo menos a opção de email marcada. Então, prossiga e forneça as informações para permitir que o dispositivo (ou emulador) conecte-se diretamente ao Exchange. Isso permitirá que o seu programa envie e receba mensagens sempre que houver uma rede disponível; mesmo quando não estiver conectado. Conclua a configuração da parceria com o dispositivo ou emulador.

Se você decidir fazer uso do transporte de email usando o seu endereço de email pessoal, poderá escolher definir uma Regra do Exchange para redirecionar todas as mensagens com uma linha de assunto iniciando com "SM": a outra pasta. O transporte de email .NET Compact Framework 3.5 verificará somente duas pastas quanto a mensagens do WCF, Inbox (Caixa de entrada) e Service E-Mail (Email de serviço); portanto, certifique-se de manter as suas mensagens do WCF em uma dessas duas pastas.

Escrita de um aplicativo do Windows Mobile

Comece escrevendo um aplicativo do Windows Mobile ao criar um projeto do Visual C#® Smart Device. Nomeie o projeto como DeviceMessagingApp e a solução como MsdnMessagingSample, como na Figura 1. Você será conduzido para outra caixa de diálogo, na qual poderá escolher a sua plataforma de destino e a versão do .NET Compact Framework. Escolha a plataforma do Windows Mobile 6 Professional SDK e defina a versão do .NET Compact Framework como 3.5. Se você não quiser ver uma opção para o Windows Mobile SDK, acesse go.microsoft.com/fwlink/?LinkID=81684 e baixe o Windows Mobile SDK; em seguida, instale-o e trabalhe na etapa anterior.

Figura 1 Configuração do seu projeto
Figura 1 Configuração do seu projeto(Clique na imagem para diminuir a exibição)
Figura 1 Configuração do seu projeto
Figura 1 Configuração do seu projeto (Clique na imagem para aumentar a exibição)

A próxima etapa será visualizar o criador de formulários do Visual Studio 2008 com o dispositivo do Windows Mobile e seu principal formulário vazio exibido. Para escrever um simples programa de bate-papo, solte alguns controles TextBox e Label no formulário. Você pode definir TextBox.ReadOnly = True na caixa de texto do histórico para impedir entrada não intencional. Além disso, certifique-se de definir TextBox.Multiline = True para a caixa de histórico. Acrescente um botão Send ao menu, de modo que possa ser pressionando usando um botão de hardware. E agora o meu favorito: defina Form.MinimizeBox como False, de modo que o X no canto mude para "ok"; dessa forma, o seu aplicativo fechará em vez de apenas ser ocultado ao pressioná-lo. Por fim, você deverá ter algo semelhante ao apresentado na Figura 2.

Figura 2 A IU do aplicativo
Figura 2 A IU do aplicativo

Como você estará usando a versão Compact Framework do WCF, precisará acrescentar algumas referências de assembly. Clique com o botão direito na pasta References do projeto no Solution Explorer e clique em Add Reference (Adicionar referência). A partir da guia .NET, selecione esses assemblies:

  • System.ServiceModel.dll
  • System.Runtime.Serialization.dll
  • Microsoft.ServiceModel.Channels.Mail.dll
  • Microsoft.ServiceModel.Channels.Mail.WindowsMobile.dll

O WCF conta com qualquer implementação da classe abstrata XmlObjectSerializer para preencher o corpo de suas mensagens. Na versão desktop, o WCF é fornecido com o DataContractSerializer, que deriva dessa classe. O Compact WCF não inclui o DataContractSerializer; portanto, em vez disso você utilizará o XmlSerializer. Como o XmlSerializer não é derivado do XmlObjectSerializer, como exigido pelo WCF, você pode escrever uma pequena classe derivada dele e simplesmente chamar o XmlSerializer para fazer o trabalho pesado.

Adicione uma nova classe intitulada MessageSerializer, derivada do XmlObjectSerializer, ao seu projeto. Se você clicar com o botão direito do mouse no nome da classe base no seu arquivo de origem, o Visual Studio oferecerá para criar automaticamente um stub de todas as funções que você precisa implementar. Tudo que você precisa fazer nessa classe é adicionar algumas linhas de código, como mostra a Figura 3. Elas definem um construtor que inicializa o XmlSerializer e implementa os métodos ReadObject e WriteObject. Você precisará escrever você mesmo no método WriteObject.

Figure 3 XmlObjectSerializer (Close Figure 3)
using System;
using System.Runtime.Serialization;
using System.Xml.Serialization;

class MessageSerializer : XmlObjectSerializer {
    XmlSerializer serializer;

    public MessageSerializer(Type type) {
        serializer = new XmlSerializer(type);
    }

    public override void WriteObject(
        System.Xml.XmlDictionaryWriter writer, object graph) {
        serializer.Serialize(writer, graph);
    }

    public override object ReadObject(
        System.Xml.XmlDictionaryReader reader,
        bool verifyObjectName) {
        return serializer.Deserialize(reader);
    }

    // ...
}

Esses métodos que o Visual Studio tiver adicionado à sua classe que você não relacionar aqui simplesmente lançarão NotImplementedException, pois o WCF não os chamará de qualquer forma.

Serviço de mensagens do WCF

Para enviar e receber mensagens, você deve inicializar o canal de email do WCF. Como o .NET Compact Framework 3.5 não implementa muitas das classes Service Model incluídas no WCF de desktop, você deve usar a API de mensagem, que requer um pouco mais de código. Certifique-se de escrever esse código de modo neutro para a plataforma ao chamar somente a API do WCF exposta pelo .NET Compact Framework, de modo que você possa compartilhá-lo entre o seu dispositivo e os aplicativos desktop, tornando assim cada aplicativo individual pequeno e simples.

Adicione um novo arquivo de classe intitulado Messaging.cs. Ele terá a classe Messaging, que incluirá todo o serviço necessário para enviar e receber mensagens. Em um aplicativo real, essa classe pode ser muito maior para uma melhor segurança e escalabilidade de thread, mas serei breve no meu exemplo breve para manter o foco na tarefa nesta amostra.

Preferi tornar a classe Messaging genérica uma classe Messaging<T> genérica, onde T é o tipo de dados sendo passados de volta e adiante entre aplicativos (consulte a Figura 4). Em um aplicativo mais complexo, você deveria passar vários tipos de objetos e distinguir as mensagens usando a seqüência Action de mensagem, mas não farei isso aqui. Qualquer T funcionará para essa classe, desde que possa ser serializado usando XmlSerializer.

Figure 4 Classe de mensagem simples (Close Figure 4)
using Microsoft.ServiceModel.Channels.Mail;

class Messaging<T> {
    public delegate void IncomingMessageCallback(T body);
    public const string DeviceSendChannel = "toDesktop";
    public const string DesktopSendChannel = "toDevice";

    public Messaging(MailBindingBase binding,
        string sendChannelName, string listenChannelName,
        IncomingMessageCallback incomingMessageCallback) {
        // ...
    }

    public void SendMessage(string recipient, T body) {
        // ...
    }

    public void Close() {
        // ...
    }
}

A classe Messaging<T> oferecerá dois métodos para envio e recebimento de mensagens. O recebimento de mensagens envolverá um thread de segundo plano na escuta de mensagens e invocando um método de chamada de retorno sempre que uma mensagem for recebida. Será preciso criar o canal no construtor e oferecer um método Close para subdividir o canal quando o aplicativo fechar.

Como há duas classes de ligação de email (uma para desktops e outra para dispositivos móveis), essa classe utilizará a ligação como um argumento ao seu construtor. Portanto, para você usar a mesma conta de email para desktop e dispositivo sem os dois aplicativos lendo as mensagens de entrada do outro, precisará usar diferentes nomes de canal para desktop e dispositivo; assim, o construtor utilizará esses dois argumentos também.

Até agora, criamos a estrutura do esqueleto da nossa classe, como mostra a Figura 4. Com esse código de esqueleto escrito, você poderá continuar escrevendo o restante dos aplicativos desktop e dispositivo. A implementação completa de Messaging.cs está incluída no download do código-fonte deste artigo. Praticamente tudo é código de camada de mensagem comum do WCF sem qualquer relação com um transporte de email.

A próxima etapa é a construção e a subdivisão do canal de email. Abra o código por trás do formulário principal (Form1.cs). Você precisará inicializar o canal de email no construtor e subdividi-lo quando o formulário for fechado. Para iniciar, adicione um campo que controlará a instância Messaging<T>. Como o aplicativo se comunica usando mensagens de texto, o T será uma seqüência:

Messaging<string> messaging;

Você inicializará esse campo em breve. Como o construtor Messaging<T> requer um retorno de chamado, primeiro você definirá um método compatível:

void incomingMessage(string message) {
}

Agora, inicialize o campo de mensagem no construtor. Tenha cuidado para ordenar os nomes dos dois canais de modo que o aplicativo do dispositivo envie mensagens usando o nome DeviceToDesktopChannel e escute o nome DesktopToDeviceChannel. Dessa forma, o dispositivo nunca receberá mensagens que ele envia para o desktop, mesmo se o desktop e o dispositivo estiverem compartilhando um único endereço de email:

messaging = new Messaging<string>(
    new WindowsMobileMailBinding(),
    Messaging<string>.DeviceToDesktopChannel,
    Messaging<string>.DesktopToDeviceChannel,
    (Messaging<string>.IncomingMessageCallback)incomingMessage);

Feche o canal quando o aplicativo fechar o manipulador de eventos Form.Closed. O método Form1_Closed somente precisa chamar o método Close no objeto Messaging<T>:

void Form1_Closed(object sender, EventArgs e) {
    messaging.Close();
}

Criação e envio de mensagem

O aplicativo do dispositivo está praticamente concluído. Tudo que você precisará é enviar e responder às mensagens. Você precisará adicionar um manipulador para o botão Send (Enviar). O manipulador de eventos deverá chamar SendMessage no objeto Messaging<T> e então apagar a caixa de texto da mensagem, de modo que o usuário saiba que a mensagem foi enviada, como a seguir:

void sendMenuItem_Click(object sender, EventArgs e) {
    messaging.SendMessage(toTextBox.Text, messageTextBox.Text);
    messageTextBox.Text = "";
}

Isso foi fácil. Agora, você precisa apenas responder às mensagens de entrada ao implementar o método incomingMessage (consulte a Figura 5). O problema aqui é que esse retorno de chamada é invocado do thread de segundo plano que está na escuta para novas mensagens, mas você deve adicionar texto à sua caixa de texto do histórico de conversas a partir do thread da interface do usuário. Marque a propriedade InvokeRequired para ver se você está em um thread de segundo plano; se estiver, chame Invoke para chamar o método de retorno de chamad ano thread da interface do usuário. Assim que estiver no thread da interface do usuário, basta acrescentar a mensagem de entrada à caixa de conversa e então se certificar de que a caixa de histórico seja rolada para baixo o suficiente para ler a mensagem mais recente.

Figure 5 Resposta a mensagens de entrada (Close Figure 5)
void incomingMessage(string message) {
    // Invoke ourselves on the UI thread
    if (InvokeRequired) {
        Invoke((Messaging<string>.IncomingMessageCallback)
            incomingMessage, message);
    } else {
        // append incoming message to message history
        historyTextBox.Text += message + Environment.NewLine;
        // scroll to end of history window
        historyTextBox.Select(historyTextBox.Text.Length, 0);
        historyTextBox.ScrollToCaret();
    }
}

Isso conclui o aplicativo do dispositivo. Se você tiver contas de email diferentes em dois dispositivos diferentes e configurá-las com os nomes de canal corretos, esse aplicativo seria tudo que você precisaria para ter um bate-papo de mensagens instantâneas entre os dispositivos. Mas vamos continuar supondo um dispositivo com um correspondente em desktop e apenas um endereço de email.

Escrita do aplicativo de desktop

Adicione um novo projeto de aplicativo do WPF à sua solução. Se você não vir isso como uma opção, marque a área suspensa no canto superior direito da caixa de diálogo Add New Project (Adicionar novo projeto) dialog box is set to .NET Framework 3.5. A Windows Forms Application would work equally well, but, for these examples, we'll use the newer technology. Defina o nome do projeto como DesktopMessagingApp.

No criador de formulários, arraste os mesmos controles que você arrastou para o aplicativo do dispositivo. Se você nunca tiver usado o WPF, não se intimide pela alteração na aparência do criador. Além disso, você não precisa definir TextBox.Multiline para a caixa de histórico no WPF.

O resultado final será algo semelhante ao apresentado na Figura 6. Se você preferir apenas inserir o XAML para esse formulário, ele está incluído no download do código-fonte.

Figura 6 Interface do usuário básica para o aplicativo de desktop
Figura 6 Interface do usuário básica para o aplicativo de desktop(Clique na imagem para diminuir a exibição)
Figura 6 Interface do usuário básica para o aplicativo de desktop
Figura 6 Interface do usuário básica para o aplicativo de desktop (Clique na imagem para aumentar a exibição)

Adicione vários assemblies de referência ao projeto de desktop, semelhante ao modo como você fez com o projeto do seu dispositivo. Os assemblies que você precisa para referência são:

  • System.ServiceModel.dll
  • System.Runtime.Serialization.dll
  • Microsoft.ServiceModel.Channels.Mail.dll
  • Microsoft.ServiceModel.Channels.Mail.ExchangeWebService.dll

Em seguida, vincule os arquivos MessageSerializer.cs e Messaging.cs que você criou no projeto do seu dispositivo ao seu projeto de desktop. Clique com o botão direito do mouse em DesktopMessagingApp no Solution Explorer; em seguida, clique em Add (Adicionar) | Existing Item (Item existente). Vá até o diretório do aplicativo do dispositivo. Selecione MessageSerializer.cs e Messaging.cs, mas não clique ainda no botão Add (Adicionar). Em vez disso, clique na sexta para baixo que está na borda direita do botão e selecione Add As Link (Adicionar como link). Isso permite que você compartilhe arquivos de origem entre os dois projetos.

O único problema aqui é que como você criou os arquivos de origem no projeto do dispositivo, o namespace em torno das classes compartilhadas corresponde ao namespace do projeto do dispositivo. Se isso for muito inconveniente para você, poderá simplesmente alterar o namespace dessas classes para algo mais comum. Além disso, você poderia inserir esses dois arquivos de origem em um projeto de biblioteca compartilhado (tendo cuidado para não fazer referência aos assemblies ExchangeWebService.dll e WindowsMobile.dll).

A construção e a divisão do canal de email do aplicativo de desktop serão semelhantes ao aplicativo do dispositivo, exceto que você instanciará o ExchangeWebServiceMailBinding em vez do WindowsMobileMailBinding. Também é válido observar que enquanto o WindowsMobileMailBinding utilizou apenas a caixa de correio no dispositivo (ou emulador), o ExchangeWebServiceMailBinding precisa de credenciais de rede. Supondo que as credenciais sejam as mesmas que as credenciais de login de sua rede, você pode apenas passar nulo para o construtor ExchangeWebServiceMailBinding e o Exchange fará a autenticação usando a segurança integrada do Windows. Você pode manter esse padrão (que é mais seguro) ou pode codificar o nome de usuário e senha, ou poderá solicitar as credenciais do usuário no tempo de execução antes de construir a instância Messaging<T>.

Outra diferença importante entre as duas classes de ligação de email: o WindowsMobileMailBinding é notificado imediatamente quando o ActiveSync apresentar uma nova mensagem, mas o ExchangeWebServiceMailBinding precisa chamar periodicamente o WebMethods para consultar se há novas mensagens; portanto, você pode aceitar o período padrão de 30 segundos ou defini-lo você mesmo.

Comece adicionando os dois campos a seguir ao arquivo de code-behind Window1.xaml.cs. Altere o servidor Exchange para sua própria URL de acesso à Web do Microsoft Outlook®:

Messaging<string> messaging;
readonly Uri exchangeUrl =  new Uri("https://mail.wingtiptoys.com");

Assim como no aplicativo do dispositivo, crie a mensagem recebendo o método para que possa passá-lo como um retorno de chamada ao construtor Messaging<T>:

void incomingMessage(string message) {
}

Você pode instanciar uma classe Messaging<T> no construtor Window1, passando-a para a ligação de email do Exchange (consulte a Figura 7). Esta próxima etapa é muito importante: troque a ordem dos nomes de canal de sua ordem no aplicativo do dispositivo. Ao trocar a ordem, você permite que o desktop receba mensagens do dispositivo e o dispositivo receba mensagens do desktop. Se você falhar em trocar os canais, terá ambos os lados se comunicando no mesmo canal e ambos na escuta do outro canal – nenhuma mensagem será recebida.

Figure 7 Início da ligação de transporte de email (Close Figure 7)
public Window1() {
    InitializeComponent();
    ExchangeWebServiceMailBinding binding =
        new ExchangeWebServiceMailBinding(exchangeUrl, null);
    ((ExchangeWebServiceMailTransport)
        binding.Transport).ServerQueryInterval = 2000;

    messaging = new Messaging<string>(
        binding,
        Messaging<string>.DesktopToDeviceChannel,
        Messaging<string>.DeviceToDesktopChannel,
        incomingMessage);
}

E, da mesma forma, você não terá que aguardar meio minuto para ver quaisquer resultados; nesta amostra, forcei a sondagem do servidor Exchange a cada dois segundos. Lembre-se disso: definir ao intervalo de consulta como dois segundos em um aplicativo de produção provavelmente introduziria um problema de escalabilidade.

Para fechar o canal, você precisaria conectar um método para responder ao evento Window.Closed. Abra o código XAML no arquivo Window1.xaml e adicione o atributo Closed="Window_Closed" à sua marca de abertura <Window>. Isso seria semelhante ao seguinte:

<Window x:Class="DesktopMessagingApp.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Desktop Messaging App" Height="331" Width="440"
    Closed="Window_Closed">

Adicione um método Window_Closed ao seu arquivo code-behind Window1.xaml.cs e então chame o método Close no seu objeto Messaging<T>, como mostrado aqui:

void Window_Closed(object sender, EventArgs e) {
    messaging.Close();
}

Mais uma vez, tudo que você precisará é enviar e responder às mensagens. Adicione um manipulador ao botão de menu Send clicando duas vezes nele no criador. Chame SendMessage e apague a caixa de texto na qual o usuário digitou a mensagem de modo que ele possa ver que a mensagem foi enviada:

void sendButton_Click(object sender, RoutedEventArgs e) {
    messaging.SendMessage(toBox.Text, messageBox.Text);
    messageBox.Clear();
}

Para implementar o método incomingMessage, lembre-se novamente de que ele será chamado de um thread em segundo plano e que você precisará invocá-lo no thread da interface do usuário. O modo do WPF de fazer isso é um pouco diferente do modo do Windows Forms. Em vez da propriedade InvokeRequired, você chamará CheckAccess e, em vez de chamar Invoke no controle, você o chamará no objeto Dispatcher do controle. Por fim, o WPF tem alguns métodos úteis no controle TextBox que fazem exatamente o que queremos fazer; portanto, aproveite-os:

void incomingMessage(string message) {
    if (historyBox.CheckAccess()) {
        historyBox.AppendText(message + Environment.NewLine);
        historyBox.ScrollToEnd();
    } else {
        historyBox.Dispatcher.Invoke(DispatcherPriority.Normal,
            (Messaging<string>.IncomingMessageCallback)incomingMessage,
                message);
    }
}

Agora você concluiu! Vamos executá-lo!

Execução da amostra

Crie e implante seu projeto de dispositivo. Certifique-se de implantar o aplicativo do dispositivo no mesmo emulador ou dispositivo no qual sua conta de email está configurada. Inicie cada projeto individualmente. No Solution Explorar, clique com o botão direito do item do projeto do dispositivo e selecione Debug (Depurar) | Start new instance (Iniciar nova instância). Repita o projeto para o desktop. A Figura 8 mostra qual deve ser a aparência do aplicativo do dispositivo.

Figura 8 Envio de mensagem
Figura 8 Envio de mensagem

Siga em frente e insira o endereço de email do Exchange no campo To: (Para:) de cada aplicativo. Digite uma mensagem e clique em Send (Enviar). Enquanto você está aguardando para que a mensagem seja exibida no outro aplicativo, deixe-me explicar o que está acontecendo e como você poderá visualizá-la.

Quando o aplicativo envia uma mensagem, a seqüência de texto que você envia é encapsulada em um envelope SOAP e solta na sua caixa de saída. O Exchange a coleta e a envia para a caixa de entrada do destinatário (nesse caso, você mesmo). Cada aplicativo está sondando sua caixa de entrada, procurando por mensagens com um nome de canal correspondente. Assim que ele observar a mensagem em espera, o aplicativo fará o download da mensagem, a excluirá do servidor (ou a moverá para a sua pasta de Itens Excluídos) e a processará desserializando a seqüência da marca body do envelope e acrescentando essa seqüência à caixa de texto do histórico da conversa.

Se você estiver interessado em saber como é a aparência do email elaborado para SOAP, poderá verificar a sua caixa de saída ou de entrada (se você verificar antes que o aplicativo o localize e o exclua) ou por meio da sua pasta Itens Excluídos (se o aplicativo já tiver processado a mensagem).

Vários itens podem falhar à medida que você escrever ou configurar este aplicativo. Se as mensagens não forem exibidas como você esperava, verifique suas pastas Caixa de Entrada ou Itens Enviados no Outlook ou no Pocket Outlook para ver se as mensagens setão sendo enviadas e/ou recebidas. Se as mensagens estiverem apenas localizadas em Itens Enviados do Pocket Outlook ou na Caixa de Entrada do Outlook mas não estão sendo sincronizadas, verifique se o seu dispositivo está conectado à sua conta de email do Exchange e force o ActiveSync no seu dispositivo para enviar e receber mensagens. Se as mensagens parecerem ter sido entregues às Caixas de Entrada apropriadas mas o seu aplicativo parece não coletá-las, verifique se você tem os nomes de canal para envio e recebimento de mensagens alinhados corretamente.

Como o método que recebe mensagens é executado em um thread em segundo plano, qualquer exceção que encerrar o thread não estará visível à sua interface do usuário. É possível que algo não dê certo e o seu aplicativo não esteja mais na escuta de mensagens. Verifique a janela de saída do depurador para ver se há um rastreamento de pilha para exceção, a fim de ver se algo falhou. Você pode usar o depurador para ver se ainda tem um thread na espera por mensagens. Em um aplicativo comercial, você pode querer adicionar a manipulação de exceções e talvez um registro em log de diagnóstico a esse thread em segundo plano.

Chamada de serviços do WCF

O .NET Compact Framework 3.5 não é fornecido com suporte para o Modelo de serviço do WCF, que ofereceria suporte aos serviços de hospedagem e chamada sem todo o código de serviço para o envio e o recebimento de mensagens. Mas com as classes adequadas auxiliares, a chamada de WebMethods nos serviços da Web do ASP.NET, serviços do WCF e outros serviços em conformidade com WSDL usando o Compact WCF podem ser tão fáceis quanto chamar um método.

O .NET Compact Framework 3.5 PowerToys é fornecido com uma ferramenta, a NetCFSvcUtil.exe, que gera esses classes de proxy de serviço para você. Como o NetCFSvcUtil.exe não é fornecido com o Visual Studio 2008, você precisa baixar o Power Toys for .NET Compact Framework 3.5 de msdn2.microsoft.com/aa497280.

Como o .NET Compact Framework 3.5 é fornecido com um subconjunto de ligações que você encontrará com o WCF no desktop, um serviço deve expor um ponto de extremidade que utilize ligações compatíveis com o .NET Compact Framework. Você também pode escrever suas próprias ligações usando o Compact WCF, mas isso está além do escopo deste artigo.

Agora, vamos construir um serviço WCF compatível com o .NET Compact Framework no desktop e escrever um aplicativo que chame esse serviço usando a classe de proxy gerada pelo NetCFSvcUtil.exe. Para o aplicativo de serviço, crie um novo site da Web do Serviço do WCF no Visual Studio 2008, verificando se o .NET Framework 3.5 está selecionado no canto superior direito da caixa de diálogo New Web Site (Novo site da Web). Para fins de brevidade, vá em frente e aceite o serviço do WCF exatamente como o modelo do Visual Studio 2008 lhe fornece. Isso lhe fornecerá os métodos GetData e GetDataUsingDataContract criados automaticamente.

Como o .NET Compact Framework 3.5 não é compatível com wsHttpBinding, será necessário alterar o ponto de extremidade padrão que o Visual Studio criou para basicHttpBinding ou adicionar um novo ponto de extremidade desse tipo. Como o WCF facilita a exposição de vários pontos de extremidade e como o wsHttpBinding acrescenta segurança para esses aplicativos compatíveis com ele, o mantenha e adicione um novo ponto de extremidade que utilize basicHttpBinding.

Abra o arquivo web.config do serviço do WCF. Localize as marcas <endpoint> e adicione uma nova com um endereço de básico e com a ligação basicHttppBinding:

<!-- Service Endpoints -->
<endpoint address="" binding="wsHttpBinding" contract="IService"/>
<endpoint address="mex" binding="mexHttpBinding"
    contract="IMetadataExchange"/>
<endpoint address="basic" binding="basicHttpBinding"
    contract="IService"/>

Verifique o seu serviço clicando com o botão direito no seu item de projeto Service.svc da Web no Solution Explorer e clicando em Exibir no navegador.

Observe que o BasicHttpBinding apresenta algumas limitações em relação ao que você pode fazer com o seu serviço. Por exemplo, não é oferecido suporte a retornos de chamada, transações e contratos de serviços duplicados. Se você tentar usar um ponto de extremidade BasicHttpBinding para expor um serviço que utiliza qualquer um desses recursos (ou alguns outros), obterá um erro do tempo de execução do WCF.

Há uma outra limitação no seu serviço além apenas do uso do BasicHttpBinding: os cabeçalhos personalizados não são compatíveis com o NetCFSvcUtil.exe. Se o seu serviço exigir que cabeçalhos personalizados sejam incluídos nas suas mensagens, você precisará modificar o código emitido pela ferramenta ou escrever sua própria classe de proxy para adicionar suporte a esses cabeçalhos personalizados.

Agora, adicione um aplicativo do .NET Compact Framework à sua solução. Crie um novo projeto usando o modelo de projeto de dispositivo inteligente e o nomeie como UNetCFClient. Adicione os assemblies do Compact WCF como suas referências de projeto: System.ServiceModel.dll e System.Runtime.Serialization.dll. Em seguida, adicione um único botão à superfície de design Form1.cs e defina sua legenda como "Call Service". Clique duas vezes nele para criar um manipulador de eventos. Agora, você precisa apenas de uma classe de proxy para invocar o serviço.

Criação de uma classe de proxy

Execute novamente a ferramenta NetCFSvcUtil.exe no serviço para gerar a classe de proxy, a fim de incluí-la no projeto do dispositivo (consulte a Figura 9). Você desejará que os arquivos de origem gerados sejam enviados para o diretório de origem do aplicativo do dispositivo; portanto, execute a ferramenta a partir dele. A própria ferramenta (depois de instalada) deve estar localizada em %PROGRAMFILES%\Microsoft .NET\SDK\CompactFramework\v3.5\bin; no entanto, se você estiver executando em uma máquina de 64 bits, verifique o diretório Program Files (x86).

Figure 9 Geração de classe de proxy (Close Figure 9)
C:\demos\MsdnCFServiceSample\NetCFClient>"\Program Files (x86)\Microsoft .NET\SDK\CompactFramework\v3.5\bin\NetCFSvcUtil.exe"
http://localhost:53222/Service/Service.svc?wsdl
Microsoft (R) .NET Compact Framework Service Model Metadata Tool
[Microsoft (R) Windows (R) Communication Foundation, Version 3.5.0.0]
Copyright (c) Microsoft Corporation.  All rights reserved.

Attempting to download metadata from 'http://localhost:53222/Service/Service.svc?wsdl' using WS-Metadata Exchange or DISCO.
Generating files...
C:\demos\MsdnCFServiceSample\NetCFClient\Service.cs
C:\demos\MsdnCFServiceSample\NetCFClient\CFClientBase.cs

C:\demos\MsdnCFServiceSample\NetCFClient>

Para executar a ferramenta, você precisa apenas passar a URL para os metadados de serviço (o serviço deve estar em execução). Você pode adicionar /language:vb se quiser que a ferramenta emita a classe de proxy para um projeto doVisual Basic®. Observe que a URL que você passar para ferramenta na linha de comando será a URL que o dispositivo utilizará para entrar em contato com o seu serviço; portanto, o http://localhost poderá funcionar para gerar o seu proxy, mas o seu aplicativo de dispositivo tentará se comunicar com ele próprio em vez de entrar em contato com o computador desktop.

Observe também que essa ferramenta não gerou um arquivo output.config como a ferramenta svcutil.exe o faria. O .NET Compact Framework 3.5 não oferece suporte à configuração do WCF por meio de arquivos de configuração; portanto, todas as informações do ponto de extremidade de um serviço precisam estar em código. O servidor de desenvolvimento do ASP.NET não responderá às solicitações do dispositivo, a não ser que o dispositivo ou o emulador esteja conectado. O que fiz aqui foi usar o localhost para gerar o proxy e então modificar o arquivo Service.cs gerado ao localizar o campo EndpointAddress, definido na classe ServiceClient, e alterar a URL para usar meu nome de host (o endereço de servidor) em vez do host local. O código deverá ter uma aparência semelhante a esta:

public static System.ServiceModel.EndpointAddress EndpointAddress =
  new System.ServiceModel.EndpointAddress(
  "http://mycomputername:53222/Service/Service.svc/
  basic");

Além disso, observe o sufixo /basic na URL. Isso é como o cliente indica ao serviço qual dos três pontos de extremidade oferecidos ele deseja usar. Quando você tiver gerado os arquivos de origem de classe de proxy, basta adicioná-los ao projeto do dispositivo no Visual Studio.

Uso do serviço do WCF

Inicialmente, você criou um manipulador de eventos para o botão único Call Service (Serviço de chamada). Veja agora como implementá-lo. O código deve ser semelhante a como chamar um serviço do WCF de um aplicativo de desktop do WCF:

void callServiceButton_Click(object sender, EventArgs e) {
    ServiceClient client = new ServiceClient();
    MessageBox.Show(client.GetData(5));
}

Implante e execute o aplicativo do dispositivo. Clique no botão Call Service (Serviço de chamada). Depois de um momento, você deve ver caixa de mensagem pop-up com "You entered: 5" (Você inseriu: 5) (consulte a Figura 10).

Figura 10 Executando o aplicativo com serviço do WCF
Figura 10 Executando o aplicativo com serviço do WCF

Se você receber um erro de que não foi possível acessar o seu host, tente conectar o dispositivo. Se o dispositivo for um emulador, você poderá conectar o emulador por meio do Device Emulator Manager no Visual Studio.

A classe de proxy gerada pela ferramenta converte as chamadas do método em mensagens do WCF e as envia usando a camada de mensagens do .NET Compact Framework. As mensagens de resposta são então desserializadas e passadas de volta para o seu aplicativo por meio do valor de retorno do método.

O código gerado pela ferramenta NetCFSvcUtil será executado da mesma forma na versão completa do WCF, caso em que as classes de proxy derivam de uma classe do WCF denominada ClientBase<T>. Como o .NET Compact Framework 3.5 não é fornecido com o ClientBase<T>, a ferramenta NetCFSvcUtil emite código para um CFClientBase<T>, que deve ser incluído com o seu aplicativo. Embora essa classe execute uma função semelhante na classe ClientBase<T> localizada no WCF no desktop, ela não é exatamente a mesma e poderá mudar em versões futuras.

Andrew Arnott programa usando tecnologias Microsoft há 12 anos e suas estruturas preferidas incluem WCF, WPF e ASP.NET. Atualmente, ele está trabalhando para a Microsoft como Engenheiro de desenvolvimento de software do .NET Compact Framework. Andrew é casado com Cheryl e tem um filho de menos de um ano.

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


IIS 7.0: Construa solu&#231;&#245;es de servidor Web com extensibilidade ponta a ponta

IIS 7.0

Construa soluções de servidor Web com extensibilidade ponta a ponta

Mike Volodarsky
Este artigo aborda:
  • Pontos de extensão do IIS
  • Criação de um módulo de modificação de resposta
  • Acréscimo da configuração do Gerenciador do IIS
  • Implantação do módulo
Este artigo utiliza as seguintes tecnologias:
IIS 7.0, .NET Framework
Conteúdo
Extensão do servidor Web
Um módulo de modificação de resposta
Extensão da configuração
Configuração de modificação de resposta
Uma classe de configuração de tipo forte
Extensão do Gerenciador do IIS
Criação do serviço
Criação de uma página de módulo
Implantação da extensão
Juntando as peças
Crie o seu próprio

A
plataforma da Web do IIS 7.0 é compatível com mais tecnologias de estrutura de aplicativos para hospedagem de aplicativos avançados do que qualquer versão anterior do IIS e é fornecida repleta de recursos que você pode usar para implantar esses aplicativos prontos para uso. No entanto, ao mesmo tempo, o que você vê (na Instalação do Windows®) não é necessariamente o que você obtém.

A arquitetura do IIS 7.0 foi desenvolvida para ser extensível do início ao fim, permitindo que você substitua qualquer parte do conjunto de recursos incorporados por uma implementação personalizada que atenda às suas necessidades. Como resultado, em vez de fornecer um colcha de retalhos de pontos de plug-in, o IIS 7.0 faz o compromisso final com a extensibilidade ao implementar todos os seus próprios recursos por cima do modelo de extensibilidade pública. Esse design é encontrado em toda a plataforma, do próprio mecanismo de servidor Web modular ao sistema de configuração para o console do Gerenciador do IIS.

Neste artigo, apresentarei o modelo de extensibilidade do IIS 7.0 ao exminar o projeto de modificação de resposta de origem compartilhado, que permite que as respostas dos aplicativos IIS sejam modificadas imediatamente usando regras de modificação de resposta configuráveis. Primeiro, criarei o módulo de servidor Web ao aproveitar a extensibilidade do ASP.NET integrada do servidor. Em seguida, apresentarei os recursos de implantação e gerenciamento do módulo ao desenvolver uma seção de configuração e criar uma página de gerenciamento personalizado para o Gerenciador do IIS.

Extensão do servidor Web

A arquitetura modular do IIS 7.0 oferece a capacidade de personalizar totalmente o servidor Web para uma carga de trabalho necessária. Isso pode ser feito com freqüência simplesmente ao instalar apenas os recursos necessários para o seu aplicativo, oferecendo um servidor Web enxuto que faz exatamente o que é necessário e nada mais.

No entanto, essa é somente a primeira etapa. Com freqüência, a carga de trabalho desejada da Web requer funcionalidade adicional que pode não ser parte do conjunto de recursos incorporados do IIS. Ou, em alguns casos, o aplicativo pode requerer um conjunto personalizado de funcionalidade para o qual os recursos incorporados não são flexíveis o suficiente. Como todos os recursos do IIS 7.0 são incorporados nas APIs de extensibilidade pública, você pode substituir qualquer um deles por uma implementação personalizada que melhor atenda às suas necessidades.

O IIS 7.0 fornece duas opções para o desenvolvimento de módulos de servidor Web. Primeiro, você pode usar a nova API do módulo C++ na qual a maioria dos recursos incorporados são baseados. A API do módulo substitui a extensão API e a API de filtro oferecidas nas versões anteriores do IIS. Essa API é uma melhoria significativa em relação à ISAPI, pois é suficientemente avançada para oferecer suporte a todos os recursos do IIS 7.0 e é muito mais fácil de programar. Você pode obter mais informações sobre as melhorias nessa API em mvolo.com/blogs/serverside/archive/2006/10/07/10-reasons-why-server-development-is-better-with-IIS7.aspx.

Em segundo lugar, o IIS 7.0 apresenta a integração do ASP.NET, que permite que os módulos do IIS 7.0 sejam desenvolvidos usando as conhecidas APIs do módulo do ASP.NET. Com o modo integrado ASP.NET, esses módulos se tornam cidadãos de primeira classe no pipeline de processamento de solicitação do IIS, como mostra a Figura 1. Isso permite que os módulos do ASP.NET acessem objetos intrínsecos do IIS, como solicitação e resposta, em todas as fases do processamento de solicitação, bem como processem solicitações de todos os tipos de recurso – não somente aquelas manipuladas pela estrutura do ASP.NET.

Figura 1 Módulos do ASP.NET no processamento de solicitação do IIS 7.0
Figura 1 Módulos do ASP.NET no processamento de solicitação do IIS 7.0(Clique na imagem para diminuir a exibição)
Figura 1 Módulos do ASP.NET no processamento de solicitação do IIS 7.0
Figura 1 Módulos do ASP.NET no processamento de solicitação do IIS 7.0 (Clique na imagem para aumentar a exibição)

Isso permite que recursos avançados do ASP.NET, como autenticação baseada em formulários, controles de Logon e serviço de associação, sejam usados uniformemente em todo o site da Web. Para uma visão aprofundada de como o modo integrado do ASP.NET pode ser usado para agregar valor aos aplicativos existentes não escritos para a estrutura do ASP.NET, certifique-se de ler meu artigo na edição de janeiro de 2008 da MSDN® Magazine em msdn.microsoft.com/msdnmag/issues/08/01/PHPandIIS7.

Para desenvolvedores, o valor real do modo integrado do ASP.NET é a capacidade de estender o servidor Web do IIS usando a Microsoft® .NET Framework. Isso oferece vantagens significativas no rápido desenvolvimento de aplicativos ao fornecer a capacidade de se basear no suporte de estrutura avançada do .NET Framework, ASP.NET e outras estruturas, como o Windows Workflow Foundation.

Um módulo de modificação de resposta

O pipeline integrado do ASP.NET permite que os módulos do ASP.NET executem uma ampla variedade de tarefas durante o processamento de cada solicitação. Essa ampla variedade abrange desde a execução de autenticação à autorização para modificar a resposta de saída antes que ela seja enviada para o cliente.

A finalidade do módulo de modificação de resposta é fazer o último, a fim de permitir que as respostas existentes sejam modificadas imediatamente usando um conjunto de regras de substituição. Para fazer isso, podemos aproveitar o mecanismo de filtragem de resposta do ASP.NET, que, graças à integração do tempo de execução, permite a filtragem de respostas de saída para qualquer conteúdo, incluindo arquivos estáticos e scripts ASP e PHP.

Se você tiver desenvolvido anteriormente um módulo do ASP.NET, gostará do fato de que continuará usando as mesmas APIs para desenvolver módulos do ASP.NET para o IIS 7.0. Na verdade, os seus módulos existentes continuam funcionando da mesma forma que nas versões anteriores do IIS (a não ser que você aproveite o modo integrado do IIS 7.0 para executá-los para todas as solicitações). O módulo é uma classe que implementa a interface System.Web.IHttpModule e registra os manipuladores de eventos para um ou mais eventos do pipeline de solicitação. O módulo de modificação de resposta não é diferente:

class ResponseModificationModule : IHttpModule
{
  public void Init(HttpApplication app)
  {
    // Wire up the filter in PreRequestHandlerExecute
    app.PreRequestHandlerExecute +=
      new EventHandler(OnPreRequestHandlerExecute);
  }

  public void OnPreRequestHandlerExecute(
    Object source, EventArgs e)
  {
    ...
  }
}

Essencialmente, o módulo fará a inscrição no evento PreRequestHandlerExecute, que ocorre logo antes de o manipulador de solicitação ser executado. O método OnPreRequestHandlerExecute é usado para ler as informações de configuração, a fim de determinar se a resposta tem quaisquer regras de substituição aplicáveis (saiba mais sobre configuração posteriormente no artigo) e, portanto, registre um fluxo do filtro de respostas que será posteriormente usado para filtrar a resposta de saída, como mostra a Figura 2.

Figure 2 Filtro de respostas (Close Figure 2)
public void OnPreRequestHandlerExecute(Object source, EventArgs e)
{
  HttpApplication app = (HttpApplication)source;
  HttpContext context = app.Context;

  // Read configuration
  ResponseModificationConfigurationSection config;

  ...

  // Get the list of filters
  ResponseFilterList filters = GetApplicableFilters(context, config);

  // Create the filter and wire it up
  if (null != filters && filters.Count > 0)
  {
    context.Response.Filter =
      new ChainedBufferedStringResponseFilter(
        context, filters);
  }
}

O trabalho de filtragem de resposta é feito na classe ChainedBufferedStringResponseFilter, que é responsável pela conversão dos bytes de resposta em uma seqüência usando o conjunto de caracteres de resposta e pela invocação de um ou mais filtros de resposta que se aplicam a essa solicitação. O módulo associa o filtro à resposta ao definir a propriedade HttpResponse.Filter. Essa propriedade aceita objetos herdados da classe System.IO.Stream abstrata e posteriormente os utiliza para filtrar o corpo da entidade de resposta de saída.

O recurso de filtragem de resposta do ASP.NET é apenas um dos mecanismos que você pode aproveitar ao desenvolver módulos do ASP.NET para o modo integrado do IIS 7.0. Você também pode reescrever URLs, executar autenticação e autorização de solicitações, modificar ou emitir cookies e cabeçalhos de resposta, registrar a solicitação e assim por diante. A maioria das tarefas que anteriormente exigiam desenvolvimento ISAPI nativo agora podem ser feitas simplesmente escrevendo um módulo do ASP.NET. Você pode encontrar uma orientações passo a passo sobre a criação de módulos do ASP.NET para o IIS 7.0, as opções de desenvolvimento e as etapas de implantação em mvolo.com/blogs/serverside/archive/ 2007/08/15/Developing-IIS7-web-server-features-with-the-.NET-framework.aspx.

Ao desenvolver um módulo do IIS 7.0, você pode aproveitar vários recursos do IIS que auxiliam no gerenciamento e na implantação. Um desses recursos é a capacidade de especificar a configuração de módulo personalizado nos arquivos de configuração do IIS 7.0 e que essa configuração seja implantada automaticamente com o aplicativo ou gerenciada com várias APIs e ferramentas de administração do IIS 7.0. Fazer isso é fundamental para garantir que a funcionalidade fornecida pelo módulo possa ser adequadamente configurada e gerenciada pelos usuários finais.

Extensão da configuração

O novo sistema de configuração é a base para muitos dos possíveis cenários de implantação e gerenciamento com o IIS 7.0. Em vez de ser um armazenamento de configuração central de máquina proprietário, o sistema de configuração do IIS 7.0 tem como base arquivos de configuração XML estruturados localizados nos mesmos arquivos de configuração utilizados pelo sistema de configuração do ASP.NET. Além disso, a sintaxe das informações de configuração do IIS é idêntica à das informações de configuração do ASP.NET e pode ser posicionada lado a lado em arquivos web.config distribuídos com os quais os desenvolvedores do ASP.NET já estão muito familiarizados e adoram.

Esse design abre muitas portas. Em primeiro lugar, ele permite que aplicativos especifiquem as informações de configuração do IIS junto com seu conteúdo de aplicativo, o que facilita a sua implantação ao simplesmente publicar seu conteúdo no servidor. Isso garante que os aplicativos funcionem corretamente sem a necessidade de modificar informações de configuração centralizadas na máquina em cada servidor no qual eles foram implantados, bem como sem exigir privilégios administrativos em cada servidor. O formato de configuração XML estruturado, além da capacidade de reunir a configuração do ASP.NET com a configuração do IIS em um só arquivo, facilita muito para que os desenvolvedores criem e gerenciem todas as informações de configuração relacionadas ao IIS. Isso pode ser feito com um editor XML, como o Bloco de notas e o Visual Studio®, ou pode ser automatizado usando as APIs de configuração fornecidas pela pilha de administração do IIS 7.0.

A melhor parte de tudo isso é o fato de que o sistema de configuração do IIS 7.0 é completamente extensível. Permitir que os recursos personalizados do servidor Web publiquem a configuração que pode ser definida e gerenciada lado a lado com a configuração do IIS 7.0 exerce um papel fundamental na criação de soluções completas para o IIS 7.0.

Ao contrário do sistema de configuração do .NET, o acréscimo de uma nova seção de configuração do IIS 7.0 não requer código, pois todas as seções de configuração do IIS 7.0 são elas mesmas definidas usando um esquema baseado em XML. Na verdade, todas as seções de configuração incorporadas do IIS 7.0 são definidas usando esse mecanismo. Você pode localizar as definições de todas as seções de configuração do IIS no arquivo %windir%\system32\inetsrv\config\schema directory in the IIS_Schema.xml. Por exemplo, a seção de configuração <defaultDocument>, que habilita e configura documentos padrão para seu aplicativo, é definida da seguinte forma:

<sectionSchema name="system.webServer/defaultDocument">
  <attribute name="enabled" type="bool" defaultValue="true" />
  <element name="files">
    <collection addElement="add" clearElement="clear"
      removeElement="remove" mergeAppend="false">
      <attribute name="value" type="string" isUniqueKey="true"/>
    </collection>
  </element>
</sectionSchema>

Este esquema define todos os elementos e atributos que fazem parte da seção de configuração, seus tipos e informações adicionais, como definições de coleção e comportamento de validação do atributo. Para saber mais sobre a sintaxe do esquema de definição, veja os comentários no início do arquivo de configuração IIS_Schema.xml.

Configuração de modificação de resposta

O módulo de modificação de resposta requer sua própria seção de configuração, a fim de configurar várias das definições específicas, incluindo o que as regras de filtragem permitem ao aplicativo. Para permitir que essa seção de configuração seja usada nos arquivos de configuração do IIS, é necessário criar primeiro o arquivo do esquema descrevendo a estrutura da seção (consulte a Figura 3). Esse arquivo define a seção responseModification e sua estrutura, incluindo o atributo habilitado e a coleção de regras do filtro que podem ser configuradas para invocar tipos de filtro com informações adicionais sobre o que substituir e pelo que substituir.

Figure 3 Esquema de configuração de modificação de resposta (Close Figure 3)
<configSchema>
  <sectionSchema name="responseModification">
    <attribute name="enabled" type="bool" defaultValue="false" />
    <collection addElement="add" clearElement="clear"
      removeElement="remove">
      <attribute name="name" type="string" required="true"
        isUniqueKey="true" validationType="nonEmptyString" />
      <attribute name="conditionType" type="string" required="false" />
      <attribute name="condition" type="string" required="false" />
      <attribute name="replaceType" type="string" required="true"
        validationType="nonEmptyString" />
      <attribute name="replace" type="string" required="false" />
      <attribute name="replaceWith" type="string" required="false" />
      <attribute name="options" type="string" required="false" />
    </collection>
  </sectionSchema>
</configSchema>

Para instalar essa seção de configuração, você precisará executar o seguinte. Primeiro, copie o arquivo responsemod_schema.xml contendo as informações do esquema de seção para o diretório %windir%\system32\inetsrv\config\schema. Em seguida, declare a seção de configuração no arquivo de configuração principal do servidor, o applicationHost.config. A última tarefa requer a escrita de algum código se você quiser executar essa instalação por meio de programação.

Para simplificar o processo de instalação das seções de configuração do IIS 7.0, escrevi a ferramenta iisschema.exe, que executa ambas as tarefas automaticamente. Você pode obter a ferramenta em mvolo.com/blogs/serverside/archive/2007/08/04/IISSCHEMA.EXE-_2D00_-A-tool-to-register-IIS7-configuration-sections.aspx. Com essa ferramenta, a instalação da seção do esquema de configuração se torna um processo de uma só etapa:

IisSchema.exe /install responsemod_schema.xml

Agora que o esquema da seção de configuração está instalado e a seção por si só está declarada no applicationHost.config, você pode começar a utilizá-la imediatamente para definir as informações de configuração do módulo.

Você pode usar qualquer uma das APIs ou ferramentas de configuração do IIS para gerenciar a configuração dessa seção, basta fazer como normalmente faria para qualquer uma das seções incorporadas de configuração do IIS 7.0. Por exemplo, posso habilitar o recurso de modificação de resposta para meu aplicativo usando AppCmd, a ferramenta da linha de comando do IIS 7.0:

%windir%\system32\inetsrv\AppCmd Set Config "Default Web Site/"
/section:responseModification  /enabled:true

Também posso adicionar uma regra à coleção de regras da seção de configuração responseModification:

%windir%\system32\inetsrv\AppCmd Set Config "Default Web Site/"
"/+[name='StripWhitespace',replaceType=
'Mvolo.ResponseModification.Filters.HtmlDeflate']"

Se então você abrir o arquivo web.config na raiz do site da Web padrão (que é geralmente %windir%\inetpub\wwwroot), localizará o seguinte código:

<configuration>
  <responseModification enabled="true">
    <add name="StripWhitespace"
      replaceType="Mvolo.ResponseModification.Filters.HtmlDeflate" />
  </responseModification>
</configuration>

Se você já tiver modificado o arquivo web.config para que o aplicativo altere as informações de configuração do módulo e então o carregue no servidor, o aplicativo será configurado para usar as novas configurações. Da mesma forma, se você carregar o aplicativo em outro servidor que tenha a seção de configuração instalada, poderá estar certo de que o aplicativo utilizará as configurações desejadas sem precisar reconfigurá-las nesse servidor.

Além do AppCmd, agora você pode usar qualquer uma das APIs de configuração do IIS 7.0 para gerenciar as informações de configuração dessa seção, incluindo a API Microsoft.Web.Administration gerenciada, o provedor Windows Management Instrumentation (WMI) do IIS 7.0, os objetos COM de configuração do IIS 7.0 e o Windows PowerShell®. Além da definição e da leitura dessas informações de configuração, você também pode executar qualquer tarefa de gerenciamento compatível com o sistema de configuração do IIS 7.0, incluindo o controle de como essa seção é delegada aos aplicativos, a proteção de seu conteúdo com criptografia de configuração e assim por diante. No entanto, para que essas informações de configuração sejam úteis, você precisará lê-las a partir do módulo de modificação de resposta.

A API do Microsoft.Web.Administration é uma nova API .NET Framework fornecida no IIS 7.0 que permite que os programas acessem as informações de configuração do IIS 7.0. Além de permitir que programas de configuração e instalação gerenciem a configuração, também é a API que pode ser usada para ler as informações de configuração a partir de um módulo gerenciado.

Você pode usar a API para ler a seção de configuração no módulo imediatamente depois de registrar a seção de configuração, pois a API expõe um modelo de tipo incerto para leitura das seções de configuração. Antes de fazer isso, você precisará adicionar uma referência à Microsoft.Web.Administration.dll, que está localizada no diretório %windir%\system32\inetsrv. Observe que essa DLL é incluída somente no IIS 7.0 no Windows Vista® ou Windows Server® 2008 e não está incluída com SDKs do .NET Framework ou Windows.

Com a referência de assembly adicionada, posso ler as informações de configuração do módulo:

ConfigurationSection section =
  WebConfigurationManager.GetSection(
    context,
    "responseModification");

bool enabled = (bool)section["enabled"];

A classe WebConfigurationManager no namespace Microsoft.Web.Administration é praticamente idêntica ao WebConfigurationManager no namespace System.Web.Configuration e se destina a substituir o uso do último para os módulos gerenciados do IIS 7.0 que lêem a configuração do IIS 7.0. O método GetSection recupera o objeto da seção de configuração para o caminho da solicitação HTTP atual.

Uma classe de configuração de tipo forte

O objeto ConfigurationSection permite acesso de tipo incerto a qualquer seção de configuração sem qualquer código adicional. Você pode acessar o atributo habilitado simplesmente ao recuperá-lo pelo nome do indexador do objeto da seção e convertê-lo para o tipo esperado.

No entanto, você pode utilizá-lo em uma etapa posterior ao gerar um wrapper de tipo forte para a seção de configuração. Você pode gerar rapidamente a classe de wrapper ao usar a ferramenta escrita por Kanwaljeet Singla, membro da equipe do IIS, localizada em blogs.iis.net/ ksingla/archive/2006/12/04/tool-to-generate-strongly-typed-classes-for-configuration-sections.aspx. Isso irá gerar uma classe de wrapper para a seção, semelhante à apresentada na Figura 4. Essa classe simplesmente encapsula a classe ConfigurationSection subjacente e fornece propriedades de tipo forte para o acesso aos dados de configuração subjacentes.

Figure 4 Wrapper de seção de configuração de tipo forte (Close Figure 4)
class ResponseModificationSection : ConfigurationSection
{
  public bool Enabled
  {
    get { return (bool)this["enabled"]; }
    set { this["enabled"] = value; }
  }

  public ResponseModificationRuleCollection FilterRules
  {
    get
    {
      return (ResponseModificationRuleCollection)
        GetCollection(typeof(ResponseModificationRuleCollection));
    }
  }
}

Como você pode ver a partir da implementação da propriedade Enabled, o encapsulamento dos atributos de configuração é bem simples. O encapsulamento de elementos filho e coleções filha requer implementação adicional de classes de wrapper herdadas de ConfigurationElement e ConfigurationCollection, respectivamente (não mostradas), de modo semelhante ao próprio wrapper de seção.

Depois que a classe de wrapper tiver sido gerada, você poderá compilá-la em um assembly separado e fazer referência a ela no projeto do seu módulo ou simplesmente adicioná-la como um arquivo de código-fonte. Isso permitirá que o módulo acesse a seção de configuração em uma forma de tipos seguros:

ResponseModificationSection section =
  (ResponseModificationSection)WebConfigurationManager.GetSection(
    context,
    "responseModification",
    typeof(ResponseModificationSection)
  );

bool enabled = section.Enabled;

Da mesma forma, você também pode começar a acessar a coleção de regra de modificação de resposta e as propriedades de cada um dos elementos em uma forma de tipos seguros. Isso facilita muito a programação de seções de configuração e minimiza a quantidade de erros de segurança de tipos que são comuns com acesso a dados de tipos não seguros. Além disso, como todo o código utilizado para acessar os dados subjacentes é gerado automaticamente pela ferramenta, você pode configurar o Visual Studio para que crie automaticamente as classes de wrapper a partir do arquivo do esquema de configuração sempre que compilar o projeto. Isso simplifica o gerenciamento de alterações no esquema da seção de configuração.

Extensão do Gerenciador do IIS

Até o momento, mostrei a você como criar o módulo de modificação de resposta e habilitá-lo para a leitura da seção de configuração personalizada que controla seu comportamento. Essa configuração pode ser armazenada nos mesmos arquivos de configuração que o restante das informações de configuração do IIS, pode ser implantada junto com o aplicativo nos arquivos web.config e pode ser gerenciada com uma variedade de ferramentas e APIs, incluindo AppCmd.exe e Microsoft.Web.Administration.

Nesse ponto, o módulo de modificação de resposta pode ser implantado e adequadamente configurado em qualquer ambiente do IIS. No entanto, você pode utilizá-lo em uma etapa posterior ao fornecer uma experiência personalizada de gerenciamento para o console do Gerenciador do IIS. Fazer isso oferecerá várias vantagens. Ele permite que os administradores do sistema configurem facilmente o módulo usando o console do Gerenciador do IIS, sem precisar editar os arquivos de configuração diretamente ou usar qualquer uma das APIs e ferramentas de nível inferior. Ele também permite que as informações de configuração de módulo sejam gerenciadas usando os recursos de administração remota do Gerenciador do IIS, que aproveitam o WmSvc (Web Management Service, Serviço de gerenciamento da Web).

Ao contrário das outras ferramentas que oferecem suporte à administração remota, a arquitetura de administração remota do Gerenciador do IIS oferece várias vantagens importantes. Primeiro, permite que os usuários que não sejam Administradores no servidor gerenciem sites e aplicativos dos quais eles tenham controle. Em segundo lugar, o mecanismo remoto do Gerenciador do IIS usa HTTPS, em vez de DCOM, que é mais fácil de expor por meio de firewalls corporativos. Juntos, esses dois recursos tornam o Gerenciador do IIS atrativo para o gerenciamento remoto delegado de sites da Web do IIS, especialmente em ambientes de hospedagem da Web compartilhados.

No verdadeiro espírito do IIS 7.0, o Gerenciador do IIS fornece uma arquitetura extensível na qual a maioria dos recursos incorporados do Gerenciador do IIS é baseada. Para facilitar o cenário de gerenciamento remoto, cada recurso de gerenciamento consiste em duas partes: os componentes do lado do cliente que fornecem a experiência da interface do usuário no Gerenciador do IIS e o componente do lado do servidor que fornece de fato os serviços de gerenciamento.

O serviço do lado do servidor é carregado no Gerenciador do IIS para cenários de gerenciamento local ou no Serviço de gerenciamento da Web em cenários de gerenciamento remoto. No último caso, o Gerenciador do IIS trata a comunicação necessária entre os componentes no Gerenciador do IIS na máquina cliente e o serviço em execução no WmSvc na máquina do servidor de destino.

Criação do serviço

Os componentes do cliente e do servidor são geralmente implementados em dois assemblies separados que não fazem referência entre si. Toda a comunicação entre os componentes do cliente e do servidor é feita por meio da abstração ModuleServiceProxy do Gerenciador do IIS que utiliza recipientes de propriedade de tipos não seguros e tipos .NET básicos para a troca das informações necessárias.

O assembly do servidor geralmente contém uma implementação da classe Microsoft.Web.Management.Server.ModuleProvider, que executa o registro necessário de serviços e módulos do lado do cliente, bem como uma implementação da classe Microsoft.Web.Management.Server.ModuleService, que expõe os métodos de serviço para a execução das tarefas de gerenciamento necessárias.

Meu assembly Mvolo.ResponseModificationUI.Server.dll contém a classe ResponseModificationModuleProvider, que é responsável pelo registro da classe de serviço ResponseModificationModuleService, além da classe do módulo ResponseModificationModule cliente, como mostra a Figura 5.

Figure 5 Classe de provedor de módulo (Close Figure 5)
public sealed class ResponseModificationModuleProvider :
  ConfigurationModuleProvider
{
  public override Type ServiceType
  {
    get { return typeof(ResponseModificationModuleService); }
  }

  public override ModuleDefinition
    GetModuleDefinition(IManagementContext context)
  {
    return new ModuleDefinition(Name,
      "Mvolo.ResponseModification.UI.Client.ResponseModificationModule,"+
      "Mvolo.ResponseModificationUI.Client," +
      "Version=1.0.0.0, Culture=neutral," +
      "PublicKeyToken=309ac0e1b5482072");
  }

  protected override string ConfigurationSectionName
  {
    get { return "responseModification"; }
  }
}

A classe ResponseModificationModuleService é derivada da classe ConfigurationModuleProvider, que fornece suporte incorporado para o controle da delegação de recurso do Gerenciador do IIS, com base no estado de delegação da seção de configuração subjacente. Tudo que você precisa fazer é fornecer o nome da seção de configuração, responseModification, e o Gerenciador do IIS alternará automaticamente o estado de delegação do recurso com base no fato de a seção estar bloqueada ou desbloqueada para qualquer caminho de configuração específico. A estrutura do Microsoft.Web.Management fornece várias outras classes base do ModuleProvider compatíveis com vários outros cenários de delegação para a ferramenta do Gerenciador do IIS.

O método GetModuleDefinition retorna o tipo do módulo do Gerenciador do IIS que será instanciado no cliente para registrar os componentes da interface do usuário do Gerenciador do IIS necessários, como a própria página de gerenciamento.

Observe que o tipo de classe ResponseModificationModule do lado do cliente é uma seqüência, não um objeto Type, pois o assembly do servidor pode não fazer referência ao assembly do cliente que será carregado no Gerenciador do IIS em uma máquina de cliente remoto. Esse é um efeito unilateral do fato de que os componentes do cliente e servidor podem estar sendo executados em diferentes processos e podem não compartilhar tipos personalizados.

A classe ResponseModificationModuleService é responsável pela implementação da funcionalidade de gerenciamento e exposição dos métodos de serviço que serão invocados pela página de gerenciamento no Gerenciador do IIS (veja a Figura 6).

Figure 6 Classe de serviço de módulo (Close Figure 6)
public sealed class ResponseModificationModuleService : ModuleService
{
  [ModuleServiceMethod]
  public ArrayList GetFilterRules()
  {
    ArrayList items = new ArrayList();

    ResponseModificationSection section =
      (ResponseModificationSection)ManagementUnit.Configuration
        .GetSection("responseModification",
          typeof(ResponseModificationSection));

    foreach (ResponseModificationRuleElement rule in section.Rules)
    {
      items.Add(GetPropertyBag(rule));
    }

  return items;
  }
  ...
}

O método GetFilterRules é um exemplo de vários métodos expostos pelo serviço, que executa tarefas de gerenciamento usando o sistema de configuração e possivelmente outros recursos locais no servidor. O serviço é sempre instanciado no servidor; portanto, ele sempre atua nos recursos locais e não precisa se preocupar em acessar os recursos de modo diferente no cenário de administração remota. Observe que estou novamente usando a API Microsoft.Web.Administration para acessar a seção de configuração responseModification e estou usando o wrapper ResponseModificationConfigurationSection de tipo forte criado para o módulo para o acesso à configuração.

A propriedade ManagementUnit é o ponto de entrada central no servidor local, expondo vários outros objetos que permitem acesso ao sistema de configuração e outros objetos de gerenciamento centralizados no servidor que foram criados pela estrutura do Microsoft.Web.Management para auxiliar em tarefas de gerenciamento do servidor. Como prática recomendada, as implementações de serviço sempre devem usar a unidade de gerenciamento para tarefas de gerenciamento de configuração em vez de criar seus próprios sistemas de configuração.

Os métodos como GetFilterRules podem retornar informações para o cliente usando tipos básicos, como ArrayList, para retransmitir informações para os componentes do cliente. Novamente, você não pode se comunicar usando classes de tipos fortes definidas nos assemblies do servidor ou do cliente, pois se destinam a serem separadas e não devem fazer referência entre si. O ResponseModificationModuleService também define métodos de serviço adicionais semelhantes a GetFilterRules, incluindo EditFilterRule, AddFilterRule e DeleteFilterRule, a fim de oferecer suporte às operações de gerenciamento necessárias que podem ser solicitadas pela página do módulo de modificação de resposta.

Criação de uma página de módulo

Os componentes do cliente definem a funcionalidade real do gerenciamento da interface gráfica do usuário fornecida ao usuário pela ferramenta do Gerenciador do IIS. O assembly de cliente geralmente contém a implementação das seguintes classes:

  • A classe Microsoft.Web.Management.Client.Module, que é responsável pelo registro de quaisquer páginas necessárias do Gerenciador do IIS e outras extensões da interface do usuário.
  • A classe Microsoft.Web.Management.Client.ModulePage, que descreve a página de gerenciamento real exibida pelo Gerenciador do IIS.
  • A classe Microsoft.Web.Management.Client.ModuleServiceProxy, que fornece os componentes de cliente com uma classe de proxy local para invocar os métodos de serviço expostos pelo ModuleService.

O assembly Mvolo.ResponseModificationUI.Client.dll contém a classe ResponseModificationModule, mostrada na Figura 7 (não confunda com a classe IHttpModule real que fornece os serviços de módulo no pipeline de processamento de solicitação do IIS), que fornece o principal ponto de entrada para o registro de todos os componentes de IU necessários no Gerenciador do IIS. Essa classe registra a classe ResponseModificationModulePage, que descreve a página de gerenciamento real.

Figure 7 Classe de módulo (Close Figure 7)
public sealed class ResponseModificationModule : Module
{
  protected override void Initialize(IServiceProvider serviceProvider,
    ModuleInfo moduleInfo)
  {
    base.Initialize(serviceProvider, moduleInfo);

    IControlPanel controlPanel =
      (IControlPanel)GetService(typeof(IControlPanel));

    controlPanel.RegisterPage(
      new ModulePageInfo(this,
      typeof(ResponseModificationModulePage),
      Resources.PageTitle,
      Resources.PageDescription,
      null,
      null,
      Resources.PageText
      ));
  }
}

Cada implementação de módulo é criada uma vez no tempo de vida de uma conexão de gerenciamento feita com o Gerenciador do IIS e é responsável pelo registro de todas as páginas de gerenciamento e elementos de IU necessários para a conexão. O ResponseModificationModule registra a classe ResponseModificationModulePage, que define o conteúdo da página de gerenciamento.

Para ser honesto, eu não sou muito fã da programação da interface gráfica do usuário e prefiro muito mais escrever código de servidor de baixo nível que não requeira o desenvolvimento de interface. Felizmente para mim, a estrutura do Microsoft.Web.Management fornece várias classes base e classes de suporte para a implementação de páginas de gerenciamento de aparência comum, incluindo a classe Microsoft.Web.Management.Client.Win32.ModuleListPage que utilizei para ResponseModificationModulePage.

Essa classe fornece uma visualização de lista incorporada para visualização e gerenciamento de listas de itens, incluindo a classificação da lista e interação com itens de lista. Com uma quantidade mínima de personalização, foi possível criar uma página de gerenciamento contendo uma lista de regras de modificação de resposta (veja o painel central da Figura 8).

Figura 8 Página de modificação de resposta no Gerenciador do IIS
Figura 8 Página de modificação de resposta no Gerenciador do IIS(Clique na imagem para diminuir a exibição)
Figura 8 Página de modificação de resposta no Gerenciador do IIS
Figura 8 Página de modificação de resposta no Gerenciador do IIS (Clique na imagem para aumentar a exibição)

Em seguida, criei a lista de ações, exibida no painel direito, para permitir que sejam adicionadas, editadas e removidas regras de modificação de resposta. Aproveitei a classe Microsoft.Web.Management.Client.TaskList para criar esta lista de tarefas (exibida no painel direito na Figura 8).

Com apenas um pouco mais de trabalho, surgiu o recurso de modificação de resposta mostrado na Figura 8. Você pode rever os detalhes da interface do usuário no código-fonte da estrutura de modificação de resposta.

A criação de página inicial e as operações de acréscimo, edição e exclusão acionadas do painel de ações são executadas pela invocação de métodos na classe ResponseModificationModuleServiceProxy, que fornece um proxy local para os métodos do módulo expostos pelo serviço de módulo:

public sealed class ResponseModificationModuleServiceProxy :
  ModuleServiceProxy
{
  public ArrayList GetFilterRules()
  {
    return (ArrayList)Invoke("GetFilterRules");
  }

  public void AddFilterRule(PropertyBag bag)
  {
    Invoke("AddFilterRule", bag);
  }

  ...
}

O Gerenciador do IIS retransmite automaticamente chamadas feitas à classe de proxy do serviço de módulo ao serviço de módulo no canal de comunicação, que pode ser um canal de memória compartilhada local ou um canal https para um servidor remoto.

Implantação da extensão

Para implantar a extensão do Gerenciador do IIS, observe que você precisa registrar o tipo de provedor de módulo no arquivo de configuração do Gerenciador do IIS no servidor (%windir%\system32\inetsrv\config\administration.config). A configuração mostrada na Figura 9 registra o provedor de módulo e permite que ela seja usada para todos os sites por padrão. Observe que os assemblies de cliente e servidor que envolvem a extensão, Mvolo.ResponseModificationUI.Server.dll e Mvolo.ResponseModificationUI.Client.dll, devem ser registrados no GAC (Global Assembly Cache, cache de assembly global) e, portanto, devem ser de nomes seguros e assinados. Essa é uma exigência para a instalação de extensões do Gerenciador do IIS.

Figure 9 Configuração da extensão do Gerenciador do IIS (Close Figure 9)
<moduleProviders>
  ...

  <!-- Mvolo ResponseModification -->
  <add name="ResponseModification"
    type="Mvolo.ResponseModification.UI.Server
        .ResponseModificationModuleProvider,
      Mvolo.ResponseModificationUI.Server,
      Version=1.0.0.0, Culture=neutral,
      PublicKeyToken=895e90205a14f2aa,
      processorArchitecture=MSIL" />
</moduleProviders>

<!-- For all Sites -->
<location path=".">
  <modules>
    ...
    <add name="ResponseModification" />
  </modules>
</location>

Quando o Gerenciador do IIS for usado para gerenciar o sevidor remotamente, a ferramenta solicitará automaticamente que o usuário faça o download do assembly de cliente sempre que ainda não estiver presente no cliente (isso atualmente funciona apenas ao gerenciar o IIS 7.0 no Windows Server 2008 de um cliente Windows Vista SP1, Windows XP ou Windows Server 2003 ao usar o Gerenciador do IIS remoto disponível online de iis.net/ downloads/?tabid=34&g=6&i=1524). Certifique-se de você confia no servidor no qual está conectado e no editor da extensão do Gerenciador do IIS antes de aceitar essa instalação, pois caso contrário isso poderá possivelmente permitir que código não confiável seja executado na sua máquina com os seus privilégios de conta de usuário.

Juntando as peças

Depois de implantar o módulo e instalar o esquema da seção de configuração e a extensão do Gerenciador do IIS, o recurso de modificação de resposta estará pronto para uso. Agora, você pode habilitar o recurso para qualquer um dos aplicativos no seu servidor e configurá-lo com qualquer uma das APIs e ferramenta de configuração do IIS 7.0.

Além disso, você pode usar a ferramenta do Gerenciador do IIS para gerenciar rapidamente as regras de modificação de resposta para aplicativos, no servidor local ou em um servidor remoto. Usando o suporte à delegação do Gerenciador do IIS, você pode até mesmo configurar quais usuários têm a capacidade de criar e gerenciar remotamente as regras de modificação de resposta para sites específicos em um servidor.

Para demonstrar o recurso de modificação de resposta em ação, a Figura 10 mostra como configurei regras para meu aplicativo de galeria de imagens Qdig (apresentado em msdn.microsoft.com/msdnmag/issues/08/01/PHPandIIS7). A Figura 11 mostra como usar o filtro Mvolo.ResponseModification.Filters.RegExReplace fornecido com a estrutura de modificação de resposta para inserir de modo inteligente o conteúdo do cabeçalho e do rodapé em todas as páginas no site da Web, sem modificar o código-fonte real do aplicativo.

Figure 10 Habilitação de cabeçalhos e rodapés (Close Figure 10)
<responseModification enabled="true">
  <add name="Header"
    conditionType=
      "Mvolo.ResponseModification.Conditions.ContentTypeCondition"
    condition="text/html"
    replaceType="Mvolo.ResponseModification.Filters.RegExReplace"
    replace="&lt;body([^>]*)>"
    replaceWith="&lt;body$1>&lt;h1 align=&quot;center&quot;>
      Welcome to my gallery!&lt;/h1>"
    options="IgnoreCase" />

  <add name="Footer"
    conditionType=
      "Mvolo.ResponseModification.Conditions.ContentTypeCondition"
    condition="text/html"
    replaceType="Mvolo.ResponseModification.Filters.RegExReplace"
    replace="&lt;/body>"
    replaceWith="&lt;h3 align=&quot;center&quot;>
      Copyright mvolo 2007&lt;/h3>&lt;/body>"
    options="IgnoreCase"  />
 </responseModification>
Figura 11 Uso de modificação de resposta para modificar respostas
Figura 11 Uso de modificação de resposta para modificar respostas

O módulo de modificação de resposta também pode ser usado para implementar facilmente qualquer número de inserções de conteúdo personalizado, substituições de hiperlink e outros esquemas de modificação de resposta ao implementar filtros de substituição. Esses filtros podem então ser implantados globalmente ou com um aplicativo e configurados por meio de regras de configuração na seção de configuração responseModification.

Crie o seu próprio

Há alguns usos criativos para o recurso de modificação de resposta que nunca conseguiram atingir os mesmos resultados ao modificar o código-fonte de um aplicativo (que é sempre mais eficiente do que modificar a resposta imediatamente). Por exemplo, também o utilizo para injetar um pequeno script em respostas HTML, que altera as marcas de imagem para usar versões extremamente compactas de imagens e então as substitui por versões de qualidade superior imediatamente, a fim de fornecer uma experiência de navegação mais responsiva para clientes lentos. Isso também utiliza um manipulador de imagem personalizado que pode fornecer imagens JPEG com qualquer nível de compactação desejado para uma imagem estática usando as APIs do Microsoft Windows Imaging Component. A Figura 11 mostra o resultado em ação, incluindo o cabeçalho, o rodapé e a imagem pré-carregada de baixa qualidade (logo antes de ser automaticamente substituída pela versão de resolução superior).

A modificação de respostas imediatamente pode ser um mecanismo dispendioso, tanto em termos de requisitos de memória e uso de CPU como vários buffers grandes devem ser alocados para armazenar versões transformadas das respostas. Essa sobrecarga deve ou não ser aceitável na produção de sites da Web, dependendo dos tipos de filtros de modificação de resposta utilizados, do número deles e dos requisitos de desempenho do aplicativo.

Para saber mais sobre como usar a estrutura de modificação de resposta, você deve fazer o download dela, junto com seu código-fonte, a partir do site de desenvolvimento da modificação de resposta (www.mvolo.com/responsemod). Depois de compreender como os filtros de resposta funcionam e poder planejar o uso deles em virtude das considerações de sobrecarga de desempenho, verifique outras amostras no site de desenvolvimento da modificação de resposta. Para saber mais sobre a extensão do IIS 7.0, acesse o site iis.net e o meu blog (mvolo.com), que é dedicado ao desenvolvimento e ao uso do IIS 7.0.

Mike Volodarsky é gerente técnico de programa na equipe de ferramentas e plataformas da Web na Microsoft. Nos últimos quatro anos, ele tem se dedicado ao design e ao desenvolvimento do conjunto de recursos básicos do ASP.NET 2.0 e do IIS 7. Seu foco atual consiste em ajudar os clientes a aproveitar a capacidade dessas tecnologias no Windows Server 2008.


VSTO 3.0: Desenvolvendo aplicativos comerciais de escrit&#243;rio com o Visual Studio 2008

VSTO 3.0

Desenvolvendo aplicativos comerciais de escritório com o Visual Studio 2008

Steve Fox
Este artigo aborda:
  • Novos recursos no Visual Studio 2008
  • Criando regiões de formulário personalizadas para o Outlook
  • Conectando-se a uma fonte de dados
  • Adicionado recursos de pesquisa
Este artigo utiliza as seguintes tecnologias:
Visual Studio 2008
Download do código disponível em: CustomOutlookRegion2008_Launch.exe (4168KB)
Conteúdo
Amplie o Outlook 2007 com as regiões de formulário personalizadas
Criando sua primeira região de formulário personalizada
Mostre dados de vendas e pesquisa do cliente
Conectando-se a uma fonte de dados
O formulário de pesquisa de dados do cliente
Adicionando o controle do usuário de vendas do WPF
Adicionando a última parte
Criando e executando a região de formulário Replacement personalizada
Leitura adicional

A
té agora, estou certo de que já ouvi coisas interessantes sobre o Visual Studio® 2008. Ele tem alguns recursos impressionantes, como o suporte a LINQ, desenvolvimento da Web aperfeiçoado e melhor integração com o Windows Vista® e o SharePoint®, para nomear apenas alguns. No entanto, uma área na qual o Visual Studio 2008 realmente se destaca é no seu suporte ao desenvolvimento de soluções do Microsoft® Office.

O Visual Studio Team System 2005 é incluído no Visual Studio 2005 Tools for Office (VSTO) — você também pode fazer download do VSTO Second Edition para começar o desenvolvimento de soluções para aplicativos do Office 2003 e do 2007 Microsoft Office system. No entanto, no Visual Studio 2008, todos os modelos de projeto do VSTO estão incluídos.

Você pode usar o Visual Studio 2008, especificamente o VSTO 3.0, para desenvolver personalizações do Microsoft Outlook®, que será coberto neste artigo. Mostrarei a você o que poderá fazer com um novo tipo de tecnologia de desenvolvimento de formulários denominada regiões de formulário, que pode ter áreas especiais nos formulários existentes do Outlook. Também mostrarei a você como adicionar suporte ao Windows® Presentation Foundation (WPF) para as regiões de formulário, a fim de ajudar a aprimorar a aparência e a interatividade.

Amplie o Outlook 2007 com as regiões de formulário personalizadas

O VSTO 3.0 fornece um ambiente de design baseado no Windows Forms no Visual Studio 2008. Isso permite que você crie e codifique novas regiões de formulário do Outlook em um ambiente de desenvolvimento e ofereça muitas vantagens do Windows Forms ao ambiente de hospedagem do Outlook. Por exemplo, você pode criar uma região de formulário do VSTO que se conecte a um Web service para adquirir dados de CRM (customer relationship management, gerenciamento de relação com o cliente) e os exiba em uma grade. Você pode até mesmo adicionar uma Faixa de Opções personalizada a um inspetor do Outlook 2007 que hospedará a região de formulário personalizada e adicionar código à Faixa de Opções, de modo que interage com os controles na região.

Observe que há quatro tipos de regiões de fomulário do Outlook 2007 à sua disposição:

Adjoining (Adjacente) Essas regiões aparecem na parte inferior do formulário de destino do Outlook na primeira página do formulário do Outlook e/ou na parte inferior do painel de leitura do Outlook.

Separate (Separada) Essas regiões adicionam uma nova página ao formulário.

Replacement (Substituição) Essas regiões podem fazer com que a região de formulário substitua todo o conteúdo na primeira página de formulário do formulário do Outlook (todas as páginas de formulário existentes são deixadas no lugar).

Replace All (Substituir Tudo) Essas regiões podem fazer com que a região de formulário substitua todo o conteúdo na primeira página de formulário do formulário do Outlook e remova todas as outras páginas de formulário do formulário.

Cada uma dessas regiões de formulário permitem que você amplie o Outlook de formas interessantes, mas aqui me concentrarei na região do formulário de Replacement. A região de formulário Replacement pode ser usada para criar um formulário personalizado a partir de uma classe de mensagem personalizada. Ao contrário das regiões de formulário Adjoining e Separate que são criadas para classes existentes de mensagens do Outlook — por exemplo, Mail Message (IPM.Note), Contact (IPM.Contact) e Appointment (IPM.Appointment) — ao selecionar a região de formulário Replacement (ou Replace All), você criará uma classe de mensagem personalizada derivada de uma das classes existentes de mensagens do Outlook. Por exemplo, na amostra, apresentarei uma classe de mensagem personalizada denominada CustomersSalesData, derivada do tipo Mail Message; portanto, o que resulta é uma classe de mensagem personalizada denominada IPM.Note.RecentSales.

Criando sua primeira região de formulário personalizada

Para criar essa região de formulário Replacement personalizada, utilizei o Visual Studio 2008 para criar um projeto de suplemento do Outlook 2007 (consulte a Figura 1). Nomeie o projeto CustomOFR e utilizei o local padrão do projeto. Nesse momento, o Visual Studio cria um shell de projeto de suplemento do Outlook 2007 para você. Isso significa que quando você criar e implantar o seu suplemento (que terá a sua região de formulário do Outlook personalizada), o Visual Studio carregará automaticamente seu suplemento quando o Outlook for carregado.

Figura 1 Selecionando o modelo de projeto de suplemento do Outlook
Figura 1 Selecionando o modelo de projeto de suplemento do Outlook(Clique na imagem para diminuir a exibição)
Figura 1 Selecionando o modelo de projeto de suplemento do Outlook
Figura 1 Selecionando o modelo de projeto de suplemento do Outlook (Clique na imagem para aumentar a exibição)

Com o seu shell de projeto agora criado, vamos prosseguir e adicionar uma região de formulário do Outlook personalizada para ele. Selecione Outlook Form Region (Região de Formulário do Outlook) na caixa de diálogo Add New Item e defina o nome da região como SalesData. Isso invoca o New Outlook Form Region Wizard, que envolve várias etapas para criar uma região de formulário Replacement. Na primeira etapa, indique se está criando uma nova região de formulário ou importando uma existente (arquivo .ofs). Nesta amostra, você desejará criar uma nova região de formulário; portanto, siga em frente e clique em Next. Em seguida, será solicitado que selecione o tipo de região de formulário a ser criada. Selecione o tipo Replacement e, em seguida, clique em Next. Nesse momento, você definirá algumas propriedades para a sua região de formulário; especificamente, um nome, um título, uma descrição e os modos nos quais deseja exibir a região de formulário (consulte a Figura 2). Depois que tiver inserido essas informações, clique em Next.

Figura 2 Propriedades da região de formulário personalizada
Figura 2 Propriedades da região de formulário personalizada(Clique na imagem para diminuir a exibição)
Figura 2 Propriedades da região de formulário personalizada
Figura 2 Propriedades da região de formulário personalizada (Clique na imagem para aumentar a exibição)

Na etapa final, você associará sua região de formulário a uma classe de mensagem específica. Como mencionei anteriormente, como essa é uma região de formulário Replacement, você deverá criar uma classe de mensagem personalizada em vez de usar as classes de mensagem de stock fornecidas com o Outlook. Você observará que nesse caso, essa etapa tem todas as opções de classe de mensagem de stock esmaecidas e o único campo disponível para essa etapa é a caixa de texto da classe de mensagem personalizada na parte inferior da caixa de diálogo (consulte a Figura 3). Você verá que nesse campo inseri IPM.Note.CustomerSalesData, que significa que vou derivar minha classe de mensagem personalizada do tipo de classe de mensagem IPM.Note do Outlook (ou criar essencialmente um tipo de classe de mensagem personalizada que herde toda a mesma funcionalidade na classe Mail Message de stock).

Figura 3 Inserindo um tipo de classe de mensagem personalizada
Figura 3 Inserindo um tipo de classe de mensagem personalizada(Clique na imagem para diminuir a exibição)
Figura 3 Inserindo um tipo de classe de mensagem personalizada
Figura 3 Inserindo um tipo de classe de mensagem personalizada (Clique na imagem para aumentar a exibição)

Todas as regiões de formulário se destinam a uma ou mais classes de mensagens específicas no ambiente do Outlook. Essas classes de mensagens estão diretamente relacionadas ao tipo de item do Outlook. Há oito classes de mensagem padrão (consulte a Figura 3) e, teoricamente, um número infinito de classes de mensagens personalizadas. Você deveria derivar da mesma forma sua classe personalizada de outros tipos de classe personalizada. Depois de inserir sua derivação e nome de classe de mensagem personalizada, clique em Finish. O nome será validado quando você clicar no botão Finish.

Quando tiver concluído as etapas no assistente, uma região de formulário será adicionada ao projeto de suplemento do Outlook. No meu caso, os três itens a seguir foram adicionados ao projeto:

RecentSales.cs O principal arquivo de classe da região de formulário.

RecentSales.Designer.cs O arquivo do designer que contém algum código padrão para a região do formulário.

RecentSales.rexs O arquivo XML necessário para exibir a região do formulário.

Agora, vamos começar a personalização. Depois de criar a região de formulário, a visualização padrão no Visual Studio deveria ser o modo de designer da região de formulário. Caso contário, clique com o botão direito do mouse em RecentSales.cs e selecione View Designer. Agora, você pode começar a adicionar controles ao seu formulário.

Para manter as coisas simples, adicionarei três principais itens à minha região de formulário Replacement personalizada: um controle de pesquisa que relaciona informações do cliente, um controle do WPF que exibe os dados de vendas atualizadas e uma caixa de seleção para controlar se o usuário será exibido ou ocultado. A Figura 4 ilustra os controles que adicionei ao meu projeto no contexto do meu Visual Studio IDE.

Figura 4 Designer de região de formulário
Figura 4 Designer de região de formulário(Clique na imagem para diminuir a exibição)
Figura 4 Designer de região de formulário
Figura 4 Designer de região de formulário (Clique na imagem para aumentar a exibição)

Mostre dados de vendas e pesquisa do cliente

A região do formulário representa a superfície de design e é compatível com arrastar e soltar controles a partir da Caixa de ferramentas para a superfície de criação. Portanto, arraste uma caixa de seleção à superfície de criação e defina sua propriedade Name como showChart e sua propriedade Text como "Show Sales Data" (Mostrar dados de vendas). Em seguida, clique duas vezes no controle da caixa de seleção e adicione o seguinte código para chamar o método UpdateChart — esse método atualizará os dados e a visibilidade do gráfico de vendas do WPF:

private void checkBox1_CheckedChanged(object sender, EventArgs e)
  {
    UpdateChart();
  }

Retornarei a esse método um pouco mais adiante no artigo.

O controle de usuário Sales Search permite que você insira alguns critérios de pesquisa e navegue em um banco de dados pequeno com informações do usuário que é dinamicamente atualizado no controle. Para habilitar essa funcionalidade de pesquisa, adicionei um novo projeto denominado SalesControlLibrary (um projeto Windows Forms Control Library) à solução CustomOFR e, em seguida, adicionei o controle da Caixa de ferramentas na região de formulário. Há duas partes principais para o projeto SalesControlLibrary na minha solução: uma conexão com a fonte de dados e o controle do usuário que utilizarei na minha região de formulário Replacement personalizada.

Conectando-se a uma fonte de dados

Com o novo projeto adicionado à solução, você agora pode adicionar uma fonte de dados clicando em Data e em Add New Data Source. Como você deve esperar, isso desativa o Add New Data Source Wizard, que primeiro solicita que você defina e configure uma conexão simples a uma fonte de dados (banco de dados). Para esta amostra, estou usando um banco de dados do Access denominado Sales.mdb (que está incluído no download de código com os seguintes quatro elementos de esquema (como você pode ver na Figura 5): CustomerID é a ID exclusiva de um cliente, SalesQuarter representa o trimestre fiscal de vendas, SalesYear representa o ano fiscal no qual as vendas foram registradas e SalesAmount é a quantidade de dólar de uma venda.

Figura 5 Dados de cliente e vendas
Figura 5 Dados de cliente e vendas(Clique na imagem para diminuir a exibição)
Figura 5 Dados de cliente e vendas
Figura 5 Dados de cliente e vendas (Clique na imagem para aumentar a exibição)

Se você for integrar esse suplemento a um sistema de LOB (linha de negócios), então classifique esse aplicativo como um Office Business Application (OBA); em seguida, provavelmente você usaria um proxy de serviço do Windows Communication Foundation (WCF) para gerenciar a conexão e a comunicação com o seu sistema LOB. O uso de serviços como proxies para sistemas LOB é uma excelente forma de circumnavegar interfaces difíceis do sistema ou inserir dados comerciais específicos diretamente no contexto dos aplicativos office comuns do seu cliente.

Quando você pensa sobre valor específico agregado quando as organizações criam OBAs, certamente uma das principais vantagens é a capacidade de aproveitar a interface do usuário do Office para integração com os dados do LOB. Isso também descarrega de certa forma o que de outro modo seria um trabalho de desenvolvimento personalizado para recursos nativos do Office. E talvez o mais importante, mantém os usuários finais em um ambiente confortável e familiar (pense na formatação e visualização nativa de dados no Excel® 2007). Para obter mais informações sobre a área geral de OBAs, vá para msdn2.microsoft.com/aa905528.

O formulário de pesquisa de dados do cliente

Com a fonte de dados adicionada ao projeto SalesControlLibrary, você agora pode criar o componente para pesquisar os dados do cliente. No designer para meu formulário de pesquisa de vendas, adicionei vários controles ao projeto, incluindo alguns rótulos, campos, uma grade de dados conectada ao banco de dados Sales e alguns campos que exibem as informações de contato do cliente que corresponde ao item selecionado na grade de item. A Figura 6 mostra o formulário completo com todos os controles adicionados. Observe que enquanto a maioria desses controles é fácil de adicionar, com a exibição de grade de dados, você precisará primeiro abrir o painel Data Sources, clicar com o botão direito do mouse na fonte de dados Sales e selecionar Details e, em seguida, arrastar e soltar os registros Sales na exibição de grade de dados. Em seguida, clique com o botão direito no controle, selecione Edit Columns e remova SalesYear e SalesAmount, pois você está interessado em mostrar CustomerID e SalesQuarter na exibição de grade de dados.

Figura 6 Criando o controle de usuário de dados do cliente
Figura 6 Criando o controle de usuário de dados do cliente(Clique na imagem para diminuir a exibição)
Figura 6 Criando o controle de usuário de dados do cliente
Figura 6 Criando o controle de usuário de dados do cliente (Clique na imagem para aumentar a exibição)

Nesse momento, você tem os novos controles adicionados ao seu formulário e já está pronto para adicionar algum código aos controles. Portanto, vamos dar uma olhada em algum código-chave que foi adicionado a esse formulário. As primeiras coisas que veremos são as declarações que adicionamos à classe SalesSearch. Especificamente, declarei um adaptador de tabela para Company e Sales, e criei um manipulador de eventos para todas as alterações de dados de vendas:

namespace SalesControlLibrary
{
  public partial class SalesSearch : UserControl
  {
    private SalesDataSetTableAdapters.CompaniesTableAdapter
      companiesAdapter;
    private SalesDataSetTableAdapters.SalesTableAdapter
      salesAdapter;
    public event EventHandler<SalesSearchEventArgs>
      SalesDataChangeEvent;

    ...
  {
}

O principal evento sendo acionado é o evento executeSearch_click que é acionado quando o botão Search é clicado. É localizado na classe SalesSearch, como você pode ver na Figura 7.

Figure 7 Executar pesquisa acionada a partir do botão Search (Close Figure 7)
private void executeSearch_Click(object sender, EventArgs e)
{
  try
  {
    companiesAdapter = new
      SalesDataSetTableAdapters.CompaniesTableAdapter();
    CompaniesBindingSource.DataSource =
      companiesAdapter.GetDataByCompanyAndIndustry(
        companyNameText.Text,
        industryText.Text);
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message, "Sales Add-in", MessageBoxButtons.OK);
  }
}

Você pode ver que isso cria uma nova instância de companiesAdapter e chama o método GetDataByCompanyAndIndustry para recuperar dados de acordo com os parâmetros que o usuário inseriu nos campos de pesquisa (o sinal de percentual na Figura 8 indica uma pesquisa de curinga). Esses dados serão então usados para preencher os campos Contact e Address para exibir as informações do cliente.

Figure 8 Como lidar com a alteração BindingSource (Close Figure 8)
private void CompaniesBindingSource_CurrentChanged(object sender,
  EventArgs e)
{
  DataRowView company = (DataRowView)(CompaniesBindingSource.Current);
  try
  {
    string[] xData = new string[4];
    double[] yData = new double[4];
    if (company != null)
    {
      salesAdapter = new SalesDataSetTableAdapters.SalesTableAdapter();
      SalesDataSet.SalesDataTable sales =
        salesAdapter.GetDataByCustomerID((int)(company["ID"]));
      DataTableReader rdr = sales.CreateDataReader();
      int i = 0;
      while ((rdr.Read()) && (i < xData.Length) && (i < yData.Length))
      {
        xData[i] = String.Format("Q{0} {1}",
        rdr["SalesQuarter"].ToString(), rdr["SalesYear"].ToString());
        yData[i] = Convert.ToDouble(rdr["SalesAmount"]);
        i++;
      }
      OnSalesDataChanged(company["CompanyName"].ToString(),
        xData, yData);
    }
  }
  catch (Exception ex)
  {
  MessageBox.Show(ex.Message, "Sales Add-in", MessageBoxButtons.OK);
  }
}

No code-behind desse formulário, também há algumas funções auxiliares. Por exemplo, adicionei um evento no objeto CompaniesBindingSource para administrar o caso em que um usuário esteja navegando nos dados de vendas a partir dos controles (consulte a Figura 8).

Além disso, adicionei um manipulador de eventos para processar quaisquer alterações nos dados de vendas. Por exemplo, o código a seguir mostra o método OnSalesDataChanged invocado caso haja uma alteração na seleção do registro atual na fonte de ligação:

protected virtual void OnSalesDataChanged(string companyName,
  string[] x, double[] y)
{
  if (SalesDataChangeEvent != null)
    SalesDataChangeEvent(this,
      new SalesSearchEventArgs(companyName, x, y));
}

Com a alteração de seleção, o evento OnSalesDataChanged tem parâmetros do nome da empresa, do ano e das vendas do registro atual.

Adicionando o controle do usuário de vendas do WPF

Agora, vejamos como criar o controle do WPF e adicioná-lo à região do formulário do Outlook personalizada. São necessárias duas etapas para adicionar um controle de usuário do WPF à região de formulário Replacement personalizada do Outlook. A primeira é criar o controle de usuário do WPF e a segunda é adicioná-lo à região de formulário Replacement (que é a região de formulário RecentSales que criei anteriormente).

Para criar o formulário, adicione um novo projeto da Biblioteca de Controle do usuário do WPF à solução. O Visual Studio criará um controle padrão e exibirá o editor XAML. Nesse momento, você poderá criar um controle de gráfico do WPF e adicionar manipuladores de eventos. A Figura 9 mostra o controle do WPF no designer do Visual Studio 2008 XAML.

Figura 9 Controle de gráfico do WPF
Figura 9 Controle de gráfico do WPF(Clique na imagem para diminuir a exibição)
Figura 9 Controle de gráfico do WPF
Figura 9 Controle de gráfico do WPF (Clique na imagem para aumentar a exibição)

Como você adicionou um novo projeto, precisará ter algum código XAML padrão; no entanto, não precisará de nada no designer que não seja um recipiente vazio. Então, você precisa adicionar algum código XAML aqui para criar o gráfico. Para ajudá-lo, a Figura 10 mostra o código XAML que usei para criar o pequeno gráfico de vendas do WPF para a região do formulário. O código é praticamente fácil, pois cria um pequeno controle com duas colunas e cinco linhas e insere o texto do espaço reservado no gráfico. O texto será atualizado com dados do banco de dados Sales no tempo de execução. Observe que o XAML é associado à classe SalesChart, como declarado na linha de abertura do código XAML.

Figure 10 Código XAML (Close Figure 10)
<UserControl x:Class="WpfChart.SalesChart"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Height="300" Width="300">
  <Grid Name="Grid1">
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="80"/>
      <ColumnDefinition Width="190"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
      <RowDefinition Height="30"/>
      <RowDefinition Height="30"/>
      <RowDefinition Height="30"/>
      <RowDefinition Height="30"/>
      <RowDefinition Height="30"/>
    </Grid.RowDefinitions>
    <TextBlock Name="Category1" Text="X1" Grid.Column="0"
      Grid.Row="0" VerticalAlignment="Center"
      HorizontalAlignment="Center"/>
    <TextBlock Name="Category2" Text="X2" Grid.Column="0"
      Grid.Row="1" VerticalAlignment="Center"
      HorizontalAlignment="Center"/>
    <TextBlock Name="Category3" Text="X3" Grid.Column="0"
      Grid.Row="2" VerticalAlignment="Center"
      HorizontalAlignment="Center"/>
    <TextBlock Name="Category4" Text="X4" Grid.Column="0"
      Grid.Row="3" VerticalAlignment="Center"
      HorizontalAlignment="Center"/>
    <TextBlock Name="Title" FontSize="14" FontWeight="Bold"
       Text="Chart Title" Grid.Column="1" Grid.Row="4"
      VerticalAlignment="Center" HorizontalAlignment="Right"/>
    <Rectangle Name="Bar1"  Fill="LightSteelBlue" RadiusX="5"
      RadiusY="5" HorizontalAlignment="Left" Grid.Column="1"
      Grid.Row="0" Width="170" Height="18"/>
    <Rectangle Name="Bar2"  Fill=" LightSteelBlue" RadiusX="5"
      RadiusY="5" HorizontalAlignment="Left" Grid.Column="1"
      Grid.Row="1" Width="170" Height="18"/>
    <Rectangle Name="Bar3"  Fill=" LightSteelBlue" RadiusX="5"
      RadiusY="5" HorizontalAlignment="Left" Grid.Column="1"
      Grid.Row="2" Width="170" Height="18"/>
    <Rectangle Name="Bar4"  Fill=" LightSteelBlue" RadiusX="5"
      RadiusY="5" HorizontalAlignment="Left" Grid.Column="1"
      Grid.Row="3" Width="170" Height="18"/>
    <TextBlock Name="Label1" Text="$Y1" Grid.Column="1"
      Grid.Row="0" VerticalAlignment="Center"
      HorizontalAlignment="Right"/>
    <TextBlock Name="Label2" Text="$Y2" Grid.Column="1"
      Grid.Row="1" VerticalAlignment="Center"
      HorizontalAlignment="Right"/>
    <TextBlock Name="Label3" Text="$Y3" Grid.Column="1"
      Grid.Row="2" VerticalAlignment="Center"
      HorizontalAlignment="Right"/>
    <TextBlock Name="Label4" Text="$Y4" Grid.Column="1"
      Grid.Row="3" VerticalAlignment="Center"
      HorizontalAlignment="Right"/>
  </Grid>
</UserControl>

Com o gráfico do WPF criado, a última etapa no processo é garantir que você crie métodos para atualizar o gráfico em resposta às alterações de dados. Na classe SalesChart, o método UpdateWPFChart utiliza essencialmente um título de gráfico (nome da empresa) e duas matrizes de seqüência representando dados de vendas como parâmetros, bem como dimensiona o gráfico de acordo com esses dados e atualiza o texto do gráfico e os indicadores de barra, como mostra a Figura 11.

Figure 11 Código para atualizar gráfico (Close Figure 11)
public void UpdateWPFChart(string chartTitle,
  string[] xVals, double[] yVals)
{
  double maxY = 0;
  foreach (double y in yVals)
  {
    if (y > maxY)
      maxY = y;
  }
  try
  {
    for (int i = 0; i < 4; i++)
    {
      if ((i >= xVals.Length) || (i >= yVals.Length))
        break;
      else
      {
        TextBlock xText = (TextBlock)(FindName(
          String.Format("Category{0}", i + 1)));
        if (xText != null)
          xText.Text = xVals[i];
        TextBlock yText = (TextBlock)(FindName(
          String.Format("Label{0}", i + 1)));
        if (yText != null)
          yText.Text = yVals[i].ToString();
        Rectangle bar = (Rectangle)(FindName(
          String.Format("Bar{0}", i + 1)));
        bar.Width = (yVals[i] / maxY) *
          Grid1.ColumnDefinitions[1].Width.Value;
      }
    }
    Title.Text = chartTitle;
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message, "SalesChart", MessageBoxButton.OK);
  }
}

Nesse momento, todos os controles da região de formulário são concluídos; portanto, agora você pode retornar para o designer da região de formulário Replacement do Outlook, na qual adicionaremos os controles recém-criados e os conectaremos à nossa região de formulário personalizada.

Adicionando a última parte

A última etapa é adicionar os controles à região do formulário e então conectar o código na região de formulário Replacement personalizada do Outlook ao nosso controle SalesSearch e ao controle de gráficodo WPF. Para adicionar o controle SalesSearch e o controle do WPF à região de formulário, crie todos os componentes no seu projeto. Essa ação adicionará os controles à sua caixa de ferramentas, de modo que você poderá arrastá-los na superfície de criação. Arraste o controle SalesSearch e o controle do gráfico do WPF para onde deseja que eles permaneçam na superfície de criação da região do seu formulário. Com os controles posicionados, agora posso criar o código que conectará esses controles à região do formulário e um ao outro.

Primeiro, na classe RecentSales adicionei algumas declarações variáveis importantes representando as vendas (ano e quantidade de vendas) e dados da empresa:

partial class RecentSales
{
  string[] currentXData;
  double[] currentYData;
  string currentCompany;
  ...
}

Em segundo lugar, no método RecentSales_FormRegionShowing (que essencialmente invoca o código que você adicionar a ele quando o Outlook carregar a região de formulário personalizada), defini a visibilidade do gráfico de vendas do WPF como oculta por padrão quando o Outlook carrega a região do formulário:

private void RecentSales_FormRegionShowing(object sender,
  System.EventArgs e)
{
  salesChart1.Visibility = System.Windows.Visibility.Hidden;
}

Em seguida, o método UpdateChart (lembre-se desse método anteriormente) usa os dados de vendas e da empresa para chamar o método UpdateWPFChart e atualizar os dados apresentados no gráfico de vendas — as barras baseadas no WPF que indicam quantidades de vendas. Observe que também obtive um manipulador de eventos, que responde aos eventos de clique da caixa de seleção e chama o método UpdateChart. A Figura 12 mostra o código para o método UpdateChart e o evento CheckChanged da caixa de seleção.

Figure 12 Método UpdateChart e evento CheckChanged (Close Figure 12)
private void UpdateChart()
{
  if (showChart.Checked)
  {
    if ((currentXData != null) && (currentYData != null))
    {
      salesChart1.UpdateWPFChart(
      currentCompany, currentXData, currentYData);
      salesChart1.Visibility = System.Windows.Visibility.Visible;
    }
    else
    {
      salesChart1.Visibility = System.Windows.Visibility.Hidden;
    }
  }
  else
  {
    salesChart1.Visibility = System.Windows.Visibility.Hidden;
  }
}

private void checkBox1_CheckedChanged(object sender, EventArgs e)
{
  UpdateChart();
}

Por último, tenho um método para tratar os dados de eventos alterados no controle SalesSearch. Meu método atualiza os dados de vendas e da empresa e, em seguida, chama UpdateChart:

private void salesSearch1_SalesDataChangeEvent(object sender,
  SalesControlLibrary.SalesSearchEventArgs e)
{
  currentCompany = e.CompanyName;
  currentXData = e.XValues;
  currentYData = e.YValues;
  UpdateChart();
}

Criando e executando a região de formulário Replacement personalizada

Com o código agora posicionado, vamos criar e executar o projeto. Para criar e executar suplementos do Outlook, você precisa fechar todas as instâncias do Outlook. Se você abrir a visualização do Designer para a região de formulário personalizada, deverá ver algo semelhante à Figura 13. Agora, você pode criar e executar o projeto. O Visual Studio criará a solução e, ao fazer isso, iniciará o Outlook e executará o assembly do suplemento.

Figura 13 Região de formulário Replacement personalizada final
Figura 13 Região de formulário Replacement personalizada final(Clique na imagem para diminuir a exibição)
Figura 13 Região de formulário Replacement personalizada final
Figura 13 Região de formulário Replacement personalizada final (Clique na imagem para aumentar a exibição)

Para navegar na região de formulário Replacement personalizada do Outlook, clique em File, New e em Choose Forms. Isso abrirá a caixa de diálogo Choose Forms que relacionará todos os formulários Replacement personalizados instalados nesse cliente. Nesse caso, você verá que o formulário Recent Sales é o único disponível na biblioteca da região de formulário Replacement personalizada. Para abrir a região do formulário, selecione-a e clique em Open.

Nesse momento, o Outlook abrirá o formulário e o que você verá será algo semelhante à Figura 14. Para testar a sua região de formulário, clique no botão Search. Isso deveria carregar todos os dados no seu banco de dados (ou no caso do exemplo acima, o banco de dados Sales.mdb) na grade de dados. Então, você pode clicar na caixa de seleção Show Sales Data para exibir os dados de vendas da empresa selecionada. Observe que você pode selecionar a empresa diretamente na grade de dados ou pode usar a faixa de navegação na parte inferior do controle SalesSearch para navegar de volta e para a frente nos dados. Sempre que se mover nos dados, o gráfico de vendas será automaticamente atualizado. É excelente!

Figura 14 Região do formulário Replacement do Outlook personalizada final (com o gráfico do WPF visível)
Figura 14 Região do formulário Replacement do Outlook personalizada final (com o gráfico do WPF visível)(Clique na imagem para diminuir a exibição)
Figura 14 Região do formulário Replacement do Outlook personalizada final (com o gráfico do WPF visível)
Figura 14 Região do formulário Replacement do Outlook personalizada final (com o gráfico do WPF visível) (Clique na imagem para aumentar a exibição)

Leitura adicional

Você tem disponível todo o código fornecido neste artigo. Fique à vontade para experimentar como quiser. Além disso, criamos um laboratório prático online que apresentará um aplicativo semelhante ao descrito neste artigo caso queira alguma orientação sobre como criar regiões de formulário do Outlook. Você pode localizar esse laboratório prático no Portal do desenvolvedor do VSTO em msdn2.microsoft.com/aa905533. Também recomendo que você verifique a Central OBA (obacentral.com), um portal da Web dedicado a casos e ensino sobre OBA. Além disso, veja o meu novo livro, Programming Microsoft Office Business Applications (Microsoft Press®, 2008), que fornece informações detalhadas sobre como desenvolver e implantar OBAs. O OBA é uma área interessante no desenvolvimento do Office sobre a qual você certamente ouvirá mais no futuro.

Como uma observação final, gostaria de agradecer a Lori Turner (que auxiliou na codificação e no laboratório online), Mike Morton e Andrew Whitechapel pela revisão deste artigo.

Steve Fox é gerente de programas na equipe do Visual Studio Tools for Office e do Visual Studio Tools for Applications (VSTO) na Microsoft. Ele divide seu tempo trabalhando externamente com clientes VSTO, fornecendo treinamento em VSTO, apresentando palestras sobre VSTO em conferências e trabalhando internamente com a equipe de desenvolvimento para ajudar a aprimorar o produto VSTO.


Teste de unidade: Aplique o desenvolvimento controlado por testes aos projetos de banco de dados

Teste de unidade

Aplique o desenvolvimento controlado por testes aos projetos de banco de dados

Jamie Laflen
Este artigo aborda:
  • As vantagens do TDD
  • Uso dos testes de unidade no desenvolvimento de seu banco de dados
  • Combinação de linguagens compatíveis com T-SQL e .NET
  • Conexões, condições de testes e transações
Este artigo utiliza as seguintes tecnologias:
Visual Studio 2008, SQL Server
Download do código disponível em: DatabaseUnitTesting2008_Launch.exe (174KB)
Conteúdo
Teste de unidade no desenvolvimento de software
Teste da unidade de banco de dados
Definição de conexões
Gerenciamento de scripts T-SQL
Modificação do código de teste gerado
Transações
System.Transactions
Teste controlado por dados
Condições personalizadas de teste
Definição da condição de teste
Modificação da execução do teste
Olhando para o futuro

E
m novembro de 2006, a Microsoft lançou o Visual Studio® Team System Database Edition, também conhecido como DBPro ou Data Dude, que introduziu uma metodologia do ciclo de vida do produto ao desenvolvimento de banco de dados. O DBPro também introduziu um criador de testes de unidade para banco de dados que facilita a geração ou escrita de testes de unidade orientados para T-SQL, a fim de verificar os objetos de banco de dados antes da implantação. Neste artigo, explicarei em detalhes o teste da unidade de banco de dados, como o recurso foi implementado e mostrarei como executar o teste de unidade nos seus próprios projetos de banco de dados.

Teste de unidade no desenvolvimento de software

No desenvolvimento de software, o teste de unidade exerce um papel fundamental para garantir que o nível apropriado de qualidade seja mantido e que as metas sejam atingidas. Embora a maioria dos desenvolvedores concorde que os testes de unidade são importantes, a maioria não reserva um momento para escrever o teste de unidade no término do ciclo de um produto. Para solucionar esse problema, foi criada a metodologia de TDD (test-driven development, desenvolvimento orientado a testes). Usando essa metodologia, os desenvolvedores escrevem testes de unidade para recursos individuais antes de implementar de fato o recurso. Esse processo permite que o desenvolvedor veja a API de dentro para fora, proporcionando assim a oportunidade do design mais simples possível.

Atualmente, muitos desenvolvedores trabalhando em C# e Visual Basic® seguem o TDD ao criar um teste de unidade, a fim de utilizar um método em uma classe que ainda não foi escrita e então implementar a quantidade mínima de código necessário para compilar o teste. Assim que o teste for compilado e falhar, o desenvolvedor escreverá somente o código necessário para que o teste não falhe na próxima execução. Com cada iteração, todos os testes da unidade serão executados para verificar se não há erros de regressão. O código de teste da unidade e aplicativo são então verificados no controle de código-fonte. Cada teste de unidade é executado pelo menos duas vezes para o método sendo desenvolvido.

Para que seja útil, um teste de unidade deve ser configurado em seu ambiente de execução antes que seja executado, a fim de que possa verificar consistentemente o código que deve ser testado. Assim que o teste tiver sido implementado e verificado, ele será verificado no controle de código-fonte e será usado pelo restante da vida útil do produto, a fim de garantir que o método continue se comportando como previsto.

O TDD é mais desafiador no desenvolvimento do banco de dados por que você precisa fornecer um ambiente de teste consistente, a fim de verificar o código antes de verificar as alterações. Para que um teste de unidade de banco de dados seja executado com êxito, é necessário que ele tenha o esquema apropriado, bem como os dados adequados. Isso significa que para a execução dos testes, outros desenvolvedores devem duplicar o ambiente utilizado quando os testes de unidade foram criados. Isso é um pedido exigente a ser atendido sem a criação de uma infra-estrutura muito dispendiosa. Felizmente, o DBPro fornece recursos que permitem que os desenvolvedores de dados sigam o mesmo processo de teste de unidade que os outros desenvolvedores seguem, como neste cenário:

  1. Obtenha o mais recente teste e projetos de banco de dados do controle de código-fonte e adicione um novo procedimento armazenado ao projeto do banco de dados. Configure os parâmetros de entrada e saída do procedimento.
  2. Clique com o botão direito do mouse no novo procedimento armazenado e selecione “criar testes de unidade”. Adicione o teste de unidade ao projeto de teste existente. Uma nova classe de teste será adicionada ao projeto de teste com o T-SQL que executará o novo procedimento armazenado com parâmetros de entrada padrão.
  3. Modifique os parâmetros de entrada para que tenham os valores esperados e adicione uma condição de teste para verificar se o procedimento armazenado retorna os resultados esperados.
  4. Execute o teste. Como parte da configuração do teste, o projeto do banco de dados será implantado na instância local do SQL Server® e o plano de geração de dados será executado para preencher o banco de dados recentemente implantado com os dados de testes previstos. O teste é executado em relação a uma instância local do SQL Server e falha (como esperado porque a lógica do procedimento armazenado ainda não foi implementada).
  5. Implemente a lógica do procedimento armazenado e execute o teste novamente. O procedimento armazenado atualizado será implantado e o teste será aprovado.
  6. Execute todos os testes para verificar o banco de dados e, em seguida, verifique o procedimento armazenado e o código de teste da unidade no controle de código-fonte.

Os recursos fornecidos pelo DBPro incluem: a geração dos testes de unidade do T-SQL stub de funções existentes, procedimentos armazenados ou disparadores; implantação automática de atualizações do projeto do banco de dados a uma instância de proteção local; geração de dados como parte da configuração do ambiente de teste usando um plano de geração de dados; e execução de testes do T-SQL em relação a um banco de dados de destino. Embora esses recursos possam ser usados juntos, eles não são obrigatórios. Por exemplo, um teste de unidade de banco de dados pode ser escrito a partir do zero e a geração de dados não precisará ser executada antes da execução de cada teste.

Teste da unidade de banco de dados

O recurso de teste da unidade de banco de dados reflete dois objetivos concorrentes de certa forma: o desejo por parte do desenvolvedor de banco de dados de usar scripts conhecidos do T-SQL para expressar a lógica do teste de unidade e o desejo de aproveitar as poderosas linguagens destinadas ao Microsoft® .NET Framework, como C# ou Visual Basic. Esses objetivos concorrentes são conciliados com o encapsulamento do teste no código do T-SQL e, em seguida, com a hospedagem do T-SQL no código do C# ou Visual Basic .NET. O criador de teste de unidade gera ambos, permitindo que um deles (ou ambos) seja modificado para atender a necessidades individuais.

O teste da unidade de banco de dados com o Team System fornece ou é compatível com recursos que você gostaria se estivesse criando você mesmo uma estrutura de teste de banco de dados. Você precisa estar capacitado para executar um teste com o contexto apropriado. Por exemplo, você precisa utilizar as credenciais de sua camada Web que gostaria de usar ao se conectar ao banco de dados ou para poder usar um conjunto diferente de credenciais para acessar ou modificar as tabelas subjacentes antes e depois que um teste tiver sido executado. É útil poder executar um script antes e depois de cada teste em um “grupo de testes” para configurar ou redefinir o estado do banco de dados, assim como a capacidade de executar um script (para um teste individual) antes e depois de um teste para configurar ou redefinir o estado do banco de dados.

Finalmente, você deseja ampliar o conjunto de condições de teste disponíveis ao adicionar condições específicas para seu banco de dados, aproveitar as transações para controlar modificações no estado do banco de dados e usar dados para orientar várias execuções do seu teste com diferentes parâmetros de entrada e saída. Em alguns casos, para usar esses recursos você precisará de código adicional. Vejamos o que isso envolve.

Definição de conexões

Como eu mencionei, é importante que uma estrutura de teste de banco de dados permita a execução usando outra conexão que não seja a usada para verificar e configurar o banco de dados. Para oferecer suporte a essa meta, a Microsoft criou o conceito de um contexto de conexão que tenha todas as informações necessárias para definir a conexão e como ela deveria ser usada para executar o teste. O contexto de conexão base contém uma conexão aberta, a fábrica de provedor (msdn2.microsoft.com/system.data.common.dbproviderfactory), que será usada ao criar objetos ADO.NET adicionais, bem como o tempo limite e DbTransaction a serem usados na execução de um teste.

Usando o tipo de contexto, você pode definir duas instâncias de contexto – contexto de execução e contexto privilegiado – para oferecer suporte à configuração, à execução e à verificação de teste. Como já esclarece o nome, o contexto de execução é utilizado para executar o teste de unidade. Ele deve representar a configuração do aplicativo que utiliza o banco de dados. O contexto privilegiado é usado para verificar os testes de unidade, bem como executar todas as outras operações de modificação (incluindo a implantação e a geração de dados). As definições desses dois contextos são armazenadas no arquivo app.config do projeto de teste de amostra fornecido, em uma seção especial definida para o teste da unidade de banco de dados.

Gerenciamento de scripts T-SQL

De uma perspectiva de código, o teste de unidade de banco de dados gera um código C# ou Visual Basic .NET que se destina à estrutura da unidade de teste no Visual Studio. Na estrutura de teste de unidade, uma classe é utilizada como um contêiner para conter os testes (cada teste é definido como um método). Há várias oportunidades para adicionar scripts no criador de teste de unidade para compatibilidade com os recursos de script que descrevi. Todos os scripts, a não ser o script de teste, são executados usando o contexto privilegiado, pois eles se destinam à configuração ou à verificação do ambiente de teste. Um script será executado uma ou mais vezes, dependendo de onde foi adicionado. Portanto, vejamos os scripts em mais detalhes e como eles são executados (consulte a Figura 1).

Figure 1 Scripts de teste e sua execução (Close Figure 1)

Script | Execução
Inicialização do teste | Este script T-SQL é utilizado para configurar o ambiente de teste antes da execução de cada teste. Esse script é executado antes de cada teste na classe.
Configuração de pré-teste | A configuração antes de um teste específico ser executado. O script pré-teste está disponível de modo que você possa executar uma ação de configuração para o teste e verificar se o banco de dados está no estado apropriado para que o teste seja realizado.
Teste | O teste por si só.
Limpeza de pós-teste | A limpeza depois que um teste específico for executado. O script de pós-teste é fornecido de modo que você possa restaurar ou verificar o estado do banco de dados.
Limpeza de teste | Este script T-SQL é utilizado para restaurar o ambiente de teste depois que o teste for concluído. Esse script é executado depois de cada teste.

Agora, vejamos como criar, acessar e modificar scripts no criador de teste da unidade de banco de dados. Primeiro, crie um teste de unidade de banco de dados selecionado Novo teste no menu Teste e, em seguida, selecione o modelo de teste de unidade de banco de dados. Assim que o assistente tiver sido finalizado, você verá uma caixa de diálogo solicitando que você configure o projeto – cancele isso por enquanto (veremos isso posteriormente). No explorador de soluções, um novo arquivo intitulado DatabaseUnitTest1 será criado e o criador associado será aberto, de modo que você poderá criar o primeiro teste de unidade.

Na Figura 2, há três áreas interessantes. Primeiro, a área suspensa superior esquerda permite que você selecione entre scripts e testes comuns definidos na classe. Quando uma nova classe de teste for criada, um teste padrão intitulado DatabaseTest1 será adicionado à classe; você pode renomear esse teste padrão usando o botão Renomear e adicione outros testes usando o botão com o símbolo de mais verde.

Figura 2 Novo teste de unidade
Figura 2 Novo teste de unidade(Clique na imagem para diminuir a exibição)
Figura 2 Novo teste de unidade
Figura 2 Novo teste de unidade (Clique na imagem para aumentar a exibição)

A área suspensa à esquerda desse botão permite que você alterne entre scripts. Se scripts comuns tiverem sido selecionados na área suspensa à esquerda, os scripts de inicialização/limpeza de teste que forem executados antes e depois de cada testes estarão disponíveis para edição. Por outro lado, se for selecionado um teste, os scripts de pré/teste/pós estarão disponíveis para edição.

Em terceiro lugar, o painel de condições de teste lhe fornece acesso às condições de teste que funcionam com base no resultado da execução do script T-SQL, a fim de verificar se os resultados são os esperados. A área suspensa é habilitada assim que você clicar no hiperlink “Clique aqui para criar”. Essa área suspensa fornece acesso às condições de teste registradas na sua máquina.

Como você pode ver a partir da interface de usuário, há um padrão consistente para todos os scripts. Em qualquer parte em que você definir um script, também poderá definir uma ou mais condições de teste que podem ser usadas para verificar se o script foi executado como você esperava. A verificação de testes é fundamental ao escrever o código de teste de unidade. Não é suficiente executar apenas parte do código; você também precisa verificar se a execução foi como você esperava.

Nesse ponto, você provavelmente está se perguntando como o teste sabe à qual banco de dados se conectar e do que se trata o contexto privilegiado que mencionei. Bem, para responder a essas dúvidas você precisa voltar para a caixa de diálogo que pedi que você cancelasse anteriormente. Para acessar a caixa de diálogo, selecione Projeto de teste e vá para Teste | Configuração do teste de banco de dados (consulte a Figura 3).

Figura 3 Configuração do teste de unidade
Figura 3 Configuração do teste de unidade(Clique na imagem para diminuir a exibição)
Figura 3 Configuração do teste de unidade
Figura 3 Configuração do teste de unidade (Clique na imagem para aumentar a exibição)

Na primeira opção suspensa, selecione a seqüência de conexão que será usada para executar o seu teste. Essa seqüência de conexão provavelmente utilizará a autenticação Windows® por padrão, mas você pode especificar a autenticação SQL Server. Se você o fizer, a senha será removida, criptografada e armazenada no Registro (a senha será recuperada do Registro durante a execução do teste).

Na segunda área suspensa, especifique um conjunto diferente de credenciais para a modificação e a verificação do estado do banco de dados. Por exemplo, se você quiser executar os seus testes usando as credenciais que o aplicativo utilizará (por exemplo, a camada Web se comunicando com o banco de dados), poderá utilizar um conjunto diferente de credenciais com privilégios superiores para verificar o estado do banco de dados. Todos os outros scripts, incluindo a implantação e a geração de dados, utilizarão essa seqüência de conexão. Se você não especificar uma seqüência de conexão, a seqüência de conexão de execução será utilizada.

Na terceira área suspensa, selecione um banco de dados a ser implantado antes da execução de quaisquer testes. Se você selecionar um projeto de banco de dados (e selecionar a configuração padrão), uma compilação diferenciadora será executada e a alteração implantada no banco de dados especificado na seqüência de conexão de execução. Se você não tiver feito alterações, essa será uma operação rápida (graças às alterações no DBPro SP1), pois o MSBuild e o script de implantação detectarão que não há alterações a serem feitas. Depois que os testes tiverem sido executados, você poderá ver os resultados da implantação de banco de dados clicando no hiperlink, na parte superior da janela da ferramenta de resultados do teste.

Na última área suspensa, selecione um plano de geração de dados que deverá ser implantado antes da execução do teste. Esse plano será executado sempre que os seus testes forem executados. Portanto, você desejará configurar uma pequena quantidade de dados, a fim de reduzir o tempo que levará para que seus testes comecem de fato a execução.

Depois de clicar em OK na caixa de diálogo, as informações serão armazenadas no arquivo app.config do projeto de teste, como mostra a Figura 4. Até esse ponto, mostrei a você os principais recursos apresentados pela interface do usuário – vejamos agora o código de fato.

Figure 4 App.config (Close Figure 4)
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="DatabaseUnitTesting"
      type="Microsoft.VisualStudio.TeamSystem.Data
        .UnitTesting.Configuration.DatabaseUnitTestingSection,
        Microsoft.VisualStudio.TeamSystem.Data.UnitTesting,
        Version=9.0.0.0, Culture=neutral,
        PublicKeyToken=b03f5f7f11d50a3a" />
  </configSections>
  <DatabaseUnitTesting>
    <DataGeneration ClearDatabase="true" />
    <ExecutionContext
      Provider="System.Data.SqlClient"
      ConnectionString="Data Source=(local)\sqlexpress
        ;Initial Catalog=Database1Sandbox;Integrated
        Security=True;Pooling=False"
      CommandTimeout="60"/>
    <PrivilegedContext
      Provider="System.Data.SqlClient"
      ConnectionString="Data Source=(local)\sqlexpress
      ;Initial Catalog=Database1Sandbox;Integrated
      Security=True;Pooling=False" />
  </DatabaseUnitTesting>
</configuration>

Modificação do código de teste gerado

A API de teste de unidade foi criada para atender à sua necessidade de alterar ou aprimorar o modo como a estrutura de teste funciona. A Microsoft previa que os usuários desejariam modificar o código gerado, escrever suas próprias condições de teste e substituir o serviço de teste padrão por sua própria implementação. A Figure 5 relaciona as classes que compõem a API de teste de unidade do banco de dados. (Para obter mais informações, acesse também msdn2.microsoft.com/microsoft.visualstudio.teamsystem.data.unittesting.)

Figure 5 Classes na API de teste da unidade de banco de dados (Close Figure 5)

Classe | Finalidade
TestCondition | A classe base para todas as condições de teste usadas para verificar os scripts de teste. Uma subclasse de Component, é compatível com o tempo de design e a execução do tempo de execução. No tempo de design, configure a condição de teste com os valores que você espera durante a execução de teste e no tempo de execução use esses valores do tempo de design para verificar os resultados do teste real.
DatabaseTestClass | A classe base para todos os testes da unidade do banco de dados no Visual Studio. A definição dessa classe integra-se no ambiente de design do Visual Studio e é responsável pela serialização do código em InitializeComponent.
ConnectionContext | Encapsula uma conexão a um banco de dados.
DatabaseTestActions | Essa classe é um componente, uma representação do tempo de design de um teste de unidade do banco de dados. As instâncias dessa classe hospedam as instâncias do DatabaseTestAction que envolvem um teste.
ExecutionResult | Representa o resultado da execução de um lote durante a execução de teste.
DatabaseTestAction | Essa classe é um componente. As instâncias dessa classe contêm um script e uma coleção de condições de teste.
DatabaseTestService | A implementação padrão da estrutura do teste. Ela implementa a implantação do banco de dados, a geração de dados, a criação de execução e contextos de conexão privilegiada e execução de teste.

A instância padrão do serviço de teste é acessada por meio de uma propriedade estática em DatabaseTestClass. Se você quiser, poderá substituir a implementação padrão pela sua própria e substituir qualquer uma das implementações anteriores.

Há vários motivos pelos quais você pode desejar modificar o código Visual Basic ou C# gerado. Os dois cenários mais comuns são executar os seus testes de unidade em uma transação e aproveitar o recurso de teste orientado a dados da estrutura de teste da unidade.

Transações

Os testes de unidade executados em um ambiente compartilhado devem procurar não poluir o ambiente de execução; a modificação de dados ou esquemas previstos pode fazer com os testes subseqüentes falhem inesperadamente. Os autores dos testes de unidade de banco de dados devem ter um cuidado especial, pois é muito fácil cometer esse tipo de erro.

Dito isso, não sendo uma surpresa para qualquer desenvolvedor de banco de dados, o modo como os autores de teste de unidade de banco de dados evitam a modificação do banco de dados subjacente é por meio do uso de transações. A seguir, mostrarei três formas de aproveitar as transações. No T-SQL executado, use esta transação:

begin transaction

  -- Execute test

  -- We can clean up when the test passes successfully; otherwise, it
  -- is up to the engine to clean up the transaction when the
  -- connection is closed

rollback transaction

Na transação ADO.NET, execute o código mostrado na Figura 6. Para usar o System.Transactions, veja a Figura 7.

Figure 7 Uso de System.Transactions (Close Figure 7)
[TestMethod()]
public void SystemTransactionsExample()
{
  using (TransactionScope transactionScope =
    new System.Transactions.TransactionScope(
    TransactionScopeOption.Required))
  {
    base.ExecutionContext.Connection.EnlistTransaction(
      Transaction.Current);
    base.PrivilegedContext.Connection.EnlistTransaction(
      Transaction.Current);

    DatabaseTestActions testActions = this.SystemTransactionsExampleData;

    // Execute the pre-test script
    Trace.WriteLineIf((testActions.PretestAction != null),
      "Executing pre-test script...");
    ExecutionResult[] pretestResults = TestService.Execute(
      this.PrivilegedContext,
      this.PrivilegedContext,
      testActions.PretestAction);

    // Execute the test script
    Trace.WriteLineIf((testActions.TestAction != null),
      "Executing test script...");
    ExecutionResult[] testResults = TestService.Execute(
      this.ExecutionContext,
      this.PrivilegedContext,
      testActions.TestAction);

    // Execute the post-test script
    Trace.WriteLineIf((testActions.PosttestAction != null),
      "Executing post-test script...");
    ExecutionResult[] posttestResults = TestService.Execute(
      this.PrivilegedContext,
      this.PrivilegedContext,
      testActions.PosttestAction);
  }
}
Figure 6 Transação ADO.NET (Close Figure 6)
[TestMethod()]
public void ADONetTransactionSample()
{
  base.ExecutionContext.Transaction =
    base.ExecutionContext.Connection.BeginTransaction();
  try
  {
    DatabaseTestActions testActions = this.ADONetTransactionSampleData;

    // Execute the test script
    Trace.WriteLineIf(
      (testActions.TestAction != null),
      "Executing test script...");
    ExecutionResult[] testResults = TestService.Execute(
      this.ExecutionContext,
      this.ExecutionContext,
      testActions.TestAction);
  }
  finally
  {
    base.ExecutionContext.Transaction.Rollback();
  }
}

Dessas três opções, as transações T-SQL e ADO.NET funcionam como se você não estivesse usando os scripts de pré/pós-teste para configurar o banco de dados ou verificar os resultados do teste; lembre-se, os scripts pré/pós são executados usando uma conexão diferente, de modo que suas modificações não serão revertidas. Usando System.Transactions, as conexões privilegiadas e de execução já foram criadas, de modo que devem estar relacionadas na transação ambiente criada quando o TransactionScope foi instanciado, mas quaisquer alterações serão revertidas.

System.Transactions

Como usar as transações ambiente (como no exemplo System.Transactions mostrado anteriormente) é a solução mais abrangente, vejamos mais sobre essa solução. (Para obter mais informações, você também pode acessar msdn2.microsoft.com/ms172152.) A transação ambiente é usada em duas conexões diferentes que podem ter diferentes vidas úteis. Portanto, o serviço de DTC (Distributed Transaction Coordinator, Coordenador de transações distribuídas) deve ser usado para gerenciar o tempo de vida da transação. Isso significa que para usar System.Transactions, o serviço de DTC do banco de dados deve ser iniciado do teste para que seja aprovado. Se o serviço não for iniciado, o seu teste falhará com uma exceção como a seguinte:

Test method TestSamples.DatabaseUnitTest1.SystemTransactionsExample threw exception:
System.Data.SqlClient.SqlException: MSDTC on server '(LOCAL)\SQLEXPRESS' is unavailable.

A modificação de cada método para utilizar uma transação ambiente é, no mínimo, entediante; é mais provável que você queira agrupar todos os testes de transação juntos em uma classe e então modificar a classe de modo que uma transação seja iniciada e interrompida para cada teste (veja a Figura 8).

Figure 8 Uso de transações para cada teste (Close Figure 8)
TransactionScope _ambientTransaction;

[TestInitialize()]
public void TestInitialize()
{
  // Create the transaction prior to the
  // connections being created.  When the
  // connections are created they will
  // automatically enlist in the transaction
  ambientTransaction = new TransactionScope(
    TransactionScopeOption.Required);

  base.InitializeTest();
}

[TestCleanup()]
public void TestCleanup()
{
  // Since Complete() was not called, disposing
  // of the ambient transaction will cause all
  // changes to be rolled back
  ambientTransaction.Dispose();

  base.CleanupTest();
}

Teste controlado por dados
Team System for Database Professionals: recursos

Ao reduzir os riscos envolvidos na realização de alterações no esquema do banco de dados, você pode controlar melhor a integridade do banco de dados durante todo o processo de desempenho. Veja alguns recursos que irão ajudá-lo:

  • Rename Refactoring Support for Database Objects
  • Schema Compare to Keep Two Schema Versions Synchronized
  • Data Compare to Keep the Data in Two Databases Synchronized
  • Use Offline Database Projects for Change Isolation

Pequenas alterações feitas no banco de dados podem ter um impacto significativo em outras áreas do aplicativo sem que o impacto seja imediatamente visível. Como tal, é válido determinar antecipadamente o impacto das alterações do esquema. Estes recursos do Team System o auxiliarão:

  • Funcionalidade de teste de unidade extensível
  • Gerador de dados para definir conjuntos de dados de teste repetidos
  • Novo designer para criar testes de unidade em T-SQL ou código gerenciado

Uma das outras vantagens do DBPro é que, como parte da família Team System, ele é integrado ao Team Foundation Server para fornecer aos membros da equipe visibilidade completa em todos os artefatos de desenvolvimento e atividades em um projeto. Isso resulta na redução de problemas relacionados à comunicação insuficiente e na melhoria de produtividade e eficácia da equipe.

  • Obtenha uma visão unificada do seu projeto com o Team Explorer
  • Gerencie as alterações nos artefatos do projeto com o Team Foundation Version Control
  • Comunique e gerencie o trabalho da equipe com o Team Foundation Work Item Tracking
  • Colabore usando o Team Portal
  • Defina seu processo de desenvolvimento usando os modelos de processo do Team Foundation
  • Integre de forma regular o trabalho da sua equipe com o Team Foundation Build
  • Tome decisões em tempo real baseadas em desempenho e em métricas de qualidade com Reporting and Business Intelligence
  • Suporte para equipes de desenvolvimento externas e distribuídas

A estrutura de teste fornecida com o Visual Studio 2005 permite que você execute um teste de unidade várias vezes, cada vez com um conjunto diferente de parâmetros de entrada. A documentação descrevendo como usar essa estrutura pode ser localizada em "Codificando um teste de unidade orientado a dados" em msdn2.microsoft.com/ms182527 e em "Passo a passo: usando um arquivo de configuração para definir uma fonte de dados" em msdn2.microsoft.com/ms243192.

A API do teste de unidade de banco de dados é compatível com o teste orientado a dados, permitindo que você forneça zero ou mais instâncias DbParameter para o método Execute. Para compreender como as instâncias DbParameter fornecidas são usadas, considere o script de teste SQL como sendo um SQL parametrizado que faz referência diretamente às instâncias DbParameter como variáveis no código. Um exemplo é mostrado na Figura 9.

Figure 9 Fornecimento de instâncias DbParameter (Close Figure 9)
[DataSource ("System.Data.SqlClient", "Data Source=(local)\\sqlexpress" +
  ";Initial Catalog=TestDB_DataDriven;Integrated Security=True",
  "DecisionAllocationTestData", DataAccessMethod.Sequential), TestMethod()]
public void DataDriveTestSample()
{
  // Setup and execute the test
  DatabaseTestAction test = new DatabaseTestAction();

  DbParameter customerID = CreateParameter(
    "@customerID", DbType.Int32,
    ParameterDirection.Input, "CustomerID");
  DbParameter offerID = CreateParameter(
    "@offerID", DbType.Int32,
    ParameterDirection.Input, "OfferID");
  DbParameter decision = CreateParameter(
    "@decision", DbType.StringFixedLength,
    ParameterDirection.Input, "Decision");
  DbParameter actualVault = CreateParameter(
    "@vault", DbType.Int32,
    ParameterDirection.Output);
  DbParameter actualBroker = CreateParameter(
    "@broker", DbType.Int32,
    ParameterDirection.Output);
  DbParameter actualMarket = CreateParameter(
    "@market", DbType.Int32,
    ParameterDirection.Output);

  test.SqlScript = @"
    declare @rc int
    execute @rc = AllocatePositionsByDecision
    @customerID,
    @offerID,
    @decision,
    @vault out,
    @broker out,
    @market out
  ";

  DatabaseTestClass.TestService.Execute(
    base.ExecutionContext, base.PrivilegedContext,
    test,customerID, offerID, decision, actualVault,
    actualBroker, actualMarket);

  // Verify the results
  int expectedVault = (int) base.TestContext.DataRow["ExpectedVault"];
  int expectedBroker = (int)base.TestContext.DataRow["ExpectedBroker"];
  int expectedMarket = (int)base.TestContext.DataRow["ExpectedMarket"];

  Assert.AreEqual(expectedVault, actualVault.Value, "Vault incorrect");
  Assert.AreEqual(expectedBroker, actualBroker.Value, "Broker incorrect");
  Assert.AreEqual(expectedMarket, actualMarket.Value, "Market incorrect");
}

private DbParameter CreateParameter(string paramName,
  DbType paramType, ParameterDirection paramDirection)
{
  return CreateParameter(paramName, paramType, paramDirection, null);
}

private DbParameter CreateParameter(string paramName,
  DbType paramType, ParameterDirection paramDirection,
  string dataColumnName)
{
  DbParameter newParameter =
    base.ExecutionContext.Provider.CreateParameter();
  newParameter.DbType = paramType;
  newParameter.Direction = paramDirection;
  newParameter.ParameterName = paramName;

  if (paramDirection == ParameterDirection.Input ||
    paramDirection == ParameterDirection.InputOutput)
  {
    newParameter.Value = base.TestContext.DataRow[dataColumnName];
  }

  return newParameter;
}

Como você pode ver na Figura 9, o método de teste é decorado com um atributo que informa à estrutura de teste o que será chamado para cada linha de dados recuperados da tabela DecisionAllocationTestData, no banco de dados TestDB_DataDriven, na minha instância SQL Server Express local. A classe DatabaseTestAction representa uma ação em um teste composto por um script de teste para execução e teste das condições, a fim de avaliar os resultados. Você observará que criei cinco instâncias DbParameter representando as variáveis referenciadas no script de teste; é importante observar que alguns desses parâmetros são parâmetros de saída. O método CreateParameter é apenas um pequeno método auxiliar que cria a instância e opcionalmente a preenche com dados do contexto de teste.

O script de teste que você verá é definido para fazer referência às instâncias DbParameter que estou fornecendo como argumentos ao método Execute. Se você estiver familiarizado com o código ADO.NET, verá que isso é simplesmente SQL parametrizado.

Em seguida, chamo o método Execute na instância TestService padrão, passando as instâncias DbParameter como argumentos. Depois da execução, verificarei os resultados. Isso é simplesmente uma questão de recuperar os valores esperados de DataRow e compará-los aos parâmetros de saída.

O exemplo na Figura 9 demonstra que a API de teste da unidade de banco de dados pode ser diretamente almejada em vez de usar o criador. Nesse exemplo, estou usando dados recuperados de uma tabela de teste para acionar a execução de um procedimento armazenado; o procedimento armazendo sendo testado utiliza os primeiros três valores de entrada para calcular o número de compartilhamentos que serão recuperados de vários locais para satisfazer aos critérios do cliente. Esse cálculo é complexo, depende somente das entradas e tem saídas simples. Portanto, faz sentido escrever esse teste como acionado por dados em vez de duplicar o mesmo teste muitas vezes variando somente os dados.

Nesse ponto, cobri várias formas diferentes pelas quais você pode modificar ou usar a API existente para aproveitar transações ou criar testes orientados a dados. A API de teste de unidade de banco de dados pode ser ampliada ou modificada de várias formas seja adicionando condições adicionais de teste ou criando seu próprio serviço de teste. Portanto, vejamos agora como você pode ampliar a estrutura.

Condições personalizadas de teste

O recurso de teste de unidade de banco de dados é fornecido com seis condições de teste que podem ser usadas para verificar a execução do teste da sua unidade. Embora essas condições de teste sejam suficientes para o uso básico, a maioria dos usuários irá detectar rapidamente que elas precisam verificar um conjunto de resultados em vez de um único valor; portanto, criarei duas condições adicionais de teste. Essas duas condições de teste, mais o LoggingTestService, são parte adicional de um novo conjunto de ferramentas potentes para o Visual Studio 2008 lançado pela equipe Team System Data. Para baixar as novas poderosas ferramentas, acesse go.microsoft.com/fwlink/?Linkid=107300.

A primeira condição de teste é ChecksumCondition. No momento da criação, ela gera um hash de resultados esperados. No momento da execução, os resultados reais estão em hash e o valor resultante é comparado ao resultado esperado. O segundo, ExpectedSchemaCondition, coleta e armazena o esquema previsto no momento da criação. No momento da execução, o esquema real é comparado ao esquema previsto.

Ao utilizar condições de teste, um usuário cria uma classe de teste de unidade de banco de dados e adiciona as condições de teste a partir de uma lista de condições disponíveis (uma referência ao assembly contendo as novas condições é adicionada automaticamente quando a nova classe de teste é adicionada ao projeto).

O usuário pode acessar a configuração da condição de teste pressionando as elipses na propriedade de Configuration. A caixa de diálogo resultante, mostrada na Figura 10, permite que o usuário especifique e execute a consulta que recupera os dados esperados; nessa figura, os dados exibidos são aleatórios (sem sentido) gerados pelo DBPro Data Generator. Quando exibido pela primeira vez, o T-SQL do teste será fornecido na caixa de texto. O usuário pode então modificar o código de teste para refletir a consulta que deseja de fato e, em seguida, clicar no botão Recuperar para executar o código no servidor e exibir os resultados. Assim que o usuário estiver satisfeito com os resultados de acordo com o previsto, clicará em OK e a condição de teste será configurada automaticamente.

Figura 10 Configuração da condição de teste compartilhado
Figura 10 Configuração da condição de teste compartilhado(Clique na imagem para diminuir a exibição)
Figura 10 Configuração da condição de teste compartilhado
Figura 10 Configuração da condição de teste compartilhado (Clique na imagem para aumentar a exibição)

O usuário não precisará mais interagir com a condição. Agora que você tem uma idéia de como será o resultado final, poderá criar essas condições de teste. Ao desenvolver as condições de teste anteriores, o volume de código interessante está no comportamento do tempo de design. Apresentarei os pontos mais importantes na seção a seguir; o código completo está disponível em msdn.microsoft.com/msdnmag/code08.aspx.

Definição da condição de teste

Ao definir uma nova condição de teste, considerei mais fácil definir primeiro o esqueleto e o registro da condição e então verificar se o esqueleto funciona antes de desenvolver de fato a lógica da condição. Como as duas condições de teste de amostra são muito semelhantes, defini uma classe base comum, como você pode ver na Figura 11. Assim que o projeto foi compilado, defini o arquivo XML de registro, intitulado UnitTesting.Samples.extensions.xml, mostrado na Figura 12.

Figure 12 UnitTesting.Samples.extensions.xml (Close Figure 12)
<?xml version="1.0" encoding="utf-8"?>
<extensions
  assembly="UnitTesting.Samples, Version=1.0.0.0,
    Culture=neutral, PublicKeyToken=382814fd17d42dea" version="1"
  xmlns="urn:Microsoft.VisualStudio.TeamSystem.Data.Extensions"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="urn:Microsoft.VisualStudio.TeamSystem
    .Data.Extensions
    Microsoft.VisualStudio.TeamSystem.Data.Extensions.xsd">
  <extension
    type="UnitTesting.Samples.TestConditions.ExpectedSchemaCondition"
    enabled="true"/>
  <extension type=" UnitTesting.Samples.TestConditions.ChecksumCondition"
    enabled="true"/>
</extensions>
Figure 11 Classe base comum definida (Close Figure 11)
namespace UnitTesting.Samples.TestConditions
{
  public abstract class DataSetTestCondition : TestCondition
  {
    //...
  }
}

namespace UnitTesting.Samples.TestConditions
{
  [DisplayName("Checksum")]
  public class ChecksumCondition : DataSetTestCondition
  {
    // ...
  }
}

namespace UnitTesting.Samples.TestConditions
{
  [DisplayName("Expected schema" )]
  public class ExpectedSchemaCondition : DataSetTestCondition
  {
    // ...
  }
}

Gosto de adicionar o arquivo de extensões diretamente no projeto e defini-lo para que seja copiado automaticamente para o diretório de saída, de modo que seja fácil consultá-lo e usá-lo durante o desenvolvimento e a implantação. A etapa final é configurar a implantação automática copiando o arquivo de extensões no diretório DBPro e instalando o assembly das condições de teste no GAC (Global Assembly Cache, cache de assembly global). Isso pode ser realizado por meio de uma etapa de pós-compilação ou pela execução de um prompt de comando sem privilégios elevados. Utilizei o seguinte script na etapa de pós-compilação do meu projeto:

set TargetNameVar=$(TargetName)
set DevEnvDirVar=$(DevEnvDir)
set WindowsSDKVar=$(ProgramFiles)\Microsoft SDKs\Windows\V6.0A

copy /Y %TargetNameVar%.extensions.xml "%DevEnvDirVar%..\..\DBPro\*"
copy /Y %TargetNameVar%.pdb "%DevEnvDirVar%PublicAssemblies\*"
"%WindowsSDKVar%\Bin\gacutil" /if %TargetNameVar%.dll

Se você quiser executar o script como um arquivo .bat separado, modifique as primeiras três linhas para recuperar valores de variáveis do ambiente (em vez de variáveis MSBuild).

Depois que a configuração inicial tiver sido concluída, posso ver as duas novas condições na área suspensa de condições de teste. Acrescentei o DisplayNameAttribute (msdn2.microsoft.com/system.componentmodel.displaynameattribute) às condições de teste, de modo que apareçam como Checksum e Expectedschema. Agora que concluí a configuração, preciso desenvolver a experiência do tempo de criação das condições de teste de modo que o usuário possa clicar nas elipses no navegador de propriedades e configurar a instância da condição.

Se você tiver criado componentes para o Windows Forms no Visual Studio, então provavelmente já está familiarizado com o UITypeEditor no .NET Framework. O UITypeEditor (msdn2.microsoft.com/system.drawing.design.uitypeeditor) e o EditorAttribute (msdn2.microsoft.com/system.componentmodel.editorattribute) permitem que o desenvolvedor de componentes crie uma experiência de edição personalizada e atribua essa experiência (por meio do EditorAttribute) a uma propriedade de componente criada no IDE. Nesse caso, quero que a experiência seja uma caixa de diálogo modal, então defino o editor como na Figura 13. Como ambas as condições de teste estão configuradas da mesma forma, defino a propriedade na classe base:

Figure 13 Definição do estilo do editor (Close Figure 13)
namespace UnitTesting.Samples.TestConditions.Design
{
  class DataSetEditor : UITypeEditor
  {
    public override UITypeEditorEditStyle
      GetEditStyle(ITypeDescriptorContext context)
    {
      return UITypeEditorEditStyle.Modal;
    }

    public override object EditValue(
      ITypeDescriptorContext context,
      IServiceProvider provider, object value)
    {
      MessageBox.Show("DataSetEditor::EditValue");
      return value;
    }
  }
}

[DesignOnly(true)]
[Editor(
  typeof(UnitTesting.Samples.TestConditions.Design.DataSetEditor),
  typeof(System.Drawing.Design.UITypeEditor))]
public string Configuration
{
    get { return "Press to configure"; }
    set { }
}

Quando compilo e implanto as condições de teste, vejo a propriedade Configuration e, quando clico nas elipses, a caixa de mensagem é exibida. Nesse ponto, todo o código necessário para integração com o Visual Studio foi concluído e posso me concentrar na lógica das condições de teste.

Primeiro, crio um formulário – que será exibido pelo DataSetEditor que acabei de criar – que permite que o usuário configure o DataSet previsto pela condição de teste. Esse formulário (intitulado DataSetSelectorDialog) é implantado com o T-SQL a partir do teste e permite que o usuário execute esse T-SQL em um servidor de destino, a fim de visualizar os dados no servidor de destino para visualizar os dados antes da configuração da condição de teste. Assim que o usuário estiver satisfeito com DataSet, poderá clicar em OK na caixa de diálogo, sinalizando que a condição de teste deve usar DataSet e retornar o controle ao editor.

Em seguida, modifico o editor para exibir e processar o conteúdo do formulário depois que o usuário clicar em OK. Geralmente, um editor de interface do usuário passa o valor de volta, o qual deveria ser armazenado na propriedade sendo editada, mas, nesse caso, quero que a propriedade exiba o texto "Pressione aqui para configurar" e passe de volta o DataSet para a condição de teste editada. Como as condições processariam diferentemente o DataSet, defino um método virtual na classe base e faço com que o editor chame esse método para definir a instância DataSet; as classes derivadas substituem o método para adicionar sua implementação específica. Com essas alterações, o método EditValue agora será semelhante à Figura 14. Assim, recupero o IUIService para exibir uma caixa de diálogo modal; esse serviço se integra à exibição da caixa de diálogo modal com o restante do comportamento de janelas do Visual Studio.

Figure 14 Método EditValue (Close Figure 14)
public override object EditValue(
ITypeDescriptorContext context, IServiceProvider provider, object value)
{
  IUIService uiService = (IUIService)provider
    .GetService(typeof(IUIService));
  DataSetTestCondition cond = context.Instance as DataSetTestCondition;

  if (uiService != null && cond != null)
  {
    DataSetSelectorDialog dsDialog = new DataSetSelectorDialog();

    if (cond.Site != null)
    {
      dsDialog.Text = string.Format(CultureInfo.CurrentCulture,
        "Configuration for {0}", cond.Site.Name);
    }

    dsDialog.Query = cond.GetTestScript();
    dsDialog.StartPosition = FormStartPosition.CenterParent;

    if (uiService.ShowDialog(dsDialog) == DialogResult.OK)
    {
      value = dsDialog.Query;
      if (cond != null)
      {
        cond.ConfigureExpectedDataSet(dsDialog.Result);
      }
    }
  }

  return value;
}

Agora que você já viu o comportamento do tempo de design das condições de teste, vejamos o que acontece quando o script de teste é executado no servidor de destino e os resultados dessa execução são coletados em um DataSet. Depois que o teste for executado, uma instância da classe ExecutionResult será criada e preenchida com informações sobre a execução do teste (incluindo o DataSet). A estrutura de teste faz então um loop por meio de todas as condições de teste configuradas, a fim de verificar os resultados da execução e chamar o método Assert em cada condição de teste para verificar esses resultados. No caso das condições de teste, nesse exemplo, elas recuperam o DataSet do primeiro ExpectedResult na matriz de resultados e, seguida, ChecksumCondition gera um hash do DataSet real e compara esse hash com o hash gerado durante o tempo de design. A ExpectedSchemaCondition desserializa o DataSet vazio que representa as tabelas de dados esperados (e suas colunas) e os compara com o DataSet gerado pelo teste.

Modificação da execução do teste

O comportamento padrão para seqüências de conexão, implantação do banco de dados e execução de teste é acessado por meio da instância da classe DatabaseTestService que é registrada na propriedade TestService na DatabaseTestClass. Os métodos na classe DatabaseTestService são marcados como virtuais; portanto, eles podem ser substituídos individualmente ou todos juntos para alterar como os testes são executados. Você pode desejar modificar o comportamento padrão se, por exemplo, estiver usando um mecanismo de criptografia/descriptografia da seqüência de conexão personalizada, você estará modificando como a implantação do banco de dados ou geração de dados é realizada ou estará registrando automaticamente os resultados da execução.

Cada uma dessas modificações (e mais) podem ser atingidas ao escrever sua própria classe de serviço de teste e então registrar esse serviço de teste com a estrutura, de modo que possa ser usado durante a execução do teste. Nesta amostra, escreverei um serviço de teste que registrará os resultados de uma execução de teste em um arquivo usando a serialização DataSet. O serviço pode ser configurado para não registrar resultados, registrar todos os resultados ou somente os resultados com falha. Esse simples serviço facilita a visualização do resultado da execução dos seus testes na estrutura.

Primeiro, defino a classe; estou simplesmente modificando o modo como os testes são executados; portanto, a classe será semelhante à apresentada na Figura 15. Como você pode ver, o LoggingTestService cria uma subclasse da classe DatabaseTestService e, em seguida, substitui o método Execute. Em seguida, preciso adicionar o registro em log dos resultados. Observe que a implementação padrão do Execute executa o script de teste e, em seguida, avalia as condições de teste; no entanto, essa implementação não atenderá às minhas necessidades, pois não tenho acesso aos resultados da execução na hipótese de falha na verificação. Portanto, para atender aos meus objetivos, em vez disso avaliarei as condições de teste atribuídas depois de chamar o método Execute e então registrar em log os resultados. A implementação completa de Execute será semelhante à apresentada na Figura 16.

Figure 16 Implementação de Execute (Close Figure 16)
public override ExecutionResult[] Execute(
  ConnectionContext scriptExecutionContext,
  ConnectionContext privilegedExecutionContext,
  DatabaseTestAction action,
  params System.Data.Common.DbParameter[] sqlParameters)
{
  // We want to evaluate the conditions ourselves so that we
  // can log the results from every test's execution
  List<TestCondition> conditions = new List<TestCondition>();
  if (action != null)
  {
    conditions.AddRange(action.Conditions);
    action.Conditions.Clear();
  }

  // Execute the test
  ExecutionResult[] results = base.Execute(
    scriptExecutionContext,
    privilegedExecutionContext,
    action,
    sqlParameters);

  // Verify and log the results
  bool verificationFailed = false;
  try
  {
    foreach (TestCondition condition in conditions)
    {
      if (condition.Enabled)
      {
        condition.Assert(privilegedExecutionContext.Connection, results);
      }
    }
  }
  catch
  {
    verificationFailed = true;
    throw;
  }
  finally
  {
    if (results != null &&
      (Verbosity == LogVerbosity.AllResults ||
      (verificationFailed && Verbosity == LogVerbosity.OnlyFailure)))
    {
      // Serialize the dataset to an .xml file to be visualized
      string resultPrefix = Guid.NewGuid().ToString("N");

      for (int resultIndex = 0; resultIndex < results.Length;
        resultIndex++)
      {
        string dsFilePath = Path.GetFullPath(string.Format(
          CultureInfo.CurrentCulture,
          "{0}-{1}.xml", resultPrefix, resultIndex));

        ExecutionResult result = results[resultIndex];
        result.DataSet.WriteXml(dsFilePath,
          System.Data.XmlWriteMode.IgnoreSchema);

        Trace.TraceInformation("Log {0} {1}", resultIndex, dsFilePath);
      }
    }
  }

  return results;
}
Figure 15 Classe LoggingTestService (Close Figure 15)
namespace UnitTesting.Samples
{
  public class LoggingTestService : DatabaseTestService
  {
    public override ExecutionResult[] Execute(
      ConnectionContext scriptExecutionContext,
      ConnectionContext privilegedExecutionContext,
      DatabaseTestAction action,
      params System.Data.Common.DbParameter[] sqlParameters)
    {

      // Execute the test
      ExecutionResult[] results = base.Execute(
        scriptExecutionContext,
        privilegedExecutionContext,
        action,
        sqlParameters);

      return results;
    }
  }
}

Essa implementação ainda usa a implementação Execute base, mas posteriormente ela verifica os resultados usando as condições de teste da ação e grava os resultados dessa execução em um arquivo. Estou usando uma GUID para gerar um nome único, pois esse método será chamado várias vezes durante uma única execução de teste. Também é importante verificar se há resultados nulos, pois é comum que o Execute seja chamado sem ação.

Assim que o serviço tiver sido compilado em um assembly, ainda precisarei registrá-lo para que seja usado durante a execução de teste. No meu caso, quero que o serviço seja registrado para todos os testes em meu assembly mas somente para registrar em log a falha de um teste; portanto, modifiquei o método IntializeAssembly para que seja semelhante ao seguinte:

[AssemblyInitialize()]
public static void IntializeAssembly(TestContext ctx)
{
  LoggingTestService testService = new LoggingTestService();
  testService.Verbosity = LoggingTestService.LogVerbosity.OnlyFailure;
  DatabaseTestClass.TestService = testService;

  // Setup the test database based on setting in the
  // configuration file
  DatabaseTestClass.TestService.DeployDatabaseProject();
  DatabaseTestClass.TestService.GenerateData();
}

Agora, quando eu executar um teste de unidade com falha, o caminho para o arquivo de log será a saída para o diretório de execução de teste e o caminho para o arquivo estará na janela de resultados de teste desse teste.

Para verificar os resultados que causaram a falha do teste, criei um aplicativo muito simples do Windows Forms (está no download do código de amostra que o acompanha) que desserializa e exibe o DataSet. Antes de concluir, devo mencionar que a estrutura de teste do Visual Studio executa cada assembly de teste em seu próprio AppDomain. Como a instância do serviço é registrada por meio de uma propriedade estática, o registro do serviço persistirá somente durante o tempo de vida do AppDomain. Isso significa que você precisará registrar o serviço de teste durante a inicialização de cada assembly de teste. Além disso, para obter mais informações sobre como fazer alterações no esquema do banco de dados, consulte a barra lateral, “Team System for Database Professionals: recursos”.

Olhando para o futuro

Apresentei vários recursos do DBPro neste artigo, mas a melhor forma de compreender o que você precisa fazer é colocar isso em prática. Sugiro que você experimente o produto, incluindo o uso da API de geração de dados e as novas potentes ferramentas do Visual Studio 2008. Se você tiver problemas ou dúvidas, o fórum estará sempre aberto em: forums.microsoft.com/MSDN/ShowForum.aspx?ForumID=725&SiteID=1. Além disso, leia o whitepaper de Sachin Rekhi, intitulado “Database Unit Testing with Team Edition for Database Professionals” em msdn2.microsoft.com/bb381703.

Jamie Laflen é Líder técnico da equipe DBPro; ele é responsável pelos testes de unidade, geração de dados e recursos de criação/implantação. Antes de fazer parte da equipe de desenvolvimento, Jamie fazia parte do Microsoft Services e ajudava os clientes na implementação de aplicativos .NET no Visual Studio.


F# Primer: Uso de t&#233;cnicas de programa&#231;&#227;o funcionais no .NET Framework

F# Primer

Uso de técnicas de programação funcionais no .NET Framework

Ted Neward
Este artigo aborda:
  • Instalando a F#
  • Fundamentos da linguagem F#
  • .Interoperabilidade .NET
  • F# assíncrona
Este artigo utiliza as seguintes tecnologias:
.NET Framework, F#
Conteúdo
Por que usar a F#?
Instalando a F#
“Hello, F#”
A expressão Let
A palavra-chave “For”
O pipeline
A F# também cria objetos
F# assíncrona
Ajustando-se à linguagem F#

Um
membro recente da família Microsoft® .NET Framework, a F# fornece segurança de tipos, desempenho e habilidade de trabalhar como uma linguagem de script, tudo parte do ambiente .NET. Essa linguagem funcional foi criada por Don Syme da Microsoft Research como uma variante OCaml de sintaxe compatível ao CLR, mas a F# está se movendo rapidamente do laboratório para a vida prática.

Conforme a programação funcional surge em linguagens mais importantes como C# e Visual Basic® através de tecnologias como genéricos do .NET e LINQ, a visibilidade da F# cresceu na comunidade .NET – tanto que em novembro de 2007 a Microsoft anunciou que daria à linguagem F# a estabilidade das linguagens de programação .NET suportadas.

Durante anos, a área das linguagens funcionais (ML, Haskell, etc.) tem sido considerada mais apropriada para a pesquisa acadêmica do que para o desenvolvimento profissional. Não é que essas linguagens não sejam interessantes. Na verdade, algumas melhorias importantes no .NET – genéricos, LINQ, PLINQ e Futures, por exemplo – vêm da aplicação dos conceitos de programação funcional às linguagens que nunca os viram antes. A falta de interesse nessas linguagens se baseia mais no fato de que elas almejavam plataformas de pouca relevância para os desenvolvedores que escreviam programas para Windows®, elas não se integravam bem com a plataforma subjacente, ou não suportavam funcionalidades-chave, como acesso a banco de dados relacional, análise XML e mecanismos de comunicação fora do processo.

Entretanto, o CLR e sua abordagem "muitas linguagens, uma plataforma" tornou inevitável que mais dessas linguagens entrassem no mundo do desenvolvimento para Windows. Também era igualmente inevitável que elas começassem a serem notadas pelos programadores iniciantes. A F# é uma dessas linguagens. Neste artigo, apresentarei a você alguns dos conceitos por trás e as vantagens da F#. Em seguida, para ajudá-lo a começar a usar a F#, darei orientações de instalação e criação de alguns programas simples.

Por que usar a F#?

É óbvio para uma pequena porcentagem de programadores .NET que aprender uma linguagem funcional para o .NET Framework é um passo à frente na criação de softwares poderosos. Para o restante, a motivação para aprender a F# é um mistério completo. Como os desenvolvedores se beneficiam da F#?

Escrever programas simultâneos e seguros se tornou a preocupação principal nos últimos três anos, conforme as CPUs de vários núcleos se tornaram mais populares. As linguagens funcionais ajudam os desenvolvedores a dar suporte à simultaneidade, estimulando a existência de estruturas de dados imutáveis que podem ser passadas entre threads e máquinas sem a preocupação com a segurança do thread ou o acesso atômico. As linguagens funcionais também tendem a facilitar a escrita de melhores bibliotecas compatíveis com simultaneidade, tais como os fluxos de trabalho assíncronos da F#, que examinarei mais adiante neste artigo.

Embora não pareça que tais programadores tenham muita experiência no desenvolvimento orientado a objeto, os programas funcionais são freqüentemente mais simples de escrever e manter que certos tipos de aplicativos. Considere, por exemplo, a necessidade de escrever um programa para converter um documento XML em um formato diferente de dados. Embora fosse certamente possível escrever um programa em C# que analisasse o documento XML e aplicasse uma variedade de instruções “if” para determinar quais ações tomar em pontos diferentes no documento, uma abordagem indiscutivelmente de alto nível seria escrever a transformação como um programa XSLT (eXtensible Stylesheet Language Transformation). Como era de se esperar, a XSLT possui várias funcionalidades dentro dela, como o SQL.

A F# desestimula fortemente o uso de valores nulos e estimula o uso de estruturas de dados imutáveis. Isso ajuda a reduzir a freqüência dos erros na programação, diminuindo a quantidade necessária de código de caso especial.

Os programas escritos em F# também tendem a ser mais sucintos. Você realmente digita menos, em ambos os sentidos do termo: menos pressionamentos de tecla e menos lugares em que o compilador deve ser informado de qual tipo de variável, argumentos ou tipo de retorno deve haver. Isso pode significar muito menos código a ser mantido.

A F# tem um perfil de desempenho semelhante à C#. Entretanto, ela tem um perfil de desempenho muito melhor do que linguagens sucintas semelhantes, em particular as linguagens dinâmicas e de script. E, como muitas linguagens dinâmicas, a F# inclui as ferramentas que permitem que você explore dados, escrevendo fragmentos de programa e executando-os interativamente.

Instalando a F#

Disponível para download gratuito em research.microsoft.com/fsharp/fsharp.aspx, a F# instala não somente todas as ferramentas de linha de comando mas também um pacote de extensão do Visual Studio® que fornece realce de sintaxe de cor, modelos de projeto e de arquivo (incluindo um exemplo muito detalhado do código da F#, como um guia de inicialização), e suporte a IntelliSense®. Também existe um shell interativo da F#, que pode ser executado dentro do Visual Studio, permitindo aos desenvolvedores usar expressões a partir das janelas do arquivo de código-fonte, colá-las na janela do shell interativo, e ver os resultados imediatos do trecho do código – em algo como uma janela Immediates aprimorada.

Enquanto escrevo este artigo, a F# é executada como uma ferramenta externa dentro do Visual Studio, o que significa que falta um pouco da otimização que os desenvolvedores obtêm com o C# ou Visual Basic. A F# também não possui suporte ao designer de página ASP.NET, entre outras coisas. (Isso não quer dizer que a F# não possa ser usada no ASP.NET – muito longe disso. Mas simplesmente que o suporte ao Visual Studio da F# não oferece o mesmo tipo de experiência de desenvolvimento de arrastar e soltar da F# que é oferecido em C# e no Visual Basic).

Dito isso, entretanto, a versão atual da F# pode ser usada em qualquer lugar que outra linguagem compatível com .NET possa ser usada. Nas próximas páginas veremos alguns exemplos.

“Hello, F#”

A introdução inevitável a qualquer linguagem acontece através do programa universal "Hello, World". Na F# não é diferente:

printf "Hello, world!"

Embora seja um pouco anticlimático, esse pequeno exemplo mostra que a F# pertence à categoria das linguagens que não requerem um ponto de entrada explícito (ao contrário da C#, Visual Basic e C++/CLI); a linguagem presume que a primeira linha do programa é o ponto de entrada e começa a execução a partir daí.

Para executá-lo, o desenvolvedor da linguagem F# possui duas opções: compilado ou interpretado. Executá-lo dentro do interpretador da F# (fsi.exe) é trivial. Simplesmente execute fsi.exe na linha de comando e insira a linha acima no prompt resultante, como mostrado na Figura 1.

Figura 1 Executando o 'Hello, World' de dentro do interpretador da F#
Figura 1 Executando o 'Hello, World' de dentro do interpretador da F#(Clique na imagem para diminuir a exibição)
Figura 1 Executando o 'Hello, World' de dentro do interpretador da F#
Figura 1 Executando o 'Hello, World' de dentro do interpretador da F# (Clique na imagem para aumentar a exibição)

Observe que no shell a instrução precisa ser terminada com dois pontos-e-vírgulas. Esta é uma sutileza do modo interativo e não é necessária para programas F# compilados.

Para executar esse exemplo como um executável standard.NET, inicie o Visual Studio como sempre e crie um novo projeto F# (que você encontrará em Other Project Types). Um projeto F# consiste, no início, em um único arquivo de origem F# , chamado file1.fs. Abrir esse arquivo revela uma grande coleção de códigos F# de exemplo. Observe seu conteúdo, apenas para ter uma idéia de como é a sintaxe da F#. Ao terminar, substitua todo o arquivo pelo código "Hello, world!" mostrado anteriormente. Execute o aplicativo e, como era de se esperar, "Hello, world!" aparece em uma janela do aplicativo do console.

Se você preferir uma linha de comando, poderá compilar o código usando a ferramenta fsc.exe encontrada no subdiretório \bin do diretório de instalação da F#. Observe que o fsc.exe age como a maioria dos compiladores de linha de comando, pegando a origem na linha de comando e produzindo um executável como resultado. Muitas das opções de linha de comando são documentadas, embora muitas já devam ser familiares para você, caso tenha experiência com os compiladores csc.exe ou cl.exe. Entretanto, observe que uma área em que a F# está atrasada no momento é no uso do MSBuild; não há suporte direto na instalação atual (1.9.3.7 enquanto escrevo este arquivo) para a compilação direcionada ao MSBuild.

Se você preferir que o "Hello, world!" seja um pouco mais gráfico, a F# oferece facilmente fidelidade e interoperabilidade completas com a plataforma CLR subjacente, incluindo as bibliotecas do Windows Forms. Experimente fazer isso:

System.Windows.Forms.MessageBox.Show "Hello World"

Ter a habilidade de usar a Biblioteca de classes do .NET Framework assim como as bibliotecas F#, torna a linguagem F# atrativa para a comunidade matemática e científica que já está usando linguagens funcionais como OCaml ou Haskell e para o corpo existente dos desenvolvedores internacionais do .NET.

A expressão Let

Vamos observar um código F# que não é tão trivial como o tradicional "Hello, world!". Considere o seguinte:

let results = [ for i in 0 .. 100 -> (i, i*i) ]
printfn "results = %A" results

Um elemento curioso nessa sintaxe da F# é a expressão let. Essa é a expressão mais importante da linguagem. Formalmente, let atribui um valor a um identificador. A tentação do desenvolvedor de Visual Basic e de C# seria converter essa expressão, já que ela "define uma variável". Essa seria uma presunção falsa. Em vez disso, os identificadores na linguagem F# incorporam dois princípios. Primeiro, um identificador, uma vez definido, nunca pode ser alterado (é dessa maneira que a linguagem F# ajuda os programadores a criar programas à prova de concorrência - porque ela desencoraja o estado mutável). Segundo, o identificador pode ser não somente um tipo de objeto ou primitivo, como visto em C# e no Visual Basic, mas também um tipo de função, semelhante ao que você vê no LINQ.

Observe também como os identificadores nunca são definidos explicitamente como se tivessem um tipo. O identificador de resultados, por exemplo, nunca é definido; ele é inferido a partir do lado direito da expressão que o segue. Isso é conhecido como inferência de tipos, e representa a habilidade do compilador de analisar o código, determinar o valor de retorno, e conectá-lo automaticamente. (Isso é semelhante às novas expressões de tipo de inferência de C# através da palavra-chave var.)

A expressão let não precisa trabalhar somente com dados. Você pode usá-la para definir funções, que a linguagem F# reconhece como conceitos de primeira classe. Sendo assim, o exemplo a seguir define uma função de adição que usa dois parâmetros, a e b:

let add a b =
    a + b

A implementação faz exatamente o que você esperada dela: adiciona a e b e retorna implicitamente o resultado para o chamador. Isso significa que, tecnicamente, cada função em F# retorna um valor, mesmo que esse valor não seja um valor, conhecido pelo nome especial unit. Isso terá algumas implicações interessantes no código da linguagem F#, particularmente onde ele faz interseção com a biblioteca de classes do .NET Framework; mas, por enquanto, os desenvolvedores de C# e de Visual Basic podem pensar na unidade como se fosse void.

Há vezes em que uma função deve ignorar um parâmetro passado para ela. Para fazer isso em F#, simplesmente use o sublinhado como um espaço reservado do próprio parâmetro:

let return10 _ =
    add 5 5

// 12 is effectively ignored, and ten is set to the resulting
// value of add 5 5
let ten = return10 12

printf "ten = %d\n" ten

Como muitas linguagens funcionais, a F# permite currying, onde a aplicação de uma função pode ser definida apenas parcialmente, contando com sua invocação para fornecer o restante dos parâmetros:

let add5 a =
    add a 5

Em certo sentido, isso é semelhante a criar um método sobrecarregado que usa um conjunto de parâmetros diferentes e chamadas para outro método:

public class Adders {
    public static int add(int a, int b) { return a + b; }
    public static int add5(int a) { return add(a, 5); }
}

Mas existe uma diferença sutil. Observe que, na versão da linguagem F#, não há tipos definidos explicitamente. Isso significa que o compilador fará a mágica da inferência de tipos, determinará se o parâmetro para add5 é compatível com tipo ao ser adicionado a um literal inteiro 5, e compilará como tal ou sinalizará um erro. Na verdade, muito da linguagem F# é implicitamente de tipo parametrizado (ou seja, faz uso de genéricos).

No Visual Studio, passar o ponteiro sobre a definição de “tem” mostrada anteriormente revelará que seu tipo é declarado como:

val ten : ('a -> int)

Em F# isso significa que “tem” é um valor, uma função que usa um parâmetro de qualquer tipo, e produz um resultado int. A sintaxe de escala é o equivalente aproximado da sintaxe <T> em C#, portanto, a conversão mais próxima para uma função C# seria dizer que “tem” parece com uma instância delegada para um método de tipo parametrizado cujo tipo você realmente gostaria de ignorar (mas não pode de acordo com as regras de C#):

delegate int Transformer<T>(T ignored);

public class App
{
  public static int return10(object ignored) { return 5 + 5; }

  static void Main()
  {
    Transformer<object> ten = return10;
    System.Console.WriteLine("ten = {0}", return10(0));
  }
}

A palavra-chave “For”

Agora vamos examinar a palavra-chave “for” no primeiro exemplo:

#light

let results = [ for i in 0 .. 100 -> (i, i*i) ]
printfn "results = %A" results

Começando da parte superior do código, observe a sintaxe #light. Isso é uma concessão para os programadores não-OCaml que estão mudando para a linguagem F#, deixando alguns dos requisitos da linguagem OCaml e usando espaço em branco significativo para definir blocos de código. Embora o espaço em branco não seja necessário, ele torna a sintaxe mais fácil de analisar para o desenvolvedor médio que está mudando de C# ou Visual Basic e, portanto, ele aparece freqüentemente nos exemplos da linguagem F# e nos trechos de código postados, tornando-o o padrão, de fato, da programação em F#. (Uma versão futura da linguagem F# pode realmente tornar a #light a sintaxe padrão, e não o contrário).

Esse loop “for” aparentemente inocente, na verdade, não é nem um pouco simples. Oficialmente, trata-se de uma lista gerada, que é uma forma elegante de dizer que se trata de um bloco de código que produzirá uma lista como resultado.

Uma lista é uma construção primitiva encontrada freqüentemente em linguagens funcionais, e nesse sentido é semelhante em muitas formas a uma matriz. Entretanto, uma lista não permite o acesso baseado em posição (tal como a sintaxe a[i] tradicional em C#). As listas são mostradas em uma variedade de lugares na programação funcional e, na maior parte, podem ser consideradas como o equivalente na linguagem F# para a List<T> do .NET Framework com alguns recursos aprimorados.

Uma lista é sempre de algum tipo particular e, nesse caso, os resultados de um identificador é uma lista de tuplas, especialmente o tipo de tupla identificado pela F# como sendo do tipo (int * int). Essa idéia de uma lista de tuplas é familiar, se pensada como equivalente a um par de colunas retornadas por uma instrução SELECT em SQL. Portanto, o exemplo cria essencialmente uma lista de pares de inteiros que possui 100 itens.

Comumente, em linguagens funcionais, as definições da função são usadas em qualquer lugar em que o próprio código possa aparecer. Sendo assim, se quisesse estender o exemplo anterior, você escreveria:

let compute2 x = (x, x*x)
let compute3 x = (x, x*x, x*x*x)
let results2 = [ for i in 0 .. 100 -> compute2 i ]
let results3 = [ for i in 0 .. 100 -> compute3 i ]

Essa idéia de loop através de uma lista (ou matriz ou alguma outra construção que pode ser iterada) é uma tarefa comum em linguagens funcionais que foi generalizada como uma chamada de método básica: List.iter. Isso simplesmente chama uma função em cada elemento da lista. Outras funções de biblioteca semelhantes oferecem recursos úteis. Por exemplo, List.map usa uma função como um argumento e aplica a função a cada elemento da lista, retornando uma nova lista no processo.

O pipeline

Vamos examinar mais uma construção em F# – o operador pipeline – que usa os resultados de uma função e, em uma veia semelhante a pipes dos shells de comando (como o Windows PowerShell®), usa-os como a entrada para uma função de acompanhamento. Considere o trecho de F# mostrado na Figura 2. Esse código usa o namespace System.Net para se conectar a um servidor HTTP, acessar o HTML correspondente e analisar os resultados.

Figure 2 Recuperando e analisando o HTML (Close Figure 2)
/// Get the contents of the URL via a web request
let http(url: string) =
  let req = System.Net.WebRequest.Create(url)
  let resp = req.GetResponse()
  let stream = resp.GetResponseStream()
  let reader = new System.IO.StreamReader(stream)
  let html = reader.ReadToEnd()
  resp.Close()
  html
let getWords s = String.split [ ' '; '\n'; '\t'; '<'; '>'; '=' ] s
let getStats site =
  let url = "http://" + site
  let html = http url
  let words = html |> getWords
  let hrefs = html |> getWords |> List.filter (fun s -> s = "href")
  (site,html.Length, words.Length, hrefs.Length)

Observe o identificador de palavras na definição de getStats. Ele usa o valor html retornado da URL e aplica a função getWords a ele. Eu também poderia ter escrito a definição como:

let words = getWords html

As duas são idênticas. Mas o identificador de hrefs mostra o poder do operador pipeline: nele você pode inserir um número arbitrário de aplicações. Aqui eu uso a lista resultante de palavras e a coloco na função List.filter, que procura anonimamente em uma função a palavra href e a retorna se a expressão for verdadeira. E, para finalizar bem, os resultados da chamada getStats será outra tupla, ou seja, uma (string * int *int * int). Para escrevê-la usando C# será necessário muito mais do que 15 linhas de código.

O exemplo da Figura 2 também mostra mais da compatibilidade da linguagem F# com o .NET Framework, um tema que você pode ver repetido aqui:

open System.Collections.Generic
let capitals = Dictionary<string, string>()
capitals.["Great Britain"] <- "London"
capitals.["France"] <- "Paris"
capitals.ContainsKey("France")

Isso é realmente nada mais do que exercitar o tipo Dictionary<K,V>, mas demonstra como os genéricos são especificados em F# (usando os colchetes angulares, como em C#), como usar indexadores em F# (usando a sintaxe de colchete, novamente como em C#), e como executar métodos do .NET (usando o "ponto" e parênteses, como em C#). Na verdade, a única novidade aqui é como fazer a atribuição de valores mutáveis, que usa o operador seta esquerda. Isso é necessário porque a linguagem F#, como a maioria das linguagens funcionais, reserva o uso do operador igual para comparação, mantendo a noção matemática de que se x = y, então x e y são do mesmo valor, e não de que y está sendo atribuído a x. (Matemáticos de verdade são famosos por torcerem o nariz – ou rirem histericamente – da idéia de que x = x + 1 poderia ser verdadeiro em qualquer universo real ou imaginário.)

A F# também cria objetos

Obviamente, nem todos os desenvolvedores de .NET que estão se mudando para a linguagem F# desejarão imediatamente adotar os conceitos funcionais. Na verdade, muitos desenvolvedores que estão mudando para a linguagem F# a partir de C# ou Visual Basic precisarão saber que podem voltar aos velhos hábitos sem quebrar a linguagem. E de certa forma, eles podem.

Considere, por exemplo, a definição de classe do vetor bidimensional mostrado na parte superior da Figura 3. Algumas idéias interessantes surgem a partir daí. Primeiro, observe que não há corpo de construtor explícito; os parâmetros na primeira linha indicam os parâmetros pelos quais as pessoas construirão as instâncias de Vector2D, servindo essencialmente como um construtor. O tamanho do identificador, junto com os identificadores dx e dy, portanto, se tornam elementos privados dentro do tipo Vector2D, enquanto a palavra-chave do membro indica os membros que devem estar disponíveis fora de Vector2D, através do acesso à propriedade padrão do .NET. Em essência, esse código F# declara o que você vê na parte inferior da Figura 3 (como reportado pelo Reflector).

Figure 3 Variações de vetor em F# e C# (Close Figure 3)
            VECTOR2D IN F#
type Vector2D(dx:float,dy:float) =
    let length = sqrt(dx*dx + dy*dy)
    member obj.Length = length
    member obj.DX = dx
    member obj.DY = dy
    member obj.Move(dx2,dy2) = Vector2D(dx+dx2,dy+dy2)

VECTOR2D IN C# (REFLECTOR>
[Serializable, CompilationMapping(SourceLevelConstruct.ObjectType)]
public class Vector2D
{
  // Fields
  internal double _dx@48;
  internal double _dy@48;
  internal double _length@49;

  // Methods
  public Vector2D(double dx, double dy)
  {
    Hello.Vector2D @this = this;
    @this._dx@48 = dx;
    @this._dy@48 = dy;
    double d = (@this._dx@48 * @this._dx@48) +
               (@this._dy@48 * @this._dy@48);
    @this._length@49 = Math.Sqrt(d);
  }

  public Hello.Vector2D Move(double dx2, double dy2)
  {
    return new Hello.Vector2D(this._dx@48 + dx2, this._dy@48 + dy2);
  }

  // Properties
  public double DX
  {
    get
    {
      return this._dx@48;
    }
  }

  public double DY
  {
    get
    {
      return this._dy@48;
    }
  }

  public double Length
  {
    get
    {
      return this._length@49;
    }
  }
}

Lembre-se de que a linguagem F#, como muitas linguagens funcionais, encoraja valores e estado imutáveis. Isso deveria ser óbvio quando se observa o código na Figura 3, já que todas as propriedades são somente leitura, e o membro Move não modifica o Vector2D existente, mas, em vez disso, cria um novo a partir do Vector2D atual e aplica os valores modificados nele antes de retorná-lo.

Observe, também, que a versão da F# não é totalmente à prova de thread, mas é totalmente acessível a partir do código C# ou Visual Basic. Essa é uma forma fácil de começar a usar a linguagem F#: usando-a para definir objetos comerciais ou outros tipos que precisam ser à prova de thread e imutáveis. E embora seja certamente possível criar tipos em F# que ofereçam o conjunto usual de operações mutáveis (definir propriedades e afins), ela requer mais trabalho e uso do que a palavra-chave mutável para fazê-lo. Em um mundo em que as preocupações com a concorrência estão cada vez mais na ordem do dia, essa é exatamente a maneira certa de ser – imutável por padrão, mutável quando necessário ou desejado.

Criar tipos em F# é interessante, mas também é possível usar a linguagem F# para fazer o que o código C# ou Visual Basic pode fazer, tal como criar um aplicativo Windows Forms simples e coletar entradas do usuário, como mostrado na Figura 4.

Figure 4 Windows Forms com a F# (Close Figure 4)
#light

open System
open System.IO
open System.Windows.Forms
open Printf

let form = new Form(Text="My First F# Form", Visible=true)

let menu = form.Menu <- new MainMenu()
let mnuFile = form.Menu.MenuItems.Add("&File")
let filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*"

let mnuiOpen =
  new MenuItem("&Open...",
    new EventHandler(fun _ _ ->
      let dialog =
        new OpenFileDialog(InitialDirectory="c:\\",
          Filter=filter;
          FilterIndex=2,
          RestoreDirectory=true)
        if dialog.ShowDialog() = DialogResult.OK then
          match dialog.OpenFile() with
          | null -> printf "Could not read the file...\n"
          | s ->
            let r = new StreamReader(s)
            printf "First line is: %s!\n" (r.ReadLine());
            s.Close();
    ),
    Shortcut.CtrlO)

mnuFile.MenuItems.Add(mnuiOpen)

[<STAThread>]
do Application.Run(form)

Qualquer desenvolvedor familiarizado com o Windows Forms poderá reconhecer rapidamente o que está acontecendo aqui: um formulário simples é criado; algumas propriedades, populadas; um manipulador de eventos, preenchido; e o aplicativo é informado para ser executado até o usuário clicar no botão Close no canto direito superior. Itens padrão como aplicativos .NET permitem que o foco permaneça inteiramente na sintaxe da linguagem F#.

A instrução aberta opera quase da mesma forma que a linguagem C# usando instrução, essencialmente abrindo um namespace .NET a ser usado sem qualificadores formais. O namespace Printf é original da F# , tecnicamente uma porta do módulo OCaml do mesmo nome. A linguagem F# vem não somente com fidelidade completa com a biblioteca de classes do .NET Framework, mas também com uma porta mais direta das bibliotecas OCaml, fazendo os programadores familiarizados com essa linguagem se sentirem mais em casa com o .NET Framework. (Para os que estão curiosos, Printf reside dentro do assembly FSharp.Core.dll.) Nada impede você de usar System.Console.WriteLine – é simplesmente uma questão de preferência estética.

A criação do identificador de formulários usa os parâmetros nomeados da F#, o que é equivalente a instanciar o objeto e fazer uma série de chamadas ao conjunto de propriedades para popular essas propriedades com valores. Fazer o mesmo com o identificador de caixas de diálogo cria algumas linhas abaixo dele.

A definição do identificador mnuiOpen contém uma construção interessante, que não será tão estranha para os desenvolvedores familiarizados com delegações anônimas do .NET Framework 2.0 ou expressões lambda do .NET Framework 3.5. Na construção do EventHandler associado ao Open MenuItem, você vê uma função anônima, definida com a sintaxe:

fun _ _ -> ...

Como delegações anônimas, isso cria uma função que será invocada quando o item de menu for selecionado, mas a sintaxe é um pouco complicada.

A definição da parte EventHandler da definição MenuItem é uma função anônima que ignora os dois parâmetros passados para ela, que correspondem ordenadamente ao remetente e aos argumentos de evento no tipo delegado padrão EventHandler. A função estipula que um novo OpenFileDialog será exibido, e quando O.K. for clicado, os resultados serão examinados... de alguma forma:

if dialog.ShowDialog() = DialogResult.OK then
    match dialog.OpenFile() with
    | null -> printf "Could not read the file...\n"
    | s ->
        let r = new StreamReader(s) in
        printf "First line is: %s!\n" (r.ReadLine());
        s.Close();

Os resultados são examinados por meio de uma correspondência de padrões, um recurso poderoso do mundo da linguagem funcional. Ostensivamente semelhante em certo sentido a um switch/case da linguagem C#, a correspondência de padrões faz exatamente o que seu nome sugere: ela compara um valor com uma variedade de padrões diferentes – nem todos precisam ser valores de constantes – e executa o bloco de código correspondente. Sendo assim, por exemplo, no bloco matemático mostrado aqui, o resultado de OpenFile é comparado com dois valores possíveis: nulo, significa que não foi possível abrir nenhum arquivo; ou “s”, que é atribuído a qualquer valor não-nulo e, em seguida, usado como o construtor para StreamReader abrir e ler a primeira linha do arquivo de texto determinado.

A correspondência de padrões é uma parte significativa da maioria das linguagens funcionais e merece um pouco de explicação. Um dos seus usos mais comuns é em conjunto com um tipo de união discriminado, vagamente remanescente de um tipo enumerado de C# ou Visual Basic:

// Declaration of the 'Expr' type
type Expr =
  | Binary   of string * Expr * Expr
  | Variable of string
  | Constant of int

// Create a value 'v' representing 'x + 10'
let v = Binary("+", Variable "x", Constant 10)

Isso é comumente usado em linguagens funcionais para criar representações importantes de linguagens de domínio específico que os desenvolvedores podem usar para escrever construções mais complicadas e poderosas. Por exemplo, não é difícil imaginar a extensão dessa sintaxe para criar uma linguagem computacional completa que pode ser facilmente estendida simplesmente pela adição de novos elementos ao tipo Expr. Um cuidado que se deve tomar aqui: a sintaxe que usa o caractere * não indica que usamos multiplicação, mas é um padrão entre as linguagens funcionais para indicar um tipo que consiste em várias partes.

Na verdade, as linguagens funcionais são comumente usadas para escrever ferramentas de programação orientadas à linguagem, tais como interpretadores e compiladores, com o tipo Expr finalmente se tornando o conjunto completo de tipos de expressões de linguagem. A linguagem F# torna isso ainda mais simples, incluindo duas ferramentas, fslex e fsyacc, especificamente projetadas para usar entradas de linguagem tradicionais – arquivos lex e yacc – e compilá-las no código F# para facilitar a manipulação. Baixe o instalador da linguagem F# para explorá-lo, se você tiver interesse; em particular, o exemplo Parsing na distribuição padrão da F# fornecerá uma boa estrutura básica de onde começar.

A união discriminada é apenas um dos benefícios da correspondência de padrões; o segundo é a execução das expressões, e você vê isso na Figura 5. O rec na definição de eval é necessário para informar ao compilador F# que eval será invocada recursivamente no corpo da definição. Sem ele, a linguagem F# ficará esperando que uma função aninhada local chamada eval esteja presente. Eu uso a função getVarValue para retornar alguns valores predefinidos para as variáveis. Em um aplicativo da vida real, getVarValue provavelmente examinaria um dicionário em busca dos valores a serem retornados, como estabelecido na hora em que a variável foi criada.

Figure 5 Execução da expressão (Close Figure 5)
let getVarValue v =
    match v with
    | "x" -> 25
    | "y" -> 12
    | _ -> 0

let rec eval x =
    match x with
    | Binary(op, l, r) ->
        let (lv, rv) = (eval l, eval r) in
        if   (op = "+") then lv + rv
        elif (op = "-") then lv - rv
        else failwith "E_UNSUPPORTED"
    | Variable(var) ->
        getVarValue var
    | Constant(n) ->
        n

do printf "Results = %d\n" (eval v)

Quando eval é chamada, ela usa o valor “v” e descobre que ele é um valor binário. Isso coincide com a primeira subexpressão, que por sua vez vincula o valor (lv, rv) aos resultados avaliados das partes esquerda e direita do valor binário que acabamos de examinar. O valor não nomeado (lv, rv) é uma tupla, essencialmente um valor único de partes múltiplas, que não é diferente de um conjunto relacional ou struct C.

Quando eval l é chamada pela primeira vez, l da instância binária é do tipo variável, sendo assim, a chamada recursiva à eval coincide com essa ramificação do bloco de correspondência de parâmetros. Isso, por sua vez, chama getVarValue, que retorna o 25 codificado, que é finalmente vinculado ao valor lv. A mesma seqüência é executada para r, que é uma constante contendo o valor 10, portanto, ela é vinculada à rv. O restante do bloco é executado, um bloco if/else-if/else, que pode ser facilmente lido por um desenvolvedor familiarizado com C#, Visual Basic ou C++.

O mais importante a ser reconhecido aqui é que, mais uma vez, cada expressão retorna um valor, mesmo dentro de um bloco de correspondência de padrões. Nesse caso, o valor de retorno é um valor inteiro, o valor da operação, o valor recuperado da variável ou a própria constante. Esse único ponto, mais do que qualquer outro, parece atrair os desenvolvedores mais acostumados com uma programação orientada a objeto ou imperativa, em C#, Visual Basic ou C++. Ter valores de retorno é opcional e pode ser ignorado mesmo quando especificado. Em linguagens funcionais como a F#, ignorar um valor de retorno requer um idioma de codificação explícito. Nesses casos, os programadores podem alimentar os resultados para uma função chamada “ignore", que faz exatamente o que o seu nome sugere.

F# assíncrona

Até aqui, meu mergulho na sintaxe da linguagem F# tomou uma entre duas formas: trabalhar com construções funcionais extremamente simples ou fazer com que ela pareça uma variação estranha – e mais simples – das linguagens orientadas a objeto tradicionais, compatíveis com .NET (C#, Visual Basic ou C++/CLI). Isso se torna, de longe, um argumento para a adoção da linguagem F# nas empresas.

Mas dê uma olhada na Figura 6. Isso muito certamente não entra em nenhuma das duas categorias mencionadas anteriormente. À parte dos caracteres ! que aparecem em alguns lugares e do uso do modificador assíncrono, trata-se de um código relativamente direto: carregando uma imagem gráfica de origem, extraindo seus dados, passando esses dados para uma função independente para manipulação (rotation, skew ou qualquer outra) e gravando esses dados de volta em um arquivo de saída.

Figure 6 Manipulando uma imagem (Close Figure 6)
let TransformImage pixels i =
  // Some kind of graphic manipulation of images

let ProcessImage(i) =
  async { use  inStream  = File.OpenRead(sprintf "source%d.jpg" i)
          let! pixels    = inStream.ReadAsync(1024*1024)
          let  pixels'   = TransformImage(pixels,i)
          use  outStream = File.OpenWrite(sprintf "result%d.jpg" i)
          do!  outStream.WriteAsync(pixels')
          do   Console.WriteLine "done!"  }

let ProcessImages() =
  Async.Run (Async.Parallel
               [ for i in 1 .. numImages -> ProcessImage(i) ])

O que não é óbvio é que o uso do modificador assíncrono torna esse código no que a linguagem F# chama de um fluxo de trabalho assíncrono (sem relação com o Windows Workflow Foundation), o que significa que essas etapas carregar/processar/salvar estão acontecendo em threads paralelos a partir de um pool de threads do .NET.

Simplificando, considere o código da Figura 7. Essa seqüência em particular mostra os fluxos de trabalho assíncronos de uma forma extremamente simples e fácil de assimilar. Sem entrar em muito detalhe, evals é uma matriz de funções que estão esperando para serem executadas, cada uma sendo enfileirada para execução no pool de threads pela chamada Async.Parallel. Quando executada, torna-se aparente que as funções dentro de evals estão, na verdade, em um thread separado da função em awr (embora em razão da natureza do pool de threads do sistema .NET, algumas ou todas as funções evals possam ser executadas no mesmo thread).

Figure 7 Executando funções assincronamente (Close Figure 7)
#light
open System.Threading

let printWithThread str =
    printfn "[ThreadId = %d] %s" Thread.CurrentThread.ManagedThreadId str

let evals =
    let z = 4.0
    [ async { do printWithThread "Computing z*z\n"
              return z * z };
      async { do printWithThread "Computing sin(z)\n"
              return (sin z) };
      async { do printWithThread "Computing log(z)\n"
              return (log z) } ]

let awr =
    async { let! vs = Async.Parallel evals
            do printWithThread "Computing v1+v2+v3\n"
            return (Array.fold_left (fun a b -> a + b) 0.0 vs) }

let R = Async.Run awr
printf "Result = %f\n" R

O fato de elas serem executadas fora do pool de threads do .NET destaca novamente o bom nível de suporte que a linguagem F# oferece à interoperabilidade com o tempo de execução subjacente. Essa confiança na biblioteca de classes do .NET Framework, inclusive para áreas tradicionalmente reservadas para implementação especializada (tal como threading) em linguagens funcionais, significa que o programador de C# pode usar as bibliotecas ou módulos de F#, assim como os desenvolvedores de F# usam as bibliotecas de C#. Na verdade, no futuro, os recursos de F# como tarefas assíncronas poderão tirar vantagem das novas bibliotecas do .NET Framework, bibliotecas como Task Processing Library na biblioteca Parallel Extensions.

Ajustando-se à linguagem F#

Caso ainda não esteja claro, há muito mais a ser dito sobre a linguagem F# do que pode ser abordado em um único artigo; na verdade, entre a nova sintaxe e a forma inteiramente nova de pensar (funcional versus imperativa), é justo dizer que para o desenvolvedor médio orientado a objeto acostumado com C# ou Visual Basic, dominar a linguagem F# levará algum tempo. Felizmente, a linguagem F# permanece inteiramente interoperável com o resto do ecossistema do .NET, o que significa que você pode tirar vantagem de muitos conhecimentos e ferramentas existentes para ajudar a trazer a linguagem F# para o seu arsenal de codificação.

Todas as bibliotecas de base são totalmente acessíveis para o desenvolvedor da F# e, já que a F# oferece suporte a certos aspectos do desenvolvimento imperativo e de objeto, é totalmente factível considerar o uso do modo interativo da F# como uma forma de aprender a sintaxe da F# e os detalhes do Windows Presentation Foundation, Windows Communication Foundation ou Windows Workflow Foundation sem ter que fazer pausa nos ciclos de compilação.

Como já foi mencionado, os desenvolvedores podem escrever objetos comerciais na linguagem F# a serem usados com outras partes do código do aplicativo deles. Como a construção do tipo F# produz classes que são, na maior parte, idênticas às suas equivalentes C# ou Visual Basic, as bibliotecas de persistência como NHibernate manterão os tipos de F# sem problemas, fazendo uma injeção transparente da F# em aplicativos comerciais de trabalho.

Simplesmente aprender a F# ajudará você a compreender muitos dos recursos das versões futuras de C# e Visual Basic, já que muitas dessas idéias e conceitos – incluindo genéricos, iteradores (a palavra-chave “yield” em C#), e LINQ – surgiram de raízes funcionais e da pesquisa conduzida pela equipe da F#. Seja qual for sua opinião sobre a F#, a programação funcional está aqui e veio para ficar.

Ted Neward é um consultor independente especializado em sistemas empresariais de grande escala. Ele é autor e co-autor de vários livros, MVP da Microsoft em arquitetura, diretor técnico da BEA, palestrante INETA e instrutor da Pluralsight. Entre em contato com o Ted em ted@tedneward.com ou visite o blog dele em blogs.tedneward.com.


Nota do Editor: Uma onda de grandes lan&#231;amentos est&#225; a caminho

Nota do Editor
Uma onda de grandes lançamentos está a caminho
Howard Dierking

Esta edição da MSDN Magazine ocorre durante a maior onda de lançamentos na história da Microsoft, uma onda que consiste no Windows Server 2008, no SQL Server 2008 e no Visual Studio 2008. Em minha Nota do Editor de dezembro, falei sobre os novos recursos que foram planejados para o lançamento do Visual Studio 2008. Ainda nesse assunto dos lançamentos, comentei as plataformas totalmente novas com nomes como "Oslo" e "Volta". Da mesma forma, foram feitos anúncios formais das novas linguagens de programação, como F#. E, como se isso não fosse suficiente, agora temos dois lançamentos adicionais (e enormes): Windows Server 2008 e SQL Server 2008. Então, o que esses lançamentos significam para você? Não são tecnologias para administradores de sistemas?

Quando trabalhava na equipe de certificação na Microsoft, agrupamos as funções de trabalho em três principais categorias: desenvolvedor, profissional de TI e operador de informações. Os desenvolvedores são como você – pessoas que escrevem software usando ferramentas como o Visual Studio. Os profissionais de TI são colegas que projetam, implementam e mantêm os componentes da infra-estrutura de TI, como domínios do Windows, clusters do SQL Server e farms de servidor Web. Finalmente, os operadores de informações descrevem pessoas que representam os clientes para muitos de vocês. Eles são indivíduos que utilizam produtos como o Microsoft Office e aplicativos comerciais personalizados para realizar tarefas não relacionadas à tecnologia.

Por que estou gastando tempo descrevendo essas categorias? Porque, como acredito que essa última onda de lançamentos ilustra, as linhas que separam essas categorias não são simplesmente tão claras como costumavam ser. Por exemplo, ao considerar uma tecnologia como o SQL Server, há uma linha separando o desenvolvedor do profissional de TI? Naturalmente, há algumas tarefas óbvias que podem se encaixar facilmente em uma categoria ou outra (os backups são um exemplo clássico), mas o desenvolvedor deve se preocupar com planos de consulta e índices? Um profissional de TI deve compreender algo sobre o SQL CLR para gerenciar adequadamente uma instância do SQL Server? E, se a delineação ainda parece muito clara com o SQL Server, considere o SharePoint – é uma tecnologia para desenvolvedores, profissionais de TI e operadores de informações (e algumas outras categorias que nem mesmo têm nomes).

O importante aqui é que para se ter êxito como desenvolvedor atualmente, precisamos ir adiante de nossos compiladores (e intérpretes se você for dessa convicção) e desenvolver uma base de dados de conhecimento mais ampla. Precisamos compreender, mesmo em um nível básico, como as redes são agrupadas e como os sistemas de diretório, como o Active Directory, são utilizados para gerenciá-las. Precisamos desenvolver a importância sobre como os negócios funcionam para que possamos saber como aproveitar melhor as tecnologias, como SQL Server Analysis Services, SQL Server Reporting Services e SharePoint, para fornecer melhores informações para os responsáveis pelas decisões.

E, é essa idéia de criar uma base de dados de conhecimento mais ampla que torna essa onda de lançamentos tão interessante. Nesta edição, falaremos sobre como as novas tecnologias de plataformas podem permitir que você crie aplicativos melhores, para os operadores de informações que as utilizarão e para os administradores de sistemas que irão gerenciá-las. Desde o aproveitamento de novos recursos nos serviços de plataforma, como o IIS, à criação de funcionalidade personalizada a aplicativos existentes de operadores de informações, como o Microsoft Office, para reunir o desenvolvedor e o DBA no ciclo de vida do produto, esta edição é útil para todos.

Finalmente, quero chamar a atenção para um último ponto interessante. Esta edição tem uma data oficial de "Lançamento". Como tal, o pedido das suas edições de 2008 será da seguinte forma: janeiro, fevereiro, lançamento, março, etc... Embora inicialmente pareça confuso, aproveitamos o lançamento para fazer algo revolucionário – ou seja, aproximar a data da edição da data real. Praticamente, isso significa que agora, enquanto a sua edição de lançamento chegará em algum momento em fevereiro, a sua edição de março chegará... incrivelmente impressionante... em março!

Thanks to aos seguintes especialistas técnicos da Microsoft pela ajuda nesta edição: David Aiken, David Anson, Matt Gibbs, Ed Hintz, Luke Hoban, Dominic Hopton, David Kline, Thomas Marquardt, Keith Pijanowski, Dave Reed, Don Smith, Bill Staples e Don Syme.


Toolbox: Conclus&#227;o de c&#243;digo SQL, ferramentas do Subversion, desenvolvimento Agile e muito mais

Toolbox
Conclusão de código SQL, ferramentas do Subversion, desenvolvimento Agile e muito mais
James Avery

Conteúdo
Obtenha ajuda para escrever em SQL
Integração do Visual Studio e Subversion
Desenvolvimento rápido de aplicativos
Para sua estante

Obtenha ajuda para escrever em SQL

Como desenvolvedor, pode ser difícil alternar linguagens. Por exemplo, se você está usando o Visual Basic® .NET e precisa escrever algo em C#, pode demorar até você se lembrar da sintaxe equivalente de C#. Felizmente, é raro que os desenvolvedores usando o Microsoft®.NET Framework precisem escrever em Visual Basic .NET e em C# rotineiramente. Há outra linguagem na qual escrevemos bastante − o SQL. Mas ao contrário do Visual Basic .NET e C#, que fornecem IntelliSense® para nos ajudar a lembrar palavras-chave da linguagem, o Visual Studio® 2005 e o SQL Server® Management Studio não oferecem quaisquer dicas.

É aí que o SqlAssist entra em ação. Há duas edições do SqlAssist que fornecem conclusão de código do tipo IntelliSense: uma para o Visual Studio 2005 e outra para o SQL Server Management Studio.

A conclusão de código no SqlAssist inicia com comandos básicos do SQL. Por exemplo, em vez de digitar FROM, você pode simplesmente digitar “f” e pressionar a tecla Tab. Ela também inclui suporte para tabelas e colunas de bancos de dados (mesmo que tenha sido efetuado alias). Além disso, o SqlAssist inclui suporte a variáveis locais e exibe informações de parâmetros ao chamar procedimentos ou funções armazenadas.

Mas talvez o recurso de conclusão de código mais interessante ocorra quando você está escrevendo uma declaração SQL e começa a escrever uma cláusula Join: você verá uma lista de chaves estrangeiras para a tabela que está consultando. Selecione uma chave estrangeira e a cláusula Join será automaticamente adicionada.

O SqlAssist também fornece algum suporte valioso para editor de texto. Quando você digitar um nome de tabela ou coluna, ele garantirá automaticamente que o nome seja inserido adequadamente em maiúsculas ou minúsculas. Por exemplo, vendoraddress será automaticamente alterado para VendorAddress. Além disso, ao abrir arquivos SQL, não é incomum que eles sejam formatados de modos estranhos (estranho significa de modo diferente do que você gosta); o SqlAssist fornece uma funcionalidade de formatação SQL que reformatará automaticamente o arquivo com base nas opções configuráveis.

O SqlAssist também fornece um mecanismo para criar e armazenar trechos de código SQL, denominados modelos SQL. Você pode criar facilmente modelos para SQL ou cabeçalhos comumente utilizados e, em seguida, poderá arrastá-los na sua consulta, quando necessário.

O suplemento do Visual Studio também fornece excelente suporte para o trabalho com arquivos SQL no Visual Studio. Basta abrir um arquivo SQL e você poderá se conectar ao seu banco de dados, obter o suporte da conclusão de código do SqlAssist e executar seus arquivos de script. Desde a instalação do SqlAssist, não me imagino abrindo o SQL Server Management Studio, exceto para verdadeiras tarefas administrativas, como anexar ou desanexar bancos de dados.

Preço: $49 por edição, $75 para ambas.

roundpolygons.com

O SqlAssist fornece conclusão de código
O SqlAssist fornece conclusão de código (Clique na imagem para diminuir a exibição)
O SqlAssist fornece conclusão de código
O SqlAssist fornece conclusão de código (Clique na imagem para aumentar a exibição)

Integração do Visual Studio e Subversion

O Subversion é um sistema é um controle da origem e de código-fonte aberto que aumentou em popularidade nos últimos anos. Ele utiliza uma abordagem simples e acessível para o usuário para o gerenciamento de muitos dos recursos comuns de controle da origem, como ramificação, marcação e mesclagem. A popularidade do Subversion se deve a vários fatores: é gratuito e de código-fonte aberto, é fácil de instalar e usar e tem excelentes ferramentas, como o TortoiseSVN. Uma extensão do Windows® Explorer, o TortoiseSVN lhe fornece a capacidade de executar todas as suas funções de controle da origem na janela padrão do Explorer, em vez de depender de uma ferramenta separada.

O SourceForge começou a fornecer hospedagem ao e o CodePlex fornece até mesmo emulação do Subversion, de modo o que o TortoiseSVN pode ser usado com o site. Com alguns .NET shops migrando para o Subversion, um dos recursos mais freqüentemente faltando é a total integração com o Visual Studio por meio de outros sistemas conhecidos de controle de origem.

Use o VisualSVN, um suplemento do Visual que fornece excelente integração entre o Visual Studio e o Subversion. Quando você usar o VisualSVN, verá marcadores de fácil compreensão ao lado de cada um dos seus arquivos no Solution Explorer fornecido pelo Visual Studio (observe que a sua solução deve estar armazenada em um repositório do Subversion). Os marcadores mostram quando um arquivo é modificado, não modificado ou está em conflito. Você pode clicar com o botão direito no arquivo e mostrar as alterações, revertê-las e atualizar ou confirmar.

Usando o menu VisualSVN, você pode gerenciar as alterações de todo o projeto. O menu VisualSVN também contém atalhos para funções comuns do TortoiseSVN, como repo-browser, criação e aplicação de patches, exibição de um log do Subversion, bem como ramificação, mesclagem e alternância de repositórios.

Um dos mais importantes conceitos de agilidade é poder fazer o check in sempre e desde o início. Ao integrar-se o mais rápido possível, você descobre quaisquer possíveis problemas de mesclagem e, se implementar a integração contínua, todo o seu código será incorporado e os testes executados. O VisualSVN facilita o check in precoce e freqüente devido à total integração com o Visual Studio – você será constantemente lembrado de que realizou alterações e ainda não fez o check in das mesmas. Não há motivo para deixar para depois já que você pode fazer o check in das alterações diretamente do Visual Studio.

Preço: $49.

visualsvn.com

VisualSVN fornece atalhos de função
VisualSVN fornece atalhos de função (Clique na imagem para diminuir a exibição)
VisualSVN fornece atalhos de função
VisualSVN fornece atalhos de função (Clique na imagem para aumentar a exibição)

Desenvolvimento rápido de aplicativos

Neste último outono, a Microsoft anunciou o ASP.NET MVC Framework, que permite os desenvolvedores escrevam aplicativos da Web baseados no Model View Controller. Muitos desenvolvedores ASP.NET estão pedindo essa alternativa ao modelo de página padrão ASP.NET.

Não se preocupe − o modelo de página ASP.NET não desaparecerá. Isso apenas significa que agora temos outra opção. O ASP.NET MVC Framework facilitará o desenvolvimento de aplicativos Web com uma separação clara de preocupações. A lógica da página será armazenada no seu controlador, o HTML estará na visualização e a lógica do aplicativo estará no seu modelo.

O ASP.NET MVC Framework é a primeira parte do quebra-cabeça da Microsoft. Um mês ou em torno disso depois que o MVC Framework foi anunciado, a segunda parte do quebra-cabeça foi considerada: a Microsoft contratou Rob Conery para trabalhar em tempo integral em seu projeto, o SubSonic.

O SubSonic é um conjunto de ferramentas de aplicativo que está centralizado em sua capacidade de gerar completamente sua camada de acesso de dados. Ao contrário de algumas outras estruturas ORM (Object Relation Mapping, Mapeamento relacional de objetos), o SubSonic utiliza a abordagem de gerar e compilar sua camada de acesso de dados em oposição à execução de um mapeamento baseado em reflexão no tempo de execução (e por esse motivo, alguns o chamariam de ferramenta de geração de código em vez de um ORM).

O SubSonic é diferente dos outros ORMs em relação a como ele mapeia suas tabelas para o seu modelo de domínio. O NHibernate, outro ORM para .NET, utiliza a abordagem de especificar o seu modelo (ou entidades comerciais) e então mapear essas entidades para o seu banco de dados por meio de seus atributos XML ou .NET. O SubSonic alterna isso e cria um modelo baseado no esquema do seu banco de dados. Ambas as abordagens têm suas vantagens e desvantanges, mas em geral acho que isso tem a ver com a preferência pessoal.

Para obter um modelo completo do seu banco de dados, instale o SubSonic e configure-o no seu aplicativo por meio de algumas configurações no seu arquivo web.config. O modelo consiste em classes simples com os mesmos nomes que as suas tabelas, que contêm métodos para todas as ações comuns do banco de dados. Por exemplo, uma tabela Product que seria convertida em uma classe Product e métodos como Save, Destroy e Where, o último sendo usado para consultar o banco de dados. Um objeto separado Query está disponível para consultas complicadas.

Ao contrário da maioria das ferramentas de ORM, o SubSonic fornece uma forma amigável para lidar com procedimentos armazenados. Os procedimentos armazenados podem ser encapsulados em uma classe única e são chamados da mesma forma que você chamaria um método. O acesso a visualizações também é compatível.

Embora a parte do ORM do SubSonic é o seu maior recurso, o SubSonic também inclui várias outras funções e controles auxiliares. Um dos controles mais úteis, o Scaffold, facilita a rápida execução do aplicativo −e embora você possa não usá-lo para seu código de produção, ele é perfeito para a criação de um protótipo. Alguns dos métodos auxiliares mais úteis se concentra em validações com um host de diferentes métodos de validação disponíveis para praticamente todos os cenários (números, somente alfa, códigos postais, todos os principais cartões de crédito e assim por diante)

O SubSonic por si só é um conjunto de ferramentas de aplicativo muito poderoso; quando emparelhado com o ASP.NET MVC, ele se torna um estrutura valiosa de aplicativo que será um concorrente atrativo para outras estruturas de aplicativo de desenvolvimento rápido disponíveis no .NET e em outras linguagens.

Preço: gratuito, código-fonte disponível.

subsonicproject.com

Para sua estante

Inicialmente, quando as metodologias Agile começaram a se popularizar, na época elas foram amplamente desconectadas do software de gerenciamento de projetos e modelagem; as fases com projeto pesado de UML e gráficos de Gannt gigantescos contribuíram para que o Agile fosse bem-sucedido. À medida que as metodologias Agile se tornaram mais importantes, foram criadas ferramentas para aproveitar projetos Agile. Na verdade, a ferramenta premier de gerenciamento do ciclo de vida do aplicativo da Microsoft, o Visual Studio Team System (VSTS), foi desenvolvida para funcionar bem com o Agile (embora você não precise usar o Agile para usar o VSTS).

Visual Studio Team System: Better Software Development for Agile Teams, de Will Stott e James W. Newkirk (Addison-Wesley Professional, 2007), é uma excelente introdução para a ferramenta do Team System e para trabalhar em um projeto Agile. O livro introduz as partes do Visual Studio Team System e explica como elas se encaixam.

A seguir, há uma breve lição sobre Extreme Programming. Se você quiser usá-lo em seu projeto, leia um livro separado dedicado a Extreme Programming. O livro apresenta mais detalhes sobre o Microsoft Solutions Framework para processos Agile e sobre como alterar o seu atual processo.

Assim, o livro começa a se aprofundar. Ele apresenta o controle da origem, a integração contínua e o desenvolvimento orientado a testes, destacando como o Team System fornece funcionalidade que se integra bem com essas práticas Agile fundamentais. Cada seção não inclui somente orientações sobre práticas recomendadas, mas inclui instruções do Team System passo a passo e capturas de tela.

O livro cobre o método acessível Agile de modelagem com modelagem simples inspirada em cartão CRC (Class Responsibility Collaborator). Foi uma excelente surpresa ver uma seção dedicada ao FIT (Framework for Integrated Test). O FIT é uma ferramenta de teste de código-fonte que pode permitir que os usuários ou analistas comerciais orientem o teste do seu aplicativo.

O livro tem como base o Visual Studio Team System 2005, mas o Visual Studio Team System 2008 provavelmente já terá sido lançado no momento em que essa coluna for publicada. As orientações gerais ainda serão relevantes, mas as instruções específicas da versão e capturas de tela poderão estar desatualizadas.

Há outros livros melhores sobre Agile e há outros que utilizam uma abordagem mais independente de metodologia para o aprendizado do Team System. No entanto, se você estiver no início do projeto Agile usando o Team System, este livro será ideal para você começar.

Preço: $49,99.

aw-bc.com

(Clique na imagem para diminuir a exibição)

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

James Avery possui sua própria empresa de consultoria em .NET, a Infozerk Inc. Seu livro mais recente é o Windows Developer Power Tools (O'Reilly, 2006). Você pode contatá-lo em javery@infozerk.com e ler o seu blog em www.dotavery.com/blog.


Cutting Edge: Caixas de di&#225;logo modais com AJAX

Cutting Edge
Caixas de diálogo modais com AJAX
Dino Esposito

Download do código disponível em: CuttingEdge2008_Launch.exe (419KB)
Conteúdo
O controle ModalPopupExtender
Fechando o popup usando a tecla Esc
Adicionando animação na exibição
Retornando dados para o servidor
É a sua vez

C
aixas de diálogo existem no Windows® há muito tempo, e elas possuem suas vantagens. Mas se quiser que o aplicativo Web tenha caixas de diálogo, você está basicamente preso a popups e, como sabe, a maioria dos usuários os desabilitam com bloqueadores de popup. Portanto, o que você deve fazer se precisar de uma caixa de diálogo popup?

Com o Microsoft® ASP.NET AJAX, as caixas de diálogo são especialmente importantes para exibir informações sensíveis ao contexto sem uma recarga de página ou nova página. Portanto, é importante planejar uma implementação diferente das caixas de diálogo que seja tão eficaz quanto popups modais mas sem complicações para os usuários.

As extensões ASP.NET e AJAX não possuem suporte interno a popups ou caixas de diálogo da Web; entretanto, o AJAX Control Toolkit possui. O AJAX Control Toolkit é uma biblioteca de códigos-fonte compartilhados de controles do extensor ASP.NET. Um dos seus controles mais úteis é o controle ModalPopupExtender que apresentei resumidamente no artigo de janeiro de 2008 de Cutting Edge (msdn.microsoft.com/msdnmag/issues/08/01/CuttingEdge). O extensor ModalPopup usa a marcação gerada por um painel ASP.NET do lado do servidor e mostra-o ou o oculta conforme o usuário clica em um elemento HTML associado. Inicialmente com estilo oculto, a caixa de diálogo é baixada para o cliente quando a página é carregada e, em seguida, mostrada ou oculta sob demanda. Como a modalidade é garantida? Vamos examinar o trecho de código na Figura 1.

Figure 1 Detalhes do controle ModalPopupExtender (Close Figure 1)
// Code excerpted from modalpopupbehavior.js.
// Method initialize()
//
// Download the source code of the AJAX Control Toolkit from
// http://www.codeplex.com/atlascontroltoolkit.
//

// The panel defined as the popup is saved as the foreground element.
this._foregroundElement = this._popupElement;

// A new DIV tag is created as the background element for the panel.
// The panel is invisible and placed at 0,0 coordinates (fixed position).
// In addition, the background element is given a high z-index so that it
// sits above everything else.
this._backgroundElement = document.createElement('div');
this._backgroundElement.id = this.get_id() + '_backgroundElement';
this._backgroundElement.style.display = 'none';
this._backgroundElement.style.position = 'fixed';
this._backgroundElement.style.left = '0px';
this._backgroundElement.style.top = '0px';
this._backgroundElement.style.zIndex = 10000;

// The background element is styled as specified.
if (this._BackgroundCssClass)
{
     this._backgroundElement.className = this._BackgroundCssClass;
}

// The background element is appended to the parent of the foreground
// element.
this._foregroundElement.parentNode.appendChild(this._backgroundElement);

// The foreground element is programmatically hidden from view. In
// addition, it is given absolute positioning and an higher z-index than
// the background element.
this._foregroundElement.style.display = 'none';
this._foregroundElement.style.position = 'fixed';
this._foregroundElement.style.zIndex = $common.getCurrentStyle(
this._backgroundElement, 'zIndex', this._backgroundElement.style.zIndex) + 1;

// A click handler is added to the target element of the extender. In
// this case, It is the DOM element whose ID is passed on as the
// TargetControlID property.
this._showHandler = Function.createDelegate(this, this._onShow);
$addHandler(this.get_element(), 'click', this._showHandler);

O código mostra um excerto do script usado para inicializar o extensor ModalPopup no cliente. Observe que um extensor ASP.NET AJAX geralmente possui um controle de servidor – o extensor – e uma classe de comportamento do cliente escrita em JavaScript. O código na Figura 1 vem da classe de comportamento do cliente do extensor ModalPopup. Se você baixar o código-fonte do AJAX Control Toolkit de codeplex.com/atlascontroltoolkit, encontrará o código de script supracitado no arquivo modalpopupbehavior.js.

Como você pode ver na Figura 1, o elemento raiz da árvore de marcação da caixa de diálogo é projetado como o elemento de primeiro plano e oculto programaticamente. Ao mesmo tempo, uma marca DIV é criada automaticamente e designada como o elemento de segundo plano. A marca recebe uma posição fixa nas coordenadas 0,0 e com estilo apropriado. A marca DIV também recebe um z-index extremamente alto (mas arbitrário) que praticamente garante que ela ficará sobre todos os outros elementos no documento, já que a propriedade z-index define a ordem da pilha de um elemento HTML, e o elemento com a ordem de pilha mais alta é sempre exibido sobre os elementos com ordem de pilha mais baixa.

A marca DIV é adicionada ao DOM (Document Object Model) como um filho do pai do elemento de primeiro plano. Finalmente, o elemento de primeiro plano – o conteúdo da caixa de diálogo – recebe um z-index mais alto do que o do primeiro plano.

O efeito dessa marca DIV intermediária é que o novo elemento transparente é disposto em camadas sobre todos os elementos da página exceto o painel popup. Um manipulador de cliques também é aplicado dinamicamente ao destino dos controles do extensor para garantir que qualquer entrada do usuário realizada fora do painel popup (no elemento de primeiro plano) seja perdida e nunca alcance o destino pretendido. Esse é o comportamento que garante a modalidade.

O conteúdo do painel do servidor que você define na página ASPX de origem surge como uma janela modal, como uma caixa de mensagem clássica do Windows. Ao mesmo tempo, ele oferece um nível sem precedentes de flexibilidade de interface do usuário – afinal de contas, trata-se de um painel ASP.NET, sendo assim, você pode preenchê-lo com qualquer combinação de controles que desejar, e pode estilizá-lo como quiser.

O controle ModalPopupExtender

Configurar um popup modal por meio da biblioteca do AJAX Control Toolkit é uma tarefa relativamente simples. Comece definindo um painel para fornecer a interface do usuário e, em seguida, adicione um controle de botão para disparar a exibição da caixa de diálogo:

<asp:Button runat="server" ID="btnEditCustomer"
    Text="Edit text" />
<asp:Panel runat="server" ID="pnlEditCustomer">
  ...
</asp:Panel>

Em seguida, configure o extensor e especifique a ID do controle de destino e a ID do controle de popup:

<act:ModalPopupExtender ID="ModalPopupExtender1"
    runat="server"
    TargetControlID="btnEditCustomer"
    PopupControlID="pnlEditCustomer"
    BackgroundCssClass="modalBackground"
    OkControlID="editBox_OK"
    OnOkScript="yes()" />

A ID do controle de destino de um extensor ModalPopup é a ID do controle do servidor que, quando clicado, faz com que a caixa de diálogo surja. A ID do controle popup é a ID do controle do servidor que fornece o conteúdo da caixa de diálogo.

A estrutura do AJAX Control Toolkit não fornece qualquer estilo padrão para a caixa de diálogo. O usuário precisa empregar os elementos visuais no painel para descartar a caixa de diálogo. A Figura 2 mostra uma caixa de diálogo de exemplo em ação. Ela permite confirmação e fornece algumas informações. Mas se você quiser caixas de diálogo sensíveis ao contexto onde é necessária mais alguma interação do usuário, é preciso mais algum trabalho.

Figura 2 A caixa de diálogo modal da Web de exemplo em ação
Figura 2 A caixa de diálogo modal da Web de exemplo em ação(Clique na imagem para diminuir a exibição)
Figura 2 A caixa de diálogo modal da Web de exemplo em ação
Figura 2 A caixa de diálogo modal da Web de exemplo em ação (Clique na imagem para aumentar a exibição)

Para tornar o extensor ModalPopup mais parecido com uma caixa de diálogo do Windows, podem ser adicionados inúmeros recursos. Por exemplo, a habilidade de descartar a caixa de diálogo, simplesmente pressionando a tecla Esc – uma funcionalidade comum no Windows mas sem suporte ainda no AJAX Control Toolkit – é uma boa idéia.

Antes que eu implemente qualquer um desses recursos, vamos revisar rapidamente os métodos e as propriedades do controle ModalPopupExtender. A Figura 3 lista suas propriedades mas não inclui todas as propriedades herdadas automaticamente das classes base.

Figure 3 Propriedades do controle ModalPopupExtender (Close Figure 3)

Propriedade | Descrição
BackgroundCssClass | A classe CSS vinculada ao aplicativo host a ser aplicada a qualquer conteúdo abaixo do popup modal quando o popup é exibido.
DropShadow | A propriedade Boolean que indica se o extensor adicionará automaticamente uma sombra ao popup modal. True por padrão.
OkCancelID | ID do elemento DOM na interface de usuário exibida que descarta o popup modal com uma ação Cancel.
OkControlID | ID do elemento DOM na interface de usuário exibida que descarta o popup modal com uma ação OK.
OkCancelScript | O nome de uma função JavaScript vinculada à página a ser executada quando o popup modal é descartado com a ação Cancel.
OnOkScript | O nome de uma função JavaScript vinculada à página a ser executada quando o popup modal é descartado com a ação OK.
PopupControlID | ID do elemento raiz na árvore DOM a ser exibida como o conteúdo do popup modal.
PopupDragHandleControlID | ID do elemento incorporado que contém o cabeçalho/título que será usado como uma alça de arrasto do popup inteiro.
RepositionMode | Indica se o popup precisa ser reposicionado quando a janela do navegador é redimensionada ou rolada. Os valores factíveis pertencem ao tipo enumerado ModalPopupRepositionMode.
TargetControlID | ID do elemento DOM que, quando clicado, ativa o popup modal.
X | Coordenada X do canto esquerdo superior do popup modal.
Y | Coordenada Y do canto esquerdo superior do popup modal.

A assinatura do controle ModalPopupExtender é mostrada aqui:

    public class ModalPopupExtender :
        DynamicPopulateExtenderControlBase

A classe base aqui é um extensor definido pela biblioteca que fornece suporte DynamicPopulate a vários extensores. O DynamicPopulate é outro extensor no AJAX Control Toolkit que substitui a marcação de um elemento DOM por texto retornado por uma chamada de serviço da Web. O extensor ModalPopup possui dependências em outros extensores do AJAX Control Toolkit incluindo DropShadow e DragPanel.

O conteúdo da caixa de diálogo é completamente determinado por uma sub-árvore DOM enraizada em um elemento especificado por meio da propriedade PopupControlID. Com certa freqüência, esse elemento será um controle de servidor ASP.NET tal como um painel. A posição do popup é determinada pelas propriedades X e Y, mas o popup será criado horizontalmente se não forem especificadas as coordenadas.

Como em qualquer caixa de diálogo clássica do Windows, o popup modal da Web pode ser arrastado. Para habilitar isso, especifique na propriedade PopupDragHandleControlID a ID do elemento a ser usado como a alça de arrasto, e o script incorporado faz o resto. Observe que você pode arrastar o popup modal, mas somente na área da página coberta pelo DOM. Em outras palavras, se a área da página for definida por uma marca DIV com uma altura de 100 pixels, e você exibir a página em uma janela do navegador de 1600 por 1024 pixels, será possível arrastar o popup somente verticalmente para 100 pixels, independentemente da altura física da janela do navegador.

Definindo a propriedade RepositionMode como qualquer valor menos "None" (o padrão), você permite que o script de comportamento atualize a posição do popup quando o usuário rolar ou redimensionar a janela do navegador da Web (veja a Figura 4).

Figura 4 Reposicionando a caixa de diálogo modal
Figura 4 Reposicionando a caixa de diálogo modal(Clique na imagem para diminuir a exibição)
Figura 4 Reposicionando a caixa de diálogo modal
Figura 4 Reposicionando a caixa de diálogo modal (Clique na imagem para aumentar a exibição)

Fechando o popup usando a tecla Esc

No Windows, o usuário pode descartar uma mensagem ou caixa de diálogo pressionando a tecla Esc. No AJAX Control Toolkit, essa habilidade não está presente nativamente no comportamento do popup modal. Como você verá daqui a pouco, entretanto, detectar e manipular a tecla Esc não é um grande problema. Considere o código JavaScript seguinte, que é anexado ao evento KeyDown do DOM da página:

   function OnKeyPress(args)
   {
     if(args.keyCode == Sys.UI.Key.esc)
     {
       $find("ModalPopupExtender1").hide();
     }
   }

O código é disparado por pressionamentos de tecla que afetam o documento inteiro. O manipulador recebe um objeto Sys.UI.DomEvent através do parâmetro args. O objeto descreve o status do mouse e do teclado quando o evento de nível DOM é detectado. Em particular, a propriedade keyCode indica o código ASCII da tecla pressionada. Estão disponíveis informações adicionais sobre as teclas Ctrl, Shift e Alt, assim como informações sobre a posição do mouse e o status do botão. A enumeração Sys.UI.Key lista algumas constantes predefinidas para o código da teclas mais usadas, incluindo os botões Esc, Enter e Del. Usando essas ferramentas, detectar se a tecla Esc é pressionada é muito simples. Agora vamos ver como você pode ocultar o popup programaticamente.

Um extensor do AJAX Control Toolkit possui uma interface de programação de cliente e de servidor. A interface do servidor é um controle de servidor ASP.NET; a interface do cliente é uma classe de comportamento JavaScript. Essa classe expõe um modelo de programação JavaScript que espelha o modelo de programação do lado do servidor, para que cada uma das propriedades da Figura 3 tenha seu JavaScript correspondente. O recipiente dessas propriedades é um objeto com a ID de controle do extensor ModalPopup. Esse controle não é parte do DOM, já que é criado pela infra-estrutura da Biblioteca do Microsoft AJAX. Assim, você não pode usar a função $get para obter uma instância de uma classe de comportamento; em vez disso, use a função $find. A API que o servidor e o cliente do popup modal possuem mostra e oculta os métodos para controlar a visibilidade.

Observe que o manipulador de eventos OnKeyPress não funcionará até que você registre a Biblioteca do Microsoft AJAX. O melhor lugar para executar esse código de registro é a função pageLoad, desta forma:

function pageLoad(sender, args)
{
  $addHandler(document, "keydown", OnKeyPress);
}

A Biblioteca do Microsoft AJAX invoca a função pageLoad quando todas as suas tarefas de inicialização estiverem concluída e tudo na página e na biblioteca tenha sido inicializado. A biblioteca também conecta automaticamente seu estágio de carregamento a qualquer função JavaScript com o nome que pode ser encontrado na página. Agora, qualquer popup modal gerenciado por meio do extensor ModalPopup do AJAX Control Toolkit pode ser descartado com a tecla Esc.

Adicionando animação na exibição

Com essa etapa concluída, podemos continuar com outras coisas. Você não adoraria atingir o mesmo tipo de efeito fade in que o Windows Vista® usa em seus aplicativos Web ? O efeito é interno para janelas regulares gerenciadas pelo Windows, tal como a janela que surge quando o método window.alert é invocado. Em popups modais personalizados, como o que estou compilando aqui, você deve fazê-lo por si mesmo.

A animação pode se tornar um recurso nativo do extensor ModalPopup num futuro próximo, mas, por enquanto, fazê-la funcionar é relativamente fácil graças à API de animação já disponível na biblioteca do AJAX Control Toolkit. O código seguinte se refere ao extensor Animation, que dá suporte à animação de inúmeros eventos DOM predefinidos comuns, tais como OnLoad, OnClick, OnMouseOver e OnMouseOut. TargetControlID aponta para o elemento DOM cujos eventos dispararão a animação:

<act:AnimationExtender ID="popUpAnimation" runat="server"
  TargetControlID="btnViewMore">
  <Animations>
    <OnClick>
      <Parallel AnimationTarget="pnlViewCustomer"
        Duration=".3" Fps="25">
        <FadeIn />
      </Parallel>
    </OnClick>
  </Animations>
</act:AnimationExtender>

As animações podem ser combinadas em uma seqüência, e algumas podem ser reproduzidas em paralelo. O código precedente causa o fade in do painel associado a um popup modal pela duração especificada e pelo número de quadros depois que o botão é clicado. O código abaixo mostra uma animação mais sofisticada que executa vários efeitos:

<act:AnimationExtender ID="popUpAnimation" runat="server"
  TargetControlID="btnViewMore">
  <Animations>
    <OnClick>
      <Parallel AnimationTarget="pnlViewCustomer"
        Duration=".3" Fps="25">
        <Move Horizontal="100" Vertical="100" />
        <Resize Width="280" Height="180" />
        <Color PropertyKey="backgroundColor"
          StartValue="#FFFFFF"
          EndValue="#FFFF00" />
      </Parallel>
    </OnClick>
  </Animations>
</act:AnimationExtender>

Aqui, o popup modal é movido primeiro para uma nova posição relativa e, em seguida, redimensionado e colorido. A galeria de exemplos em asp.net/AJAX/AjaxControlToolkit/Samples/Animation/Animation.aspx mostra algumas demonstrações; entretanto, muitas delas podem ser aplicadas a um popup modal sem modificar o código-fonte. O problema principal reside no fato de que o popup modal é exibido antes que a animação se inicie. Embora isso possa funcionar para um efeito fade in simples, se você quiser criar explosões ou efeitos de apagamento, precisará ser esperto.

O extensor ModalPopup acima aciona alguns eventos úteis no cliente, incluindo o evento de exibição. Veja como ela é instanciada:

function pageLoad(sender, args)
{
    $find("ModalPopupExtender1").add_showing(onModalShowing);
}

Qualquer código que você associa ao evento de exibição é executado antes que o popup seja exibido. Você pode usar esse evento para realizar quaisquer tarefas de inicialização do lado do cliente. Outros eventos do cliente aos quais você pode vincular incluem hiding, hidden e shown.

Inicializando os elementos do popup

No cliente, o extensor ModalPopup alterna a visibilidade da árvore DOM, como identificado pelo atributo PopupControlID. No código-fonte do comportamento do cliente, você verá o código adicional para interceptar a marcação que foi baixada do servidor como uma parte constituinte da página host, e poderá estilizá-lo para exibição.

Por esse motivo você pode fazer alterações no modelo ou no conteúdo do popup, quando a página tiver sido servida. Considere o seguinte cenário. O usuário escolhe um cliente em, digamos, uma lista suspensa e visualiza alguns detalhes. Em seguida, talvez ele queira editar algumas das informações do cliente. Em um aplicativo da Web tradicional, você apenas redireciona o usuário para uma nova página ou , talvez, faça uma postagem para carregar uma exibição diferente. No ASP.NET 2.0 e em versões mais recentes, isso é o que o controle DetailsView faz. Em um aplicativo de área de trabalho, você usaria uma caixa de diálogo modal. Para aplicativos habilitados por AJAX, uma caixa de diálogo da Web agora é uma opção.

A caixa de diálogo usada para editar um cliente, entretanto, possui um layout fixo que é preenchido com novo conteúdo cada vez que ela vai aparecer. Suponha que você origine uma postagem completa quando um cliente é selecionado. No evento alterado pela seleção, você atualiza a exibição do cliente e opcionalmente os controles no popup modal. Dessa forma, quando o botão é clicado para fazer surgir a caixa de diálogo, as informações mais recentes já estão carregadas.

Mas um aplicativo AJAX normalmente não realiza postagens completas. A Figura 5 mostra um fragmento de uma página ASP.NET AJAX que usa processamento parcial para atualizar uma exibição do usuário. A região atualizável é vinculada ao evento SelectedIndexChanged no controle filho da lista suspensa. A lista suspensa, por sua vez, possui a propriedade AutoPostBack definida como verdadeira. O efeito é que sempre que o usuário altera a seção na lista suspensa, a tabela na Figura 5 é atualizada sem uma recarga completa da página. Até aqui, tudo bem.

Figure 5 Usando o processamento parcial para atualizar a exibição do cliente (Close Figure 5)
<table>
  <tr>
    <td valign="top">
      <b>Customers</b><br />
      <asp:DropDownList id="ddlCustomers" runat="server"
        DataSourceID="odsCustomers"
        DataTextField="CompanyName"
        DataValueField="ID"
        AutoPostBack="true"
        OnSelectedIndexChanged="ddlCustomers_SelectedIndexChanged"
        ondatabound="ddlCustomers_DataBound" />
      <asp:ObjectDataSource ID="odsCustomers" runat="server"
        TypeName="IntroAjax.CustomerManager"
        SelectMethod="LoadAll">
      </asp:ObjectDataSource>
    </td>
    <td valign="top">
      <asp:UpdatePanel ID="UpdatePanel1" runat="server">
        <ContentTemplate>
          <table>
            <tr>
              <td><b>Customer ID:</b></td>
              <td>
                <asp:Label runat="server" id="lblCustomerID" />
              </td>
            </tr>
            <tr>
              <td><b>Company Name:</b></td>
              <td>
                <asp:Label runat="server" id="lblCompanyName" />
              </td>
            </tr>
            <tr>
              <td><b>Contact Name:</b></td>
              <td>
                <asp:Label runat="server" id="lblContactName" />
              </td>
            </tr>
            <tr>
              <td><b>Country:</b></td>
              <td><asp:Label runat="server" id="lblCountry" /></td>
            </tr>
          </table>
        </ContentTemplate>
        <Triggers>
          <asp:AsyncPostBackTrigger ControlID="ddlCustomers"
            EventName="SelectedIndexChanged" />
        </Triggers>
      </asp:UpdatePanel>
    </td>
  </tr>
</table>

A próxima etapa envolve a atualização do painel na Figura 6, que representa o conteúdo do popup modal. O painel deve estar sincronizado com a exibição do cliente.

Figure 6 O conteúdo de um popup modal dá suporte ao processamento parcial (Close Figure 6)
<asp:Panel ID="pnlEditCustomer" runat="server"
  CssClass="modalPopup" style="display:none">
  <div style="margin:10px">
    <asp:UpdatePanel runat="server" ID="ModalPanel1"
      RenderMode="Inline" UpdateMode="Conditional">
      <ContentTemplate>
        <table>
          <tr>
            <td><b>Customer ID:</b></td>
            <td>
              <asp:Label runat="server" id="editCustomerID" />
            </td>
          </tr>
          <tr>
            <td><b>Company Name:</b></td>
            <td>
              <asp:TextBox runat="server" id="editTxtCompanyName" />
            </td>
          </tr>
          <tr>
            <td><b>Contact Name:</b></td>
            <td>
              <asp:TextBox runat="server" id="editTxtContactName" />
            </td>
          </tr>
          <tr>
            <td><b>Country:</b></td>
            <td>
              <asp:TextBox runat="server" id="editTxtCountry" />
            </td>
          </tr>
        </table>
        <hr />
        <asp:Button ID="btnApply" runat="server"
          Text="Apply" OnClick="btnApply_Click" />
      </ContentTemplate>
    </asp:UpdatePanel>

    <asp:Button ID="editBox_OK" runat="server"
       Text="OK" OnClick="editBox_OK_Click" />
    <asp:Button ID="editBox_Cancel" runat="server" Text="Cancel" />
  </div>
</asp:Panel>

Você pode atualizar o painel que está sendo exibido no popup quando um novo cliente é exibido ou logo antes de o popup ser exibido:

<act:ModalPopupExtender ID="ModalPopupExtender1" runat="server"
  TargetControlID="hiddenTargetControlForModalPopup"
  PopupControlID="pnlEditCustomer"
  BackgroundCssClass="modalBackground"
  DropShadow="false"
  OkControlID="editBox_OK"
  OnOkScript="ok()"
  OnCancelScript="cancel()"
  CancelControlID="editBox_Cancel" />

Se você atualizar o conteúdo do popup logo antes da exibição (a abordagem que recomendei), então, o código de inicialização da caixa de diálogo precisará ser executado no servidor, mas você não terá qualquer evento de postagem de qualquer controle que acione a caixa de diálogo modal. E caso você tenha feito, seria muito tarde para editar os controles na caixa de diálogo. O extensor ModalPopup, na verdade, adiciona um manipulador de eventos onclick do lado do cliente e impede que a ação padrão – nesse caso, uma postagem – aconteça.

A idéia, então, é usar um controle de destino falso do extensor ModalPopup para que o extensor nunca seja acionado automaticamente a partir do controle. Como você dispararia o popup modal? Aqui está um exemplo. Quando o botão abaixo é clicado, ele executa seu manipulador OnClick do lado do servidor em uma operação de processamento parcial:

<asp:UpdatePanel runat="server" ID="DialogBoxUpdatePanel"
  UpdateMode="Conditional">
  <ContentTemplate>
    <asp:Button runat="server" ID="btnEditText"
      Text="Edit text"
      OnClick="btnEditText_Click" />
  </ContentTemplate>
</asp:UpdatePanel>

Isso tem dois benefícios. Primeiro, nenhuma atualização completa afeta a página. Segundo, o código do servidor OnClick pode ser usado para inicializar corretamente o painel popup e, em seguida, solicitar ao extensor que mostre o popup:

protected void btnEditText_Click(object sender, EventArgs e)
{
    InitDialog();
    ModalPanel1.Update();
    ModalPopupExtender1.Show();
}

O método InitDialog contém o código interno necessário para inicializar todos os controles no painel na Figura 6. O código é suficiente para alterar o estado dos controles envolvidos mas não modifica sua marcação. Isso acontece porque você está executando o código através de postagem de processamento parcial. Sendo assim, na próxima etapa, você atualiza explicitamente o painel atualizável. Finalmente, chame o método Show no extensor ModalPopup. Essa última chamada garante que o script que mostra a caixa de diálogo quando a página é carregada seja baixado corretamente para o navegador. A Figura 7 mostra uma página de exemplo em ação projetada junto com essas diretrizes.

Figura 7 Caixa de diálogo modal com dados do contexto atual
Figura 7 Caixa de diálogo modal com dados do contexto atual(Clique na imagem para diminuir a exibição)
Figura 7 Caixa de diálogo modal com dados do contexto atual
Figura 7 Caixa de diálogo modal com dados do contexto atual (Clique na imagem para aumentar a exibição)

Você realmente precisa de todas essas regiões de processamento parcial na página? Bem, se a sua única opção é descobrir como inicializar a caixa de diálogo com o código do servidor, então, você talvez pudesse salvar algumas regiões personalizáveis. Entretanto, se a sua caixa de diálogo requer trabalho de inicialização do lado do servidor, então, ela provavelmente requer algum trabalho para atualizar a página subjacente com os dados coletados. Nesse caso, você precisaria retornar os dados para o servidor.

Retornando dados para o servidor

O extensor ModalPopup permite que você identifique controles que servem como os botões OK e Cancel, usando as propriedades OkControlID e CancelControlID para essa finalidade. Quando qualquer um desses botões é clicado, o popup é descartado e algum código JavaScript é opcionalmente executado. Você pode definir uma função JavaScript a ser executada quando os botões OK e Cancel são clicados por meio das propriedades OnOkScript e OnCancelScript. O popup não é postado quando qualquer um dos botões OK e Cancel predefinidos é clicado. O excerto de código seguinte do arquivo de origem modalpopupbehavior.js explica. Este código pertence aos manipuladores internos do evento click dos botões OK e Cancel:

var element = $get(this._OkControlID);
if (element && !element.disabled) {
   if (this.hide() && this._OnOkScript) {
      window.setTimeout(this._OnOkScript, 0);
   }
   e.preventDefault();
   return false;
}

Veja um manipulador de exemplo do botão OK:

function onOK(sender, e)
{
  // refresh the UI

  // if you need to run server code, you
  // can invoke a Web service method
}

É a sua vez

Para atualizar, não adicionei o código ao controle existente ou o comportamento do cliente para concluir este exemplo. Simplesmente usei o conjunto existente de membros (cliente e servidor) para melhorar a inicialização da caixa de diálogo e a troca de dados com a página host. Para evitar atualizações completas de página, empreguei o processamento parcial sempre que fez sentido.

Para replicar os truques no código de exemplo discutido aqui em seu próprio aplicativo, instale a versão mais recente do AJAX Control Toolkit e empacote o painel modal em uma região UpdatePanel, prestando atenção para não incluir os botões OK e Cancel na região parcial. Em seguida, vincule o extensor ModalPopup a um controle invisível e exiba e oculte o popup programaticamente. Finalmente, se você não precisar de botões adicionais no popup para postar sem sair da caixa de diálogo, tudo o que você precisa fazer é adicionar esses botões ao UpdatePanel que empacota o popup. Para obter mais detalhes, observe o código-fonte; uma linha de código vale mil palavras.

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

Dino Esposito é o autor da Programming ASP.NET 3.5 Core Reference (Referência Básica de Programação do ASP.NET 3.5) (Microsoft Press, 2008). Residente na Itália, Dino é um palestrante sempre presente em eventos do setor no mundo inteiro. Entre em contato com ele através de seu blog em weblogs.asp.net/despos.


Execu&#231;&#227;o de teste: Automa&#231;&#227;o de teste personalizada com o Team System

Execução de teste
Automação de teste personalizada com o Team System
Dr. James McCaffrey

Download do código disponível em: TestRun2008_Launch.exe (205KB)
Conteúdo
Empacotando um script de automação de teste simples
Resultados de teste estendidos:
Publicando resultados de teste no Team Foundation Server
Resumindo

A
qui não há uma única forma melhor de testar o software. Além do teste manual, dependendo do seu ambiente de desenvolvimento específico, você pode usar as estruturas de automação de teste comerciais, as estruturas de automação de teste de código-fonte aberto e internas, e os scripts de automação de teste personalizados. Todas essas abordagens têm vantagens e desvantagens.

Os scripts de teste p
ersonalizados possuem as vantagens de serem rápidos de escrever e fornecerem flexibilidade máxima. Entretanto, uma desvantagem de usar a automação de teste personalizada é a gerenciabilidade. Seu esforço de teste pode ficar sobrecarregado pelo volume enorme de scripts de teste, dados de caso de teste e resultados de teste. Por sorte, o Visual Studio® 2005 Team System lhe oferecem a habilidade de gerenciar a automação de teste personalizada. Deixe-me mostrá-lo o que quero dizer usando duas capturas de tela. Primeiro, considere o script de automação de teste mostrado em execução na Figura 1.

Figura 1 Script de automação de teste personalizado típico
Figura 1 Script de automação de teste personalizado típico(Clique na imagem para diminuir a exibição)
Figura 1 Script de automação de teste personalizado típico
Figura 1 Script de automação de teste personalizado típico (Clique na imagem para aumentar a exibição)

A automação de teste é um script JavaScript bem pequeno que está realizando teste de módulo em um método chamado TriMax que simplesmente retorna o maior de três inteiros. O método TriMax é hospedado dentro de um arquivo COM DLL clássico. Embora seja simples e eficaz, a automação de teste usando esse tipo de abordagem possui alguns pontos fracos. Onde os resultados de teste estão sendo armazenados? Em que compilação da DLL a execução de teste está sendo executada? Como esses resultados de teste podem ser armazenados com outros membros da equipe de desenvolvimento? Esses resultados estão relacionados a quaisquer bugs abertos? O Visual Studio Team System é projetado para lidar com esses problemas, como mostra a Figura 2.

Figura 2 Gerenciando a automação personalizada com o Team System
Figura 2 Gerenciando a automação personalizada com o Team System(Clique na imagem para diminuir a exibição)
Figura 2 Gerenciando a automação personalizada com o Team System
Figura 2 Gerenciando a automação personalizada com o Team System (Clique na imagem para aumentar a exibição)

O Visual Studio 2005 Team System consiste no Team Foundation Server (TFS) e uma das edições do lado do cliente do Visual Studio. Você pode pensar no TFS como um depósito de dados back-end inteligente que armazena e gerencia todos os dados associados a um projeto de desenvolvimento de software, incluindo código-fonte, bugs, resultados de teste, documentos de especificação, e muito mais.

A funcionalidade de automação de teste personalizada que descrevo nesta coluna é fornecida no Visual Studio Team Edition for Testers. Você pode baixar a versão de avaliação de 180 dias do Visual Studio 2005 Team Suite, que inclui a funcionalidade de todas as edições, mais o Team Foundation Server de microsoft.com/downloads.

Apresentarei três exemplos nesta coluna. O primeiro mostra como usar o VSTE for Testers para pegar um pedaço muito simples da automação de teste personalizada e empacotá-lo em um teste que pode ser gerenciado pelo Team System. O segundo exemplo expande o primeiro, mostrando-lhe como modificar a automação de teste personalizada a fim de criar resultados de caso de teste que possuem informações mais detalhadas do que uma simples aprovação ou resultado de falha. Meu terceiro exemplo mostra como usar o Team Foundation Server para armazenar e gerenciar os dados do resultado da execução de teste.

Antes de começar, deixe-me descrever resumidamente o módulo a ser testado para que você compreenda a automação de teste personalizada simples, e para que possa criar o módulo se quiser. (Se você estiver familiarizado com a criação de objetos COM usando ATL, talvez queira ir para a próxima seção).

Meu módulo é um método Win32® C++ chamado TriMax. Usando o Visual Studio, criei um novo projeto usando um modelo ATL C++ e o chamei de MyCOMLib. Aceitei as configurações padrão do assistente da ATL, que instrui o Visual Studio a criar uma DLL. No Solution Explorer do projeto, cliquei com o botão direito do mouse no nome do projeto e selecionei Add | Class no menu de contexto. Em seguida, na categoria ATL, selecionei o ATL Simple Object Template. Em seguida, no assistente ATL Simple Object Template, digitei "MyMethods" no campo C++ Short Name, que preencheu automaticamente os sete campos de nome restantes (arquivo .h como MyMethods.h, Class como CMyMethods, arquivo .cpp como MyMethods.cpp, Coclass como MyMethods, Type como MyMethodsClass, Interface como IMyMethods e ProgID como MyCOMLib.MyMethods).

Depois de concluir o assistente, alternei para a janela Class View, cliquei com o botão direito do mouse na interface IMyMethods e selecionei Add | Add Method no menu de contexto. No assistente Add Method, chamei meu método de TriMax. Em seguida, adicionei os parâmetros de entrada, chamados x, y, e z, todos do tipo LONG e adicionei um parâmetro retval LONG* para conter o valor de retorno do meu método. Depois de clicar no botão Finish no assistente Add Method, voltei para a exibição Solution Explorer no Visual Studio e cliquei duas vezes em MyMethods.cpp na pasta Source Files. Aqui está o código simples do meu método que está sendo testado:

STDMETHODIMP CMyMethods::TriMax(LONG x, LONG y, LONG z, LONG* pRes)
{
  if (x > y && x > z)
    *pRes = x;
  else if (y > z)
    *pRes = y;
  else
    *pRes = z;

  return S_OK;
}

Pressionei Ctrl+Shift+B para realizar uma compilação local do meu objeto COM. E, finalmente, usei o Windows Explorer para verificar se o arquivo MyCOMLib.dll resultante foi criado com êxito no subdiretório \MyCOMLib\Debug. Como você pode ver, meu método TriMax é inacreditavelmente simples; só preciso de algo com que realizar a automação de teste. Deixe-me enfatizar que embora os exemplos seguintes usem o código de automação de teste JavaScript para testar um objeto COM Win32, os princípios do uso do Visual Studio Team System para gerenciar a automação de teste personalizada se aplicam a qualquer tipo de automação de teste personalizada (incluindo C#, Visual Basic® .NET, Windows PowerShell®, e semelhantes) destinados a qualquer tipo de sistema que está sendo testado (incluindo bibliotecas .NET gerenciadas, aplicativos baseados em formulário do Windows®, aplicativos Web ASP.NET, serviços da Web ASP.NET, etc.).

Empacotando um script de automação de teste simples

Agora vamos observar o script de automação simples mostrado na Figura 3. Mais tarde, mostrarei como integrar script no VSTE for Testers e descreverei as vantagens de fazer isso. Meu script começa exibindo duas mensagens usando o método Echo do Windows Script Host. Em seguida, instanciarei o objeto COM que contém meu método que está sendo testado:

Figure 3 Script de automação de teste simples (Close Figure 3)
// testSimple.js

WScript.Echo("\nBegin test run\n");
WScript.Echo("Testing TriMax() in MyCOMLib.dll\n");
var o = new ActiveXObject("MyCOMLib.MyMethods");

var cases = new Array( "001:3*7*5:7",
                       "002:0*0*0:1",
                       "003:2*4*6:6" );
var allPassed = true;

for (var i = 0; i < cases.length; ++i)
{
  var tokens = cases[i].split(":");
  var id = tokens[0];
  var inputs = tokens[1];
  var expected = tokens[2];

  var temp = inputs.split("*");
  var arg1 = temp[0];
  var arg2 = temp[1];
  var arg3 = temp[2];

  WScript.Echo("==============");
  WScript.Echo("Case ID = " + id);
  WScript.Echo("Inputs = " + arg1 + " " + arg2 + " " + arg3);
  var actual = o.TriMax(arg1,arg2,arg3);
  WScript.Echo("Expected = " + expected);
  WScript.Echo("Actual = " + actual);
  if (actual == expected) {
    WScript.Echo("Pass");
  }
  else {
    WScript.Echo("**FAIL**");
    allPassed = false;
  }
}

WScript.Echo("==============");
if (allPassed == true) {
  WScript.Echo("\nAll test cases passed");
  WScript.Quit(0);
}
else {
  WScript.Echo("\nOne or more cases failed");
  WScript.Quit(1);
}

var o = new ActiveXObject("MyCOMLib.MyMethods");

Como estou testando um objeto COM clássico, posso fazer referência ao objeto usando o ProgID do objeto. Além disso, como compilei o objeto COM no meu computador host de teste, as informações do objeto foram armazenadas no registro do meu sistema. Observe que se eu tivesse compilado o arquivo MyCOMLib.dll no meu computador host de teste a partir de um computador diferente, eu precisaria registrar a DLL chamando regsvr32.exe por meio do método Run do objeto WScript.Shell. Os armazenamentos do meu script de exemplo testam seus dados de caso internamente:

var cases = new Array( "001:3*7*5:7",
                       "002:0*0*0:1",
                       "003:2*4*6:6" );
var allPassed = true;

Como regra geral, é melhor armazenar os dados do caso de teste externamente para testar o código de automação. O armazenamento externo permite que os casos de teste sejam compartilhados e modificados mais facilmente do que os dados armazenados internamente. Entretanto, os dados internos são mais simples e às vezes uma boa opção de design.

Neste exemplo simples, tenho três casos de teste. O primeiro teste do método TriMax(x,y,z) possui uma ID 001, entradas 3, 5, e 7, e um resultado esperado 7. O segundo caso de teste possui uma falha deliberada para fins de demonstração. Observe que inicializo uma variável chamada allPassed como verdadeira. Essa variável controla se todos os casos de teste individuais passam ou não. Minha automação de teste itera através de cada caso de teste, analisando a ID do caso de teste, as entradas e os valores esperados. Com esses valores em mãos, meu script invoca o método que está sendo testado e compara o resultado real com o resultado esperado para determinar um resultado de aprovação ou reprovação do caso de teste individual:

var actual = o.TriMax(arg1,arg2,arg3);
if (actual == expected) {
  WScript.Echo("Pass");
}
else {
  WScript.Echo("**FAIL**");
  allPassed = false;
}

Caso encontre algum caso de teste reprovado, defino o valor da variável allPassed como falso. Finalmente, meu script de teste de exemplo termina usando o método WScript.Quit para definir o código de saída do processo como 0 ou 1:

if (allPassed == true) {
  WScript.Echo("\nAll test cases passed");
  WScript.Quit(0);
}
else {
  WScript.Echo("\nOne or more cases failed");
  WScript.Quit(1);
}

Se você pretende gerenciar a automação de teste personalizada simples com o VSTE for Testers, a automação deve definir um código de saída 0 para indicar que o resultado geral do teste é aprovado ou um código de saída 1 para indicar falha. Isso significa que se for integrar a automação de teste existente com o VSTS, você provavelmente terá que editar a sua automação porque a automação de teste raramente define o valor do código de saída. A forma exata de definir o código de saída varia ligeiramente de linguagem para linguagem. Por exemplo, em um programa de linguagem compatível com .NET, você pode usar a propriedade System.Environment.ExitCode, e em um script do Windows PowerShell, você pode usar a instrução de saída.

Nesse momento, você pode executar o script de automação de teste localmente, como mostrado na Figura 1. Mas você também pode executar a automação personalizada de dentro do VSTE for Testers. Selecione File | New | Project. A caixa de diálogo New Project possui um tipo Test Projects com um subtipo Test Documents . Selecione Test Documents e você verá um modelo Test Project. Em seguida, dê um nome para o projeto de teste tal como MyTestProject e especifique um local para o diretório raiz do projeto.

Um Test Project pode conter um ou mais testes. Cada teste pode conter um ou mais casos de teste individuais. Por padrão, você obterá um Test Project que contém um documento de texto de ajuda geral chamado AuthoringTests.txt e os esqueletos dos dois testes: um teste manual e um teste de unidade. Você pode excluir esses três arquivos na janela Solution Explorer.

Agora, clique com o botão direito do mouse em seu nome Test Project e selecione Add | New Test no menu de contexto. Na caixa de diálogo Add New Test, selecione o modelo Generic Test, chame-o de Simple.GenericTest e clique em OK. O VSTE for Testers possui modelos internos para testes de carga, testes de unidade e testes da Web. O tipo Generic Test é essencialmente um invólucro da automação de teste personalizada e possui todos os detalhes necessários para permitir que a automação personalizada interopere com o VSTE for Testers. A captura de tela na Figura 4 mostra-lhe a aparência do processo de invólucro da automação de teste personalizada como um Generic Test.

Figura 4 Criando Generic Test simples
Figura 4 Criando Generic Test simples(Clique na imagem para diminuir a exibição)
Figura 4 Criando Generic Test simples
Figura 4 Criando Generic Test simples (Clique na imagem para aumentar a exibição)

O primeiro campo especifica um programa de teste existente a ser executado. Esse valor deve ser o caminho para um arquivo .exe. (Embora eu tenha usado um caminho absoluto neste exemplo, você pode adicionalmente especificar o caminho do script como uma variável de ambiente, um caminho relativo ou um caminho de implantação). No caso da automação de teste baseada em script, é o programa cscript.exe, geralmente localizado em C:\Windows\System32\. Se a sua automação de teste for um .exe, tal como um programa C#, você especificaria o caminho completo para a automação. O segundo campo especifica argumentos da linha de comando que são passados para o arquivo .exe especificado no primeiro campo. Neste exemplo, o valor seria o caminho completo para o script, tal como C:\VSTTIntegration\Scripts\testSimple.js. Esses são os dois únicos campos que você precisa especificar para executar testes simples baseados em script com armazenamento de caso de teste interno.

Existem várias formas de executar testes de dentro do VSTE for Testers. Uma forma simples é exibir todos os testes em seu projeto clicando em Test | Windows | Test View. Em seguida, selecione o teste que deseja executar, clique com o botão direito do mouse no nome do teste e clique em Run Selection. A captura de tela na Figura 5 mostra um exemplo.

Figura 5 Executando um Generic Test
Figura 5 Executando um Generic Test(Clique na imagem para diminuir a exibição)
Figura 5 Executando um Generic Test
Figura 5 Executando um Generic Test (Clique na imagem para aumentar a exibição)

A janela Test Results exibe o resultado geral de aprovação e reprovação. Se clicar duas vezes no resultado, você obtém resultados detalhados, incluindo a duração da execução de teste, a hora de início, a hora de término e uma cópia da saída do console. Um resultado de teste é armazenado por padrão na máquina host como um arquivo XML com uma extensão de arquivo .trx (resultados de teste XML) em um subdiretório TestResults. Por padrão, esses nomes de arquivo .trx são marcados com nome de usuário / nome da máquina / data / hora, o que os torna fáceis de organizar. A estrutura real dos arquivos .trx é algo complexa mas fácil de interpretar. Por exemplo, uma parte pequena do arquivo .trx gerado por este Generic Test de exemplo é:

   <errorInfo>
     <message type="System.String">
       The test failed with exit code 1.
     </message>
     <stackTrace type="System.String" />
   </errorInfo>

O exemplo que acabei de descrever empacota a automação de teste personalizada que possui dados de caso de teste incorporados ao código de automação. Se a automação lê dados do caso de teste a partir de uma origem externa, talvez seja preciso editar um dos campos Generic Test. Suponha que a automação de teste se pareça com:

   var fsi = new ActiveXObject(
     "Scripting.FileSystemObject");
   var fi = fsi.OpenTextFile(".\\testCases.txt");

   while (!fi.AtEndOfStream)
   {
     line = fi.ReadLine();
     // parse test case data
     // call method under test
     // determine pass/fail
   }

Aqui, o script usa um caminho de arquivo relativo para ler os dados do caso de teste a partir de um arquivo localizado no mesmo diretório que o script. Quando um Generic Test é executado, ele copia os executáveis para um diretório de trabalho, o que significa que os dados do caso de teste não seriam encontrados. Uma solução é especificar um caminho codificado no campo Working Directory quando criar um Generic Test. Uma abordagem um pouco mais flexível é definir os valores de uma ou mais variáveis de ambiente como %TestDir%, %TestLocation% e %TestOutputDirectory% e, em seguida, usar essas variáveis de ambiente para controlar a execução de teste. Entretanto, a opção mais flexível é declarar o script como um item de implantação no Visual Studio. Fazendo isso, os dados do caso de teste serão copiados para o diretório de saída para que possam ser referenciados através de um caminho local e relativo.

Resultados de teste estendidos:

Agora você viu como usar o VSTE for Testers para gerenciar a automação de teste personalizada simples. Você também pode criar uma automação de teste que salva informações adicionais e os resultados de teste mais detalhados. A idéia básica é codificar sua automação de teste de forma que ela salve resultados de teste detalhados intermediários como um simples arquivo XML que esteja em conformidade com um esquema SummaryResult.xsd especial. O VSTE for Testers usa automaticamente o arquivo de resultados XML simples emitido para criar o arquivo de resultados .trx mais complexo que é gerenciado pelo Visual Studio Team System. Esse processo de duas etapas é muito mais fácil do que tentar escrever a automação de teste que produz diretamente um arquivo de resultados de teste .trx. É mais fácil entender esse processo trabalhando de frente para trás. A listagem na Figura 6 mostra um arquivo de resultados de teste XML que está em conformidade com SummaryResult.xsd e, portanto, pode ser convertido automaticamente para um arquivo de resultados .trx.

Figure 6 Arquivo de resultados intermediários de teste estendido intermediário (Close Figure 6)
<SummaryResult>
  <TestName>MyCOMLib Test Run</TestName>
  <TestResult>Failed</TestResult>
  <DetailedResultsFile>
    C:\AdditionalResults\Details.txt
  </DetailedResultsFile>
  <InnerTests>
    <InnerTest>
      <TestName>001</TestName>
      <TestResult>Passed</TestResult>
    </InnerTest>
    <InnerTest>
      <TestName>002</TestName>
      <TestResult>Failed</TestResult>
    </InnerTest>
  </InnerTests>
</SummaryResult>

O arquivo na Figura 6 exigiu valores gerais TestName e TestResult e um valor opcional geral DetailedResultsFile. Outros valores opcionais definidos pelo esquema SummaryResult.xsd incluem InnerTest para armazenar o nome e os resultados dos casos de teste que formam os valores gerais Generic Test e ErrorMessage.

O arquivo do esquema SummaryResult.xsd é instalado com o VSTE for Testers e geralmente se encontra em C:\Program Files\Microsoft Visual Studio 8\Xml\Schemas\. O esquema define os tipos de resultado diferentes de simplesmente aprovado e reprovado. Os tipos de resultado definidos adicionais são Aborted, Error, Inconclusive, NotRunnable, NotExecuted, Discontinued, Warning, InProgress, Pending, PassedButRunAborted e Completed.

Como exatamente você escreve sua automatização de teste personalizada para emitir um arquivo de resultados de caso de teste XML estendido e intermediário depende da linguagem de programação que está usando e da estrutura da lógica de automação. Se você quiser usar a marca InnerTest, o esquema SummaryResult.xsd define que deve especificar o resultado de teste geral antes de especificar o resultado de cada caso de teste. Isso significa que você não pode compilar o arquivo de resultados XML intermediário de uma forma estritamente seqüencial enquanto processa os dados do caso de teste.

Há muitas abordagens possíveis. A listagem na Figura 7 mostra uma forma de modificar o script de automação de caso de teste apresentado na Figura 3 para emitir um arquivo de resultados estendido. A idéia geral é compilar o arquivo de resultado XML como uma cadeia de caracteres longa, capturando os resultados de cada caso de teste conforme cada caso é processado e, em seguida, construindo a cadeia de caracteres final depois que o resultado geral do teste for conhecido.

Figure 7 O script de teste que emite um arquivo de resultados estendido (Close Figure 7)
// testExtended.js

function main()
{
  WScript.Echo("\nBegin test run\n");
  var o = new ActiveXObject("MyCOMLib.MyMethods");

  var cases = new Array( "001:3*7*5:7",
                         "002:0*0*0:1",
                         "003:2*4*6:6" );
  var allPassed = true;
  var innerTestText = new Array(cases.length);

  for (var i = 0; i < cases.length; ++i)
  {
    var tokens = cases[i].split(":");
    var id = tokens[0];
    var inputs = tokens[1];
    var expected = tokens[2];

    var temp = inputs.split("*");
    var arg1 = temp[0];
    var arg2 = temp[1];
    var arg3 = temp[2];

    WScript.Echo("==============");
    WScript.Echo("Case ID = " + id);
    WScript.Echo("Inputs = " + arg1 + " " + arg2 + " " + arg3);
    var actual = o.TriMax(arg1,arg2,arg3);
    WScript.Echo("Expected = " + expected);
    WScript.Echo("Actual = " + actual);

    innerTestText[i] = "<TestName>" + id + "</TestName>";
    innerTestText[i] += "<TestResult>";  // inner test result

    if (actual == expected) {
      WScript.Echo("Pass");
      innerTestText[i] += "Passed";
    }
    else {
      WScript.Echo("FAIL");
      innerTestText[i] += "Failed";
      allPassed = false;
    }

    innerTestText[i] += "</TestResult>";
  } // main loop

  WScript.Echo("==============");
  var extendedResults = "<SummaryResult>";
  extendedResults += "<TestName>MyCOMLib Test Run</TestName>";
  extendedResults += "<TestResult>";  // overall meta-result
  if (allPassed == true) {
    extendedResults += "Passed</TestResult>";
  }
  else {
    extendedResults += "Failed</TestResult>";
  }

  extendedResults += "<InnerTests>";

  for (var j = 0; j < innerTestText.length; ++j) {
    extendedResults += "<InnerTest>";
    extendedResults += innerTestText[j];
    extendedResults += "</InnerTest>";
  }

  extendedResults += "</InnerTests>";
  extendedResults += "</SummaryResult>";

  var fso = new ActiveXObject("Scripting.FileSystemObject");
  var f = fso.CreateTextFile("C:\\VSTTIntegration\\Scripts\\results.xml");
  f.WriteLine(extendedResults);
  f.Close();
} // main()

main()

Eu crio uma matriz de cadeias de caracteres, uma cadeia para cada caso de teste, para conter os valores InnerTest:

var innerTestText = new Array(cases.length);

Dentro do loop de processamento principal, escrevo o valor TestName e o início do valor TestResult:

innerTestText[i] = "<TestName>" + id + "</TestName>";
innerTestText[i] += "<TestResult>"; // inner test result

Depois de processar um caso de teste individual, posso fornecer um valor de resultado de teste e adicionar a marca TestResult de fechamento:

if (actual == expected) {
  WScript.Echo("Pass");
  innerTestText[i] += "Passed";
}
else {
  WScript.Echo("FAIL");
  innerTestText[i] += "Failed";
  allPassed = false;
}
innerTestText[i] += "</TestResult>";

Depois que o loop de processamento principal tiver terminado e todos os meus dados InnerTest tiverem sido construídos, posso criar a primeira parte do meu arquivo de resultados estendido:

var extendedResults = "<SummaryResult>";
extendedResults += "<TestName>MyCOMLib Test Run</TestName>";
extendedResults += "<TestResult>";  // overall meta-result
if (allPassed == true) {
  extendedResults += "Passed</TestResult>";
}
else {
  extendedResults += "Failed</TestResult>";
}

Termino adicionando os dados de InnerTest que salvei anteriormente e escrevendo a cadeia de caracteres inteira como um arquivo XML:

extendedResults += "<InnerTests>";
for (var j = 0; j < innerTestText.length; ++j) {
  extendedResults += "<InnerTest>";
  extendedResults += innerTestText[j];
  extendedResults += "</InnerTest>";
}

extendedResults += "</InnerTests>";
extendedResults += "</SummaryResult>";

var fso = new ActiveXObject("Scripting.FileSystemObject");
var f = fso.CreateTextFile("C:\\VSTTIntegration\\Scripts\\results.xml");
f.WriteLine(extendedResults);

Há muitas outras abordagens que você pode usar para emitir resultados XML. Minha coluna "Five Ways to Emit Test Results as XML" (Cinco maneiras de emitir resultados de teste como XML) da edição de junho de 2006 da MSDN® Magazine (msdn.microsoft.com/msdnmag/issues/06/06/TestRun) descreve algumas dessas técnicas.

Nesse momento, possuo um script de automação de teste personalizado que escreve resultados de teste detalhados em um arquivo results.xml que está em conformidade com o esquema SummaryResult.xsd. Para usar essa automação de dentro do VSTE for Testers, criei um novo Generic Test em torno da automação, usando a técnica que descrevi anteriormente, mas com uma alteração. Depois de selecionar Add | New Test e nomear o teste (digamos, Extended.GenricTest) no painel de definição, marco a caixa de seleção Summary Results File e insiro a localização do arquivo de resultados na caixa de texto associada (digamos, C:\VSTTIntegration\Scripts\Results\results.xml). Agora, quando executo meu Generic Test, o VSTE for Testers executará a minha automação, localizará o arquivo results.xml intermediário e usará automaticamente os dados para criar o arquivo de resultado .trx final, detalhado, marcado com nome-data-hora. Muito claro!

Publicando resultados de teste no Team Foundation Server

Empacotar a automação de teste personalizada como um Generic Test usando o VSTE for Testers lhe oferece uma ótima maneira de gerenciar testes e resultados de teste. Se estiver usando o Team Foundation Server no ambiente de implantação, você pode publicar os resultados de teste no depósito de dados do TFS para gerenciar melhor os resultados de teste.

O depósito de dados back-end do TFS é extremamente complexo, para que você não queira tentar salvar os resultados de teste .trx diretamente nas tabelas SQL subjacentes. Entretanto, você pode salvar os dados do resultado de teste no TFS por meio do VSTE for Testers. O processo é muito simples. Depois de executar um Generic Test, conecte o projeto TFS, selecionando Tools | Connect to Team Foundation Server no menu principal. No menu da janela Test Results, clique no ícone Publish. Você verá uma caixa de diálogo Publish Test Results semelhante a que é mostrada na Figura 8.

Figura 8 Publicando resultados de teste no Team Foundation Server
Figura 8 Publicando resultados de teste no Team Foundation Server(Clique na imagem para diminuir a exibição)
Figura 8 Publicando resultados de teste no Team Foundation Server
Figura 8 Publicando resultados de teste no Team Foundation Server (Clique na imagem para aumentar a exibição)

Você pode selecionar um ou mais resultados de teste carregados e, em seguida, escolher a compilação apropriada do sistema que está sendo testado a partir de um controle suspenso. Observe que o arquivo de resultados de teste deve se localizar no diretório de saída de teste para que esteja disponível para publicação no depósito de dados. Depois que você clicar em OK, os resultados de teste serão publicados no depósito de dados do TFS. Essa abordagem permite que qualquer pessoa da sua equipe de desenvolvimento visualize facilmente os resultados de teste.

Uma forma de exibir os resultados de dentro do VSTE for Testers é conectar o projeto TFS e expandir a pasta Builds na janela Team Explorer. Se você clicar duas vezes no tipo de compilação apropriado, verá uma lista de todas as compilações específicas desse tipo. Se você clicar duas vezes em uma entrada de compilação específica, verá os dados associados a essa publicação, incluindo os resultados de teste publicados. Melhor ainda, como os dados do relatório de bugs do TFS podem ser associados aos resultados de teste publicados, você pode exibir resultados internos tais como Tests Failing without Active Bugs, ou pode criar relatórios personalizados que usam esses dados.

Observe que para publicar os resultados de teste no Team Foundation Server, você deve associar esses resultados de teste a uma compilação específica do sistema que está sendo testado. Isso significa que você deve ter um sistema de compilação do TFS disponível. Mas e se você estiver realizando teste de regressão em um componente herdado que não esteja mais sendo compilado ativamente, ou até mesmo um componente cujo código-fonte não existe mais? Uma solução simples é criar um projeto fictício, realizar uma única compilação desse projeto e, em seguida, usar esse número de compilação como um espaço reservado para direcionar os resultados do teste. Para obter mais informações, consulte a coluna Team System escrita por Brian Randell, "Team Foundation Server Version Control," na edição de janeiro de 2007 da MSDN Magazine (msdn.microsoft.com/msdnmag/issues/07/01/TeamSystem).

Resumindo

O Visual Studio 2005 Team Edition for Testers possui a habilidade de criar uma ampla gama de tipos de teste internos, tais como testes de unidade e testes de carga. Esses tipos de teste bem conhecidos estão completamente integrados no processo de desenvolvimento e são geralmente associados ao desenvolvimento controlado por teste. Entretanto, há muitas situações de desenvolvimento possíveis em que você possui automação de teste herdada ou deseja criar a automação de teste personalizada. Por exemplo, você pode terceirizar o esforço de teste e por motivos de segurança não deseja que a equipe de teste tenha acesso direto ao código-fonte do sistema. O VSTE for Testers permite que você empacote qualquer tipo de automação de teste personalizada em um tipo Generic Test. Além de fornecer uma estrutura de gerenciamento centralizada para a automação de teste personalizada, você pode ganhar automaticamente dados de resultados extras como a data de início, a data de término e a duração de cada execução de teste, assim como um log de qualquer saída do console.

Os resultados Generic Test são armazenados como arquivos .trx na máquina host local, e podem facilmente interoperar com outros sistemas de teste, se necessário, pois são armazenados em um formato XML padrão. Embora você possa configurar a automação de teste personalizada para registrar um simples resultado de aprovação e reprovação, o VSTE for Testers também lhe dá a habilidade de registrar resultados estendidos, incluindo tipos de resultado como Inconclusive e Aborted. Faça isso modificando a automação de teste personalizada para produzir um arquivo de resultados XML intermediário que esteja em conformidade com um esquema SummaryResult.xsd. O VSTE for Testers usa automaticamente o arquivo XML intermediário para produzir um arquivo .trx detalhado.

Em cenários de desenvolvimento em que você esteja usando o Team Foundation Server para gerenciar o código-fonte, os documentos de especificação, os dados de bug e o processo de compilação, é possível publicar os resultados de teste no depósito de dados do TFS. Publicar os resultados de teste no TFS permite que os mesmos sejam visualizados por todos os membros da equipe de desenvolvimento e determina facilmente a relação entre bugs, compilações e resultados de teste.

O Visual Studio 2008 foi lançado recentemente e, pelo que viu até agora, você terá toda a habilidade existente de gerenciar a automação de teste personalizada, além do gerenciamento de teste adicional que o ajudará a criar sistemas de software melhores.

Agradecemos a Howard Dierking por sugerir o assunto desta coluna e ao especialista em Team System Brian Randell por revisá-la.

Envie suas dúvidas e comentários para James em testrun@microsoft.com.

Dr. James McCaffrey trabalha para a Volt Information Sciences Inc., onde gerencia o treinamento técnico para engenheiros de software que trabalham no campus de da Microsoft em Redmond. Ele trabalhou em vários produtos da Microsoft, incluindo o Internet Explorer e o MSN Busca e James é o autor de Receitas de automação de teste de .NET (Apress, 2006) e pode ser contactado através do email jmccaffrey@volt.com ou v-jammc@microsoft.com.


Wicked Code: Dicas, truques e pr&#225;ticas recomendadas do Silverlight

Wicked Code
Dicas, truques e práticas recomendadas do Silverlight
Jeff Prosise

Download do código disponível em: WickedCode2008_Launch.exe (33860KB)
Conteúdo
Siga as dicas de desempenho à risca
Ofereça uma experiência de instalação atrativa
Não faça o usuário esperar
Use CreateFromXaml para reduzir o tamanho do XAML
Para acessibilidade, ofereça suporte a zooms interativos
Use Storyboards para animações manuais
Use a propriedade Tag para armazenar dados por objeto
Use ASP.NET AJAX para retornos de chamada do servidor

A
não ser que você estivesse preso em uma ilha deserta nos últimos meses, sem dúvida já ouviu falar sobre o Silverlight™, a nova solução para vários navegadores e plataformas da Microsoft, que auxilia você a criar aplicativos avançados da Internet e experiências avançadas e de imersão no navegador. A Versão 1.0, que combina mecanismo de processamento XAML com uma API JavaScript, fornecida em setembro de 2007. A Versão 2.0, que em breve estará na versão beta, será emparelhada com um mecanismo XAML avançado com uma versão do CLR executada no navegador, uma versão do Silverlight do Microsoft® .NET Framework Base Class Library (BCL) e API gerenciada. (Isso mesmo: C# no navegador!)

Os aplicativos Silverlight 1.0 estão se proliferando na Web, graças, em parte, a algumas das empresas de perfil alto que foram pioneiras na sua ação e ajudaram a disseminar a admiração. O modelo de programação é simples o suficiente para que um desenvolvedor experiente comece a criar aplicativos Silverlight em uma questão de horas. No entanto, o que está faltando para essa recente plataforma é a infinidade de conhecimento e recursos que acompanham plataformas mais desenvolvidas.

Estou desenvolvendo aplicativos Silverlight há mais de um ano agora e reuni uma lista das práticas recomendadas que eu e outros desenvolvedores na minha equipe utilizam para a criação de aplicativos melhores. Esta coluna contém algumas das mais úteis.

Siga as dicas de desempenho à risca

Não muito antes de o Silverlight 1.0 ser fornecido, a Microsoft publicou um documento intitulado “Dicas de desempenho para aplicativos baseados em Silverlight” (msdn2.microsoft.com/bb693295). Este guia é uma compilação de dicas para a otimização de desempenho ao evitar alguns erros comuns. As práticas recomendadas que você encontrará incluem:

  • Use a propriedade Visibility em vez de Opacity para ocultar objetos.
  • Não use as propriedades Width e Height dos objetos MediaElement e Path.
  • Desconecte por programação manipuladores de eventos registrados depois do uso.
  • Use com cautela planos de fundo de controle.

A maioria dessas dicas faz sentido à medida que você prosseguir com a leitura. Não é difícil imaginar por que é melhor codificar vídeos usando a largura e a altura na qual você pretende exibi-los (em vez de codificá-los em uma escala e exibi-los em outra). Isso por que o mecanismo de processamento do Silverlight pode evitar o redimensionamento de cada estrutura imediatamente. Ainda ssim, é bom vê-los escritos e saber que o Silverlight não faz nada especial para redimensionar vídeos sem incorrer em uma penalidade de desempenho.

A melhor prática seria adicionar isso ao documento, a fim de evitar chamadas redundantes a FindName. Vejo com freqüência manipuladores de eventos estruturados desta forma:

function onClick(sender, args)
{
    var rect = sender.findName('Rect');
    rect.fill = 'red';
}

O problema é que FindName precisa percorrer a árvore dos objetos XAML para localizar seu destino. Se você pretende fazer referência ao objeto XAML nomeado "Rect" muitas vezes ao longo do tempo de vida do aplicativo, deverá inicializá-lo uma vez — por exemplo, em um manipulador de eventos para o evento Loaded da canvas raiz — e armazenar a referência em uma variável global. Em seguida, em vez de chamar FindName sempre que onClick for chamado, você poderá fazer isto:

function onClick(sender, args)
{
    _rect.fill = 'red';
}

O código resultante será mais rápido do que o original. Mas se um desenvolvedor fizer isso, ele deverá estar certo de liberar essa referência ao concluir o plug-in. Em páginas normais, isso nunca é um problema, mas em um aplicativo AJAX em que as peças completas estão indo e vindo, é fundamental liberar essas referências para evitar uma perda de memória. Por esse motivo, o tipo ajax Sys.UI.Silverlight.Control que é utilizado ao usar o controle do Silverlight do ASP.NET (atualmente disponível no Extensions 3.5 Preview Community Technology Preview) fornece um método pluginDispose que você pode substituir para liberar referências e manipuladores de eventos.

Ofereça uma experiência de instalação atrativa

Com muita freqüência, a experiência de instalação do aplicativo baseado em Silverlight não é acessível suficiente para os usuários que não têm o Silverlight instalado, como exemplificado pelo ambiente apresentado na Figura 1. Silverlight.createObjectEx — a função geralmente chamada para instanciar um controle do Silverlight — exibe um botão "Get Microsoft Silverlight" quando o Silverlight não estiver presente. Clicar no botão transporta o usuário para o site da Web do Silverlight para download e instalação. Você pode aprimorar a experiência de certa forma ao definir o parâmetro inplaceInstallPrompt do Silverlight.createObjectEx como true, permitindo que o usuário faça download e instale o Silverlight sem sair da página da Web. Mas mesmo que sair seja algo desejado, especialmente quando o Silverlight envolve a maior parte ou todo o conteúdo na página.

Figura 1 Experiência de instalação do Silverlight padrão
Figura 1 Experiência de instalação do Silverlight padrão(Clique na imagem para diminuir a exibição)
Figura 1 Experiência de instalação do Silverlight padrão
Figura 1 Experiência de instalação do Silverlight padrão (Clique na imagem para aumentar a exibição)

A Microsoft publicou recentemente um documento que deveria ser leitura obrigatória para todos os desenvolvedores usando o Silverlight — "Silverlight Installation Experience Guide" (Guia de experiência de instalação do Silverlight). Ele está disponível para download junto com o código de amostra em go.microsoft.com/fwlink/?LinkId=106023. O documento descreve uma técnica geral para a exibição de conteúdo HTML (incluindo um botão "Get Microsoft Silverlight" e instruções específicas do navegador) em um DIV do Silverlight quando o Silverlight não estiver instalado e quando o conteúdo XAML estiver. A idéia é ter uma visão geral do que o usuário veria se o Silverlight estivesse instalado e felizmente o convenceria a clicar no botão.

Esta coluna é acompanhada por um aplicativo de amostra nomeado RevolvingAuto que demonstra algumas das práticas recomendadas apresentadas aqui, incluindo como criar uma melhor experiência de instalação. Mostrarei brevemente a você as principais partes desse código-fonte.

RevolvingAuto é notável para sua experiência de instalação. A Figure 2 mostra como seria a aparência da página inicial para um visitante que não tenha o Silverlight instalado. Por trás do botão "Get Microsoft Silverlight" há uma versão sombreada da mesma experiência de usuário que o usuário veria se o Silverlight estivesse instalado. Como a chamada para Silverlight.createObjectEx incluu um parâmetro inplaceInstallPrompt="true", o usuário pode instalar o Silverlight sem sair da página da Web. E, para finalizar bem, a página inicial inclui lógica para criar o controle do Silverlight depois que a instalação for concluída, eliminando a necessidade de uma atualização manual. (Esse recurso funciona muito bem no Internet Explorer®, pois ele não precisa ser reiniciado depois que o Silverlight for instalado. Infelizmente, é ineficaz em navegadores que requeiram uma reinicialização.)

Figura 2 RevolvingAuto antes da instalação do Silverlight
Figura 2 RevolvingAuto antes da instalação do Silverlight(Clique na imagem para diminuir a exibição)
Figura 2 RevolvingAuto antes da instalação do Silverlight
Figura 2 RevolvingAuto antes da instalação do Silverlight (Clique na imagem para aumentar a exibição)

O JavaScript que aciona a experiência de instalação reside no Default.html (consulte a Figura 3). Para criar um controle do Silverlight, a maioria dos aplicativos do Silverlight estruturam o código e a marcação da seguinte forma:

Figure 3 Aplicativo RevolvingAuto — Default.html (Close Figure 3)
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Revolving Auto</title>
    <script type="text/javascript" src="Silverlight.js"></script>
    <script type="text/javascript" src="Default.html.js"></script>
    <style>
        .AgInstalled {
            margin-left: auto; margin-right: auto;
            width: 612px; height: 700px; text-align: left }
        .AgNotInstalled {
            margin-left: auto; margin-right: auto;
            width: 612px; height: 700px; text-align: left;
            background-image: url(Images/Install.jpg);
            background-repeat: no-repeat; padding-top: 100px }
    </style>
</head>

<body style="background-color: black; text-align: center">
    <div style="height: 40px"></div>
    <div id="Container" class="AgInstalled">
        <div id="SilverlightPlugInHost" style="padding-left: 200px">
        </div>
    </div>
    <script type="text/javascript">
        var _id;

        if (!Silverlight.isInstalled('1.0'))
        {
            document.getElementById('Container').className =
                'AgNotInstalled';
            _id = window.setTimeout('checkInstall()', 3000);
        }
        else
            document.getElementById ('SilverlightPlugInHost')
                .removeAttribute('style');

        createSilverlight();

        function checkInstall()
        {
            if (Silverlight.isInstalled('1.0'))
            {
                window.clearInterval(_id);
                document.getElementById('Container')
                    .className = 'AgInstalled';
                document.getElementById ('SilverlightPlugInHost')
                    .removeAttribute('style');
                createSilverlight();
            }
        }
    </script>
</body>
</html>

<div id="SilverlightPlugInHost">
  <script type="text/javascript">
    createSilverlight();
  </script>
</div>

RevolvingAuto o estrutura da seguinte forma:

<div id="Container">
  <div id="SilverlightPlugInHost">
  </div>
</div>
<script type="text/javascript">
   // Code to create the control
</script>

O código no elemento <script> chama o Silverlight.isInstalled (que está implementado no Silverlight.js junto com Silverlight.createObjectEx) para determinar se o Silverlight está instalado. Se a resposta for sim, o script chama createSilverlight para criar o controle. Se a resposta for não, o script cria dinamicamente o estilo do DIV contendo o DIV do Silverlight, a fim de mostrar uma imagem em segundo plano e então chama o createSilverlight para exibir o botão "Get Microsoft Silverlight". A imagem em segundo plano é a que está atrás do botão na Figura 2.

Se ela determinar que o Silverlight não está instalado, o script de criação também utilizará window.setTimeout para configurar chamadas para uma função local nomeada checkInstall a cada três segundos. Assim que a instalação for concluída, checkInstall removerá a imagem em segundo plano, apagará o timer para que a função não seja chamada novamente e chamará createSilverlight de novo para criar o controle do Silverlight.

Não faça o usuário esperar

O ponto forto do Silverlight 1.0 é uma experiência de mídia avançada para imagens, áudio e vídeo. Mas arquivos como esses podem ser muito grandes (muito grandes!) e, se você declarar objetos Image ou MediaElement no XAML e declarar a mídia que os acompanhará, o tempo de download pode ser enorme. O problema é que o Silverlight não exibe um speck do XAML até que todo o XAML e os ativos aos quais ele faz referência tenham sido baixados. Enquanto isso, o usuário está se perguntando o que está acontecendo — ou se algo está acontencendo. Não é um grande problema se o usuário aguardar alguns segundos, mas se os segundos se tornarem minutos, é provável que o usuário desista e vá para sites mais bem desenvolvidos.

É por isso que aplicativos do Silverlight bem-desenvolvidos não são estatisticamente vinculados a ativos de mídia; em vez disso, eles usam o objeto de download do Silverlight incorporado, o que carrega a pilha XmlHttpRequest do navegador para que o download desses ativos seja realizado de modo assíncrono. Em vez de fazer isso

<MediaElement Source="FunnyVideo.wmv" />

declare o MediaElement sem um atributo Source, o que impede que o controle do Silverlight aguarde o download de um vídeo antes de processar qualquer XAML. Em seguida, crie um objeto de download e faça o download do vídeo de modo assíncrono, chamando o método SetSource do MediaElement para passar o vídeo para MediaElement assim que o download tiver sido concluído. Se você desejar, poderá atualizar um indicador de andamento aqui também. O downloader acionará os eventos downloadProgressChanged e Completed para auxiliar no processo.

O Silverlight também facilita o download de vários ativos em vez de apenas um. Basta empacotar todos os seus ativos — áudio, vídeo, imagens e até mesmo XAML ou XML — em um arquivo ZIP e usar o downloader para baixá-lo. Em seguida, ao chamar SetSource para atribuir conteúdo a um objeto, especifique o nome do arquivo no arquivo ZIP no segundo parâmetro. Isso não poderia ser mais simples.

Veja um exemplo:

downloader.open('GET', 'Assets.zip');
downloader.send();
  ...
// In the Completed event handler
_media.setSource(sender, 'FunnyVideo.wmv');

O aplicativo RevolvingAuto utiliza um objeto de download para fazer o download assíncrono de um arquivo ZIP contendo 105 arquivos de imagem. Juntos, esses arquivos têm uma exibição de 360 graus de um automóvel em incrementos de 3,5 graus. No tempo de execução, o aplicativo troca as imagens para dentro e para fora para girar o automóvel.

O manipulador de eventos DownloadProgressChanged atualiza uma barra de andamento, que não é nad mais do que um retângulo XAML cuja largura aumenta à medida que o download progride. O manipulador de eventos Completed atribui as imagens baixadas aos objetos Image criados dinamicamente. Ele também oculta a barra de andamento e cancela o registro dos dois manipuladores de evento do downloader, de acordo com o documento de desempenho referenciado na primeira seção deste artigo.

Falando em não fazer o usuário esperar, a maioria dos navegadores aciona a interface do usuário e executa JavaScript no mesmo thread. Conseqüentemente, se você se envolver em uma tarefa JavaScript de longa execução (digamos, uma manipulação de números que demore 5 ou 10 segundos para ser executada), a interface do usuário deixará de responder à duração. O documento sobre dicas de desempenho mencionado anteriormente recomenda dividir tarefas JavaScript de longa execução em tarefas menores e mais granulares por esse motivo. No Silverlight 2.0, você poderá delegar tarefas de longa execução a threads de segundo plano. No entanto, na versão 1.0, isso não é uma opção.

Use CreateFromXaml para reduzir o tamanho do XAML

Outra vantagem do aplicativo de amostra é como ele usa CreateFromXaml para criar objetos XAML Image dinamicamente em vez de desorganizar o arquivo XML com pontuações de declarações Image praticamente idênticas. O arquivo XAML que você verá na Figura 4, Scene.xaml, está praticamente vazio. Ele declara alguns Canvases e Rectangles, um Storyboard e um ScaleTransform, mas não declara um único objeto Image. Em vez disso, um loop for no Default.html.js cria 105 objetos Image — um por imagem na animação — e os inicializa com os bits de imagem a partir do downloader (consulte a Figura 5).

Figure 5 Aplicativo RevolvingAuto — Default.html.js (Close Figure 5)
function createSilverlight()
{
  Silverlight.createObjectEx({
    source: 'Scene.xaml',
    parentElement: document.getElementById('SilverlightPlugInHost'),
    id: 'SilverlightPlugIn',
    properties: {
      width: '900',
      height: '700',
      background:'black',
      isWindowless: false,
      inplaceInstallPrompt: true,
      version: '1.0'
    },
    events: {
      onError: null,
      onLoad: null
    },
    context: null
  });
}

var _zoom = 1;                  // Zoom factor
var _index = 0;                 // Image index
var _progressBar;               // Progress bar
var _progressBarContainer;      // Progress bar container
var _progressBarCanvas;         // Progress bar canvas
var _progressBarWidth;          // Full width of progress bar
var _control                    // Silverlight control
var _transform;                 // Zoom transform
var _timer;                     // Timer storyboard
var _token1, _token2;           // Event handler tokens
var _images = new Array(36);    // References to XAML images
var _photos = 105;              // Number of photos

function onLoaded(sender)
{
  // Initialize XAML references and other variables
  transform = sender.findName('ZoomTransform');
  timer = sender.findName('TimerStoryboard');
  progressBar = sender.findName('ProgressBar');
  progressBarContainer = sender.findName('ProgressBarContainer');
  progressBarCanvas = sender.findName('ProgressBarCanvas');
  progressBarWidth = _progressBarContainer.width - 4;
  control = sender.getHost();

  var downloader = _control.createObject('downloader');
  token1 = downloader.addEventListener('downloadProgressChanged',
    downloadProgressChanged);
  token2 = downloader.addEventListener('completed', downloadCompleted);
  downloader.open('GET', 'Assets/AutoPhotos.zip');
  downloader.send();
}

function downloadProgressChanged(sender, args)
{
  // Update the progress bar
 _progressBar.width = _progressBarWidth * sender.downloadProgress;
}

function downloadCompleted(sender, args)
{
  // Hide the progress bar
  progressBarCanvas.visibility = 'Collapsed';

  // Deregister downloader event handlers
  sender.removeEventListener('downloadProgressChanged', _token1);
  sender.removeEventListener('completed', _token2);

  // Create XAML images and assign downloaded bits to them
  var canvas = sender.findName('AutoCanvas');

  for (i=0; i<_photos; i++)
  {
    var xaml =
      '<Image Canvas.Left="225" Width="450" Visibility="Collapsed" />';
    var image = _control.content.createFromXaml(xaml);
    image.setSource(sender, (i + 1).toString() + '.JPG');
    canvas.children.add(image);
    images[i] = image;
  }

  // Register mousewheel event handler
  if (window.addEventListener)
    window.addEventListener('DOMMouseScroll', onMouseWheelTurned, false);
  else
    window.onmousewheel = document.onmousewheel = onMouseWheelTurned;

  // Make the first image visible
  images[0].visibility = 'Visible';

  // Start rotating
  timer.begin();
}

function onTick(sender, args)
{
  // Hide the current image
  images[_index].visibility = 'Collapsed';

  // Update the image index
  if (++_index == _photos)
    index = 0; // Wrap around

  // Show the new image
    images[_index].visibility = 'Visible';

  // Restart the timer
  timer.begin();
}

function onMouseWheelTurned(event)
{
  var delta = 0;
  if (!event) // Internet Explorer
    event = window.event;

  if (event.wheelDelta) // Internet Explorer
  {
    delta = event.wheelDelta;
    if (window.opera)
      delta = -delta;
  }
  else if (event.detail) // Mozilla
    delta = -event.detail;

  if (delta != 0)
  {
    if (delta > 0)
    {
      // Zoom in
      zoom = Math.min(2, _zoom + 0.05);
    }
    else
    {
      // Zoom out
      zoom = Math.max(1, _zoom - 0.05);
    }
    transform.scaleX = _transform.scaleY = _zoom;
  }

  if (event.preventDefault)
    event.preventDefault();

  event.returnValue = false;
}
Figure 4 Aplicativo RevolvingAuto — Scene.xaml (Close Figure 4)
<Canvas
  xmlns="http://schemas.microsoft.com/client/2007"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Loaded="onLoaded">
  <Canvas x:Name="ProgressBarCanvas"
    Canvas.Left="260" Canvas.Top="100">
    <Rectangle x:Name="ProgressBarContainer" Canvas.Left="0"
      Canvas.Top="0" Width="380" Height="12" Stroke="#606060" />
    <Rectangle x:Name="ProgressBar" Canvas.Left="2" Canvas.Top="2"
      Width="0" Height="8" Fill="#303030" />
  </Canvas>
  <Canvas x:Name="AutoCanvas">
    <Canvas.Resources>
      <Storyboard x:Name="TimerStoryboard" Duration="0:0:0.2"
        Completed="onTick" />
    </Canvas.Resources>
    <Canvas.RenderTransform>
      <ScaleTransform x:Name="ZoomTransform" CenterX="450" />
    </Canvas.RenderTransform>
  </Canvas>
</Canvas>

É sempre aconselhável manter arquivos XAML do menor tamanho possível, a fim de reduzir o tempo de carga do aplicativo. E não é mais lento. (A análise de XML não é, afinal, uma proposição barata.) Sempre que você se deparar declarando repetidamente objetos XAML, considere usar CreateFromXaml.

Para acessibilidade, ofereça suporte a zooms interativos

Atualmente, a "Seção 508" é uma palavra de efeito nos círculos de usabilidade e por um bom motivo: ela apresenta os requisitos federais dos EUA para tornar software acessível a pessoas com deficiências. Se você ainda não foi questionado sobre a conformidade com a seção 508, apenas aguarde — se você reside nos EUA, precisará saber sobre ela e provavelmente antes do que imagina. (Para obter detalhes sobre a Seção 508, acesse section508.gov.)

A Seção 508 cobre muitos aspectos básicos, desde a exigência de que as informações transmitidas por codificação de cor também estejam acessíveis por outros meios que não a codificação de cor (por exemplo, por meio de rótulos ou dicas de ferramentas pop-up) para impor limites inferiores e superiores sobre a freqüência de cintilação de página. Estranho o suficiente, ela não especifica um tamanho de fonte mínimo para auxiliar usuários com deficiências visuais. O tamanho do texto é algo familiar para mim, pois cheguei em uma idade em que preciso usar óculos de leitura para ler texto com fontes pequenas. É por isso que freqüentemente prossigo e crio um recurso de zoom nos aplicativos do Silverlight que crio. Felizmente, o Silverlight facilita muito o meu trabalho.

As ScaleTransforms do Silverlight podem ser usadas para dimensionar qualquer objeto XAML, incluindo objetos Canvas contendo outros objetos. A implementação de zooms interativos é tão simples quanto declarar um ScaleTransform e modificar suas propriedades ScaleX e ScaleY em resposta às ações da roda do mouse ou outros eventos. Para dimensionar toda a exibição do Silverlight, basta aplicar ScaleTransform à Canvas raiz.

Quando a página inicial do RevolvingAuto for exibida pela primeira vez, as imagens giratórias do automóvel serão relativamente pequenas. No entanto, você poderá aumentar a exibição em até duas vezes ao rolar a roda do mouse (consulte a Figura 6). O código-chave está localizado na função onMouseWheelTurned na Figura 6 (Default.html.js). Ele faz algum trabalho para considerar as diferenças no modo como os navegadores reportam eventos da roda do mouse e então incrementa ou reduz as propriedades ScaleX e ScaleY de um ZoomTransform.

Figura 6 RevolvingAuto antes e depois de aplicar zoom
Figura 6 RevolvingAuto antes e depois de aplicar zoom(Clique na imagem para diminuir a exibição)
Figura 6 RevolvingAuto antes e depois de aplicar zoom
Figura 6 RevolvingAuto antes e depois de aplicar zoom (Clique na imagem para aumentar a exibição)

Observe que você não perde a resolução quando aplica zoom no carro em rotação. Isso ocorre por que as imagens baixadas são duas vezes o tamanho dos objetos Image aos quais estão atribuídas. À medida que você aplicar zoom, o ScaleTransform aumentará as dimensões do objeto Image atualmente exibido, permitindo ao objeto Image um estado mais real para exibir as informações disponíveis para ele.

Use Storyboards para animações manuais

Um dos recursos mais interessates do Silverlight é seu suporte avançado para animações. Você pode fazer com que os objetos sejam exibidos e ocultados, aplicar zoom na página e exibir e ocultar algumas linhas do XAML. Mas você não pode animar tudo, pelo menos não de modo declarativo. Você pode animar facilmente propriedades numéricas, propriedades Color e Point. Mas se você desejar, digamos, animar uma imagem ao variar sua propriedade Source ao longo do tempo (alternando uma imagem para outra em cada marca), precisará escrever algum código e o modo como estruturá esse código afetará a qualidade do animação.

O Silverlight 1.0 não tem um objeto temporizador explícito e window.setTimeout é inferior ao ideal para animações. É por isso que os desenvolvedores experientes do Silverlight usam objetos Storyboard quando precisam de temporizadores programáveis. Tudo que você precisa fazer é declarar um objeto Storyboard vazio em XAML e designar um manipulador para seu evento concluído, da seguinte forma:

<Storyboard x:Name="Timer"
  Duration="0:0:0.05"
  Completed="onTick">
</Storyboard>

Quando estiver pronto para iniciar a animação, você poderá chamar o método de início do Storyboard:

_timer.begin();

Finalmente, no manipulador de eventos Completed, faça o que precisar (por exemplo, modificar uma propriedade Source do objeto Image) e a chamada começará para iniciar a execução do temporizador novamente:

function onTick(sender, args)
{
    // TODO: Do work
    _timer.begin();
}

Neste exemplo, _timer é uma variável que faz referência ao objeto XAML Storyboard nomeado "Timer".

Essa é precisamente a técnica que o RevolvingAuto usa para processar animações de rotação. A função onTick na Figura 5, que é chamada em resposta a eventos Storyboard.Completed, avança a animação em um quadro ao desativar a propriedade Visibility da imagem anterior (Collapsed) e ativar a propriedade Visibility da próxima imagem (Visible).

Use a propriedade Tag para armazenar dados por objeto

Cada objeto XAML que um aplicativo do Silverlight criar terá uma propriedade read/write nomeada Tag, que poderá ser usada para armazenar as configurações definidas pelo usuário. A propriedade Tag fornece um meio avançado e fácil de usar para associar dados arbitrários com objetos XAML.

Os membros da minha equipe e eu utilizamos a Tag de várias formas. Em um projeto cujos requisitos incluíam fornecer ao usuário a capacidade de pesquisar por meio de conteúdo processado pelo Silverlight, incorporamos palavras-chave nos objetos XAML da página e criamos um recurso de pesquisa que verificava o XAML do início ao fim inspecionando cada propriedade Tag do objeto. Em outra instância, utilizamos a Tag para incluir metadados nos nossos elementos XAML. No tempo de execução, utilizamos os metadados, que foram armazenados na forma de pares nome/valor, a fim de criar XAML mais complexo em torno dos elementos XAML no documento XAML. Um designer criando conteúdo para nosso aplicativo poderia, por exemplo, declarar um objeto Image desta forma:

<Image Source="ProductDemo.jpg"
  Tag="Mode:Lightbox" />

O nosso aplicativo veria "Mode:Lightbox" e encapsularia uma caixa leve em torno da imagem, permitindo que os usuários aumentem e diminuam o zoom, apliquem panorâmico na imagem e muito mais.

Um uso muito pragmático da propriedade Tag é como um meio de anexar descrições a imagens. Imagine uma tela contendo centenas de imagens, acompanhadas por uma exigência de exibir uma descrição da imagem quando o cursor for parado sobre uma delas. Você poderia usar a propriedade Tag para anexar descrições a cada imagem e ler a propriedade Tag da imagem no cursor antes de exibir uma descrição.

Use ASP.NET AJAX para retornos de chamada do servidor

Às vezes, os desenvolvedores lamentam o fato de que o Silverlight 1.0 tem essa pilha de rede limitada. Fora o objeto de download, a versão 1.0 oferece pouco em termos de rede. Isso irá mudar no Silverlight 2.0, que terá uma pilha de rede de vários protocolos avançada. No entanto, até então você poderia usar o ASP.NET AJAX para acionar chamadas de um navegador hospedando um aplicativo do Silverlight ao servidor.

O visualizador do Silverlight no aplicativo de catalogação da revista em quadrinhos MyComix que publiquei recentemente oferece um exemplo concreto. Para ver para você mesmo, digite na URL mycomix.wintellect.com/Spotlight.aspx?Item=1166 e aguarde até que seja exibida a capa de uma revista em quadrinhos. Em seguida, mova o cursor para a parte superior da janela; um painel de informações será exibido com informações sobre a revista em quadrinhos, incluindo seu título, número da edição, classificação e ano de publicação. Os dados no painel são originados de um banco de dados armazenado no servidor e são recuperados pela chamada de um Web service ASP.NET AJAX.

O Web service, MyComix.asmx, expõe um método nomeado GetComicInfo que aceita uma ID da revista em quadrinhos como um parâmetro de entrada e retorna um objeto ComicInfo contendo todas as informações disponíveis em relação à revista em quadrinhos. (Um Web service ASP.NET AJAX é basicamente uma ASMX convencional com a classe Web service atribuída ScriptService em vez de WebService.) A página do visualizador declara uma referência ao Web service da seguinte forma:

<asp:ScriptManager ID="ScriptManager1" runat="server">
    <Services>
        <asp:ServiceReference Path="~/MyComix.asmx" />
    </Services>
</asp:ScriptManager>

Em seguida, ela aciona uma chamada assíncrona ao método GetComicInfo do Web service, como mostrado aqui:

MyComixScriptService.GetComicInfo(_id, OnGetComicInfoCompleted);

A chamada é extremamente eficiente na conexão, pois usa codificação JSON (JavaScript Object Notation) e por não haver estado de exibição nem transmissão de outros dados desnecessários. E, como a chamada é assíncrona, o navegador não é congelado aguardando sua conclusão. Se o usuário exibir o painel de informações antes do retorno da chamada, o painel simplesmente será exibido em branco.

Você poderá fazer download do código-fonte MyComix, incluindo o Web service ASP.NET AJAX, a partir de wintellect.com/Downloads/MyComix.zip. O visualizador do Silverlight pode ser localizado nos arquivos nomeados Spotlight.aspx e Spotlight.xaml.

Como você está experimentando o Silverlight, poderá você mesmo saber quais são as práticas recomendadas e adicioná-las às apresentadas aqui. Se esse for o caso, envie um email para wicked@microsoft.com. Enquanto isso, feliz desenvolvimento no Silverlight!

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

Jeff Prosise é colaborador da MSDN Magazine e autor de vários livros, incluindo Programming Microsoft .NET (Microsoft Press, 2002). Ele também é co-fundador do Wintellect (www.wintellect.com), uma empresa de treinamento e consultoria em software, especializada na estrutura Microsoft .NET. Tem um comentário sobre esta coluna? Entre em contato com o Jeff em wicked@microsoft.com.


Esta&#231;&#227;o de servi&#231;o: Web Service Software Factory Modeling Edition.

Estação de serviço
Web Service Software Factory Modeling Edition.
Gerardo de Geest and Gerben van Loon

Download do código disponível em: ServiceStation2008_Launch.exe (199KB)
Conteúdo
Modelo de contrato de dados
Modelo de contrato de serviço
ASMX ou WCF?
Modelo de host
Gerando código
Personalizando o Service Factory
Proteção de exceção do Enterprise Library
Construindo a extensão
Usando a extensão

W
eb Service Software Factory: o Modeling Edition, também conhecido como Service Factory, é uma coleção de recursos para ajudá-lo a modelar e construir serviços Web para Windows® Communication Foundation (WCF) e ASMX de forma fácil e eficiente. A diferença principal das versões anteriores do Service Factory é que essa última edição usa modelos, enquanto a versão anterior é baseada em assistentes por meio do Guidance Automation Toolkit (GAT). O Service Factory agora permite que você construa um serviço da Web criando três modelos diferentes: o modelo de contrato de dados, o modelo de contrato de serviço e o modelo de host. Discutiremos primeiro esses três modelos e, em seguida, mostraremos como personalizar o Service Factory. Você pode encontrar informações adicionais nos sites listados na barra lateral "Online Service Factory Resources".

A desvantagem principal do uso de assistentes por meio do GAT para gerar código é que ele complica as coisas quando você precisa alterar algo no código gerado. Suponha que você queira renomear uma operação de um serviço gerado – é necessário salvar seu código personalizado e refazer o assistente inteiro onde o novo nome é usado, ou renomear a operação no código gerado. Refazer o assistente é demorado; como o assistente não tem memória, você precisa especificar tudo novamente. Alterar o código gerado pelo assistente não é ideal, já que você volta a trabalhar com os detalhes enquanto a meta do assistente é evitar isso. Os modelos oferecem uma boa solução para o problema porque, ao contrário dos assistentes, são capazes de lembrar da sua entrada.

Construir linguagens de modelagem que se integram no Visual Studio® era difícil no passado, mas as ferramentas DSL (linguagem específica do domínio) que vêm com o SDK do Visual Studio tornam isso mais fácil. O Service Factory agora vem com três tipos de modelos construídos com as ferramentas DSL. Observe que não há modelos para acesso aos dados e segurança de serviço. O Data Access Guidance Package da versão anterior do Service Factory agora está disponível como uma fábrica separada chamada Repository Factory, e o Security Guidance Package está disponível como um pacote separado no site da comunidade do Service Factory.

A nova versão do Service Factory também serve como um bom exemplo para construir suas próprias fábricas de software, já que ele usa e combina todas as ferramentas disponíveis para construir:

  • O GAT dos assistentes usados para gerar projetos e fornecer menus de contexto
  • As ferramentas DSL dos três tipos diferentes de modelo
  • O DIS (Designer Integration Service) para dar suporte a referências de modelo cruzado

A origem do Service Factory também contém algumas bibliotecas interessantes que possuem recursos que as ferramentas de fábricas não possuem. Poderia ser interessante reutilizar essas bibliotecas se você estivesse construindo sua própria fábrica:

  • Uma biblioteca de geração de código que permite gerar código em arquivos separados
  • Uma biblioteca de validação de modelo cruzado para ajudá-lo a verificar referências de modelo cruzado
  • Uma biblioteca de objetos de simulação do Visual Studio para ajudá-lo a fazer facilmente o teste de unidade da sua fábrica

Como mencionamos, o Service Factory consiste em três tipos de modelos. No modelo de contrato de dados, os contratos de dados do serviço da Web são definidos. No modelo de contrato de serviço, o serviço e seus contratos, operações e mensagens são definidos. Finalmente, o modelo de host é usado para modelar pontos de extremidade dos serviços e proxies de cliente.

Para apresentar a fábrica, criaremos um serviço da Web para pedir pizzas da Peedy's Pizza Palace. O serviço da Web receberá os pedidos de pizzas, permitindo que você escolha entre tamanhos e sabores diferentes. Quando o pedido é feito, o preço do pedido é retornado para o cliente como uma confirmação.

Modelo de contrato de dados

No modelo de contrato de dados, tipos diferentes de dados podem ser modelados juntos. Além de definir os tipos básicos como elementos do contrato, você também pode definir coleções, enumerações e contratos de falha. Os contratos de falha podem ser retornados quando algum erro ocorre no serviço.

No modelo de contrato de dados da Peedy's Pizza Palace, definimos PizzaOrder como uma coleção de pizzas que o cliente gostaria de pedir. Cada pizza possui um tamanho e um sabor. Isso é modelado com enumerações. Além disso, definimos um contrato de falha, OutOfOrder, que é lançado caso não haja uma pizza, o que significa que a Peedy's Pizza Palace não pode entregar a pizza porque está faltando algum dos ingredientes. Também é possível retornar valores mesmo quando há um contrato de falha. Por exemplo, nesse caso, mostramos o preço do pedido para o usuário mesmo assim. O modelo de contrato de dados completo é mostrado na Figura 1.

Figura 1 Modelo de contrato de dados da Peedy's Pizza Palace
Figura 1 Modelo de contrato de dados da Peedy's Pizza Palace(Clique na imagem para diminuir a exibição)
Figura 1 Modelo de contrato de dados da Peedy's Pizza Palace
Figura 1 Modelo de contrato de dados da Peedy's Pizza Palace (Clique na imagem para aumentar a exibição)

Modelo de contrato de serviço

Os contratos de dados são usados no modelo de contrato de serviço. Nesse exemplo, os contratos de serviço e as operações do serviço da Web da Peedy's Pizza Palace são modelados. O modelo (consulte a Figura 2) é, na verdade, muito simples, já que precisamos somente de uma operação para pedir pizzas.

Figura 2 Modelo de contrato de serviço da Peedy's Pizza Palace
Figura 2 Modelo de contrato de serviço da Peedy's Pizza Palace(Clique na imagem para diminuir a exibição)
Figura 2 Modelo de contrato de serviço da Peedy's Pizza Palace
Figura 2 Modelo de contrato de serviço da Peedy's Pizza Palace (Clique na imagem para aumentar a exibição)

Cada operação em um modelo de contrato de serviço pode ter um máximo de dois contratos de mensagem associados a ela: um contrato da mensagem de solicitação e um contrato da mensagem de resposta. Você pode adicionar partes a um contrato de mensagem, clicando com o botão direito do mouse nele. Uma parte pode ser um tipo primitivo, tal como System.Decimal, ou um contrato de dados como definido no modelo de contrato de dados. O DSL Model Element Selector, mostrado na Figura 3, exibe todos os contratos de dados disponíveis no projeto.

Figura 3 Selecionando o contrato de dados
Figura 3 Selecionando o contrato de dados

ASMX ou WCF?

Antes de começarmos a falar sobre o modelo de host, primeiro devemos criar a nossa estrutura de solução. Trata-se de um conjunto de projetos no qual o código dos serviços da Web será gerado. É nesse momento que temos que escolher a nossa tecnologia de implementação. É importante notar que no Service Factory, duas tecnologias já estão disponíveis – ASMX e WCF – e é fácil estender o Service Factory com a sua própria tecnologia de serviço. Para a nossa demonstração, usaremos o WCF. Para fazer essa seleção, simplesmente clicamos com o botão direito do mouse na solução e as operações mostradas na Figura 4 são exibidas. Selecionanos os projetos de implementação de WCF na parte inferior do menu, e o Service Factory cria dois projetos onde o código gerado a partir dos modelos será armazenado.

Figura 4 Criar a solução WCF Estrutura
Figura 4 Criar a solução WCF Estrutura

A Figura 5 mostra a estrutura da solução gerada. Observe que uma variedade de projetos foram gerados para facilitar o gerenciamento do código. Voltaremos a falar sobre essa estrutura de solução daqui a pouco, quando discutiremos a tabela de mapeamento do projeto.

Figura 5 Estrutura de solução da Peedy's Pizza Palace
Figura 5 Estrutura de solução da Peedy's Pizza Palace

Também precisamos especificar se querermos usar ASMX ou WCF no modelo de contrato de dados e no modelo de contrato de serviço. Cada um desses modelos possui uma propriedade chamada Implementation Technology que pode ser definida para ASMX, WCF, ou sua própria tecnologia de implementação personalizada. Quando essa propriedade é definida, muitas das entidades dos modelos obtêm novas propriedades que são específicas da tecnologia escolhida (consulte a Figura 6a). Como você pode ver na Figura 6b, depois que tivermos selecionado WCF como a tecnologia de implementação, uma categoria de configurações WCF é adicionada à janela de propriedades. Isso permite que você escolha de qual tipo deve ser a coleção PizzaOrder.

Figura 6a Antes de selecionar uma tecnologia
Figura 6a Antes de selecionar uma tecnologia
Figura 6b Depois de selecionar uma tecnologia
Figura 6b Depois de selecionar uma tecnologia

Modelo de host

O modelo de host é usado para modelar pontos de extremidade do serviço da Web e proxies de cliente. Esse modelo parece completamente diferente de dois outros modelos que discutimos anteriormente. Nesse momento, você pode começar a modelar no explorador do host, clicando com o botão direito do mouse nos elementos, ao contrário de simplesmente arrastar e soltar elementos na superfície de design. Clicando com o botão direito do mouse no elemento do modelo de host, você pode adicionar um novo host e um novo cliente, como mostrado na Figura 7.

Figura 7 Adicionando clientes e hosts ao modelo de host
Figura 7 Adicionando clientes e hosts ao modelo de host

Depois de adicionar um PizzaHost e um PizzaClient, a tecnologia da implementação de ambos pode ser selecionada. Optamos por definir ambos como WCF. Também é necessário selecionar um projeto de implementação para ambos. Felizmente, o Service Factory já os gerou para nós. Como você pode ver, a pasta Tests na estrutura da solução mostrada na Figura 5 contém os projetos PizzaService.Host e PizzaService.Client. Eles podem ser usados como projetos de implementação.

Criamos um novo ServiceReference, clicando com o botão direito do mouse no PizzaHost. Para esse novo ServiceReference, precisamos selecionar uma implementação de serviço – trata-se de uma referência ao nosso modelo de serviço. Podemos selecionar a implementação da mesma forma que selecionamos os contratos de dados na Figura 2.

Para cada ServiceReference que você cria, é possível definir pontos de extremidade diferentes. Criamos um ponto de extremidade, chamado PizzaEndpoint, e o configuramos para usar o wsHttpBinding.

Agora que temos um ponto de extremidade, podemos criar um proxy para nosso cliente. Para adicionar um novo proxy, clicamos com o botão direito do mouse no PizzaClient no explorador do host. Com uma caixa de combinação na janela de propriedade do proxy, podemos selecionar o ponto de extremidade. O modelo de host completo é mostrado na Figura 8.

Figura 8 Host completo Modelo do serviço de pizza
Figura 8 Host completo Modelo do serviço de pizza

Gerando código
Online Service Factory Resources
  • Ferramentas de linguagem específicas do domínio
  • Designer Integration Powertoy para ferramentas de linguagem específicas do domínio
  • Web Service Software Factory
  • O Repository Factory
  • Enterprise Library

Todos os modelos que precisamos para nosso serviço de pedido de pizza agora estão definidos, e estamos pontos para gerar o serviço. Para fazê-lo, primeiro precisamos preencher o campo da tabela de mapeamento do projeto de cada um dos nossos modelos – isso pode ser definido na janela de propriedade de cada modelo. Quando essa propriedade é selecionada, uma janela de seleção aparece. Nesse caso, só existe uma opção: PizzaService. Isso acontece porque todas as funções já foram definidas quando criamos a estrutura da solução. As funções podem ser encontradas no arquivo projectmapping.xml, que também faz parte da nossa estrutura de solução. Nesse arquivo, uma função pode ser associada a um projeto.

A próxima etapa é realmente gerar o código. Um clique com o botão direito do mouse no modelo de contrato de dados nos traz a opção Generate Code. Quando essa opção é selecionada, os contratos de dados no projeto PizzaService.DataContracts são gerados. O mesmo pode ser feito para o modelo de contrato de serviço, que gera código nos projetos PizzaService.MessageContracts, PizzaService.ServiceContracts e PizzaService.ServiceImplementation.

A geração de código do modelo de host é feita em duas etapas: geramos o código uma vez para o host e uma vez para o cliente. Para gerar o código do host, clicamos no PizzaService ServiceReference e o designer do Visual Studio apresenta um botão chamado Validate Model. Quando clicamos no botão, o modelo é validado corretamente e, em seguida, aparece o botão Generate Service. Um clique nesse botão, como você provavelmente já deve ter percebido, gera o código no projeto PizzaService.Host.

Para gerar o cliente, primeiro temos que executar o host, portanto, compilamos o projeto PizzaService.Host e o executamos em um navegador da Web. Em seguida, clicamos no PizzaProxy em nosso modelo de host e clicamos em Generate Code. Isso abre um assistente, mostrado na Figura 9, que permite que configuremos o cliente. Enquanto o endereço do serviço é preenchido automaticamente pelo Service Factory, prosseguimos e definimos as configurações de segurança do host. Por exemplo, se um certificado X.509 for necessário para se conectar a esse serviço, podemos adicioná-lo ao cliente usando esse assistente.

Figura 9 Assistente usado para gerar o cliente
Figura 9 Assistente usado para gerar o cliente(Clique na imagem para diminuir a exibição)
Figura 9 Assistente usado para gerar o cliente
Figura 9 Assistente usado para gerar o cliente (Clique na imagem para aumentar a exibição)

Nesse momento, o serviço está pronto para ser usado e pode ser publicado no IIS. A lógica comercial e as entidades do serviço podem ser adicionadas aos projetos PizzaService.BusinessEntities e PizzaService.BusinessLogic. A única outra coisa que precisamos fazer é criar uma nova classe parcial, chamada PizzaServicePeedy, no projeto PizzaService.ServiceImplementation. Ela é usada para adicionar lógica comercial à implementação de serviço gerada. Infelizmente, se editarmos o código gerado diretamente, nossa lógica será substituída na próxima vez em que gerarmos o código. Porém, com uma classe parcial, esse não é o caso, já que o gerador não toca em um arquivo que nós mesmos criamos.

Para exemplificar, definimos uma classe no projeto PizzaService.BusinessLogic chamada PriceCalculator com um método chamado CalculatePrice. (É importante saber que essa não é uma prática recomendada – seria muito melhor se a lógica comercial não "soubesse" nada sobre o serviço. É por isso que existe uma camada de conversão. Para dar suporte a esse processo, o Service Factory possui um conversor incorporado que ajuda você a converter seu contrato de dados para entidades comerciais. Embora você talvez não queira fazer isso em produção, estamos usando essa técnica como uma forma simples de ilustrar um ponto fundamental.) Esse método usa um contrato de dados PizzaOrder como um parâmetro e retorna o preço do pedido. A única coisa que precisamos fazer agora é encapsular esse preço em um MessageContract e, em seguida, retornar o MessageContract. Portanto, para fazer isso criamos uma classe parcial no projeto PizzaService.ServiceImplementation chamado PizzaServicePeedy que substitui o método OrderPizza gerado pelo Service Factory. Essa classe parcial é mostrada na Figura 10.

Figure 10 Classe parcial PizzaServicePeedy (Close Figure 10)
namespace PizzaService.ServiceImplementation
{
  public partial class PizzaServicePeedy
  {
    public override PizzaService.MessageContracts.OrderPizzaResp
      OrderPizza(PizzaService.MessageContracts.OrderPizzaReq request)
    {
      PizzaService.MessageContracts.OrderPizzaResp result = new
        PizzaService.MessageContracts.OrderPizzaResp();
      result.price = PizzaService.BusinessLogic.PriceCalculator
        .CalculatePrice(request.order);
      return result;
    }
  }
}

Para testar nosso serviço, iniciamos o projeto PizzaService.Host. No projeto PizzaService.Client, podemos definir o método seguinte para chamar o serviço:

static void Main()
{
  PizzaOrder order = new PizzaOrder();
  Pizza pizza = new Pizza();
  pizza.PizzaSize= PizzaSize.Large;
  pizza.Topping = Topping.Hawaii;
  order.Add(pizza);
  PizzaContractClient proxy = new PizzaContractClient();
  Console.WriteLine(proxy.OrderPizza(order));
  proxy.Close();
}

Quando o programa é executado, o número 7 é impresso na janela de saída do Visual Studio, já que esse é o preço de cada tipo de pizza grande disponível na Peedy's.

Personalizando o Service Factory

O Service Factory automatiza muitas coisas para você quando constrói serviços. Mas e se você quiser fazer coisas de forma um pouco diferente ou precisar integrar esses serviços a uma estrutura de aplicativos existente? Bem, o código-fonte completo do Service Factory está disponível no site CodePlex (www.codeplex.com/servicefactory), o que significa que você pode personalizar a sua própria fábrica.

Quando observamos a origem do Service Factory, descobrimos que ela consiste nas seguintes partes:

Linguagens específicas do domínio Incluem exclusivamente o contrato de dados, o designer de host e o contrato de serviço. Esses projetos especificam como as linguagens de modelagem são construídas e visualizadas.

Extensores Existem seis projetos de extensor. Esses projetos especificam as propriedades extras que são necessárias para que as DSLs forneçam suporte a tecnologias específicas (ASMX e WCF).

Pacote de diretrizes do Service Factory Ele contém o modelo de soluções e os menus de contexto personalizados.

Bibliotecas Elas contêm a estrutura de geração de código, a estrutura de validação, as estratégias de geração e código e as regras de validação.

Projeto de modelo Esse projeto personalizado contém os modelos.

Para esta discussão, dividiremos as personalizações em duas categorias: aquelas que requerem que você reconstrua a origem e aquelas que não requerem que você reconstrua a origem (extensões). Isso é destacado na Figura 11.

Figure 11 As duas categorias de personalização (Close Figure 11)

É preciso reconstruir a fábrica
Alterações | Descrição
Modificando recibos GAX (Guidance Animation eXtensions) existentes | Você deve fazer alterações no pacote de diretrizes do Service Factory e isso, por sua vez, significa que é preciso reconstruir a sua própria fábrica.
Adicionando novas funções do projeto | Você deve fazer alterações no pacote de diretrizes do Service Factory e isso, portanto, significa que é preciso reconstruir a sua própria fábrica.
Modificando os modelos de soluções | Você deve fazer alterações no pacote de diretrizes do Service Factory e isso, por sua vez, significa que é preciso reconstruir a sua própria fábrica.
Adicionando um novo modelo | Você deve recompilar a fábrica, já que precisa fazer alterações no projeto do modelo para dar suporte ao seu novo modelo.
Alterando as linguagens de modelagem | Isso requer alterações aos projetos DSL existentes e, portanto, é preciso reconstruir sua fábrica.
Não é preciso reconstruir a fábrica.
Alterações | Descrição
Adicionando novos modelos de solução | Você pode simplesmente definir seus modelos de solução em seu próprio pacote de diretrizes GAT separado.
Adicionando novos recibos GAX | Você pode definir novos recibos GAX, tais como assistentes e menos de contexto, em seu próprio pacote de diretrizes GAT .
Adicionando novas estratégias de geração | Você pode adicionar estratégias extras em seu próprio assembly usando a interface ICodeGenerationStrategy a partir de bibliotecas. (Os extensores de tecnologia são capazes de usar o assembly.)
Modificando os modelos de texto | Você pode alterar os modelos de texto T4 usados para gerar código, se quiser alterar a forma em que o código é gerado. Eles se encontram na pasta de instalação do service factory na subpasta "Guidance Package\TextTemplates".
Adicionando novas regras de validação de modelo | Você pode adicionar regras de validação extras ao ruleset.config na pasta Guidance Package da pasta de instalação do service factory.
Adicionando novas extensões ou propriedades de tecnologia | Por padrão, o Service Factory vêm com extensores de tecnologia para ASMX e WCF. Você pode adicionar propriedades extras a esses extensores ou adicionar seu próprio extensor em seu assembly derivado de ExtensionProviderBase. Extensores novos ou alterados podem ser soltos na pasta de instalação do service factory na subpasta "Guidance Package\Lib".

Em geral, você deve tentar evitar as personalizações que requerem que você reconstrua a fábrica e crie um novo instalador. Além de serem mais complexas e potencialmente sujeitas a erros, esses tipos de personalizações também aumentam a probabilidade de encontrar problemas durante a migração para a próxima versão da fábrica ou integração com futuros produtos da Microsoft. Dito isso, a origem do Service Factory inclui um projeto de configuração para ajudá-lo a reconstruir o processo. Esse projeto inclui modelos de geração de código que geram WIX (Windows Installer XML), facilitando e agilizando a tarefa.

Na próxima seção, mostraremos como adicionar uma propriedade ao extensor WCF existente e alterar os modelos para gerar código extra para a nova propriedade. O exame de outros pontos de extensão está além do propósito dessa coluna, portanto, consulte o laboratório prático das extensões do Service Factory, disponível em codeplex.com/servicefactory.

Observe que estamos modificando a origem da fábrica, mas estamos apenas alterando um único assembly, que é carregado dinamicamente. Portanto, não estamos reconstruindo a fábrica.

Proteção de exceção do Enterprise Library

Nos serviços, é comum evitar o envio de exceções desconhecidas para os clientes. O Exception Handling Application Block no Enterprise Library (EntLib) 3.1 pode ajudar aplicando as diretivas de proteção de exceção configuradas aos serviços WCF. Ele requer o atributo ExceptionShielding em um serviço com o nome da diretiva de proteção de exceção do bloco de manipulação de exceções como um parâmetro.

No Service Factory, a classe de serviço inteira é gerada para que possamos definir o atributo manualmente no código gerado, mas ela será substituída sempre que tenhamos que regerar o código. Uma solução para isso é estender o modelo de serviço com uma definição do atributo de proteção de exceção EntLib. Observe que a proteção de exceção EntLib só funciona com serviços WCF. Além disso, adicionar novas propriedades aos extensores não requer que reconstruamos a fábrica inteira.

Construindo a extensão

Vamos modificar a origem da fábrica, mas estamos fazendo alterações em um único assembly: o extensor do contrato de serviço WCF. O assembly do extensor é carregado dinamicamente, portanto, só precisamos construir o extensor alterado. Para construir essa extensão, presumimos que você tenha o Service Factory instalado por meio de instaladores binários. Você também precisa instalar a origem, já que vamos fazer alterações em uma parte dela. Você pode seguir parcialmente as "Instruções de instalação do instalador de código-fonte" nas instruções de instalação do Service Factory. Você precisa ignorar as etapas em que reconstrói a solução inteira ou registra o pacote de diretrizes, já que isso irá alterar a versão binária original instalada.

Vamos adicionar nossa nova propriedade ao extensor do contrato de serviço WCF, encontrado no projeto ServiceFactory.Extenders.ServiceContract.Wcf da origem do Service Factory. Basta abrir esse projeto único em vez da solução inteira, já que só precisamos fazer alterações nesse projeto. A definição da proteção de exceção é uma propriedade de um serviço WCF, portanto, podemos adicionar uma nova propriedade à classe WCFService (como mostrado na Figura 12).

Figure 12 Código de propriedade extra (Close Figure 12)
namespace
  Microsoft.Practices.ServiceFactory.Extenders.ServiceContract.Wcf
{
  [Serializable]
  [CLSCompliant(false)]
  [ObjectExtender(typeof(Service))]
  public class WCFService : ObjectExtender<Service>
  {
    ...

    private string entLibWCFExceptionPolicy;

    [Category("Enterprise Library Integration"),
      Description("EntLib WCF Exception Shielding Policy"),
      DisplayName("EntLib Exception Shielding Policy"),
      ReadOnly(false),
      Browsable(true)]
    [XmlElement("EntLibWCFExceptionPolicy")]
    public string EntLibWCFExceptionPolicy
    {
      get { return entLibWCFExceptionPolicy; }
      set { entLibWCFExceptionPolicy = value; }
    }
  }
}

Também precisamos de referências extras para as DLLs EntLib quando estamos usando o atributo ExceptionShielding. Podemos adicionar referências extras à classe ServiceLink e, dessa forma, os projetos gerados obterão automaticamente essas referências:

[AssemblyReference("Microsoft.Practices.EnterpriseLibrary" +
  ".ExceptionHandling, Version=3.1.0.0, Culture=neutral," +
  "PublicKeyToken=b03f5f7f11d50a3a")]

[AssemblyReference("Microsoft.Practices.EnterpriseLibrary" +
  ".ExceptionHandling.WCF, Version=3.1.0.0, Culture=neutral," +
  "PublicKeyToken=b03f5f7f11d50a3a")]

Agora temos que alterar os modelos de geração de código para gerar o código extra da nova propriedade. Os modelos de geração de código WCF estão na subpasta Guidance Package\TextTemplates\WCF\CS da pasta de instalação do Service Factory.

Vamos adicionar o código extra ao arquivo ServiceImplementation.tt. Salve uma cópia do arquivo original primeiro, caso queira alternar de volta para o modelo original. Na parte inferior do arquivo ServiceImplementation.tt, adicionamos o seguinte método, que retorna a diretiva de exceção EntLib somente quando um valor é preenchido:

private string
  GetEntLibExceptionShieldingPolicy(
    Service service)
{
  WCFService wfcService =
    GetObjectExtender<WCFService>(service);

  if(string.IsNullOrEmpty(
    wfcService.EntLibWCFExceptionPolicy))
  {
    return string.Empty;
  }
  else
  {
    return "[ExceptionShielding(\"" +
      wfcService.EntLibWCFExceptionPolicy +
      "\")]";
  }
}

Para completar a nossa extensão, também precisamos inserir uma instrução de uso do namespace EntLib e uma chamada para o método criado anteriormente, logo acima da definição de classe base, como mostrado aqui:

using System;
using WCF = global::System.ServiceModel;
using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.WCF;

namespace <#=CurrentExtender.ArtifactLink.Namespace#>
{
  /// <summary>
  /// Service Class - <#= CurrentElement.Name #>
  /// </summary>
  [WCF::ServiceBehavior(Name = "<#= CurrentElement.Name #>", ...
  <#= GetEntLibExceptionShieldingPolicy(CurrentElement) #>
  public abstract class <#= CurrentElement.Name #>Base...
...
}

Usando a extensão

Para usar a extensão, só precisamos reconstruir o projeto ServiceFactory.Extenders.ServiceContract.Wcf alterado e verificar se a fábrica de serviço instalada carrega nosso assembly do extensor WCF. Mas temos que avisá-lo: não reconstrua todo o código-fonte do Service Factory nesse momento; isso irá alterar a versão binária instalada. Apenas reconstrua o projeto do extensor.

Novamente, recomenda-se salvar uma cópia do extensor original. (O extensor WCF original Microsoft.Practices.ServiceFactory.Extenders.ServiceContract.Wcf.dll pode ser encontrado na subpasta Guidance Package\Lib da pasta de instalação do Service Factory). Agora encerramos todas as instâncias do Visual Studio e substituímos a DLL do extensor por nossa própria versão. Em seguida, reiniciamos o Visual Studio e criamos um novo serviço que usa a extensão WCF, nossa nova propriedade aparece quando o serviço é selecionado (consulte a Figura 13).

Figura 13 Nova propriedade EntLib no serviço
Figura 13 Nova propriedade EntLib no serviço(Clique na imagem para diminuir a exibição)
Figura 13 Nova propriedade EntLib no serviço
Figura 13 Nova propriedade EntLib no serviço (Clique na imagem para aumentar a exibição)

Quando preenchemos uma diretiva de exceção e geramos o código, os assemblies EntLib corretos são adicionados ao projeto gerado. Além disso, a instrução de uso e o atributo EntLib são adicionados à classe de implementação do serviço. Obviamente, agora nosso atributo adicionado não é perdido durante a nova geração, já que alteramos o modelo de geração do código em vez do código gerado. O novo código do serviço é similar ao seguinte:

using System;
using WCF = global::System.ServiceModel;
using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.WCF;

namespace WCFService1.ServiceImplementation
{
  /// <summary>
  /// Service Class - Service1
  /// </summary>
  [WCF::ServiceBehavior(Name = "Service1", ...
  [ExceptionShielding("MyExceptionPolicy")]
  public abstract class Service1Base :
    WCFService1.ServiceContracts.IServiceContract1
  {
    ...
  }
}

Para que a proteção de exceção EntLib realmente funcione, também temos que configurar um Exception Handling Application Block com uma diretiva de proteção de exceção. Você pode consultar a documentação do EntLib para obter mais detalhes sobre a criação de diretivas de manipulação de exceção.

Para essa extensão, adicionamos uma propriedade ao extensor WCF existente, informamos ao extensor que precisávamos de referências de assembly extras, alteramos os modelos para gerar o código correto da nova propriedade e fizemos com que o Service Factory usasse nosso extensor WCF alterado. O ponto seguinte de extensão das propriedades de diretiva tornará esses tipos de extensão mais fáceis, permitindo que você especifique novas propriedades em assemblies separados. Dessa forma, não será necessário fazer alterações nos extensores existentes.

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

Gerardo de Geest é um Desenvolvedor de soluções na Avanade, especializado em fábricas de software e linguagens específicas de domínio. Ele tem feito pesquisas sobre a evolução das linguagens específicas de domínio, especialmente no Web Service Software Factory Modeling Edition. Gerardo pode ser contatado pelo email gerardod@avanade.com.

Gerben van Loon é um Desenvolvedor de soluções na Avanade, especializado em fábricas de software e linguagens específicas de domínio. Ele começou a usar as ferramentas de DSL da Microsoft com os primeiros CTPs. Gerben pode ser contatado pelo email gerbenva@avanade.com.


Fundamentos: Servi&#231;os de fluxo de trabalho

Fundamentos
Serviços de fluxo de trabalho
Matt Milner

Download do código disponível em: Foundations2008_Launch.exe (174KB)
Conteúdo
O que é necessário para a integração
Comunicações de modelagem com as atividades Enviar e Receber
Hospedando fluxos de trabalho como serviços
Hospedagem usando o IIS ou os Serviços de Ativação do Windows
Usando contexto para gerenciar serviços de execução longa

Q
uando uma nova versão do Visual Studio® é lançada, a primeira coisa que os desenvolvedores geralmente querem saber são quais novos recursos eles encontrarão na sua ferramenta de programação favorita. Embora o Windows® Workflow Foundation (WF) e o Windows Communication Foundation (WCF) tenham sido lançados em novembro de 2006 como parte do Microsoft® .NET Framework versão 3.0, foi somente com o lançamento do Visual Studio 2008 e do .NET Framework 3.5 que as pessoas passaram a conhecer os incríveis recursos que essas tecnologias oferecem.

Infelizmente, com o .NET Framework 3.0, não havia um modelo muito bom para combinar o modelo de programação declarativa do Windows WF com o modelo de comunicações simplificado e rico exibido no WCF. O que chama a atenção no Windows WF no .NET Framework 3.5 é que ele oferece uma real integração com o WCF. Essa integração significa que os fluxos de trabalho agora podem consumir e coordenar mais facilmente os serviços e podem ser expostos através de pontos de extremidade de serviço.

O que é necessário para a integração

Quando se considera a integração do Windows WF e do WCF, existem várias áreas-chave a serem focalizadas. Os componentes principais de qualquer fluxo de trabalho são as atividades que permitem a modelagem da lógica do aplicativo. No .NET Framework 3.5, duas novas atividades, Enviar e Receber, fornecem um modelo genérico para consumir serviços e implementar pontos de extremidade de serviço em um fluxo de trabalho. Falarei mais sobre como usar essas atividades em breve.

Além de poder modelar a comunicação, a integração requer um mecanismo para hospedar os tempos de execução do WCF e do Windows WF. Como cada tecnologia possui seu próprio tempo de execução, configuração e modelo de extensibilidade, precisamos hospedar ambas as tecnologias em um único processo de alguma forma. As ferramentas fornecidas na nova estrutura são usadas em pontos de extensibilidade no WCF que permitem que o tempo de execução do fluxo de trabalho seja hospedado pelo tempo de execução do WCF.

O item final necessário para a integração é realmente o x da questão: como realizar marshaling de mensagens do WCF em instâncias do fluxo de trabalho e mapear essas operações para atividades no fluxo de trabalho. Novamente, esse recurso conta com o modelo de extensibilidade do WCF e é responsável por questões de segurança, pelo gerenciamento da instância do fluxo de trabalho e pela realização de marshaling dos corpos de mensagem como dados para atividades.

Um novo conjunto, System.WorkflowServices.dll, hospeda as novas classes que fornecem a integração entre o Windows WF e o WCF. Esse novo conjunto é específico para .NET 3.5, enquanto os conjuntos 3.0 existentes permanecem inalterados, exceto para correções de erro. Usando os modelos de extensibilidade existentes do WCF e do Windows WF, é possível fornecer integração sem ter que modificar componentes existentes. Abordarei as classes principais usadas durante a integração dessas tecnologias e explicarei como elas se encaixam.

Além do novo conjunto, o Visual Studio 2008 vem com alguns novos modelos de projeto e de item especificamente para construir fluxos de trabalho a serem publicados como serviços. Na nova caixa de diálogo de projeto, você encontrará projetos de Sequential Workflow Service Library e de State Machine Workflow Service Library na categoria WCF. Você também encontrará novos modelos de item para itens do Windows WF Sequential Service e do Windows WF State Machine e seus respectivos XAML. Cada um desses novos modelos adiciona o fluxo de trabalho, a interface que define o contrato de serviço e um arquivo app.config que configura os pontos de extremidade do serviço.

Comunicações de modelagem com as atividades Enviar e Receber

A nova atividade receber é usada para modelar pontos no fluxo de trabalho onde o processo receberá dados como parte de um contrato de serviço do WCF. Isso significa que cada atividade receber no fluxo de trabalho precisará ser configurada com as informações apropriadas relacionadas ao contrato de serviço e vinculada a uma operação nesse contrato. Para realizar essa vinculação entre a atividade e a operação, a atividade possui uma propriedade ServiceOperationInfo.

O contrato pode ser declarado de duas formas: usando os atributos do contrato de serviço do WCF ou diretamente no fluxo de trabalho. O último é geralmente chamado de design de "primeiro fluxo de trabalho", em vez de design de "primeiro contrato". Quando o contrato é definido diretamente no fluxo de trabalho, as informações do contrato são incorporadas à definição do fluxo de trabalho, em vez de serem expostas como uma interface .NET, como acontece ao definir um contrato com o WCF.

Quando você editar a propriedade ServiceOperationInfo, a caixa de diálogo exibida lhe permitirá escolher a operação, e é aqui também que você deve decidir se vai usar um contrato de serviço existente ou definir um novo. A caixa de diálogo é mostrada na Figura 1 e possui dois botões no canto superior direito que permitem importar um contrato de serviço ou adicionar um novo contrato. Se você optar por importar um contrato, todas as operações com um atributo OperationContract serão importadas para uma caixa de diálogo na qual é possível escolher qual operação a atividade implementa. Na parte inferior da caixa de diálogo, existem três guias mostrando os parâmetros, as propriedades e as permissões da operação. Quando você importar o contrato, os parâmetros e as propriedades serão criados diretamente a partir do contrato de serviço e não poderão ser alterados.

Figura 1 Caixa de diálogo de configuração ServiceOperationInfo
Figura 1 Caixa de diálogo de configuração ServiceOperationInfo(Clique na imagem para diminuir a exibição)
Figura 1 Caixa de diálogo de configuração ServiceOperationInfo
Figura 1 Caixa de diálogo de configuração ServiceOperationInfo (Clique na imagem para aumentar a exibição)

Durante a criação de um contrato de serviço, a caixa de diálogo permite que você configure as propriedades de cada operação para que o contrao de serviço seja formulado corretamente. Especifique os parâmetros e os tipos de retorno da operação e determine o padrão de troca de mensagens, definindo a propriedade IsOneWay. Além disso, você pode especificar o nível de proteção a ser aplicado às mensagens, de forma que elas sejam assinadas e/ou criptografadas.

Depois que o contrato for importado ou definido, e a operação for selecionada para a atividade receber, ainda há alguns itens a serem configurados na atividade para recepção básica. O primeiro é a propriedade CanCreateInstance, que informa ao tempo de execução se o fluxo de trabalho pode ser iniciado com essa operação. Cada fluxo de trabalho que será exposto como um serviço deve ter pelo menos uma atividade receber com a propriedade CanCreateInstance definida como verdadeira; caso contrário, não há como iniciar o fluxo de trabalho com uma invocação de serviço.

A atividade receber é implementada como uma atividade composta, o que significa que ela possui atividades filhos. Essencialmente, a atividade receber funciona como uma atividade seqüencial no sentido em que executa todas as suas atividades filho na ordem até a conclusão. A Figura 2 mostra uma atividade receber implementando uma operação usando uma atividade filho. Essas atividades filho compõem a implementação da operação do serviço. No fim da execução, a atividade receber retornará o valor vinculado à sua propriedade de valor de retorno, ou emitirá a falha vinculada à sua propriedade de falha.

Figura 2 Atividade Receber Implementando uma operação
Figura 2 Atividade Receber Implementando uma operação

Existem dois pontos-chave a serem lembrados sobre o trabalho feito na atividade receber: você deve definir o valor de retorno ou a propriedade de falha durante a execução, e qualquer trabalho feito deve ser concluído antes que a chamada ao serviço possa retornar. Definir a saída ou a falha é importante, mas a perda dessa etapa se apresenta muito cedo no ciclo de teste. O que às vezes é menos óbvio é a duração do seu trabalho, o que pode causar impacto no desempenho do serviço. Como você está no designer do fluxo de trabalho, talvez seja fácil esquecer que está codificando uma operação de serviço e que um aplicativo cliente está esperando por uma resposta. Se a operação for unidirecional, obviamente, essa é uma preocupação menor, e também não há necessidade de definir a saída ou as propriedades de falha, já que o cliente não irá recebê-las.

A atividade enviar consome serviços de dentro de um fluxo de trabalho, permitindo que você use fluxos de trabalho para coordenar serviços ou tirar vantagem da lógica e dos dados que residem em um serviço. No WCF, os clientes criam canais configurados com informações do ponto de extremidade para invocar uma operação de serviço. A atividade enviar usa a mesma caixa de diálogo ServiceOperationInfo para permitir que você importe um contrato de serviço e escolha a operação que deseja invocar. O designer da atividade enviar cria dinamicamente as propriedades dos parâmetros e do valor de retorno da operação que você escolheu, de forma que você possa vincular ou definir esses valores em seu fluxo de trabalho antes de chamar o serviço.

Para configurar o ponto de extremidade do cliente, use duas propriedades: ChannelToken e CustomAddress. ChannelToken possui uma propriedade EndpointName que deve ser configurada com o nome de um ponto de extremidade de cliente do WCF encontrado no arquivo de configuração do processo que está hospedando o fluxo de trabalho. Esse ponto de extremidade conterá o endereço e as informações de vinculação que o tempo de execução precisa para criar um canal e invocar a operação no tempo de execução. Além disso, a propriedade CustomAddress permite que você altere o endereço, provavelmente a parte mais dinâmica do ponto de extremidade no tempo de execução ou obtenha esse valor a partir dos dados presentes no contexto do fluxo de trabalho.

Outros recursos-chave da atividade enviar incluem eventos de pré e pós-processamento e cache de canais do WCF em um nível do escopo. Os eventos fornecem ganchos para que o código seja executado e preparam a atividade enviar e seus parâmetros para processamento ou processam o resultado da invocação da operação antes que o controle prossiga para a próxima atividade no fluxo de trabalho. O armazenamento em cache proporciona uma melhora no desempenho se você for fazer várias chamadas para o mesmo serviço em um fluxo de trabalho. A criação do canal do cliente e a negociação de segurança e outros protocolos podem ser caras, e a habilidade de armazenar em cache o canal pode reduzir o tempo gasto nessas etapas. Configure o escopo do armazenamento em cache usando a propriedade ChannelToken na atividade enviar e escolhendo uma atividade composta pai da propriedade OwnerActivityName no token. Para reutilizar o mesmo canal em outra atividade enviar, defina a propriedade ChannelToken como a que é usada na outra atividade.

Hospedando fluxos de trabalho como serviços

Para fluxos de trabalho que recebem dados através de serviços do WCF, o fluxo de trabalho deve ser hospedado como qualquer outro serviço do WCF. A hospedagem do serviço cria o tempo de execução do WCF, que é responsável por receber mensagens através da rede, processar essas mensagens e entregá-las a um serviço. Se o serviço for implementado como um fluxo de trabalho, a carga da mensagem deve ser enviada para a instância do fluxo de trabalho onde a atividade receber está esperando pelos dados. Para obter mais informações sobre como os dados são comunicados para o fluxo de trabalho, consulte a minha coluna anterior sobre comunicações do fluxo de trabalho (msdn.microsoft.com/msdnmag/issues/07/09/Foundations).

Para hospedar um serviço de fluxo de trabalho, use a nova classe, WorkflowServiceHost, que deriva da classe ServiceHostBase encontrada no System.ServiceModel. Observe que a classe ServiceHostBase fornece a lógica básica para hospedar um serviço do WCF e é responsável por construir o tempo de execução do WCF incluindo canais, comportamentos, etc. Quando um desenvolvedor deseja personalizar o tempo de execução do WCF, uma opção é criar de um host de serviço personalizado. O modelo de host usado com os serviços de fluxo de trabalho depende muito da extensão do modelo de tempo de execução do WCF. Para obter mais informações sobre a extensão do WCF, consulte a coluna Estação de Serviço na edição de dezembro de 2007 da MSDN® Magazine (msdn.microsoft.com/msdnmag/issues/07/12/ServiceStation).

O código seguinte mostra a hospedagem de um tipo de fluxo de trabalho simples usando a classe WorkflowServiceHost em um aplicativo .NET:

WorkflowServiceHost host = new
  WorkflowServiceHost(typeof(Workflows.SimpleWorkflow));
host.Open();

Nesse exemplo, passei o tipo de informação do fluxo de trabalho para o construtor do WorkflowServiceHost. É exatamente assim que você constrói um ServiceHost de um serviço padrão do WCF.

Ao definir os fluxos de trabalho, você pode definir a estrutura em uma nova classe ou o fluxo de trabalho usando a XAML (Extensible Application Markup Language). O WorkflowServiceHost fornece vários construtores sobrecarregados (consulte a Figura 3). Você não somente pode fornecer o caminho para o seu arquivo XAML, mas também pode oferecer um fluxo. Isso lhe dá mais flexibilidade para armazenar definições de fluxo de trabalho. Além disso, essas sobrecargas usam caminhos ou fluxos dos arquivos XML que contêm as regras do fluxo de trabalho. Nesses construtores sobrecarregados, você poderá ver o começo da sobreposição das duas tecnologias quando o método CreateWorkflow do WorkflowRuntime e o construtor da classe ServiceHost forem combinados no WorkflowServiceHost.

Figure 3 Construtores do WorkflowServiceHost (Close Figure 3)
public WorkflowServiceHost(Stream workflowDefinition,
  params Uri[] baseAddress);
public WorkflowServiceHost(string workflowDefinitionPath,
  params Uri[] baseAddress);
public WorkflowServiceHost(Type workflowType,
  params Uri[] baseAddress);
public WorkflowServiceHost(Stream workflowDefinition,
  Stream ruleDefinition, params
  Uri[] baseAddress);
public WorkflowServiceHost(string workflowDefinitionPath,
  string ruleDefinitionPath,
  params Uri[] baseAddress);
public WorkflowServiceHost(Stream workflowDefinition,
  Stream ruleDefinition,
  ITypeProvider typeProvider,
  params Uri[] baseAddress);
public WorkflowServiceHost(string workflowDefinitionPath,
  string ruleDefinitionPath,
  ITypeProvider typeProvider,
  params Uri[] baseAddress);

Quando o WorkflowServiceHost tiver sido criado, você poderá configurar o tempo de execução do WCF como faria com qualquer outro serviço que estivesse usando o código ou a configuração. Isso permite que você adicione pontos de extremidade ou comportamentos ao tempo de execução ou modifique a descrição do serviço. Durante a edição das definições do fluxo de trabalho usando somente XAML, o nome usado para identificar o serviço no arquivo de configuração depende da marcação que você usa. Por exemplo, se você usar o atributo x:Name, os valores se tornarão o nome do seu serviço. Entretanto, se você omitir o atributo de nome, será usado o nome da primeira atividade, que, geralmente, não é um nome muito exclusivo nos fluxos de trabalho.

Como o modelo de hospedagem do WCF é usado para o serviço, você acessa o WorkflowRuntime através do host do WCF. Novo no .NET Framework 3.5 é o WorkflowRuntimeBehavior, que é adicionado automaticamente pelo WorkflowServiceHost, se você não adicioná-lo explicitamente à descrição do WCF. Por meio desse comportamento, você pode configurar o objeto WorkflowRuntime no código ou no arquivo de configuração. No arquivo de configuração, você define os serviços que deseja adicionar ao tempo de execução usando uma configuração de comportamento de serviço que inclui esse novo elemento. Em seguida, você configura os serviços normalmente, como faria ao configurar o tempo de execução do Windows WF. A Figura 4 mostra um exemplo de um arquivo de configuração onde o tempo de execução está sendo configurado.

Figure 4 Configurando o tempo de execução do fluxo de trabalho através de comportamentos (Close Figure 4)
<behaviors>
  <serviceBehaviors>
    <behavior name="wfService">
      <serviceMetadata httpGetEnabled ="true"/>
      <workflowRuntime cachedInstanceExpiration="00:10:00"
        validateOnCreate="false">
        <services>
          <add
type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService ..." ConnectionString="server=.;database=wfpersist;integrated security=SSPI" UnloadOnIdle="false"/>
        </services>
      </workflowRuntime>
    </behavior>
  </serviceBehaviors>
</behaviors>

Um item de configuração não disponível na seção de configuração do fluxo de trabalho padrão é o atributo cachedInstanceExpiration, que permite que você defina quanto tempo o fluxo de trabalho mantém uma instância do fluxo de trabalho na memória depois que ele fica inativo. Isso fornece um pouco mais de controle do que a funcionalidade base dos fluxos de trabalho do serviço de persistência SQL quando eles ficam inativos.

Quando você desejar acessar o tempo de execução do fluxo de trabalho no código do host, primeiro deverá acessar o comportamento e obter o tempo de execução das propriedades no comportamento. Para acessar o comportamento, acesse a descrição do serviço no WorkflowServiceHost e localize o WorkflowRuntimeBehavior por tipo. O comportamento possui uma propriedade de tipo para acessar o tempo de execução, com a qual você pode interagir para adicionar serviços, registrar manipuladores de eventos ou escrever qualquer código específico do host, código relacionado ao tempo de execução do fluxo de trabalho. A Figura 5 fornece um exemplo do acesso do tempo de execução para adicionar serviços e registrar manipuladores para hospedar eventos relacionados.

Figure 5 Acessando o tempo de execução do fluxo de trabalho no código do host (Close Figure 5)
WorkflowServiceHost host = new
  WorkflowServiceHost(typeof(Workflows.SimpleWorkflow));

WorkflowRuntimeBehavior wfbehavior =
  host.Description.Behaviors.Find<WorkflowRuntimeBehavior>();

WorkflowRuntime wfruntime = wfbehavior.WorkflowRuntime;

wfruntime.WorkflowTerminated +=
  delegate(object sender, WorkflowTerminatedEventArgs wte)
  {
    Console.WriteLine(wte.Exception.Message);
  };

host.Open();

Depois que você tiver modificado os objetos no WCF e os tempos de execução do Windows WF no código, simplesmente chame Open no WorkflowServiceHost, e ele irá compilar e configurar ambos os tempos de execução e registrar todos os pontos de extremidade do serviço para escutar os dados. A utilização direta do WorkflowServiceHost é conhecida como hospedagem interna, ao contrário da utilização de um processo de host fornecido, tal como o IIS ou o WAS (Serviço de Ativação de Processos do Windows) — cada um deles será discutido na próxima seção.

Como o WorkflowRuntime é gerenciado por um comportamento do WCF, haverá um único WorkflowRuntime para cada WorkflowServiceHost e, portanto, para cada serviço no host. (Isso é contrário à prática comum no Windows WF de ter um único WorkflowRuntime que hospeda muitos tipos diferentes de fluxos de trabalho). Você também terá várias instâncias dos serviços de tempo de execução que escolheu, e cada uma será executada independentemente. Por exemplo, se você possui três serviços hospedados no mesmo aplicativo, e cada um é configurado para usar o serviço de persistência do SQL, então você terá três instâncias em execução do serviço de persistência, cada uma pesquisando periodicamente no banco de dados os temporizadores expirados. Se você configurar esses serviços com a mesma seqüência de conexão, eles agirão como três processos de host diferentes; sendo assim, a duração da propriedade e o bloqueio podem se tornar um problema. Finalmente, observe que esse comportamento existe mesmo se você configurar todos os serviços com a mesma configuração de comportamento. Como se trata apenas de configuração, cada WorkflowServiceHost criará uma nova instância do comportamento, que, por sua vez, criará um novo WorkflowRuntime.

Hospedagem usando o IIS ou os Serviços de Ativação do Windows

Hospedar serviços no IIS ou WAS é semelhante em muitas formas às tecnologias de serviço Web anteriores, tais como os serviços Web ASMX no .NET Framework 2.0, mas com opções de extensibilidade. Um ponto de extremidade de serviço do WCF hospedado no IIS usa um arquivo com a extensão .svc para agir como o recurso endereçável que será invocado pelos clientes. Esse arquivo simplesmente indica o tipo de serviço usado para implementar o serviço, e a extensão .svc é mapeada para o manipulador do WCF para que ele possa rotear corretamente as mensagens para esse serviço.

Um ponto-chave de extensibilidade nesse modelo é que você pode obter o controle do processo de criação do ServiceHost, criando uma fábrica personalizada. Em geral, a hospedagem-padrão do IIS funciona em inúmeros cenários e cria uma instância do ServiceHost para o serviço nomeado no arquivo .svc. Entretanto, por ter permissão para personalizar o host do serviço, você pode usufruir dos recursos de hospedagem no IIS sem ter que abrir mão completamente do controle do host.

Para hospedar um serviço de fluxo de trabalho, você cria o arquivo .svc e especifica o tipo de fluxo de trabalho ou o caminho para um arquivo XAML que contém a definição do fluxo de trabalho, e especifica um novo tipo, WorkflowServiceHostFactory do atributo Factory. Veja abaixo um serviço de fluxo de trabalho básico em um arquivo .svc:

<%@ ServiceHost Language="C#" Service=" SimpleWorkflow.xoml"
  Factory="System.ServiceModel.Activation.WorkflowServiceHostFactory" %>

Observe que o serviço é definido como o caminho para um arquivo XAML. O WorkflowServiceHostFactory é responsável por carregar o arquivo XAML e usá-lo, junto com um arquivo de regras, se houver, para criar um host de serviço do fluxo de trabalho e retorná-lo quando o tempo de execução do WCF solicitá-lo. O arquivo de regras deve ter o mesmo nome que o arquivo XAML, mas com uma extensão .rules. Esse é um modelo poderoso no qual você pode soltar alguns arquivos de texto — os arquivos XAML, .rules e .svc — em um diretório virtual e ter apenas que adicionar a configuração dos pontos de extremidade ao arquivo de configuração.

Usando contexto para gerenciar serviços de execução longa

A utilização de fluxos de trabalho para implementar o serviço torna-se especialmente atraente quando o serviço é de execução longa e possui várias interações com um cliente. Por exemplo, suponha que você tenha um serviço para gerenciar um processo de aprovação de relatório de despesas. Esse serviço, implementado como um fluxo de trabalho, poderia ter pelo menos dois pontos de interação, incluindo o envio inicial pelo funcionário e a aprovação ou rejeição pelo gerente. Em tais cenários, você precisa de uma forma de gerenciar o fluxo de trabalho ao longo do tempo e rotear as mensagens para a instância correta do fluxo de trabalho. Em outras palavras, você precisa de uma maneira de gerenciar o contexto da sua mensagem.

No .NET Framework 3.5, o suporte ao contexto é manifestado em um novo elemento de vinculação chamado ContextBindingElement. Esse elemento permite aos desenvolvedores criar vinculações que reconhecem o contexto e podem ajudar a gerenciar o roteamento de mensagens para a instância correta do fluxo de trabalho. A estrutura também inclui três vinculações predefinidas para a maioria dos cenários comuns: WSHttpContextBinding, BasicHttpContextBinding e NetTcpContextBinding. Cada uma dessas vinculações corresponde à sua contraparte não-contextual nos recursos e na configuração, mas adiciona o canal de contexto à pilha do tempo de execução. Esse canal de contexto gerencia os identificadores de conversação exclusivos das interações conforme eles fluem através da pilha do canal.

A Figura 6 mostra como esse gerenciamento de contexto funciona com o canal de contexto incluído. Quando um canal do cliente é usado para interagir com um serviço, as informações do contexto são enviadas de volta para o cliente na primeira troca de mensagens. Essas informações contêm minimamente a ID da instância do fluxo de trabalho que foi criado para manipular a solicitação. A próxima vez em que o canal do cliente for usado para chamar o serviço, as informações do contexto serão enviadas de volta para o serviço em que são usadas para rotear a mensagem para a instância do fluxo de trabalho que já está sendo executada.

Figura 6 Gerenciamento de contexto
Figura 6 Gerenciamento de contexto(Clique na imagem para diminuir a exibição)
Figura 6 Gerenciamento de contexto
Figura 6 Gerenciamento de contexto (Clique na imagem para aumentar a exibição)

Além de rotear as mensagens para a instância correta, algumas interações entre os serviços ficam mais complexas. Por exemplo, um fluxo de trabalho deve iniciar a comunicação com três instâncias diferentes de um serviço usando a atividade enviar e esperando para receber respostas de todas as três instâncias. Nesse caso, o fluxo de trabalho terá três instâncias da atividade receber esperando que o mesmo tipo de mensagem chegue. Como resultado, deve haver um mecanismo para rotear a mensagem de entrada, não apenas para o fluxo de trabalho correto, mas também para a atividade receber correta no fluxo de trabalho. O mecanismo de contexto suporta não somente a passagem da ID da instância, mas também inclui um identificador de conversação para esses cenários em que o fluxo de trabalho está esperando várias mensagens na mesma operação. A Figura 7 mostra um exemplo desse tipo de cenário.

Figura 7 Gerenciamento de conversação
Figura 7 Gerenciamento de conversação(Clique na imagem para diminuir a exibição)
Figura 7 Gerenciamento de conversação
Figura 7 Gerenciamento de conversação (Clique na imagem para aumentar a exibição)

O contexto é gerenciado como uma coleção de pares de nome-valores e pode ser passado via cabeçalhos SOAP ou um cookie HTTP. O canal de contexto manipula isso para você nas camadas inferiores na maioria dos casos, mas, como um autor do fluxo de trabalho, você é responsável por gerenciar o contexto no fluxo de trabalho. No exemplo anterior, em que chamei três instâncias de um serviço, cada instância desse serviço precisaria do código que define o contexto antes que ele possa enviar uma resposta para meu serviço de fluxo de trabalho. Isso significa que, de alguma forma, o fluxo de trabalho que chama deve enviar as informações do contexto sem usar os recursos de canal internos, e o serviço chamado deve usar essas informações de contexto quando mais tarde chamar o fluxo de trabalho. A atividade enviar contém convenientemente uma propriedade de contexto que pode ser definida antes que o serviço seja chamado, o que faz com que a atividade defina o contexto no canal antes de invocar o serviço.

Se toda a comunicação for iniciada a partir do fluxo de trabalho de chamada, a necessidade de passar o contexto para o fluxo de trabalho chamado é irrelevante, já que o fluxo de trabalho chamado pode simplesmente enviar respostas para as solicitações que recebe. Embora essa implementação vise principalmente os aplicativos de estilo intranet e tenha algumas limitações, ela fornece uma estrutura de extensibilidade, que abordarei em uma edição posterior.

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

Matt Milner é um consultor de software independente especializado em tecnologias da Microsoft, incluindo .NET, Web services, Windows Workflow Foundation, Windows Communication Foundation e BizTalk Server. Na Pluralsight, Matt ministra cursos sobre Workflow, BizTalk Server e Windows Communication Foundation. Entre em contato com ele através de seu blog em pluralsight.com/blogs/matt.


Windows com C++: Aprimoramentos nos servi&#231;os do Windows

Windows com C++
Aprimoramentos nos serviços do Windows
Kenny Kerr

Conteúdo
Atrasando serviços de auto-início
Aprimorando a possibilidade de previsão de desligamentos
Ações de falha e parada controlada
Reduzindo privilégios
Protegendo dados de serviço
Protegendo outros com tokens restritos
Recebendo notificações de serviço
O que vem a seguir?

O
desenvolvimento de serviços do Windows® permaneceu inalterado desde os primórdios dos serviços no Windows NT®. Entretanto, o Windows Vista® e o Windows Server® 2008 trazem algumas mudanças significativas nesses serviços. Muitos desses recursos são focados em simplificar a produção de serviços mais seguros, mas há alguns, entre os não relacionados à segurança, que têm por objetivo aprimorar a resposta e a confiabilidade do Windows como um todo.

Atrasando serviços de auto-início

Os serviços podem ser configurados para iniciar apenas quando especificamente requisitados por meio da função StartService. Esse tipo de serviço é chamado de serviço com início por demanda. Como alternativa, eles podem ser configurados para iniciar automaticamente durante o processo de inicialização do sistema operacional. Esse tipo de serviço é conhecido como de início automático, e esse modo faz sentido para muitos serviços cujas funcionalidades devem permanecer disponíveis durante todo o tempo, ou podem ser solicitadas por outros serviços. Alguns serviços de início automático, contudo, não precisam estar em execução no momento que o usuário loga no computador, pois o uso dos mesmos poderá não ser imediato.

O Windows Vista ajuda a reduzir o tempo de inicialização do Windows fornecendo um recurso que faz com que os serviços de início automático só sejam inicializados após o Windows ter concluído seu próprio processo de inicialização. Especificamente, um serviço de início automático atrasado é ativado mais ou menos 2 minutos após os serviços de início automático regular. O Service Control Manager (SCM) também cria o segmento de serviços principais com uma menor prioridade, garantindo que nenhum usuário logado seja prejudicado pelos serviços de início automático atrasado. A prioridade é redefinida com os valores normais depois que o serviço atualiza o estado indicando que já está em execução. Você pode forçar o início antecipado de um serviço de início automático atrasado usando a função StartService caso ele não tenha sido inciado no momento que o seu aplicativo precisar usá-lo.

A função a seguir mostra como você pode controlar essa opção (ela deve ser chamada pelo programa de intslação do serviço):

bool ChangeDelayedAutoStart(
    SC_HANDLE service, bool delayed)
{
    ASSERT(0 != service);

    SERVICE_DELAYED_AUTO_START_INFO info = { delayed };

    return 0 != ::ChangeServiceConfig2(
        service,
        SERVICE_CONFIG_DELAYED_AUTO_START_INFO,
        &info);
}

O identificador do serviço mostra o serviço que deve ser configurado. O identificador pode ser obtido chamando as funções OpenService ou CreateService. A função ChangeServiceConfig2 permite que você altere algumas opções de configuração mais avançadas do serviço. Neste caso, estou usando o sinalizador SERVICE_CONFIG_DELAYED_AUTO_START_INFO para informar que a função deve esperar um ponteiro SERVICE_DELAYED_AUTO_START_INFO como terceiro parâmetro. Se falhar, ChangeServiceConfig2 retornará zero, e você poderá obter mais informações sobre o erro chamando a função GetLastError.

Aprimorando a possibilidade de previsão de desligamentos

Antes do Windows Vista, nem sempre era possível garantir que os serviços fossem interrompidos normalmente quando o computador era desligado. Quando o sistema notificava ao SCM que estava desligando, ele tinha apenas aproximadamente 20 segundos para informar a todos os serviços que eles deveriam parar. Se demorasse muito, o sistema simplesmente encerrava o processo do SCM. Isso poderia resultar em uma série de problemas, incluindo, ironicamente, um aumento do tempo de inicialização já que os serviços que não foram encerrados naturalmente muitas vezes precisavam lidar com um estado incosistente quando eram inicializados novamente.

O Windows Vista possui uma inovadora notificação pré-desligamento que os serviços podem solicitar receber. Se você estiver desenvolvendo um serviço que precise desligar normalmente e não necessariamente consiga fazer isso rapidamente, você pode solicitar uma notificação de pré-desligamento do SCM. O SCM irá aguardar (possivelmente por tempo indeterminado) até que todos os serviços de pré-desligamento sejam encerrados antes de prosseguir com o processo de desligamento tradicional. Apesar de ser bom para os serviços, esse recurso não é necessariamente bom para os usuários que desejem que seus computadores sejam desligados imediatamente. Por essa razão, use esse recurso apenas em casos críticos e, preferencialmente, apenas em ambientes de servidor.

Um serviço indica seu desejo de receber uma notificação de pré-desligamento incluindo o indicador SERVICE_ACCEPT_PRESHUTDOWN no seus status de serviço. A função de manipulador do serviço será, então, notificada por meio do código de controle SERVICE_CONTROL_PRESHUTDOWN. Apesar de o Windows ficar esperando indefinitivamente pelo encerramento de um serviço de pré-desligamento, é necessário que o serviço permaneça responsivo a consultas do SCM e atualize o estado do serviço com valores de pontos de verificação incrementais. Se o SCM decidir que o seu serviço não está mais responsivo, é possível que ele desista e que o sistema prossiga com o desligamento. Lembre-se de que se o seu serviço usar o código de controle SERVICE_CONTROL_PRESHUTDOWN ele não receberá o código de controle tradicional SERVICE_CONTROL_SHUTDOWN. Na maioria dos casos, o melhor é usar apenas um procedimento de desligamento para todos os códigos de controle relacionados ao encerramento.

Uma característica significativa do comportamento do SCM é que ele se comunica com apenas um serviço de cada vez. Isso tem determinadas implicações que, se ainda não forem óbvias, ficarão a seguir: se dois aplicativos (ou segmentos) chamam funções de controle de serviço, como StartService ou ControlService, o SCM os colocará em fila e atenderá às solicitações uma por vez. Outro cenário mais problemático é durante o encerramento do Window. Essa comunicação é realizada por meio de um pipe nomeado estabelecido entre o SCM e o serviço quando o SCM chama a função StartServiceCtrlDispatcher. Quando o SCM envia um código de controle a um serviço, ele se comunica por meio do pipe nomeado com a função StartServiceCtrlDispatcher dentro do serviço. Ele encaminha a solicitação ao manipulador do serviço em particular (para seviços de processos compartilhados), e o manipulador tem a oportunidade de processar a solicitação antes de retornar. O problema é que não existe nada que evite que uma função de manipulador bloqueie durante muito tempo. A solicitação enviada pelo SCM poderá esgotar o tempo limite caso o serviço não responda em 30 segundos mais ou menos, mas enquanto isso, nenhum outro serviço poderá ser controlado.

Como desenvolvedor de serviço, você deve sempre se lembrar disso e nunca bloquear a função do manipulador do serviço. Um serviço com comportamento adequado deve confirmar a solicitação, definir um sinalizador ou disparar um segmento de trabalho para manipular a solicitação e retornar imediatamente. Isso permite que o SCM continue se comunicando com os outros serviços enquanto o seu serviço manipula a solicitação. A Figura 1 mostra como você pode implementar os códigos de controle de desligamento e encerramento.

Figure 1 Lidando com interrupções e desligamento (Close Figure 1)
DWORD WINAPI StopThread(PVOID)
{
  // perform any length shutdown operation

  m_status.dwCurrentState = SERVICE_STOPPED;

  VERIFY(::SetServiceStatus(m_handle,
                            &m_status));
  return 0;
}

...

switch (control)
{
  case SERVICE_CONTROL_STOP:
  case SERVICE_CONTROL_SHUTDOWN:
  case SERVICE_CONTROL_PRESHUTDOWN:
  {
    m_status.dwCurrentState = SERVICE_STOP_PENDING;

    VERIFY(::SetServiceStatus(m_handle,
                              &m_status));

    CHandle thread(::CreateThread(0, // default security
                                  0, // default stack size
                                  StopThread,
                                  this, // context
                                  0, // no flags
                                  0)); // ignore thread id

    break;
  }

  ...
}

O manipulador de controle, que manipula os códigos de controle SERVICE_CONTROL_STOP, SERVICE_CONTROL_SHUTDOWN e SERVICE_CONTROL_PRESHUTDOWN define o estado do serviço como SERVICE_STOP_PENDING, cria um segmento de trabalho e retorna. A função StopThread pode bloquear pelo tempo necessário e, então, definir o estado do serviço como SERVICE_STOPPED, informando ao SCM, que, subsequentemente, pode deixar a função StartServiceCtrlDispatcher retornar e concluir o processo (contanto que seja o último serviço do processo compartilhado). Lembre-se de que se o seu serviço estiver com o estado pendente, o estado precisará ser atualizado regularmente com um valor de ponto de verificação incremental para garantir ao SCM que ele não está suspenso.

Ações de falha e parada controlada

As ações de falha estão disponíveis há tempos como meio de serviços que encerram de repente para recuperar de alguma forma. Antes do Windows Vista, as ações de falha eram executadas somente se o processo de serviço era encerrado sem primeiro definir o estado de serviço como SERVICE_STOPPED. O Windows Vista permite que o serviço solicite que suas ações de falha também sejam executadas se o estado do serviço estiver definido como SERVICE_STOPPED, mas o código de saída do serviço, fornecido pelo membro SERVICE_STATUS::dwWin32ExitCode, está definido como outro ERROR_SUCCESS. Isso permite que você empregue ações de falha quando o seu serviço falhar e, ao mesmo tempo, permitir que você interrompa o serviço normalmente.

A função a seguir mostra como você pode controlar essa opção para interromper um serviço (e, novamente, ela deve ser chamada pelo programa de intslação do serviço):

bool ChangeFailureActionsOnNonCrashFailures(
  SC_HANDLE service,
  bool failureActionsOnNonCrashFailures)
{
  ASSERT(0 != service);

  SERVICE_FAILURE_ACTIONS_FLAG flag = {
   failureActionsOnNonCrashFailures };

  return 0 != ::ChangeServiceConfig2(service,
    SERVICE_CONFIG_FAILURE_ACTIONS_FLAG,
    &flag);
}

Para compatibilidade reversa, o padrão dessa opção é false, preservando assim o comportamento das versões anteriores do Windows.

Reduzindo privilégios

A maioria dos administradores prefere serviços que são executados em contas de serviço incorporadas do Windows. Isso evita as dores de cabeça inerentes no gerenciamento de senha para contas de serviço. É claro, há muitas contas de serviço incorporadas e os privilégios e permissões que oferecem podem ser além das suas necessidades de serviço. Embora o Windows Vista não tenha adicionado mais contas de serviço — o que simplesmente não seria prático — ele introduziu algumas novas opções de configuração avançadas que permitem que você bloqueie precisamente o serviço usando tokens restritos.

A primeira opção de configuração permite que você especifique exatamente quais privilégios o seu serviço requer. O SCM garantirá que o token de processo do serviço inclua somente os privilégios necessários. A Figura 2 mostra como você pode controlar essa opção. Isso seria chamado pelo programa de instalação do seu serviço.

Figure 2 Definido privilégios de serviço (Close Figure 2)
bool ChangeRequiredPrivileges(
    SC_HANDLE service,
    const CAtlArray<PCWSTR>& requiredPrivileges)
{
  ASSERT(0 != service);

  size_t bufferSize = 1;

  for (size_t index = 0; index < requiredPrivileges.GetCount(); ++index)
  {
    bufferSize += wcslen(requiredPrivileges[index]) + 1;
  }

  CAtlArray<WCHAR> buffer;

  if (!buffer.SetCount(bufferSize))
  {
    ::SetLastError(ERROR_OUTOFMEMORY);
    return false;
  }

  PWSTR position = &buffer[0];

  for (size_t index = 0; index < requiredPrivileges.GetCount(); ++index)
  {
    PCWSTR name = requiredPrivileges[index];
    const size_t nameSize = wcslen(name);

    wcscpy_s(position,
             nameSize + 1,
             name);

    position += nameSize + 1;
  }

  SERVICE_REQUIRED_PRIVILEGES_INFO info = { &buffer[0] };

  return 0 != ::ChangeServiceConfig2(
    service,
    SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO,
    &info);
}

Essa função pode parecer um pouco complicada, mas é somente por que a função ChangeServiceConfig2 requer que a lista de nomes de privilégio seja fornecida como uma seqüência de cadeias terminadas em nulo, terminadas por uma seqüência vazia. No entanto, o uso da função ChangeRequiredPrivileges é simples:

CAtlArray<PCWSTR> requiredPrivileges;
requiredPrivileges.Add(SE_CHANGE_NOTIFY_NAME);
requiredPrivileges.Add(SE_IMPERSONATE_NAME);

if (!ChangeRequiredPrivileges(service, requiredPrivileges))
{
    // call GetLastError for more info
}

É importante ter em mente que os privilégios especificados de fato precisam ser concedidos à conta que o seu serviço está configurado para usar. Se, por exemplo, você configurar o seu serviço para exigir o privilégio SE_TCB_NAME, mas o seu serviço estiver configurado para execução na conta LocalService, que não tem esse privilégio, o SCM irá falhar para iniciar o serviço, pois ele supõe que o seu serviço não funcionará corretamente sem o privilégio necessário.

O outro item a considerar são os serviços de processo compartilhados. Como todos os serviços que compartilham um processo também compartilham o mesmo contexto de segurança, o SCM incluirá a união dos privilégios necessários por todos os serviços que podem potencialmente compartilhar o processo. Isso pode impactar quais serviços você decide que serão candidatos a compartilhamento de um espaço de endereço e contexto de segurança.

Protegendo dados de serviço

Embora a redução de privilégios disponíveis seja útil na limitação do que um serviço explorado pode fazer (em um certo grau), ela não limita quais dados o serviço pode acessar. Para todas suas vantagens administrativas, as contas de serviço incorporadas nunca foram ideais, pois todos os serviços que usam a mesma conta de serviço obtêm acesso aos mesmos recursos protegidos. Antes do Windows Vista, não havia como dizer que o Serviço A executado como LocalService teria determinadas permissões em algum recurso enquanto o Serviço B não teria. Isso tornou mais difícil proteger dados específicos de serviço do ataque de outros serviços.

O Windows Vista corrige isso ao introduzir SIDs (service security identifiers, identificadores de segurança de serviço) que podem ser usados no acesso de listas de controle (ACLs), a fim de definir as permissões não somente para a conta de serviço específica mas também para o serviço específico. Cada serviço no Windows Vista e no Windows Server 2008 tem um SID que o identifica e esse SID pode ser usado nos editores de controle de acesso e de fato em praticamente qualquer descritor de segurança especificado. A Figura 3 fornece um exemplo de uma pasta com uma entrada de controle de acesso especificamente concedendo permissões de serviço à pasta.

Figura 3 Permissões concedidas ao Serviço A
Figura 3 Permissões concedidas ao Serviço A

Você pode especificar um nome de serviço nos editores de controle de acesso usando o domínio NT Service. No exemplo mostrado na Figura 3, o nome da conta é NT Service\ServiceA. Você também pode usar a função LookupAccountName se precisar usar a SID do serviço por meio de programação.

Por padrão, o token de processo criado para um serviço não inclui a SID do serviço. Alterar essa opção padrão é um trabalho da função ChangeServiceConfig2. A função a seguir mostra como você pode controlar essa opção (sim, ela deve ser chamada pelo programa de intslação do serviço):

bool ChangeSidType(SC_HANDLE service,
  DWORD sidType)
{
  ASSERT(0 != service);

  SERVICE_SID_INFO info = { sidType };

  return 0 != ::ChangeServiceConfig2(service,
    SERVICE_CONFIG_SERVICE_SID_INFO,
    &info);
}

O valor padrão do tipo SID é SERVICE_SID_TYPE_NONE. Isso é para a compatibilidade do aplicativo e produz um contexto de segurança semelhante que você pode obter para um serviço em uma versão anterior do Windows. A especificação de SERVICE_SID_TYPE_UNRESTRICTED como o tipo SID instrui o SCM a adicionar a SID de serviço ao token do processo do serviço, permitindo assim que o serviço obtenha acesso a recursos que você pode ter configurado para permitir acesso somente ao seu serviço específico.

Protegendo outros com tokens restritos

Embora proteger os recursos de um serviço com uma SID de serviço seja bom para impedir outros usuários e serviços de acessar ou pelo menos modificar seus dados ou configurações, isso não faz nada para impedir que o serviço acesse outros recursos que o serviço precisar acesar. Por que isso importa? Bem, se o seu serviço for comprometido, ele poderia ser usado como um vetor de ataque.

O Windows fornece uma terceira opção para o sinalizador SERVICE_CONFIG_SERVICE_SID_INFO usando o tipo SID SERVICE_SID_TYPE_RESTRICTED. Esse tipo SID resulta na inclusão de SID de serviço no token do processo de serviço, mas também instrui o SCM a adicionar uma lista de SIDs de restrição ao token para restringir ainda mais os recursos aos quais o serviço tem acesso.

A restrição de SIDs tem sido tradicionalmente difícil de aplicar geralmente, pois pode ser complicado ter uma lista restrita de SIDs que efetivamente reduzirá o alcance de um token sem também restringi-lo, de modo que se tornará inútil. Para resolver esse problema, o Windows Vista introduz um novo tipo de token conhecido como token com restrição de gravação. Ele é basicamente um relaxamento das regras regendo a restrição de SIDs. Em vez de ser usada para simplesmente restringir o acesso além do que é permitido pela DACL, o token restrito de gravação é usado somente ao avaliar verificações de acesso de gravação. Isso aumenta a aplicabilidade de SIDs de restrição consideravelmente.

Com o tipo SID do SERVICE_SID_TYPE_RESTRICTED, o SCM irá criar um token restrito para o processo de serviço com as seguintes SIDs de restrição: Everyone SID, Logon SID, service SID e write-restricted SID. Isso permite que você tenha liberdade suficiente para conceder acesso de gravação a recursos em uma SID de serviço específica ou a qualquer token restrito com gravação. O mais importante, isso restringe severamente onde o serviço pode ser gravado, limitando assim o impacto que pode ter no caso de ser comprometido.

Recebendo notificações de serviço

O controle e a consulta do status de serviço tem sido tradicionalmenta uma tarefa desafiadora. Você pode, por exemplo, chamar a função StartService para solicitar que um determinado serviço seja iniciado. No entanto, você não será imediatamente notificado de que o serviço foi iniciado com êxito, pois ele pode demorar algum tempo para concluir seu processo de inicialização. Em vez disso, você precisa aguardar para que o serviço inicie e poder chamar a função QueryServiceStatus periodicamente para verificar seu andamento. Não é necessário dizer que essa não é uma solução ideal. Entre outras razões, a sondagem leva ao desempenho insatisfatório, pois o thread de chamada é desnecessariamente programado para verificar o status do serviço. Felizmente, o Windows Vista finalmente soluciona esse problema com a introdução de notificações de status de serviço.

A nova função NotifyServiceStatusChange permite que os chamadores recebam notificações na forma de chamadas de procedimento assíncrono (APCs). Cada chamada para NotifyServiceStatusChange resultará em, no máximo, uma APC sendo colocada em fila, de modo que se você precisar ser informado de várias alterações, precisará chamar novamente NotifyServiceStatusChange. Se você não estiver mais interessado em receber uma notificação, tudo que precisará fazer é simplesmente fechar o identificador do serviço e o sistema retirará da fila qualquer APC pendente.

O NotifyServiceStatusChange tem a capacidade de fornecer notificações para um serviço específico ou para o próprio SCM. A Figura 4 mostra a você como receber uma notificação quando o serviço Windows Search for iniciado.

Figure 4 Recebendo notificações de APC (Close Figure 4)
void CALLBACK ServiceNotifyCallback(void* param)
{
  SERVICE_NOTIFY* serviceNotify = static_cast<SERVICE_NOTIFY*>(param);

  // Check the notification results
}

int main()
{
  SC_HANDLE scm = ::OpenSCManager(
    0,                              // local SCM
    SERVICES_ACTIVE_DATABASE,
    SC_MANAGER_ENUMERATE_SERVICE);

  ASSERT(0 != scm); // Call GetLastError for more info.

  SC_HANDLE service = ::OpenService(
    scm,
    L"wsearch",
    SERVICE_QUERY_STATUS);

  ASSERT(0 != service); // Call GetLastError for more info.

  SERVICE_NOTIFY serviceNotify =
    { SERVICE_NOTIFY_STATUS_CHANGE,
      ServiceNotifyCallback };

  const DWORD result = ::NotifyServiceStatusChange(
    service,
    SERVICE_NOTIFY_RUNNING,
    &serviceNotify);

  ASSERT(ERROR_SUCCESS == result);

  ::SleepEx(INFINITE, TRUE); // Wait for the notification

  ::CloseServiceHandle(service);
  ::CloseServiceHandle(scm);
}

Se você quiser receber notificações do SCM, basta usar o identificador para SCM em vez do identificador de serviço. Observe que diferentes sinalizadores de notificação são usados. Somente os sinalizadores SERVICE_NOTIFY_CREATED e SERVICE_NOTIFY_DELETED aplicam-se ao SCM. O restante, incluindo SERVICE_NOTIFY_RUNNING, SERVICE_NOTIFY_STOPPED e outros, todos requerem um identificador de serviço. Para obter uma lista completa dos sinalizadores, verifique na documentação NotifyServiceStatusChange.

O endereço da mesma estrutura SERVICE_NOTIFY é passado para a função de retorno de chamada de notificação. Você pode então determinar as especificações da notificação.

Finalmente, o uso de APCs para notificações deve ser um tanto intimidador se você não estiver familiarizado com o processo, mas é bem simples assim que você estiver acostumado a usá-lo. Para obter uma visão detalhada sobre APCs, verifique a minha programação paralela com a série C++ de artigos em weblogs.asp.net/kennykerr.

O que vem a seguir?

O que cobre os aprimoramentos de serviços do Windows introduzidos com o Windows Vista. Devo mencionar que a nova API do Windows Firewall também foi aprimorada com suporte à restrição de um acesso de serviço à rede. No entanto, a API do Windows Firewall está alem do que se pode cobrir nesta edição, mas espero dedicar algum espaço a ela em uma coluna futura.

Envie suas perguntas e comentários para Kenny pelo email mmwincpp@microsoft.com.

Kenny Kerr é um profissional de fabricação de software especializado no desenvolvimento para o Windows. Ele adora escrever e ensinar programação e design de software aos desenvolvedores. Entre em contato com o Kenny em weblogs.asp.net/kennykerr.


Team System: Observa&#231;&#245;es e diretivas de check-in.

Team System
Observações e diretivas de check-in.
Brian A. Randell

Download do código disponível em: TeamSystem2008_Launch.exe (150KB)
Conteúdo
Observações de check-in
Diretivas de check-in
Conflitos de check-in

N
o artigo de janeiro de 2007 desta coluna, iniciei uma discussão sobre o uso das APIs de controle de versão do Team Foundation Server (TFS) a partir de um suplemento do Microsoft® Word 2003. Aqui estou, mais de um ano depois, com a quinta e última parte dedicada às APIs de controle de versão e o suplemento do Word. (Realmente não pensei que fosse render tantos artigos.)

Você deve se lembrar das colunas anteriores que o suplemento atualmente suporta check-in, check-out, a função desfazer alterações pendentes e a habilidade de associar itens de trabalho a um check-in. Na minha coluna mais recente, examinei as observações e diretivas de check-in. Este mês, mostrarei como adicionar suporte a observações e diretivas de check-in ao suplemento do Word. Além disso, você aprenderá como lidar com os conflitos de check-in. E caso você não tenha lido o restante desta série (listado na barra lateral "Primeiras quatro partes desta série"), lembre-se de verificá-la.

Observações de check-in

As observações de check-in são campos de texto de forma livre que você pode associar a um check-in. Você pode marcar as observações de check-in como obrigatórias e controlar a ordem em que elas aparecem na caixa de diálogo de check-in do Visual Studio®. Para imitar esse comportamento no suplemento do Word, você deve obter a lista de observações de check-in do projeto de equipe ativo, classificar a lista e exibir um rótulo e uma caixa de texto para cada observação de check-in.

O objeto VersionControlServer expõe o método GetCheckinNoteDefinitions, que aceita uma matriz de nomes de projeto de equipe e retorna uma matriz de objetos CheckinNoteFieldDefinition. Cada instância de CheckinNoteFieldDefinition expõe propriedades do nome da observação de check-in, a ordem de exibição, se necessária, e uma cadeia de caracteres de um item do servidor. GetCheckinNoteDefinitions retorna a lista de itens em que parece haver uma ordem aleatória. Em meus testes, descobri que as observações retornam classificadas por nome de observação de check-in e agrupadas por nome de projeto de equipe, independentemente da ordem em que as referências do projeto de equipe são passadas no método. Portanto, presumindo-se que haja dois projetos de equipe definidos, um chamado MSF-Agile e um chamado MSF-CMMI com o conjunto padrão de observações, os resultados apareceriam como mostrado na Figura 1. E caso você tivesse criado uma nova observação de check-in chamada Build Master e a adicionado ao projeto de equipe MSF-CMMI, ela apareceria primeiro na lista.

Figure 1 Resultados de Calling GetCheckinNoteDefinitions (Close Figure 1)

Nome da observação de check-in | Team Project
Revisor de código | MSF-Agile
Revisor do código | MSF-CMMI
Revisor de desempenho | MSF-Agile
Revisor do desempenho | MSF-CMMI
Revisor de segurança | MSF-Agile
Revisor de segurança | MSF-CMMI

A classe tfsVCUtil, criada para o suplemento do Word, possui dois novos métodos compartilhados que você pode usar para recuperar observações de check-in: GetAllCheckInNotesByTeamProjectName e GetAllCheckInNotesByItemLocalPath. Usando um desses métodos, você pode obter a lista não-modificada de CheckinNoteFieldDefinitions, se houver, do projeto de equipe.

O suporte das observações de check-in na caixa de diálogo de check-in requer que você adicione um botão de alternância extra sob os botões de itens de trabalho existentes, um painel extra para agir como um recipiente, um rótulo e uma caixa de texto para cada observação de check-in. (O download do código fornece uma implementação simples). Entretanto, adicionar suporte a observações de check-in na caixa de diálogo de check-in é apenas uma peça do quebra-cabeças. Você também precisa adicionar código para reforçar as observações necessárias. As diretivas de check-in e a detecção de conflitos requerem que você faça alterações adicionais na forma em que a caixa de diálogo e o código de suporte na classe tfsVCUtil funcionam.

Diretivas de check-in
Quatro primeiras partes desta série
  • Controle de versão do Team Foundation Server (janeiro de 2007)
  • Controle de item de trabalho (abril de 2007)
  • Itens de trabalho e suporte à função Desfazer (setembro de 2007)
  • Diretivas de check-in personalizadas (novembro de 2007)

Como acontece com as observações de check-in, a primeira coisa a fazer para dar suporte às diretivas de check-in é descobrir quais diretivas estão habilitadas para o projeto de equipe ativo. A segunda é decidir como processar as diretivas de check-in. Como a Microsoft criou as diretivas de check-in principalmente para os desenvolvedores fazerem check-in do código do Visual Studio, duas das três diretivas internas que vêm com o TFS 2005 não funcionam corretamente quando usadas para validar o check-in de um único documento do Word. Portanto, é necessário que o suplemento somente processe erros de diretivas de check-in que façam sentido no contexto de um documento do Word.

Só há uma maneira de resolver esse problema. Fazer o suplemento avaliar todas as diretivas habilitadas e ignorar os erros gerados por aquelas que não fazem sentido. Na inspeção inicial do funcionamento das diretivas de check-in, parece que uma alternativa é fazer o suplemento executar o processo de avaliação manualmente nessas diretivas que devem funcionar. Mas constata-se que isso não é possível.

Por padrão, as diretivas e as observações de check-in não são avaliadas como parte do processo de check-in quando se usa o código personalizado. Para imitar o comportamento do Visual Studio, depende do seu aplicativo garantir que as observações de check-in necessárias sejam fornecidas e que não haja violações de diretiva - ou, se houver violações, que sejam fornecidas informações de substituição de diretiva.

Para facilitar um pouco a vida, a classe Workspace expõe um método EvaluateCheckin. Use-o para verificar condições que talvez façam com que um check-in falhe. Aqui está o protótipo do método da documentação do MSDN®:

Public Function EvaluateCheckin ( _
    options As CheckinEvaluationOptions, _
    allChanges As PendingChange(), _
    changes As PendingChange(), _
    comment As String, _
    checkinNote As CheckinNote, _
    workItemChanges As WorkItemCheckinInfo() _
) As CheckinEvaluationResult

O método primeiro quer saber o que você gostaria de avaliar. Você fornece essas informações passando o valor de enumeração correto através dos parâmetros de opções. Você pode fazer o método procurar por conflitos de check-in, observações de check-in necessárias, violações de diretivas ou os três.

Em seguida, o método requer que você passe uma matriz de objetos PendingChange do segundo e do terceiro parâmetros. Por que dois conjuntos? No Visual Studio, por exemplo, você poderia ter dez arquivos modificados no espaço de trabalho atual, mas o usuário pode optar por selecionar apenas três arquivos para check-in. Portanto, o Visual Studio passa os dez arquivos através de parâmetros allChanges e os três arquivos selecionados através do parâmetro “changes”. O método fornece essas informações para os assemblies da diretiva habilitada. No caso do suplemento do Word, você passa a mesma matriz para ambos os parâmetros.

Para o quarto parâmetro, você passa o texto do comentário, se houver, fornecido pelo usuário que está fazendo o check-in. Para passar observações de check-in, crie uma instância do tipo CheckInNote, que aceita uma matriz de instâncias CheckinNoteFieldValue, e a passa como o quinto parâmetro. Finalmente, você passa uma matriz de instâncias WorkItemCheckinInfo para o último parâmetro. Não é possível passar nada para qualquer um dos três últimos parâmetros.

Quando você tiver chamado EvaluateCheckin, receberá uma instância CheckinEvaluationResult. Esse objeto expõe os resultados através de três propriedades de matriz: Conflicts, NoteFailures e PolicyFailures. Se não houver valores para um conjunto de resultados particular, EvaluateCheckin retorna uma matriz de tamanho zero. Cabe a você enumerar cada matriz e processar os resultados. Observe que mesmo se EvaluateCheckin retornar falhas para observações ou diretivas de check-in, isso não bloqueia um check-in. Você deve implementar a lógica em seu código para evitar um check-in quando essas regras não forem satisfeitas. Entretanto, se houver conflitos, o TFS não bloqueia o check-in. Para tirar vantagem desse método para o suplemento do Word, exponha EvaluateCheckin através da classe tfsVCUtil usando um método compartilhado chamado PreValidateCheckIn:

Public Shared Function PreValidateCheckIn( _
  ByVal docPath As String, _
  ByVal options As CheckinEvaluationOptions, _
  ByVal comments As String, ByVal notes As CheckinNote, _
  ByVal workitems() As WorkItemCheckinInfo) _
  As CheckinEvaluationResult

  Dim pc As PendingChange() = _
      m_userWorkspace.GetPendingChanges(docPath)

  Return m_userWorkspace.EvaluateCheckin(options, _
      pc, pc, comments, notes, workitems)
End Function

Como você pode ver na listagem do código, PreValidateCheckIn imita a assinatura de EvaluateCheckin com o parâmetro adicional do documento a ser avaliado. Quando chama esse método da caixa de diálogo de check-in, você enumera as matrizes dos itens que solicitou para serem validados, procurando por problemas e, se necessário, fornecendo uma indicação visual para o usuário do que está errado.

Com relação às violações de diretiva, você precisa decidir quais diretivas fazem sentido para o seu aplicativo. Naturalmente, as diretivas internas oferecidas pela Microsoft, assim como muitas das diretivas disponíveis, tendem para a experiência de check-in do Visual Studio. No caso do suplemento do Word , somente a diretiva de item de trabalho necessário faz sentido quando examinamos as três diretivas internas da edição de 2005 do Visual Studio Team System. Além das diretivas internas, a solução do suplemento do Word precisa se preocupar somente com os erros de diretiva gerados por assemblies relevantes. Usando um mecanismo fora da banda, tal como pares de nome/valor armazenados no arquivo de configuração do suplemento, o código sabe quais diretivas devem ser respeitadas. Em seguida, no tempo de execução, o código examina a coleção de erros de diretiva e ignora todos os erros que não estão listados. Uma forma de fazer isso é converter a referência IPolicyEvaluation retornada a partir da propriedade Policy da instância PolicyFailure para uma instância de IPolicyDefinition. Então, IPolicyDefinition expõe uma propriedade Type que pode ser usada para identificar a origem da diretiva. Este é um exemplo simples:

Dim cer As CheckinEvaluationResult = {Call to server elided}
Dim i As IPolicyDefinition = Nothing
For Each pf As PolicyFailure In cer.PolicyFailures
  i = DirectCast(pf.Policy, IPolicyDefinition)
  If i.Type = "Work Items" Then
    ' process this policy failure
  End If
Next

Existe uma desvantagem nessa abordagem. Como parte do Visual Studio 2005 Team Foundation Server Power Tools, a Microsoft adicionou quatro diretivas de check-in adicionais (consulte msdn2.microsoft.com/vstudio/aa718351). Uma diretiva em particular, a diretiva de caminho personalizada, usa um caminho indireto através da delegação para executar outra diretiva, tal como a diretiva de item de trabalho, quando as alterações pendentes que estão sendo fornecidas correspondem a uma expressão regular específica. As equipes usam essa diretiva para reforças as regras de check-in mais estritas apenas em parte do repositório do código enquanto outra seção pode ser menos restritiva. Portanto, se essa diretiva for usada, o código listado anteriormente vê o tipo da diretiva de caminho personalizada, não a diretiva real que está executando – por exemplo, a diretiva de itens de trabalho.

Você pode tentar fazer o que o Visual Studio faz, mas isso não funciona. Embora você possa descobrir a lista de diretivas habilitadas chamando GetCheckinPolicies de uma instância TeamProject válida, não há uma maneira (razoável) de adquirir uma referência IPendingCheckin válida que seja necessária como um parâmetro para o método Initialize de IPolicyEvaluation. Portanto, para lidar com a diretiva de caminho personalizada, você precisa examinar a propriedade Description retornada de IPolicyDefinition. Os autores da diretiva optaram por armazenar o valor Type da diretiva delegada aqui. Portanto, o código refatorado é similar ao seguinte:

Dim cer As CheckinEvaluationResult = '{Call to server elided}
Dim i As IPolicyDefinition = Nothing
Dim processPolicy As Boolean = False
For Each pf As PolicyFailure In cer.PolicyFailures
  i = DirectCast(pf.Policy, IPolicyDefinition)

  processPolicy = (i.Type = "Work Items")
  If Not processPolicy AndAlso _
    i.Type = "Custom Path Policy" Then
    processPolicy = (i.Description = "Work Items")
  End If
  If processPolicy Then
    ' process this policy failure
  End If

Esta versão faz um trabalho básico de manipular a diretiva Itens de trabalho padrão e uma versão delegada usando a diretiva Caminho personalizado. Sua própria implementação pode ser mais complexa e talvez requeira uma solução mais elaborada, mas agora você está equipado com as ferramentas para resolver o problema.

Mostrei a você como obter a lista de observações de check-in para que apresente uma interface de usuário. Se observar EvaluateCheckin e o método de wrapper PreValidateCheckIn, você verá que para validar as observações de check-in, será necessário criar e passar uma instância de CheckinNote. Depois de executar EvaluateCheckin, diretamente ou através do wrapper, você precisa examinar a coleção NoteFailures. Cada instância de NoteFailure expõe uma propriedade Definition que representa o CheckinNoteFieldDefinitions da observação de check-in. Aí, você pode facilmente acessar a propriedade Name para saber quais observações de check-in devem ser realçadas na sua interface do usuário. A propriedade Message é uma mensagem genérica que afirma "Um valor deve ser especificado para a observação de check-in". Como mencionado anteriormente, nem as violações de diretiva nem as observações de check-in bloquearão um check-in executado a partir do código. Depende de você garantir que as coisas estejam realmente satisfatórias antes de chamar o código de check-in.

Conflitos de check-in

Por outro lado, os conflitos de check-in bloqueiam o seu check-in. Você controla o comportamento de check-out padrão dos artefatos do repositório de controle de versão usando a caixa de diálogo File Types (mostrada na Figura 2), que pode ser acessada via Team | Team Foundation Server Settings | Source Control File Types. Usando essa caixa de diálogo você controla quais arquivos dão suporte à mesclagem de arquivo e, portanto, ao check-out simultâneo. Por exemplo, você pode ver na Figura 2 que os arquivos do Microsoft Office têm a mesclagem de arquivos desabilitada. Conseqüentemente, quando faz um check-out usando o explorador de controle de origem ou as APIs, você obtém um check-out exclusivo. Para ajustar as configurações de check-out múltiplo e de mesclagem de arquivos para um conjunto de extensões de arquivo, simplesmente clique no botão Edit para abrir a caixa de diálogo Edit File Types.

Figura 2 Tipos de arquivo configurados para controle de versão
Figura 2 Tipos de arquivo configurados para controle de versão(Clique na imagem para diminuir a exibição)
Figura 2 Tipos de arquivo configurados para controle de versão
Figura 2 Tipos de arquivo configurados para controle de versão (Clique na imagem para aumentar a exibição)

Se a mesclagem de arquivos estiver habilitada para uma extensão de arquivo específica e ocorrer um conflito no check-in, o Visual Studio, por padrão, exibe a ferramenta Team Explorer Merge. A Microsoft tornou essa opção configurável por desenvolvedor. No Visual Studio, você pode ir para Tools | Options e no nó Source Control, selecionar o nó Visual Studio Team Foundation Server. Na página de opções, clique em Configure User Tools e em Add. Agora você tem controle total sobre como uma extensão de arquivo específica ou um conjunto de extensões são manipulados (consulte a Figura 3).

Figura 3 Configurando uma nova ferramenta de mesclagem para uma extensão de arquivo
Figura 3 Configurando uma nova ferramenta de mesclagem para uma extensão de arquivo(Clique na imagem para diminuir a exibição)
Figura 3 Configurando uma nova ferramenta de mesclagem para uma extensão de arquivo
Figura 3 Configurando uma nova ferramenta de mesclagem para uma extensão de arquivo (Clique na imagem para aumentar a exibição)

Portanto, o comportamento padrão é para um check-out exclusivo quando você usa o suplemento do Word para fazer check-out de um documento do Word. Em teoria, isso elimina a necessidade de lidar com conflitos no código do suplemento. Entretanto, há um problema com esse comportamento padrão que pega vários novos usuários do TFS desprevenidos. Por padrão, se você solicitar um check-out usando o TFS 2005 através do Visual Studio ou das APIs, ocorre o check-out do arquivo local atual em seu espaço de trabalho – mesmo que essa não seja a versão mais atual armazenada no TFS. O código subjacente não obtém a versão mais recente. Sendo assim, é possível que você faça check-out de um arquivo através do suplemento do Word, faça alterações e, em seguida, haja uma falha no check-in quando você terminar.

Você pode resolver esse problema de várias formas. Na verdade, a Microsoft ouviu os comentários dos clientes e no TFS 2008 você pode habilitar o recurso “Get Latest on Check out” por projeto de equipe; você também pode controlar esse recurso na instalação local. A Microsoft também atualizou a API PendEdit para dar suporte a esse recurso. Entretanto, para que o suplemento funcione no TFS 2005, você precisa modificar o código de check-out existente obter a versão mais recente manualmente antes do check-out. Para fazê-lo, você precisa adicionar uma linha de código que chama Workspace.Get com os parâmetros corretos antes de chamar PendEdit. O código da Figura 4 faz isso.

Figure 4 Código mais recente a ser obtido antes do check-out (Close Figure 4)
Public Shared Function CheckOutDocument( _
  ByVal docPath As String) As Boolean

  If serverValidated Then
    If m_userWorkspace Is Nothing Then
      ' We need the Workstation
      Dim userWorkstation As Workstation = Workstation.Current
      ' We need the Workspace
      Dim docWSInfo As WorkspaceInfo = _
        userWorkstation.GetLocalWorkspaceInfo(docPath)

      m_userWorkspace = m_tfsVCS.GetWorkspace(docWSInfo)
    End If

    Dim status As GetStatus = _
    m_userWorkspace.Get(New String() {docPath}, _
      VersionSpec.Latest, RecursionType.None, _
      GetOptions.GetAll Or GetOptions.Overwrite)

    Dim statusCount As Integer = status.NumConflicts + _
      status.NumFailures + status.NumOperations + _
      status.NumWarnings

    If statusCount = 1 Then
      If m_userWorkspace.PendEdit(docPath) = 1 Then
        Return True
      Else
        Return False
      End If
    Else
      Throw New tfsUtilException( _
        String.Format(MSG_UNEXPECTED_GETLATEST_CONFLICT, _
          "Failures count: " & status.NumFailures))
    End If
  Else
    Throw New tfsUtilException(MSG_SERVER_NOT_VALIDATED)
  End If
End Function

O comando Get retorna uma instância de um objeto GetStatus, que expõe um conjunto de propriedades que fornecem estatísticas relacionadas ao número de conflitos, falhas, operações e avisos. Você pode usar esses dados para determinar se o comando Get funcionou como esperado.

Com o suplemento usando essa versão de CheckOutDocument, não deve haver conflitos. Mas se você ainda estiver preocupado com conflitos, apenas verifique a matriz Conflicts retornada por EvaluateCheckin. Caso haja conflitos, você precisa oferecer uma experiência que permita ao usuário selecionar o chamado "vencedor" Se você quiser que o vencedor seja a pessoa que está tentando realizar o check-in atual, é necessário usar o método QueryConflicts do objeto Workspace. Isso retorna uma matriz de objetos Conflict. Em seguida, defina a propriedade Resolution da instância Conflict como Resolution.AcceptYours e passe o objeto para o método Workspace.ResolveConflict. Oferecer uma experiência mais rica que essa irá requerer uma reflexão séria e um investimento adicional em tempo e código.

Agora, desde que tenha todas as cinco partes desta série em mão, você deve ter todas as ferramentas e informações necessárias para codificar suas próprias experiências de check-in ricas e personalizadas para seus aplicativos. Mais adiante, você precisará consultar as APIs atualizadas fornecidas com o Team Foundation Server 2008 e se preparar para mais melhorias na versão "Rosario" que será lançada futuramente.

Um agradecimento especial para Martin Woodward da Teamprise e a todas as pessoas da equipe do Visual Studio Team System, que contribuiram para esta coluna e para as anteriores desta série.

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. Brian é o autor do curso Applied Team System da Pluralsight e é um MVP da Microsoft. Você pode entrar em contato com o Brian através do blog dele em mcwtech.com/cs/blogs/brianr.


{ End Bracket }: A declara&#231;&#227;o de independ&#234;ncia digital

{ End Bracket }
A declaração de independência digital
Bill Hill

A tecnologia digital está criando o nosso mundo. Daqui há quinhentos anos, os historiadores poderão observar tudo o que nós que lemos a MSDN® Magazine estamos fazendo atualmente e acredito que eles chamarão a nossa era de A renascença digital.

Tudo que diz respeito à ciência, à medicina e à prosperidade econômica que usufruimos atualmente − e o formato político do planeta − pode ser rastreado de volta à tecnologia da impressão desenvolvida por Johannes Gutenberg. É difícil imaginar como qualquer um desses desenvolvimentos poderia ter ocorrido sem o conhecimento adulto disseminado e capacidade de registrar e compartilhar o conhecimento, que sua tecnologia permitiu. A Bíblia que Gutenberg produziu em 1455 não foi o primeiro livro impresso. Mas foi a primeira impressão usando um tipo de metal movível em um sistema mecanizado. Essa inovação foi como um terremoto embaixo do mar. Nem mesmo observável primeiramente, ela se tornou uma onda de maré que mudou o mundo para sempre; os efeitos ainda são sentidos depois de mais de meio milênio.

A capacidade de armazenar e compartilhar facilmente rompeu as barreiras entre a igreja e o estado. Isso permitiu o conhecimento adulto disseminado e levou à explosão de pensamento que chamamos de Renascença. Ela também levou à nova política e pensamento econômico que os historiadores chamariam de Idade do Iluminismo. As pessoas sabiam que o mundo estava mudando. Thomas Paine, um dos pensadores por trás da Revolução Americana, capturou isso da melhor forma. No Dia de St. Valentine, em 1776, ele disse, "Temos nisso o poder para começar o mundo novamente. Uma situação, semelhante à presente, não acontecia desde os dias de Noé até agora. O nascimento de um novo mundo está em nossas mãos”.

Assim que o formato do futuro estava claro, o grupo de pensadores à qual Paine pertencia começou a se perguntar: "Que tipo de mundo queremos construir?". O pensamento deles levou à Declaração da Independência, que definiu um conjunto de direitos humanos em relação ao que ainda buscamos hoje em dia. Ainda não atingimos tudo que os ideais ambiciosos oferecem. Mas é assim que podemos verificar o nosso progresso.

Uma mudança ainda maior está acontencendo agora. Um novo maremoto ocorreu há 40 anos, quando o primeiro “computador pessoal” surgiu. Desde então essa onda não parou de crescer no planeta. Meu filho, que tem 17 anos, tem naturalmente o poder de levar com ele onde quer que vá toda sua gigantesca coleção de músicas. Ele pode conversar online com seus amigos em Massachusetts ou na Mongólia. Ele pode produzir sua própria música, vídeo ou filme. Ele pode obter notícias de qualquer parte do mundo, comprar passagens áereas ou qualquer tipo de mercadorias online e assim por diante.

Nós que criamos a tecnologia digital para tornar essas coisas possíveis − e coisas que ainda não pensamos − temos o poder de “começar o mundo novamente", afirmou Paine. E então? Que tipo de mundo você quer criar? Há grandes problemas e perguntas que precisam ser respondidas.

A Microsoft, com mais de um bilhão de usuários, alcançou muitos deles primeiro. Nunca houve uma empresa cujos produtos tivessem o potencial de mudar as vidas de bilhões de pessoas em todo o mundo. E não há um manual a ser seguido. Às vezes, paramos, mas continuaremos crescendo se fizermos mais coisas certas do que erradas. Os nossos primeiros bilhões de clientes são os “pioneiros” que pagam um preço premium para estar na primeira onda? E o próximo bilhão?

Algumas dessas grandes questões levaram a Microsoft a criar o programa Unlimited Potential. Este programa, que visa atingir os cinco bilhões de pessoas vivendo em comunidades necessitadas em todo o mundo, tenta auxiliar esses indivíduos e comunidades a atingir seus sonhos ao fornecer tecnologias atuais e acessíveis.

Não sei nem mesmo uma fração das perguntas que todos nós precisaremos fazer − muito menos as respostas. Não há uma mapa detalhado dessa jornada. Mas precisamos de outro incentivo para nos dar uma direção. E eu não poderia encontrar um melhor do que a Declaração da Independência, atualizada para a Era digital. Então, aqui está a minha declaração de independência digital:

Consideramos essa verdade como sendo auto-evidente, que cada ser humano tem o direito igual e inalienável aos meios de criar, distribuir e consumir informações, a fim de perceber seu potencial para a Vida, a Liberdade e a Busca da felicidade − independentemente do país em que vive, seu sexo, crenças, origem racial, idioma ou quaisquer deficiências que possa ter.

Isso não é impor a democracia em todo o mundo. Nem diz respeito a tentar mudar tudo de uma só vez. Lembre-se, levou mais de 200 anos desde a primeira Declaração para que houvesse direitos iguais nos Estados Unidos, mesmo que a escravidão tivesse sido abolida há mais de 100 anos − ainda vivemos isso?

A Declaração digital não é um mapa; é uma bússola, uma direção. Quer juntar-se a mim nessa jornada?

Bill Hill entrou na Microsoft em 1995, pois acreditava que a empresa lideraria a transição da leitura no papel para a leitura na tela. Ele é um dos criadores da tecnologia ClearType que aprimorou drasticamente a capacidade de leitura de texto na tela.