Tuesday, April 20, 2010

Two facts you may not know about storing ASP.Net Session in SQL DB

  1.   If saving of large objects in the Session in InProc mode is bad practice then doing the same in SQLServer mode is real evil. The main problem in this case is serialization/deserialization data on every request to the server. Each user's session is a single record in ASPStateTempSessions table so all objects need to be deserialized even if you request something which is rather small. Symptoms of this problem: you notice using a profiler that usual int userId = Session["UserId"] takes a few seconds!!! to execute.
  2.     If you noticed that ASPStateTempSessions table is rather large and permanently grows and grows then check if the SQL Server agent is running. It is responsible for deleting of expired sessions. It should have a job to execute the DeleteExpiredSessions stored procedure every minute (by default).

Friday, March 20, 2009

How to merge cells with equal values in the GridView


My solution is not the first; however, I think, it is rather universal and very short - less than 20 lines of the code.

The algorithm is simple: to bypass all the rows, starting from the second at the bottom, to the top. If a cell value is the same as a value in the previous (lower) row, then increase RowSpan and make the lower cell invisible, and so forth.

The code that merges the cells is very short:
public class GridDecorator
{
    public static void MergeRows(GridView gridView)
    {
        for (int rowIndex = gridView.Rows.Count - 2; rowIndex >= 0; rowIndex--)
        {
            GridViewRow row = gridView.Rows[rowIndex];
            GridViewRow previousRow = gridView.Rows[rowIndex + 1];

            for (int i = 0; i < row.Cells.Count; i++)
            {
                if (row.Cells[i].Text == previousRow.Cells[i].Text)
                {
                    row.Cells[i].RowSpan = previousRow.Cells[i].RowSpan < 2 ? 2 : 
                                           previousRow.Cells[i].RowSpan + 1;
                    previousRow.Cells[i].Visible = false;
                }
            }
        }
    }
}

The last action is to add an OnPreRender event handler for the GridView:
protected void gridView_PreRender(object sender, EventArgs e)
{
    GridDecorator.MergeRows(gridView);
}

Wednesday, June 4, 2008

Charts: let Google work for you

I can't class myself as an developer who is always hip to the latest news in the software industry, partly through laziness and partly through necessity to write code, and not merely to surf the Internet :) That is why I missed appearance of a very cute and handy service from Google, which allowed to integrate charts into the web pages.

For the time being it is only half a year old as far as I can judge by the date of the first record in the discussion group, therefore I think this information can be interesting for someone else besides me.

The service allows to dynamically generate practically all essential types of charts such as bar charts, pie charts and so on. The only thing you have to do to get the chart is forming the url in concordance with some rules and the service will return the PNG-format image.

As an example I decide to use data from another Google service, namely Google Analytics, which together with all its merits has one lack: the absence of public access to the analytic data that does not allow to boast :) of the nice designed statistics of your site unlike, for example, ClustrMaps.

At the beginning I save the data about visits to my site for the last three monthes (Mar 1, 2008 - May 31, 2008) in the XML format. I want to build two graphs - the map overlay and the pie chart - based on these data.
//analytic data file path
string xmlFilePath = Server.MapPath("~/map-overlay.xml");

XmlDocument doc = new XmlDocument();
doc.Load(xmlFilePath);

XmlNodeList nodes = doc.SelectNodes("/AnalyticsReport/Report/GeoMap/Region");

//the world map overlay
Image1.ImageUrl = GetGeoMapChartUrl(nodes);
//the pie chart
Image2.ImageUrl = GetPieChartUrl(nodes);

Chart #1. The site visits overlay on the world map

The site visits overlay on the world map

Google Charts is one of the few services that has the clear and comprehensible documentation, therefore this is realy pleasant to use it :)
//maximal number of visits from a country
private int max;

private string GetGeoMapChartUrl(XmlNodeList nodes)
{
//list of countries names
StringBuilder sbCountries = new StringBuilder();
//list of values for each country to be colored
StringBuilder sbColors = new StringBuilder();

if (nodes.Count > 0)
{
//maximal number of visits from a country (data in the file are sorted in descending order)

max = int.Parse(nodes[0]["Value"].InnerText, NumberStyles.Number);

foreach (XmlNode node in nodes)
{
//NumberStyles.Number has to be set because of the presence of whitespaces in the numbers spelling, for example, "1 329"
int value = int.Parse(node["Value"].InnerText, NumberStyles.Number);
//country code in concordance with ISO 3166
sbCountries.Append(node["Id"].InnerText);
//country color which depends on the number of visits - the more visits the more saturated color
sbColors.Append(GetColorCode(value));
}
}

//http://chart.apis.google.com/chart - service url
//?chs=440x220 - image size (440х220 is the maximum available for all maps)
//&cht=t - chart type
//&chtm=world - geographical area (there are also available separate continents and the territory of USA)
//&chd=s:{0} - codes of colors
//&chld={1} - codes of countries
//&chco=ffffff,ffebcc,ff9900 - ffffff (default country color), ffebcc ... ff9900 (color gradient for painted countries)
//&chf=bg,s,99ccff - color of the image background (seas, oceans)

return string.Format("http://chart.apis.google.com/chart?chs=440x220&chd=s:{0}&cht=t&chtm=world&chld={1}&chco=ffffff,ffebcc,ff9900&chf=bg,s,99ccff",
sbColors,
sbCountries
);
}
The country color depends on the number of visits and has to be one of the symbols from A...Za...z0...9 array, where A is the least value and 9 is the greatest one.
private char GetColorCode(int value)
{
int res = value*61/max;
if (res < 26) //A...Z
return (char)(65 + res);
else if (res < 51) //a...z
return (char)(71 + res);
else //0...9
return (char)(res - 4);
}

