Monday, July 04, 2005 8:53 PM
hannes
Custom ASP .net updown control
I created this custom control to simplify numeric inputs on web pages, since I could not find anything similar.
It is not very feature rich at the moment, because it is built only to serve our purposes, which are:
- Perform client-side validation and formatting of the number (JavaScript)
- Keep the number between the specified minimum and maximum values
- Allow the user to scroll the value by clicking on up/down buttons
I will post a link to the control's source code as soon as I have uploaded the file (no FTP access at work).
Here is description of the control for those who want to create simple custom web controls.
1. Helper functions
The value is converted to a string using the following formatting string. This is done to avoid floating-point inaccuracies, eg. a result of 1.999999999 instead of 2.
private const string PRECISION_FORMATSTR = "0.#########";
A unique identifier for JavaScript is obtained from the control's UniqueID property by removing the illegal : characters.
private string JID
{ get { return this.UniqueID.Replace(":", "_"); } }
The number of decimals to display, are automatically calculated from the step value. Eg a step value of 0.05 would need 2 decimals.
private int Decimals()
{
string s = Step.ToString(PRECISION_FORMATSTR);
int i = s.IndexOf('.');
if (i == -1)
return 0;
return s.Length - (i+1);
}
Whenever the value is adjusted, it is clipped, ie. forced to be between the minimum/maximum values. Another issue is that the step value indicates the smallest allowable step. Therefore a value of 1.05 with a step value of 0.02, would be corrected to 1.06.
private double ClipValue(double d)
{
double minv = MinValue;
double maxv = MaxValue;
double stp = Step;
double ret = Math.Round((d - minv) / stp) * stp + minv;
return (ret < minv) ? minv : ( (ret > maxv) ? maxv : ret );
}
The text formatting is simple - a prefix, suffix, and a fixed number of decimal places.
private string TextFromValue(double d)
{ return Prefix + string.Format("{0:F" + Decimals().ToString() + "}", d) + Suffix; }
The text input is "forgiving" - all non-numeric characters are ignored. This is needed because the prefix/suffix are part of the text in the text box, and a user may opt to not remove these when editing the text. This function simply strips away all the garbage and converts what is left to a number.
private double ValueFromText(string s)
{
int pos;
string s2 = "";
for (pos=0; pos {
char c = (s[pos]);
if ((c == '+') || (c == '-') || (c == '.') || ((c >= '0') && (c <= '9')))
s2 += c;
}
double stp = Step;
return Math.Round(Convert.ToDouble(s2) / stp) * stp;
}
2. Post-back
The first thing to do is to implement IPostBackDataHandler. A server-side Change event is also implemented. Note that this control posts back a single text value. This simplifies things.
public event EventHandler Changed;
public virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection)
{
double newval = ValueFromText(postCollection[postDataKey]);
if (newval != Value)
{
Value = newval;
return true;
}
else
return false;
}
public virtual void RaisePostDataChangedEvent()
{ if (Changed != null) Changed(this, new EventArgs()); }
3. Properties
The control's properties are all stored using ViewState. This is a bit wasteful and lazy, but there are only a couple of values to persist.
The control's value can be set through either the Value or Text property. Both will update ViewState["Value"].
[Bindable(true), Category("Appearance"), DefaultValue(0)]
public double Value
{
get
{
return (ViewState["Value"] == null) ? 0 :
Convert.ToDouble(ViewState["Value"]);
}
set
{
ViewState["Value"] = ClipValue(value);
// must not SET the text (it is calculated from Value)
}
}
[Bindable(true), Category("Appearance"), DefaultValue("0.00")]
public string Text
{
get
{
return TextFromValue(Value);
}
set
{
try
{
Value = ValueFromText(value);
}
catch
{
// ignore invalid text assignment.
}
}
}
These remaining properties are all simple read/write values stored in ViewState:
public double MinValue
public double MaxValue
public double Step
public string Prefix
public string Suffix
4. Validation
Validation is not essential, but by implementing IValidator, the control can take part in form validation. A ServerValidatorEventHandler is exposed to allow the developer to easily perform custom server-side validation.
public event ServerValidateEventHandler ServerValidate;
public bool IsValid
{
get
{
return _IsValid;
}
set
{
_IsValid = value;
}
}
public string ErrorMessage
{
get
{
return "The number is invalid.";
}
set
{
}
}
public void Validate()
{
if (ServerValidate != null)
{
_IsValid = true;
ServerValidateEventArgs e = new ServerValidateEventArgs(Value.ToString(PRECISION_FORMATSTR), _IsValid);
ServerValidate(this, e);
_IsValid = e.IsValid;
}
}
protected override void OnInit(EventArgs e)
{
_IsValid = true;
Page.Validators.Add(this);
base.OnInit (e);
}
protected override void OnUnload(EventArgs e)
{
if (Page != null)
{
Page.Validators.Remove(this);
}
base.OnUnload(e);
}
5. Rendering
The actual HTML and JavaScript required for each instance of the updown control is quite big (perhaps 2kb). This was optimised, so the HTML/Javascript for the control instance is now emitted by JavaScript. Unfortunately this also broke design-time rendering.
As you can imagine, JavaScript which emits JavaScript can be tough to read, especially within escaped string constants. Instead, I will explain the AddUpDown script later.
In the OnPreRender event, this global script is registered.
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
string Script = "<SCRIPT>\n" +
"var iPPUpDownTimerID = 0;\n" +
"function udgetval(s, stp) {\n" +
"var pos; var s2;" +
"s2 = \"\";" +
"for (pos=0; pos<s.length; pos++) {" +
"var c = s.charAt(pos);" +
"if ((c=='+') || (c=='-') || (c=='.') || ((c>='0') && (c<='9'))) {" +
"s2 = s2 + c; } }" +
"var ret = Math.round(parseFloat(s2) / stp) * stp;" +
"if (isNaN(ret)) ret = 0;" +
"return ret; }\n" +
"function AddUpdown(jid,inc,min,max,prefix,decimals,suffix,textid,changejscript," +
"text,textheight,textwidth,btnheight,btnwidth,upimgurl,downimgurl) {\n" +
... ADDUPDOWN SCRIPT ...
"}\n" +
"</SCRIPT>
\n";
if (!Page.IsClientScriptBlockRegistered("ppudctrl"))
Page.RegisterClientScriptBlock("ppudctrl", Script);
}
The control is emitted as a single JavaScript call to the function "AddUpDown", which in turn emits the control and its required JavaScript.
protected override void Render(HtmlTextWriter output)
{
string HTML = "<SCRIPT>\n" +
"AddUpdown(" +
"\"" + JID + "\"," +
"\"" + Step.ToString(PRECISION_FORMATSTR) + "\"," +
"\"" + MinValue.ToString(PRECISION_FORMATSTR) + "\"," +
"\"" + MaxValue.ToString(PRECISION_FORMATSTR) + "\"," +
FmtConstantJScript(Escape_SingleQuotes_Backslash(Prefix)) + "," +
"\"" + Decimals().ToString() + "\"," +
FmtConstantJScript(Escape_SingleQuotes_Backslash(Suffix)) + "," +
"\"" + TEXTID() + "\"," +
FmtConstantJScript(ChangeJScript) + "," +
FmtConstantJScript(Text.Replace("\"", """)) + "," +
"\"" + TEXTHEIGHT().ToString() + "\"," +
"\"" + TEXTWIDTH().ToString() + "\"," +
"\"" + BTNHEIGHT().ToString() + "\"," +
"\"" + BTNWIDTH().ToString() + "\"," +
FmtConstantJScript(Escape_SingleQuotes_Backslash(UPIMGURL())) + "," +
FmtConstantJScript(Escape_SingleQuotes_Backslash(DOWNIMGURL())) + ");" +
"</SCRIPT>
\n";
output.Write(HTML);
}
6. AddUpDown() JavaScript
This script produces the actual dhtml updown control.
It emits a table containing a input textbox and two buttons.
JavaScript is emitted to allow the user to scroll the textbox value up/down with the arrow keys
The user can also click and hold the up/down buttons for scrolling the value.
The value is also validated/clipped/formatted whenever the textbox loses focus.
Filed under: ASP .net