So I've been playing around a bit with a little JavaScript plug-in library extension set for ASP.Net Ajax that Matt Berseth came up with that he's dubbed 'majax'When last we left off, I was trying to make the check box group extension of his library to work with ASP.Net Checkboxes as well as regular HTML Checkboxes.  All in all, I think it was a fairly successful effort.

As with all good things, necessity is the mother of invention.  Our core application at the day job has a GridView that displays some records.  Now these records have a set of options associated with them, but the problem is, only one record can have one of the options available to them at a time.  So, if record A, has option 1 chosen, then records B, C...n can not have option 1 selected.

The developer who wrote this particular page (a few years ago), satisfied the rules checking by posting the page back with every... single... checkbox onclick event in the gridview.  The page would work itself through all of the rows in the grid, find the check box that was just checked, and uncheck all of the other checkboxes in that column.  User experience goes right out the window with that one.

So I decided to see if I could throw majax at the problem and see if anything suck ...

Download Sample App | Live Demo

Let's get this party started!

I won't get into the mark up of the ASPX page to deeply in the post, but what I did was drop a GridView on the page and setup a series of TemplateFields in the columns markup.  Then in the ItemTemplate for the columns, I dropped in some checkboxes.  As with my previous post and Matt's Demo, I added the needed ScriptReference's to the ScriptManager on the page:

<asp:ScriptManager ID="ScriptManager1" runat="server">
    <Scripts>
        <asp:ScriptReference Assembly="AjaxControlToolkit" Name="AjaxControlToolkit.ExtenderBase.BaseScripts.js" />
        <asp:ScriptReference Assembly="AjaxControlToolkit" Name="AjaxControlToolkit.Common.Common.js" />
        <asp:ScriptReference Assembly="AjaxControlToolkit" Name="AjaxControlToolkit.MutuallyExclusiveCheckBox.MutuallyExclusiveCheckBoxBehavior.js" />
        <asp:ScriptReference Path="~/_assets/js/majax.js" />
        <asp:ScriptReference Path="~/_assets/js/majax.mutexCheckBox.js" />
    </Scripts>
</asp:ScriptManager>

 

I created a VERY simple class to act as a dummy data holder, so that I could bind the gridview to a generic List<>.

   1: public class MutexCheckboxSampleData
   2: {
   3:     public string SomeText1 { get; set; }
   4:     public string SomeText2 { get; set; }
   5:     public bool Option1 { get; set; }
   6:     public bool Option2 { get; set; }
   7:     public bool Option3 { get; set; }
   8:     public bool Option4 { get; set; }
   9: }

 

The actual creating of the data and binding it to the gridview happens with a helper BindGridView() method and a static GetSampleData() method.  I like to cheat a little bit when it comes to creating random strings of data and make a quick call to Path.GetRandomFileName().

   1: private void BindGridView()
   2: {
   3:     gvMutexCheckBoxes.DataSource = GetSampleData();
   4:     gvMutexCheckBoxes.DataBind();
   5: }
   6:  
   7: private static List<MutexCheckboxSampleData> GetSampleData()
   8: {
   9:     List<MutexCheckboxSampleData> retValue = new List<MutexCheckboxSampleData>();
  10:     // Create our first one as a default with all options on
  11:     retValue.Add(new MutexCheckboxSampleData
  12:     {
  13:         SomeText1 = Path.GetRandomFileName(),
  14:         SomeText2 = Path.GetRandomFileName(),
  15:         Option1 = true,
  16:         Option2 = true,
  17:         Option3 = true,
  18:         Option4 = true
  19:     });
  20:     // and create some more records with out the options..
  21:     for (int i = 0; i < 5; i++)
  22:         retValue.Add(new MutexCheckboxSampleData { SomeText1 = Path.GetRandomFileName(), SomeText2 = Path.GetRandomFileName() });
  23:  
  24:     return retValue;
  25: }

 

Now, this is where things can get a little dicey and hard to follow.  Since we don't know at design time, how many checkboxes we're going to have in the mutually exclusive group, or what their id's will even be... we need to have a way to keep track of them.  What I did was to create a List<string> as a data holder for each option column that's going to be displayed in the grid:

   1: private List<string> _mutexOpt1 = new List<string>();
   2: private List<string> _mutexOpt2 = new List<string>();
   3: private List<string> _mutexOpt3 = new List<string>();
   4: private List<string> _mutexOpt4 = new List<string>();

 

Then, in the RowDataBound() event of the gridview, when I find and set the checked property of the checkbox, I also save that checkbox's UniqueID property to that option group's List<string>:

   1: CheckBox chk1 = row.FindControl("chkOption1") as CheckBox;
   2: if (chk1 != null)
   3: {
   4:     chk1.Checked = data.Option1;
   5:     _mutexOpt1.Add(chk1.UniqueID);
   6: }
   7:  
   8: CheckBox chk2 = row.FindControl("chkOption2") as CheckBox;
   9: if (chk2 != null)
  10: {
  11:     chk2.Checked = data.Option2;
  12:     _mutexOpt2.Add(chk2.UniqueID);
  13: }

 