Chart #2. Pie chart

Pie chart

Unfortunately, if you simply pass an array of data (for example: 4, 6, 10) the pie chart is shown incorrectly, all secrors are of the same size. You have to pass percentage instead of absolute values (for the above example it should be 20, 30, 50). Through percentage calculation next code looks more complicated then the previous chart code.
private string GetPieChartUrl(XmlNodeList nodes)
{
//the total number of visits
int total = 0;
//the number of visits from each country
List<int> values = new List<int>(nodes.Count);
//list of countries names
List<string> names = new List<string>(nodes.Count);

foreach (XmlNode node in nodes)
{
//NumberStyles.Number has to be set because of the presence of whitespaces in the numbers spelling, for example, "1 329"

int value = int.Parse(node["Value"].InnerText, NumberStyles.Number);
total += value;
values.Add(value);
names.Add(string.Format("{0} ({1})", node["Name"].InnerText, value));
}

//data have to be in percents
List<string> data = new List<string>();

int sumPercents = 0;
int sumValues = 0;

for (int i = 0; i < values.Count; i++)
{
int percent = values[i] * 100 / total;
if (percent > 1)
{
sumPercents += percent;
sumValues += values[i];
data.Add(percent.ToString());
}
else
{
//visits from the countries that take 1% or less are shown together
names.RemoveRange(i, names.Count - i);
names.Add(string.Format("Other Countries ({0})", total - sumValues));
data.Add((100 - sumPercents).ToString());
break;
}
}


//http://chart.apis.google.com/chart - service url
//?cht=p3 - chart type
//&chd=t:{0} - data
//&chs=600x150 - image size (limited to 300000 pixels that is, for example, 300х1000)
//&chl={1} - labels

return string.Format("http://chart.apis.google.com/chart?cht=p3&chd=t:{0}&chs=600x150&chl={1}",
string.Join(",", data.ToArray()),
string.Join("|", names.ToArray()));
}
P.S. One thing that you can't find in the documentation: how to pass non-latin characters?
Is this case you have to convert characters to UTF8 encoding: for example, cyrilic character "B" has to be "%D0%92".
string value = ...//text that contains non-latin characters 
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(value);

StringBuilder sb = new StringBuilder();
foreach (byte b in bytes)
sb.AppendFormat("%{0}", b.ToString("X"));

string encodedValue = sb.ToString();

Sunday, April 6, 2008

How to use standard FileUpload in AJAX-enabled web applications

I would like to note that this article is not about the ability to upload files to the server without the postback. There are a lot of articles on this topic, just type "AJAX FileUpload" on any search engine and you'll get many examples. However with AJAX they actually have little in common, because the XMLHttpRequest does not support asynchronous uploading of files to the server, they are rather a variety of imitations, for example, using hidden IFRAME element. Nevertheless I want to emphasize that the article is not about that but about the standard FileUpload control.

There are two problems you might encounter when using it on UpdatePanel.

Problem 1
If the postback is caused by the control which lies on UpdatePanel, FileUpload is always empty when it has come to the server, regardless whether a file has been selected or not.
Example:
<asp:UpdatePanel ID="UpdatePanel1" runat=server>
<ContentTemplate>
<asp:FileUpload ID="FileUpload1" runat=server />
<asp:Button ID="btnUpload" runat=server Text="Upload" OnClick="btnUpload_Click"/>
</ContentTemplate>
</asp:UpdatePanel>

Solution
As XMLHttpRequest does not allow to send files asynchronously, they have to be submitted in a common manner. This problem is well described around, it is solved by registration of the control that has to submit the form as a postback trigger (in the above example it is btnUpload button).
<asp:UpdatePanel ID="UpdatePanel1" runat=server>
<ContentTemplate>
<asp:FileUpload ID="FileUpload1" runat=server />
<asp:Button ID="btnUpload" runat=server Text="Upload 2" OnClick="btnUpload_Click"/>
</ContentTemplate>
<Triggers>
<asp:PostBackTrigger ControlID="btnUpload" />
</Triggers>
</asp:UpdatePanel>

