Wednesday, November 14, 2007

Dynamic Rounded Panel

This is another variant of the rounded panel creation (the first one you can see here), that uses images to present curved corners. To be more precise, the only image is used, the one that is build based on the specified circle radius, the background color, the border color and width. The panel appearance varies according to all these parameters and can be dynamically changed in runtime.


Picture 1. The structure of the top part of the rounded panel
a - the left part where the upper left part of the image is shown.
b - the middle that is filled with the background color and contains a block element (d). The d element imitates the image border.
c - the right part where the upper right part of the image is shown.

Of course, the middle can be simpler if to use the only block that is filled with the background color and has the upper border. But different browsers interpret borders ambiguously, some of them enlarge total height of the block element. It caused a small complication of the control.

The bottom part of border is build on the same principle, only mirror-like.

The code overview

The rounded panel is inherited from the ordinary ASP.Net Panel, one property is added:
Radius - the circle radius

Besides, the control implements interface IHttpHandler and contains a code for the image creation. It has to be registered in web.config in order to process requests for the ~/roundedpanel.ashx?... url.

Generation and registration of the styles that are required for rendering the panel occur in the overriden OnPreRender method.
private string styleGroupName = null;
//the name for the styles group that is responsible for the panel representation
private string StyleGroupName
{
get
{
if (styleGroupName == null)
{
string key = string.Format("{0}{1}{2}{3}",
this.BorderColor.ToArgb(),
this.BackColor.ToArgb(),
this.BorderWidth,
this.Radius);
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(key);
styleGroupName = Convert.ToBase64String(bytes).Replace("=", "");
}
return styleGroupName;
}
}

//query key for the circle radius
private const string radiusQueryKey = "rd";
//query key for the border width
private const string borderWidthQueryKey = "bw";
//query key for the border color
private const string borderColorQueryKey = "bc";
//query key for the background color
private const string backgroundColorQueryKey = "bg";

protected override void OnPreRender(EventArgs e)
{
if (this.Radius > 0)
{
//creates url to the image showing the curved corners
string imageUrl = string.Format(
"~/roundedpanel.ashx?{0}={1}&{2}={3}&{4}={5},{6},{7}&{8}={9},{10},{11}",
radiusQueryKey,
this.Radius,
borderWidthQueryKey,
this.BorderWidth.Value,
borderColorQueryKey,
this.BorderColor.R,
this.BorderColor.G,
this.BorderColor.B,
backgroundColorQueryKey,
this.BackColor.R,
this.BackColor.G,
this.BackColor.B);
//creates styles
StringBuilder sb = new StringBuilder();
sb.AppendFormat(
@".{0} .a, .{0} .a b, .{0} .d, .{0} .d b
{{display:block;font-size:1px;overflow:hidden;}}
.{0} .a, .{0} .d, .{0} .b, .{0} .e
{{background-image:url({4});background-repeat:no-repeat;height:{1}px;}}
.{0} .b, .{0} .e {{margin-left:{1}px;}}
.{0} .a {{background-position:left top;}}
.{0} .b {{background-position:right top;}}
.{0} .d {{background-position:left bottom;}}
.{0} .e {{background-position:right bottom;}}
.{0} .c, .{0} .f {{background-color:{5};height:{1}px;margin-right:{1}px;}}
.{0} .c b, .{0} .f b {{height:{2}px;background-color:{6};}}
.{0} .f b {{margin-top:{3}px;}}
.{0} .m {{background-color:{5};border-left:solid {2}px {6};border-right:solid {2}px {6};}}",
this.StyleGroupName,
this.Radius,
this.BorderWidth.Value,
this.Radius - this.BorderWidth.Value,
ResolveUrl(imageUrl),
this.BackColor.IsNamedColor ? this.BackColor.Name : "#" + this.BackColor.Name.Substring(2),
this.BorderColor.IsNamedColor ? this.BorderColor.Name : "#" + this.BorderColor.Name.Substring(2));
//adds styles for the panel width, if it is required
if (!this.Width.IsEmpty)
sb.AppendLine(string.Format(".{0}{{width:{1}}}",
this.StyleGroupName, this.Width));

//adds styles for the panel height, if it is required
if (!this.Height.IsEmpty && this.Height.Type == UnitType.Pixel)
sb.AppendLine(string.Format(".{0} .m{{height:{1}px;overflow:visible}}",
this.StyleGroupName, this.Height.Value - this.Radius * 2));
//registers styles on the page
if (!StylesController.IsStyleSheetIncludeRegistered(this.Page, "Rounded Panel"))
StylesController.RegisterStyleSheetInclude(this.Page, "Rounded Panel", sb.ToString());
}
base.OnPreRender(e);
}

StylesController - a class that contains a few methods for registration of the styles created in runtime. It is described in the How to Register Stylesheet Created in Runtime article.

Creation of HTML representing the control occurs in the overriden RenderControl method; the upper, bottom and middle parts are rendered separately.
public override void RenderControl(HtmlTextWriter writer)
{
if (this.Radius > 0)
{
//renders the upper part of the control
writer.Write(string.Format(
@"<div class='{0}'><b class='a'><b class='b'><b class='c'><b></b></b></b></b><div class='m'>",
this.StyleGroupName));
//renders inner controls
base.RenderContents(writer);

//renders the bottom part of the control
writer.Write("</div><b class='d'><b class='e'><b class='f'><b></b></b></b></b></div>");
}
else
base.RenderControl(writer);
}

The image representing curved corners is generated in the ProcessRequest method of the IHttpHandler implementation.

It turns out to be the most complicated part of the work, as for me. The required image has to show a bordered circle filled with the specified background color, the rest of the image has to be transparent. It can be done by means of the transparent gif. My knowledge of GDI+ is rather superficial and examples found on the Internet are too complicated to simply copypaste.
The Bob Powell's excellent resource on GDI+ and especially the article about peculiarities of the different images formats help me.
    Here is the way to draw partially transparent gif image for rounded corners:
  • First of all, you need to create a common bitmap in RGB format and draw the required image on its canvas. It is not possible to draw directly on gif-image because it contains indexed colors palette.
  • Create a gif-image of the same size that the image in RGB format.
  • Change color palette of the gif-image, it has to contain 3 colors: a transparent color, a border color and a background color.
  • Read colors pixel-by-pixel from the RGB-image and set colors for the corresponding images in the indexed image.
  • Save gif-image to the Response.