The thing that brings the whole shooting match together is the DataBound event of the gridview.  Since that fires once binding has finished, I know at that point I have all of my lists fully built.  From that point, it's a simple matter of building some javascript on the fly and registering it as a client script block through ASP.Net Ajax.  You'll notice that I'm not just building any javascript, I'm actually building the pageLoad() method which is called natively from ASP.Net Ajax.  If I didn't want to write out pageLoad (say I was using that as a method elsewhere), then it would be a matter of one more line of code to call the ScriptManager.RegisterStartupScript().

   1: protected void gvMutexCheckBoxes_DataBound(object sender, EventArgs e)
   2: {
   3:     if (!(_mutexOpt1.Count > 1)) return;
   4:  
   5:     StringWriter sw = new StringWriter();
   6:  
   7:     sw.WriteLine("function pageLoad(sender, args){");
   8:     
   9:     string[] opts1 = _mutexOpt1.ToArray();
  10:     for (int x = 0; x < opts1.Length; x++)
  11:         opts1[x] = string.Concat("#", opts1[x]);
  12:     sw.WriteLine("$select('{0}').each(function(e){1});", string.Join(", ", opts1), @"{ e.mutexCheckBox('GroupOption1'); }");
  13:  
  14:     string[] opts2 = _mutexOpt2.ToArray();
  15:     for (int i = 0; i < opts2.Length; i++)
  16:         opts2[i] = string.Concat("#", opts2[i]);
  17:     sw.WriteLine("$select('{0}').each(function(e){1});", string.Join(", ", opts2), @"{ e.mutexCheckBox('GroupOption2'); }");
  18:  
  19:     string[] opts3 = _mutexOpt3.ToArray();
  20:     for (int i = 0; i < opts3.Length; i++)
  21:         opts3[i] = string.Concat("#", opts3[i]);
  22:     sw.WriteLine("$select('{0}').each(function(e){1});", string.Join(", ", opts3), @"{ e.mutexCheckBox('GroupOption3'); }");
  23:  
  24:     string[] opts4 = _mutexOpt4.ToArray();
  25:     for (int i = 0; i < opts4.Length; i++)
  26:         opts4[i] = string.Concat("#", opts4[i]);
  27:     sw.WriteLine("$select('{0}').each(function(e){1});", string.Join(", ", opts4), @"{ e.mutexCheckBox('GroupOption4'); }");
  28:     
  29:     sw.WriteLine("}");
  30:  
  31:     ScriptManager.RegisterClientScriptBlock(this, GetType(), "MutexCheckSamplePageLoad", sw.ToString(), true);
  32: }

 

Finally, the dynamic javascript gets written out to the browser.. 4 option groups ($select) and 5 rows of data...

   1: <script type="text/javascript">
   1: function pageLoad(sender, args){
   2: $select('#gvMutexCheckBoxes$ctl02$chkOption1, #gvMutexCheckBoxes$ctl03$chkOption1, #gvMutexCheckBoxes$ctl04$chkOption1, #gvMutexCheckBoxes$ctl05$chkOption1, #gvMutexCheckBoxes$ctl06$chkOption1, #gvMutexCheckBoxes$ctl07$chkOption1').each(function(e){ e.mutexCheckBox('GroupOption1'); });
   3: $select('#gvMutexCheckBoxes$ctl02$chkOption2, #gvMutexCheckBoxes$ctl03$chkOption2, #gvMutexCheckBoxes$ctl04$chkOption2, #gvMutexCheckBoxes$ctl05$chkOption2, #gvMutexCheckBoxes$ctl06$chkOption2, #gvMutexCheckBoxes$ctl07$chkOption2').each(function(e){ e.mutexCheckBox('GroupOption2'); });
   4: $select('#gvMutexCheckBoxes$ctl02$chkOption3, #gvMutexCheckBoxes$ctl03$chkOption3, #gvMutexCheckBoxes$ctl04$chkOption3, #gvMutexCheckBoxes$ctl05$chkOption3, #gvMutexCheckBoxes$ctl06$chkOption3, #gvMutexCheckBoxes$ctl07$chkOption3').each(function(e){ e.mutexCheckBox('GroupOption3'); });
   5: $select('#gvMutexCheckBoxes$ctl02$chkOption4, #gvMutexCheckBoxes$ctl03$chkOption4, #gvMutexCheckBoxes$ctl04$chkOption4, #gvMutexCheckBoxes$ctl05$chkOption4, #gvMutexCheckBoxes$ctl06$chkOption4, #gvMutexCheckBoxes$ctl07$chkOption4').each(function(e){ e.mutexCheckBox('GroupOption4'); });
   6: }
</script>

 

And then the rains came...

This whole setup is very slick and works a treat.  I was all excited about putting together the sample project and demo application and blog post.  Then I uploaded the demo to my site..  Wanting to be impressed with myself a bit longer, I browsed the demo to see what I had done in all it's glory.

I use FireFox as my primary browser when surfing the web.  It's not that I'm an IE bigot.. but more to the point, I keep javascript debugging turned on in IE so that I can debug it with VS2008... and SOOOO many sites out there throw js errors... rather than put up with a bazillion "do you wish to debug this" messages, I keep FireFox my default browser for windows, and IE my default browser for visual studio.

So, you can imagine my chagrin when this is what greeted me on the demo site:

captured_Image.png

When in fact, I had been expecting to see this:

captured_Image.png[5]

So we've got a cross-browser compatibility issue... anyone care to take a stab at it?