New to Telerik UI for WinForms? Download free 30-day trial

Mutiselect drop down list column in RadGridView

Date Posted Product Author
Q3 2012 SP1 Telerik UI for WinForms Tsvetan Raikov

How To

This article will demonstrate how to create a custom column in RadGridView with a multi-select drop down editor with check boxes. It allows keyboard selection with Ctrl and Shift keys and/or by using the mouse and the items' check boxes. The selection is stored in an integer array in the cell value. 

As of Q3 2014 (version 2014.3.1021) Telerik UI for WinForms suite offers RadCheckedDropDownList control which combines RadDropDownList and RadAutoCompleteBox in order to provide functionality to check items in the drop down area and tokenize them in the text area. All previous functionality is preserved, such as visual formatting and data binding, which is now extended. RadCheckedDropDownList can also be used as an editor in RadGridView.

Solution

The solution to this case is to create a custom column which uses a specific cell and editor. The editor allows you to have multiple selection, while the cell holds the values. The column is used to easily incorporate the cell and the editor in RadGridView.

In the column we will define the desired DataType. In this case it will be an array of integers. Additionally, we will specify the desired editor and cell types:

public class CustomColumn : GridViewComboBoxColumn
{
    public CustomColumn(string name)
        : base(name)
    {

    }

    public override Type DataType
    {
        get
        {
            if (UseGetLookupValue)
            {
                return typeof(int);
            }
            return typeof(int[]);
        }
        set { }
    }

    public bool UseGetLookupValue = false;

    public override Type GetDefaultEditorType()
    {
        return typeof(CustomDropDownListEditor);
    }

    public override Type GetCellType(GridViewRowInfo row)
    {
        if (row is GridViewDataRowInfo || row is GridViewNewRowInfo)
        {
            return typeof(CustomCellElement);
        }
        return base.GetCellType(row);
    }
}

Public Class CustomColumn
    Inherits GridViewComboBoxColumn
    Public Sub New(name As String)

        MyBase.New(name)
    End Sub

    Public Overrides Property DataType() As Type
        Get
            If UseGetLookupValue Then
                Return GetType(Integer)
            End If
            Return GetType(Integer())
        End Get
        Set
        End Set
    End Property

    Public UseGetLookupValue As Boolean = False

    Public Overrides Function GetDefaultEditorType() As Type
        Return GetType(CustomDropDownListEditor)
    End Function

    Public Overrides Function GetCellType(row As GridViewRowInfo) As Type
        If TypeOf row Is GridViewDataRowInfo OrElse TypeOf row Is GridViewNewRowInfo Then
            Return GetType(CustomCellElement)
        End If
        Return MyBase.GetCellType(row)
    End Function
End Class

Now, let's create the cells that we are going to use in our column. Here we will just override the SetContent method, where we will set the content of the cell - the selected item's text separated by semi column or empty string when the value is null:

public class CustomCellElement: GridDataCellElement
{
    public CustomCellElement(GridViewColumn column, GridRowElement row)
        : base(column, row)
    { }

    protected override Type ThemeEffectiveType
    {
        get
        {
            return typeof(GridDataCellElement);
        }
    }

    public override void SetContent()
    {
        int[] values = this.Value as int[];
        if (values == null)
        {
            this.Text = "";
        }
        else
        {
            string text = "";

            CustomColumn col = this.ColumnInfo as CustomColumn;
            if (col != null)
            {
                foreach (int i in values)
                {
                    col.UseGetLookupValue = true;
                    object val = col.GetLookupValue(i);
                    col.UseGetLookupValue = false;
                    if (val != null)
                    {
                        text += val.ToString() + "; ";
                    }
                }
            }
            this.Text = text;
        }
    }
}

Public Class CustomCellElement
    Inherits GridDataCellElement
    Public Sub New(column As GridViewColumn, row As GridRowElement)
        MyBase.New(column, row)
    End Sub

    Protected Overrides ReadOnly Property ThemeEffectiveType() As Type
        Get
            Return GetType(GridDataCellElement)
        End Get
    End Property

    Public Overrides Sub SetContent()
        Dim values As Integer() = TryCast(Me.Value, Integer())
        If values Is Nothing Then
            Me.Text = ""
        Else
            Dim text As String = ""

            Dim col As CustomColumn = TryCast(Me.ColumnInfo, CustomColumn)
            If col IsNot Nothing Then
                For Each i As Integer In values
                    col.UseGetLookupValue = True
                    Dim val As Object = col.GetLookupValue(i)
                    col.UseGetLookupValue = False
                    If val IsNot Nothing Then
                        text += val.ToString() + "; "
                    End If
                Next
            End If
            Me.Text = text
        End If
    End Sub