void IHttpHandler.ProcessRequest(HttpContext context)
{
if (context.Request[radiusQueryKey] != null
&& context.Request[borderWidthQueryKey] != null
&& context.Request[borderColorQueryKey] != null
&& context.Request[backgroundColorQueryKey] != null)
{
int radius = int.Parse(context.Request[radiusQueryKey]);
int borderWidth = int.Parse(context.Request[borderWidthQueryKey]);
string[] args = context.Request[borderColorQueryKey].Split(new char[] { ',' });
Color borderColor = Color.FromArgb(int.Parse(args[0]), int.Parse(args[1]), int.Parse(args[2]));
args = context.Request[backgroundColorQueryKey].Split(new char[] { ',' });
Color bgColor = Color.FromArgb(int.Parse(args[0]), int.Parse(args[1]), int.Parse(args[2]));
//sets the transparent color
Color transparentColor = Color.FromArgb(0, 0, 0, 0);
//draws the image in the 32 bit RGB format

Bitmap source = new Bitmap(radius * 2, radius * 2, PixelFormat.Format32bppRgb);
Graphics g = Graphics.FromImage(source);
g.FillRectangle(new SolidBrush(transparentColor), 0, 0, source.Width, source.Height);
g.FillEllipse(new SolidBrush(bgColor), borderWidth / 2, borderWidth / 2,
source.Width - borderWidth, source.Height - borderWidth);
g.DrawEllipse(new Pen(borderColor, borderWidth), 0 + borderWidth / 2, 0 + borderWidth / 2,
source.Width - borderWidth, source.Height - borderWidth);
//creates the image in the indexed format
Bitmap dest = new Bitmap(source.Width, source.Height, PixelFormat.Format8bppIndexed);

//changes the color palette
ColorPalette pal = dest.Palette;
pal.Entries[0] = transparentColor;
pal.Entries[1] = bgColor;
pal.Entries[2] = borderColor;
dest.Palette = pal;
//sets a rectangle that occupies the whole picture area
Rectangle rect = new Rectangle(0, 0, source.Width, source.Height);
//locks the images in the memory
BitmapData sourceData = source.LockBits(rect, ImageLockMode.ReadOnly, source.PixelFormat);
BitmapData destData = dest.LockBits(rect, ImageLockMode.WriteOnly, dest.PixelFormat);
for (int x = 0; x < source.Width; x++)
{
for (int y = 0; y < source.Height; y++)
{
//reads color of the pixel with coordinates x,y from the source image
Color color = Color.FromArgb(Marshal.ReadInt32(sourceData.Scan0, sourceData.Stride * y + x * 4));
//sets color of the corresponding pixel in the destination image
if (color == bgColor)
Marshal.WriteByte(destData.Scan0, destData.Stride * y + x, 1);
else if (color == borderColor)
Marshal.WriteByte(destData.Scan0, destData.Stride * y + x, 2);
else
Marshal.WriteByte(destData.Scan0, destData.Stride * y + x, 0);
}
}
//unlocks the images
dest.UnlockBits(destData);
source.UnlockBits(sourceData);
source.Dispose();
//writes the indexed image to the Response
context.Response.Clear();
context.Response.ContentType = "image/gif";
dest.Save(context.Response.OutputStream, System.Drawing.Imaging.ImageFormat.Gif);
dest.Dispose();
context.Response.End();
}
}

In the end it is required to register in web.config HttpHandlers that are responsible for drawing the image and the stylesheet registration.
<httpHandlers>
...
<add verb="GET" path="roundedpanel.ashx" type="MyAssembly.MyNamespace.RoundedPanel, MyAssembly"/>
<add verb="GET" path="stylesheet.css" type="MyAssembly.MyNamespace.StylesController, MyAssembly"/>
</httpHandlers>

Source code - 3.5 kB

Thursday, September 27, 2007

How to avoid unexpected postback after pressing Enter in textbox

Problem
You fill a form with many input fields. After filling first textbox you mechanically press Enter and see that the page are submitted to the server. Inasmuch as the rest of the fields stay blank the result of this submit is either saving incomplete data or a few validators of type "Field XXX is required." are shown. The well-known situation, is not it? It is not very good to allow such behaviour in a web application, especially because it easily can be corrected.

Of course, there are situations when this is useful (e.g.: you have a single search box and after typing a search keyword it is very convenient to start search by pressing Enter). But in the most cases such behaviour just
annoys visitors.

Solution
Surprisingly, but this inconvenience caused by built-in browser conveniences. :)

Convenience #1. If a form contains only textbox then regardless of the submit button presence pressing Enter will send the form to the server.
http://msdn2.microsoft.com/en-us/library/ms535249.aspx


Candidate solutions
  • If design allows add one more textbox
  • If design allows set the textbox TextMode to "MultiLine". In this case the textbox is rendered as textarea element instead of input type="text"
  • Add an invisible text field (do not confuse with hidden field). See the below example:
<form ... >
<asp:TextBox ... >
<input type="text" style="display:none">
</form>

Convenience #2.
If the form contains input type="submit" or input type="image" pressing Enter submits the form using the focused input, if any, or first input on the form.
http://msdn2.microsoft.com/en-us/library/ms535840.aspx

Candidate solutions
  • In order to rid of input type="submit" do not use a Button with UseSubmitBehavior="True"
  • In order to rid of input type="image" do not use ImageButton. It can be replaced with the following construction:
<asp:LinkButton ID="btn" runat="server">
<asp:Image id="im" runat=server ImageUrl="..." style="border:0px"/>
</asp:LinkButton>

Appendix. Interrelation between ASP.Net controls and HTML elements.

