Silverlight

Crie aplicativos empresariais de linha de negócios com o Silverlight, parte 1

Hanu Kommalapati

Este artigo aborda:

  • O ambiente de runtime do Silverlight
  • Programação assíncrona do Silverlight
  • Diretivas entre domínios
  • Um aplicativo empresarial de exemplo
Este artigo usa as seguintes tecnologias:
Silverlight 2

Código disponível para download na MSDN Code Gallery
Navegue pelo código online

Sumário

Conceitos básicos do Silverlight: o CoreCLR
Runtime do Silverlight
O cenário do aplicativo
Notificações de envio com o servidor de soquete
Loops de E/S assíncronos
Caixas de diálogo restritas no Silverlight
Implementação de notificações de envio
Acesso entre domínios de serviços TCP
Diretivas entre domínios com serviços TCP
Conclusão

Quando apresentei um resumo executivo sobre as implicações comerciais do Silverlight a uma grande empresa de Houston pouco tempo atrás, a resposta foi indiferente. As demonstrações incríveis que ilustravam o DeepZoom, vídeo com qualidade Picture-In-Picture e alta definição e animações de alta qualidade deveriam ter facilmente entusiasmado o grupo. Quando sondei a audiência sobre seu interesse limitado, ficou claro que, embora os gráficos brilhantes fossem excelentes, havia muito pouca orientação prática disponível sobre como criar aplicativos LOB (linha de negócios) centrados em dados e de qualidade empresarial com o Silverlight.

Hoje, os aplicativos empresariais exigem entrega segura de informações LOB nos limites da rede, muitas vezes pela Internet, com interface do usuário e remoção de dados baseadas em função aplicadas no contexto da empresa. A execução do Silverlight no cliente e do Microsoft .NET Framework 3.5 no servidor confere excelentes recursos para criar tais aplicativos LOB seguros e escalonáveis. O runtime leve do Silverlight em execução dentro de uma área de segurança fornece as bibliotecas de estrutura para a integração com serviços de dados back-office. Para desenvolver aplicativos robustos com o Silverlight, arquitetos e desenvolvedores precisam entender o modelo de programação do Silverlight e seus recursos de estrutura no contexto de um aplicativo real.

Meu principal objetivo neste artigo é pegar um cenário de LOB e criar um aplicativo a partir do zero, ilustrando diversos aspectos do desenvolvimento com o Silverlight no decorrer do processo. A solução que analisarei é um aplicativo de call center; sua arquitetura lógica é mostrada na Figura 1. Neste artigo, abordarei a notificação de preenchimento da tela, o modelo de programação assíncrona, caixas de diálogo do Silverlight e a implementação de servidor de diretivas TCP entre domínios. Na Parte 2, falarei sobre segurança do aplicativo, integração com Web Services, particionamento do aplicativo e vários outros aspectos do aplicativo.

fig01.gif

Figura 1 Arquitetura lógica de um call center Silverlight

Conceitos básicos do Silverlight: o CoreCLR

Antes de eu começar, vamos recapitular os conceitos básicos do Silverlight. Primeiro, vou analisar o runtime do Silverlight para que você entenda melhor o que ele permite fazer. O CoreCLR é a máquina virtual usada pelo Silverlight. É parecido com o CLR que ativa o .NET Framework 2.0 e versões mais recentes e contém sistemas semelhantes de carregamento de tipos e coleta de lixo (GC).

O CoreCLR tem um modelo de CAS (segurança de acesso a código) muito simples — mais simples do que o do CLR da área de trabalho, pois o Silverlight só precisa impor diretivas de segurança no nível de aplicativo. Isso acontece porque, como cliente Web independente de plataforma, ele não pode contar que haja alguma diretiva empresarial ou de máquina em vigor e não deve permitir que um usuário altere diretivas existentes. Existem algumas exceções, como OpenFileDialog e IsolatedStorage (alteração da cota de armazenamento), em que o Silverlight precisa do consentimento explícito do usuário para violar o conjunto de regras padrão da área de segurança. OpenFileDialog é usado para acessar o sistema de arquivos e IsolatedStorage para acessar o armazenamento isolado por homônimo e aumentar a cota de armazenamento.

Para aplicativos de área de trabalho, cada executável carrega exatamente uma cópia do CLR, e o processo do SO conterá apenas um aplicativo. Cada aplicativo tem um domínio de sistema, um domínio compartilhado, um domínio padrão e inúmeros AppDomains criados explicitamente (consulte "JIT and Run: Analise detalhadamente as operações internas do .NET Framework para saber como o CLR cria objetos de runtime"). Um modelo de domínio semelhante encontra-se no CoreCLR. No caso do Silverlight, vários aplicativos, possivelmente de diferentes domínios, serão executados no mesmo processo de SO.

No Internet Explorer 8.0, cada guia é executada em seu próprio processo isolado; portanto, todos os aplicativos do Silverlight hospedados na mesma guia serão executados no contexto da mesma instância de CoreCLR, conforme ilustrado na Figura 2. Como cada aplicativo pode ser de diferentes domínios de origem, por motivo de segurança, tal aplicativo será carregado em seu próprio AppDomain. Haverá tantas instâncias de CoreCLR quantas forem as guias que no momento estiverem hospedando aplicativos do Silverlight.

Semelhantemente ao CLR de área de trabalho, cada AppDomain terá seu próprio pool de variáveis estáticas. Cada pool específico de domínio será inicializado durante o processo de inicialização de AppDomain.

fig02.gif

Figura 2 Cada aplicativo do Silverlight será executado em seu próprio AppDomain

