Поделиться через


ICallbackEventHandler

ASP.NET AJAX is a piece of fundamentally enabling technology for developers; everything from partial page rending using UpdatePanels to the cross-browser compatible script library enable you to provide engaging web solutions while minimizing effort and time. Personally, I think the ASP.NET AJAX libraries are something that every software engineer building solutions for the Web should be be intimately familiar with.

While ASP.NET AJAX may be a wonderful, wonderful thing, it is helpful to remember some of the other ways we can make use of script on the client to achieve fast, friendly sites while minimizing post-backs. ICallbackEventHandler, an interface added to ASP.NET way back in the 2.0 days, is a wonderful solution. Not only is it relatively simple to implement, it allows you to keep everything nice and tidy inside your server control assembly. Way back in 2005, Dino Esposito of MSDN Magazine fame (amongs other claims to fame) wrote an excellent article describing ICallbackEventHandler in all its glory.

Now, I spend a lot of time in developing for SharePoint. SharePoint has some wonderful controls that produce a great experience for the end user. One of the controls I love the most is the ubiquitous people picker control. Type in a name, click a button, and voila - SharePoint automagically figures out if the name you typed matches a user account it knows about, and if so, formats the user account name to show that it knows. All without a postback. This is great when doing things like assigning users to groups in SharePoint, as your not constantly switching back and forth between one page and the next, creating a disjointed and post-back happy user experience.

How does the people picker control in SharePoint do this? Does it host the control in an UpdatePanel, or use a Web Service to process the data you type? Why no - in fact, it uses ICallbackEventHandler to achieve its functionality.

And so, for this article, I'll show you how to write a control that's inspired by the people picker control in SharePoint, and I'll use ICallbackEventHandler to do it. Along the way, you'll see the benefits of embedded javascript and CSS resources for server control developers and how a few WebResource attributes can really simplify our lives. When all is said and done, you'll have a control that can verify that a particular persons username exists in the membership store for the current sites configured membership provider in a nice, attractive, and reusable way.

Let's get started!

Assumptions

For this walkthrough, I'm assuming that you're using Visual Studio 2008. Realistically, the technique described here can be done in Visual Studio 2005; the attached sample code consists of a Visual Studio 2008 solution. If I get enough requests, I'll gladly provide a VS 2005 sample solution.

Anatomy of the Control

For our control, we'll keep the user interface relatively simple. A styled textbox with a styled button adjacent to it should do the trick. The end user will type the name of the user he wishes to verify the existence of in the textbox; when he clicks the button, we'll perform a callback to the server through script, executing our callback handler which will grab the value of the textbox and verify the existing of a user with that name in the membership provider. We'll then return a result to the client which will be processed by a script method of our choosing; this script method will update the style of the textbox to indicate to the user that the user was found; otherwise, the script will leave the textbox as it is. You could always embellish the solution with a few more bells and whistles - perhaps instead of relying on the style of the textbox to indicate success or failure, maybe throw a little status icon next to the textbox itself. That I'll leave to you.

People Picker Render Example

The image on the left shows what are control will look like. We'll achieve this by producing a custom composite control. Let's start our little sojourn by writing the JavaScript that will make our solution shine.

The Script

To start, crack open Visual Studio and pick the ASP.NET Server Control project template. My project is named PeoplePicker.Controls, so I'll assume you'll name yours the same as well. Now, add a new JavaScript file to the project and name it PeoplePicker.js. Select the JavaScript file and change the Build Action to Embedded Resource. This is important, as we'll discuss later.

Open your newly created JavaScript file, and we're going to add two functions to this file. The first function, verifyUserName(), is the function that will be invoked after our callback has executed on the server. This is where we get our results back, and do some processing. The second function, makeEditable(), is going to be called whenever someone double clicks on our text box. If verifyUserName() receives a positive response from the server, we're going to style the contents of the text box by making the text it contains underlined. We'll also make the textbox read-only, further indicating to the user that we were able to find the user they specified. If the user double clicks in the textbox, we'll undo these changes to allow the user to specify someone else.

function verifyUserName(result, context) {``     // result contains a string value provided by our callback function on the server     // context contains a reference to our text box control     context.value = result;     context.readOnly = true;     context.style.textDecorationUnderline = true;}

function makeEditable(textElement) {     var textBox = document.getElementById(textElement);     textBox.readOnly = false;     textBox.style.textDecorationUnderline = false;     textBox.select(); }

You'll notice that in verifyUserName, we set the value of the textbox (rememer, context points to our textbox on the web page) to whatever happens to be in the result variable. We do this because our server-side callback sends the name of the user found back to the client. Now, this implementation doesn't have any facility for searching - if your username is john and you type in joh, no dice - but using this technique, it'd be a snap to add something pretty cool.

Now, on to our stylesheet. Add a new stylesheet to the project named, ahem, PeoplePicker.css. Just like the JavaScript file, set the Build Action of this file to Embedded Resource. Now, open it up if it's not already open.

.peoplepicker-input{     border: 1px solid black;      text-decoration: none;      width: 150px;     height: 17px;}

.peoplepicker-button{     border: 1px solid black;     width: 50px;     height: 21px;} 

These two styles are all that we'll have - give a little shine and polish to the textbox and button is really what we're looking for. Of course, you should feel free to edit these to suit your own tastes. When it comes to art direction, you don't want my advice. ;-)

That leaves just one thing left to build! And that's the server control itself. But before we get to that, a quick word about web resources (and all that Embedded Resource action we've been seeing...)

