Chad Scharf's Weblog
It's always time to upgrade!

3-State CheckBox using Microsoft AJAX

Thursday, 30 October 2008 05:53 by Chad

Preview

Blank (or unset): blank, Checked (or granted): checked, and unchecked (or denied): unchecked.

I recently had some very difficult requirements to fulfill on an access control and configuration application with a web front end. Those of you who have designed or implemented access control interfaces know this is a daunting task, especially when the access control needs to be fairly flexible. Although it’s easy to implement a user interface that directly represents your data model, your users may not always be able to easily use or understand how to accomplish tasks using it. Usability is important to consider when tackling complex concepts or data structures through web UI.

The requirements for our security model were for granting or denying access to specific data elements in the database. By default a user has access to everything. If you explicitly grant access to an element, the user then only has access to those elements you’ve explicitly granted access to. You can also deny access to a given element; however a single element can only have one preference state, un-specified, grant access, or deny access. To top it all off, all the elements are in a recursive tree structure (always makes things more fun).

I had immediately made the decision that whatever we did, it needed to be in a tree fashion to show the end user the structure of what they were granting access to, however the question remained as to how to represent each state in an intuitive and easy to understand way. Enter the 3-State check box.

Checkboxes only have 2 states

This is a very real statement, and there is no way around it. Although you can typically represent a 3rd pseudo state by setting the checkbox’s disabled property, this may not always be intuitive to the end user as to what the intention of this state really is. To solve this I first needed to conceptualize what this 3rd state looks like, and bingo! Here are the 3 states, which I created in Adobe Photoshop from a screen shot of a normal checkbox; Blank (or unset): blank, Checked (or granted): checked, and unchecked (or denied): unchecked.

Why AJAX?

Given that I’ve decided to create a new server control to handle my 3-state check-box, I need to make some decisions about its behavior. The Microsoft AJAX library in .NET 3.5 offers some powerful APIs for implementing your own behaviors, wiring events and using well-fashioned OO JavaScript among other features. Since on my screen in particular I wasn’t sure if I wanted to use a WCF service on the checkbox’s changed event to update the database, or if I wanted a full or partial post-back to completely rebind the tree after a click, the control needed to be as flexable as possible and easily scripted both on the client and server. Because I needed to manage both client and server side behavior in a consistent manner, I decided to go the MS AJAX route.

The good stuff

The first step in creating my server control was to create a class library project to put my server control in. I’m using the .NET 3.5 framework and used the ASP.NET AJAX server control project template to create my new project. After creating a directory for the images in their own folder, I set their build action in the properties to "Embedded Resource". I also had to remove the [WebResource] and [ScriptResource] assembly attributes from the AssemblyInfo.cs file (I like putting these on their relevant pages). Next I need to create my server control. The first thing I do is allocate my embedded resources as web resources at the top of the code page under the using statements and before my namespace declaration. I do this simply as a preference to keep embedded resources together with the specific code page that is using them. If they're more universal to your project you may want to put this back in the AssemblyInfo.cs file.

[assembly: System.Web.UI.WebResource("ChadScharf.ThreeStateCheckBox.images.blank.gif", "image/gif")]
[assembly: System.Web.UI.WebResource("ChadScharf.ThreeStateCheckBox.images.checked.gif", "image/gif")]
[assembly: System.Web.UI.WebResource("ChadScharf.ThreeStateCheckBox.images.unchecked.gif", "image/gif")]
[assembly: System.Web.UI.WebResource("ChadScharf.ThreeStateCheckBox.ThreeStateCheckBox.js", "text/javascript", PerformSubstitution = true)]

Server Control, ThreeStateCheckBox

The ThreeStateCheckBox server control inherits System.Web.UI.WebControls.CompositeControl (since I’ll be using child controls and need a wrapper tag, this was the easiest way to get started). I’m also going to explicitly implement the System.Web.UI.INamingContainer and System.Web.UI.IScriptControl (since we’re using AJAX behaviors).

[PersistChildren(false)]
[ParseChildren(true)]
[ToolboxData(@"<{0}:ThreeStateCheckBox runat=""server""> </{0}:ThreeStateCheckBox>")]

public class ThreeStateCheckBox : CompositeControl, INamingContainer, IScriptControl

