Multiple enabled and disabled checkbox states for the ASP.NET AJAX Data Grid

Damyan Petev / Friday, March 7, 2014

ASP.NET AJAX Data Grid with enabled and disabled checkbox statesIs it checked or not quite? Multi-state checkboxes are in the menu today. Which, of course, begs the question “Doesn’t the WebDataGrid already support checkbox columns out-of-the box?”. Why yes, it does support a tri-state CheckBox Field, but there are cases where you might need a little bit more flexibility. As we have been reminded on our Product Ideas feedback platform, having more checkbox states with enabled and disabled variations is something the users themselves can benefit directly. This is partly because the Grid’s editing  behavior treats checkboxes fields in bulk (as in the entire column) and perhaps even because there’s no visual cue the checkboxes in the column are not editable. So if you want more than the standard tri-state checkboxes that can be setup for each row – read on. Based on the feedback we received, the team has prepared a solution and I added my own spin as well and I want to share those with you.

Setup

There are a few things assumed here – you want to show a data grid with checkbox column bound or unbound, but you also want to use the Editing behavior and to control if each row’s checkbox is editable based on a condition or your other data columns. This way from three states, in combination with enabled and disabled, we go to six and that’s something you might need to store in more than one column. Depending on your data and case that extra column can be used as a flag weather or not the checkbox should be active or the entire state. Implementation of something like this can be very different, so a bit of adapting might be required but I’ll try to point out the basics that are reusable and mark optional features in the demo code.

Basics

The idea behind adding a disabled state is quite clever – replace the checkbox image with respective inactive image and perform runtime check for those images to prevent editing. This way you get control over the visual indication, but also on the image details you can use for detection. So you need 3 images, along with a settings object for the client-side with paths and values for them and mapping between checkbox and their hidden state cells.

Client events to handle:

  • Initialize – go through checkboxes that have extra state and apply enabled/disabled look
  • Handle Editing’s CellValueChanging to prevent disabled editing. Also keep in mind the event is fired when using the API so if you want to still be able to change values programmatically client-side you might need to use the “_set_value_internal” method instead.
  • If you are using unbound checkboxes with a header box, you should also handle the HeaderCheckBoxClicked to set proper state and perhaps value.

You can use either bound fields from your  current data, add new ones to do that if not available, use unbound fields with the sole purpose of holding flags or even enable and disable checkboxes based on logic executed on the client.

External flag approach

This uses the standard checkbox functionality along with applying editable states based on a flag – that can either be another data column, predetermined by server logic and carried over as (un)bound field or completely based on client side logic. The implementation is really up to you, I will use another bool column as part of the data – and since I have two checkbox columns the last two fields carry disabled flags for them:

  1. <ig:WebDataGrid ID="WebDataGrid1" runat="server" Height="350px" Width="100%" AutoGenerateColumns="False" DataKeyFields="ID" StyleSetName="IG">
  2.     <Columns>
  3.         <ig:BoundDataField DataFieldName="ID" Key="ID">
  4.             <Header Text="ID" />
  5.         </ig:BoundDataField>
  6.         <ig:BoundDataField DataFieldName="CheckBoxState" Key="CheckBoxState">
  7.             <Header Text="CheckBox State" />
  8.         </ig:BoundDataField>                
  9.         <ig:BoundCheckBoxField DataFieldName="BoundCheckBox" Key="BoundCheckBox" DataType="System.Boolean">
  10.             <Header Text="Bound CheckBox" />
  11.         </ig:BoundCheckBoxField>
  12.         <ig:BoundCheckBoxField Key="DisableBoundCheckBox">
  13.             <Header Text="Check to disable corresponding BoundCheckBox" />
  14.         </ig:BoundCheckBoxField>                
  15.         <ig:UnboundCheckBoxField Key="UnboundCheckBox" HeaderCheckBoxMode="TriState">
  16.             <Header Text="UnboundCheckBox"/>
  17.         </ig:UnboundCheckBoxField>   
  18.         <ig:UnboundCheckBoxField Key="DisableUnboundCheckBox" HeaderCheckBoxMode="TriState">
  19.             <Header Text="Check to disable corresponding UnboundCheckBox" />
  20.         </ig:UnboundCheckBoxField>
  21.         <%-- Hidden columns to store the states for the CheckBoxFields --%>   
  22.         <ig:BoundDataField Key="BoundCheckBoxDisabled" Hidden="true" DataType="System.Boolean">
  23.             <Header Text="Bound CheckBox Disabled?" />
  24.         </ig:BoundDataField>
  25.         <ig:BoundDataField Key="UnboundCheckBoxDisabled" Hidden="true" DataType="System.Boolean">
  26.             <Header Text="Unbound CheckBox Disabled?" />
  27.         </ig:BoundDataField>                      
  28.     </Columns>
  29.     <ClientEvents Initialize="WebDataGrid_Grid_Initialize" HeaderCheckBoxClicked="WebDataGrid_Grid_HeaderCheckboxClicked" />
  30.     <Behaviors>
  31.         <ig:EditingCore>
  32.             <EditingClientEvents CellValueChanging="WebDataGrid_Editing_CellValueChanging" CellValueChanged="WebDataGrid_Editing_CellValueChanged" />
  33.             <Behaviors>
  34.                 <ig:CellEditing>
  35.                         <ColumnSettings>
  36.                             <ig:EditingColumnSetting ColumnKey="ID" ReadOnly="True" />
  37.                         </ColumnSettings>
  38.                         <EditModeActions MouseClick="Single" EnableOnKeyPress="true"/>
  39.                 </ig:CellEditing>
  40.             </Behaviors>
  41.         </ig:EditingCore>
  42.     </Behaviors>
  43. </ig:WebDataGrid>