ASP.Net ControlHTML Element
TextBox (TextMode="SingleLine")input type="text"
TextBox (TextMode="Password")input type="password"
TextBox (TextMode="Multiline")textarea
Button (UseSubmitBehavior="False")input type="button"
Button (UseSubmitBehavior="True")input type="submit"
ImageButtoninput type="image"

Tuesday, September 4, 2007

How to Register Stylesheet Created in Runtime

Here is an example from MSDN:
// Create a Style object for the  section of the Web page.
Style bodyStyle = new Style();
bodyStyle.ForeColor = System.Drawing.Color.Blue;
bodyStyle.BackColor = System.Drawing.Color.LightGray;
// Add the style to the header of the current page.
Page.Header.StyleSheet.CreateStyleRule(bodyStyle, this, "BODY");

It is the simple and handy method but has a few flaws.
1. Output is far from optimal. For example, following code that adds a border
someStyle.BorderColor = System.Drawing.Color.Red;
someStyle.BorderStyle = BorderStyle.Solid;
someStyle.BorderWidth = new Unit("1px");
produces this output
border-color:Red;border-width:1px;border-style:Solid;
instead of
border:Solid 1px Red;

It has no matter if you don't pay attention to the size of pages you develop, but it is taken into consideration if you take into consideration your visitor's needs (sorry for tautology). But it's a trifle, of course. I mentioned it just for completeness. There is a more serious flaw.

2. Number of style attributes that can be registered in such way is very limited. You can't add padding or margin attributes or even particular border (e.g. border-left).

The code overview

