Friday, May 29, 2009

JavaScript and User Controls 101

I've been doing quite a bit with user controls and JavaScript. There is an art to using the JavaScript correctly, wiring it in and having it work consistently. I thought I'd post a brief article on the "101" around this based on the things I've found. Hopefully this can be a quick-and-dirty reference guide for those of you working to extend your user controls with JavaScript functionality.

Understand Multiplicity

First, understand if your user control can potentially be rendered multiple times on a page. For the most part, I assume this will be the case. However, there are some circumstances, such as a large grid control, where you may absolutely know there is only going to be a single instance. The reason you want to understand this is because you want to try to have exactly one copy of the JavaScript code emitted to the browser, so it will need to be "aware" of the instance it is working with.

JQuery

I am a major fan of JQuery because of the fact it feels "natural" with the combination of XPath selector and CSS-style selections, chaining, and the browser compatibility is handled for me. There are correlating functions in other libraries, but this is what I'll be using for my examples.

Part 1: The ASCX file

I try to keep as clean a separation of code from the ASCX file as possible. The exception are elements that I must derive on the server side. These typically include references for callbacks, URLs (I like to strongly type my URL references), and wiring "init" events.

We'll use a hypothetical "contact" control. Here is what I have at the bottom of my ASCX file, and this assumes I am using the AJAX framework and therefore have somehow registered a ScriptManager (this is outside the scope of this document — see the Script Manager control overview:


<script type="text/javascript">
   window.Sys.Application.add_init(contactInit);
   contactCtrlClientID = '<%=ClientID%>';
</script>

Basically, I've registered with the AJAX framework and told it to call contactInit once everything is loaded. I've also created a reference to the client id. If I'm going to have multiples, I'd push them to an array or use some other method to keep track (windows provides handles and references in AJAX as well) of them ... this is just a quick and easy way to know how it is registered to the page if it is inside of a master page, etc.

The JavaScript file

There are many ways you can include the related JavaScript. Some people like to make it a straight include reference so they can easily update the JavaScript on disk and test changes, etc. We produce commericial software so the code is embedded (I like to do initial testing with an external resource, then embed the JavaScript). My favorite convention is to name the JavaScript the same as the control and have it side-by-side with the control in the designer (i.e. if my control is ContactCtrl.ascx, then my JavaScript is ContactCtrl.js and at the same level as the control).

Embedding your JavaScript

