Nel tentativo di aggiornare questo blog quanto più frequentemente possibile, mi piacerebbe postare qui del codice che ho scritto qualche tempo fà allo scopo di implementare qualcosa di simile all’effetto di menù a tendina fatto conoscere al grande pubblico (se non sbaglio) da Google Suggest qualche anno orsono:

Dunque, immagino che su internet si possano trovare decine di esempi simili (anche su CodeProject, probabilmente) ma ritengo che la bellezza della mia soluzione consista nel fatto che:

  • L’intera implementazione è veramente esigua. Questo significa che è possibile, per chi si sta avvicinando adesso ad AJAX, di capire un poco di più di che cosa si tratti, da un punto di vista molto di “basso livello”, se così si può dire. Nella mia esperienza di consulente e formatore, noto sempre più spesso la carenza di competenze Javascript (anche nei più abili programmatori ASP.NET !!) e contemporaneamente come tali competenze siano sempre più richieste dal mercato (vedi ad esempio la miriade di siti, anche italiani, recentemente usciti o rifatti, dove popup e menù a tendina come quello in oggetto sono ovunque).
  • In virtù del punto precedente, eventuali “customizzazioni” al mio lavoro risultano estremamente semplici da implementare: in altri controlli simili al mio che ho visto in giro (anche blasonati), tale semplicità si perde molto spesso in una miriade di eventi client o server nei quali il programmatore medio (e non) tende ad affogare (senza alla fine raggiungere il risultato desiderato).

Di contro però, c’è da dire che il codice che segue è stato espressamente scritto per Internet Explorer (quindi ideale per una applicazione di tipo intranet dove i requisiti dei client sono prefissati e conosciuti). Comunque, adeguare questo codice affinchè funzioni anche con altri browser è cosa assai semplice.

Per cominciare c’è la parte HTML. Questo markup permette di renderizzare un editbox predisposto a visualizzare il menù di Auto-Completion quando si preme un tasto, per esempio:

<div style="position: relative;">

    <input name="ctrlAcSearchBox" id="ctrlAcSearchBox" type="text" autocomplete="off" Style="width: 250px;"

        onkeydown='<%= "AutoCompletion_onEditBoxKey(\"" + "ctrlAcSearchBox" + "\", event.keyCode);" %>'

        onkeyup='<%= "AutoCompletion_onEditBoxKeyChange(\"http://localhost/folder/AutoCompletionInterface.aspx\", \"" + "ctrlAcSearchBox" + "\", event.keyCode);" %>'

        ondeactivate='<%= "document.getElementById( \"" + "ctrlAcSearchBox" + "\" + \"div\" ).style.display = \"none\";" %>' />

    <div id='<%= "ctrlAcSearchBox" + "div" %>' style="position: absolute; top: 23px;

        left: 0px; width: 250px; display: none;">

    </div>

</div>

La presenza qui delle due DIV è fondamentale: infatti la seconda DIV (ossia quella innestata), con l’attributo di stile “position: absolute” permette di visualizzare il box di Auto-Completion esattamente sotto l’editbox: infatti la direttiva “absolute”, che normalmente renderizzerebbe il contenuto della DIV rispetto all’angolo superiore-sinistro della finestra di browsing, qui viene invece riferita alla posizione della prima DIV, che possiede un attributo di stile di tipo “position: relative”. Togliendo infatti la prima DIV, si ottiene l’effetto di vedere il box di Auto-Completion nell’angolo superiore-sinistro della finestra, confermando quindi questa regola dell’HTML.

Il controllo INPUT TYPE=”TEXT” gestisce una serie di eventi che permettono al box di Auto-Completion di apparire e scomparire all’occorrenza (onkeydown, onkeyup, ondeactivate). Ricordo che da Code-Behind è possibile leggere il valore inserito nell’editbox scrivendo del codice come questo:

Request.Form[“ctrlAcSearchBox”]

 

Inoltre in una posizione idonea della pagina è necessario includere il file .js che di fatto implementa il box di Auto-Completion lato-client:

 

<script type="text/javascript" src="http://localhost/folder/AutoCompletion.js"></script>

 

E’ possibile scaricare tale file da questo indirizzo: AutoCompletion.js (3,43 KB). In ogni caso, di seguito, è riportato integralmente:

 

function AutoCompletion_getSelection (n, def)

{

    for( i=0; i<10; i ++ )

    {

        var tr = document.getElementById( n + "tr" + i );

        if ( tr == null )

            return def;

        else if ( tr.style.color == "#ffffff" )

            return i;

    }

   

    return def;

}

 

function AutoCompletion_putSelection (n, s)

{

    for( i=0; i<10; i ++ )

    {

        var tr = document.getElementById( n + "tr" + i );

        if ( tr == null )

            return;

       

        if ( s == i )

        {

            tr.style.backgroundColor = "#0040D0";

            tr.style.color = "#ffffff";

        }

        else

        {

            tr.style.backgroundColor = "#ffffff";

            tr.style.color = "#000000";

        }

    }

}

 

function AutoCompletion_getNum (n)

{

    var i;

    for( i=0; i<10; i ++ )

    {

        var tr = document.getElementById( n + "tr" + i );

        if ( tr == null )

            return i;

    }

    return i;

}

 

function AutoCompletion_processResponse (n)