Class StylesController has four public static methods:
//includes registered styles into page header
public static void RegisterStyleSheetBlock(Page page, string key, string styles)
{
LiteralControl ctrl = new LiteralControl(string.Format(@"<style type=""text/css"">
{0}
</style>", styles));
ctrl.ID = key;
page.Header.Controls.Add(ctrl);
}
//includes link to the newly created stylesheet file into page header
public static void RegisterStyleSheetInclude(Page page, string key, string styles)
{
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(key);
string base64Key = Convert.ToBase64String(bytes).Replace('+', '!');
LiteralControl ctrl = new LiteralControl(string.Format(@"<link
type=""text/css"" rel=""stylesheet"" href=""{0}?key={1}""/>",
page.ResolveUrl("~/stylesheet.css"),
base64Key));
ctrl.ID = key;
page.Header.Controls.Add(ctrl);
HttpContext.Current.Session[base64Key] = styles;
}
//checks whether inline styles with specified key have been already registered
public static bool IsStyleSheetBlockRegistered(Page page, string key)
{
return (page.Header.FindControl(key) != null);
}
//checks whether a file with styles with specified key have been already registered
public static bool IsStyleSheetIncludeRegistered(Page page, string key)
{
return (page.Header.FindControl(key) != null);
}
The class StylesController implements IHttpHandler interface and serves as HttpHandler that produces the stylesheet file.
public class StylesController : IHttpHandler, System.Web.SessionState.IRequiresSessionState 
{
...
#region IHttpHandler Members
bool IHttpHandler.IsReusable
{
get { return false; }
}
void IHttpHandler.ProcessRequest(HttpContext context)
{
context.Response.Clear();
context.Response.ContentType = "text/css";
string key = context.Request["key"];
if (key != null)
{
object script = context.Session[key];
if (script != null)
{
context.Response.Write(script.ToString());
HttpContext.Current.Session.Remove(key);
}
}
context.Response.End();
}
#endregion
}
HttpHandler has to be registered in web.config
<httpHandlers>
<add verb="GET" path="StyleSheet.css" type="TypeName, AssemblyName"/>

</httpHandlers>
where AssemblyName is the name of the assembly, where the class StylesController is located,
TypeName is the fully qualified name of the StylesController, including the namespace.

How to use

string styles = "div{padding:2px 5px;background-color:red;}";
if (!StylesController.IsStyleSheetBlockRegistered(this, "Red Divs"))
StylesController.RegisterStyleSheetBlock(this, "Red Divs", styles);
or
string styles2 = "a,a:link {text-decoration:none;background-image:url(img/back.gif);}";
if (!StylesController.IsStyleSheetIncludeRegistered(this, "New Links"))
StylesController.RegisterStyleSheetInclude(this, "New Links", styles2);

Source code - 1kB

Wednesday, July 25, 2007

How to Add Additional Element to GridView Pager without PagerTemplate

PagerTemplate allows to create any configuration of pager but it also requires custom paging to be implemented. Sometimes built-in paging completely meet all needs but a little modification is reduired, for example, ability to change page size. That can be done within RowCreated event handler.

<asp:GridView ID="GridView1" runat=server AllowPaging="True" OnRowCreated="GridView1_RowCreated" ...
Code behind:
protected void GridView1_RowCreated(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.Pager)
{
DropDownList ddl = new DropDownList();
//adds variants of pager size
ddl.Items.Add("5");
ddl.Items.Add("10");
ddl.AutoPostBack = true;
//selects item due to the GridView current page size
ListItem li = ddl.Items.FindByText(GridView1.PageSize.ToString());
if (li != null)
ddl.SelectedIndex = ddl.Items.IndexOf(li);
ddl.SelectedIndexChanged += new EventHandler(ddl_SelectedIndexChanged);
//adds dropdownlist in the additional cell to the pager table
Table pagerTable = e.Row.Cells[0].Controls[0] as Table;
TableCell cell = new TableCell();
cell.Style["padding-left"] = "15px";
cell.Controls.Add(new LiteralControl("Page Size:"));
cell.Controls.Add(ddl);
pagerTable.Rows[0].Cells.Add(cell);
}
}
void ddl_SelectedIndexChanged(object sender, EventArgs e)
{
//changes page size
GridView1.PageSize = int.Parse(((DropDownList)sender).SelectedValue);
//binds data source
GridView1.DataSource = ...;
GridView1.DataBind();
}
Source code - 1.9 kB

Tuesday, July 17, 2007

Rounded Panel without Images

Panels with rounded borders are widely used in the web site design. So widely that it is hard to add something new. I just try to wrap existing ideas in the form of a web control.

Features:
  • Can be set radius of the circle, back color and border color
  • Without images (pure HTML)
  • Only 130 lines of code
  • Easy to use (see examples below this article)
<cc:RoundedPanel ID="RoundedPanel1" runat="server" CssClass="r1" BorderColor="Gold" BackColor="Beige" Radius="9"></cc:RoundedPanel>

I inherit control from WebControl. Mainly it will be a container for another elements so it needs PersistChildrenAttribute and ParseChildrenAttribute to be set correctly. BackColor and BorderColor properties have been defined in the base class, only radius of circle has to be added.
 [PersistChildren(true), 
ParseChildren(false),
ToolboxData("<{0}:RoundedPanel runat=\"server\"></{0}:RoundedPanel>"),
Designer(typeof(System.Web.UI.Design.ContainerControlDesigner ))]
public class RoundedPanel : WebControl
{
[Browsable(true), DefaultValue(5), Category("Appearance")]
public int Radius
{
get
{
return (int)(ViewState["Radius"] ?? 5);
}
set
{
ViewState["Radius"] = value;
}
}
...

Here is my favorite site where I am looking for examples of the use of CSS in real life. I'd like to take one of those examples of boxes with rounded corners as a base for web control but unfortunately none of the implementation on the basis of DIV cannot properly stretch if inner content is larger then the given DIV width. But in the development of the sites with dynamically loadable content, this problem is not that it would be everywhere but rather predictable.


So I decide to take as a basis a TABLE element - don't judge severely, I am developer not web designer :).

The corners are made of blocks (B elements) of varying widths and with different borders (look at the picture). B element are taken taken on the sole reason that its name is shorter than DIV. And inasmuch as corners are built from n elements (n = radius), that gives some savings of HTML.
public override void RenderControl(HtmlTextWriter writer)
{
//forms four corners
StringBuilder sbTopLeft = new StringBuilder();
StringBuilder sbTopRight = new StringBuilder();
StringBuilder sbBottomLeft = new StringBuilder();
StringBuilder sbBottomRight = new StringBuilder();
for (int i = 0; i < Radius; i++)
{
sbTopLeft.AppendFormat("<b class='a{0}'></b>", i);
sbTopRight.AppendFormat("<b class='b{0}'></b>", i);
sbBottomLeft.AppendFormat("<b class='a{0}'></b>", Radius - i - 1);
sbBottomRight.AppendFormat("<b class='b{0}'></b>", Radius - i - 1);
}
//renders upper part of the panel
writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "0");
writer.AddAttribute(HtmlTextWriterAttribute.Cellspacing, "0");
writer.AddAttribute(HtmlTextWriterAttribute.Class, CssClass);
writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);
if (!Width.IsEmpty)
writer.AddStyleAttribute(HtmlTextWriterStyle.Width, Width.ToString());
if (!Height.IsEmpty)
writer.AddStyleAttribute(HtmlTextWriterStyle.Height, Height.ToString());
writer.RenderBeginTag("table");
writer.Write(string.Format(@"<tr>
<td class='l' align=right>{0}</td>
<td class='m mt'> </td>
<td class='r'>{1}</td>
</tr>

<tr>
<td class='m ml'> </td>
<td class='m'>", sbTopLeft, sbTopRight));
//renders child controls
base.RenderContents(writer);
//renders bottom part of the panel
writer.Write(string.Format(@"</td>

<td class='m mr'> </td>
</tr>
<tr>
<td class='l' align=right>{0}</td>
<td class='m mb'> </td>
<td class='r'>{1}</td>

</tr>", sbBottomLeft, sbBottomRight));
writer.RenderEndTag();
}

Then I create styles based on selected colors and desired circle radius. They make all work.
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
//I register styles using RegisterClientScriptBlock because it hard to get desired style names by means
//of proper function Page.Header.StyleSheet.RegisterStyle. Also if there are a few same panels on a page
//the styles will be registered just once.

if (!Page.ClientScript.IsClientScriptBlockRegistered(typeof(Page), CssClass))
{
//gets color name in either "red" or "#f5d724" form
string backColorStr = BackColor.IsNamedColor ? BackColor.Name : "#"+BackColor.Name.Substring(2);
string borderColorStr = BorderColor.IsNamedColor?BorderColor.Name:"#"+BorderColor.Name.Substring(2);
StringBuilder sb = new StringBuilder();
sb.AppendLine("<style type='text/css'>");
sb.AppendFormat(@".{0} .l b,.{0} .r b(background-color:{1};height:1px;overflow:hidden;display:block;)
.{0} .ml(border-left:1px solid {2};)
.{0} .mr(border-right:1px solid {2};)
.{0} .m(background-color:{1};)
.{0} .mt(border-top:1px solid {2};height:{4}px;font-size:1px;)
.{0} .ml,.{0} .mr(width:{3}px;font-size:1px;)
.{0} .mb(border-bottom:1px solid {2};height:{4}px;font-size:1px;)",
CssClass, backColorStr, borderColorStr, Radius, Radius - 1);
int prevWidth = -1;

//particular implementation for Internet Explorer is required if quirks mode is used.
bool isIE = HttpContext.Current.Request.Browser.Browser.Equals("IE");
for (int i = 0; i < Radius; i++)
{
double angle = Math.Asin((double)(Radius - i) / Radius);
int width = (int)Math.Ceiling((Radius * Math.Cos(angle)));
if (width - prevWidth < 2)
sb.AppendFormat(".{0} .a{1}(width:{2}px;border-left:solid 1px {3})",
CssClass, i, width, borderColorStr);
else
{
if (isIE)
sb.AppendFormat(".{0} .a{1}(width:{2}px;border-left:solid {3}px {4})",
CssClass, i, width, (width - prevWidth), borderColorStr);
else
sb.AppendFormat(".{0} .a{1}(width:{2}px;border-left:solid {3}px {4})",
CssClass, i, (1 + prevWidth), (width - prevWidth), borderColorStr);
}
if (width - prevWidth < 2)
sb.AppendFormat(".{0} .b{1}(width:{2}px;border-right:solid 1px {3})",
CssClass, i, width, borderColorStr);
else
{
if (isIE)
sb.AppendFormat(".{0} .b{1}(width:{2}px;border-right:solid {3}px {4})",
CssClass, i, width, (width - prevWidth), borderColorStr);
else
sb.AppendFormat(".{0} .b{1}(width:{2}px;border-right:solid {3}px {4})",
CssClass, i, (1 + prevWidth), (width - prevWidth), borderColorStr);
}
prevWidth = width;
}
sb.Append("\r\n</style>");
string styles = sb.ToString().Replace("(", "{").Replace(")", "}");
Page.ClientScript.RegisterClientScriptBlock(typeof(Page), CssClass, styles);
}
}

This is implementation in case of your page is processed as HTML (<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >). If the page is processed as XHTML (<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">) then remove the condition for Internet Explorer.

Source Code, 1.6 Kb

Sunday, June 24, 2007

Image-Based Bot Detector

Image-Based Bot Detector is a variant of image-based CAPTCHAs that uses human ability to recognize a distorted region on the image he never seen before.

Licence
Download Image-Based Bot Detector

Requires .NET Framework 2.0

Overview, help

In order to use it you have to set TemplateImageFolder property to relative virtual path of the template images folder - for example, "~/templates" and to add registration of HttpHandler in web.config.
<httpHandlers>
<add verb="GET" path="ImageBasedBotDetector.ashx"
type="Marss.Web.UI.Controls.ImageBasedBotDetector, Marss.Web"/>
</httpHandlers>
There are 3 types of predefined distortion (Stretched, Random, Volute) and user-defined (Custom) distortion. In order to use predefined distortion it needs to set DistortionType property. User-defined distortion can be used in derived classes with overriden DrawCustom method.

If you want to use template images that are placed elsewhere then a subfolder of your web application - for example, from database - you have to override GetTemplateImage method.

To check whether user passed the test IsValid property is used. Also user can be locked after a predefined number of unsuccessful attempts to pass the test (FailedAttemptsBeforeLocking property, default value is 5).

Public properties
TemplateImageFolderGets or sets template image folder.
FailedAttemptsBeforeLockingGets or sets the number of unsuccessful attempts before locking.
IsValidGets result of test passing.
IsLockedGets or sets value indicating where user is locked.
DistortionTypeGets or sets distortion type. Available types:
  • Stretched - image within distorted area is stretched.
  • Random - points within distorted area is mixed in a random way.
  • Volute - image within distorted area is volute.
  • Custom - custom distortion. Have to be used together with overriden DrawCustom method.
Public events
LockedOccurs when detector has been locked because of exceeding the failed attempts limit.
public event EventHandler Locked

Protected virtual methods
GetTemplateImageGets template image.
protected virtual System.Drawing.Image GetTemplateImage()
See example of usage.
DrawCustomDistorts specified rectangle of image in a user defined way. Have to be used coupled with DistortionType=Custom
protected virtual void DrawCustom(System.Drawing.Bitmap bitmap, System.Drawing.Image img, Rectangle distortedRectangle)
where
bitmap - the output image,
img - the template (input) image,
distortedRectangle - area on the image to be distorted.

Wednesday, June 6, 2007

Merging columns in GridView/DataGrid header

As necessity to show header columns in a few rows occurs fairly often it would be good to have such functionality in the GridView/DataGrid control as an in-built feature. But meanwhile everyone solves this problem in his own way.

The described below variant of the merging implementation is based on irwansyah's idea to use the SetRenderMethodDelegate method for custom rendering of grid columns header. I guess this approach can be simplified in order to get more compact and handy code for reuse.

The code overview


As it may be required to merge a few groups of columns - for example, 1,2 and 4,5,6 - we need a class to store common information about all united columns.
[Serializable]
private class MergedColumnsInfo
{
// indexes of merged columns
public List<int> MergedColumns = new List<int>();
// key-value pairs: key = the first column index, value = number of the merged columns
public Hashtable StartColumns = new Hashtable();
// key-value pairs: key = the first column index, value = common title of the merged columns
public Hashtable Titles = new Hashtable();

//parameters: the merged columns indexes, common title of the merged columns
public void AddMergedColumns(int[] columnsIndexes, string title)
{
MergedColumns.AddRange(columnsIndexes);
StartColumns.Add(columnsIndexes[0], columnsIndexes.Length);
Titles.Add(columnsIndexes[0], title);
}
}

Attribute Serializable is added in order to have a possibility to store information about merged columns in ViewState - it is required if paging or sorting is used.

That is the only additional action. Now the code usage.
.ascx file:
//for GridView
<asp:GridView ID="grid" runat=server OnRowCreated="GridView_RowCreated" ... ></asp:GridView>

//for DataGrid
<asp:DataGrid ID="grid" runat=server OnItemCreated="DataGrid_ItemCreated" ... ></asp:DataGrid>

Columns can be defined in design time or can be autogenerated - it does not matter and doesn't influence the further code. Merging also does not harm sorting and paging if they are used in the GridView/DataGrid.

.cs file:
//property for storing of information about merged columns
private MergedColumnsInfo info
{
get
{
if (ViewState["info"] == null)
ViewState["info"] = new MergedColumnsInfo();
return (MergedColumnsInfo)ViewState["info"];
}
}

protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
//merge the second, third and fourth columns with common title "Subjects"
info.AddMergedColumns(new int[] { 1, 2, 3 }, "Subjects");
grid.DataSource = ... //some data source

grid.DataBind();
}
}

Particular code for GridView:
protected void GridView_RowCreated(object sender, GridViewRowEventArgs e)
{
//call the method for custom rendering the columns headers
if (e.Row.RowType == DataControlRowType.Header)
e.Row.SetRenderMethodDelegate(RenderHeader);
}

and for DataGrid:
protected void DataGrid_ItemCreated(object sender, DataGridItemEventArgs e)
{
//call the method for custom rendering the columns headers
if (e.Item.ItemType == ListItemType.Header)
e.Item.SetRenderMethodDelegate(RenderHeader);
}

Next code is common for both GridView and DataGrid:
//method for rendering the columns headers 
private void RenderHeader(HtmlTextWriter output, Control container)
{
for (int i = 0; i < container.Controls.Count; i++)
{
TableCell cell = (TableCell)container.Controls[i];

//stretch non merged columns for two rows
if (!info.MergedColumns.Contains(i))
{
cell.Attributes["rowspan"] = "2";
cell.RenderControl(output);
}
else //render merged columns common title
if (info.StartColumns.Contains(i))
{
output.Write(string.Format("<td align='center' colspan='{0}'>{1}</td>",
info.StartColumns[i], info.Titles[i]));
}
}

//close the first row
output.Write("</tr>");
//set attributes for the second row

grid.HeaderStyle.AddAttributesToRender(output);
//start the second row
output.RenderBeginTag("tr");

//render the second row (only the merged columns)
for (int i = 0; i < info.MergedColumns.Count; i++)
{
TableCell cell = (TableCell)container.Controls[info.MergedColumns[i]];
cell.RenderControl(output);
}
}

That is all. The code can be used without any modification, the only part that has to be changed in a concrete case is:
info.AddMergedColumns(new int[] { 1, 2, 3 }, "Foo");
info.AddMergedColumns(new int[] { 6, 7 }, "Bar");
//and so forth ...

Download code - 1.4 Kb

Tuesday, May 29, 2007

Flaw with buttons in the GridView pager

For unknown reason the GridView pager in Numeric and NumericFirstLast modes does not take into consideration values of the PreviousPageText and NextPageText properties from the pager settings. Instead an ellipsis is shown. Of course, you may say that the buttons with the ellipsis implement a bit different functionality. Yes, they do. But the problem leaves - text on these buttons is not customizable. Sometimes it becomes problematic especially if your customer is too pernickety. To solve this flaw you can use next approach:
GridView1.RowDataBound+=new GridViewRowEventHandler(GridView1_RowDataBound);
protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.Pager)
{
Table pagerTable = (Table)e.Row.Cells[0].Controls[0];
TableRow pagerRow = pagerTable.Rows[0];
PagerSettings pagerSettings = ((GridView)sender).PagerSettings;
int cellsCount = pagerRow.Cells.Count;
if (pagerSettings.Mode == PagerButtons.Numeric
|| pagerSettings.Mode == PagerButtons.NumericFirstLast)
{
int prevButtonIndex = pagerSettings.Mode==PagerButtons.Numeric ? 0 : 1;
int nextButtonIndex = pagerSettings.Mode==PagerButtons.Numeric ? cellsCount-1 : cellsCount-2;
if (prevButtonIndex < cellsCount)
{
//check whether previous button exists
LinkButton btnPrev = pagerRow.Cells[prevButtonIndex].Controls[0] as LinkButton;
if (btnPrev != null && btnPrev.Text.IndexOf("...") != -1)
btnPrev.Text = pagerSettings.PreviousPageText;
}
if (nextButtonIndex > 0 && nextButtonIndex < cellsCount)
{
//check whether next button exists
LinkButton btnNext = pagerRow.Cells[nextButtonIndex].Controls[0] as LinkButton;
if (btnNext != null && btnNext.Text.IndexOf("...") != -1)
btnNext.Text = pagerSettings.NextPageText;
}
}
}
}