This is an easy three-step process.

  1. Right click on the properties of your JavaScript file and indicate you wish to make it an Embedded Resource. If you don't want to ship the source, make sure you have "do not copy" selected for the destination.
  2. Your web project should have a Properties folder with AssemblyInfo.cs. At the bottom of this file, you need to add a tag to identify the embedded resource. You will use the namespace, including the folders, and then the filename itself. For example, let's say the namespace for the project is Company.Project and the control is in a UserControls/Contact folder. Your resource will look like this:
    [assembly: WebResource("Company.Project.UserControls.Contact.ContactCtrl.js", "text/javascript")]
  3. Finally, register a reference to it. When you register an embedded resource, you must specify a type. This ties the code to the control and prevents it from being emitted multiple times if the control appears multiple times. It is important to use typeof instead of GetType() or it may not behave as expected. Our example, which can go in onPreRender or onLoad:
    ...
    Page.ClientScript.RegisterClientScriptResource(typeof(ContactCtrl),
       ("Company.Project.UserControls.Contact.ContactCtrl.js");
    ...

Initialization

When you register an initialize event, you can then hook in to "prep" the control. For example, maybe my contact control has some dynamic sections that are conditional based on a contact type. I can do something like this:


function contactInit() {

   $(document).ready({
      $("#divAdmin").hide();
      $("#selType").change(contactTypeChange); 
   });

}

function contactTypeChange() {

...
   $("#divAdmin").show();

}

Essentially, this will hide the admin section, then wire in a change event to the dropdown and call the type change method which can conditionally show it, etc.

Events

Good development practice dictates I design my control as self-contained as possible. For example, if I am making a search criteria control, I can't just assume I'll have a "search results grid" and when the user clicks "submit" send some client information over to the control. The proper way to have user controls talk on the client side is the same way on the server: through events. There are two ways I like to wire in my events. If I'm doing a light weight control, I'll tab into the global event pool and have a unique event name. If I'm developing a more extensive control, I'll wire in an AJAX behavior and expose events that way. To learn how to create a full-blown AJAX client control (something that can expose all of your server side events in a similar clientside model) read Creating Custom ASP.NET AJAX Client Controls.

Let's assume I want to expose an event that is raised whenever the user changes the contact type. This way I can have other controls that might dynamically respond to the change on the client side, avoiding a server round trip. I don't have a true AJAX client control that corresponds to my contact control, so what can I do? An easy method is to plug into the Application events - think of these as "global" events. In my contact, I just need to know if someone is listening to my event. I'll give it the name "contactTypeChange" and send along the new type as well as my client id in case I have multiple instances.


function contactTypeChange(clientID,contactType) {

   var contactHandler = window.Sys.Application.get_events().getHandler("contactTypeChanged");
   if (contactHandler) {
      contactHandler(clientID,contactType);
   }

}

Now, in my control that is listening to the event, I simply register to do something with it. Let's call this my "Contact Security" panel and it will show special admin security rights if the contact is an admin. First, in my init function, I'll register for the event. Second, I'll provide a function to handle the event.


function contactSecurityInit() {

   window.Sys.Application.get_events().addHandler("contactTypeChanged", contactTypeChangeHandler); 

}

function contactTypeChangeHandler(clientID, contactType) {

   if (contactType == "admin") {
      $("#divSecurityAdminInfo").show(); 
   }

}

Callbacks

Finally, a caveat on callbacks. I use these a lot, because of the fact that update panels tend to add a lot of overhead. If your application requires javascript, you can manage a lot of the state in the browser page and use more tightly defined callbacks to perform dynamic updates. One "gotcha" with callbacks is that you must ask for the callback function in order for the control to listen for it. The three steps I find are easiest to manage callbacks:

  1. Implement ICallbackHandler on your UserControl. This will generate two methods: one that takes an event argument, and one that asks for a return result. That's it! The event argument is what you control on the client side and pass down. In your control, you'll process some result and then can send back something to the client. It can be as simple as a little message to display, or as complex as a dynamic snippet of JavaScript to eval() on the client.
  2. Ask for the callback reference. This is important! Even if you will explicitly callback from your code, you need to "ask" for the reference in order for the control to listen. It's as simple as:
    ...
    Page.ClientScript.GetCallbackEventReference(this, string.Empty, string.Empty, null);
    ...
  3. Wire in the call!

For example, let's say we want to display some text in a div based on the contact type. We want the server to retrieve the text and perform globalization functions against it before sending it back from the browser. This snippet of code will do the trick:


WebForm_DoCallback(callbackRef, contactType, function(args, ctx) { 
   $("#divHelp").html(args);    }, null, null, false);

Callbackref is the reference to the control. You can embed the GetCallbackEventReference above in your ASCX page and render the page to see how the control is referenced. It's essentially the path to the control separated by dollar signs (if you have a masterCtrl then a pageCtrl then the contactCtrl, it will look like masterCtrl$pageCtrl$contactCtrl). The next is the argument that gets passed to the server, following by the function to process when the result is returned. Here, we do an inline function and find the div and fill it with what we got back from the server. (You can read more about script callbacks).

Conclusion

Obviously, there is a lot you can do with scripts and user controls and this post only scratched the surface. Hopefully this gave you a good idea of some good practices for associating JavaScript to a user control and ways you can use JavaScript to extend the functionality, allow for greater interoperability between controls, and streamline performance through the use of callbacks.

Jeremy Likness