Matt Sigman

Hands-On Software Leader

Matt Sigman - Hands-On Software Leader

Telerik RadComboTree – A ComboBox with collapsible items

Background

One of the most useful existing controls in the Telerik RadControls suite is the RadComboBox.  It offers multi-column support to template support, anything you need in a drop-down can be rendered in RadComboBox. The control provides a rich set of features which include: client-side API with events, load-on-demand, auto-complete, filtering, multi-column support, highly customizable appearance through skins, content template support and much more. All this flexibility comes packaged in a control that renders lightweight, semantic HTML for optimum page performance and SEO.

The one option it doesn’t have out of the box, is the ability to group nodes into a hierarchical relationship and collapse/expand them, similar to the RadTreeView control.

Introducing the RadComboTree

This control basically combines the ComboBox and the TreeView to have a drop-down hierarchical list of nodes that can optionally have checkboxes.

radcombotree

It works exactly the same as RadComboBox: when user selects items, it will display them in the textbox area.  If several items are checked, it will instead summarize them as “5 items checked.”  Side-by-side with a plain ComboBox, they look and behave identically.

Source Code

The source code is available here.

It is packaged as a discrete custom server-side control and appears in the Toolbox similar to any other control.  It works by starting with a basic RadComboBox, then creating a custom Item Template that contains a RadTreeView control in it.  Then, it overrides various properties and methods to allow the two controls to work seamlessly together.  It also does a few other things, such as handling clicks, displaying checked items, etc.  Finally, it handles JavaScript registration on its own and only registers the scripts once per page, regardless of how many are actually on the page.


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using Telerik.Web.UI;