Os aplicativos do Silverlight não podem criar seus próprios domínios de aplicativo personalizados; essa habilidade é reservada para uso interno. Para ver uma análise mais detalhada sobre o CoreCLR, consulte as seguintes colunas Tudo sobre CLR, da equipe de CLR: "Programe o Silverlight com o CoreCLR" e "Segurança no Silverlight 2."

Runtime do Silverlight

O Silverlight é voltado para uma ampla gama de aplicativos que exigem variados graus de suporte a estrutura e biblioteca. Um aplicativo simples pode, por exemplo, apenas executar arquivos de áudio de poucos bytes para ajudar na pronúncia de palavras de um site de dicionário ou exibir um anúncio em faixa. Por outro lado, aplicativos LOB empresariais exigem segurança, privacidade de dados, gerenciamento de estado, integração com outros aplicativos e serviços, suporte para instrumentação, entre outros. Ao mesmo tempo, o Silverlight precisa ter um runtime menor para que a implantação via Internet não seja um problema em conexões mais lentas.

Esses requisitos parecem conflitantes, mas a equipe do Silverlight resolveu isso particionando a estrutura na exibição em camadas mostrada na Figura 2. O runtime CoreCLR + Silverlight é o "plug-in" que todos os usuários instalarão para poder executar aplicativos. O plug-in é suficiente para a maioria dos aplicativos voltados para o consumidor. Se um aplicativo requer o uso de uma biblioteca do SDK (integração com WCF ou runtimes DLR, como o Iron Ruby) ou de uma biblioteca personalizada, ele deve empacotar esses componentes no pacote XAP para que o Silverlight saiba como resolver os tipos necessários em tempo de execução (consulte a coluna Cutting Edge desta edição para saber mais sobre XAPs).

O runtime do Silverlight tem aproximadamente 4 MB e, além das bibliotecas do CoreCLR, como agcore.dll e coreclr.dll, contém as bibliotecas necessárias exigidas por desenvolvedores de aplicativos. Entre elas estão as seguintes bibliotecas fundamentais: mscorlib.dll, System.dll, System.Net.dll, System.Xml.dll e System.Runtime.Serialization.dll. O runtime que dá suporte ao plug-in de navegador normalmente é instalado no diretório C:\Arquivos de Programas\Microsoft Silverlight\2.0.30930.0\. Este é o diretório criado quando um computador baixa e instala o Silverlight como parte da sessão de navegação na Web.

Os desenvolvedores que criam e testam aplicativos no mesmo computador terão duas cópias do runtime: uma instalada pelo plug-in e a outra através da instalação do SDK. A segunda cópia pode ser encontrada no diretório C:\Arquivos de Programas\Microsoft SDKs\Silverlight\v2.0\Assemblies de Referência. Ela será usada por modelos do Visual Studio como parte da lista de referências de tempo de compilação.

A área de segurança impede os aplicativos do Silverlight de interagir com a maioria dos recursos locais, o que é válido para qualquer aplicativo Web típico. Por padrão, um aplicativo do Silverlight não pode acessar um sistema de arquivos (que não seja o armazenamento isolado), fazer conexões de soquete, interagir com dispositivos conectados ao computador nem instalar componentes de software. Isso por certo impõe algumas restrições quanto aos tipos de aplicativos que se pode construir na plataforma Silverlight. No entanto, o Silverlight tem todos os ingredientes necessários para desenvolver um aplicativo LOB empresarial controlado por dados que precisa se integrar aos serviços e processos de negócios back-end.

O cenário do aplicativo

O aplicativo LOB que criarei aqui demonstra uma arquitetura de controle de chamadas de terceiros em que um servidor centralizado acessa uma infra-estrutura de PBX (central privada de comutação telefônica) para controlar telefones centralmente. Como o meu objetivo é enfocar o Silverlight como a superfície de interface do usuário, não me deterei muito na integração com a telefonia. Em vez disso, usarei um simulador de chamadas simples para gerar um evento de chamada recebida. O simulador colocará um pacote de dados que representa a chamada em uma fila de espera do Gerenciador de Chamadas, que dispara o processo fundamental para este projeto.

Meu cenário fictício requer que o aplicativo de call center seja executado em um navegador da Web de uma maneira independente de plataforma e, ao mesmo tempo, oferecendo interações com o usuário ricas como aplicativo de área de trabalho. O Silverlight foi a escolha natural, uma vez que o ActiveX não é muito popular em ambientes de cliente que não sejam o Windows.

Vejamos os aspectos da arquitetura do aplicativo. Você implementará notificações de envio, integração de eventos, integração de serviços de negócios, armazenamento em cache, segurança e integração com os serviços de nuvem.

Notificações de envio Elas são necessárias porque o sistema precisa capturar o evento de chamada recebida e transferir dados de IVR (resposta interativa de voz) inseridos pelo chamador para fazer um "preenchimento de tela", ou seja, preencher a tela da interface do usuário com informações da chamada recebida. Além disso, o usuário deve ter a oportunidade de aceitar ou rejeitar a chamada.

Streaming de eventos Em um aplicativo Web típico, o servidor Web tem todo o conhecimento dos eventos de negócios, uma vez que executa a maior parte dos processos de negócios. No caso de um RIA (Rich Internet Application), entretanto, a implementação de processos de negócios será compartilhada pelo aplicativo em execução no interior do navegador da Web e pelo servidor que implementa Web Services de negócios. Isso significa que os eventos de negócios e de tecnologia gerados no aplicativo do Silverlight precisam ser enviados ao servidor através de um conjunto de Web Services especiais.

Exemplos de eventos de negócios deste caso de solução são quando o usuário (representante) rejeita a chamada ("o representante rejeitou a chamada") ou aceita a chamada ("o representante aceitou a chamada"). Os eventos de tecnologia típicos são "Falha na conexão com o servidor TCP do Gerenciador de Chamadas" e "Exceção de Web Service".