End Class

Let's continue with the editor. First we have to override the CreateEditorElement method where we will return the CustomEditorElement which we will use. Also, we will have to override the Value property in the getter of which we will return the selected items as an array of integers and in the setter we will set the selected items according to the value:

public class CustomDropDownListEditor : RadDropDownListEditor
{
     public override object Value
    {
        get
        {
            CustomEditorElement editorElement = this.EditorElement as CustomEditorElement;
            if (editorElement != null)
            {
                List<int> selected = new List<int>();
                foreach (RadListDataItem item in editorElement.ListElement.SelectedItems)
                {
                    selected.Add((int)item.Value);
                }
                return selected.ToArray();
            }
            return base.Value;
        }
        set
        {
            CustomEditorElement editorElement = this.EditorElement as CustomEditorElement;
            if (editorElement != null)
            {
                int[] names = value as int[];
                if (names != null)
                {
                    foreach (int val in names)
                    {
                        RadListDataItem item = FindByValue(val);
                        if (item != null)
                        {
                            item.Selected = true;
                        }
                    }
                }
                editorElement.CallTextChanged();
            }
        }
    }

    private RadListDataItem FindByValue(object value)
    {
        CustomEditorElement editorElement = this.EditorElement as CustomEditorElement;
        foreach (RadListDataItem item in editorElement.Items)
        {
            if (value.Equals(item.Value))
            {
                return item;
            }
        }
        return null;
    }

    protected override RadElement CreateEditorElement()
    {
        return new CustomEditorElement();
    }
}

Public Class CustomDropDownListEditor
    Inherits RadDropDownListEditor

    Public Overrides Property Value() As Object
        Get
            Dim editorElement As CustomEditorElement = TryCast(Me.EditorElement, CustomEditorElement)
            If editorElement IsNot Nothing Then
                Dim selected As New List(Of Integer)()
                For Each item As RadListDataItem In editorElement.ListElement.SelectedItems
                    selected.Add(CInt(item.Value))
                Next
                Return selected.ToArray()
            End If
            Return MyBase.Value
        End Get
        Set
            Dim editorElement As CustomEditorElement = TryCast(Me.EditorElement, CustomEditorElement)
            If editorElement IsNot Nothing Then
                Dim names As Integer() = TryCast(value, Integer())
                If names IsNot Nothing Then
                    For Each val As Integer In names
                        Dim item As RadListDataItem = FindByValue(val)
                        If item IsNot Nothing Then
                            item.Selected = True
                        End If
                    Next
                End If
                editorElement.CallTextChanged()
            End If
        End Set
    End Property

    Private Function FindByValue(value As Object) As RadListDataItem
        Dim editorElement As CustomEditorElement = TryCast(Me.EditorElement, CustomEditorElement)
        For Each item As RadListDataItem In editorElement.Items
            If value.Equals(item.Value) Then
                Return item
            End If
        Next
        Return Nothing
    End Function

    Protected Overrides Function CreateEditorElement() As RadElement
        Return New CustomEditorElement()
    End Function
End Class

Now, the editor element. It will consist of a LightVisualElement, which displays the values and is placed in the EditableArea of the control and a button for closing the drop down.

In the element's constructor we will first initialize the close button and add it accordingly to the sizing grip of the popup. We will also subscribe to its click, where we will close the popup. Next, we will set the SelectionMode to MultiSimple, which means that the users will be able to select item with mouse click or space button. We will also subscribe to the following events:

  • PopupClosing - here we will cancel the popup closure when it contains mouse so we can use it to select items
  • CreatingVisualItem - here replace the default visual item with a custom one
  • ItemDataBinding - replace the default data item with a custom one

In the CreateChildElements override, we will initialize and add the LightVisualElement which will hold the text.

Another useful override is the one of the ShowPopup method, where prior calling the base functionality we will save the selected items and restore them after.

Finally, we will create a method that fires the OnTextChanged event, used to set the element's text accordingly:

public class CustomEditorElement : RadDropDownListEditorElement
{
    LightVisualElement customText;
    RadButtonElement closeButton;
    bool textChanged;

