CodeProject

Tempo fa mi ero imbattuto in un problema che credo abbiamo incontrato il 99% degli sviluppatori web: lanciare un’operazione che impiega diverso tempo per l’esecuzione (Long running task).

Descrizione del problema

La cosa più sgradevole per un utente è avere la sensazione che la pagina web si sia bloccata, vedendo il browser in “caricamento” ma non vedendo un risultato in tempi brevi.
Nel mio caso l’applicazione lancia un pacchetto SSIS su SQL Server per importare ed elaborare dati da file Excel. Quando i dati raggiungono una certa sostanza, l’esecuzione può durare anche qualche minuto. Pensate cosa significa fissare un browser vuoto, con la barra di caricamento praticamente ferma. A me già 15 secondi danno fastidio!!!!!

Possibili soluzioni

Dando una risposta al problema in maniera veloce ed istintiva, mi vengono in mente tre o quattro possibili soluzioni per dare una sorta di “speranza” all’utente.

Partiamo dalla più semplice e veloce: utilizzare il Parial Rendering con un UpdatePanel ed un UpdateProgress. I questo modo possiamo per lo meno creare una piccola interfaccia di attesa mentre il codice lato server rimane in esecuzione. A parte una minore (molto minore) frustrazione da parte dell’utente, questa soluzione si può applicare a situazioni dove l’attesa non supera il minuto, anche perché in genere i timeout di ambiante sono settati a 2 minuti, e dove non è necessario avere feedback durante l’esecuzione del codice.

Altra soluzione, simile ma più elaborata, si può implementare attraverso una chiamata AJAX pura. Possiamo implementare un WebService piuttosto che un PageMethod, piuttosto che una semplice pagina aspx che espone una funzione da poter chiamare tramite una classe Proxy creata con ASP.NET AJAX, oppure JQuery, MooTools o qualsiasi altro metodo di implementazione. Questa soluzione prevede la consocenza della metodologia AJAX e una seppur piccola parte di programmazione lato Client per la modifica dell’interfaccia e l’interazione con l’utente.

Precisando che la soluzione AJAX è molto valida e che permette elasticità e personalizzazione, voglio proporre comuque una terza soluzione, basata prevalentemente su ASP.NET senza ausilio di javascript.

ASP.NET BackGroundProcess

Rivediamo lo scopo di questa soluzione: 

  1. Eseguire metodi che impiegano diverso tempo per l’esecuzione (long running task) in maniera asincrona sul server.
  2. Mantenere un’interazione con l’utente senza bloccare l’interfaccia web sul browser
  3. Informare l’utente sul procedere delle operazioni e sull’esito finale (polling).

Ecco come intendo risolvere:

  1. Utilizzo un metodo di una classe sul server che lancia il task utilizzando un nuovo Thread che girerà indipendente dal thread della pagina web.
  2. Per semplicità e perché per questo esempio voglio evitare l’utilizzo di javascript, utilizzerò un semplicissimo UpdatePanel
  3. Con un Timer nella pagina web, andrò a controllare l’andamento del processo e lo visualizzerò sulla pagina.

   

Precisazione

Navigando sul web, ho visto spesso fare confuzione tra pagine/medodi (task) asincroni, e metodi (task) in background.
L’utilizzo di pagine asincrone che utilizzano ad esempio RegisterAsyncTask, o l’interfaccia IHttpAsyncHandler, non ha niente a che fare con l’eseguire thread separati. Rendere le pagine asincrone significa poter eseguire il thread della pagina fino al PreRenderComplete, per poi semplicemente permette l’utilizzo di più chiamate asincrone e l’implementazione di callback. Il nuovo thread principale per il render finale della pagina non partirà comunque fino a che tutte le chiamate asincrone non sono finite. Da parte dell’utente quindi non si avverte nessuna differenza.

La classe BackgroudProcess

Purtroppo non è possibile utilizzare la classe System.ComponentModel.BackgroundProcess messa a disposizione dal framework, poiché il risultato sarebbe identico a quanto spiegato appena sopra. Allora implemento una nuova classe simile che ci aiuterà nella gestione del problema:

using System.Threading;

public class BackgroundWorker
{
    Thread _innerTH = null;
    private int _progress;
    public int Progress
    {
        get { return _progress; }
    }

