function MVXComboBox(IDElementoTexto, Tabela, Campo, Filtros) 
{
  // Inicializa propriedades. Algumas delas tem seu valor já definido, outras são setadas para null.
  this.Tabela = Tabela;
  this.Campo = Campo;
  this.Filtros = Filtros;
  this.ElementoTexto = null;
  this.ElementoImagem = null;
  this.ElementoLista = null;
  this.TimerAjax = null;
  this.TimerBlur = null;
  // Associação dos métodos. Fazemos assim, para poder usá-los mesmo antes deles serem implementados.
  this.Inicializa = Inicializa;
  this.OnKeyUp = OnKeyUp;
  this.OnChange = OnChange;  
  this.OnFocus = OnFocus;
  this.OnBlur = OnBlur;
  this.Sincroniza = Sincroniza;
  this.Popula = Popula;
  // Inicializa o objeto. É uma espécie de construtor.
  this.Inicializa(this.ElementoTexto);

  // Método que inicializa o objeto.
  function Inicializa(ElementoTexto)
  {
    // O objeto/controle contém referências para cada um dos elementos HTML que o contém: campo de texto, 
    // imagem de espera e listbox. Vamos começar obtendo o campo texto informado, que o objeto vai usar como
    // ponto de partida para montar o combobox. Vamos identificá-lo a partir do ID passado, e guardar
    // nele uma referência para o controle, que é seu pai. Precisaremos disto no tratamento de eventos.
    this.ElementoTexto = GetElementoID(IDElementoTexto);
    this.ElementoTexto.controle = this;
    // Agora criamos uma imagem, setamos o arquivo origem e a ocultamos.
    this.ElementoImagem = document.createElement("img");
    this.ElementoImagem.src = GLURLImagens+'/mvx/aguarde_ajax_p.gif';
    OcultaElemento(this.ElementoImagem);
    // Agora ela adicionada ao fluxo do documento.
    AdicionaElementoApos(this.ElementoImagem, this.ElementoTexto);
    // Agora o listbox é criado.
    this.ElementoLista = document.createElement("select");
    // Aqui é feita uma manobra incomum: como usamos como base um textbox pre-existente para montar um combo, 
    // o código que chamou o objeto conhece o controle pelo nome dado ao textbox. Mas o ID do item selecionado,
    // no final de processo, precisará ser lido do listbox que estamos criando. Portanto, vamos dar ao listbox, o 
    // nome do textbox, e acrescentar '_1' ao final do textbox, para evistar colisão de nomes. Desta forma, 
    // fica transparente para o código server-side a leitura do valor do controle.
    this.ElementoLista.name = this.ElementoTexto.name;
    this.ElementoTexto.name = this.ElementoTexto.name+'_1';
    this.ElementoLista.id = this.ElementoTexto.id;
    this.ElementoTexto.id = this.ElementoTexto.id+'_1';
    // A exemplo do textbox, vamos guardar no listbox uma referência ao controle.
    this.ElementoLista.controle = this;
    // O listbox é setado para multiple, para não ser desenhado na tela como um combobox, seu tamanho é zerado e ele é ocultado.
    this.ElementoLista.multiple = 'multiple';
    this.ElementoLista.size = 0
    OcultaElemento(this.ElementoLista);
    // Agora ele adicionado ao fluxo do documento.
    AdicionaElementoApos(this.ElementoLista, this.ElementoImagem);
    // Tendo todos os elementos HTML gerados e parametrizados, vamos associar eventos javascript a eles.
    AdicionaEvento(this.ElementoTexto, 'keyup', OnKeyUp);
    AdicionaEvento(this.ElementoLista, 'change', OnChange);
    AdicionaEvento(this.ElementoTexto, 'focus', OnFocus);
    AdicionaEvento(this.ElementoLista, 'focus', OnFocus);
    AdicionaEvento(this.ElementoTexto, 'blur', OnBlur);
    AdicionaEvento(this.ElementoLista, 'blur', OnBlur);
  }

  // Trata digitação no textbox.
  function OnKeyUp(Evento)
  {
    // Primeiro, vamos obter uma referência ao objeto, visto que neste contexto, this pode ser a window ou o elemento HTML, 
    // dependendo do browser.
    Objeto = GetElementoEvento(Evento).controle;
    // Se o timer que cuida do disparo da chamada AJAX foi setado, ele é limpo aqui.
    clearTimeout(Objeto.TimerAjax);
    // E em seguida, ele é reiniciado. Desta forma, o usuário pode digitar um trecho do item desejado, sem que o controle 
    // dispare a busca AJAX imediatamente.
    Objeto.TimerAjax = setTimeout(function() { Objeto.Popula(); }, 1000);
  }

  // Trata seleção de itens no listbox.
  function OnChange(Evento)
  {
    // Quando a seleção do listbox muda, chamamos o método que sincroniza os dois elementos: textbox e listbox.
    GetElementoEvento(Evento).controle.Sincroniza();
  }

  // Trata o recebimento de foco no textbox e listbox.
  function OnFocus(Evento)
  {
    // Primeiro, vamos obter uma referência ao objeto, visto que neste contexto, this pode ser a window ou o elemento HTML, 
    // dependendo do browser.
    Objeto = GetElementoEvento(Evento).controle;
    // Se o timer que cuida da saída de foco do controle foi setado, ele é limpo aqui. Afinal, se um dos dois elementos
    // (textbox e listbox) receberam foco, o processo de saída do controle deve ser suspenso.
    clearTimeout(Objeto.TimerBlur);
    // O texto do textbox deve ser selecionado, para facilitar uma nova digitação por parte do usuário.
    Objeto.ElementoTexto.select();
    // E por fim, o listbox deve ser exibido.
    ExibeElemento(Objeto.ElementoLista);
  }

  // Trata a perda de foco do textbox e listbox.
  function OnBlur(Evento)
  {
    // Primeiro, vamos obter uma referência ao objeto, visto que neste contexto, this pode ser a window ou o elemento HTML, 
    // dependendo do browser.
    Objeto = GetElementoEvento(Evento).controle;
    // O timer de saída dos elementos é setado, para ocultar o listbox após o intervalo de tempo setado.
    Objeto.TimerBlur = setTimeout(function() { OcultaElemento(Objeto.ElementoLista); }, 500);
  }

  // Sincroniza os valores dos elementos textbox e listbox.
  function Sincroniza()
  {
    // Esta é uma manobra para que apenas um elemento do listbox seja selecionado. Como o listbox precisa ser "multiple",
    // para não ser desenhado como um combo, ele acaba permitindo a seleção de mais de um item. Para evitar isto, setamos a
    // seleção para ela mesma. O efeito disto é que apenas o primeiro item selecionado continua selecionado. Os outros são
    // desmarcados.
    this.ElementoLista.selectedIndex = this.ElementoLista.selectedIndex;
    // O texto do elemento selecionado vai para o textbox.
    this.ElementoTexto.value = this.ElementoLista.options[this.ElementoLista.selectedIndex].text;
    // O listbox é ocultado, afinal a seleção foi feita e não precisamos mais vê-lo.
    OcultaElemento(this.ElementoLista);
    // E tiramos a seleção do textbox, pois ela pode ter ido para lá depois que o listbox foi ocultado.
    this.ElementoTexto.blur();
  }


  // Popula o listbox, utilizando os filtros padrão associados, e opcionalmente o texto existente no textbox, ou ainda
  // um ItemID que pode ter sido passado (se existir ItemID, ele ignora o texto presente no textbox).
  function Popula(ItemID)
  {
    // Vamos começar criando um array de filtros, e checando a existência de um ItemID.
    var FiltrosProcessados = new Array();
    if(is_int(ItemID))
      // Se ele existir, um filtro é adicionado, especificando que queremos apenas o item indicado por ItemID. 
      // À primeira vista, este seria o único filtro necessário quando queremos apenas um item em particular, mas
      // os filtros associados são necessários para nos certificarmos de que o ItemID indicado realmente está 
      // relacionado com as outras entidades envolvidas no contexto. O campo "valor" deve ser hardcoded, porque a rotina
      // server-side sempre chama de "valor" o campo que contém o texto de exibição do combobox.
      FiltrosProcessados.push("valor="+ItemID);
    else if(this.ElementoTexto.value.length >= GLMVXComboBoxNumMinCaracteres)
      // Se não houver ItemID, mas houver conteúdo no textbox, faremos o filtro através dele, desde que ele tenha 
      // o tamanho mínimo exigido. O campo "nome" deve ser hardcoded, porque a rotina server-side sempre chama de "nome" 
      // o campo que contém o texto de exibição do combobox.
      FiltrosProcessados.push("nome like '%"+this.ElementoTexto.value+"%'");
    else
      // Se não foi passado nenhum ID, e se o texto existente em textbox não atende aos requisitos mínimos, 
      // devemos abortar o processo, sem preencher o listbox.
      return;
    
    // Agora vamos começar a tratar os filtros associados ao controle, inicializando algumas variáveis auxiliares.
    var Valor;
    var FiltrosAusentes = false;
    // Agora faremos um loop por cada filtro definido dentro do elemento. Os filtros sempre referem-se a valores de outros
    // elementos existentes no mesmo formulário, portanto, nem sempre estão presentes.
    for(i in this.Filtros)
    {
      // Vamos obter o valor do controle referenciado neste filtro.
      Valor = GetElementoID(this.Filtros[i][1]).value;
      // Vamos checar se o valor é nulo ou vazio.
      if(is_null(Valor) || Valor == '')
      {
        // Se for, isto significa que, para preencher este combo, precisamos do valor de um controle que ainda não foi
        // setado, ou pior, que o controle não existe no form, o que é um bug, e não pode acontecer. 
        // De qualquer forma, existe um filtro ausente. A flag FiltrosAusentes é setada.
        FiltrosAusentes = true;
        // E o loop é abandonado.
        break;
      }
      else
        // Por outro lado, se tivermos valor definido, o filtro é processado e adicionado ao array FiltrosProcessados.
        FiltrosProcessados.push(Filtros[i][0]+"="+Valor);
    }
    // Vamos agora checar a flag FiltrosAusentes.
    if(FiltrosAusentes)
    {
      // Se ela for true, significa que um ou mais filtros estão faltando. Neste caso, o combo não pode ser preenchido.
      // Vamos apenas esvaziar o listbox, preencher o textbox com uma mensagem explicando o ocorrido e torná-lo inativo.
      this.ElementoLista.options.length = 0;
      OcultaElemento(this.ElementoLista);
      this.ElementoTexto.text = GLFiltroAusente;
      this.ElementoTexto.disabled = true;
    }
    else
    {
      // Com todos os filtros prontos, podemos dar continuidade na chamada AJAX. É hora de exibir a imagem de espera.
      ExibeElemento(this.ElementoImagem);
      // Criamos uma referência ao objeto atual, para que o callback possa manipulá-lo.
      Objeto = this;
      // E finalmente a chamada AJAX é feita, com a passagem da URL que vai retornar o XML desejado, 
      // e o callback que vai processá-lo.
      AcionaAjax("xml?a=4&t="+this.Tabela+"&n="+this.Campo+"&f="+escape(FiltrosProcessados.join(" and ")),
        function(XMLDoc) 
        {
          // Vamos checar se houve retorno.
          if(XMLDoc.hasChildNodes())
          { 
            // Se sim, vamos checar quantos registros vieram.
            var Registros = XMLDoc.getElementsByTagName('Registro');
            var NumRegistros = Registros.length;
            if(NumRegistros > 0)
            {
              // Se veio algum, começamos a preparação do preenchimento do listbox, zerando seus itens.
              Objeto.ElementoLista.options.length = 0;
              // E o primeiro item (default) é criado.
              var NovoElemento;
              NovoElemento = document.createElement("option");
              NovoElemento.setAttribute("value", ""); 
              NovoElemento.appendChild(document.createTextNode(GLComboDefault));
              // E adicionado ao listbox.
              Objeto.ElementoLista.appendChild(NovoElemento);
              // Agora vem o loop que vai adicionar os elementos encontrados ao listbox.
              for(var i = 0; i<NumRegistros; i++)
              {
                // Cria um option vazio, adiciona o valor da opção, cria um nó de texto, que é o nome da opção, 
                // e finalmente, a nova opção é adicionada ao listbox.
                NovoElemento = document.createElement("option");
                NovoElemento.setAttribute("value", Registros.item(i).childNodes[1].childNodes[0].nodeValue);
                NovoElemento.appendChild(document.createTextNode(Registros.item(i).childNodes[0].childNodes[0].nodeValue));
                Objeto.ElementoLista.appendChild(NovoElemento);
              }
              // Terminada a carga, vamos dar um "reset" no listbox e selecionar o primeiro item dele.
              Objeto.ElementoLista.selectedIndex = 0;
            }
            else
            {
              // Se nenhum item foi encontrado, o listbox é zerado.
              Objeto.ElementoLista.options.length = 0;
            }
          }
          // Terminado o processamento, a imagem de espera é novamente ocultada.
          OcultaElemento(Objeto.ElementoImagem);
          // E o número de itens encontrados no listbox é lido.
          var NumItens = Objeto.ElementoLista.options.length;
          // Se não temos nenhum item, exibimos uma mensagem avisando.
          if(NumItens == 0) { Objeto.ElementoLista.size = 0; alert("Nenhum resultado encontrado"); }
          // Se temos 2 itens (o default, que é neutro, e mais um item real), devemos selecionar o segundo 
          // automaticamente, e então disparar o evento OnChange para sincronizar o listbox e o textbox.
          else if(NumItens == 2) { Objeto.ElementoLista.selectedIndex = 1; Objeto.Sincroniza(); }
          // Se temos mais de um item, em número maior do que o parâmetro de máximo de itens exibidos, exibimos apenas 
          // o limite estabelecido.
          else if(NumItens > GLMVXComboBoxNumItensExibidos) Objeto.ElementoLista.size = GLMVXComboBoxNumItensExibidos; 
          // Por fim, se temos itens, e o número deles é menor do que o limite, o listbox é setado para exibir todos.
          else Objeto.ElementoLista.size = NumItens;
        }
      );
    }
  }
}