Integração de serviços de negócios A solução de call center, assim como qualquer aplicativo LOB, precisa ser integrada a dados que podem estar armazenados em um banco de dados relacional. Usarei Web Services como um veículo para esta integração.

Armazenamento em cache Para uma melhor experiência do usuário, armazenarei as informações localmente na memória e no disco. As informações armazenadas podem incluir os arquivos XML que indicam os scripts de prompter do representante e outros dados de referência que não podem ser alterados com freqüência.

Segurança do aplicativo A segurança é um dos requisitos fundamentais deste tipo de aplicativo. A segurança inclui autenticação, autorização, privacidade de dados em trânsito e em repouso e remoção de dados com base no perfil do usuário.

Integração com os serviços de nuvem A integração com um serviço fundamental baseado em nuvem, como o serviço de armazenamento, requer infra-estruturas de servidor especiais. Dessa forma, o uso de serviços de nuvem pode ser monitorado de perto e controlado quanto à responsabilidade e aos níveis de serviço.

Abordarei os temas integração com serviços de negócios, segurança do aplicativo, diretivas entre domínios para Web Services e particionamento do aplicativo na Parte 2 deste artigo.

Notificações de envio com o servidor de soquete

O preenchimento da tela é um dos requisitos fundamentais do aplicativo de call center para transferir o contexto de chamada da infra-estrutura de telefonia para a tela de um agente. O contexto de chamada transferido pode incluir qualquer informação falada (no caso de sistemas IVR) ou digitada pelo consumidor que está ao telefone.

A notificação pode ser enviada ao aplicativo do Silverlight no navegador de uma das seguintes maneiras: através de sondagem do cliente ou de envio pelo servidor. A sondagem é razoavelmente fácil de implementar, mas pode não ser a opção ideal para cenários de call center em que a sincronização de estado entre os eventos de telefonia e o aplicativo cliente deve ser precisa. É por esse motivo que usarei notificações de envio utilizando soquetes do Silverlight.

Um dos recursos importantes do Silverlight é a comunicação com soquetes TCP. Por motivo de segurança, o Silverlight só permite as conexões com as portas de servidor no intervalo de 4502 a 4532. Esta é uma das muitas diretivas de segurança implementadas na área de segurança. Outra importante diretiva da área de segurança é que o Silverlight não pode ser um ouvinte e, portanto, não pode aceitar conexões de soquete de entrada. Por esses motivos, vou criar um servidor de soquete que escute na porta 4530 e manter um pool de conexões, sendo que cada conexão representará um representante de call center ativo.

O runtime de soquete do Silverlight também impõe diretivas opcionais entre domínios no servidor para todas as conexões de soquete. Quando o código do aplicativo do Silverlight tentar iniciar uma conexão com um ponto de extremidade IP em um número de porta permitido, opaco para o código do usuário, o runtime estabelecerá uma conexão com um ponto de extremidade IP no mesmo endereço IP com o número de porta 943. Esse número de porta é vinculado à implementação do Silverlight e não pode ser configurado por aplicativos nem alterado pelo desenvolvedor do aplicativo.

A Figura 1 mostra onde o servidor de diretivas se encaixa na arquitetura. Quando Socket.ConnectAsync é chamado, a seqüência de fluxo de mensagens é parecida com a ilustrada na Figura 3. Por design, as mensagens 2, 3 e 4 são totalmente opacas para o código do usuário.

fig03.gif

Figura 3 Diretiva entre domínios de solicitações do runtime do Silverlight aplicada automaticamente para conexões de soquete

Preciso implementar um servidor de diretivas no mesmo endereço IP do servidor do gerenciador de chamadas. Eu poderia implementar ambos os servidores em um único processo de SO, mas, para simplificar, implementarei os servidores em dois programas de console diferentes. Esses programas de console podem facilmente ser convertidos em serviços do Windows e ter o reconhecimento de cluster para failover, visando confiabilidade e disponibilidade.

Loops de E/S assíncronos

O .NET Framework 3.5 introduziu novas APIs de programação assíncrona para soquetes; elas são os métodos que terminam com Async(). Os métodos que usarei no servidor são Socket.AcceptAsync, Socket.SendAsync e Socket.ReceiveAsync. Os métodos assíncronos são otimizados para aplicativos de servidor de alta produtividade mediante o uso de Portas de Conclusão de E/S e gerenciamento eficiente do buffer de envio e recebimento através da classe reutilizável SocketAsyncEventArgs.

Pelo fato de o Silverlight não ter permissão para criar ouvintes TCP, sua classe Socket dá suporte apenas para ConnectAsync, SendAsync e ReceiveAsync. O Silverlight só aceita um modelo de programação assíncrona, e isso não se aplica apenas à API de soquete, mas a qualquer interação na rede.

Como usarei um modelo de programação assíncrona no servidor e no cliente, vamos nos familiarizar com os padrões de design. Um padrão de design recorrente é o loop de E/S, aplicável a todas as operações assíncronas. Primeiro, examinarei a execução síncrona típica do loop de aceitação de soquete, como visto aqui:

_listener.Bind(localEndPoint);
 _listener.Listen(50);
 while (true)
 {
    Socket acceptedSocket = _listener.Accept();
    RepConnection repCon = new 
      RepConnection(acceptedSocket);
    Thread receiveThread = new Thread(ReceiveLoop);
    receiveThread.Start(repCon);
 }

A aceitação síncrona é intuitiva e fácil de programar e manter; todavia, esta implementação não é escalonável para servidores porque há threads dedicados para cada conexão de cliente. Esta implementação poderá facilmente atingir o ponto máximo em algumas poucas conexões se elas forem muito prolixas.