    public CustomEditorElement()
    {
        closeButton = new RadButtonElement("Close");
        closeButton.SetValue(DockLayoutPanel.DockProperty, Dock.Bottom);
        closeButton.Click += new EventHandler(closeButton_Click);
        this.Popup.SizingGripDockLayout.Children.Insert(1, closeButton);

        this.SelectionMode = System.Windows.Forms.SelectionMode.MultiSimple;

        this.PopupClosing += new RadPopupClosingEventHandler(CustomEditorElement_PopupClosing);
        this.CreatingVisualItem += new CreatingVisualListItemEventHandler(CustomEditorElement_CreatingVisualItem);
        this.ListElement.ItemDataBinding += this.CustomEditorElement_ItemDataBinding;
    }

    void closeButton_Click(object sender, EventArgs e)
    {
        ClosePopup();
        GridDataCellElement cell = this.Parent as GridDataCellElement;
        if (cell != null)
        {
            cell.GridViewElement.EndEdit();
        }
    }

    private void CustomEditorElement_ItemDataBinding(object sender, ListItemDataBindingEventArgs args)
    {
        args.NewItem = new CustomListDataItem();
    }

    void CustomEditorElement_CreatingVisualItem(object sender, CreatingVisualListItemEventArgs args)
    {
        args.VisualItem = new CustomListVisualItem();
    }

    void CustomEditorElement_PopupClosing(object sender, RadPopupClosingEventArgs args)
    {
        CustomEditorElement editor = (CustomEditorElement)sender;
        if (args.CloseReason == RadPopupCloseReason.Mouse)
        {
            if (editor.PopupForm.Bounds.Contains(Control.MousePosition))
            {
                args.Cancel = true;
            }
        }
    }

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

        customText = new LightVisualElement();
        customText.DrawBorder = false;
        customText.DrawFill = true;
        customText.GradientStyle = GradientStyles.Solid;
        customText.BackColor = Color.White;
        customText.TextAlignment = ContentAlignment.MiddleLeft;
        this.EditableElement.Children.Add(customText);
    }

    public override void ShowPopup()
    {
        bool[] selected = new bool[this.Items.Count];
        for (int i = 0; i < selected.Length; i++)
        {
            selected[i] = this.Items[i].Selected;
        }
        base.ShowPopup();
        for (int i = 0; i < selected.Length; i++)
        {
            this.Items[i].Selected = selected[i];
        }
    }


    public void CallTextChanged()
    {
        OnTextChanged(EventArgs.Empty);
    }

    protected override void OnTextChanged(EventArgs e)
    {
        if (textChanged)
        {
            return;
        }
        textChanged = true;
        string text = "";
        foreach (RadListDataItem item in this.ListElement.SelectedItems)
        {
            text += item.Text + "; ";
        }
        customText.Text = text;
        textChanged = false;
    }
}

Public Class CustomEditorElement
    Inherits RadDropDownListEditorElement
    Private customText As LightVisualElement
    Private closeButton As RadButtonElement
    Private textChanged As Boolean

    Public Sub New()
        closeButton = New RadButtonElement("Close")
        closeButton.SetValue(DockLayoutPanel.DockProperty, Dock.Bottom)
        closeButton.Click += New EventHandler(closeButton_Click)
        Me.Popup.SizingGripDockLayout.Children.Insert(1, closeButton)

        Me.SelectionMode = System.Windows.Forms.SelectionMode.MultiSimple

        Me.PopupClosing += New RadPopupClosingEventHandler(CustomEditorElement_PopupClosing)
        Me.CreatingVisualItem += New CreatingVisualListItemEventHandler(CustomEditorElement_CreatingVisualItem)
        Me.ListElement.ItemDataBinding += Me.CustomEditorElement_ItemDataBinding
    End Sub

    Private Sub closeButton_Click(sender As Object, e As EventArgs)
        ClosePopup()
        Dim cell As GridDataCellElement = TryCast(Me.Parent, GridDataCellElement)
        If cell IsNot Nothing Then
            cell.GridViewElement.EndEdit()
        End If
    End Sub

    Private Sub CustomEditorElement_ItemDataBinding(sender As Object, args As ListItemDataBindingEventArgs)
        args.NewItem = New CustomListDataItem()
    End Sub

    Private Sub CustomEditorElement_CreatingVisualItem(sender As Object, args As CreatingVisualListItemEventArgs)
        args.VisualItem = New CustomListVisualItem()
    End Sub

    Private Sub CustomEditorElement_PopupClosing(sender As Object, args As RadPopupClosingEventArgs)
        Dim editor As CustomEditorElement = DirectCast(sender, CustomEditorElement)
        If args.CloseReason = RadPopupCloseReason.Mouse Then
            If editor.PopupForm.Bounds.Contains(Control.MousePosition) Then
                args.Cancel = True
            End If
        End If
    End Sub

    Protected Overrides Sub CreateChildElements()
        MyBase.CreateChildElements()

        customText = New LightVisualElement()
        customText.DrawBorder = False
        customText.DrawFill = True
        customText.GradientStyle = GradientStyles.Solid
        customText.BackColor = Color.White
        customText.TextAlignment = ContentAlignment.MiddleLeft
        Me.EditableElement.Children.Add(customText)
    End Sub

    Public Overrides Sub ShowPopup()
        Dim selected As Boolean() = New Boolean(Me.Items.Count - 1) {}
        For i As Integer = 0 To selected.Length - 1
            selected(i) = Me.Items(i).Selected
        Next
        MyBase.ShowPopup()
        For i As Integer = 0 To selected.Length - 1
            Me.Items(i).Selected = selected(i)
        Next
    End Sub


    Public Sub CallTextChanged()
        OnTextChanged(EventArgs.Empty)
    End Sub

    Protected Overrides Sub OnTextChanged(e As EventArgs)
        If textChanged Then
            Return
        End If
        textChanged = True
        Dim text As String = ""
        For Each item As RadListDataItem In Me.ListElement.SelectedItems
            text += item.Text + "; "
        Next
        customText.Text = text
        textChanged = False
    End Sub
