Search code examples
jqueryasp.net-mvctwitter-bootstrapasp.net-mvc-4jquery-ui-multiselect

jquery multiselect - grouping from database


I am using a jquery multiselect plugin [https://github.com/davidstutz/bootstrap-multiselect] and binding it dynamically with database values.

HTML

@Html.ListBoxFor(m => m.Classes, new SelectList(Model.Classes, "Value", "Text"), new { @id = "classList" })

SCRIPT

$('#classList').multiselect({ enableClickableOptGroups: true });

The model in the view is a view model and contains a property for a SelectList

public class SearchControlViewModel 
{
    ....
    public SelectList Classes { get; set; }
}

and the code in the controller

SearchControlViewModel  model = new SearchControlViewModel()
{
    ....
    Classes = new SelectList(repClass.GetClassesByYear(23), "classID", "classname")
};
return View(model);

It works like a charm except for one thing - I want to add grouping/group header like <optgroup> does. How can I do that?

The GetClassesByYear() method used for generating the SelectList returns an object containing properties int classID, string classname and string grade and I want to be able to group the options by grade.


Solution

  • Option groups are not supported in MVC-4, and you would need to upgrade to MVC-5 to make use out-of-the-box functions. If you do upgrade, refer Constructing a Select List with OptGroup groups for examples using both the SelectList constructor, and by generating an IEnumerable<SelectListItem>

    Without upgrading, you could pass a model representing your groups and their options to the view , and use some javascript/jquery to generate the elements. Start by creating some additional view models

    public class OptionGroupVM
    {
        public string GroupName { get; set; }
        public IEnumerable<OptionVM> Options { get; set; }
    }
    public class OptionVM
    {
        public string Value { get; set; }
        public string Text { get; set; }
        public bool IsSelected { get; set; } // Only applicable if your not binding to a model property
    }
    

    Then modify your main view model to include the following

    public class SearchControlViewModel 
    {
        ....
        public IEnumerable<int> SelectedClasses { get; set; }
        public IEnumerable<OptionGroupVM> ClassOptions { get; set; }
    }
    

    Note that your current model and use of ListBoxFor() is incorrect because

    1. you cannot bind a <select multiple> to a collection of complex objects which your Classes property is - the model needs to be a collection of value types, and

    2. you cannot use the same name for the property your binding to and the SelectList - refer Will there be any conflict if i specify the viewBag name to be equal to the model property name inside @Html.DropDownListFor for more detail

    In the controller

    var data = repClass.GetClassesByYear(23);
    var groupedOptions = data.GroupBy(x => x.grade).Select(x => new OptionGroupVM()
    {
        GroupName = x.Key.ToString(),
        Options = x.Select(y => new OptionVM()
        {
            Value = y.classId.ToString(),
            Text = y.classcame,
        })
    });
    SearchControlViewModel model = new SearchControlViewModel()
    {
        ....
        SelectedClasses = ...., // the values of the options that will be pre-selected
        ClassOptions = groupedOptions
    };
    return View(model);
    

    And in the view use

    @Html.ListBoxFor(m => SelectedClasses, Enumerable.Empty<SelectListItem>(), new { id = "classList" })
    

    which will initially generate the <select> without any options. Then use the following script to generate the options

    <script type="text/javascript">
        // Get data
        var listBox = $('#classList');
        var selected = @Html.Raw(Json.Encode(Model.SelectedClasses));
        var groups = @Html.Raw(Json.Encode(Model.ClassOptions));
    
        // Generate options
        createGroupedOptions(listBox, selected, groups);
        // Attach plug-in
        listBox.multiselect({ enableClickableOptGroups: true });
    
        // This function could be in an external js file
        function createGroupedOptions(element, selected, groups) {
            for (var i = 0; i < groups.length; i++) {
                var group = groups[i];
                var groupElement = $('<optgroup></optgroup>').attr('label', group.GroupName);
                for(var j = 0; j < group.Options.length; j++) {
                    var option = group.Options[j];
                    var optionElement = $('<option></option>').val(option.Value).text(option.Text);
                    if (selected) {
                        if (selected.toString().indexOf(option.Value) >= 0) {
                            optionElement.attr('selected', 'selected')
                        }
                    } else {
                        if (option.IsSelected) {
                            optionElement.attr('selected', 'selected')
                        }
                    }
    
                    $(groupElement).append(optionElement);
                }
                $(element).append(groupElement);
            }
        }
    </script>