Para o Silverlight funcionar bem com o ambiente de runtime do navegador, ele deve se intrometer nos recursos o mínimo possível. Todas as chamadas no pseudocódigo de "aceitação de soquete" mostrado anteriormente bloqueiam os threads em que elas executam e, por isso, têm um impacto negativo na escalabilidade. Por esse motivo, o Silverlight é bastante restritivo em chamadas de bloqueio e, na verdade, só permite a interação assíncrona com recursos de rede. Os loops assíncronos exigem que você ajuste o modelo mental para vislumbrar uma caixa de mensagem invisível que sempre deve ter no mínimo uma solicitação para que o loop funcione.

A Figura 4 mostra um loop de recebimento (há uma implementação mais completa no código para download). Não existem construções de programação de loop infinito como o loop while (true) que vimos anteriormente no pseudocódigo de aceitação de soquete síncrono. Acostumar-se a este tipo de programação é essencial para um desenvolvedor do Silverlight. Para que o loop de recebimento continue a receber dados, após uma mensagem ter sido recebida e processada, deve haver pelo menos uma solicitação na fila da Porta de Conclusão de E/S associada ao soquete conectado. Um loop típico está ilustrado na Figura 5 e é aplicável a ConnectAsync, ReceiveAsync e SendAsync. AcceptAsync pode ser adicionado a esta lista no servidor onde você usará o .NET Framework 3.5.

Figura 4 Loops de envio/recebimento assíncronos com soquetes do Silverlight

public class CallNetworkClient
{
   private Socket _socket;
   private ReceiveBuffer _receiveBuffer;

   public event EventHandler<EventArgs> OnConnectError;
   public event EventHandler<ReceiveArgs> OnReceive;
   public SocketAsyncEventArgs _receiveArgs;
   public SocketAsyncEventArgs _sendArgs;
//removed for space
    public void ReceiveAsync()
    {
       ReceiveAsync(_receiveArgs);
    }

    private void ReceiveAsync(SocketAsyncEventArgs recvArgs)
    {
       if (!_socket.ReceiveAsync(recvArgs))
       {
          ReceiveCallback(_socket, recvArgs);
       }
    }

    void ReceiveCallback(object sender, SocketAsyncEventArgs e)
    {
      if (e.SocketError != SocketError.Success)
      {
        return;
      }
      _receiveBuffer.Offset += e.BytesTransferred;
      if (_receiveBuffer.IsMessagePresent())
      {
        if (OnReceive != null)
        {
           NetworkMessage msg = 
                       NetworkMessage.Deserialize(_receiveBuffer.Buffer);
           _receiveBuffer.AdjustBuffer();
           OnReceive(this, new ReceiveArgs(msg));
        }
      }
      else
      {
        //adjust the buffer pointer
        e.SetBuffer(_receiveBuffer.Offset, _receiveBuffer.Remaining);
      }
      //queue an async read request
      ReceiveAsync(_receiveSocketArgs);
    }
    public void SendAsync(NetworkMessage msg) { ... }

    private void SendAsync(SocketAsyncEventArgs sendSocketArgs)  
    { 
    ... 
    }

     void SendCallback(object sender, SocketAsyncEventArgs e)  
    { 
    ... 
    }
   }

fig05.gif

Figura 5 Padrão de loop de soquete assíncrono

Na implementação do loop de recebimento mostrada na Figura 4, ReceiveAsync é um wrapper para o método reentrante ReceiveAsync(SocketAsyncEventArgs recvArgs) que enfileirará a solicitação na porta de conclusão de E/S do soquete. SocketAsyncEventArgs, introduzido no .NET Framework 3.5, tem uma função similar na implementação de soquete do Silverlight e pode ser reutilizado entre várias solicitações, evitando o transtorno da coleta de lixo. Será responsabilidade da rotina de retorno de chamada extrair a mensagem, disparar um evento de processamento de mensagem e enfileirar o próximo item de recebimento para dar continuidade ao loop.

Para lidar com casos de recebimento parcial de mensagens, ReceiveCallback ajusta o buffer antes de enfileirar uma solicitação. NetworkMessage é encapsulado em uma instância de ReceiveArgs e passado para o manipulador de eventos externo a fim de processar a mensagem recebida.

O buffer é zerado a cada recebimento completo de NetworkMessage após a cópia da mensagem parcial, se houver, para o início do buffer. Um design semelhante é usado no servidor, mas as implementações reais podem se beneficiar de buffers circulares.

Para implementar o cenário de "Aceitação de Chamada", você precisa criar uma arquitetura de mensagem extensível que permita serializar e desserializar mensagens de conteúdo arbitrário sem ter de reescrever a lógica de serialização para cada nova mensagem.

fig06.gif

Figura 6 Layout dos tipos NetworkMessage serializados

A arquitetura de mensagem é bastante simples: cada objeto filho de NetworkMessage declara sua assinatura no momento da instanciação com o MessageAction apropriado. As implementações de NetworkMessage.Serialize e Deserialize funcionarão no Silverlight e no .NET Framework 3.5 (no servidor) graças à compatibilidade no nível de código-fonte. A mensagem serializada terá o layout mostrado na Figura 6.

Em vez de inserir o comprimento no início da mensagem, você pode usar os marcadores "begin" e "end" com as seqüências de escape apropriadas. Codificar o comprimento na mensagem é muito mais simples para o processamento dos buffers.

Os quatro primeiros bytes de cada mensagem serializada incluirão o número de bytes do objeto serializado após os 4 bytes. O Silverlight suporta o XmlSerializer localizado na System.Xml.dll que faz parte do SDK do Silverlight. O código de serialização está incluído no código para download. Você perceberá que ele não tem dependências diretas das classes filhas, como RegisterMessage, nem de outras mensagens que incluem UnregisterMessage e AcceptMessage. Uma série de anotações XmlInclude ajudará o serializador a resolver os tipos .NET corretamente e, ao mesmo tempo, serializar as classes filhas.