Update: It turns that it is rather simply to completely simulate the Previous and Next buttons. Add following snippets to the code above.
if (btnPrev != null && btnPrev.Text.IndexOf("...") != -1)
{
...
btnPrev.CommandName = "Page";
btnPrev.CommandArgument = "Prev";
}
...
if (btnNext != null && btnNext.Text.IndexOf("...") != -1)
{
...
btnNext.CommandName = "Page";
btnNext.CommandArgument = "Next";
}

Sunday, May 20, 2007

Image-based CAPTCHA. Demo Project

Demo project that illustrates the idea of the image-based CAPTCHA.

The code overview

The proposed image-based CAPTCHA control works in such a way: a visitor sees a picture with a distorted part and he has to click elsewhere in the anomalous region boundaries. The point he clicks on will be marked with a red spot.

The control fulfils a double functionality, it renders its HTML content and forms the picture itself. It has two child controls: an image, and a hidden field that serves to store coordinates of the visitor's chosen point. The image URL forms by adding the special parameter to the currently requested URL. When the request to this new URL comes, the control interrupts the usual process of page loading and writes the image as a byte array in the response.
protected override void OnInit(EventArgs e)
{
if (HttpContext.Current.Request[queryKey] != null)
DrawImage();
.....
}
private void DrawImage()
{
Bitmap bitmap;

//the image creation goes here
....

HttpContext.Current.Response.Clear();
HttpContext.Current.Response.ContentType = "image/jpg";
bitmap.Save(HttpContext.Current.Response.OutputStream, ImageFormat.Jpeg);
HttpContext.Current.Response.End();
}

