John Sheehan : Blog

About This Post

Custom jQuery Selector for ASP.NET WebForms

UPDATE: Commenters have posted bug fixes and a version compatible with jQuery 1.3.

I’ve built a ton of ASP.NET web sites using WebForms. Unfortunately I don’t have the luxury of rewriting them all using ASP.NET MVC so I’m stuck dealing with INamingContainer and the resulting auto-generated HTML element IDs.

Here are all the ways I know of that people use to get around this.

Output the ClientID using <%= %>
I don’t like this because it can make my markup messy, requires server-side interaction and doesn’t work in external script files.

Output a JavaScript Object
I first saw this method on Rick Strahl’s blog. I don’t like this method because it generates even more script to send down the wire and it requires you to specify the IDs in multiple places (codebehind to include them in the object and again in the jQuery selector).

Select by Class Instead of ID
The purist in me doesn’t like using classes as IDs and in some situations, this will conflict with well-structured CSS. Also, selecting by class is slower than by ID (although this is improving in newer browsers).

Select using jQuery’s Attribute Selectors
I came across this method on StackOverflow (don’t recall which question though). It’s the best of the bunch as it is the most concise and doesn’t require any server-side preparation. Here’s an example:

$("*[id$='TextBox1']").show();

This looks for all elements (*) that have an ID that ends with the specified text. Since INamingContainer-generated IDs always end in the ID you specify in your ASPX markup, it works. It’s also a valid CSS3 selector so it’s consistent with the way jQuery works. Still feels like a kludge to me.

I decided to see if I could try to find a more readable selection method. This was my goal:

$("#TextBox1:asp").show();

After looking into jQuery’s custom selectors and stepping through a crap ton of the jQuery core code in Firebug, I don’t believe this is possible. After tweaking and playing, I got this working:

$(":asp('TextBox1')").show();

Here’s how it works. It’s actually pretty simple. Add jQuery to your page. Then add the following script, either inline or in an external file.

String.prototype.endsWith = function(str) {
    return (this.match(str + '$') == str)
}

jQuery.extend(
    jQuery.expr[":"],
    {
        asp: "jQuery(a).attr('id').endsWith(m[3]);"
    }
);

I’m not going to go into the details of how this all works. The first section adds an ‘endsWith’ method to strings for brevity’s sake. The second section extends the jQuery object with the custom selector. For more info on how that works, I would suggest reading the jQuery docs and this tutorial.

Whether or not this is better than an attribute selector is up to your readability preferences, but it’s another option you can use. If anyone knows how to access the part of a selector before the colon, I’d love to hear about it.

  • Rick
    There's one problem with any EndsWith selection - the same problem that INamingContainer is trying to solve. If the element is inside a repeater, each will end with the same value. Or, if you have two similarly named elements "Password" / "VerifyPassword" would both match for "Password". I would suggest delimiting by the separator character for some added protection.
  • Rick: Couldn't you then use the .each() function? Also, in a situation like like a repeater it wouldn't be out of order to use a class selector.
    I do agree though, there are a few situations where something like "TextBox1" might happen more than once, such as multiple user controls on one page.

    This is a brilliant solution John, thanks! I can't wait to try it.
  • Items in a repeater shouldn't have the same ID as that's invalid HTML (each ID should only be used once on a page) and as Eric mentioned, that's a great case for using classes. Similar named items is definitely a valid concern. The problem is if I add the underscore ASP.NET generates before the ID, pages without panels/user controls/master pages won't work with this method as they spit out a valid, unaltered ID.

    I'd be fine with INamingContainer if it was limited to repeaters and such. It's master pages and user controls that cause the most problems for me.
  • John, this is pretty cool. Actually I didn't realize you could extend selector filters at all, but that makes sense.

    FWIW, some time ago I updated the component you reference above and have an option to auto generate ALL clientIds on the object. As you mention the downside there is that it sends all those ids to the client when the page is loaded but you do end up with a single object with properties. (serverVars.txtNameId for example) without having to modify server code specifically to add controls. I've used this quite a bit and it works and is worth the overhead on all but really huge forms with tons of controls.

    Ultimately though I've come to prefer marking up controls with pseudo classes rather than IDs. That makes it much easier:

    $(".txtName").blah()

    Still an extra step but an easy and also very explicit one.
  • @John - great idea. I wrote the original StackOverflow item that you discuss as a result of running into this issue while adding jQuery support to DotNetNuke. This selector would definitely simplify things a little.
  • "Since INamingContainer-generated IDs always end in the ID you specify in your ASPX markup, it works."

    What happens when you have two user controls on a page, each with a textbox1? These will be client-ized by msft as something like ctl01_textbox1 and ctl02_textbox1. Which textbox1 will your code select?

    You could tell developers on large projects to communicate with one another and make sure their user controls absolutely never have nested controls with overlapping names, but ...

    (BTW - great blog!)
  • I don't usually need to select just one item in a repeater, so that's not an issue for me. This code would select all matches and act on them all. That might be what you want anyway.
  • Loque
    You should subscribe to the jQuery list on google groups... people on there are amazing and can give feedback to people of all levels - even if I dont have time to read it most of the time (as its very popular) its still great for posting a quick question like this and seeing what you get back :¬)

    Wish i had more time to actually offer a suggestion too, i dont think you need to extend jQuery to do what you want :¬)
  • I posted a question about this on the mailing list and didn't get a response.
  • You're been voted!!
    Track back from http://webdevvote.com/Tips_And_Tricks/Custom_jQ...
  • Tim Banks
    Hey John. I just upgraded to jQuery 1.3 and I am receiving an error when trying to use your custom selector. Have you experienced any problems when upgrading to 1.3?

    I will continue to look into the situation myself and let you know if I find anything.

    Thanks.
  • Tim Banks
    I just figured out how to update this to make it work.

    Instead of using:
    jQuery.extend(
    jQuery.expr[":"],
    {
    asp: "jQuery(a).attr('id').endsWith(m[3]);"
    }
    );

    Use this instead:
    jQuery.extend(
    jQuery.expr[":"].asp = function(a,i,m) {
    return jQuery(a).attr('id').endsWith(m[3]);
    }
    );

    Hope this helps out
  • Thanks Tim. I was actually going to revisit this tonight, so I'll
    include this in any update I post.
  • Brian Grinstead
    I noticed that this doesn't work in Internet Explorer when testing (an error like 'attr(...)' is null or not an object. I figured this meant that it was trying to apply endswith on an object that is undefined. I was able to make it work by:

    jQuery.expr[":"].asp = function(a, i, m) {
    return jQuery(a).attr('id') && jQuery(a).attr('id').endsWith(m[3]);
    };

    May perform slightly better if it is formed like this:

    jQuery.expr[":"].asp = function(a, i, m) {
    return (id = jQuery(a).attr('id')) && id.endsWith(m[3]);
    };

    Either way, it works fine in Internet Explorer now.
  • Thanks, I'll make note of this here: http://john-sheehan.com/blog/updated-webforms-c...
  • Thanks for this. I think you should alter it though, so that either the entire string has to match, or it has to end with an underscore followed by the search term. Otherwise it can match other elements which end in the same string.
  • aXiniXe
    function SelectByID(type, endPart)
    {
    return element = $(type + "[id$=" + endPart + "]");
    }

    seemed to do the trick for me,
    also using _ as leading in the endpart to avoid mismacthes.
  • Recently I made a post about custom jQuery selectors and thought you might find it usefule. It explains how one can create a custom selector WITH parameters:

    http://jquery-howto.blogspot.com/2009/06/jquery...
blog comments powered by Disqus