O uso de NetworkMessage.Serialize e Deserialize é mostrado em ReceiveCallback e SendAsync na Figura 4. No loop de recebimento, o processamento de mensagens propriamente dito é feito pelo manipulador de eventos vinculado ao evento NetworkClient.OnReceive. Eu poderia ter processado a mensagem dentro de CallNetworkConnection, mas conectar o manipulador de recebimento para processar a mensagem ajudará na extensibilidade através da separação do manipulador de CallNetworkConnection no tempo de design.

A Figura 7 mostra o aplicativo do Silverlight RootVisual, que inicia CallNetworkClient (ilustrado na Figura 4). Todos os controles do Silverlight estão conectados a um único thread de interface do usuário, e qualquer atualização da interface do usuário só poderá ser feita quando o código executar no contexto desse thread. O modelo de programação assíncrona do Silverlight executa o código de acesso à rede e os manipuladores de processamento nos threads de trabalho do pool de threads. Todas as classes derivadas FrameworkElement (como Control, Border, Panel e a maioria dos elementos da interface do usuário) herdam a propriedade Dispatcher (de DispatcherObject), que pode executar código no thread de interface do usuário.

Na Figura 7, a ocorrência MessageAction.RegisterResponse atualizará a interface do usuário com os detalhes de turno do call center através de um delegado anônimo. A interface do usuário atualizada resultante da execução pelo delegado é mostrada na Figura 8.

Figura 7 UserControl do Silverlight que processa as mensagens recebidas

public partial class Page : UserControl
{
  public Page()
  {
    InitializeComponent();
    ClientGlobals.socketClient = new CallNetworkClient();
    ClientGlobals.socketClient.OnReceive += new 
                         EventHandler<ReceiveArgs>(ReceiveCallback);
    ClientGlobals.socketClient.Connect(4530);
    //code omitted for brevity
  }
  void ReceiveCallback(object sender, ReceiveArgs e)
  {
    NetworkMessage msg = e.Result;
    ProcessMessage(msg);
  }
  void ProcessMessage(NetworkMessage msg)
  {
    switch(msg.GetMessageType())
    {
      case MessageAction.RegisterResponse:
           RegisterResponse respMsg = msg as RegisterResponse;
           //the if is unncessary as the code always executes in the 
           //background thread
           this.Dispatcher.BeginInvoke(
              delegate()
              {
                 ClientGlobals.networkPopup.CloseDialog();
                 this.registrationView.Visibility = Visibility.Collapsed;
                 this.callView.Visibility = Visibility.Visible;
                 this.borderWaitView.Visibility = Visibility.Visible;
                 this.tbRepDisplayName.Text = this.txRepName.Text;
                 this.tbRepDisplayNumber.Text = respMsg.RepNumber;
                 this.tbCallServerName.Text = 
                                      respMsg.CallManagerServerName;
                 this.tbCallStartTime.Text = 
                                respMsg.RegistrationTimestamp.ToString(); 
              });
            break;
      case MessageAction.Call:
           CallMessage callMsg = msg as CallMessage;
       //Code omitted for brevity
           if (!this.Dispatcher.CheckAccess())
           {
              this.Dispatcher.BeginInvoke(
                 delegate()
                 { 
                    ClientGlobals.notifyCallPopup.ShowDialog(true); 
                 });
           }
           break;
           //
           //Code omitted for brevity  
           //
      default:
             break;
    }
  }
}

fig08.gif

Figura 8 Tela inicial de registro do representante

fig08.gif

Figura 9 O registro no servidor do call center está em andamento

Caixas de diálogo restritas no Silverlight

Quando um representante do call center fizer logon, ele terá de iniciar o turno registrando-se no servidor do call center. O processo de registro no servidor salvará a sessão indexada pelo número do representante. Esta sessão será usada para preenchimento subseqüente da tela e outras notificações. A transição de tela do aplicativo de call center para o processo de registro é mostrada nas Figuras 8 e 9. Usarei uma caixa de diálogo restrita que mostre o andamento do envio pela rede. Os aplicativos LOB empresariais típicos usam caixas de diálogo pop-up, restritas e não restritas, muito livremente. Como não há um DialogBox interno no SDK do Silverlight, você aprenderá a desenvolver um no Silverlight para usá-lo neste aplicativo.

Até o Silverlight, não havia uma forma simples de criar caixas de diálogo restritas, como também não havia um jeito fácil de impedir que eventos de teclado fossem colocados na interface do usuário. A interação com o mouse pode ser desabilitada indiretamente configurando UserControl.IsTestVisible = false. A partir do RC0, configurar Control.IsEnabled = false impede que os controles da interface de usuário recebam eventos de teclado ou de mouse. Usarei System.Windows.Controls.Primitives.Popup para exibir a interface do usuário da caixa de diálogo acima do controle existente.

A Figura 10 mostra um controle SLDialogBox básico com os métodos abstratos GetControlTree, WireHandlers e WireUI. Esses métodos serão substituídos pelas classes filhas, conforme ilustrado na Figura 11. Primitives.Popup requer uma instância de controle que não faz parte da árvore de controles à qual Popup será anexado. No código da Figura 10, o método ShowDialog(true) desabilitará toda a árvore de controles recursivamente para que nenhum dos controles contidos receba eventos de mouse ou de teclado. Como a minha caixa de diálogo deve ser interativa, Popup.Child deve ser definido a partir de uma nova instância de controle. A implementação de GetControlTree nas classes filhas funcionará como um alocador de controle e fornecerá uma nova instância de controle de usuário apropriado para os requisitos de interface de usuário da caixa de diálogo.