namespace Msigman.Web.Controls
{
 [DefaultProperty("Text")]
 [ToolboxData("<{0}:RadComboTree runat=server></{0}:RadComboTree>")]
 public class RadComboTree : RadComboBox
 {
 #region Properties
 /// <summary>
 /// Gets or sets a direct reference to the Tree View.
 /// </summary>
 private RadTreeView TreeView
 {
 get
 {
 return this.Items[0].Controls[0].Controls[0] as RadTreeView;
 }
 }

/// <summary>
 /// Gets or sets the data source for the control.
 /// </summary>
 public override object DataSource
 {
 get
 {
 return TreeView.DataSource;
 }
 set
 {
 TreeView.DataSource = value;
 }
 }

/// <summary>
 /// Databind the control to the datasource previously assigned.
 /// </summary>
 public override void DataBind()
 {
 TreeView.DataBind();
 MarkParentsAsBold();
 }

/// <summary>
 /// Clear all checked nodes.
 /// </summary>
 public override void ClearCheckedItems()
 {
 TreeView.UncheckAllNodes();
 this.Text = string.Empty;
 this.SelectedItem.Text = string.Empty;
 }

/// <summary>
 /// Gets or sets the checked items.
 /// </summary>
 public override IList<RadComboBoxItem> CheckedItems
 {
 get
 {
 return TreeView.CheckedNodes.Where(n => n.Nodes.Count == 0 && n.Text != "(Select All)").Select(n => new RadComboBoxItem(n.Text, n.Value)).ToList();
 }
 }
 #endregion
 /// <summary>
 /// Mark checked items.
 /// </summary>
 /// <param name="selectedItems"></param>
 /// <returns></returns>
 public void MarkCheckedItems(List<string> selectedItems)
 {
 if (selectedItems.Count == 0) return;

var text = string.Empty;
 foreach (var selectedItem in selectedItems)
 {
 var comboItem = TreeView.FindNodeByValue(selectedItem);

if (comboItem != null && comboItem.Nodes.Count == 0)
 {
 comboItem.Checked = true;
 text += comboItem.Text + ", ";
 }
 }
 if (text.Length > 0)
 {
 if (text.Length < 30)
 this.SelectedItem.Text = text.Substring(0, text.Length - 2);
 else
 this.SelectedItem.Text = selectedItems.Count + " items checked";
 }
 }

/// <summary>
 /// Mark checked items.
 /// </summary>
 /// <param name="selectedItems"></param>
 /// <param name="keySuffix"></param>
 /// <returns></returns>
 public void MarkCheckedItems(List<int> selectedItems, string keySuffix = null)
 {
 if (selectedItems.Count == 0) return;

var text = string.Empty;
 foreach (var selectedItem in selectedItems)
 {
 var key = string.IsNullOrEmpty(keySuffix) ? selectedItem.ToString(CultureInfo.InvariantCulture) : selectedItem + "-" + keySuffix;
 var comboItem = TreeView.FindNodeByValue(key);

if (comboItem != null && comboItem.Nodes.Count == 0)
 {
 comboItem.Checked = true;
 text += comboItem.Text + ", ";
 }
 }
 if (text.Length > 0)
 {
 if (text.Length < 30)
 this.SelectedItem.Text = text.Substring(0, text.Length - 2);
 else
 this.SelectedItem.Text = selectedItems.Count + " items checked";
 }
 }

/// <summary>
 /// Get all selected nodes from the TreeView;
 /// </summary>
 /// <returns></returns>
 public string GetCheckedNodes()
 {
 var strBuilder = new StringBuilder();
 var nodes = TreeView.CheckedNodes.ToList();

foreach (var nd in nodes.Where(nd => nd.Text != "(Select All)" && nd.Nodes.Count == 0))
 {
 strBuilder.Append(nd.Text);
 strBuilder.Append(", ");
 }

return strBuilder.Length > 0 ? strBuilder.ToString().Substring(0, strBuilder.Length - 2) : string.Empty;
 }

/// <summary>
 /// Initialization handler.
 /// </summary>
 /// <param name="e"></param>
 protected override void OnInit(EventArgs e)
 {
 this.ItemTemplate = new ComboTreeTemplate();

// Add a default item to the combobox.
 var item = new RadComboBoxItem();
 this.Items.Add(item);

// Now set up the ComboBox itself.
 this.OnClientDropDownClosing = "triggerSearchIfDirtyComboTree";
 this.MaxHeight = Unit.Pixel(300);
 this.DropDownWidth = Unit.Pixel(300);
 this.Height = Unit.Pixel(300);
 this.AllowCustomText = false;

// The RadComboTree requires certain client-side functions.
 // This registers them once per page regardless of how many controsl appear on it.
 const string javaScript = "function nodeClicked(sender,args){var node=args.get_node();if(node.get_checked()){node.uncheck()}else{node.check()}nodeChecked(sender,args)}function nodeChecked(sender,args){markAsDirtyComboTree(sender,args);var id=sender.get_id();var index=id.indexOf('_i');if (index > -1){id = id.substr(0, index);}var comboBox=$find(id);var tempNode=args.get_node();if(tempNode.get_text().toString()=='(Select All)'){tempNode.set_text('(Deselect All)');sender.checkAllNodes();}else if(tempNode.get_text().toString()=='(Deselect All)'){tempNode.set_text('(Select All)');sender.uncheckAllNodes();}var nodes=new Array();nodes=sender.get_checkedNodes();var vals='';var i=0;var nodeCount=0;for(i=0;i<nodes.length;i++){var n=nodes[i];var nodeText=n.get_text().toString();if(nodeText!='(Select All)' && n.get_nodes().get_count()==0){if(vals != ''){vals=vals+', '}vals=vals+n.get_text().toString();nodeCount++;}}supressDropDownClosing=true;if(vals.length>30){vals=nodeCount+' items checked';}comboBox.set_text(vals);comboBox.trackChanges();comboBox.get_items().getItem(0).set_text(vals);comboBox.commitChanges();}function StopPropagation(e){e.cancelBubble=true;if(e.stopPropagation){e.stopPropagation()}}function markAsDirtyComboTree(sender,eventArgs){sender.get_element().setAttribute('isDirty','true');}function triggerSearchIfDirtyComboTree(sender,eventArgs){if(sender.get_dropDownElement().childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[0].getAttribute('isDirty')=='true'){triggerSearch();}}";
 Page.ClientScript.RegisterClientScriptBlock(Page.GetType(), "radcombotree", javaScript, true);

base.OnInit(e);
 }
 /// <summary>
 /// Make all parent items in the TreeView bold.
 /// </summary>
 private void MarkParentsAsBold()
 {
 foreach (var itm in TreeView.Nodes.Cast<RadTreeNode>().Where(itm => itm.Nodes.Count > 0))
 {
 itm.Font.Bold = true;
 }
 }
 }

/// <summary>
 /// This is where the treeview is added inside the ItemTemplate programatically.
 /// Represents the ItemTemplate for the RadComboBox. Contains the RadTreeView.
 /// </summary>
 class ComboTreeTemplate : ITemplate
 {
 /// <summary>
 /// Instantation handler. Analogous to OnLoad.
 /// </summary>
 /// <param name="container"></param>
 public void InstantiateIn(Control container)
 {
 // Parent div that will contain the RadTreeView.
 var div = new HtmlGenericControl("div");
 div.Attributes["onclick"] = "StopPropagation(event)";

// Create the TreeView.
 var treeView = new RadTreeView();
 treeView.CheckBoxes = true;
 treeView.CheckChildNodes = true;
 treeView.TriStateCheckBoxes = true;
 treeView.OnClientNodeChecked = "nodeChecked";
 treeView.OnClientNodeClicked = "nodeClicked";
 treeView.DataFieldID = "ID"; // This is so that the Parent can find it.
 treeView.DataFieldParentID = "ParentID"; // This is to create hierarchy.
 treeView.DataTextField = "Name"; // This is the text value displayed on the UI.
 treeView.DataValueField = "ID"; // This is the value sent back to the filter methods.
 treeView.AppendDataBoundItems = true;

// Create the select all node.
 var node = new RadTreeNode();
 node.Text = "(Select All)";
 node.Value = "SelectAll";
 node.Expanded = false;

// Add the select all node to the tree view.
 treeView.Nodes.Add(node);

// Now add the tree view to the parent div and add the whole thing to the page.
 div.Controls.Add(treeView);
 container.Controls.Add(div);
 }
 }
}

