Enforcing TextBox MaxLength with MS AJAX
If you've ever worked on data-driven webforms, then you know how crucial it is to protect against bad user input. One issue that has plagued me for a while is enforcing the MaxLength property on a TextBox when the TextMode property is set to MultiLine. The default behavior for the TextBox is to render an HTML input tag with its type attribute set to 'text'. When the TextMode is set to MultiLine, the TextBox renders and HTML textarea tag. Unfortunately, the textarea element does not have an attribute named 'maxlength', therefore, you can type whatever you want into it.
After about an hour of googling, I found dozens of examples, but none that really seemed to work "properly". By "properly", I'm saying that they didn't work, but most of them relied on the 'onkeyup' or 'onblur' events to trim the value entered after it had already been entered. Although this kind of worked, it didn't seem like the clean behavior you see in the browser when using the TextBox in SingleLine mode. I also had a lot of trouble finding scripts that worked reliably across browsers. To work around the various inconsistancies on different platforms, I made use the Sys.UI.DomEvent class to abstract the event argument properties and browser keycodes. In order to keep this solution as flexible as possible, I provided the sample in a format that does not depend on any server-side code.
Alright, let's get to the code. In order to enforce the maxlength the way I wanted, I knew I would need to create a javascript function to prevent extra keys from being typed into the box. The function I came up with relies on the way javascript event handlers can supress the browser's default behavior by specifying a return value. When the function returns true, the browser continues with it's default behavior. When the function returns false, then the browser essentially prevents the default behavior from occurring. The following was my initial starting point:
function handleKeyPress(e) { var textarea = e.target; var actualLength = textarea.value.length; var maxLength = textarea.maxlength; if (actualLength >= maxLength) { return false; } return true; }
Getting the basics down was easy. The basic idea is that you assign a 'maxlength' expando attribute on the TextBox. This can done using the ClientScriptManager class offered by the framework. This allows you to add attributes to elements of the DOM without invalidating the xhtml content. (NOTE that in the sample, I just used a fixed startup script to assign this attribute to alleviate the necessity for this to use server-side code.) This can be done with the following code in the Page_Load event on the server:
protected void Page_Load(object sender, EventArgs e) { Page.ClientScript.RegisterExpandoAttribute(TextArea1.ClientID, "maxlength", 20); }
Or with the following code on the client side:
document.getElementById('textarea1').maxlength = 20;
Then, you handle keystrokes made by the user by assigning an event handler to the textarea's onkeypress event. On each keystroke, you need to check the length of the value of the textarea and see if the current keystroke will put you over the limit. This can be done with the following code in the Page_Load event on the server:
protected void Page_Load(object sender, EventArgs e) { TextArea1.Attributes["onkeypress"] = "return handleKeyPress(event);"; }
This yields the following output when the page is rendered:
<textarea id="textarea1" cols="20" rows="3" onkeypress="return handleKeyPress(event);"></textarea>
What proved really difficult was getting it to work reliably across browsers. The final iteration of my function has 3 separate lines marked as hacks and relies significantly on the MS AJAX client-side framework. Each one is to accomodate a behavior of a different browser. Here is how the final function turned out:
function handleKeyPress(e) { var domEvent = new Sys.UI.DomEvent(e); // Hack to accomodate Firefox inconsistencies with the keyCode if (Sys.Browser.agent == Sys.Browser.Firefox && e.keyCode && (e.keyCode === 46)) { domEvent.keyCode = 127; } else { domEvent.keyCode = e.keyCode; } var textarea = domEvent.target; var charCode = domEvent.charCode; var textareaValue = textarea.value; // Hack to accomodate IE inconsistencies with whitespace textareaValue = textareaValue.replace(/\r\n/g, '\n'); var actualLength = textareaValue.length; if (actualLength >= textarea.maxlength) { switch(domEvent.keyCode) { case Sys.UI.Key.backspace: case Sys.UI.Key.tab: case Sys.UI.Key.esc: case Sys.UI.Key.pageUp: case Sys.UI.Key.pageDown: case Sys.UI.Key.end: case Sys.UI.Key.home: case Sys.UI.Key.left: case Sys.UI.Key.up: case Sys.UI.Key.right: case Sys.UI.Key.down: case Sys.UI.Key.del: return true; case Sys.UI.Key.enter: case Sys.UI.Key.space: return false; default: { // Handle highlight/replace operations if (document.selection) { var range = document.selection.createRange(); var rangeElement = range.parentElement(); if (rangeElement == textarea) { if (range.text.length > 0) { return true; } } } else if (textarea.selectionStart < textarea.selectionEnd) { return true; } } } // Hack to accomodate Safari inconsistencies with the keyCode if (domEvent.keyCode == 0 && domEvent.charCode == 0) { return true; } return false; } return true; }
The first thing to notice is that I've used the Sys.UI.DomEvent class to encapsulate the raw javascript event argument passed to the function. This helps cover up some of the differences among browsers by providing a consistent and reliable set of properties to access. The next issue I had was that IE does not raise the onkeypress event on a textarea when certain keys are pressed - namely the arrow keys - whereas FireFox, Opera, and Safari do. In my first pass at this function, the arrow and delete keys would become non-operational once the value entered hit the maximum length specified. The next issue I faced was that FireFox has some inconsistent keycodes, which make require a bit of normalization at the beginning of the function. Next, it turns out that that when a user enters a hard return in IE, it actually adds a newline AND a readline character into the box. That is why you see that the length check actually occurs against a value where this combination has been normalized to simple newline characters. Next I had to make sure that the function would only supress the event if it was going to increase the length of the value of the textarea. What that means is that pressing the arrow keys, home, delete, and a few others should always be allowed, whereas alphanumeric keys and whitespace needs to get suppressed. Lastly, if a user had typed in the maximum number of characters, they should be able to highlight a few and then press a key to replace the highlighted text. This took some figuring but I got it worked out. It was here where the browser implementations varied the most, but the I was able to get the code working in the 4 big browsers. The only major shortcoming here is that this doesn't handle paste operations. I'll keep that on the TODO list, but from what I've read, IE is the only browser (except for the forthcoming FireFox 3.0) that supprt the 'onpaste' event.
Again, the sample provided is geared to be a purely server-independent solution. All that is required is the script references to the MS AJAX client library files. That being said, it would be pretty easy to convert this into either a specialized TextBox server control or an AJAX control extender. If there is interest in this, of if you any issues/comments, please drop me a line. Thanks for reading!
-Mark