End Class

In the custom data item will just add a property to store that information about a check operation:

public class CustomListDataItem : RadListDataItem
{
    public static readonly RadProperty CheckedProperty = RadProperty.Register("Checked", typeof(bool), typeof(CustomListDataItem), new RadElementPropertyMetadata(false));

    public bool Checked
    {
        get
        {
            return (bool)this.GetValue(CustomListDataItem.CheckedProperty);
        }
        set
        {
            this.SetValue(CustomListDataItem.CheckedProperty, value);
        }
    }

    protected override void SetDataBoundItem(bool dataBinding, object value)
    {
        base.SetDataBoundItem(dataBinding, value);
        if (value is INotifyPropertyChanged)
        {
            INotifyPropertyChanged item = value as INotifyPropertyChanged;
            item.PropertyChanged += item_PropertyChanged;
        }
    }

    private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "Checked")
        {
            this.Checked = (this.DataBoundItem  as RadListDataItem).Selected;
        }           
    }
 }

Public Class CustomListDataItem
    Inherits RadListDataItem

    Public Shared ReadOnly CheckedProperty As RadProperty = RadProperty.Register("Checked", GetType(Boolean), GetType(CustomListDataItem), New RadElementPropertyMetadata(False))

    Public Property Checked() As Boolean
        Get
            Return CBool(Me.GetValue(CustomListDataItem.CheckedProperty))
        End Get
        Set
            Me.SetValue(CustomListDataItem.CheckedProperty, value)
        End Set
    End Property

    Protected Overrides Sub SetDataBoundItem(dataBinding As Boolean, value As Object)
        MyBase.SetDataBoundItem(dataBinding, value)
        If TypeOf value Is INotifyPropertyChanged Then
            Dim item As INotifyPropertyChanged = TryCast(value, INotifyPropertyChanged)
            item.PropertyChanged += item_PropertyChanged
        End If
    End Sub

    Private Sub item_PropertyChanged(sender As Object, e As PropertyChangedEventArgs)
        If e.PropertyName = "Checked" Then
            Me.Checked = TryCast(Me.DataBoundItem, RadListDataItem).Selected
        End If
    End Sub
 End Class

And finally, the visual item. In the CreateChildElements override, we will initialize a StackLayoutElement, which will hold both the check box (RadCheckBoxElement) and the content element (a LightVisualElement). In the ToggleStateChanged event of the check box we will set the data item's Check property (which we have added in the CustomListDataItem class) and in the SynchronizeProperties override we will sync the check box and the text with its data item:

public class CustomListVisualItem : RadListVisualItem
{
    RadCheckBoxElement checkbox;
    LightVisualElement content;

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

        StackLayoutElement stack = new StackLayoutElement();
        stack.Orientation = Orientation.Horizontal;
        this.Children.Add(stack);

        checkbox = new RadCheckBoxElement();
        checkbox.ToggleStateChanged += new StateChangedEventHandler(checkbox_ToggleStateChanged);
        stack.Children.Add(checkbox);