Problem 2
FileUpload does not work if it is loaded not on the initial page load but appears only after asynchronous update of the page part.
Example (pnlUpload panel is invisible at the beginning and is shown after clicking on btnShowFileUpload button):
<asp:UpdatePanel ID="UpdatePanel1" runat=server>
<ContentTemplate>
<asp:Button ID="btnShowFileUpload" runat=server Text="Show File Upload" OnClick="btnShowFileUpload_Click"/>
<asp:Panel ID="pnlUpload" runat=server Visible="False">
<asp:FileUpload ID="FileUpload1" runat=server />
<asp:Button ID="btnUpload" runat=server Text="Upload" OnClick="btnUpload_Click"/>
</asp:Panel>
</ContentTemplate>
<Triggers>
<asp:PostBackTrigger ControlID="btnUpload" />
</Triggers>
</asp:UpdatePanel>

.......................

protected void btnShowFileUpload_Click(object sender, EventArgs e)
{
pnlUpload.Visible = true;
}

Solution
The problem is caused by the requirement that for the normal work of FileUpload the form should have enctype="multipart/form-data". Usually, it is set in overriden OnPreRender method of FileUpload control.
protected internal override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
HtmlForm form = this.Page.Form;
if ((form != null) && (form.Enctype.Length == 0))
{
form.Enctype = "multipart/form-data";
}
}

Although during asynchronous postback this code is also executed but the form is not updated. That is why it is required to set the form content type explicitly during the first page load, for example, in the Page_Load event handler of the page or a control where FileUpload is placed.
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
this.Page.Form.Enctype = "multipart/form-data";
}

In case if this task is repeated in a few places you may do a simple control derived from FileUpload with overriden OnLoad method and use it.
public class CustomFileUpload : FileUpload
{
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);

if (!Page.IsPostBack)
this.Page.Form.Enctype = "multipart/form-data";
}
}

Friday, March 14, 2008

Simultaneous Selection of Checkboxes in a GridView's column

If you are using checkboxes in GridView in order to select a few rows, then, no doubt, you have been faced with the task how to select/deselect all checkboxes in a selected column at a time. Usually it is solved by addition of special buttons somewhere or addition a checkbox in the GridView's column header.

I want to dwell on the second variant. In order to refrain from repetition of code all of required functionality can be hidden inside of a special control. This control is inherited from the CheckBox and have only one method overriden (OnLoad).
Inside the method a script is registered, this script allows to select/deselect all checkboxes in a GridView's column if a checkbox in the GridView's header is clicked. Besides, the script tracks situation when all checkboxes in rows are selected one by one and selects the checkbox in the column's header.
public class GridViewCheckBox : CheckBox
{
protected override void OnLoad(EventArgs e)
{
GridViewRow parentRow = this.NamingContainer as GridViewRow;

//checks whether the checkbox is inside of a template column (TemplateField)
if (parentRow != null)
{
//registers the script
string script = @"function gvCheckBoxClicked(sender,isHeader) {
var cell = sender.parentNode;
var rows = cell.parentNode.parentNode.rows;
if (isHeader) {
for(i=1; i<rows.length; i++)
rows[i].cells[cell.cellIndex].getElementsByTagName(""input"")[0].checked = sender.checked;
}
else {
var headerCheckBox = rows[0].cells[cell.cellIndex].getElementsByTagName(""input"")[0];
if (!sender.checked)
headerCheckBox.checked = false;
else {
for(i=1; i<rows.length; i++) {
if (!rows[i].cells[cell.cellIndex].getElementsByTagName(""input"")[0].checked) {
headerCheckBox.checked = false;
return;
}
}
headerCheckBox.checked = true;
}
}
}";

if (!Page.ClientScript.IsClientScriptBlockRegistered(typeof(Page), "GridViewCheckBox"))
Page.ClientScript.RegisterClientScriptBlock(typeof(Page), "GridViewCheckBox", script, true);

//adds onclick event handler
if (parentRow.RowType == DataControlRowType.Header)
this.Attributes["onclick"] = "gvCheckBoxClicked(this,true);";
else if (parentRow.RowType == DataControlRowType.DataRow)
this.Attributes["onclick"] = "gvCheckBoxClicked(this,false);";
}

base.OnLoad(e);
}
}
That is all. In order to use just add the control to HeaderTemplate and ItemTemplate of the required column:
<asp:GridView ID="GridView1" runat="server" >
<Columns>
<asp:TemplateField>
<HeaderTemplate>
<wc:GridViewCheckBox id="GridViewCheckBox1" runat="server"/>
</HeaderTemplate>
<ItemTemplate>

<wc:GridViewCheckBox id="GridViewCheckBox2" runat="server"/>
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField ...
...
</Columns>
</asp:GridView>

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"