This approach allows to compound all the functionality in a single control. In a heavily loaded environment, it is better to take out the image creation elsewhere to another place - for example, to the HttpHandler - to avoid complicating the creation of the page where the control is placed on.

The coordinates of the point where the visitor clicks is calculated and visualized by means of JavaScript (tested on IE 6.0, Firefox 1.0+, Opera 9.1). They are stored in the hidden field in order to be accessible on the server side.
function captchaClicked(hidID,e)
{
var sender = e.target || e.srcElement;
var offsetX, offsetY;

//the calculation of the the clicked point's coordinates
if (e.offsetX) //IE
{
offsetX = e.offsetX;
offsetY = e.offsetY;
}
else if (e.pageX) //Firefox, Opera
{
var left = sender.offsetLeft;
var top = sender.offsetTop;
var parentNode = sender.offsetParent;
while(parentNode != null && parentNode.offsetLeft != null && parentNode.offsetTop != null){
left += parentNode.offsetLeft;
top += parentNode.offsetTop;
parentNode = parentNode.offsetParent;
}
offsetX = e.pageX - left;
offsetY = e.pageY - top;
}

//storing of the coordinates
document.getElementById(hidID).value = offsetX+","+offsetY;

//the creation of the little red spot to mark the clicked point
var spot = document.getElementById("spotOnCaptha");
if (!spot)
{
spot = document.createElement("span");
spot.id = "spotOnCaptha";
spot.style.height = "3px";
spot.style.width = "3px";
spot.style.fontSize="1px";
spot.style.backgroundColor="red";
spot.style.position="absolute";
spot.style.zIndex = 100;
sender.parentNode.appendChild(spot);
}

//positioning of the spot
spot.style.left = e.pageX || (event.clientX + document.body.scrollLeft);
spot.style.top = e.pageY || (event.clientY + document.body.scrollTop);
}

