Mark Hildreth's Blog

asp:Menu in IE8

I've been using IE8 Beta 2 since sometime in August. As soon as I had downloaded it, I raced to my sites to see if there were any issues caused by the new browser's strict adherence to standards. I'm pretty conservative when it comes to my page and control design, so I didn't expect too many issues. Unfortunately, most of our sites rely on the asp:Menu control to provide navigation. The first thing I noticed was that I could no longer see the Dynamic menu items when hovering over the menu. This is a pretty serious problem because that's the main way users navigate around the site. I did some basic googling and searching on the ASP.NET Forums, and there were quite a number of people with the same problem, but there didn't seem to be an obvious solution.

Needless to say, ever since then I've been eagerly anticipating the IE8 release candidate in hopes that this issue would just fix itself. Well, the release candidate just hit yesterday and it turns out the issue with the ASP.NET Menu control wasn't resolved. I'm sure the breakage was the result of a change to make the browser more standards compliant. Of course that's a good thing, but it's still annoying that I will have to fix old code to account for new browser changes.

 As it turns out, the fix is really straight forward. I'd like to thank the original poster and the folks at Microsoft Connect for posting enough information on this post to get me started. When the menu is rendered, a script resource is included on the page that contains a function called PopOut_Show. The function detects and sets the z-index property of the panel containing the dynamic menu items. The problem is the value for the z-index is calculated differently depending only on whether or not the browser is IE, not which version of IE. Thus, the z-index calculations for IE were assuming that the element.currentStyle.zIndex property would return a numeric value, but in IE8, unless the z-index for these elements has been specified (in a stylesheet or somewhere), the return value is "auto".

I had originally toyed around with using script to adjust the z-index values. That involved swapping in a new PopOut_Show function to append additional logic to account the "auto" value. After I took a second look at it, I realized the same thing could be done using only CSS which seemed like a much better solution. It turned out that all that was really needed was to create or modify a CSS class that includes a z-index value of at least 1, then assign the class to the Menu's DynamicMenuStyle property.  The CSS class would look something like this:

.adjustedZIndex {
    z-index: 1;
}

And the resulting Menu looks something like this:

<asp:Menu ID="Menu1" runat="server">
    <DynamicMenuStyle CssClass="adjustedZIndex" />
</asp:Menu>

You may have to set your z-index to be something higher (for example, if your menu was contained in an element that already had a z-index greater than 1), but this worked for me. I hope this is a help to those who have run into the same issue. Good luck with IE8 - My experience so far has been great!

 If you're interested in the script solution, just let me know and I'll post it.

Posted: Jan 26 2009, 09:57 PM by mhildreth | with 410 comment(s)
Filed under: , , ,
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

Eliminating Whitespace in the Page Response

One of the many features that makes ASP.NET applications so convenient to develop is the ability to mix traditional html (or xhtml) with server controls. What's even more convenient is that the markup that is output by the page preserves the formatting of the source and outputs the control content with (relatively) well formatted markup accordingly. When trying to pinpoint trouble spots, having formatted output makes it easy to read through the page's html output. 

This formatting is provided by adding new-line and tab characters into the page output. When the browser interprets this html, it ignores these extra whitespace characters. Therefore, in a productions environment, the whitespace characters are just unnecessary overhead that increase the overall size of the page.

Fortunately, the .NET Framework makes it very easy to eliminate this overhead by simply overriding a couple of methods. All the html content output by an ASP.NET page is generated by an HtmlTextWriter. The HtmlTextWriter provides a series of methods that simplifly generating html tags and attributes in an html document. Fortunately, the Page class allows you to override the method used for creating the HtmlTextWriter used to generate the html response. In addition, the two methods used to provide the formatting characters have been marked virtual - making it easy to override them.

First we'll create a new HtmlTextWriter class:

public class CompactHtmlTextWriter : HtmlTextWriter
{
    public CompactHtmlTextWriter(System.IO.TextWriter writer)
        : base(writer)
    {
    }

    public CompactHtmlTextWriter(System.IO.TextWriter writer, string tabString)
        : base(writer, tabString)
    {
    }

    public override void WriteLine()
    {
        //base.WriteLine();
    }

    protected override void OutputTabs()
    {
        //base.OutputTabs();
    }
}

I've simply overidden the WriteLine and OutputTabs methods so that they do nothing. Next, we'll need to ensure that our pages utilize our new CompactHtmlTextWriter instead of a normal HtmlTextWriter. To do so, we'll need to create a BasePage that we can use to derive our new pages from:

    public class BasePage : System.Web.UI.Page
    {
        protected override HtmlTextWriter CreateHtmlTextWriter(System.IO.TextWriter tw)
        {
            if (HttpContext.Current.IsDebuggingEnabled)
            {
                return base.CreateHtmlTextWriter(tw);
            }
            return new CompactHtmlTextWriter(tw);
        }
    }

Here, I've overidden the Page's CreateHtmlTextWriter method to return and instance of our new CompactHtmlTextWriter class if the has it's debug setting turned off (as it would be in a production environment). Otherwise, the page will utilize it's original behavior.

Now all that's left to do is to modify that pages in our site to utilize our BasePage class instead of the standard System.Web.UI.Page. This can be accomplished in a couple of ways. If you're using the inline code model, then you have two options. First, you can specify the base class of individual pages by setting the Inherits attribute in the Page directive like so:

<%@ Page Language="C#" Inherits="BasePage" %>

 Alternatively, you can set the pageBaseType attribute of the pages tag in the web.config like so:

<?xml version="1.0"?>
<configuration>
 <system.web>
  <pages pageBaseType="BasePage"/>
 </system.web>
</configuration>

If you are using the code-behind model for your pages, then simply change the class your page's code-behind class inherits from from

public partial class Default2 : System.Web.UI.Page

to

public partial class Default2 : BasePage

Please note that the code above is for a Visual Studio Web Site project - If you're using the Web Application project, you may have to make some changes. I have posted a zip file of a sample Web Site that demonstrates this. If there is interest in a sample that uses the Web Application project, send me a note and I can get that up too. 

That's it! Eliminating all that extra whitespace can really add up - especially on large pages. You may see a savings of as much as 10%. It may not sound like much, but over the course of the lifetime of an application, that can really add up. Happy coding!

Posted: Oct 09 2007, 08:48 PM by mhildreth | with 9 comment(s)
Filed under:
My first blog entry

Alright. So this is my first blog entry. Not sure where to start, but here goes. First off, thanks to Joe Stagner for the chance to get this started. About me - I work for Chainbridge Technologies here in the Washington, DC metro area as a senior software developer. We’re a Microsoft shop focused on selling and providing services for our core software product, SQLDBI. My experience is with data-driven ASP.NET development and associated technologies – XHTML, CSS, Javascript, and SQL Server, so I’ll be focusing my blog on these areas. Hope you enjoy it!

More Posts