{

    var req = document.getElementById( n + "div" ).req;

    if ( req.readyState == 4 && req.status == 200 )

    {

        document.getElementById( n + "div" ).style.display = "block";

        document.getElementById( n + "div" ).innerHTML = req.responseText;

    }

}

 

function AutoCompletion_setEditText (n)

{

    var sel = AutoCompletion_getSelection (n);

    if ( sel == null )

        return;

 

    document.getElementById( n ).value = document.getElementById( n + "span" + sel ).innerHTML;

    document.getElementById( n + "div" ).style.display = "none";

    document.getElementById( n ).focus ();

}

 

function AutoCompletion_onEditBoxKey (n, key)

{

    if ( key == 27 ) // esc.

    {

        document.getElementById( n + "div" ).style.display = "none";

        event.returnValue = false;

        return;

    }   

 

    if ( key == 38 ) // up.

    {

        var s = AutoCompletion_getSelection (n, 0) - 1;

        if ( s < 0 )

            s = AutoCompletion_getNum(n) - 1;

        AutoCompletion_putSelection( n, s );

        event.returnValue = false;

    }

    else if ( key == 40 ) // down.

    {

        var s = AutoCompletion_getSelection (n, AutoCompletion_getNum(n)-1) + 1;

        if ( s >= AutoCompletion_getNum (n) )

            s = 0;

        AutoCompletion_putSelection( n, s );

        event.returnValue = false;

    }

    else if ( key == 13 ) // enter.

    {

        AutoCompletion_setEditText (n);

        event.returnValue = false;

    }

}

 

function encodeHtml ( s )

{

     s = escape(s);

     s = s.replace(/\//g,"%2F");

     s = s.replace(/\?/g,"%3F");

     s = s.replace(/=/g,"%3D");

     s = s.replace(/&/g,"%26");

     s = s.replace(/@/g,"%40");

     return s;

}

 

function AutoCompletion_onEditBoxKeyChange (base, n, key)

{

    if ( key == 8 || (key >= 32 && key != 38 && key != 40) )

    {

        var url = base + "?str=" + encodeHtml( document.getElementById( n ).value ) + "&rnd=" + Math.floor(Math.random()*1000000000) + "&n=" + n;

   

        var req = new ActiveXObject("Microsoft.XMLHTTP");

        document.getElementById( n + "div" ).req = req;

        req.onreadystatechange = new Function ( "AutoCompletion_processResponse('" + n + "');" );

        req.open ( "GET", url, true );

        req.send ();

    }

}

 

Di seguito è riportata una descrizione di ciascuna funzione JS:

 

AutoCompletion_getSelection: semplicemente ritorna l’indice dell’elemento attualmente selezionato nel box.

 

AutoCompletion_putSelection: seleziona un elemento nel box.

 

AutoCompletion_getNum: restituisce la dimensione del box, come numero di item attualmente visualizzati.

 

AutoCompletion_setEditText: in base alla selezione corrente nel box, popola l’editbox opportunamente.

 

AutoCompletion_onEditBoxKey (associata all’evento “onkeydown” dell’editbox): gestisce la pressione dei tasti quando il focus appartiene all’editbox (tasti: esc, su, giù, enter).

 

AutoCompletion_onEditBoxKeyChange (associata all’evento “onkeyup” dell’editbox): scatena una richiesta GET al server (attraverso l’oggetto "Microsoft.XMLHTTP") alla pressione di un tasto nell’editbox. Come specificato nell’evento “onreadystatechange”, la gestione della risposta da parte del server è demandata alla funzione JS con nome “AutoCompletion_processResponse”.

 

AutoCompletion_processResponse: questa funzione riceve la risposta del server. In questo caso, il server restituisce HTML (direttamente il contenuto del box di Auto-Completion). In altri casi è frequente trovare l’oggetto "Microsoft.XMLHTTP” coinvolto nello scambio di markup XML tra server e client (questo argomento, però, esula dagli obiettivi di questo post).

 

Dietro le quinte, una pagina ASPX si occupa di restituire il markup HTML che costituisce il box di Auto-Completion visualizzato. L’URL di tale pagina ASPX è fornito alla funzione “AutoCompletion_onEditBoxKeyChange” nel gestore dell’evento “onkeyup” del nostro INPUT TYPE=”TEXT” (ossia dell’editbox).

 

Il codice di questa pagina è scaricabile qui (AutoCompletionInterface.aspx (0,45 KB) + AutoCompletionInterface.aspx.cs (2,4 KB)). Il markup della pagina ASPX è trascurabile. La struttura del file di Code-Behind, invece, è molto intuitiva: nel gestore della Page_Load, viene generato dell’HTML secondo la stringa di ricerca passata (ossia ciò che l’utente ha inserito nell’editbox) quindi il markup risultante viene restituito come risposta alla GET della parte JS.

 

Tale markup verrà quindi visualizzato nella seconda DIV di cui sopra, mostrando il box di Auto-Completion:

Nel file di Code-Behind di esempio, viene chiamata una Stored Procedure che potrebbe essere qualcosa del genere:

      SELECT TOP 10

           u.nome_cognome

      FROM Utenti u

     WHERE u.nome_cognome like @Nome_Cognome + '%'

  ORDER BY u.nome_cognome