Разработка страниц ожидания (waiting pages) в ASP.NET для мониторинга длительных процессов

вторник, 29 июня 2010, Александр Краковецкий

Это перевод статьи Гайдара Магданурова, опубликованной на сайте simple-talk и посвященной разработке страниц ожидания для мониторинга длительных процессов в ASP.NET приложениях.

Пользователи веб-сайтов очень не любят, когда при выполнении длинного процесса они не знают, выполняется процесс в данный момент или приложении просто зависло. Поэтому приложения должны отображать информацию о состоянии процесса для того, чтобы пользователь не закрыл преждевременно страницу, так и не дождавшись окончания.

Если задача выполняется синхронно, то у вас нет выбора - пользователь должен дождаться окончания загрузки страницы в любом случае. Но если задача у вас выполнятся асинхронно, то есть несколько вариантов, как организовать процесс выполнения задачи.

Архитектура страниц ожидания

Перед тем, как приступить к написанию страниц ожидания, мы должны разработать архитектуру приложения. Самое простое решение показано на рисунке 1.

Рисунок 1. Архитектура страницы

Основная страница запускает новый поток и присваивает ему уникальный ID, потом перенаправляет пользователя на страницу ожидания, передает ей ID для того, чтобы следить за выполнением задачи и потоком. Процесс отсылает результаты специальному контроллеру, который содержит коллекцию имя-значение для слежения за результатами с разными ID. Страница ожидания запрашивает контроллер или задача еще выполняется или уже завершилась.

Решения

C архитектурой мы определились, можно приступать к реализации Начнем с более простого решения и плавно перейдем к более сложным.

Самое простое решение

Самое простое решение не требует отслеживание выполнения асинхронного процесса, лишь показывает два состояния - задача еще выполняется или уже завершилась.

Мы должны создать объект контроллера, который будет показывать страницу ожидания с информацией о состоянии задачи.

Controller

using System;using System.Collections;  public static class SimpleProcessCollection{    private static Hashtable _results = new Hashtable();     public static string GetResult(Guid id)    {        if (_results.Contains(id))        {            return Convert.ToString(_results[id]);        }        else        {            return String.Empty;        }    }     public static void Add(Guid id, string value)    {        _results[id] = value;    }     public static void Remove(Guid id)    {        _results.Remove(id);    }}

Основная страница должна передать уникальный ID и передать его странице ожидания, после чего она будет обращаться к SimpleProcessCollection для получения результата. После окончания выполнения задачи в коллекцию SimpleProcessCollection необходимо добавить соответствующую информацию для оповещения страницы о завершении процесса.