Credits

The idea and base code for this came from:

  1. http://blog.ropardo.ro/2011/07/15/multiple-selection-treeview-in-combobox/
  2. http://www.telerik.com/community/forums/aspnet-ajax/combobox/radtreeview-in-radcombobox-with-checkbox-and-tristate.aspx
Category: .NET
  • Kev says:

    Hi, I always struggle to get this working and get an element out of range exception on this line: return this.Items[0].Controls[0].Controls[0] as RadTreeView;

    It could be my ascx that is wrong – can you post some sample ascx please?

    June 25, 2013 at 5:19 AM
  • Kev says:

    Ok, more detail. My problem is when I set the dataset in InitializeControls in the codebehind of the page hosting the RadComboTree. What is the correct way to specify the DataSource?

    protected override void InitializeControls(GenericContainer container)
    {
    RadComboTree1.DataSource = MyDataSet;
    }

    June 25, 2013 at 5:50 AM
  • Matthew Sigman says:

    Kev, it looks like your datasource is correct. So my next question is, is the data you are binding to set up correctly? If you substitute the RadComboTree for a plain TreeList, does it bind correctly or do you get the same error?

    June 25, 2013 at 7:04 AM
    • Kev says:

      Hi Matthew,

      my data (a DataTable) is generated from a helper method. I have tried this with a TreeView and that works fine, and with it coming from a helper I am sure I am giving your control identical data.

      June 25, 2013 at 7:12 AM
      • Kev says:

        To complicate things (if it does) I am using this within Sitefinity. I am not sure if that should make a difference though.

        June 25, 2013 at 7:13 AM
        • Matthew Sigman says:

          Well, I’ve not tested mine with a DataTable, although it should work. It is probably a mismatch between the DataFieldID, DataFieldParentID, DataValueField, and DataTextField. If you look at the bottom of the code snippet above, you will see those fields, and the corresponding column names that the control is expecting each of those to have.

          I will also mention that since releasing this, Telerik has released a version of their own, which is probably more robust: http://www.telerik.com/help/aspnet-ajax/dropdowntree-overview.html

          June 25, 2013 at 7:19 AM
          • Kev says:

            HI Matthew,

            yes I found the Telerik version, but sadly that version of the aspnet-ajax controls is not the version shipped with Sitefinity :(

            It’s not a name mismatch either. Investigated that one too. I use:

            DataTable table = new DataTable();
            table.Columns.Add(“ID”);
            table.Columns.Add(“ParentID”);
            table.Columns.Add(“Name”);

            I have also tried using DataSets and DataTables with no locuk – the same error. I am baffled :( Thanks for your help so far though.

            June 25, 2013 at 7:43 AM
  • Joyin says:

    Thank you so much! It helps me a lot!

    October 31, 2014 at 12:41 PM

Your email address will not be published. Required fields are marked *

*