Now, about the CAPTCHA image creation. In the loaded template picture, a rectangle with randomly defined coordinates is selected. Then, the coordinates are saved in the Session. It is possible to store them in the ViewState, but in this case, they have to be encoded because the ViewState is accessible on the client side. Then, the image in the rectangle boundaries is stretched (any other distortion may be used, the only condition - it has to be notable for the site visitor).

Also, here is another problem. It is possible to compare the template (original) image and the final distorted image pixel by pixel in order to find the distorted area. To avoid this possibility, the template image is also changed in a random way. In the example below, it is stretched or compressed on a random scale.
//template image loading
using (System.Drawing.Image img = System.Drawing.Image.FromFile(this.Page.MapPath(TemplateImageUrl)))
{
//CAPCTHA image size is smaller then the template image one.
int captchaWidth = (int)(img.Width * 0.9);
int captchaHeight = (int)(img.Height * 0.9);
bitmap = new Bitmap(captchaWidth, captchaHeight);
Graphics g = Graphics.FromImage(bitmap);
//the rectangle dimension
int rectWidth = 20;
int rectHeight = 20;
Random r = new Random();
//the original (template) image is stretched or shrunken in a random way
float scaleX = 1 + r.Next(-100, 100) / 1000f;
float scaleY = 1 + r.Next(-100, 100) / 1000f;
g.ScaleTransform(scaleX, scaleY);
g.DrawImage(img, 0, 0, img.Width, img.Height);
g.ResetTransform();

//the rectangle coordinates are selected in a random way too
int x = r.Next(0, captchaWidth - rectWidth);
int y = r.Next(0, captchaHeight - rectHeight);
Rectangle distortedRect = new Rectangle(x, y, rectWidth, rectHeight);
HttpContext.Current.Session["ImgCAPTCHA.coords"] = distortedRect;
rectWidth = rectWidth + 10;
rectHeight = rectHeight + 10;
if (x + rectWidth > captchaWidth)
x = captchaWidth - rectWidth;
if (y + rectHeight > captchaHeight)
y = captchaHeight - rectHeight;
//draw distorted part of the image
g.DrawImage(img, distortedRect, new Rectangle(x, y, rectWidth, rectHeight), GraphicsUnit.Pixel);
g.DrawRectangle(Pens.Black, 0, 0, captchaWidth-1, captchaHeight-1);
}

Maybe the proposed image creation algorithm looks imperfect. Indeed, it is. I want to say that this article is rather the idea presentation then the control's description (it is just an example).

How to use

In order to use the control it has to assign the path of the template image - TemplateImageUrl property. The result of the CAPTCHA control action is a value of IsValid property. Besides here is the ability to lock user after the fixed number of failed attempts per session (FailedAttemptsBeforeLocking, IsLocked).

Download code - 34Kb

Update: I left the demo project for the demonstration how to implement this kind of CAPTCHAs. It is functional but not too optimal. Do not judge this code harshly :), it was written offhand specifically to illustrate this article that was published May 20, 2007. The control was greatly changed since then. If you want to include such CAPTCHA in your project then better to use ready Image-Based Bot Detector control that is available for download since Jun 24, 2007. It is free, with advanced facilities, tested in the most of modern browsers.

Image-based CAPTCHA

The brief overview of the most known implementations

  • Carnegie Mellon's PIX CAPTCHA - the so called "naming images CAPTCHA" - the user sees a few pictures, and has to select a word that is appropriate to all the shown pictures. The main problem of this type of CAPTCHAs is misspelling while writing the answer, and synonyms for the answer-word (for example: dog, hound, pooch). In the described case, this is solved by means of transferring all the variants of the answer to the client side.
  • Oli Warner's KittenAuth - in order to prove his humanity, the visitor has to select all animals of a specified species among the proposed pictures. But, the limited number of pictures allows to recreate the picture base manually.
  • Microsoft's Asirra - in outline, it is similar to KittenAuth - the user has to distinguish cats from dogs - but, it works with an extremely large array of pictures (photos of homeless animals from the specialized site), and a reconstruction of the picture base is impossible.
  • IMAGINATION - a CAPTCHA that requires two steps to be passed. At the first step, the visitor clicks elsewhere on the picture that is composed of a few images, and selects a single image. In the second step, the selected image is loaded. It is enlarged, but very distorted. Also, variants of the answer are loaded on the client side. The visitor should select a correct answer from the set of proposed words.

Why are the image-based CAPTCHAs NOT so widespread as the text-based ones?

I will not touch on the contrastive analysis of the possibility to crack them, you can find some thoughts/calculations here and here. I want to express my point of view as a web developer. So, why?
  • They are too large. A CAPTCHA should not take a dominant position on a web page. Is is only an ancillary element that serves to weed out bots under forms filling, getting some information, etc.
  • The traffic. A few pictures, each with a size of about 5-10 KB, would weigh a lot for a single page, in my opinion. Visitors using a low bandwidth network will be unpleasantly impressed, not to mention visitors using dial-up connections.
  • The inconsistency to the general conception of a website. A CAPTCHA with cats (or dolphins) will be appropriate on a leisure site, but will be irrelevant - for example - on a site of a medical institution. In this case, it is possible to gather a number of images of medical subjects, but - on the analogy of Asirra - it would be problematic to find a site with a large amount of photos of homeless doctors :).
  • The laborious process of the picture base creation.