Figura 10 Caixa de diálogo pop-up no Silverlight

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace SilverlightPopups
{
    public abstract class SLDialogBox
    {
        protected Popup _popup = new Popup();
        Control _parent = null;
        protected string _ caption = string.Empty;
        public abstract UIElement GetControlTree();
        public abstract void WireHandlers();
        public abstract void WireUI();

        public SLDialogBox(Control parent, string caption)
        {
            _parent = parent;
            _ caption = caption;
            _popup.Child = GetControlTree();
            WireUI();
            WireHandlers();
            AdjustPostion();

        }
        public void ShowDialog(bool isModal)
        {
            if (_popup.IsOpen)
                return; 
            _popup.IsOpen = true;
            ((UserControl)_parent).IsEnabled = false;
        }
        public void CloseDialog()
        {
            if (!_popup.IsOpen)
                return; 
            _popup.IsOpen = false;
            ((UserControl)_parent).IsEnabled = true;
        }
        private void AdjustPostion()
        {
            UserControl parentUC = _parent as UserControl;
            if (parentUC == null) return; 

            FrameworkElement popupElement = _popup.Child as FrameworkElement;
            if (popupElement == null) return;

            Double left = (parentUC.Width - popupElement.Width) / 2;
            Double top = (parentUC.Height - popupElement.Height) / 2;
            _popup.Margin = new Thickness(left, top, left, top);
        }
    }
}

Figura 11 Capa NotifyCallPopup.xaml

//XAML Skin for the pop up
<UserControl 
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" 
  Width="200" Height="95">
   <Grid x:Name="gridNetworkProgress" Background="White">
     <Border BorderThickness="5" BorderBrush="Black">
       <StackPanel Background="LightGray">
          <StackPanel>
             <TextBlock x:Name="tbCaption" HorizontalAlignment="Center" 
                        Margin="5" Text="&lt;Empty Message&gt;" />
             <ProgressBar x:Name="progNetwork" Margin="5" Height="15" 
                        IsIndeterminate="True"/>
          </StackPanel>
          <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" >
             <Button x:Name="btAccept"  Margin="10,10,10,10"  
                      Content="Accept" HorizontalAlignment="Center"/>
             <Button x:Name="btReject"  Margin="10,10,10,10"  
                     Content="Reject" HorizontalAlignment="Center"/>
          </StackPanel>
       </StackPanel>
      </Border>
    </Grid>
</UserControl>

É possível implementar GetControlTree para instanciar um UserControl do Silverlight compilado no pacote do aplicativo, ou você pode criar um controle a partir de um arquivo XAML (eXtensible Application Markup Language) usando XamlReader.LoadControl. Normalmente, é possível implementar caixas de diálogo facilmente em capas às quais os manipuladores compilados podem ser anexados no tempo de execução. A Figura 11 mostra uma capa XAML que tem os botões btAccept e btReject. O método LoadControl irá gerar uma exceção se você deixar o atributo de classe (<userControl class="AdvCallCenter.NotifyCallPopup"…>…</UserControl>) na XAML após a tarefa de design do Microsoft Expression Studio ou do Visual Studio. Qualquer atributo de manipulador de eventos da interface do usuário deve ser removido para uma bem-sucedida análise usando LoadControl.

Para criar capas, você pode adicionar o UserControl do Silverlight ao projeto, projetá-lo no Expression e remover o atributo "class" e os nomes de manipuladores de eventos, se houver, anexados aos controles do arquivo XAML. Os manipuladores de cliques podem fazer parte da classe pop-up filha, como ilustrado na Figura 12 ou, de maneira alternativa, é possível criar uma biblioteca de manipuladores à parte que pode ser conectada aos controles usando reflexão.

Figura 12 Implementação de NotifyCallPopup

public class NotifyCallPopup : SLDialogBox
{
   public event EventHandler<EventArgs> OnAccept;
   public event EventHandler<EventArgs> OnReject;
   public NotifyCallPopup(Control parent, string msg)
        : base(parent, msg)
   {
   }
   public override UIElement GetControlTree()
   {
      Return SLPackageUtility.GetUIElementFromXaml("NotifyCallPopup.txt");
   }
   public override void WireUI()
   {
      FrameworkElement fe = (FrameworkElement)_popup.Child;
      TextBlock btCaption = fe.FindName("tbCaption") as TextBlock;
      if (btCaption != null)
          btCaption.Text = _caption;
      }
   public override void WireHandlers()
   {
      FrameworkElement fe = (FrameworkElement)_popup.Child;
      Button btAccept = (Button)fe.FindName("btAccept");
      btAccept.Click += new RoutedEventHandler(btAccept_Click);

      Button btReject = (Button)fe.FindName("btReject");
      btReject.Click += new RoutedEventHandler(btReject_Click);
   }

   void btAccept_Click(object sender, RoutedEventArgs e)
   {
      CloseDialog();
      if (OnAccept != null)
          OnAccept(this, null);
   }
   void btReject_Click(object sender, RoutedEventArgs e)
   {
      CloseDialog();
      if (OnReject != null)
         OnReject(this, null);
   }
}

Os manipuladores podem ficar em qualquer projeto de biblioteca do Silverlight, uma vez que serão automaticamente compilados no pacote XAP devido à dependência de projeto. Para que os arquivos de capa sejam incluídos no pacote XAP, adicione-os ao projeto Silverlight como arquivos XML e mude a extensão para XAML. A ação de compilação padrão de arquivos com extensão XAML é compilá-los na DLL do aplicativo. Como eu quero que esses arquivos sejam empacotados como arquivos de texto, devemos configurar os seguintes atributos das janelas Propriedades:

  • BuildAction = "Content"
  • Copy to Output Directory = "Do Not Copy"
  • Custom Tool = <apagar qualquer valor existente>