        content = new LightVisualElement();
        content.StretchHorizontally = false;
        content.StretchVertically = true;
        content.TextAlignment = ContentAlignment.MiddleLeft;
        content.NotifyParentOnMouseInput = true;
        stack.Children.Add(content);
    }

    void checkbox_ToggleStateChanged(object sender, StateChangedEventArgs e)
    {
        ((CustomListDataItem)this.Data).Checked = this.checkbox.Checked;
    }

    protected override Type ThemeEffectiveType
    {
        get
        {
            return typeof(RadListVisualItem);
        }
    }

    protected override void SynchronizeProperties()
    {
        base.SynchronizeProperties();
        checkbox.IsChecked = this.Data.Selected;
        this.content.Text = this.Data.Text;
        this.Text = "";
    }
}

Public Class CustomListVisualItem
    Inherits RadListVisualItem
    Private checkbox As RadCheckBoxElement
    Private content As LightVisualElement

    Protected Overrides Sub CreateChildElements()
        MyBase.CreateChildElements()

        Dim stack As New StackLayoutElement()
        stack.Orientation = Orientation.Horizontal
        Me.Children.Add(stack)

        checkbox = New RadCheckBoxElement()
        checkbox.ToggleStateChanged += New StateChangedEventHandler(checkbox_ToggleStateChanged)
        stack.Children.Add(checkbox)

        content = New LightVisualElement()
        content.StretchHorizontally = False
        content.StretchVertically = True
        content.TextAlignment = ContentAlignment.MiddleLeft
        content.NotifyParentOnMouseInput = True
        stack.Children.Add(content)
    End Sub

    Private Sub checkbox_ToggleStateChanged(sender As Object, e As StateChangedEventArgs)
        DirectCast(Me.Data, CustomListDataItem).Checked = Me.checkbox.Checked
    End Sub

    Protected Overrides ReadOnly Property ThemeEffectiveType() As Type
        Get
            Return GetType(RadListVisualItem)
        End Get
    End Property

    Protected Overrides Sub SynchronizeProperties()
        MyBase.SynchronizeProperties()
        checkbox.IsChecked = Me.Data.Selected
        Me.content.Text = Me.Data.Text
        Me.Text = ""
    End Sub
End Class

Here is how to put this column in action:

public Form1()
{
    InitializeComponent();

    DataTable t = new DataTable();
    t.Columns.Add("ID", typeof(int));
    t.Columns.Add("Name", typeof(string));
    t.Rows.Add(1, "one");
    t.Rows.Add(2, "two");
    t.Rows.Add(3, "three");
    t.Rows.Add(4, "four");
    t.Rows.Add(5, "five");
    t.Rows.Add(6, "six");
    t.Rows.Add(7, "seven");
    t.Rows.Add(8, "eight");
    t.Rows.Add(9, "nine");
    t.Rows.Add(10, "ten");

    CustomColumn col = new CustomColumn("MutiSelect column");
    col.DataSource = t;
    col.DisplayMember = "Name";
    col.ValueMember = "ID";
    radGridView1.Columns.Add(col);

    radGridView1.Rows.Add( new int[] { 9, 6, 10 });
    radGridView1.Rows.Add( new int[] { 5, 1, 3 });
    radGridView1.Rows.Add( new int[] { 8,7 });
    radGridView1.Rows.Add( new int[] { 4, 2, 1 });

}

Public Sub New()
    InitializeComponent()

    Dim t As New DataTable()
    t.Columns.Add("ID", GetType(Integer))
    t.Columns.Add("Name", GetType(String))
    t.Rows.Add(1, "one")
    t.Rows.Add(2, "two")
    t.Rows.Add(3, "three")
    t.Rows.Add(4, "four")
    t.Rows.Add(5, "five")
    t.Rows.Add(6, "six")
    t.Rows.Add(7, "seven")
    t.Rows.Add(8, "eight")
    t.Rows.Add(9, "nine")
    t.Rows.Add(10, "ten")

    Dim col As New CustomColumn("MutiSelect column")
    col.DataSource = t
    col.DisplayMember = "Name"
    col.ValueMember = "ID"
    radGridView1.Columns.Add(col)

    radGridView1.Rows.Add(New Integer() {9, 6, 10})
    radGridView1.Rows.Add(New Integer() {5, 1, 3})
    radGridView1.Rows.Add(New Integer() {8, 7})
    radGridView1.Rows.Add(New Integer() {4, 2, 1})
End Sub

A complete solution in C# can be found here.

In this article