Initial state based on the values of those is established and the disabled checkbox images replace the defaults where needed:

  1. var styleName = "<%= this.WebDataGrid1.StyleSetName %>" || "Default";
  2. var clientSettings = {
  3.     "true": { src: "images/" + styleName + "/ig_checkbox_disabled_on.gif", value: true, chkState: 1 },
  4.     "false": {src: "images/" + styleName + "/ig_checkbox_disabled_off.gif", value: false, chkState: 0 },
  5.     "null": { src: "images/" + styleName + "/ig_checkbox_disabled_partial.gif", value: null, chkState: 2 },
  6.     "columns": {2: 6, 4: 7}           
  7. };
  8.  
  9. function WebDataGrid_Grid_Initialize(sender, eventArgs) {
  10.     // Set initial enabled / disabled state for all checkboxes            
  11.     try {
  12.         var rows = sender.get_rows();
  13.         var rowCount = rows.get_length();
  14.         for (var i = 0; i < rowCount; i++) {
  15.             var row = rows.get_row(i);
  16.             for (columnIndex in clientSettings["columns"]){
  17.                 // Set the correct enabled/disabled states:
  18.                 var checkBoxCell = row.get_cell(columnIndex);
  19.                 var hiddenStateCell = row.get_cell(clientSettings["columns"][columnIndex]);
  20.                 var isDisabled = hiddenStateCell.get_value();
  21.                 SetCheckState(checkBoxCell, isDisabled, hiddenStateCell);
  22.             }
  23.         }
  24.     }
  25.     catch (ex) { }
  26. }
  27.  
  28. function SetCheckState(cell, isDisabled, checkStateCell) {
  29.     // Set Enabled or disabled state to a cell (adjust image and title/alt) and store in secondary cell.
  30.     try {
  31.         if (isDisabled === isCellDisabled(cell)) return;
  32.         var checkBoxElem = cell.get_element().getElementsByTagName("img").item(0); //get checkbox image
  33.         var chkStates = clientSettings[String(cell.get_value())]; //get state info
  34.         if (!chkStates) return;
  35.                 
  36.         if (isDisabled === true) {
  37.             // Set the disabled values of src and title for the checkbox
  38.             checkBoxElem.src = chkStates.src;
  39.             checkBoxElem.title = "Disabled " + checkBoxElem.title;
  40.             checkBoxElem.alt = "Disabled " + checkBoxElem.alt;
  41.         }
  42.         else {
  43.             // re-set value to restore state
  44.             checkBoxElem.src = "";
  45.             //use _internal to force setting the same value
  46.             cell._set_value_internal(chkStates.value, chkStates.chkState);
  47.         }
  48.         checkStateCell.set_value(isDisabled); // Store the new checkbox state value in the corresponding hidden checkbox
  49.     }
  50.     catch (ex) { }
  51. }
  52.  
  53. function isCellDisabled(cell) {
  54.     var checkBoxElem = cell.get_element().getElementsByTagName("img").item(0);
  55.     return checkBoxElem && checkBoxElem.src.toLowerCase().indexOf("disabled") >= 0;
  56. }

Prevent edits on disabled checkboxes and for unbound fields preserve the state when the header box is used:

  1. function WebDataGrid_Editing_CellValueChanging(sender, eventArgs) {
  2.     // Prevent edit actions on disabled checkboxes
  3.     try {
  4.         var currCell = eventArgs.get_cell();
  5.         if (isCellDisabled(currCell))
  6.             eventArgs.set_cancel(true); // cancel event to prevent value change
  7.     }
  8.     catch (ex) { }
  9. }
  10.  
  11. function WebDataGrid_Grid_HeaderCheckboxClicked(sender, eventArgs) {
  12.     var columnIndex = eventArgs.get_column().get_index();
  13.     var rows = sender.get_rows();
  14.     var rowCount = rows.get_length();
  15.  
  16.     // When the header of the "UnboundCheckBox" column is clicked,
  17.     // set the check states for all while maintaining enabled/disabled states
  18.     if (clientSettings["columns"][columnIndex]) {
  19.         for (var i = 0; i < rowCount; i++) {
  20.             var row = rows.get_row(i);
  21.             var unboundCheckBoxCell = row.get_cell(columnIndex);
  22.             var hiddenUnboundCheckBoxCell = unboundCheckBoxCell.get_row().get_cell(clientSettings["columns"][columnIndex]);
  23.             var unboundCheckBoxCellVal = hiddenUnboundCheckBoxCell.get_value();
  24.             SetCheckState(unboundCheckBoxCell, unboundCheckBoxCellVal, hiddenUnboundCheckBoxCell);
  25.         }
  26.     }
  27. }

