Better solution for ASP.NET MVC checkbox list

Couple of years ago I worked out my first solution for checkbox list in ASP.NET MVC. Today I got some markable simplifications done and my solution is close to state where some nice-to-have tweaks can be done to add more automatics to controller side but solution is also good enough how it is like now. Let’s see how to get checkbox list functionality done with couple of simple steps.

References

This is not required topic. It’s here just to track previous steps of story so I can later remember where this solution came from.

Main problems with previous solution were the facts that generating list of items for checkbox list and making updates to changed collection later were not very well implemented and these implementations didn’t defined simple enough interface to use for front-end developers.

Events and tagsSolution

In big picture our solution is simple (think that we create tag list for event):

  1. We will create base class for business of model classes that we can use in controllers – it depends on your architecture.
  2. We will write extension method that creates list of all tags and selects the ones that were already chosen.
  3. We will write extension method to HtmlHelper that displays checkbox list to user. This extension method is used in view.
  4. We weill write extension method to update event tags collection when user clicks save button in view.

It means that our solution has three generic interfacing points:

  1. Create data source for checkbox list
  2. Display checkbox list
  3. Update data source

It is possible to go further and find the ways how to automate update process to move updating functionality to model binder by example but I don’t stop on tweaks like this here because I have no good idea right now if these tweaks make any good or are they just nice things that turn out to be too confusing for end users.

Base class for business or model classes

Before we implement full checkbox list functionality we need some preparations. My solution is not ideally generic and it has some requirements to avoid complex logic that involves reflection and smart code-level decisions. I don’t go deeper here as anyway you see later why we made this step.

Whatever you use in your MVC application – domain classes or mapped models – we need something static where we can link extension methods we will write at later steps. I’m using Entity Framework with POCO-s and this solution makes some use of Entity Framework features that maybe other mappers doesn’t support this way.

My base class for business classes is as follows:


public abstract class BaseEntity
{
   
public int Id { get; set; }
}

And here is example business class:


public class Event : BaseEntity 
{
   
public
Event()
    {
       
this.Tags = new HashSet<Tag
>();
    }

   
public string Title { get; set
; }
   
public DateTime Begins { get; set
; }
   
public DateTime Ends { get; set
; }
   
public string Description { get; set
; }
   
public string Url { get; set
; }
   
public string TitleForUrl { get; set
; }
   
public string Abstract { get; set
; }
   
public int Clicks { get; set
; }

   
public virtual ICollection<Tag> Tags { get; set
; }
   
public virtual ICollection<Party> Vendors { get; set
; }
   
public virtual ICollection<Party> Organizers { get; set; }
}

You can define also abstract property like DisplayName for business base class to make sure that all business classes have string presentation understandable to humans. But this is not very important fact here.

Creating source for checkbox list

Next step is to prepare data for checkbox list. We need two things:

  1. Collection of items that can be selected
  2. Collection of items that must be shown as selected

By example, when we want to let user select tags for some event we need list of all tags to show and list of current tags to know what tags exactly must be shown as selected.

To meet these needs I wrote simple extension method to business base class type of collections:


public static IEnumerable<SelectListItem> ToCheckBoxListSource<T>(this IEnumerable<T> checkedCollection, 
IEnumerable<T> allCollection)
where T : BaseEntity
{
   
var result = new List<SelectListItem
>();

   
foreach (var allItem in
allCollection)
    {
       
var selectItem = new SelectListItem
();
        selectItem.Text = allItem.ToString();
        selectItem.Value = allItem.Id.ToString();
        selectItem.Selected = (checkedCollection.Count(c => c.Id == allItem.Id) > 0);

        result.Add(selectItem);
    }

   
return result;
}

This method creates list of SelectListItems for allCollection and uses checkedCollection to find out if current item must be displayed as checked or not. Example of controller comes below.

Displaying checkbox list

Now as we have data for checkbox list we can focus on displaying it. I created simple extension method for HtmlHelper so we can display checkbox list like any other control on edit form.


public static MvcHtmlString CheckBoxList(this HtmlHelper helper, string name, 
IEnumerable<SelectListItem
> items)
{
   
var output = new StringBuilder
();
    output.Append(
@"<div class=""checkboxList"">"
);

   
foreach (var item in
items)
    {
        output.Append(
@"<input type=""checkbox"" name="""
);
        output.Append(name);
        output.Append(
"\" value=\""
);
        output.Append(item.Value);
        output.Append(
"\""
);

       
if
(item.Selected)
            output.Append(
@" checked=""checked"""
);

        output.Append(
" />"
);
        output.Append(item.Text);
        output.Append(
"<br />"
);
    }

    output.Append(
"</div>"
);

   
return new MvcHtmlString(output.ToString());
}

As you can see here I chose pretty safe data source for checkbox list. We use safe collection of SelectListItems that makes no calls to database. Also it doesn’t have any additional logic involved behind the scenes.

Saving changes back to collection

When user saves data checkbox list is sent to controller as array of integers. We have to update some collection of business object and make sure we don’t confuse mapper anyhow. We have to add items that are currently missing from collection and remove items that are not needed anymore.

NB! It is easier to clear collection and add selected values back there but this is not safe thing to do. Mappers handle this move differently and you may introduce negative side effects in code this way. By example, mapper may delete all current event tags and insert them back again.