Основная страница

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Start.aspx.cs" 
                                                         Inherits="Simple_Start" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
                         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" ><head runat="server">    <title>Simple Waiting Page</title></head><body>    <form id="form1" runat="server">    <div>        <asp:Button ID="btnStart" runat="server" Text="Start Long-Running Process" 
        OnClick="btnStart_Click" />    </div>    </form></body></html>using System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls;using System.Threading; public partial class Simple_Start : System.Web.UI.Page{    protected Guid id;     protected void btnStart_Click(object sender, EventArgs e)    {        // assign an unique ID        id = Guid.NewGuid();        // start a new thread        ThreadStart ts = new ThreadStart(LongRunningProcess);        Thread th = new Thread(ts);        th.Start();        // redirect to waiting page        Response.Redirect("Status.aspx?ID=" + id.ToString());    }     // this is a stub for a asynchronous process    protected void LongRunningProcess()    {        // do nothing actually, but there should be real code         // for instance, there could be a call for a remote web service        Thread.Sleep(9000);        // add result to the controller        SimpleProcessCollection.Add(id, "Some result.");    }}

Страница ожидания

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Status.aspx.cs" 
                                                         Inherits="Simple_Status" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
                          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head runat="server">    <title>Simple Waiting Page</title></head><body>    <form id="form1" runat="server">        <div>        <p align="center">            <asp:Image ID="ImageStatus" ImageUrl="~/Images/Wait.gif" 
                                                              runat="server" /></p>            <h1>                <asp:Label ID="lblStatus" runat="server" 
                                      Text="Working... Please wait..."></asp:Label>            </h1>        </div>    </form></body></html> using System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls; public partial class Simple_Status : System.Web.UI.Page{    protected void Page_Load(object sender, EventArgs e)    {        // chech if the page was called properly        if (Request.Params["ID"] != null)        {            Guid id = new Guid(Request.Params["ID"]);            // check if there is a result in the controller            if (SimpleProcessCollection.GetResult(id) == String.Empty)            {                // no result - let's refresh again                Response.AddHeader("Refresh", "3");            }            else            {                // everything's fine, we have the result                lblStatus.Text = "Job is done.";                ImageStatus.Visible = false;            }        }        else        {            Response.Redirect("Start.apsx");        }    }}

Вы можете убедиться, что решение очень простое и содержит немного кода. Мы можем использовать gif файл с анимацией, благодаря чему пользователь будет уверен, что процесс выполняется.

 

Рисунок 2. Простейшая страница ожидания

Ожидание более одного процесса

Если есть больше одного процесса, то необходимо реализовать прогресс бар. Для этого мы можем реализовать простой счетчик процессов, которые в данный момент запущены, как это показано в примере ниже.

Controller

public static class MultiProcessCollection{    private static Dictionary<Guid, int> _results = 
                        new Dictionary<Guid, int>();     public static int GetProgress(Guid id)    {        if (_results.ContainsKey(id))        {            return _results[id];        }        else        {            return 0;        }    }     public static bool IsCompleted(Guid id)    {        return (GetProgress(id) == 0);    }     public static void Add(Guid id)    {        if (!_results.ContainsKey(id))        {            _results.Add(id, 0);        }        _results[id] = _results[id] + 1;    }     public static void Remove(Guid id)    {        if (_results.ContainsKey(id) && _results[id] > 0)        {            _results[id] = _results[id] - 1;        }    }}

Этот временной контроллер увеличивает счетчик когда новый процесс регистрируется и уменьшает счетчик когда приходит уведомление о завершении какого-то процесса.

Основная страница

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Start.aspx.cs" 
                                                          Inherits="Simple_Start" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
                          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" ><head runat="server">    <title>Progress Waiting Page</title></head><body>    <form id="form1" runat="server">    <div>        <asp:CheckBox ID="cbProgress" runat="server" Text="Show Progress" /> &nbsp;         <asp:Button ID="btnStart" runat="server" 
              Text="Start Three Long-Running Processes" OnClick="btnStart_Click" />    </div>    </form></body></html> using System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls;using System.Threading; public partial class Simple_Start : System.Web.UI.Page{    protected Guid id;     protected void btnStart_Click(object sender, EventArgs e)    {        id = Guid.NewGuid();         MultiProcessCollection.Add(id);        ThreadStart ts = new ThreadStart(LongRunningProcess1);        Thread th = new Thread(ts);        th.Start();         MultiProcessCollection.Add(id);        ts = new ThreadStart(LongRunningProcess2);        th = new Thread(ts);        th.Start();         MultiProcessCollection.Add(id);        ts = new ThreadStart(LongRunningProcess3);        th = new Thread(ts);        th.Start();         if (cbProgress.Checked)        {            Response.Redirect("Progress.aspx?ID=" + id.ToString());        }        else        {            Response.Redirect("Status.aspx?ID=" + id.ToString());        }    }     protected void LongRunningProcess1()    {        Thread.Sleep(3000);        MultiProcessCollection.Remove(id);    }    protected void LongRunningProcess2()    {        Thread.Sleep(7000);        MultiProcessCollection.Remove(id);    }    protected void LongRunningProcess3()    {        Thread.Sleep(5000);        MultiProcessCollection.Remove(id);    }}

Страница ожидания

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Status.aspx.cs" 
                                                         Inherits="Simple_Status" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
                          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head runat="server">    <title>Progress Waiting Page</title></head><body>    <form id="form1" runat="server">        <div>            <h1>                <asp:Label ID="lblStatus" runat="server" 
                                       Text="Working... Please wait..."></asp:Label>            </h1>        </div>    </form></body></html> using System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls; public partial class Simple_Status : System.Web.UI.Page{    protected void Page_Load(object sender, EventArgs e)    {        if (Request.Params["ID"] != null)        {            // check for result            Guid id = new Guid(Request.Params["ID"]);            if (!MultiProcessCollection.IsCompleted(id))            {                Response.AddHeader("Refresh", "1");            }            else            {                lblStatus.Text = "Job is done.";            }        }        else        {            Response.Redirect("Start.aspx");        }    }}

Мы можем сделать работу пользователя с нашей системой более удобной, показывая прогресс бар выполнения процесса. Для этого реализуем простой прогресс бар и воспользуемся им на нашей странице ожидания.

Элемент управления Progress Bar

using System;using System.Data;using System.Configuration;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls;using System.Drawing; namespace MyControls{     public class SimpleProgressControl : WebControl    {        private int _Max;         public int Max        {            get { return _Max; }            set { _Max = value; }        }         private int _Value;         public int Value        {            get { return _Value; }            set { _Value = value; }        }         protected override void Render(HtmlTextWriter writer)        {            writer.AddAttribute(HtmlTextWriterAttribute.Width,
                this.Width.Value.ToString());            writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "0");            writer.AddAttribute(HtmlTextWriterAttribute.Cellspacing, "0");            writer.RenderBeginTag(HtmlTextWriterTag.Table);             writer.AddAttribute(HtmlTextWriterAttribute.Height, 
                                  this.Height.Value.ToString());            writer.RenderBeginTag(HtmlTextWriterTag.Tr);             for (int i = 0; i < _Max; i++)            {                // background color                if (i < _Value)                    writer.AddAttribute(HtmlTextWriterAttribute.Bgcolor,
                         ColorTranslator.ToHtml(this.ForeColor));                else                    writer.AddAttribute(HtmlTextWriterAttribute.Bgcolor,
                              ColorTranslator.ToHtml(this.BackColor));                 writer.RenderBeginTag(HtmlTextWriterTag.Td);                writer.RenderEndTag(); // td            }             writer.RenderEndTag(); // tr            writer.RenderEndTag(); // table        }      }}

Модифицированная страница ожидания

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Progress.aspx.cs" 
                                                       Inherits="Multi_Progress" %><%@ Register TagPrefix="my" Namespace="MyControls" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
                         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head runat="server">    <title>Progress Waiting Page</title></head><body>    <form id="form1" runat="server">        <div>        <my:SimpleProgressControl ID="ctlProgress" runat="server" 
     BackColor="blue" ForeCOlor="red" Max="3" Value="0" Width="300" Height="30" />        <h1>            <asp:Label ID="lblComplete" runat="server" Text="Process complete." 
                                                 Visible="false"></asp:Label></h1>        </div>    </form></body></html> using System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls; public partial class Multi_Progress : System.Web.UI.Page{    protected void Page_Load(object sender, EventArgs e)    {        if (Request.Params["ID"] != null)        {            Guid id = new Guid(Request.Params["ID"]);            int p = MultiProcessCollection.GetProgress(id);            ctlProgress.Value = 3 - p;            if (p != 0)            {                Response.AddHeader("Refresh", "1");            }            else            {                lblComplete.Visible = true;            }        }        else        {            Response.Redirect("Start.aspx");        }    }}

Эта страница будет выглядеть так, как это показано на рисунку 3.

Рисунок 3. Страница ожидания

Возвращение пользовательских объектов с данными асинхронными процессами

Следующим этапом является разработка модификация страниц для отображения дополнительной информации о состоянии процесса.

Например, если процесс выполнения состоит из нескольких этапов, то на странице ожидания можно показывать процент выполнения - это значение можно хранить в каком-то объекте.

Пользовательский объект, который хранит данные

using System;using System.Data;using System.Configuration;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls;  public class FeedbackObject{     private string _result1 = String.Empty;     public string Result1    {        get { return _result1; }        set { _result1 = value; }    }    private string _result2 = String.Empty;     public string Result2    {        get { return _result2; }        set { _result2 = value; }    }     private int _progress = 0;     public int Progress    {        get { return _progress; }        set { _progress = value; }    }      public bool Complete    {        get { return (_progress == 100); }    }} 

Controller

using System;using System.Collections;  public static class FeedbackProcessCollection{    private static Hashtable _results = new Hashtable();     public static FeedbackObject GetResult(Guid id)    {        if (_results.Contains(id))        {            return (FeedbackObject)_results[id];        }        else        {            return null;        }    }     public static void Add(Guid id, FeedbackObject stat)    {        _results[id] = stat;    }     public static void Remove(Guid id)    {        _results.Remove(id);    }}

Основная страница

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Start.aspx.cs" 
                                                          Inherits="Simple_Start" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
                          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" ><head runat="server">    <title>Feedback Waiting Page</title></head><body>    <form id="form1" runat="server">    <div>        <asp:Button ID="btnStart" runat="server" Text="Start Long-Running Process" 
                                                        OnClick="btnStart_Click" />    </div>    </form></body></html> using System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls;using System.Threading; public partial class Simple_Start : System.Web.UI.Page{    protected Guid id;     protected void btnStart_Click(object sender, EventArgs e)    {        id = Guid.NewGuid();        ThreadStart ts = new ThreadStart(LongRunningProcess);        Thread th = new Thread(ts);        th.Start();         Response.Redirect("Status.aspx?ID=" + id.ToString());    }     protected void LongRunningProcess()    {        FeedbackObject fo = new FeedbackObject();        FeedbackProcessCollection.Add(id, fo);        for (int i = 0; i <= 100; i++)        {            Thread.Sleep(50);            if (i == 100)            {                fo.Result1 = "First result.";                fo.Result2 = "Second result.";            }            fo.Progress = i;        }    }}

Страница ожидания

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Status.aspx.cs" 
                                                         Inherits="Simple_Status" %><%@ Register TagPrefix="my" Namespace="MyControls" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
                          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head runat="server">    <title>Feedback Waiting Page</title></head><body>    <form id="form1" runat="server">        <div>            <my:simpleprogresscontrol id="ctlProgress" runat="server"                                backcolor="blue" forecolor="red" max="100" 
                                value="0" width="300" height="30" />            <h1>                <asp:Label ID="lblComplete" runat="server" 
                  Text="Process complete." Visible="false"></asp:Label></h1>        </div>    </form></body></html> using System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls; public partial class Simple_Status : System.Web.UI.Page{    protected void Page_Load(object sender, EventArgs e)    {        if (Request.Params["ID"] != null)        {            // check for result            Guid id = new Guid(Request.Params["ID"]);                        ctlProgress.Value = FeedbackProcessCollection.GetResult(id).Progress;             if (!FeedbackProcessCollection.GetResult(id).Complete)            {                Response.AddHeader("Refresh", "1");            }            else            {                FeedbackObject fo = FeedbackProcessCollection.GetResult(id);                lblComplete.Text = fo.Result1 + " " + fo.Result2;                lblComplete.Visible = true;            }        }        else        {            Response.Redirect("Start.aspx");        }    }}

Теперь мы можем увидеть процесс выполнения задачи в процентном соотношении.

Добавление возможностей Ajax

Для добавления возможностей ASP.NET AJAX необходимо добавить на страницу элементы управления ScriptManager и UpdateProgress.

Страница ожидания

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Status.aspx.cs" 
                                                        Inherits="Simple_Status" %> <%@ Register TagPrefix="my" Namespace="MyControls" %><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
                         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head runat="server">    <title>Feedback Waiting Page</title></head><body>    <form id="form1" runat="server">        <asp:ScriptManager ID="ScriptManager1" runat="server"                                   EnablePartialRendering="true">        </asp:ScriptManager>        <div>            <asp:UpdatePanel ID="UpdatePanel1" runat="server" 
                                                        ChildrenAsTriggers="true">                <ContentTemplate>                    <asp:Timer ID="Timer1" runat="server" Interval="1000" 
                                                             OnTick="Timer1_Tick">                    </asp:Timer>                    <my:SimpleProgressControl ID="ctlProgress" runat="server" 
                                                  BackColor="blue" ForeColor="red"                                    Max="100" Value="0" Width="300" Height="30" />                    <h1>                        <asp:Label ID="lblComplete" runat="server" 
                        Text="Process complete." Visible="false"></asp:Label></h1>                </ContentTemplate>            </asp:UpdatePanel>        </div>    </form></body></html> using System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls; public partial class Simple_Status : System.Web.UI.Page{    protected void Page_Load(object sender, EventArgs e)    {        if (Request.Params["ID"] == null)        {            Response.Redirect("Start.aspx");        }    }    protected void Timer1_Tick(object sender, EventArgs e)    {        // check for result        Guid id = new Guid(Request.Params["ID"]);         ctlProgress.Value = FeedbackProcessCollection.GetResult(id).Progress;         if (FeedbackProcessCollection.GetResult(id).Complete)            lblComplete.Visible = true;    }} 

Теперь не вся страница ожидания будет обновляться, а лишь ее отдельная часть.

Исходный код можно загрузить здесь.


Ищите нас в интернетах!

Комментарии

Свежие вакансии