O analisador XAML (XamlReader.Load) não está preocupado com a extensão, mas usar a extensão .xaml será mais intuitivo e representativo do conteúdo. SLDialogBox só é responsável por mostrar e fechar a caixa de diálogo. As implementações filhas serão personalizadas para se adequar às necessidades do aplicativo.

Implementação de notificações de envio

Um aplicativo de call center deve ser capaz de fazer um preenchimento de tela com as informações do chamador. O dia de trabalho em um call center começa com o representante se registrando no servidor do call center. As notificações de envio são implementadas com soquetes orientados a conexão. A implementação completa do servidor do gerenciador de chamadas não é mostrada nas figuras, mas está disponível no código para download. Quando o cliente do Silverlight faz uma conexão de soquete no servidor, um novo objeto RepConnection é adicionado a RepList. RepList é uma lista geral indexada por um Número de Representante exclusivo. Quando uma chamada for recebida, você usará esta lista para encontrar um representante disponível e, utilizando a conexão de soquete associada a RepConnection, notificará o agente com as informações sobre a chamada. RepConnection usa ReceiveBuffer, como ilustrado na Figura 13.

Figura 13 RepConnection usa ReceiveBuffer

class SocketBuffer
{
  public const int BUFFERSIZE = 5120;
  protected byte[] _buffer = new byte[BUFFERSIZE]
  protected int _offset = 0;
  public byte[] Buffer
  {
    get { return _buffer; }
    set { _buffer = value; }
  }

 //offset will always indicate the length of the buffer that is filled
  public int Offset
  {
    get {return _offset ;}
    set { _offset = value; }
  }

  public int Remaining
  {
    get { return _buffer.Length - _offset; }
  }
}
class ReceiveBuffer : SocketBuffer
{
  //removes a serialized message from the buffer, copies the partial message
  //to the beginning and adjusts the offset
  public void AdjustBuffer()
  {
    int messageSize = BitConverter.ToInt32(_buffer, 0);
    int lengthToCopy = _offset - NetworkMessage.LENGTH_BYTES - messageSize;
    Array.Copy(_buffer, _offset, _buffer, 0, lengthToCopy);
    offset = lengthToCopy;
  }
  //this method checks if a complete message is received
  public bool IsMessageReceived()
  {
    if (_offset < 4)
       return false;
    int sizeToRecieve = BitConverter.ToInt32(_buffer, 0);
    //check if we have a complete NetworkMessage
    if((_offset - 4) < sizeToRecieve)
      return false; //we have not received the complete message yet
    //we received the complete message and may be more
      return true;
   }
 }

Você utilizará um simulador de chamadas do Silverlight para colocar uma chamada em CallDispatcher._callQueue e iniciar o processo de preenchimento da tela. CallDispatcher não aparece em nenhuma das figuras, mas está disponível no código para download. Ele anexa um manipulador a _callQueue.OnCallReceived e será notificado quando o simulador enfileirar a mensagem para _callQueue dentro da implementação de ProcessMessage. Aproveitando as caixas de diálogo pop-up mencionadas anteriormente, o cliente exibirá a notificação Aceitar/Rejeitar mostrada na Figura 14. Esta é a linha de código responsável por exibir a caixa de diálogo de notificação mostrada na Figura 8:

ClientGlobals.notifyCallPopup.ShowDialog(true);  

fig14.gif

Figura 14 Notificação de chamada recebida

Acesso entre domínios de serviços TCP

Diferentemente dos aplicativos de mídia e de veiculação de anúncios, os aplicativos LOB empresariais propriamente ditos exigem integração com diversos ambientes de hospedagem de serviços. Por exemplo, o aplicativo de call center está hospedado em um site (advcallclientweb hospedado em localhost:1041), usa um servidor de soquete com monitoração de estado em outro domínio (localhost:4230) para preenchimento de tela e acessa os dados LOB através de serviços hospedados em outro domínio (localhost:1043). Utilizarei ainda outro domínio para transmitir dados de instrumentação.

Por padrão, a área de segurança do Silverlight não permite acesso por rede a nenhum domínio que não seja o de origem: advcallclientweb (localhost:1041). Quando tal acesso via rede é detectado, o runtime do Silverlight verifica as diretivas opcionais estabelecidas pelo domínio de destino. Aqui está uma lista típica dos cenários de hospedagem de serviços que precisam dar suporte a solicitações de diretiva entre domínios feitas pelo cliente:

  • Serviços hospedados na nuvem
  • Web Services hospedados em um processo de serviço
  • Web Services hospedados no IIS ou em outros servidores Web
  • Recursos de HTTP, como marcação XAML e pacotes XAP
  • Serviços TCP hospedados em um processo de serviço

Embora a implementação de diretivas entre domínios para recursos de HTTP e pontos de extremidade de Web Service hospedados no IIS seja simples, os outros casos requerem o conhecimento da semântica de solicitação/resposta da diretiva. Nesta seção, farei uma breve implementação da infra-estrutura de diretiva necessária para o servidor de preenchimento de tela TCP, mencionado como Gerenciador de Chamadas na Figura 1. Os outros cenários entre domínios serão abordados na segunda parte deste artigo.

Diretivas entre domínios com serviços TCP

Qualquer acesso a serviços TCP no Silverlight é considerado uma solicitação entre domínios, e o servidor precisa implementar um ouvinte de TCP no mesmo endereço IP vinculado à porta 943. O servidor de diretivas mostrado na Figura 3 é o ouvinte implementado para esta finalidade. Este servidor implementa um processo de solicitação/resposta para transmitir diretivas declarativas pelo runtime do Silverlight antes de autorizar a pilha da rede no cliente a se conectar com o servidor de preenchimento de tela (o Gerenciador de Chamadas da Figura 3).

