

Before I get started here: I know that a lot of you here aren’t big fans of type conversion, and I’m not either… but let me stop you just for a second. The web user interface is slow. It’s the slowest part of your entire process, unless you’ve done something very unusual (or wrong?). Response time is in milliseconds, and sometimes if you’re very bogged down, it’s in seconds, so really, a bit of type conversion extracting values from your web page is not all that big of a performance hit, relatively speaking. This is not to mention that you get to be the one writing the controls on the page, so you can stick to a set of reasonable rules for yourself, and not really have to worry about corner cases tripping you up.
I find myself in the awkward position a lot of having to grab a crap-load of input from a page, and then populate some objects with the values I’ve retreived, and then send them off to a controller of some sort. This is why I hate writing web interfaces… it’s all the same boring repatative unbearable stuff day in and day out… Instantiate an object, get control field value, set object value… rinse, lather, repeat…
Well, at a basically unnoticable performance trade off you can make things a lot easier on yourself by just writing a little bit of generic (not necessarily Generics) code that will make numerous controls and values a little more managable.
Now the code I’m pasting is an example, not a take home and complain if it doesn’t do everything you want saying “That Dave Dolan’s an idiot and doesn’t know anything!” (While you may be right about that for other reasons, this isn’t supposed to be a one fits all solution.)
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Text;
/// <summary>
/// Summary description for AccessorUserControl
/// </summary>
///
namespace MyUserControls
{
public class AccessorUserControl : System.Web.UI.UserControl
{
public object GetValue(string Field)
{
WebControl ctrl = FindControl(Field) as WebControl;
if (ctrl == null) throw new Exception(string.Format("Specified Control '{0}' was not found.", Field));
if (ctrl as ITextControl != null)
return (ctrl as ITextControl).Text;
if (ctrl is ListControl)
return (ctrl as ListControl).SelectedValue;
if (ctrl is CheckBox)
return (ctrl as CheckBox).Checked;
if (ctrl is Calendar)
return (ctrl as Calendar).SelectedDate;
throw new Exception(string.Format("Getting the value of the specified control '{0}' is not supported!",Field));
}
/// <summary>
/// Try very hard to get the expected type value equivelant to that of
/// the one represented by the web control. If it's a numeric target type,
/// try very hard to UNFUZZ any number stuff, like numeric separators (1,000)
/// or currency signs ($12). Use the decimal point separator based on the
/// server's current culture (that's my favorite part!)...
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="Field"></param>
/// <returns></returns>
public T GetValue<T>(string Field)
{
Type t = typeof(T);
object value = GetValue(Field);
if (value is string && IsNumeric(t))
value = (object)FilterNumeric((string)value, t);
return FlexType<T>(value);
}
/// <summary>
/// Try to cast the value first, then try to convert it, else goto Screwed
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="Field"></param>
/// <returns></returns>
public T FlexType<T>(object returnValue)
{
if (returnValue is T) return (T)returnValue;
try
{
if (typeof(IConvertible).IsAssignableFrom(returnValue.GetType()))
return (T)Convert.ChangeType(returnValue, typeof(T));
}
catch(Exception ex)
{
throw new InvalidOperationException(string.Format("Can't support casting OR converting specified instance of {0} to type {1}", returnValue.GetType(), typeof(T)), ex);
}
throw new InvalidOperationException(string.Format("Can't support casting OR converting specified instance of {0} to type {1}", returnValue.GetType(), typeof(T)));
}
static bool IsNumeric(Type t)
{
switch(Type.GetTypeCode(t))
{
case TypeCode.Object:
case TypeCode.String:
case TypeCode.DBNull:
case TypeCode.Boolean:
case TypeCode.Char:
return false;
default:
return true;
}
}
static bool AllowsDecimals(Type t)
{
switch(Type.GetTypeCode(t))
{
case TypeCode.Decimal:
case TypeCode.Double:
case TypeCode.Single:
return true;
default:
return false;
}
}
static bool isNegativeSign(string negativeSign, string input, int pos)
{
if (input.Substring(pos).StartsWith(negativeSign)) return true;
return false;
}
static string ExtractDigits(string Input, bool allowDecimal)
{
// start simple, whack the leading space.
string input = Input.Trim();
StringBuilder sb = new StringBuilder();
bool periodFound = false;
char periodChar = System.Globalization.CultureInfo.CurrentCulture.NumberFormat.CurrencyDecimalSeparator[0];
string negativeSign = System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NegativeSign;
char c = '\n';
const char TOP = '9';
const char BOTTOM = '0';
for(int x = 0; x < input.Length; x++)
{
c = input[x];
if (char.IsWhiteSpace(c))
goto outter;
// only allow the negative sign at the 'start' of the thing, ignore it otherwise.
if (isNegativeSign(negativeSign, input, x) && x == 0)
{
x += (negativeSign.Length - 1);
sb.Append(negativeSign);
}
if ((c <= TOP && c >= BOTTOM) )
{
sb.Append(c);
}
else if ( c == periodChar)
{
if (allowDecimal && !periodFound)
{
periodFound = true;
sb.Append(c);
}
else
goto outter;
}
}
outter:
string result = sb.ToString();
if (!string.IsNullOrEmpty(result) && result[result.Length - 1] == periodChar)
result = result + "0";
return result;
}
string FilterNumeric(string src, Type t)
{
string filtered = ExtractDigits(src, AllowsDecimals(t));
// if we filtered everything out, return the default value.
if (filtered.Length == 0)
return Activator.CreateInstance(t).ToString();
return filtered;
}
public void SetValue(string Field, object Value)
{
WebControl ctrl = FindControl(Field) as WebControl;
if (ctrl == null) throw new Exception(string.Format("Specified Control '{0}' was not found.", Field));
if (ctrl as ITextControl != null)
{
(ctrl as ITextControl).Text = FlexType<string>(Value);
return;
}
if (ctrl is ListControl)
{
(ctrl as ListControl).SelectedValue = FlexType<string>(Value);
return;
}
if (ctrl is CheckBox)
{
(ctrl as CheckBox).Checked = FlexType<bool>(Value);
return;
}
if (ctrl is Calendar)
{
(ctrl as Calendar).SelectedDate = FlexType<DateTime>(Value);
return;
}
throw new Exception(string.Format("Getting the value of the specified control '{0}' is not supported!", Field));
}
public object this[string idx]
{
get
{
return GetValue(idx);
}
set
{
SetValue(idx, value);
}
}
public AccessorUserControl()
{
//
// TODO: Add constructor logic here
//
}
}
}
You’ll want to derive your usercontrols from this thing, and then you can access them from the outside world by string field name, and even coerce the types, if they are compatible at all…
Keypoints here:
Now I guess you’d like to see an example: (this is using the codebehind model, I guess you can do it without a code behind, but I can’t take the time at the moment to figure it out : - )
The markup:
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="AddressEditor.ascx.cs" Inherits="AddressEditor" %>
<table>
<tr>
<td><asp:Label ID="StreetLabel" runat="server" Text="Street Address"/></td>
<td><asp:TextBox ID="Street" runat="server" /></td>
</tr>
<tr>
<td><asp:Label ID="CityLabel" runat="server" Text="City"/></td>
<td><asp:TextBox ID="City" runat="server" /></td>
</tr>
<tr>
<td><asp:Label ID="StateLabel" runat="server" Text="State"/></td>
<td>
<asp:DropDownList ID="State" runat="server">
<asp:ListItem Text="Alabama" />
<asp:ListItem Text="Alaska" />
<asp:ListItem Text="Arizona" />
<asp:ListItem Text="Arkansas" />
</asp:DropDownList>
</td>
</tr>
<tr>
<td><asp:Label ID="ZipLabel" runat="server" Text="US Zip Code"/></td>
<td><asp:TextBox ID="Zip" runat="server" /></td>
</tr>
</table>
First a usercontrol that’s derived from this monstrosity:
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public partial class AddressEditor : MyUserControls.AccessorUserControl
{
protected void Page_Load(object sender, EventArgs e)
{
// look ma, no code!
}
}
A class to be ‘filled’ with the data from the control:
public class Address
{
string _street;
string _city;
string _state;
int _zip;
public string Street { get { return _street; } set { _street = value; } }
public string City { get { return _city; } set { _city = value; } }
public string State { get { return _state; } set { _state = value; } }
public int Zip { get { return _zip; } set { _zip = value; } }
public Address(string st, string cit, string sta, int zp)
{
Street = st;
City = cit;
State = sta;
Zip = zp;
}
}
Then a page that actually uses it:
The markup:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ Register Src="AddressEditor.ascx" TagName="AddressEditor" TagPrefix="uc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<uc1:AddressEditor ID="AddressEditor1" runat="server" />
<asp:Button id="MakePostBack" runat="server" Text="Test"/>
</div>
</form>
</body>
</html>
The codebehind:
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (Page.IsPostBack)
{
Address DummyAddress =
new Address(AddressEditor1.GetValue<string>("Street"),
AddressEditor1.GetValue<string>("City"),
AddressEditor1.GetValue<string>("State"),
AddressEditor1.GetValue<int>("Zip"));
Response.Write("<p>");
if (DummyAddress.Zip <= 0)
Response.Write("Hey, you gave me a funny zip code. Very clever of you!");
Response.Write("<br/>Street: " + DummyAddress.Street);
Response.Write("<br/>City: " + DummyAddress.City);
Response.Write("<br/>State: " + DummyAddress.State);
Response.Write("<br/>Zip: " + DummyAddress.Zip);
Response.Write("</p>");
}
}
}
That should be enough to get you started being happier about Gobs of user input.. and save you a bit of trouble with people who turn of the javascript for their validation. It’s of course possible that the inferences made by this control could produce some undesired values (like zero or something when they enter nothing, or everything incorrectly for numeric types) but 99.9% of the time you still want a value even if it’s not the correct one, so I’m sure you can write the if (x == 0) code yourself for that other 0.1% of the time. Cheers!




More Options ...

Categories
Tag Cloud
Blog RSS
Comments RSS

Void (Default)
Life
Earth
Wind
Water
Fire
Lightweight