    private object _result;
    public object Result
    {
        get { return _result; }
    }

    public bool IsRunning {
        get
        {
            if (_innerTH != null)
                return _innerTH.IsAlive;
            return false;
        }
    }
    public BackgroundWorker()
    {
        _progress = 0;
        _result = null;
    }
    public delegate void DoWorkerEvH(ref int progress, ref object result, params object[] args);
    public event DoWorkerEvH DoWork;
    public void RunWorker(params object[] args)
    {
        _innerTH = new Thread(() =>
        {
            _progress = 0;
            DoWork.Invoke(ref _progress, ref _result, args);
            _progress = 100;
        });
        _innerTH.Start();
    }
}

Come potete vedere dal codice la classe si aspetta, tramite un delegato, il metodo da eseguire in background. Questa operazione potrebbe essere l’invio di numerose mail, l’accesso al databese per leggere e/o elaborare migliaia di dati, l’esecuzione di un pacchetto SSIS, ecc, ecc.

La pagina chiamante

Per semplicità metto un metodo nella pagina aspx stessa, che molto elementarmente impiega 10 secondi per essere eseguito:

public partial class _Default : System.Web.UI.Page
{

    void worker_DoWork(ref int progress, ref object result, params object[] args)
    {
        string input = string.Empty;
        if (args.Length > 0)
            input = args[0].ToString();

        //una operazione che richiede tempo
        //esempio un ciclo di 10 secondi
        for (int i = 0; i < 100; i++)
        {
            Thread.Sleep(100);
            progress++;
        }
        result = string.Format("Input: \"{0}\". - Operazione completata.", input);
    }

    protected void Timer1_Tick(object sender, EventArgs e)
    {
        BackgroundWorker worker = (BackgroundWorker)Session["worker"];
        if (worker != null)
        {
            //visualizzare il progress con una semplice label o con una progressBar
            lblProgress = string.Concat("percentuale: ", worker.Progress, "%");

            Timer1.Enabled = worker.Progress < 100;
        }
    }
    protected void btnStart_Click(object sender, EventArgs e)
    {
        BackgroundWorker worker = new BackgroundWorker();
        worker.DoWork += new BackgroundWorker.DoWorkerEvH(worker_DoWork);
        worker.RunWorker("eventuale parametro di input");
        Session["worker"] = worker;
        Timer1.Enabled = true;
    }
}

 La classe che si occupa di eseguire il thread e memorizza lo stato di avanzamento, viene salvata nella sessione; in questo esempio il tick del timer legge da questo dato in sessione ed aggiorna l’inerfaccia.

Infine, per completezza, ecco la pagina aspx:

<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="ScriptManager1" runat="server">
    </asp:ScriptManager>
    <div>
        <asp:UpdatePanel ID="UpdatePanel1" runat="server">
        <ContentTemplate>
            <asp:Timer ID="Timer1" runat="server" ontick="Timer1_Tick">
            </asp:Timer>

            <asp:Label ID="lblProgress" Text="" runat="server" /><br />

            Inserisci un parametro:<asp:TextBox ID="txtParameter" runat="server"></asp:TextBox><br />
            <asp:Button ID="btnStart" runat="server" Text="Run Process"
                onclick="btnStart_Click" />
            </ContentTemplate>
        </asp:UpdatePanel>
    </div>
    </form>
</body>

Punti di sviluppo

Da questo semplice esempio, si può espandere il codice utilizzando ad esempio un mix tra codice ASP.NET e javacript per l’interfaccia, ed aggiungere chiamate AJAX al posto del timer per il polling verso il server. Oppure possiamo aggiornare la soluzione utilizzando WCF. O ancora migliorare la classe BackgroundWorker in modo da utilizzare un Pool e poter eseguire più thread contemporaneamente.
Ho trovato a tal proposito, dei link interessanti che implementano appunto soluzioni più complete e complesse. Buona visione.

Long running Task with feedback by Peter Bromberg
Long running Tasks in ASP.NET by Blake Anderton (Persistent Task, WCF)
Long-Running Processes using ASP.NET AJAX by Kirill Chilingarashvili (ThreadPool, AJAX)