I need 2 child controls to manage the checkbox, an ImageButton control and a HiddenField control. The ImageButton will be the actual checkbox. The HiddenField control will represent our client-side state of the checkbox control, therefore our script can modify its value and track it across post-backs if several changes to several of our controls are made at once.

protected ImageButton checkBox;
protected HiddenField hidden;

Next, I need a private ScriptManager property with a getter. Since this is an AJAX dependant control, it requires that a ScriptManager control be on the page it’s on, therefore we use something like this:

private ScriptManager ScriptManager
{
  get
  {
    ScriptManager manager = ScriptManager.GetCurrent(this.Page);
    if (manager == null)
      throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.InvariantCulture, "The control with ID '{0}' requires a ScriptManager on the page. The ScriptManager must appear before any controls that need it.", new object[] { this.ID }));
    return manager;
  }
}

Now, I needed to decide what properties would be relevant for working with the control. CompositeControl already exposes some properties relevant to style and CssClass, however we need more meaty, behavioral properties. We’ll start with the reasoning that we may not want to post-back when the checkbox is clicked, so let’s add a Boolean AutoPostBack property, which we’ll store in ViewState. Next let’s expose the ImageButton’s CausesValidation property (common among other controls that AutoPostBack such as a DropDownList or CheckBox). Since it could cause validation, we need to expose the ImageButton’s ValidationGroup property. Next to last we need to convey the state of the checkbox, so we’ll expose an enumeration value representing the Checked state, called Checked. This could be a number 0 through 2 or any other relevant property if you wish to change it. We’ll store the Checked state as a string in the HiddenField’s Value property. Finally, I need to be able to assign a data key value to my checkbox so if there is a key, user id, or other relevant data related to this checkbox I can easily pass it around on the client side as well as the server side if we put it in a more complex container control such as a TreeView, etc. The DataKey value will be stored in our ImageButton’s CommandArgument value so we can easily retrieve it later.

Event Handling

In my protected override void CreateChildControls() I’m going to wire up the ImageButton’s Command event and handle that in my server side code. That in turn will check the current Checked state, and change it appropriately, then call OnCheckChanged, which will call the public event CheckChanged, triggering any server side delegates from there.

In the PreRender stage of the page’s lifecycle I’m going to set the ImageButton’s ImageUrl property based on the state of the checkbox pulling from the embedded resource images that we set up earlier. Then, the more important stage of this is to use our IScriptControl interface implementation to register the control with the ScriptManager and register the ScriptDescriptors (we’ll get to that shortly, but we need a call to it anyway).

base.OnPreRender(e);
switch (Checked)
{
  case CheckedState.Blank:
    checkBox.ImageUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(), "ChadScharf.ThreeStateCheckBox.images.blank.gif");
    break;
  case CheckedState.Checked:
    checkBox.ImageUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(), "ChadScharf.ThreeStateCheckBox.images.checked.gif");
    break;
  case CheckedState.Unchecked:
    checkBox.ImageUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(), "ChadScharf.ThreeStateCheckBox.images.unchecked.gif");
    break;
}
this.ScriptManager.RegisterScriptControl<ThreeStateCheckBox>(this);
this.ScriptManager.RegisterScriptDescriptors(this);

IScriptControl Interface Implementation

There are 2 methods in the IScriptControl interface, IEnumerable<ScriptReference> GetScriptReferences(), and IEnumerable<ScriptDescriptor> GetScriptDescriptors(). GetScriptReferences simply returns an enumerable list of script references, in our case we only need one:

yield return new ScriptReference("ChadScharf.ThreeStateCheckBox.ThreeStateCheckBox.js", "ChadScharf.ThreeStateCheckBox");

And GetScriptDescriptors returns a enumerable list of ScriptDescriptor, in our case a ScriptControlDescriptor that we’re going to use to initialize our new type client-side in the AJAX framework, setting specific properties to the client instance so it matches our server-side instance.

