Neste artigo você verá como é fácil fazer um chat em ASP.NET sem usar nenhuma biblioteca adicional, apenas o .NET Framework. Para isso vamos usar um recurso chamado callback.

Callback é uma nova característica do ASP.NET 2.0 que permite receber valores do servidor Web sem renderizar novamente a página. São feitas requisições XMLHTTP ao servidor, ao invés de HTTP.

Para trabalhar com Callback é necessário conhecimento básico em JavaScript para chamar o servidor via XMLHTTP e depois receber o retorno do mesmo. Mas espera aí! Requisições XML assíncronas! Usando JavaScript! Isso é AJAX! Na verdade o AJAX é callback e não o contrário. Callback pode ser considerado uma forma artesanal de AJAX, ou até mesmo o esqueleto de todo componente AJAX.

Quando um postback normal é realizado, é executada uma requisição HTTP, que é processada de acordo com os métodos da interface IPostbackEventHandler. A classe System.Web.UI.Page implementa essa interface. O ciclo de vida da página ao executar um postback normal é mostrado na Figura 1.

Ciclo de vida da página ao executar um postback

Figura 1. Ciclo de vida da página ao executar um postback

Agora, quando executamos um callback, chamamos uma função JavaScript que envia uma requisição XMLHTTP assíncrona ao servidor Web e são processados os métodos da interface ICallbackEventHandler. O ciclo de vida da página ao executar um callback é mostrado na Figura 2.

Ciclo de vida da página ao executar um callback

Figura 2. Ciclo de vida da página ao executar um callback

O chat deste artigo será composto por uma página com uma lista de salas de bate-papo cadastradas no banco. O usuário escolhe a sala e então é aberto um popup com um frameset com três frames. Um frame lateral para listar os usuários, um frame inferior para enviar mensagens e um central com as mensagens dos usuários do chat. O chat também terá a opção de espiar, onde o usuário apenas visualiza as mensagens.

Criando o banco de dados

Vamos primeiro criar o banco de dados para cadastrar as salas de bate-papo e logar as mensagens do chat. Abra o SQL Server Management Studio ou utilize o IDE do Visual studio 2005 e crie um novo banco de dados chamado “DBCHAT”. Depois rode o script da Listagem 1.

Listagem 1. Script do banco