Para simplificar, hospedarei o servidor do gerenciador de chamadas em um aplicativo de console. Este aplicativo de console pode ser facilmente convertido em um serviço do Windows para implementações reais. A Figura 3 mostra a interação típica com um servidor de diretivas; o runtime do Silverlight se conectará a ele na porta 943 e enviará uma solicitação de diretiva que conterá apenas uma linha de texto: "<policy-file-request/>".

As diretivas baseadas em XML possibilitam o cenário mostrado na Figura 3. A seção sobre recursos de soquete pode especificar um grupo de portas dentro do intervalo permitido de 4502 a 4534. A lógica para se limitar a um intervalo é minimizar o vetor de ataque, atenuando assim o risco de deficiências acidentais na configuração do firewall. Como o servidor do call center (o Gerenciador de Chamadas da Figura 1) escuta na porta número 4530, o recurso de soquete é configurado dessa forma:

<access-policy>
   <policy>
     <allow-from> list of URIs</allow-from>
     <grant-to> <socket-resource port="4530" protocol="tcp"/></grant-to>
  </policy>     
</access-policy>

Também é possível configurar <socket-resource> para autorizar todos os números de porta permitidos especificando port="4502–4534".

Para ganhar tempo, vou realocar o código do servidor do gerenciador de chamadas na implementação do servidor de diretivas. Um cliente do Silverlight conecta-se ao servidor de diretivas, envia uma solicitação e lê a resposta. O servidor de diretivas encerra a conexão depois que a resposta da diretiva é enviada com êxito. O conteúdo da diretiva é lido pelo servidor de diretivas em um arquivo local, clientaccesspolicy.xml, incluso no download.

A implementação do ouvinte de TCP para servidores de diretivas é mostrada na Figura 15. Ela usa o mesmo padrão de loop assíncrono discutido anteriormente para aceitação de TCP. Clientaccesspolicy.xml é lido em um buffer e reutilizado para envio a todos os clientes do Silverlight. ClientConnection encapsula o soquete aceito e o buffer de recebimento que será associado a SocketAsyncEventArgs.

Figura 15 Implementação do servidor de diretivas TCP

class TcpPolicyServer
{
  private Socket _listener;
  private byte[] _policyBuffer;
  public static readonly string PolicyFileName = "clientaccesspolicy.xml";
  SocketAsyncEventArgs _socketAcceptArgs = new SocketAsyncEventArgs();
  public TcpPolicyServer()
  {
    //read the policy file into the buffer
    FileStream fs = new FileStream(PolicyServer.PolicyFileName, 
                        FileMode.Open);
    _policyBuffer = new byte[fs.Length];
    fs.Read(_policyBuffer, 0, _policyBuffer.Length);
    _socketAcceptArgs.Completed += new 
                 EventHandler<SocketAsyncEventArgs>(AcceptAsyncCallback);

  }
  public void Start(int port)
  {

    IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());
    //Should be within the port range of 4502-4532
    IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Any, port);

    _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, 
                                                       ProtocolType.Tcp);

    // Bind the socket to the local endpoint and listen for incoming connections 
    try
    {
      _listener.Bind(ipEndPoint);
      _listener.Listen(50);
      AcceptAsync();
    }
    //code omitted for brevity

   }
   void AcceptAsync()
   {
      AcceptAsync(socketAcceptArgs);
    }

    void AcceptAsync(SocketAsyncEventArgs socketAcceptArgs)
    {
      if (!_listener.AcceptAsync(socketAcceptArgs))
      {
         AcceptAsyncCallback(socketAcceptArgs.AcceptSocket, 
                                             socketAcceptArgs);
      }
    }

    void AcceptAsyncCallback(object sender, SocketAsyncEventArgs e)
    {
      if (e.SocketError == SocketError.Success)
      {
        ClientConnection con = new ClientConnection(e.AcceptSocket, 
                                               this._policyBuffer);
        con.ReceiveAsync();
      }
      //the following is necessary for the reuse of _socketAccpetArgs
      e.AcceptSocket = null;
      //schedule a new accept request
      AcceptAsync();
     }
   }

O exemplo de código mostrado na Figura 15 reutiliza SocketAsyncEventArgs entre várias aceitações de TCP. Para isso funcionar, e.AcceptSocket deve ser definido como null em AcceptAsyncCallback. Isso evitará transtornos de coleta de lixo no servidor com requisitos de alta escalabilidade.

Conclusão

A implementação de notificações de envio é um aspecto importante de um aplicativo de call center e possibilita a execução do processo de preenchimento de tela. O Silverlight torna a implementação do preenchimento de tela bem mais fácil do que o AJAX ou outras estruturas semelhantes. Como os modelos de programação de servidor e de programação de cliente são parecidos, consegui uma certa capacidade de reutilização do código-fonte. No caso do call center, pude usar definições de mensagem e abstrações de buffer de recebimento nas implementações de servidor e de cliente.

Na Parte 2 desta série, implementarei integração local de Web Services, segurança, integração com serviços de nuvem e particionamento do aplicativo. Espero que este esforço tenha bons resultados em alguns dos cenários de LOB com os quais você venha a se deparar e aguardo os seus comentários.

Gostaria de agradecer a Dave Murray e Shane DeSeranno da Microsoft pela orientação sobre as operações internas da implementação de soquete do Silverlight e a Robert Brooks, um especialista na área de call centers, sobre a discussão a respeito de preenchimento de tela.

Hanu Kommalapati é consultor em estratégia de plataforma da Microsoft e, em sua função atual, orienta clientes corporativos sobre o desenvolvimento de aplicativos LOB escalonáveis nas plataformas Silverlight e Azure Services.