if (this.Page != null && this.Visible)
{
  ScriptControlDescriptor desc = new ScriptControlDescriptor("ChadScharf._ThreeStateCheckBox", this.ClientID);
  desc.AddProperty("checked", this.Checked.ToString());
  desc.AddProperty("imgBlank", this.Page.ClientScript.GetWebResourceUrl(this.GetType(), "ChadScharf.ThreeStateCheckBox.images.blank.gif"));
  desc.AddProperty("imgChecked", this.Page.ClientScript.GetWebResourceUrl(this.GetType(), "ChadScharf.ThreeStateCheckBox.images.checked.gif"));
  desc.AddProperty("imgUnchecked", this.Page.ClientScript.GetWebResourceUrl(this.GetType(), "ChadScharf.ThreeStateCheckBox.images.unchecked.gif"));
  desc.AddProperty("buttonId", this.checkBox.ClientID);
  desc.AddProperty("autoPostBack", this.AutoPostBack);
  desc.AddProperty("hiddenFieldId", this.hidden.ClientID);
  desc.AddProperty("dataKey", this.DataKey);
  yield return desc;
}

Client Side Script

Now it’s time to create our client behavior, YAY! JavaScript is every web developers favorite technology to work with (GAG!), so I’ll try to keep this part as simplistic as possible and if you don’t want to learn the "why" and "how" you can simply lift the code and be happy ;-). No one will blame you.

As you can see earlier I referenced my script file as an embedded resource using the WebResource directive, passing PerformSubstitution = true. This is not imperitive, however I like the Substitution ability with embedded web resources so you can reference other embedded resources without having to write code to do so (e.g. in embedded CSS files referencing embedded images, etc). I use this to set the 3 image URLs appropriately in the JS file.

The first thing I do in my behavior script is reference the MicrosoftAjax.js file so I can have intellisense (THANK YOU VS 2008!) and register the client-side namespace for my behavior:

/// <reference name="MicrosoftAjax.js"/>
Type.registerNamespace('ChadScharf');

I won’t go into too many details on specifying the client side behavior code however I’ll try to give a good overview.

The next thing I want to do is declare my constructor for my behavior "type". In the constructor function I’m going to specify defaults for all my private properties:

ChadScharf._ThreeStateCheckBox = function ChadScharf$_ThreeStateCheckBox(element) {
  this._checked = 'Blank';
  this._buttonId = null;
  this._hiddenFieldId = null;
  this._autoPostBack = false;
  this._clickHandler = null;
  this._dataKey = null;
  this._imgBlank = '<%=WebResource("ChadScharf.ThreeStateCheckBox.images.blank.gif")%>';
  this._imgChecked = '<%=WebResource("ChadScharf.ThreeStateCheckBox.images.blank.gif")%>';
  this._imgUnchecked = '<%=WebResource("ChadScharf.ThreeStateCheckBox.images.blank.gif")%>';
  // Call base initializer
  ChadScharf._ThreeStateCheckBox.initializeBase(this, [element]);
}

If you’ll notice, the type name, ChadScharf._ThreeStateCheckBox is the same as we have in our GetScriptDescriptors() method on the server side, and you’ll also notice some commonalities in the properties we’re setting there using the AddProperty method. This is a neat feature of the AJAX framework for writing custom behaviors and makes your client code work more like your server code with far fewer headaches. The most important part here is to call our base initialization method (OO JavaScript and Inheritance rocks!).

Next are all the property accessors for the client control behavior. In the MS AJAX model, we use the syntax get_propertyName() {}, and set_propertyName(value) {}, where propertyName is the actual name of the property. The framework automatically implies these are get and set property accessors (pretty neat!).