Of course, keeping state for unbound fields is just as optional as the header box itself – it’s a matter of what you might need. At this point you can use the SetCheckState function to toggle state and you can provide the user with various ways of doing that if needed (see demo code handling value changes of neighboring standard checkbox fields).

Enabled and disabled checkbox states based on 'flag' secondary column (last two) that would be hidden from the user.

P.S. If you want better readability for column pairs in the client settings you can store keys instead and use the “get_cellByColumnKey” method where I went for indexes - because you know both on the server, and the key method will use it to find the index anyways.

All-in-one approach

This is the one if you need to keep everything in a single data cell. The all-in-one approach goes for a single data column holding the full state on the checkbox as a string just like you see it on the screenshots – “EnabledOn”, “EnabledOff” and so on. So you are once again in need of two columns, one checkbox and one hidden that carries the actual state. Cue similar set of event handlers to provide client side looks and behavior and you have a solution just as functional.

The grid markup, with variations for required bound and unbound fields, is essentially the same and so is the ‘disabling’ of checkboxes by checking their image source and canceling editing and initialize event handling. The main difference comes from the fact that setting state deals with and saves the full string values:

  1. // Sets the enabled/disabled state on the bound/unbound checkbox in the given cell
  2. function SetCheckState(cell, chkState, checkStateCell) {
  3.     try {
  4.         var checkState = chkState? chkState.replace("Enabled", "Disabled"): "";
  5.         var checkBoxElem = cell.get_element().getElementsByTagName("img").item(0);
  6.         var chkStates = checkStates[checkState];
  7.         if (!chkStates) return;
  8.  
  9.         // Get the default values of src and title for checkbox in its enabled state
  10.         var enabledSrc = null, enabledTitle = null;
  11.         if (chkState && chkState.indexOf("Enabled") >= 0) {
  12.             enabledSrc = "ig_res/Default/" + chkStates.src.replace("disabled_", "");
  13.             enabledTitle = chkState.replace("Enabled", "").replace("On", "Checked").replace("Off", "Unchecked");
  14.         }
  15.  
  16.         cell.set_value(chkStates.value, chkStates.chkState);
  17.         checkBoxElem.src = enabledSrc? enabledSrc : chkStates.src;
  18.         checkBoxElem.title = enabledTitle ? enabledTitle : chkStates.title;
  19.         checkBoxElem.alt = enabledTitle ? enabledTitle : chkStates.alt;
  20.         checkStateCell.set_value(chkState); // Store the new checkbox state value in the corresponding hidden checkbox
  21.     }
  22.     catch (ex) { }
  23. }

Enabled and disabled checkbox states based on a hidden column holding string values and logic applying those to the checkboxes

Bonus round

Of course, when I mention that you need two columns for this approach again, I mean that it won’t always be effective to bind a checkbox column to string values like “DisabledOn”. That doesn’t mean you can’t do it, though. Using the IBooleanConverter interface you can bind checkbox field (this naturally excludes unbound options) and it actually eases up the client code required as well because you won’t need the extra hidden column.

  1. public class CheckboxConverter : IBooleanConverter
  2. {
  3.  
  4.     public object DefaultFalseValue
  5.     {
  6.         get { return "EnabledOff"; }
  7.     }
  8.  
  9.     public object DefaultTrueValue
  10.     {
  11.         get { return "EnabledOn"; }
  12.     }
  13.  
  14.     public bool IsFalse(object value)
  15.     {
  16.         return value.ToString().Contains("Off");
  17.     }
  18.  
  19.     public bool IsTrue(object value)
  20.     {
  21.         return value.ToString().Contains("On");
  22.     }
  23. }

A bonus of the converter handling is that when editable checkboxes will set the default true/false values when the user interacts with them and use the rules to match checked or unchecked and when nothing matches ( read: “EnabledPartial”) it will actually set the checkbox to partial state as well. How convenient!

Options, options, options

I guess that is exactly what this blog can give you – 3 ways to achieve the Multiple enabled and disabled state checkbox functionality.  Whether you want two Boolean columns that give a combined state or a single column approach with either hidden and presentation column pair or binding fields through a converter – all versions produce similar behavior with different amount of effort and limitations. It’s up to you to decide which one is a better fit for your requirements and reuse as much of the demo code as possible!

Speaking of code, here are the demo ASP.NET sites:

Open as Web Site from Visual Studio or WebMatrix and you will need Infragistics ASP.NET installed (might need reference version adjustments) which you can grab from here:

Infragistics for ASP.NET banner

I’d love to hear your thoughts, so leave a comment down below or ping me @DamyanPetev.

And as always, you can follow us on Twitter @Infragistics and stay in touch on Facebook, Google+ and LinkedIn!