Selecting Multiple Items using Select2 in MVC 5

by | Apr 27, 2014

Select2 is honestly one of the nicest jQuery libraries available for web site development.  It gives the web developer a lot of power in creating select boxes.  You can read about it on the GitHub site. My purpose in writing this article isn’t to list all of the nice things about Select2 but rather to give an example that I don’t see much as I look for help with the tool, and that is, how to use Select2 for multi-select with remote data in ASP.NET MVC 5.  In other words, how do I get here:

Select2Makes

The Select2 GitHub site is great but doesn’t help with the following MVC concepts:

  1. How to write in the view with Razor syntax
  2. How to do the search using Web Api
  3. How to create the model variable to handle multiple values
  4. How to persist the model value for the postback

The Example

At the end of this post, you’ll be able to:

  • Type in a minimum of 2 characters which will kick off a search on automobile makes (a vast list in real life) using Web API
  • Select a value from the resultant list
  • Keep repeating until you’ve selected all you want
  • Submit the form to a controller action
  • Re-display the selected values on postback

Assumptions:

  • You can set up Select2 in your project according to the GitHub site

The Model

This is the Automobile class with a “Makes” property.

public class Automobile
{
    [Display(Name = "Makes")]
    public string Makes { get; set; }
}

Note that the Makes property will store a comma-delimited list of makes as a string.

The View

The following is the code for the strongly-typed view:

@model Select2Example.Models.Automobile

@{
    ViewBag.Title = "Home Page";
}

<div class="jumbotron">
    <h1>Select2 Multi-Select Example</h1>
</div>

@using (Html.BeginForm("Submit", "Home"))
{
    <div class="row">
        <div class="col-sm-4">
            @Html.DisplayNameFor(m => m.Makes)
            <input type="hidden" class="select2-offscreen" id="make-hdn" style="width:100%" value="@Model.Makes" />
            <input type="text" class="hidden" id="make" name="Makes" value="@Model.Makes" />
        </div>
        <div class="col-sm-1">
            <input type="submit" value="Submit" class="btn btn-primary btn-lg submit-btn" />
        </div>
    </div>
}

@section scripts
{
    @Scripts.Render("~/Scripts/select2.js")
}

The lines <input type=”hidden” & <input type=”text” are what will give you the Select2 text box.

  • The MVC things to note here are the importance of setting value=@Model.Makes in both of them and setting the name=Makes to match the property name.
    • This does the binding from the Model on load and to the Model on postback.
    • Leaving this detail out will give you much frustration, let me save you that pain!

The Controller for Showing the View and Handling the Submit

I’m just using the HomeController for the Index and Submit methods:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View(new Automobile());
    }

    public ActionResult Submit(Automobile auto)
    {
        return View("Index", auto);
    }
}

Searching for the Makes

So now the basics are set up.  We can display the page and click submit.  However, we still cannot select any values since that has to be set up in javascript.  My javascript example makes it easy to set this up multiple times in your project by creating a function to set up Select2.

$(document).ready(function () {
    select2Dropdown('make-hdn', 'make', 'Search for make(s)', 'SearchMake', 'GetMake', true);
});

function select2Dropdown(hiddenID, valueID, ph, listAction, getAction, isMultiple) {
    var sid = '#' + hiddenID;
    $(sid).select2({
        placeholder: ph,
        minimumInputLength: 2,
        allowClear: true,
        multiple: isMultiple,
        ajax: {
            url: "/api/CommonApi/" + listAction,
            dataType: 'json',
            data: function (term, page) {
                return {
                    id: term // search term
                };
            },
            results: function (data) {
                return { results: data };
            }
        },
        initSelection: function (element, callback) {
            // the input tag has a value attribute preloaded that points to a preselected make's id
            // this function resolves that id attribute to an object that select2 can render
            // using its formatResult renderer - that way the make text is shown preselected
            var id = $('#' + valueID).val();
            if (id !== null && id.length > 0) {
                $.ajax("/api/CommonApi/" + getAction + "/" + id, {
                    dataType: "json"
                }).done(function (data) { callback(data); });
            }
        },
        formatResult: s2FormatResult,
        formatSelection: s2FormatSelection
    });

    $(document.body).on("change", sid, function (ev) {
        var choice;
        var values = ev.val;
        // This is assuming the value will be an array of strings.
        // Convert to a comma-delimited string to set the value.
        if (values !== null && values.length > 0) {
            for (var i = 0; i < values.length; i++) {
                if (typeof choice !== 'undefined') {
                    choice += ",";
                    choice += values[i];
                }
                else {
                    choice = values[i];
                }
            }
        }

        // Set the value so that MVC will load the form values in the postback.
        $('#' + valueID).val(choice);
    });
}

function s2FormatResult(item) {
    return item.text;
}

function s2FormatSelection(item) {
    return item.text;
}

When the document is ready, we’ll set up the Select2 components by calling select2Dropdown.

MVC things to note:

  • Web API is called in the ajax portion of the select2 method to search for what was typed in the text box.  Here we call the SearchMake api method (see ApiController below).  For example, when FO is typed, the search will return FORD.
  • Web API is also called in the initSelection portion when the form is being shown initially or on postback.  Here we call the GetMake api method (see ApiController below).
  • The .on(“change” handler is important in MVC since it sets the value in the id=’make’ text box and that is what is used in the postback.
  • Set up the Web API route as follows so multiple actions can be used:
config.Routes.MapHttpRoute("DefaultApiWithId", "api/{controller}/{action}/{id}", new { id = RouteParameter.Optional }, null);
    • Note, I tried using route attributes in MVC 5 here but it didn’t work because Select2 generates the url with a SearchMake?id=fo instead of SearchMake/fo and route attributes don’t seem to be able to handle this. That’s not to say it can’t be done, I just don’t know how.
  • Here is the api code:
public class CommonApiController : ApiController
{
    private IEnumerable<SelectItem> _makes = new List<SelectItem>
        {
            new SelectItem { id = 1, text = "Ford" },
            new SelectItem { id = 2, text = "Chevy" },
            new SelectItem { id = 3, text = "Chrysler" },
            new SelectItem { id = 4, text = "Honda" },
            new SelectItem { id = 5, text = "Toyota" }
        };

    [HttpGet]
    public IEnumerable<SelectItem> SearchMake(string id)
    {
        var query = _makes.Where(m => m.text.ToLower().Contains(id.ToLower()));

        return query;
    }

    [HttpGet]
    public IEnumerable<SelectItem> GetMake(string id)
    {
        if (string.IsNullOrWhiteSpace(id)) return null;

        var items = new List<SelectItem>();

        string[] idList = id.Split(new char[] { ',' });
        foreach (var idStr in idList)
        {
            int idInt;
            if (int.TryParse(idStr, out idInt))
            {
                items.Add(_makes.FirstOrDefault(m => m.id == idInt));
            }
        }

        return items;
    }
}

public class SelectItem
{
    public int id { get; set; }
    public string text { get; set; }
}
  • It’s important to define a SelectItem class with properties of id and text (you can customize it in Select2 but I just do it this way to make it easy).

The End Result

Here’s what we get now when we run the code:

Select2Full

I also want you to see what happens when you click the Submit button by breaking into the HomeController’s Submit method:

Select2Postback

Note that 2,1,4 represents the id’s of Chevy, Ford and Honda.

Hopefully this has helped you to get this complicated Select2 construct working in MVC 5.