function ChadScharf$_ThreeStateCheckBox$get_checked() {
  if (arguments.length !== 0) { throw Error.parameterCount(); }
  return this._checked;
}
function ChadScharf$_ThreeStateCheckBox$set_checked(value) {
  this._checked = value;
  if ($get(this._buttonId)) { $get(this._buttonId).setAttribute('checked', value); }
  if ((this._hiddenFieldId) && $get(this._hiddenFieldId)) { $get(this._hiddenFieldId).value = value; }
}
function ChadScharf$_ThreeStateCheckBox$get_imgBlank() {
  if (arguments.length !== 0) { throw Error.parameterCount(); }
  return this._imgBlank;
}
function ChadScharf$_ThreeStateCheckBox$set_imgBlank(value) {
  this._imgBlank = value;
}
function ChadScharf$_ThreeStateCheckBox$get_imgChecked() {
  if (arguments.length !== 0) { throw Error.parameterCount(); }
  return this._imgChecked;
}
function ChadScharf$_ThreeStateCheckBox$set_imgChecked(value) {
  this._imgChecked = value;
}
function ChadScharf$_ThreeStateCheckBox$get_imgUnchecked() {
  if (arguments.length !== 0) { throw Error.parameterCount(); }
  return this._imgUnchecked;
}
function ChadScharf$_ThreeStateCheckBox$set_imgUnchecked(value) {
  this._imgUnchecked = value;
}
function ChadScharf$_ThreeStateCheckBox$get_buttonId() {
  if (arguments.length !== 0) { throw Error.parameterCount(); }
  return this._buttonId;
}
function ChadScharf$_ThreeStateCheckBox$set_buttonId(value) {
  this._buttonId = value;
}
function ChadScharf$_ThreeStateCheckBox$get_autoPostBack() {
  if (arguments.length !== 0) { throw Error.parameterCount(); }
  return this._autoPostBack;
}
function ChadScharf$_ThreeStateCheckBox$set_autoPostBack(value) {
  this._autoPostBack = value;
}
function ChadScharf$_ThreeStateCheckBox$get_hiddenFieldId() {
  if (arguments.length !== 0) { throw Error.parameterCount(); }
  return this._hiddenFieldId;
}
function ChadScharf$_ThreeStateCheckBox$set_hiddenFieldId(value) {
  this._hiddenFieldId = value;
}
function ChadScharf$_ThreeStateCheckBox$get_dataKey() {
  if (arguments.length !== 0) { throw Error.parameterCount(); }
  return this._dataKey;
}
function ChadScharf$_ThreeStateCheckBox$set_dataKey(value) {
  this._dataKey = value;
}

WOW!… that was a lot of getters and setters. Now we need to expose our checkChanged event so other components or client scripts can consume our events using event handlers. In this case the add_ and remove_ prefixes next to our method’s that take a single parameter (handler) act similarly to our getters and setters, they are simply for adding or removing an event handler to a specific event of your element, and since our base type exposes the get_events() method, which allows us to add or remove handlers to our Sys.UI.Control, this is all the code we need to accomplish this feat:

function ChadScharf$_ThreeStateCheckBox$add_checkChanged(handler) {
  this.get_events().addHandler('checkChanged', handler);
}
function ChadScharf$_ThreeStateCheckBox$remove_checkChanged(handler) {
  this.get_events().removeHandler('checkChanged', handler);
}

Next we need our own event handler methods for when our image button is clicked and when our checked state is changed:

function ChadScharf$_ThreeStateCheckBox$_onButtonClick(eventElement) {
  switch (this._checked) {
    case 'Blank':
      this._checked = 'Checked';
      eventElement.target.src = this._imgChecked;
      break;
    case 'Checked':
      this._checked = 'Unchecked';
      eventElement.target.src = this._imgUnchecked;
      break;
    case 'Unchecked':
      this._checked = 'Blank';
      eventElement.target.src = this._imgBlank;
      break;
  }
  eventElement.target.setAttribute('checked', this._checked);
  $get(this._hiddenFieldId).value = this._checked;
  if (!this._autoPostBack) {
    eventElement.preventDefault();
    this._onCheckChanged();
  }
}
function ChadScharf$_ThreeStateCheckBox$_onCheckChanged() {
  var handler = this.get_events().getHandler('checkChanged');
  if (handler) {handler(this, Sys.EventArgs.Empty)};
}

We also need our most important initialize and dispose methods to clean up our event handlers and delegates to prevent memory leaks in IE:

function ChadScharf$_ThreeStateCheckBox$dispose() {
  // IDisposable
  if (this._clickHandler && $get(this._buttonId)) {
    $removeHandler($get(this._buttonId), 'click', this._clickHandler);
    this._clickHandler = null;
  }
  ChadScharf._ThreeStateCheckBox.callBaseMethod(this, 'dispose');
}
function ChadScharf$_ThreeStateCheckBox$initialize() {
  // Initializer
  ChadScharf._ThreeStateCheckBox.callBaseMethod(this, 'initialize');
  if (!this._autoPostBack) {
    this._clickHandler = Function.createDelegate(this, this._onButtonClick)
    $addHandler($get(this._buttonId), 'click', this._clickHandler);
  }
}

Implement the behavior

Finally in order to register our control behavior and make it useable we need to create the prototype for it and then register it, specifying its inheritance). The prototype will stage the public properties and methods from our type definition above and package it in a nice neat container that we can register with the ScriptManager and the AJAX framework for use in our code elsewhere.