Now we have second place where we need base class to make collection updating functionality available for all business classes. For collection updating I wrote this extension method:


public static void UpdateCollectionFromModel<T>(this ICollection<T> domainCollection, 
IQueryable<T> objects, int[] newValues)
where T : BaseEntity
{
   
if (newValues == null
)
    {
        domainCollection.Clear();
       
return
;
    }

   
for (var
i = domainCollection.Count - 1; i >= 0; i--)
    {
       
var
domainObject = domainCollection.ElementAt(i);
       
if
(!newValues.Contains(domainObject.Id))
            domainCollection.Remove(domainObject);
    }

   
foreach (var newId in
newValues)
    {
       
var
domainObject = domainCollection.FirstOrDefault(t => t.Id == newId);
       
if (domainObject != null
)
           
continue
;

        domainObject = objects.FirstOrDefault(t => t.Id == newId);
       
if (domainObject == null
)
        {
           
continue;
        }

        domainCollection.Add(domainObject);
    }
}

This extension method takes collection we need to update, query of all objects that can be added to collection and selected id values from model. It uses these parameters to make safe updates to collection of business object.

Using checkbox list

Now let’s focus on how to use checkbox list. I’m using my old good event tags example for this. Both of these classes use BaseEntity as their base class.

Now let’s go through event editing flow in ASP.NET MVC application.

1. Display edit form

We display event in edit mode and provide view with model that has also list of tags. Some of these tags are selected because I selected some of these before.


[HttpGet]
public ActionResult Edit(int
id)
{
   
var
evt = Context.Events.FirstOrDefault(e => e.Id == id);
   
if (evt == null
)
       
return HttpNotFound("Cannot find event with id "
+ id);

   
var model = new AdminEventEditModel
();
    model.AllTags = evt.Tags.ToCheckBoxListSource(Context.Tags);
   
Mapper
.Map(evt, model);

   
return View(model);
}

2. Display checkbox list

In view we have to display checkbox list to user.


<div class="editor-label">
    @Html.LabelFor(model => model.TagIds)
</div
>
<
div class="editor-field">
    @Html.CheckBoxList("TagIds", Model.AllTags)
</div
>

This is just simple call to our extensions method.

3. Updating tags collection

When user saves event then selected tags are returned as array of integers. We need current event, tags collection and selected tags id-s to update tags collection of event.


[HttpPost]
public ActionResult Edit(AdminEventEditModel
model)
{
   
// ...
    var
evt = Context.Events.FirstOrDefault(e => e.Id == model.Id);
   
if (evt == null
)
       
return HttpNotFound("Cannot find event with id "
+ model.Id);

    evt.Tags.UpdateCollectionFromModel(Context.Tags, model.TagIds);
    Context.SaveChanges();
   
// ...
}

Now we have all flow of checkbox list covered.

Conclusion

I was not hard to build extension methods to implement checkbox list functionality. We had to use base class to write generic extension methods that cover whole domain. We also wrote method to generate data source for checkbox list, method to display it and method to update event tags collection later. We ended up with three extension methods – one for every step in flow. The solution is simple and common enough to use it in real projects.

Gunnar Peipman

Gunnar Peipman is ASP.NET, Azure and SharePoint fan, Estonian Microsoft user group leader, blogger, conference speaker, teacher, and tech maniac. Since 2008 he is Microsoft MVP specialized on ASP.NET.

    12 thoughts on “Better solution for ASP.NET MVC checkbox list

    • June 10, 2013 at 11:29 am
      Permalink

      Typo in code: output.Append(@” checked=””chekced”””);

    • June 10, 2013 at 3:47 pm
      Permalink

      Double quotes are allowed when string is prefixed with @

    • June 10, 2013 at 4:10 pm
      Permalink

      chekced should be checked.

    • June 10, 2013 at 4:37 pm
      Permalink

      Thanks for pointing out the problem, Henri! Hopefully you saved some time for many developers :)

    • June 14, 2013 at 9:30 am
      Permalink

      Thanks for good words, Cyrus! I already found some annoying dependencies in this solution and I have some ideas how to achieve even cleaner solution with less restrictions. I will blog about the new solution this month later, so stay tuned.

    • July 29, 2013 at 4:52 pm
      Permalink

      Could you post a simple working sample to download?

    • October 19, 2013 at 11:55 am
      Permalink

      thanks for that post, by checking your exasmple i found the bug in my own solution :-)

    • January 23, 2014 at 7:20 pm
      Permalink

      It helped a lot.

      Many thanks !

    • March 6, 2014 at 10:20 pm
      Permalink

      Is there a sample download?
      What I want to do is create a nested checkboxlist. Is this possible with your code?
      _ Category A
      __SubCategory AI
      __SubCategory AII
      __SubCategory AIII
      __Category B
      __SubCategory BI
      __SubCategory BII

    • March 7, 2014 at 3:37 pm
      Permalink

      Hi Gunnar, it’s so helpful post, but what is Context do you use in controller. I tried to implement the same and found two Contexts i can use: from Microsoft.Ajax.Utilities and from System.Runtime.Remoting.Contexts. Looks like your is different one. Please help. And yes, simple example will help more.

    • March 9, 2014 at 7:31 pm
      Permalink

      terrance, you can write overload for checkbox list method that accepts additional child nodes selector action. It’s the simplest way to go.

      Sergii, Context is instance of Entity Framework data context.

    Comments are closed.