A Quick Word on Web Resources

In order to expose our script and stylesheets to the client, with a minimum of muss and fuss, we're going to add two assembly-level attributes to our projects AssemblyInfo.cs file. This file is normally found underneat the Properties folder of our project. If you crack that file open, you'll see a ton of attributes applied to the assembly already - this is where most people like to keep their assembly attributes, and Visual Studio places all the assembly attributes it applies in here by default. Realistically, you can put these assembly attributes anywhere, but I prefer to keep them all together.

[assembly: WebResource("PeoplePicker.Controls.PeoplePicker.js", "text/javascript")][assembly: WebResource("PeoplePicker.Controls.PeoplePicker.css", "text/css")]

Briefly, what you are doing here is you are attaching metadata to the assembly that tells the ASP.NET runtime about two embedded resources in your assembly that you want to be surfaced on the page through a web resource link. Now, the exact form of the link is up to you - nothing magical happens without you making it happen - and we'll define how to surface these resources in our server control proper.

For more information on the power and flexibility of WebResources, take a look at this fine description on MSDN.

The Server Control

We're almost there. Now, to write the server control. Add a class to your project (or rename a class that Visual Studio created by default) named PeoplePicker. Crack open the newly created PeoplePicker.cs file, and change add the following code:

[ToolboxData("<{0}:PeoplePicker runat=server></{0}:PeoplePicker>")]public class PeoplePicker     : CompositeControl, ICallbackEventHandler{     private TextBox m_userName;     private Button m_checkUserNameButton;          private string m_callbackResult;     public PeoplePicker()          : base()     {          m_callbackResult = string.Empty;     }

What we've done here is declared that our class inherits from CompositeControl (one of the server control base classes) and implements the ICallbackEventHandler interface (more on that later). We've defined a few private variables to hold references to our child controls we'll be rendering to the client, as well as a string that will contain the results to send to the client as a result of the client callback. In our constructor, we just initialize our string to string.Empty, so that we don't ever send a null back to the client.

protected override void OnInit(EventArgs e)
{
    // Register Client Script & Style Sheets
    if (this.Page != null)
    {
        this.Page.ClientScript.RegisterClientScriptResource(typeof(PeoplePicker), "PeoplePicker.Controls.PeoplePicker.js");

        System.Web.UI.HtmlControls.HtmlLink cssLink = new System.Web.UI.HtmlControls.HtmlLink();
        string cssUrl = this.Page.ClientScript.GetWebResourceUrl(typeof(PeoplePicker), "PeoplePicker.Controls.PeoplePicker.css");

        cssLink.Href = cssUrl;
        cssLink.Attributes.Add("rel", "stylesheet");
        cssLink.Attributes.Add("type", "text/css");

        this.Page.Header.Controls.Add(cssLink);
    }

    base.OnInit(e);
}

Here, we finally get to make use of our WebResource marked embedded stylesheets and JavaScript files. The JavaScript file is the easiest to deal with - simply use the RegisterClientScriptResource method of the ClientScriptManager associated with Page object hosting our control. The stylesheet is a little more complicated, but not by much. We simply construct an instance of the very handy HtmlLink control and use the ClientScriptManager object to generate a URL to our WebResource - essentially, the same thing that ClientScriptManager does for script files, but we do the dirty work ourselves. Once we've got the URL to our stylesheet resource from the ClientScriptManager, we set the Href property of our HtmlLink instance to point to our stylesheet URL, add a few attributes to make sure the link is interpreted properly by the browser, then add the HtmlLink control to the control hierarchy of the hosting Pages' header.

Tada! We've injected script and styles with nary a funky call to HtmlTextWriter. I think this approach is a bit more maintainable to boot, and if you read the link I provided earlier to the MSDN article describing all the fun things you can do with WebResources, you'll also know that you can you use this same facility to provide localized scripts to your web clients. Pretty fancy, if you ask me.

protected override void CreateChildControls()
{
    base.CreateChildControls();

    m_userName = new TextBox();
    m_checkUserNameButton = new Button();

    m_userName.CssClass = "peoplepicker-input";
    m_checkUserNameButton.CssClass = "peoplepicker-button";

    m_checkUserNameButton.Text = "Check";

    // Controls won't have ClientID's generated until *after* they are added to the control
    // collection.
    Controls.Add(m_userName);
    Controls.Add(new LiteralControl("&nbsp;"));
    Controls.Add(m_checkUserNameButton);

    string js = String.Format("javascript:{0};{1};{2}; return false;", "__theFormPostData = ''", "WebForm_InitCallback()", this.Page.ClientScript.GetCallbackEventReference(this, "", "VerifyUserName", m_userName.ClientID));

    m_checkUserNameButton.OnClientClick = js;
    m_userName.Attributes.Add("ondblclick", string.Format("return makeEditable('{0}');", m_userName.ClientID));
}

Now, if you've ever written a composite control before, this all looks pretty straightforward.  But what's that mess in the line we build the JavaScript call used by our button? __theFormPostData = ''? WebForm_InitCallback()? Huh?

Well, here's the trick. By default, the state of your controls on the page don't get sent back to the server with the callback request. WebForm_InitCallback() is actually called early in the page render lifecycle, and so normally when your callback on the server would fire your textbox control would contain nothing. Here, we fix that - althought in a sort-of-workaround way, but hey Dino Esposito used this techinique in his MSDN article, and if its good enough for Dino, it's good enough for me. By clearing the post data variable and calling WebForm_InitCallback() again, we collect any values that have changed in our server controls, and that gets sent back to the server with our callback request - enabling us to get at the value of our client-side textbox without jumping through hoops.

Okay. Next possibly confusing part - this.Page.ClientScript.GetCallbackEventReferencce... yada yada yada. The ClientScriptManager associated with our Page object knows all about ICallbackEventHandler, and in fact provides us a method that will give us the script needed to invoke the callback method on the server. In order to do this, it needs to know the control for the callback, any arguments to provide the callback (we don't need any), the name of the client-side function to invoke when the callback is complete, and a context value.

Now, remember, way back when we were writing the script for this control, we noted that we get a reference to our textbox control passed to use in the context variable of our script callback? This is where that happens - we pass to our callback script the ClientID of the textbox control, which we can use from script to reference the control on the page. Since someone could potentially add multiple instances of our control to the same page, doing this frees us from having to keep track of which instance of our control on the page to mess with. We just use context, and we're done with it.

And to be honest, that folks concludes the most interesting portion of our control. Pretty simple, eh? But for completeness's sake, here's the rest of the code.

protected override void Render(HtmlTextWriter writer)
{
    EnsureChildControls();
    base.Render(writer);
}

#region ICallbackEventHandler Members
public string GetCallbackResult()
{
    return m_callbackResult;
}

public void RaiseCallbackEvent(string eventArgument)
{
    System.Web.Security.MembershipUserCollection users = System.Web.Security.Membership.FindUsersByName(m_userName.Text);

    if (users.Count > 1 || users.Count == 0)
        m_callbackResult = string.Empty;
    else if (users.Count == 1)
    {
        System.Web.Security.MembershipUser u = null;

        foreach (System.Web.Security.MembershipUser mu in users)
        {
            u = mu;
            break;
        }

        if (u != null)
            m_callbackResult = u.UserName;
    }
}
#endregion

In our overriden render method, I just make sure that our CreateChildControls method has been called by calling EnsureChildControls(), and in our two ICallbackEventHandler methods I do the relatively simple work this control set out to do - find a user in the configured Membership provider. Now, when it comes to ICallbackEventHandler it's important to note that RaiseCallbackEvent is the method that gets called on your callback, and GetCallbackResult is the method that gets called to return a value to the client. This is why we use a private member variable to hold the result we return to the client. In RaiseCallbackEvent, I check the membership provider for the username - conveniently stored in the Text property of our m_userName TextBox reference, thanks to our call to WebForm_InitCallback() - and if I find no matches, or more than one match, I return an empty string - which will cause our client script to clear out the contents of the textbox. If there is one match, I grab the username of that match and use that as my return value.

Conclusion

The end result - username checking without postbacks, without Web Services, and more.

Now, people can get ICallbackEventHandler crazy. I'm certainly not advocating that, just as I'm not advocating people abandon ASP.NET AJAX. You can call Web Services - real, live web services, complete with generated JavaScript proxy classes - from script in a cross-browser way. Think about the power there. But sometimes, that's a lot more than you need, and sometimes, you want something small, light, and quick to implement, that you can wrap up nice and tidy in a control library. When your needs are limited, ICallbackEventHandler can do the job for you. Heck, it's used all over the place in SharePoint.

And if its good enough for the SharePoint product team, or its good enough for Dino Esposito, it's good enough for me. ;-)

Man, that list is starting to get long...

PeoplePicker.zip