ChadScharf._ThreeStateCheckBox.prototype = {
  get_checked: ChadScharf$_ThreeStateCheckBox$get_checked,
  set_checked: ChadScharf$_ThreeStateCheckBox$set_checked,
  get_imgBlank: ChadScharf$_ThreeStateCheckBox$get_imgBlank,
  set_imgBlank: ChadScharf$_ThreeStateCheckBox$set_imgBlank,
  get_imgChecked: ChadScharf$_ThreeStateCheckBox$get_imgChecked,
  set_imgChecked: ChadScharf$_ThreeStateCheckBox$set_imgChecked,
  get_imgUnchecked: ChadScharf$_ThreeStateCheckBox$get_imgUnchecked,
  set_imgUnchecked: ChadScharf$_ThreeStateCheckBox$set_imgUnchecked,
  get_buttonId: ChadScharf$_ThreeStateCheckBox$get_buttonId,
  set_buttonId: ChadScharf$_ThreeStateCheckBox$set_buttonId,
  get_autoPostBack: ChadScharf$_ThreeStateCheckBox$get_autoPostBack,
  set_autoPostBack: ChadScharf$_ThreeStateCheckBox$set_autoPostBack,
  get_hiddenFieldId: ChadScharf$_ThreeStateCheckBox$get_hiddenFieldId,
  set_hiddenFieldId: ChadScharf$_ThreeStateCheckBox$set_hiddenFieldId,
  get_dataKey: ChadScharf$_ThreeStateCheckBox$get_dataKey,
  set_dataKey: ChadScharf$_ThreeStateCheckBox$set_dataKey,
  add_checkChanged: ChadScharf$_ThreeStateCheckBox$add_checkChanged,
  remove_checkChanged: ChadScharf$_ThreeStateCheckBox$remove_checkChanged,
  _onButtonClick: ChadScharf$_ThreeStateCheckBox$_onButtonClick,
  _onCheckChanged: ChadScharf$_ThreeStateCheckBox$_onCheckChanged,
  dispose: ChadScharf$_ThreeStateCheckBox$dispose,
  initialize: ChadScharf$_ThreeStateCheckBox$initialize
}
ChadScharf._ThreeStateCheckBox.registerClass('ChadScharf._ThreeStateCheckBox', Sys.UI.Control);

Finally

Finally! Now that I’ve probably copied and pasted WAY too much code in my blog post we get ready to do the final implementation of our three state checkbox control, consume it in a web page.

We’ll start by creating a web site in VS 2008, and adding a project reference to our class library. Then, in the Web.config file we’ll add a simple entry so we can use our new control without any other code in any of the pages:

<pages>
  <controls>
    …
    <add tagPrefix="chad" namespace="ChadScharf.ThreeStateCheckBox" assembly="ChadScharf.ThreeStateCheckBox"/>
  </controls>
</pages>

Now with that out of the way, we can test the new control by putting it on a page:

<chad:ThreeStateCheckBox ID="MyCheckBox" runat="server" AutoPostBack="false" CausesValidation="false" />

Of course, I could wire up an event to it as well, such as the CheckChanged event. This event will only be called if AutoPostBack is set to true.

OnCheckChanged="MyCheckBox_CheckChanged"

protected void MyCheckBox_CheckChanged(object sender, EventArgs e)
{
  // ...
}

You can control whether your control posts back or not by setting AutoPostBack="true" or "false". If you set it to "false" (the default), your page will not post-back when you click the checkbox, but you should see the state change instantly, and if another control, like a button, causes a post-back, you will also notice the server side state of the control has changed as well!

That’s it

This was simply a high level overview; however you can always download the entire source code here. Feel free to modify the images, scripts etc or just let me know what you think. This is definately a quick-fix for a not-so-simple business requirements problem using creative design. The 3-stage checkbox is a bit more common in windows forms applications and I've seen a lot of requests for this type of control on web development forums, typically asking how you would do a tree showing not all the options under a parent node are checked/selected. This could be done by simply altering the 3rd state of the checkbox and image to reflect whatever you wanted it to.

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Related posts

Add comment


(Will show your Gravatar icon)  

[b][/b] - [i][/i] - [u][/u]- [quote][/quote]



Live preview

January 7. 2009 06:28