USE DBCHAT
  
 SET ANSI_NULLS ON
 GO
 SET QUOTED_IDENTIFIER ON
 GO
 SET ANSI_PADDING ON
 GO
 CREATE TABLE [dbo].[CHAT](
   [COD_CHAT] [int] IDENTITY(1,1) NOT NULL,
   [NOM_CHAT] [varchar](100)  NULL,
   [DSC_CHAT] [text]  NULL,
   [MAX_USERS] [int] NULL,  
   [DAT_CHAT] [datetime] NULL 
  CONSTRAINT [DF_CHAT_DAT_CHAT]  DEFAULT (getdate()),
  CONSTRAINT [PK_CHAT] PRIMARY KEY CLUSTERED 
 (
   [COD_CHAT] ASC
 ) ON [PRIMARY]
 ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
  
 GO
 SET ANSI_PADDING OFF
  
 SET ANSI_NULLS ON
 GO
 SET QUOTED_IDENTIFIER ON
 GO
 SET ANSI_PADDING ON
 GO
 CREATE TABLE [dbo].[CHAT_MENSAGEM](
   [COD_CHAT_MENSAGEM] [int] IDENTITY(1,1) NOT NULL,
   [DAT_MENSAGEM] [datetime] NULL,
   [NICK_AUTOR] [varchar](1000)  NULL,
   [DSC_MENSAGEM] [text]  NULL,
   [RESERVADO] [bit] NULL,
   [NICK_DESTINO_RESERVADO] [varchar](1000)  NULL,
   [COD_CHAT] [int] NOT NULL
 ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
  
 GO
 SET ANSI_PADDING OFF
  
 SET ANSI_NULLS ON
 GO
 SET QUOTED_IDENTIFIER ON
 GO
  
 CREATE PROCEDURE [dbo].[SP_CHAT_SALAS_LISTAR]
   
 AS
 BEGIN
  
 SET NOCOUNT ON;
  
 SELECT [COD_CHAT],[NOM_CHAT],[DSC_CHAT],[MAX_USERS],
  [DAT_CHAT]
   FROM [dbo].[CHAT]
   ORDER BY
    [NOM_CHAT]
 END
  
  
 SET ANSI_NULLS ON
 GO
 SET QUOTED_IDENTIFIER ON
 GO
  
 CREATE PROCEDURE [dbo].[SP_CHAT_INSERIR_MENSAGEM]
    @DAT_MENSAGEM DATETIME, 
   @NICK_AUTOR varchar(1000),@DSC_MENSAGEM text,
   @RESERVADO bit,
   @NICK_DESTINO_RESERVADO varchar(1000) = null,
   @COD_CHAT int
 AS
 BEGIN
   SET NOCOUNT ON;
  
 INSERT INTO [dbo].[CHAT_MENSAGEM]
   ([DAT_MENSAGEM],[NICK_AUTOR],[DSC_MENSAGEM],
   [RESERVADO],[NICK_DESTINO_RESERVADO],[COD_CHAT])
 VALUES
  (@DAT_MENSAGEM,@NICK_AUTOR,@DSC_MENSAGEM,@RESERVADO,
   @NICK_DESTINO_RESERVADO,@COD_CHAT)
 END
 SET ANSI_NULLS ON
 GO
 SET QUOTED_IDENTIFIER ON
 GO
 CREATE PROCEDURE [dbo].[SP_CHAT_SALAS_GET]
   @COD_CHAT int 
 AS
 BEGIN
   SET NOCOUNT ON;
  
   SELECT [COD_CHAT],[NOM_CHAT],[DSC_CHAT],[MAX_USERS],
   [DAT_CHAT]
   FROM [dbo].[CHAT]
   WHERE
    COD_CHAT = @COD_CHAT
 END

Nesse script criamos duas tabelas, uma para as salas de bate papo e outra para as mensagens de cada sala. Também criamos três Stored Procedures (SP). Uma para listar as salas on-line, uma para retornar informações de uma sala específica e outra para inserir mensagens do bate-papo, para ficarem armazenadas.

Não criamos uma SP para retornar as mensagens do chat pois elas serão guardadas em uma variável de aplicação on-line, ou seja, quando o usuário ver a mensagem no chat, ela não está sendo recuperada do banco, mas sim do servidor Web. Após rodar o script o banco de dados será semelhante a Figura 3.

Tabelas e Stored Procedures do banco

Figura 3. Tabelas e Stored Procedures do banco

Criando o projeto

Abra o Visual Studio 2005 e crie um novo projeto Web (File>New>Web Site), dando o nome de “Chat”, usando a linguagem C#. No Solution Explorer clique com o botão direito no projeto e escolha Add ASP.NET Folder>App_Code.

Dentro da pasta App_Code crie uma nova pasta chamada “Chat”. Dentro dessa crie as seguintes classes (menu Website>Add New Item>Class): “ChatManager.cs”, “ChatRoom.cs”, “ChatUser.cs” e “ChatMessage.cs”. Essas classes serão o núcleo do chat. A classe ChatManager é a principal, pois controlará todo o fluxo de mensagens e usuários de todas as salas.

A classe ChatRoom representa uma sala de bate-papo e contém uma fila (Queue) de ChatMessage e uma lista (List) de ChatUser. Nessas duas propriedades usaremos Generics, um novo conceito do C# 2.0. A ChatUser representa um usuário em uma ChatRoom, e a ChatMessage representa uma mensagem enviada ao chat.

Depois volte ao nível de pasta do projeto, e crie uma nova pasta chamada “Controls”. Dentro dessa vamos criar os seguintes User Controls (no momento vamos apenas criá-los, depois vamos codificá-los), clicando com o botão direito sob a pasta e escolhendo Add New Item>Web User Control: “Enviar.ascx”, “Nick.ascx”, “ListaSalas.ascx”, “Mensagem.ascx” e “ListaUsuarios.ascx”.

Vamos usar UserControls, para deixar o chat mais flexível para mudanças e novas funcionalidades. Volte ao nível do projeto e vamos criar as seguintes páginas (no momento vamos apenas criá-las, depois vamos codificá-las): “Nick.aspx”, ”Chat.aspx”, “Usuarios.aspx”, ”Mensagem.aspx” e “Enviar.aspx”. Depois de criar os arquivos, o Solution Explorer deve estar semelhante a Figura 4.

Solution Explorer com os arquivos do projeto

Figura 4. Solution Explorer com os arquivos do projeto

Classes ChatUser e ChatMessage

Para começar vamos trabalhar na classe ChatUser. Abra o arquivo ChatUser.cs e coloque o seguinte código:

public class ChatUser
 {
    //Nick escolhido pelo usuário
    public string Nick;
 }

Depois vamos alterar a classe que representa uma mensagem de chat, a ChatMessage. Abra o arquivo ChatMessage.cs e adicione código da Listagem 2.

Listagem 2. Alterando a classe ChatMessage

using System;
 public class ChatMessage
 {
    public ChatMessage()
    {
      Codigo = DateTime.Now.Ticks;
    }
    public ChatMessage(long lngCodigo)
     {
       Codigo = lngCodigo;
     }
  
     public long Codigo;
   public ChatUser Autor = new ChatUser();
     public DateTime Data;
     public bool Reservado;
     public ChatUser DestinoReservado = null;
     public string Message;
 }

A classe ChatMessage contém os seguintes atributos públicos:

  • Codigo: A classe tem dois construtores, um onde o código é carregado automaticamente com DateTime.Now.Ticks para identificar o milisegundo exato que a mensagem foi criada. No outro construtor, o código é passado por parâmetro;
  • Autor, DestinoReservado e Reservado: O Autor nunca é nulo, porém o DestinoReservado recebe nulo quando a mensagem não é reservada. A propriedade Reservado indica se a mensagem é reservada;
  • Mensagem: A string da mensagem.

Classe ChatManager

Agora vamos codificar a principal classe do sistema, a ChatManager (ChatManager.cs), pois é nela que teremos todo o controle e mecanismo do chat. Como era de se esperar, essa classe é bastante extensa (Listagem 3). Por restrições de espaço, disponibilizamos o código completo para download, aqui mantive apenas as partes principais necessárias ao bom entendimento.

Listagem 3. Código da classe ChatManager

...
 using System.Collections.Generic;
 using System.Data.SqlClient;
  
 public class ChatManager
 {
    private const string 
     SP_CHAT_ROOM_LIST = "SP_CHAT_SALAS_LISTAR";
    private const string 
     SP_CHAT_ROOM_GET = 
     "SP_CHAT_SALAS_GET @COD_CHAT={0}";
    private const string SP_CHAT_INSERT_MESSAGE =
      "SP_CHAT_INSERIR_MENSAGEM @DAT_MENSAGEM='{0}', "+
     "@NICK_AUTOR='{1}',@DSC_MENSAGEM='{2}', "+
     "@RESERVADO={3},@NICK_DESTINO_RESERVADO='{4}', "+
     "@COD_CHAT={5} ";
    private const string 
     IDENTIFICADOR_CHAT_ROOM_IN_APPLICATION = 
     "CHATROOM_{0}";
    public const string SESSION_ID_CHAT = "idChatRoom";
    public const string SESSION_NICK_CHAT = "nick";
  
   public static List getChatRoomList()
    {
      //lista para retorno
      List chatList = new List();
      //acessa o banco e retorna um Dataset com as 
     //Salas de Bate-papo
      SqlDataAdapter Adapter = new SqlDataAdapter(
       SP_CHAT_ROOM_LIST, ConfigurationManager.
       ConnectionStrings[1].ConnectionString);
      DataSet objDataSet = new DataSet();
      Adapter.Fill(objDataSet);
      //Percorre o DataSet retornado do banco, 
     //e preencha a lista 
      foreach (DataRow row in objDataSet.Tables[0].Rows)
      {
       ChatRoom room = getChatRoomById(
         Convert.ToString(row["COD_CHAT"]), true);
       chatList.Add(room);
      }
      return chatList;
   }
  
   public static ChatRoom getChatRoomById(
    string id, bool PreencherNumeroUsuariosOnline)
 ...
  
 public static ChatRoom CurrentChatRoom
 {
   get
   {
     //Se está espiando, então busca a sala da 
     //QueryString e não da Session
       if (HttpContext.Current.Request.QueryString[
       "Espiar"] != null)
       {
         //Busca a QueryString
      if (HttpContext.Current.Request.QueryString[
        "id"] != null)
         {
         string id = HttpContext.Current.
           Request.QueryString["id"];
         return GetChatRoomObjectFromApplication(
           id, true);
         }
          //Se está espiando, e não existe a 
      //QueryString Id, então retorna null, 
      //indicando que não existe a Sala Atual
         else
         {
            return null;
         }
      }
       //Busca a queryString
    else if (HttpContext.Current.Request.QueryString[
      "id"] != null)
      {
      string id = HttpContext.Current.Request.
        QueryString["id"];
      return GetChatRoomObjectFromApplication(
        id, true);
      }
       //Se não está espiando, então busca o código da 
    //sala da Session
    else if (HttpContext.Current.Session[
      SESSION_ID_CHAT] != null)
      {
      string id = HttpContext.Current.Session[
        SESSION_ID_CHAT].ToString();
      return GetChatRoomObjectFromApplication(
        id, true);
      }
    //Se não está espiando, e não existe Session,
    //então retorna null, indicando que não existe 
    //a Sala Atual
      else
      {
         return null;
      }
 }
   set
    {
     //Busca o Código da sessão, aqui não é preciso 
     //preocupação com a funcionalidade Espiar, 
     //pois é desnecessário
     string id;
     if (HttpContext.Current.Session[
       SESSION_ID_CHAT] != null)
     {
       id = HttpContext.Current.Session[
         SESSION_ID_CHAT].ToString();
     }
     else
     {
        throw new Exception(
          "Por favor, informe o Código da "+
          "Sala de Bate-papo");
      }
      //Acrescenta a sala na variável de Application
    HttpContext.Current.Application[
      string.Format(
      IDENTIFICADOR_CHAT_ROOM_IN_APPLICATION, 
      id)] = value;
   }
 }
  
 public static string GetUserListInCurrentChatRoom(
   bool TrazCurrentUser, bool DecodificaHTML)
 {
   string r = "";
   foreach (ChatUser user in 
     CurrentChatRoom.ChatUserList)
   {
     //se é usuário atual
     if (user.Nick == 
       CurrentChatRoom.CurrentChatUser.Nick)
     {
       //se é o usuário atual entra na lista
       if (TrazCurrentUser)
       {
         r += (user.Nick) + "§";
       }
     }
     else
     {
       r += (user.Nick) + "§";
     }
   }
   // se existe algum usuário
   if (r.Length > 0)
     //tira última vírgula
     r = r.Substring(0, r.Length - 1);
     //se é para decodificar o HTML, no caso de um 
     //Textbox, ComboBox ou Textarea
     if (DecodificaHTML)
     {
       r = HttpUtility.HtmlDecode(r);
     }
     return r;
 }
 ...

No início da classe declaramos algumas variáveis que representam Stored Procedures no banco de dados. Também criamos variáveis para identificar as variáveis de sessão e aplicação, onde armazenamos as mensagens e os usuários on-line das salas de bate-papo. Quase todos os métodos dessa classe são estáticos.

O getChatRoomList busca do banco as salas de bate-papo cadastradas e retorna uma List. O getChatRoomById(string id, bool PreencherNumeroUsuariosOnline) retorna uma sala de bate-papo específica.

Uma propriedade importante é a CurrentChatRoom que representa a sala de bate-papo que o usuário escolheu, essa variável encapsula uma variável de sessão. O GetUserListInCurrentChatRoom retorna a lista de usuários separados por “§” para que no callback de JavaScript possamos recuperar a lista.

O RemoveChatUserFromCurrentChatRoomByNick exclui o usuário da sala de bate-papo atual, o getNumberUsuariosOnLineByCodigo retorna o número de usuários on-line de uma determinada sala de bate-papo a partir do código passado como parâmetro.

O GetNewMessagesForUserInCurrentChatRoom retorna uma lista de mensagens que o usuário atual ainda não viu, na parte do cliente. Existe uma variável com o código da última mensagem que o usuário recebeu, assim o usuário chama esse método passando o código, e então recebe todas as mensagens mais novas.

Para isso funcionar, existe a propriedade CurrentChatRoom.ChatMessageQueue que é uma fila que guarda as mensagens, utilizando Generics. O SendMessageInCurrentChatRoom acrescenta a mensagem que o usuário enviou na fila de mensagens da sala de bate-papo atual.

Nesse método limitamos o tamanho da fila a 30 mensagens. Com isso se um usuário entrar em uma conversa já existente, ele conseguirá ver apenas as últimas 30 mensagens. Se o seu servidor tiver uma quantidade grande de memória, pode aumentar esse número. Nesse método verificamos o Web.config, e se configurado, logamos no banco a mensagem do usuário.

Classe ChatRoom

Agora vamos codificar a classe ChatRoom, abrindo o arquivo ChatRoom.cs e adicionando código da Listagem 4.

Listagem 4. Classe ChatRoom

...
 using System.Collections.Generic;
  
 public class ChatRoom
 {
   public int Codigo;
   public string Nome;
    public string Descrição;
    public int MaximoUsuarios;
    public DateTime Data;
    public int UsuariosOnLine;
    public List ChatUserList;
    public Queue ChatMessageQueue = 
     new Queue(30);
    public ChatUser CurrentChatUser
    {
      get
      {
       string apelido;
       //Se está espiando, então não há usuário atual, 
       //e retornamos um objeto instanciado sem dados
       if (HttpContext.Current.Request.QueryString[
         "Espiar"] != null)
       {
          return new ChatUser();
       }
       //se não está espiando, então busca a Session
       else if (HttpContext.Current.Session[
         ChatManager.SESSION_NICK_CHAT] != null )
       {
          apelido = HttpContext.Current.Session[
            ChatManager.SESSION_NICK_CHAT].ToString();
          return ChatManager.
            GetChatUserByIdInCurrentChatRoom(apelido);
       }
       //Se não está na Session, então a segunda opção é 
       //verificar se existe um POST com o apelido.
       //A segunda opção acontecerá quando essa 
       //propriedade for acessada da tela de Nick
       else if (HttpContext.Current.Request.Form[
         "txtApelido"] != null )
       {
          apelido = HttpContext.Current.Request.Form[
            "txtApelido"].Replace("'", "");
          return ChatManager.
            GetChatUserByIdInCurrentChatRoom(apelido);
       }
       else
       {
          return null;
       }
    }
   }
 }

Nessa classe, a principal propriedade é a CurrentChatUser, que encapsula uma variável de sessão que guarda o usuário atual da sala de bate papo. A propriedade ChatMessageQueue é usada na classe ChatManager para guardar as mensagens da sala de bate papo.

Controle de lista de salas

Agora vamos codificar o controle que lista as salas de bate-papo disponíveis. Para isso, abra o arquivo ListaSalas.ascx e coloque o código da Listagem 5.

Listagem 5. Arquivo ListaSalas

...
<div id="chat" >
<ul>
  foreach (ChatRoom room in ChatRoomList)
  {
  %>
  <li>
  <span>   
    (/
    )
  </span>
  <a id="AncoraEspiar"
    href="javascript:EspiarChat();">
    espiar
 </a>
   <a id="AncoraEntrar"
    </href="javascript:EntrarChat(,
    ,
    );">
    Entrar
  </a>
 </li>
 
   <script type="text/javascript">
     //se usuários on-line já lotaram a sala
     if (==
      )
     {
        document.getElementById(
         'AncoraEntrar').style.display =
         'none';
     }
     //Se a sala não tem usuários OnLine
     if (==0)
     {
        document.getElementById(
         'AncoraEspiar').style.display =
         'none';
     }
    </script>
  
  </ul>
  </div>
  <script type="text/javascript">
    //Redireciona o usuário para espiar um chat
    function EspiarChat(codigo)
    {
        var urlespiar =
          'chat.aspx?Nick=Espiao&Espiar=true&id=' +
          codigo;
        window.open(urlespiar, 'EspiarChat',
          'toolbar=0, statusbar=0, location=0, '+
          'menubar=0, scrollbars=no, width=800px, '+
          'height=600px');
    }
    //redireciona o usuário para colocar seu nick
    function EntrarChat(codigo)
    {
        var urleEntrar = 'nick.aspx?id=' + codigo;
        window.open(urleEntrar, 'Chat', 'toolbar=0, '+
          'statusbar=0, location=0, menubar=0, '+
          'scrollbars=no, width=380px, height=170px');
    }
</script>

No controle de salas, escrevemos as listas no HTML do UserControl a partir dos métodos da classe ChatManager e acrescentamos duas funções JavaScript, para abrir a popup do chat em forma de espiar ou em forma de participar. Tecle F7 (ou menu View>Code) para visualizar o código e acrescente o código da Listagem 6.

Listagem 6. Código do ListaSalas.ascx.cs

...
 using System.Collections.Generic;
  
 public partial class Controls_ListaSalas : UserControl
 {
   public List ChatRoomList = 
     new List();
  
   protected void Page_Load(object sender, EventArgs e)
   {
      //Se não é a primeira vez que carrega a página
      if (!IsPostBack)
      {
         ChatRoomList = ChatManager.getChatRoomList();
      }
   }
 }

No código anterior, usamos o método da classe ChatManager para preencher uma lista de salas cadastradas no banco de dados. Agora vamos adicionar o controle de lista de salas na página Default.aspx, apenas arrastando o mesmo do Solution Explorer para a página.

Ajustando o Web.Config

Falta apenas um passo para podermos ver funcionando nosso controle, vamos preparar o Web.config. Abra o arquivo e acrescente o código da Listagem 7 (se ainda não existir o arquivo, clique em WebSite>Add New Item>Web Configuration File).

Listagem 7. Alterando o Web.config

<appSettings>
   <add key="FazLogMensagensChatNoBanco"
    value="true"/>
  appSettings>
  <connectionStrings>
   <add name="ChatStringConnetion"
    connectionString="Server=.\SQLEXPRESS;
    Database=DBCHAT;Integrated Security=True"/>
  </connectionStrings>

Acrescentamos uma AppSetting indicando se deve ser feito o log das mensagens de chat no banco de dados, e também incluímos uma string de conexão apontando para o banco que criamos no início do artigo. Agora, acesse o banco de dados e inclua duas salas de bate papo na tabela Chat (semelhante aos itens da Figura 5). Rode a aplicação e veja a mesma em execução na Figura 6.

Adicionando dados na tabela Chat

Figura 5. Adicionando dados na tabela Chat

Aplicação em execução

Figura 6. Aplicação em execução

Controle de nick

Agora que já temos a página inicial que lista as salas de bate-papo e abre uma popup para o usuário digitar o nick, vamos codificar o controle de nick. Abra o arquivo Nick.ascx e adicione o código da Listagem 8.

Listagem 8. Arquivo Nick.ascx

...
<ul>
<li>
  <label for="txtApelido">Digite seu apelido:label>
  <input type="text" id="txtApelido"
    name="txtApelido"/>
li>
<li>
  <a href="javascript:EntrarnoChat();" id="btnEntrar"
    title="Entrar">Entrara>
  <a href="javascript:Close();" id="btnFechar"
    title="Fechar">Fechara>
</li>
</ul>
<script type="text/javascript">
  //fecha a janela
  function Close()
  {
     window.close();
  }
  //Entra o usuário no chat
  function EntrarnoChat()
  {
    if (document.getElementById(
      'txtApelido').value=='')
    {
      alert('Por favor, digite seu Apelido');
    }
    else
    {
      document.forms[0].submit();
    }
  }
  //Abre popup do chat
  function AbreChat()
  {
     window.open('chat.aspx', 'ChatMSDN',
       'toolbar=0, statusbar=0, location=0, '+
       'menubar=0, scrollbars=no, width=800px, '+
       'height=600px');   
  }
</script>

Esse código será a janela onde o usuário digitará o nick. Quando o usuário clicar em Entrar, será aberto outro popup com o próprio chat. Mais adiante você verá que na tela de chat usaremos JavaScript para fechar o popup de nick. Assim, não ficamos com tantos popups abertos. Tecle F7 (ou menu View>Code) para visualizar o código e acrescente o código da Listagem 9.

Listagem 9. Arquivo Nick.ascx.cs

public partial class Controls_Nick: UserControl
 {
   protected void Page_Load(object sender, EventArgs e)
   {
     if ( Request.Form["txtApelido"] != null )
     {
       if ( Request.QueryString["id"] != null )
       {
          string apelido = Server.HtmlEncode((
            Request.Form["txtApelido"].Replace(
            "'", "").Replace("§", "") ));
             if (Request.QueryString["id"] != null)
          {
            if (Session[ChatManager.
              SESSION_ID_CHAT] == null)
            {
              Session[ChatManager.SESSION_ID_CHAT] = 
                Convert.ToInt32(Request.QueryString[
                "id"]);
            }
            if (Request.QueryString["id"] !=
              Session[ChatManager.SESSION_ID_CHAT].
              ToString())
            {
              string script = 
                "alert('Você já está participando de "+
                "outro Chat, por favor feche-o "+
                "primeiro');window.close();";
                      Page.ClientScript.RegisterStartupScript(
                GetType(), "redirect", script, true);
      }
          else
          {
         if (ProntoParaInclusaoDeUsuarioNoChat(
           apelido))
         {
           string script;
           script = "AbreChat();";
           Page.ClientScript.RegisterStartupScript(
             GetType(), "redirect", script, true);
           Session[ChatManager.SESSION_NICK_CHAT] = 
             apelido;
         }
      }
    }
   }
  }
 }
  
 private bool ProntoParaInclusaoDeUsuarioNoChat(
   string apelido)
   {
    string script;
    //se a sala não está cheia
    if (ChatManager.CurrentChatRoom.ChatUserList.Count < 
      ChatManager.CurrentChatRoom.MaximoUsuarios)
    {
      //verifica se já existe um usuário com o mesmo nick
      if (ChatManager.IsDuplicateNickInCurrentChatRoom(
        apelido))
      {
        //Mostra ao usuário que a sala está cheia
        script = "alert('Já existe outro usuário "+
          "nessa sala com o Nick que você está "+
          "usando, por favor tente outro');";
        Page.ClientScript.RegisterStartupScript(
          GetType(), "redirect", script, true);
      } 
      else
      {
         return true;
      }
      }
    else
    {
      //Mostra ao usuário que a sala está cheia
      script = "alert('A Sala Já Está Cheia, tente "+
        "novamente mais tarde');parent.window.close();";
      Page.ClientScript.RegisterStartupScript(
        GetType(), "redirect", script, true);
    }
    return false;
 }

O code behind do controle de nick verifica se o usuário está apto para entrar, verificando se o nick já existe ou se a sala está cheia. Se o usuário conseguir passar nessa validação, o controle abre a popup do chat.

Agora vamos acrescentar o controle que acabamos de codificar na página Nick.aspx (apenas arrastando o mesmo do Solution Explorer para a página). Rode a aplicação e clique em Entrar, você verá uma tela parecida com a Figura 7.

Cadastrando o nick na sala

Figura 7. Cadastrando o nick na sala

A página Chat.aspx

A página Chat.aspx conterá um frameset para servir de container para as páginas Mensagem.aspx, Usuarios.aspx e Enviar.aspx. Abra o arquivo Chat.aspx e adicione o código da Listagem 10.

Listagem 10. Arquivo Chat.aspx

...
 
<title>Untitled Page</title>
<script type="text/javascript">
  //Fecha a janela de nick, se ela existir
  if (window.opener.location.toString().
    indexOf('nick') >= 0)
  {
     window.opener.close();
  }
</script>
 
</head>
<frameset rows="*,126" cols="*" frameborder="yes"
  border="1" framespacing="0">
<frameset cols="*,200" frameborder="yes" border="1"
  framespacing="0">
<frame src="Mensagem.aspx?"
  name="leftFrame" scrolling="no" noresize="noresize"
  id="leftFrame" title="Chat" />
<frame src="Usuarios.aspx?"
  name="mainFrame" id="mainFrame" title="Usuários"/>
</frameset>
<frame src="Enviar.aspx?"  
  name="bottomFrame" scrolling="No"
  noresize="noresize" id="bottomFrame"
  title="Mensagem"/>
frameset>
<noframes>
<body>
</body>
</noframes>
</html>

Essa página funciona como um container, onde apenas existe um frameset com os frames para os controles de mostrar mensagens, enviar mensagens e mostrar usuários.

Para ver como ficou a página, inicie o sistema a partir da página Default.aspx, escolha uma sala de Chat e então digite seu nick. Ao clicar em Entrar a tela será parecida com a Figura 8.

Tela onde serão adicionadas as mensagens do Chat

Figura 8. Tela onde serão adicionadas as mensagens do Chat

Nota: O design do Visual Studio não suporta frameset, portanto, você não poderá arrastar controles para o designer, terá de trabalhar no código HTML.

O controle lista de usuários

Agora vamos codificar o controle para mostrar os usuários que estão na sala. Esse controle também controlará a saída dos usuários. Então abra o arquivo ListaUsuarios.ascx e adicione o código da Listagem 11.

Listagem 11. Arquivo ListaUsuarios.ascx

...
<script type="text/javascript">
  //Função para tirar o usuário da Sala de Bate-Papo
  function ByeByeUser()
  {  
    ChamarByeByeUserinServer("ByeBye");
  }
  //Atualiza a lista de usuários a cada segundo
  function Updater()
  {
    ChamarServerListUser();
    setTimeout("Updater()",1000);
  }
  //retorno do callback de quando o usuário está saindo
  function RetornoCallBackByByeUser(text,context)
  {
    //não faz nada pois o usuario já saiu,
    //e a tela já fechou quando passar aqui
  }
  //retorno do callback quando atualiza a
  //lista de usuários
  function RetornoCallBackListUsers(text,context)
  {
    var arrText = text.toString().split("§");
    document.getElementById('ulUsers').innerHTML =
      getHTMLToUserList(arrText);
  }
  function getHTMLToUserList(arrText)
  {
    var s = '';
    for(var i=0; i < arrText.length;i++)
    {
      s += '
        '+
        arrText[i] +'' ;
    }
    return s;
   }
script>
<div id="usuarios">
<ul id="ulUsers">
</ul>
</div>

O controle de lista de usuário é o mais importante da tela de Chat, pois ele controlará o tempo de vida de um usuário no Chat, ele controlará quem saiu e quem entrou do Chat.

Ele vai implementar um callback que fica a cada segundo enviando uma requisição ao servidor (via XMLHTTPRequest) e verificando se existem novos usuários ou se usuários saíram do Chat. Você pode configurar esse tempo para um valor maior. Agora abra o arquivo ListaUsuarios.ascx.cs e adicione o código da Listagem 12.

Listagem 12. Arquivo ListaUsuarios.ascx.cs

...
 public partial class Controls_ListaUsuarios :    
   UserControl, ICallbackEventHandler
 {
   public string GetCallbackResult()
   {
     return GetUserListInCurrentChatRoom();
   }
  
   public void RaiseCallbackEvent(string eventArg)
   {
     RaiseCallbackEventInUserControl(eventArg);
   }
  
   protected void Page_Load(object sender, EventArgs e)
   {
     //se é a primeira vez que carrega a página
     if (!IsPostBack)
     {
       //se o browser suportar callback via 
       //XMLHTTPRequest
       if (Page.Request.Browser.SupportsCallback)
       {
         //Trata toda problemática da entrada do 
         //usuário na Sala
         LogicaDeInclusaoDeUsuarioNoChat();
         //Registra o callback para atualizar a 
         //Lista de usuários
         GerarScriptCallBackListUsersInChat();
         //Registra o callback para tirar o usuário 
         //do Chat
         GerarScriptCallBackByByeUser();
      }
   }
 }
  
 private void LogicaDeInclusaoDeUsuarioNoChat()
 {
   //Verifica se o Request é válido e não está espiando
   if (Session[ChatManager.SESSION_NICK_CHAT] != null 
     && Request.QueryString["Espiar"] == null)
   {
     //string para script de possível erro
     string script;
     //se a sala não está cheia
     if (ChatManager.CurrentChatRoom.ChatUserList.Count < 
       ChatManager.CurrentChatRoom.MaximoUsuarios)
     {
       //recupera apelido da sessão
       string apelido = Session[
         ChatManager.SESSION_NICK_CHAT].ToString();
       //verifica se existe um usuário já com o 
       //mesmo nick
       if (!ChatManager.
         IsDuplicateNickInCurrentChatRoom(apelido))
       {
         //Acrescenta o novo usuário na sala
         ChatManager.CurrentChatRoom.ChatUserList.Add(
           ChatManager.RecuperarNovoChatUser(apelido));
         //Coloca mensagem de usuário entrou na sala
         string message = " entrou na Sala";
         ChatManager.SendMessageInCurrentChatRoom(
           ChatManager.getChatMessageforChatbyStrings(
           ChatManager.CurrentChatRoom.CurrentChatUser, 
           message, false, string.Empty, false));
       }
       else
       {
         //Mostra ao usuário que a já existe outro 
         //usuário nessa sala com o nick que você 
         //está usando
         script = "alert('Já existe outro usuário "+
           "nesta sala com o Nick que você está "+
           "usando, por Favor Tente outro');"+
           "window.close();";
         Page.ClientScript.RegisterStartupScript(
           GetType(), "redirect", script, true);
       }
      }
       else
       {
       //Mostra ao usuário que a sala está cheia
       script = "alert('A Sala Já Está Cheia, "+
         "tente novamente mais tarde');"+
         "window.close();;";
       Page.ClientScript.RegisterStartupScript(
         GetType(), "redirect", script, true);
     }
   }
 }
  
 public void GerarScriptCallBackByByeUser()
 {
   //gera o script da função tira o usuário do chat
   string ConteudoChamarByeByeUserinServer =
     Page.ClientScript.GetCallbackEventReference(
     this, "arg", "RetornoCallBackByByeUser", 
     "context");
   string ScriptChamarByeByeUserinServer =
     string.Format(
       "function ChamarByeByeUserinServer(arg,context)"+
       "{{{0};}}", ConteudoChamarByeByeUserinServer);
   Page.ClientScript.RegisterClientScriptBlock(
     GetType(), "ChamarByeByeUserinServerScript",
     ScriptChamarByeByeUserinServer, true);
 }
  
 public void GerarScriptCallBackListUsersInChat()
 {
   //Gera o script da função que faz a requisição 
   //XMLHTTP e a registra na página, onde ela já está 
   //referenciada para lista de usuários
   string ConteudoChamarServerListUser =
     Page.ClientScript.GetCallbackEventReference(
     this, "arg", "RetornoCallBackListUsers", 
     "context");
   string ScriptChamarServerListUser =
     string.Format(
     "function ChamarServerListUser(arg,context)"+
     "{{{0};}}", ConteudoChamarServerListUser);
   Page.ClientScript.RegisterClientScriptBlock(
     GetType(), "ChamarServerListUserScript", 
     ScriptChamarServerListUser, true);
 }
  
 public string VerificarExistenciaQueryStrings()
 {
   string str = string.Empty;
   if (Request.QueryString["Espiar"] == null)
   {
     if (Session[ChatManager.SESSION_NICK_CHAT] == 
       null)
     {
       str += "Por Favor, informe o seu Apelido. ";
     }
     if (Session[ChatManager.SESSION_ID_CHAT] == 
       null)
     {
       str += "Por Favor, informe o Código da Sala ";
     }
   }
   return str;
 }
  
 public void ExibirErroParaUsuarioEFecharJanela(
   string mensagem)
 {
   Page.ClientScript.RegisterStartupScript(GetType(), 
     "erros", string.Format(
     "alert('{0}');history.back();", mensagem), true);
 }
  
 protected override void OnInit(EventArgs e)
 {
   //Verifica a Session, para evitar que o usuário 
   //digite manualmente o endereço do Chat no browser
   string strMensagemDeFaltaDeQueryStrings = 
     VerificarExistenciaQueryStrings();
   if (strMensagemDeFaltaDeQueryStrings != 
     string.Empty)
   {
     ExibirErroParaUsuarioEFecharJanela(
       strMensagemDeFaltaDeQueryStrings);
   }
   base.OnInit(e);
 }
  
 public string GetUserListInCurrentChatRoom()
 {
   return ChatManager.GetUserListInCurrentChatRoom(
     true, false);
 }
  
 public void RaiseCallbackEventInUserControl(
   string eventArg)
 {
   if (eventArg == "ByeBye" && Request.QueryString[
     "Espiar"] == null)
   {
      //Coloca mensagem de usuário Saiu na sala
      string message = " saiu da Sala";
      ChatManager.SendMessageInCurrentChatRoom(
        ChatManager.getChatMessageforChatbyStrings(
        ChatManager.CurrentChatRoom.CurrentChatUser, 
        message, false, string.Empty, false));
      //Remove usuários da Sala
      string apelido = (Session[ChatManager.
        SESSION_NICK_CHAT].ToString().Replace(
        "'", ""));
       ChatManager.
        RemoveChatUserFromCurrentChatRoomByNick(
        apelido);
      Session[ChatManager.SESSION_ID_CHAT] = null;
      Session[ChatManager.SESSION_NICK_CHAT] = null;
   }
 }

Note que o controle de mostrar usuários implementa a interface ICallbackEventHandler, que contém os métodos RaiseCallbackEvent e GetCallbackResult. O primeiro é executado imediatamente após a chamada feita via JavaScript, e recebe um parâmetro do JavaScript.

Utilizamos a string ByeBye para identificar se o usuário fechou a janela. Se não, atualizamos a lista, utilizando o GetCallbackResult que retorna uma string com a nova lista de usuários, utilizando o GetUserListInCurrentChatRoom da classe ChatManager.

Vamos acrescentar o controle que acabamos de criar na página Usuarios.aspx, semelhante a técnica utilizada anteriormente. Mas não esqueça de chamar a função Updater no onload do body:

Rode a aplicação novamente, e faça o mesmo processo (começando pela Default.aspx) e você verá uma tela parecida com a Figura 9.

Obs.: Como usamos uma variável de aplicação, ao fechar todas as janelas do browser, o conteúdo das mesmas ainda é mantido. Isso pode causar alguma confusão entre os usuários e as salas de bate-papo, usuários que você já pensou ter saído, e de repente aparecem novamente na lista.

Para evitar isso, em tempo de desenvolvimento, sempre pare o servidor Web do Visual Studio. Já em produção a solução é parar a aplicação no IIS e logo em seguida iniciar de novo, mas existe uma dica, crie um arquivo dentro da pasta Bin e depois delete-o. Isso fará a aplicação ser reiniciada, e os dados limpados.

Lista dos usuários da sala

Figura 9. Lista dos usuários da sala

O controle Lista de Mensagens

Vamos codificar agora o controle que mostra as mensagens do Chat. Esse controle se preocupa apenas com as mensagens, ele não precisa ter conhecimento se o usuário existe ou não.

No código a seguir guardo em uma variável JavaScript o ID da última mensagem que o usuário visualizou na última atualização, assim sempre garanto não trazer mensagens repetidas para o cliente. Abra o arquivo Mensagem.ascx e coloque o código da Listagem 13.

Listagem 13. Arquivo Mensagem.ascx


...
<div id="title">
<h1>Sala de Chat –
  <span class="sala">ChatManager.CurrentChatRoom.Nome%>
</  span>
</h1>
<form name="frmChat" id="frmChat">
<p>
<input type="checkbox" checked="checked"
  name="chkRolagem" id="chkRolagem" /> <label
  id="rolagem"
  for="chkRolagem">rolagem automáticalabel>
</p>
</form>
</div>
<div id="mensagens"  >
<ul id="ulChat">          
</ul>
</div>
<script type="text/javascript">
  //variável que controla qual a última mensagem
  //que o usuário recebeu
  var LastMsgId = 0;
  //função que atualiza a lista de mensagens a
  //cada 1 segundo
  function Updater()
  {
    //Chama a função criada pelo .NET passando a
    //última mensagem que o usuário viu
    ChamaServerChatMessageList(LastMsgId);
    setTimeout("Updater()",1000);
  }
  //funcao callback que é executada depois que
  //volta do servicor
  function RetornoCallBackChatMessageList(
    text, context)
  {
    //Cria um array pelo separador de Mensagens
    var arrText = text.toString().split("|");
    document.getElementById('ulChat').innerHTML +=
      getHTMLToChatMessages(arrText);
    if (document.getElementById('chkRolagem').checked)
    {
      document.getElementById('mensagens').scrollTop =
        9000;
    }
  }
  function getHTMLToChatMessages(arrText)
  {
    var textmsg = ''
    for(var i=0; i < arrText.length;i++)
    {
      //Pega array com os campos separados da mensagem
      var arr = arrText[i].split("§");
      var message = '';
      var nick_sender = '';
      var date_msg = '';
      //[codigo]§[data]§[autor]§[mensagem]|
      message = arr[3];
      nick_sender = arr[2];
      date_msg = arr[1];
      if (message!=null)
      {
        //atualiza o código da última mensagem que
        //o usuário recebeu
        LastMsgId = arr[0];
        textmsg +=  '
(' + date_msg +
          ') ' + nick_sender + '' +
          message + '';
      }
    }
    return textmsg;
  }
</script> 
<script type="text/javascript">  //variável que controla qual a última mensagem
  //que o usuário recebeu
  var LastMsgId = 0;
  //função que atualiza a lista de mensagens a
  //cada 1 segundo
  function Updater()
  {
    //Chama a função criada pelo .NET passando a
    //última mensagem que o usuário viu
    ChamaServerChatMessageList(LastMsgId);
    setTimeout("Updater()",1000);
  }
  //funcão callback que é executada depois que
  //volta do servicor
  function RetornoCallBackChatMessageList(
    text, context)
  {
    //Cria um array pelo separador de Mensagens
    var arrText = text.toString().split("|");
    document.getElementById('ulChat').innerHTML +=
      getHTMLToChatMessages(arrText);
    if (document.getElementById('chkRolagem').checked)
    {
      document.getElementById('mensagens').scrollTop =
        9000;
    }
  }
  function getHTMLToChatMessages(arrText)
  {
    var textmsg = ''
    for(var i=0; i < arrText.length;i++)
    {
      //Pega array com os campos separados da mensagem
      var arr = arrText[i].split("§");
      var message = '';
      var nick_sender = '';
      var date_msg = '';
      //[codigo]§[data]§[autor]§[mensagem]|
      message = arr[3];
      nick_sender = arr[2];
      date_msg = arr[1];
      if (message!=null)
      {
        //atualiza o código da última mensagem que
        //o usuário recebeu
        LastMsgId = arr[0];
        textmsg +=  '
         (' + date_msg +
          ') ' + nick_sender + '' +
          message + '';
      }
    }
    return textmsg;
  }
</script>

Então agora, falta fazer o code behind do controle, acessando o arquivo Mensagem.ascx.cs e adicionando o código da Listagem 14.

Listagem 14. Arquivo Mensagem.ascx.cs

...
 public partial class Controls_Mensagem : UserControl, 
   ICallbackEventHandler
 {
   //nick do usuário que está vendo a tela
   private string NickViwerUser = "";
   //código da última mensagem que o usuário viu
   private string LastMsgIdViwedUser = "";
  
   protected void Page_Load(object sender, EventArgs e)
   {
     //Se é a primeira vez que a página é carregada
     if (!IsPostBack)
     {
       //se browser suportar callback via XMLHTTPRequest
       if (Page.Request.Browser.SupportsCallback)
       {
         //gera script para callback
         GerarScriptCallBackChamaServerChatMessageList();
       }
     }
   }
  
   private void GerarScriptCallBackChamaServerChatMessageList()
   {
     //Gera o script da função que faz a requisição 
     //XMLHTTP e a registra na página, onde ela já está 
     //referenciada para lista de Mensagems
     string ConteudoChamaServerChatMessageList = 
       Page.ClientScript.GetCallbackEventReference(
       this, "arg", "RetornoCallBackChatMessageList", 
       "context");
     //monta a função para receber o conteúdo
      string ScriptChamaServerChatMessageList = 
       string.Format(
       "function ChamaServerChatMessageList(arg, "+
       "context){{{0};}}", 
       ConteudoChamaServerChatMessageList);
     //registra o script na página
     Page.ClientScript.RegisterClientScriptBlock(
       GetType(), "ChamarServerListUserScript", 
       ScriptChamaServerChatMessageList, true);
   }
  
   public string GetCallbackResult()
   {
     //Aqui retorno as mensagens que o usuário ainda não 
     //viu, com base na última mensagem que ele viu
     return GetNewMessagesForUserInCuttentChatRoom(
       NickViwerUser, LastMsgIdViwedUser);
   }
  
   private string GetNewMessagesForUserInCuttentChatRoom(
     string apelido, string ultimaMensagem)
   {
     //Acessa a classe ChatManager e recupera a lista
      return ChatManager.
       GetNewMessagesForUserInCuttentChatRoom(
       apelido, ultimaMensagem);
   }
  
   public void RaiseCallbackEvent(string eventArg)
   {
     //chama o método para tratar e passa o argumento, 
     //que é o código da última mensagem que o usuário viu
     RaiseCallbackEventInUserControl(eventArg);
   }
  
   public void RaiseCallbackEventInUserControl(
     string eventArg)
   {
     string apelido;
      //essa está espiando
     if (Request.QueryString["Espiar"] != null)
      {
        apelido = "espiao";
      }
      else
      {
        apelido = Session[
         ChatManager.SESSION_NICK_CHAT].ToString();
      }
      NickViwerUser = apelido;
      LastMsgIdViwedUser = eventArg;
   }
 }

Note também que o controle que mostra mensagens, implementa a interface ICallbackEventHandler e faz requisições ao servidor segundo a segundo para atualizar a lista mensagens.

Por efeitos de performance recuperamos apenas as novas mensagens, que o usuário ainda não viu e fazemos isso utilizando a variável de código já citada anteriormente.

Agora que criamos o controle, como fizemos anteriormente, vamos adicionar o controle na página. Abra o arquivo Mensagem.aspx e arraste o controle do Solution Explorer.

Um detalhe importante é a linha do body, onde temos o seguinte código onload="Updater(); que faz com que a função JavaScript seja chamada pela primeira vez. Rode novamente a aplicação e veja uma tela semelhante a Figura 10.

Visualizando as mensagens do Chat

Figura 10. Visualizando as mensagens do Chat

O controle de envio de mensagens

O último controle é o de envio de mensagens, que realmente faz o que o Chat deve fazer. Então vamos abrir o arquivo Enviar.ascx e adicione o código da Listagem 15.

Listagem 15. Arquivo Enviar.ascx

...
<div id="form">
<form onsubmit="SendMesasge();return false; "
  id="frmMensagem" name="frmMensagem" method="post"
  action="#">
<ul>
<li>
<p><label for="txtMensagem">Insira aqui a sua mensagemlabel>p>
<input type="text" id="txtMensagem" name="txtMensagem"
  class="floatLeft"  />
<a href="javascript:SendMessage();" id="btnEnviar"
  title="Enviar">Enviara>
<a href="javascript:parent.window.close();"
  id="btnSair" title="Sair">Saira>
<div id="botoes">
<a href="javascript:parent.window.close();"
  id="btnSair" title="Sair">a>
div>
li>
<li>
<label for="cboFalar">Falar Paralabel>
<select id="cboFalar" onchange="TratarCheck(this)"
  onblur="FazerContinuar()" onfocus="FazerParar()"
  name="cboFalar"
  >
</select>
<input type="checkbox" id="chkReservado"
  name="chkReservado"
   /><label for="chkReservado" >reservadamentelabel>
</li>
</ul>
</form>
</div>
<script type="text/javascript">
  //variável que controla quando atualizar a
  //lista de usuários na combo
  var PararDeAtualizarUsuarios = false;
  //trata o comportamento da checkBox reservado,
  //em função da Combo de usuários
  function TratarCheck(combo)
  {
    //se selecionou todos, então desmarca o Reservado
    if (combo.selectedIndex==0)
    {
      document.getElementById(
        'chkReservado').checked = false;
    }
  }
  //Pára Atualização de usuários
  function FazerParar()
  {
    PararDeAtualizarUsuarios = true;
  }
  //Continua a monitorar a atualização de usuários
  function FazerContinuar()
  {
    PararDeAtualizarUsuarios = false;
  }
  //Retorna o valor de uma QueryString
  function getQueryVariable(variable)
  {
    var query = window.location.search.substring(1);
    var vars = query.split("&");
    for (var i=0;i
    {
      var pair = vars[i].split("=");
      if (pair[0] == variable)
      {
        return pair[1];
      }
    }
    return '';
  }
  //Envia mensagem ao Servidor
  function SendMessage()
  {
    //pega a mensagem digitada pelo usuário
    //tirando os separadores § e | para evitar erros
    var message = document.getElementById(
      'txtMensagem').value.toString().replace(
      "§","").replace("|","");
    var destino_reservado = '';
    var reservado = 'false';
    //se digitou mensagem válida
    if (message!='')
    {
      //se é reservado
      if (document.getElementById(
        'chkReservado').checked)
      {
        destino_reservado = document.getElementById(
          'cboFalar').value;
        //se reservado é valido
        if (destino_reservado=='')
        {
          alert('Você não pode enviar mensagens '+
            'reservadas para Todos');
          document.getElementById(
            'txtMensagem').value = '';
          document.getElementById(
            'txtMensagem').focus();
          return;
        }
        reservado = 'true';
      }
      //Formata mensagem no formato
      //[message]§[Reservado]§[Destino_reservado]
      var mensagemformatada = message + '§' + reservado
        + '§' + destino_reservado;
      ChamarServerSendMessage(mensagemformatada);
    }
    else
    {
      document.getElementById(
        'txtMensagem').value = '';
      document.getElementById('txtMensagem').focus();
    }
  }
  //Função que é executada depois que volta do
  //servidor, depois de enviar mensagem
  function RetornoCallBackChamarServerSendMessage(
    text, context)
  {
    document.getElementById('txtMensagem').value = '';
    document.getElementById('txtMensagem').focus();
  }
  //Função que atualiza a lista de usuários
  //segundo a segundo, se o usuário não está com o
  //foco na combo
  function Updater()
  {
    if (! PararDeAtualizarUsuarios)
    {
      ChamarServerListUser("Users");
    }
    setTimeout("Updater()",1000);
  }
  //Função que é executada depois que volta do
  //servidor, e então atualiza a lista de usuários
  function RetornoCallBackListUsers(text,context)
  {
    var arrText = text.toString().split("§");
    var cboFalar = document.getElementById(
      'cboFalar');
    if (!document.getElementById(
      'chkReservado').checked)
    {
      cboFalar.options.length = 0;
      cboFalar.options[cboFalar.options.length] =
        new Option('Todos','');           
      for(var i=0; i < arrText.length;i++)
      {
        //tira ele próprio da lista
        if (arrText[i] != getQueryVariable('Nick'))
        {
          cboFalar.options[cboFalar.options.length] =
            new Option(arrText[i],arrText[i]);           
        }
      }
    }  
  }
 
 
  //Desabilita os botões se está espiando
  function Espiar()
  {
    document.getElementById(
      'txtMensagem').disabled = true;
    document.getElementById(
      'btnEnviar').disabled = true;
    document.getElementById(
      'emoticons').disabled = true;
    document.getElementById(
      'chkReservado').disabled = true;
    document.getElementById(
      'cboFalar').disabled = true;
  }
</script>

O controle de Enviar Mensagem se preocupa apenas em enviar a mensagem para fila de mensagens no servidor. Utilizamos a mesma técnica de callback, para atualizar o DropDownList de usuários, para a funcionalidade de enviar reservadamente e para quando o usuário clicar em enviar mensagem, não seja necessário dar o postback na página.

Agora, falta apenas criar o code behind desse controle, então abra o arquivo Enviar.ascx.cs e adicione o código da Listagem 16.

Listagem 16. Arquivo Enviar.ascx.cs

...
 public partial class Controls_Enviar : UserControl, 
   ICallbackEventHandler
 {
   protected override void OnLoad(EventArgs e)
   {
     //se é a primeira vez que carrega a página
     if (!IsPostBack )
     {
       //se o browser suportar callback via 
       //XMLHTTPRequest
       if (Page.Request.Browser.SupportsCallback)
       {
         //gera script de callback para listar 
         //usuários na Combo
         GerarScriptCallBackListUsersInChat();
         //gera script de callback para enviar 
         //novas mensagens
         GerarScriptCallBackChamarServerSendMessage();
       }
       //está espiando, então chama a função Javascript 
       //Espiar();
       if (Request.QueryString["Espiar"] != null)
       {
         Page.ClientScript.RegisterStartupScript(
           GetType(), "ChamarServerSendMessageScript", 
           "Espiar()", true);
       }
    }
    base.OnLoad(e);
 }
  
 public void GerarScriptCallBackChamarServerSendMessage()
 {
   //Gera o script da função que faz a requisição 
   //XMLHTTP e a registra na página, para envio de 
   //novas mensagens
   string ConteudoChamarServerSendMessage = 
     Page.ClientScript.GetCallbackEventReference(this, 
     "arg", "RetornoCallBackChamarServerSendMessage", 
     "context");
   string ScriptChamarServerSendMessage = 
     string.Format(
       "function ChamarServerSendMessage(arg,context)"+
       "{{{0};}}", ConteudoChamarServerSendMessage);
   Page.ClientScript.RegisterClientScriptBlock(
     GetType(), "ChamarServerSendMessageScript", 
     ScriptChamarServerSendMessage, true);
 }
  
 public void GerarScriptCallBackListUsersInChat()
 {
   //Gera o script da função que faz a requisição 
   //XMLHTTP e a registra na página, onde ela já 
   //está referenciada para lista de usuários
   string ConteudoChamarServerListUser = 
     Page.ClientScript.GetCallbackEventReference(this, 
     "arg", "RetornoCallBackListUsers", "context");
   string ScriptChamarServerListUser = string.Format(
     "function ChamarServerListUser(arg,context)"+
     "{{{0};}}", ConteudoChamarServerListUser);
   Page.ClientScript.RegisterClientScriptBlock(
     GetType(), "ChamarServerListUserScript", 
     ScriptChamarServerListUser, true);
 }
  
 public string GetCallbackResult()
 {
   //Retorna a lista de usuários 
   //com o HTML DECODIFICADO
   return ChatManager.GetUserListInCurrentChatRoom(
     false, true);
 }
 public void RaiseCallbackEvent(string eventArg)
 {
   //se o usuário enviou uma mensagem
   if (eventArg.IndexOf("§") > 0)
   {
     ChatManager.SendMessageInCurrentChatRoom(
       ChatManager.getChatMessageFromSendMessageFormat(
       eventArg));
   }
   }
 }

O controle de enviar pára de fazer requisições se o usuário estiver com o foco no DropDownList de usuários, para não causar um mau comportamento do controle ficar recarregando sempre.

Bem, depois de fazer esse último controle, agora só falta arrastá-lo para Enviar.aspx e chamar Updater no onload do body (conforme fizemos anteriormente). Quando rodar, o programa estará mais ou menos como à Figura 11.

Aplicação final em execução

Figura 11. Aplicação final em execução

Finalizamos o Chat, mas você ainda pode colocar um CSS para melhorar o layout.

A novidade é o uso do recurso de callback do ASP.NET 2.0 que permite controlar os Requests sem recarregar a página, com uma quantidade de código considerável, se comparado com o antigo ATLAS e agora ASP.NET AJAX, porém tempos atrás isso também seria uma explosão inovadora para os desenvolvedores.