I have to note that it is not a criticism in any way - I only want to find an answer for the above question. Let's sum up the aforesaid. An image-based CAPTCHA might be a good alternative to a text-based one if it would be a single, light-weight image based on a limited set of pictures.

The idea

Look at these two pictures:

It is easy to notice that the right image is slightly distorted, and it is not hard to outline a rough region where the distortion takes place. In order to notice it, the original image is not required. A human easy copes with this task even he sees an image the first time and does not know what the image depicts - the aforesaid does not apply to an expressionist's pictures :).

Now about bots. I have never worked with image recognition systems and my knowledge in this area is rather poor. Perhaps, the proposed variant is intricate to parse by special programs, perhaps not - it will be interesting to hear an expert's opinion.

Demo project (C#, ASP.Net) that illustrates the above conception of the image-based CAPTCHA.

Saturday, March 17, 2007

Implementation of paging and sorting for the GridView control that works with an array of objects


Fairly often, such a data presentation model is used: an array of objects is bound to a GridView control with predefined columns. This model was popularized by the DotNetNuke web framework and has the advantage of working with the business objects instead of nameless rows of a data table. A classic example:
<asp:GridView id="gv" runat="server" AutoGenerateColumns="False" ...
<Columns>
<asp:BoundField DataField="Name" HeaderText="Name" SortExpression="Name" />
...
</Columns>
</asp:GridView>

and elsewhere in Page_Load:
if (!IsPostBack)
{
gv.DataSource = someArray;
gv.DataBind();
}

A pretty good model, in my opinion. But, the GridView control does not have built-in ability for sorting and paging if it is bound to an array of objects. Let's implement it.

The code overview

I inherit my control from the GridView control and override the OnInit method in order to add two event handlers.
this.PageIndexChanging += new GridViewPageEventHandler(BaseGridView_PageIndexChanging);
this.Sorting += new GridViewSortEventHandler(BaseGridView_Sorting);

The paging implementation is trivial.
void BaseGridView_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
this.PageIndex = e.NewPageIndex;
BindDataSource();
}

The sorting one is rather complicated. The GridView control has a sorting event handler, but it does not save information about the previous sorting state. So, I added a few variables to save the data: PreviousSortExpression, CurrentSortExpression, and CurrentSortDirection.
void BaseGridView_Sorting(object sender, GridViewSortEventArgs e)
{
//for unknown reason e.SortExpression always is Ascending
if (PreviousSortExpression == e.SortExpression)
{
e.SortDirection = SortDirection.Descending;
PreviousSortExpression = null;
}
else
PreviousSortExpression = e.SortExpression;

CurrentSortExpression = e.SortExpression;
CurrentSortDirection = e.SortDirection;

ChangeHeaders(this.HeaderRow);

BindDataSource();
}

A little something is lacking. There is no visual presentation whether the grid is sorted and how it is sorted. So, I change the sorted column header to show this information. I do it by means of two Unicode symbols: the up arrow (\u25bc) and the down arrow (\u25b2).
private void ChangeHeaders(GridViewRow headerRow)
{
for (int i = 0; i < headerRow.Cells.Count; i++)
{
if (headerRow.Cells[i] is DataControlFieldCell)
{
DataControlField field = ((DataControlFieldCell)headerRow.Cells[i]).ContainingField;
//remove all previous sorting marks if they exist
Regex r = new Regex(@"\s(\u25bc|\u25b2)");
field.HeaderText = r.Replace(field.HeaderText, "");
if (field.SortExpression != null && field.SortExpression == CurrentSortExpression)
{
//add current sorting state mark
if (CurrentSortDirection == SortDirection.Ascending)
field.HeaderText += " \u25b2";
else
field.HeaderText += " \u25bc";
}
}
}
}

Now, about filling the grid with data. I add the event that occurs when the data source is requested and the method that will handle this event.
public delegate void DataSourceRequestedEventHandler(out Array dataSource);
public event DataSourceRequestedEventHandler DataSourceRequested;

Refilling of the grid occurs when a user sorts the grid or changes the current page. For initial filling, the BindDataSource method is used.
public void BindDataSource()
{
Array dataSource = null;

//request for the data source
if (DataSourceRequested != null)
DataSourceRequested(out dataSource);

if (dataSource == null)
throw new Exception("Failed to get data source.");

//sort the data in case of need
if (CurrentSortExpression != null)
{
ArrayList ar = new ArrayList(dataSource);
ar.Sort(new ArrayComparer(CurrentSortExpression, CurrentSortDirection));
dataSource = ar.ToArray();
}

base.DataSource = dataSource;
base.DataBind();
}

ArrayComparer is the implementation of the IComparer interface to compare two objects by a specified property in a specified order (ascending or descending).

How to use it

First of all, because I hide all the paging and sorting functionality inside the new control, I have to reject the old data binding method explicitly. So, I add an event handler for all requests for the data source and the method to call the data binding. And also, it is needed to allow paging and sorting in the GridView :)
<cc:GridViewEx ID="gv" runat=server AllowPaging=True AllowSorting=True 
OnDataSourceRequested="gv_DataSourceRequested" AutoGenerateColumns="False">
<Columns>

<asp:BoundField DataField="Name" HeaderText="Name" SortExpression="Name" />
...
</Columns>>
</cc:GridViewEx>

protected void Page_Load(object sender, EventArgs e)
{
gv.DataSourceRequest+=new DataSourceRequestEventHandler(gv_DataSourceRequested);
if (!IsPostBack)
gv.BindDataSource();
}
protected void gv_DataSourceRequested(out Array dataSource)
{
dataSource = someArray;
}

That is all you need to use this control, no additional work for paging or sorting is required. The full code can be found in the above demo.

Download source/demo project - 10.5 Kb