I wrote this for an ASP.NET portal, but they didn't use it. So, to the blog it goes.
A progress bar…in a web application?
Sure, it’s possible. In fact, progress bars have been in use for about as long as server-side programming has existed. Typically, however, these are meta-refresh based progress bars. These are the progress bars that use the
tag to send a request to the server every X seconds to get the updated visual representation of the bar. Sure, it works, but results in a “white out” screen refresh (which looks clunky) and does not allow “interactive” progress (meaning that the progress bar can be modified in server-side code and it is immediately displayed in the browser without a refresh). It IS possible to achieve this effect however, by leveraging the “old school” ASP Response.Write() method.
Most Microsoft web developers are familiar with the Response.Write() method, which adds data to the response stream. This method has been superseded for most functionality in ASP.NET, but we will revive it for our progress bar. The great thing about using Response.Write() is it causes the data to be sent immediately to the browser (unless Response.Buffer is true, in which case Response.Flush() has to be called as well to force the data to be sent immediately to the browser). This is not the case when using common ASP.NET techniques (such as Label.Text = “hello”;), in which case the data is not sent to the client until the rendering part of the Page execution cycle, when RenderContents is recursively called on all child controls.
So, how is it Response.Write() used? First, on a postback, a long process that requires progress is started. Then, the initial progress bar structure is sent to the client using Response.Write, but the response is not terminated. Each time the progress bar needs to be extended, a bit of javascript is sent to the browser to extend the bar. When the long process is complete, the response is finally terminated. The result is a single loooooooooooooooooong response.
Creating the server control
The server control, since it provides a user interface, will derive from the .NET Framework class System.Web.UI.WebControls.WebControl. This base class will provide extended UI support for our control such as design time behavior, style support, etc.
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Drawing;
using System.Text;
namespace Blah
{
public class ProgressBar : System.Web.UI.WebControls.WebControl
{
}
}
Following the standard Windows forms ProgressBar, it will only add three properties onto the properties derived from WebControl.
• Minimum – The minimum value of the progress bar (typically 0)
• Maximum – The maximum value of the progress bar
• Value – The current value of the Progress
The Minimum and Maximum properties are pretty simple, just saving/restoring the value to/from ViewState in the get/set accessor.
public int Minimum
{
get
{
object o = ViewState["minimum"];
return (o == null) ? 0 : (int)o;
}
set
{
ViewState["minimum"] = value;
}
}
public int Maximum
{
get
{
object o = ViewState["maximum"];
return (o == null) ? 100 : (int)o;
}
set
{
ViewState["maximum"] = value;
}
}
The Value property is a little more complex. Since the user will be setting this property in their code, and expecting the visual update to occur immediately in the browser, special action is required when the property is set.
This special action is as follows. On the first set of the property, the basic progress bar structure is drawn. The _ProgressBarDrawn flag controls this. Then, on each subsequent set, the progress bar is visually updated. When complete, the Value property should look something like the following:
// Flag to monitor whether the progress bar has been drawn
private bool _ProgressBarDrawn = false;
public int Value
{
get
{
object o = ViewState["value"];
return (o == null) ? 0 : (int)o;
}
set
{
ViewState["value"] = value;
if(!_ProgressBarDrawn)
DrawProgressBar();
UpdateProgress();
}
}
So, what is in the DrawProgressBar() and UpdateProgress() functions? The DrawProgressBar() function is only called once per request (thus the previously discussed flag) to write out the initial progress bar structure. This content is immediately sent to the browser to be displayed.
private void DrawProgressBar()
{
Page.Response.Write("<html><body>");
Page.Response.Write(ControlStyle.ToString());
Page.Response.Write("<div id='divProgress' ");
Page.Response.Write("style='WIDTH:" + GetAdjustedValue() + ";");
Page.Response.Write("HEIGHT:" + Height.ToString() + ";");
Page.Response.Write("BACKGROUND-COLOR:" + ColorTranslator.ToHtml(BackColor) + "'>");
Page.Response.Write("</div>");
// Flush the output to the browser if required.
if(this.Page.Response.Buffer) this.Page.Response.Flush();
// Set the flag so the progress bar is not redrawn
_ProgressBarDrawn = true;
}
The UpdateProgress() function is called each time Value is set to write out some JavaScript to change the width of the div element created above to the current value. This is also flushed immediately to the browser.
private void UpdateProgress()
{
// Write out the JavaScript to extend the bar.
Page.Response.Write("<script language='JavaScript'>\r\n");
Page.Response.Write("document.getElementById('divProgress').style.width = '" + GetAdjustedValue() + "';\r\n");
Page.Response.Write("</scr");
Page.Response.Write("ipt>\r\n");
// Flush the output to the browser.
if(this.Page.Response.Buffer) this.Page.Response.Flush();
}
As you can see, both of these functions call a helper function called GetAdjustedValue(). This simply returns the proper value to use for the current progress bar width based on Value, Width, Maximum, and Minimum.
private Unit GetAdjustedValue()
{
int range = Maximum - Minimum;
double ratio = (((double)Value)/((double)range));
int adjustedValue = (int)(Width.Value * ratio);
return new Unit(adjustedValue);
}
Anything else? Yep, we have to override the Render method to suppress the default ASP.NET rendering of the control. Since we are doing our own rendering at run time (when the Value property is set, above), the ASP.NET rendering is redundant, and will seriously mess up the UI.
protected override void Render(HtmlTextWriter writer)
{
}
Why can’t we just render all of the HTML within the Render() method (or RenderContents() method)? Well, as explained above, the progress bar updates visually each time Value is set, so the appropriate JavaScript needs to be sent immediately when Value is set. The Render method is not invoked by the ASP.NET runtime until much later in page cycle, so it is too late to send the HTML at that time. Yes, this is a hack and outside of the expected ASP.NET Page Cycle, but by nature a ProgressBar is contrary to a typical HTTP request/response cycle.
That’s it. Once all of the above properties and methods are in your server control class, you’re done. Compile the server control dll.
Testing the server control
Now create a simple little ASP.NET app to test the control. First create a new WebForm in your favorite development environment, and drag an instance of our control on the form (note, you will not see a visual representation of the control on the design surface, as we did not associate a Designer with the control class. More on this in the “Extending the server control” section below). Also, add a standard ASP.NET button to the WebForm. In the Page.Load event, add the following code to configure the progress bar.
private void Page_Load(object sender, System.EventArgs e)
{
// Configure the ProgressBar
ProgressBar1.Minimum = 0;
ProgressBar1.Maximum = 100;
ProgressBar1.BackColor = Color.Blue;
ProgressBar1.Height = new Unit(20);
ProgressBar1.Width = new Unit(200);
}
Also, within the Button.Click event, add the following code to test the progress. Typically you would do something that would require actual progress (such as a file transfer, database query, webservice call, etc). To keep this sample simple, we will simply loop 100 times, pausing each iteration to simulate some process that takes time.
private void Button1_Click(object sender, System.EventArgs e)
{
// Simulate a process that requires progress
for(int i=0; i<100; i++)
{
// Set the ProgressBar.Value
ProgressBar1.Value = i;
System.Threading.Thread.Sleep(20);
}
}
Compile and run. Click the button. You should see some progress happening!
Why, when I set other style properties (such as BorderColor, etc) it doesn’t show up?
Recall how non-standard rendering techniques were used each time the Value property was set. If we were using the standard technique of putting all of the rendering code within the RenderContents() method, style attributes would be automatically applied (but the dynamic, interactive progress would no longer work). Since we are replacing the built-in rendering logic with our own, we have to also write the logic to handle style application. This is demonstrated in the DrawProgressBar() method, when BackColor, Height, and Width are used to set up the initial progress bar. It would be just as easy to check if BorderColor (and other style properties) were set and modify the rendered HTML to account for these properties.
Extending the server control.
For the sake of brevity, this server control code is kept as simple as possible. However, with a couple of additions (using the techniques demonstrated above) a professional quality product could be achieved.
• As mentioned in the section above, write the logic to account for all WebControl styles (BackColor, BorderColor, CssClass, etc).
• Create a ControlDesigner class so you can provide a better design-time representation of the control.
• Add a Text property containing text that can be updated in real time just like the Value property.
• Add a “segmented progress” mode in addition to the “smooth progress” mode.