Introduction
This article describes the process to create a reusable progress bar that can be controlled at the server or client side.
Background
In web UI development, it is sometimes necessary to display resource usage information. Some applications for progress bars include displaying the amount of messages in a queue in relation to the queue size or displaying the number of current logged-in users in relation to the maximum users to date. The possibilities are endless. In my first attempt at creating an HTML based progress bar, I used span
elements in an ASP.NET server page to visually represent the progress bar. I then used an iframe
to embed that server page where I wanted to display the progress bar. This approach had a severe drawback from very complicated span
nesting in order to display the progress bar correctly and style interactions from the containing page. There were also accuracy problems in displaying the filled portion of the progress bar. There are also several issues with using an embedded iframe
element, some of them not trivial. I then decided to create a reusable ASP.NET server control to host the progress bar. The span
elements have all the same drawbacks, however, so I then decided to find a better approach and began researching ASP.NET bar graph examples, which are very similar to a progress bar. I decided to go with a fairly robust solution using HTTP handlers and images rendered "on the fly". This is the approach I will describe in this article.
Using the code
I'll describe the following topics in this section.
- Basic server control creation.
- Progress bar server control properties.
- Rendering the progress bar server control.
- HTTP handler
CustomImageHandler
to handle images rendered "on the fly".
- Using the progress bar on a server page.
Basic server control creation
Create a new class derived from System.Web.UI.WebControls.WebControl
and add the using
statements as shown below:
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.Design;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Design;
using System.ComponentModel.Design;
using System.ComponentModel.Design.Serialization;
using System.IO;
using System.Math;
namespace YourNamespace
{
public class ProgressBar : System.Web.UI.WebControls.WebControl
{
}
}
Next, add the following attribute to the top of the class file (under the using
statements).
[assembly: TagPrefix("YourNamespace", "ShortName")]
Replace YourNamespace
and ShortName
with values that make sense in your project. This attribute controls what the visual designer places inside the <@Register>
tag on the server page when dragging the server control from the toolbox. It allows for a naming scheme that is more appropriate for your application. I'll cover this in more detail in the Using the progress bar section.
Add the following code to the top of the class declaration:
[DefaultProperty("Type")]
[ToolboxData("<{0}:ProgressBar runat="server"></{0}:ProgressBar>")]
[ToolboxBitmapAttribute( typeof(ProgressBar) ) ]
public class ProgressBar : System.Web.UI.WebControls.WebControl
{
Feel free to add/replace the comment code above the declaration to suit.
The DefaultProperty
attribute declares the Type
property to be the default property. This allows you to reference the property implicitly:
ProgressBar1 = BarType.Horizontal;
The ToolboxData
attribute declares the text that the toolbox will add to the server page when dragged onto the page. The {0}
portion of the attribute description is a replacement variable for the registered namespace of this control. This is controlled by the TagPrefix
attribute described above. The following will be added by the visual designer when the control is dropped onto a server page:
<ShortName:ProgressBar id="ProgressBar1"
runat="server"></ShortName:ProgressBar>
The ToolboxBitmapAttribute
attribute describes the bitmap to be displayed inside the toolbox. If you provide just a class type, without a name, the visual designer will search the assembly for a 16x16 bitmap of that name. You can create a 16x16 bitmap and add it to the project as an embedded resource in order to represent your control, otherwise the visual designer will use a default bitmap.
You now have a basic control that will be rendered in the toolbox and will populate your server page correctly when dragged from the toolbox. I will cover how to add this control to your toolbox in the Using the progress bar section. In the next section, I will cover adding properties to the progress bar control.
Progress bar server control properties
The following properties have been identified as necessary for basic progress bar rendering: Type
, Height
, Width
, FillColor
, BackColor
, Border
, BarSize
, FillPercent
, and Format
.
The Type
property is an enumeration that is designated as follows:
public enum BarType
{
Horizontal,
Vertical
}
The properties Height
, Width
, and BarSize
are all Unit value types and describe the height, width, and internal size of the progress bar respectively.
FillPercent
is a float
value which specifies how much of the bar to display.
The properties BackColor
and FillColor
are unsigned long values which specify the background color and bar/fill color respectively.
The property Format
is an enumeration value that is designated as follows:
public enum BarFormat
{
Gif,
Jpeg,
Png,
Bmp
}
Here's the code for those properties:
#region Private member variables
BarFormat format;
BarType type;
bool border;
System.Drawing.Color fillColor;
double fillPercent;
Unit barSize;
#endregion
#region Public properties
[DefaultValue(BarFormat.Png), Category("ProgressBar Common"),
Description("Compression format for image.")]
public BarFormat Format
{
get { return format; }
set { format = value; }
}
[DefaultValue(false), Category("ProgressBar Common"),
Description("Flag to show border or not.")]
public bool Border
{
get { return border; }
set { border = value; }
}
[DefaultValue(BarType.Horizontal), Category("ProgressBar Common"),
Description("Type of progress bar to render.")]
public BarType Type
{
get { return type; }
set { type = value; }
}
[Category("ProgressBar Common"),
Description("Background color of progress bar.")]
public System.Drawing.Color FillColor
{
get { return fillColor; }
set { fillColor = value; }
}
[DefaultValue(0.0), Category("ProgressBar Common"),
Description("Fill percentage of progress bar.")]
public double FillPercent
{
get { return fillPercent; }
set { fillPercent = value; }
}
[DefaultValue(0.0), Category("ProgressBar Common"),
Description("Size of progress bar inside frame")]
public Unit BarSize
{
get { return barSize; }
set { barSize = value; }
}
false)>
public string ContentType
{
get { return "image/" + Format.ToString().ToLower(); }
}
The last property ContentType
is declared as non-browsable using the Browsable(false)
attribute. This property is used by the HTTP handler class described in the section HTTP handler CustomImageHandler.
Rendering the progress bar server control
There are two modes for rendering an ASP.NET server control. The first, DesignMode
, provides a method for the control to represent itself in the Visual Designer. Since the progress bar control consists of an image rendered on the fly, an accurate representation cannot be specified, so a default representation can be used:
protected override void Render(HtmlTextWriter output)
{
if (this.Site != null && this.Site.DesignMode )
{
output.Write(string.Format("<font size='1' " +
"color='SeaGreen' face='arial'> [ProgressBar::{0}]" +
"</font><br>", ID) );
}
Rendering the control at runtime requires an introduction to the next topic - using an HTTP handler to handle displaying the images rendered on the fly. This handler will intercept requests for a certain webpage, which doesn't exist, but is specified within an <img>
tag generated by the progress control. I will go into detail about this handler in the next section. The code to generate the <img>
tag is shown below:
else
{
string uniqueName = GenerateUniqueName();
Page.Application[ProgressBarRenderStream.ImageNamePrefix + uniqueName] = this;
string toolTip = string.Format( "{0}",
Enabled ? ((ToolTip == null || ToolTip == string.Empty) ?
Round(FillPercent, 2).ToString()+"%" : ToolTip) : "");
output.Write(
string.Format("<img src='{0}?id={1}' " +
"border='{2}' height='{3}' width='{4}' alt='{5}'>",
ProgressBarRenderStream.ImageHandlerRequestFilename,
uniqueName, (this.Border ? "1" : "0"),
(int)this.Height.Value, (int)this.Width.Value,
toolTip );
}
string GenerateUniqueName()
{
string sControlName = System.Guid.NewGuid().ToString();
return sControlName;
}
This code will generate a unique name which will identify the progress bar class currently rendering the output. This unique name is used as the src
attribute in an <img>
tag to allow the HTTP handler to find the class later. The values ProgressBarRenderStream.ImageNamePrefix
and ProgressBarRenderStream.ImageHandlerRequestFilename
are declared in the HTTP handler class and will be described in the next section. This code should generate an HTML sequence similar to the following (the values will be different depending on property values and the GUID value generated):
<img src='image_stream.aspx?id=89f7f4ed-7433-460d-ae97-53c22aa2a232'
border='1' height='8' width='128' alt='25.5%'>
This is still not enough to render the control. The following functions will take care of drawing the progress bar and rendering it to a memory stream:
public MemoryStream RenderProgressBar()
{
try
{
if( Site != null && Site.DesignMode )
{
string sType = this.Type.ToString();
return null;
}
else
{
return makeProgressBar();
}
}
catch
{
return null;
}
}
private MemoryStream makeProgressBar()
{
Bitmap bmp = new Bitmap((int)Width.Value,
(int)Height.Value, PixelFormat.Format32bppArgb);
MemoryStream memStream = new MemoryStream();
Brush fillBrush = new SolidBrush( FillColor );
Brush backgroundBrush = new SolidBrush( this.BackColor );
System.Drawing.Graphics graphics =
Graphics.FromImage( bmp );
graphics.FillRectangle(backgroundBrush, 0,
0, (int)Width.Value, (int)Height.Value);
double fillAmount;
if( this.Type == BarType.Horizontal )
{
fillAmount = Width.Value * (FillPercent/100.0);
graphics.FillRectangle(fillBrush, 0,
((int)Height.Value - (int)BarSize.Value)/2,
(int)fillAmount, (int)BarSize.Value);
}
else
{
fillAmount = Height.Value * (FillPercent/100.0);
graphics.FillRectangle(fillBrush,
((int)Width.Value - (int)BarSize.Value)/2,
(int)Height.Value-(int)fillAmount,
(int)BarSize.Value, (int)Height.Value);
}
graphics.Save();
System.Drawing.Imaging.ImageFormat imgformat =
System.Drawing.Imaging.ImageFormat.Png;
switch( Format )
{
case BarFormat.Bmp:
imgformat = ImageFormat.Bmp;
break;
case BarFormat.Gif:
imgformat = ImageFormat.Gif;
break;
case BarFormat.Jpeg:
imgformat = ImageFormat.Jpeg;
break;
case BarFormat.Png:
imgformat = ImageFormat.Png;
break;
}
bmp.Save(memStream, imgformat);
return memStream;
}
This code will be called by the ProgresBarRenderStream
class which is described in the next section.
Rendering images on the fly using an HTTP handler
In order to render images on the fly, it is necessary to intercept the HTTP request and look for a specific web page which does not exist. This web page name is specified by the progress bar server control during rendering. The HTTP handler will find that value from the query string after it detects the "special" web page. The handler can then use this value to look up the requested progress bar control for rendering on the fly.
There are two steps necessary to provide a custom HTTP handler. First, create a class derived from IHttpModule
. This code is shown below:
public class ProgressBarRenderStream : IHttpModule
{
public const string ImageHandlerRequestFilename="image_stream.aspx";
public const string ImageNamePrefix="i_m_g";
public ProgressBarRenderStream()
{
}
public virtual void Init( HttpApplication httpApp )
{
httpApp.BeginRequest += new EventHandler(httpApp_BeginRequest);
}
public virtual void Dispose()
{
}
private void httpApp_BeginRequest(object sender, EventArgs e)
{
HttpApplication httpApp = (HttpApplication)sender;
ProgressBar pb = null;
if( httpApp.Request.Path.ToLower().IndexOf(
ImageHandlerRequestFilename) != -1 )
{
pb = (ProgressBar)httpApp.Application[ImageNamePrefix +
(string)httpApp.Request.QueryString["id"]];
if( pb == null )
{
return;
}
else
{
try
{
System.IO.MemoryStream memStream = pb.RenderProgressBar();
memStream.WriteTo(httpApp.Context.Response.OutputStream);
memStream.Close();
httpApp.Context.ClearError();
httpApp.Context.Response.ContentType = pb.ContentType;
httpApp.Response.StatusCode = 200;
httpApp.Application.Remove(ImageNamePrefix +
(string)httpApp.Request.QueryString["id"]);
httpApp.Response.End();
}
catch(Exception ex)
{
ex = ex;
}
}
}
}
}
The code above illustrates the shared values that the ProgressBar
control uses when rendering the <img>
tag. These values are then used in the httpApp_BeginRequest
function which is registered in the Init
function:
httpApp.BeginRequest += new EventHandler(httpApp_BeginRequest);
The final step required is to add the HTTP handler to your web config file. This must be added to your root level web config file in order to work properly.
<httpModules>
<add name="ProgressBarRenderStream"
type="YourNamespace.ProgressBarRenderStream,YourAssemblyName" />
</httpModules>
Using the progress bar on a server page
Using the progress bar on a server page is easy. First, add a reference to the control in your project file. Next, add the following directive to the top of your server page:
<%@ Register TagPrefix="ShortName"
Namespace="YourNamespace" Assembly="YourAssemblyName" %>
Finally, add the control to your page and change the parameters to suit:
<ShortName:ProgressBar id="ProgressBar1" runat="server"></ShortName:ProgressBar>
Conclusion
I started my research thinking there was an easy solution to my problem - provide direct feedback of a process using a graphical representation. Digging further, I realized that what I was trying to do was very difficult for a server control to do by itself without compromising security on the site. Further research led me to the HTTP handler method of dealing with images rendered on the fly. This method is powerful and can be extended to other applications, limited only by your imagination.