Foreword
This article will consider the possibility of creating your own control and using it in the framework of a new project on ASP .NET MVC 3.0. Everything written below is the point of view of the author and may not coincide with the common or generally accepted methods of creating controls, therefore criticism and comments are welcome.
Introduction
Probably many users who previously worked on their projects with WinForms or ASP .NET WebForms, noticed that Html helpers in the ASP .NET MVC project do not provide the ability to create controls like CheckBoxList, which could be useful in complex filter forms data or multiple selection of non-structured data, whether in the profile about the user or when adding a new topic to Habrahabr. Of course, no one forbids the use of a single CheckBox or CheckBoxFor, but whether working with such a group of checkboxes is convenient, and the code is easily extensible, understandable to maintainer and protected from duplication, these are not the last questions for a programmer who plans to use his achievements in future projects . And if we take into account that we can add to our control some useful options for its visual display, the need to create it is becoming more and more obvious.
Before finally diving into the subject area, I would like to note that the idea of creating the control in question was inspired after viewing the site
http://mvc.devexpress.com , on which there are many beautiful and functional controls, some of them look altogether awesome. If this article is approved and met with positive reviews, then further review and implementation of the controls presented on the site within the cycle of articles for the community habr is possible.
Analysis
First of all, we need to understand what we are planning to do. The control will be represented by the CheckBox group, which allows you to display the data collections transferred to it, uses the Model Binding concept when sending the form to the server and has the following display settings:
- Layoutt - frame (in div or table);
- Direction- output direction (vertical / horizontal);
- RepeatColumns - number of columns;
- htmlAttributes are the attributes that will be applied to the CheckBox container.
Creating a project
Create an empty ASP .NET MVC 3 project and add a
DemoController controller with the
Index method and representation:
')



After the minimum preparations are made, go directly to the control.
Adding a CheckBoxList
Add the
Core folder to the root of the project with the
Controls.cs file and the
static Controls class in which the implementation of our CheckBoxList will be placed.

At the moment, in the
static class
Controls, we can already create our own implementations of Html helpers, so we will add the necessary amount of code for this:
public static MvcHtmlString CheckBoxList(this HtmlHelper helper) { return new MvcHtmlString("Hello, i'm your CheckBoxList!"); }
And we will try to call it in our
Index view, the
DemoController controller.
Note: I deliberately do not write in the text of the article about plugin assemblies and files with a scope, because I hope that you can do this without my mentioning. The only thing worth mentioning is adding the <add namespace = "MVC3Controls_H.Core" /> line to the /Views/web.config file, where MVC3Controls_H.Core is the location of your static class with CheckBoxList.To call our element, add the following line to
Index.cshtml (if you specified Razor Engine when adding a new view):
@Html.CheckBoxList()
and specify in the
Global.asax file, which is in the project root, that the
Demo controller should be called by default:
Index :
routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Demo", action = "Index", id = UrlParameter.Optional } // Parameter defaults );
If all actions have been done correctly, then you will see the following page:

Fine. Now we are going to implement the structure of our CheckBoxList.
Implementing the CheckBoxList Structure
First of all, let's add the settings for its display in the
Controls.cs file. In our case, these will be 3
enum objects:
public enum Layoutt { Table = 0, Flow = 1 } public enum Direction { Horizontal = 0, Vertical = 1 } public enum RepeatColumns { OneColumn = 1, TwoColumns = 2, ThreeColumns = 3, FourColumns = 4, FiveColumns = 5 }
And the
CheckBoxList_Settings class, in which we will pass them in a call to our control:
public class CheckBoxList_Settings { public string cbl_Name = "SelectedCheckBoxListItems"; public Layoutt cbl_Layout = Layoutt.Table; public Direction cbl_Direction = Direction.Horizontal; public RepeatColumns cbl_RepeatColumns = RepeatColumns.FiveColumns; }
As default settings, we will choose a frame-table, a direction of output-horizontally and a display in the form of five columns. The
cbl_Name property
will be discussed later.
We change the constructor of our CheckBoxList to accept a collection of type IDictionary <string, int> and an instance of the settings class:
public static MvcHtmlString CheckBoxList(this HtmlHelper helper, IDictionary<string, int> items, CheckBoxList_Settings settings) { return new MvcHtmlString("Hello, i'm your CheckBoxList!"); }
We came to the conclusion that in the view we now need not only to call our element, but also to transfer 2 parameters to it, for this we add the data collection method of our controller to the
Index :
Dictionary<string, int> languages = new Dictionary<string, int> { {"ActionScript", 0}, {"Delphi", 1}, {"GO", 2}, {"Lua", 3}, {"Prolog", 4}, {"Basic", 5}, {"Eiffel", 6}, {"Haskell", 7}, {"Objective-C", 8}, {"Python", 9}, {"C", 10}, {"Erlang", 11}, {"Java", 12}, {"Pascal", 13}, {"Ruby", 14}, {"C++", 15}, {"F#", 16}, {"JavaScript", 17}, {"Perl", 18}, {"Scala", 19}, {"C#", 20}, {"Fortran", 21}, {"Lisp", 22}, {"PHP", 23} };
And we will provide this data to our view as a model:
return View(languages);
In the code of our presentation, we indicate that the model for it is the data type Dictionary <string, int> and call our CheckBoxList:
@model Dictionary<string, int> @using MVC3Controls_H.Core @{ ViewBag.Title = "Index"; } <h2>Index</h2> @Html.CheckBoxList(Model, new CheckBoxList_Settings { cbl_Name = "SelectedCheckBoxListItems", cbl_Layout = Layoutt.Table, cbl_Direction = Direction.Horizontal, cbl_RepeatColumns = RepeatColumns.FiveColumns })
We start the project and see the same customary entry for us:

Analysis of the current structure of the application
Before proceeding further, we will need to deal with the current structure of the application and possibly change it. This is what I would like to focus on:
- At the moment, we are passing a dictionary <string, int> type to our view; however, in most cases, the application uses other data transmitted from the controller, whether it be meta tags (title, description) of the page or entity (registration, form order), - therefore we should not monopolize the model exclusively for our control. In addition, we will need to store and retrieve the values of our element selected by the user somewhere.
- Suppose that over time, in our CheckBoxList project, we will get more and more views and at some point, we will need to add a new parameter or modify existing ones. In this case, you will have to change all calls to our control (in all views of the project), and this is not very convenient.
From the above it follows that we need:
- Add a model for the view (ViewModel) and pass the collection for display in CheckBoxList'e as part of this model, along with other data.
- To transfer the call of our control from the view in partial view, so that in case of a change in the constructor or parameters, we need to change the call for all elements used in our project in just one place.
- Deliver the transferred values of CheckBoxList to a separate data model for our control and transfer it to our partial view in order to maximally isolate our views from changes to the element call.
Note: These and many other questions should be taken into account and asked yourself before you start writing application code, however, because This article is designed for those who are trying to do something similar for the first time, then I will try to consistently lead you to their solution by trial and error.Add view model
Since our presentation model serves not only to transfer data to it, but also to receive it, we will need to add, in addition to the transmitted collection, an array of selected elements. Add the
ViewModels folder to the
Models folder, then add the
ViewModel_Index.cs file to the
ViewModels as shown below:

and the code of our view model:
public class ViewModel_Index { public IDictionary<string, int> Languages { get; set; } public int[] SelectedCheckBoxListItems { get; set; } }
The index method of our
DemoController 'a will change as follows:
public ActionResult Index() { Dictionary<string, int> languages = new Dictionary<string, int> { {"ActionScript", 0}, {"Delphi", 1}, {"GO", 2}, {"Lua", 3}, {"Prolog", 4}, {"Basic", 5}, {"Eiffel", 6}, {"Haskell", 7}, {"Objective-C", 8}, {"Python", 9}, {"C", 10}, {"Erlang", 11}, {"Java", 12}, {"Pascal", 13}, {"Ruby", 14}, {"C++", 15}, {"F#", 16}, {"JavaScript", 17}, {"Perl", 18}, {"Scala", 19}, {"C#", 20}, {"Fortran", 21}, {"Lisp", 22}, {"PHP", 23} }; ViewModel_Index _ViewModel_Index = new ViewModel_Index { Languages = languages }; return View(_ViewModel_Index); }
Since we changed the transmitted model in the controller, it is also necessary to do this in the associated view:
@using MVC3Controls_H.Models.ViewModels @using MVC3Controls_H.Core @model ViewModel_Index @{ ViewBag.Title = "Index"; } <h2>Index</h2> @Html.CheckBoxList(Model.Languages, new CheckBoxList_Settings { cbl_Name = "SelectedCheckBoxListItems", cbl_Layout = Layoutt.Table, cbl_Direction = Direction.Horizontal, cbl_RepeatColumns = RepeatColumns.FiveColumns })
Thus, if you need, in addition to the collection of data for our control, to transfer something else to the page, you can do it by simply changing the view model
/Models/ViewModels/ViewModels_Index.cs (adding new properties to it) and adding their initialization in the controller method.
Encapsulating a call to our CheckBoxList
In my
first article , I already used a similar scheme for calling the extension method, but for several other purposes. In the current project, in order to make a call to our CheckBoxList in Partial View, we will add the
Partial folder, the
Controls folder to it, and the new partial view
CheckBoxList.cshtml in the
Controls folder:

Now the structure of our application is as follows:

Change our
Index view by calling our Partial View instead of CheckBoxList:
@using MVC3Controls_H.Models.ViewModels @using MVC3Controls_H.Core @model ViewModel_Index @{ ViewBag.Title = "Index"; } <h2>Index</h2> @Html.Partial("~/Views/Partial/Controls/CheckBoxList.cshtml")
Adding a data model for CheckBoxList
Since we plan to transfer the call of our control to our Partial View, we also need to transfer data for it there too - therefore, we need to add a data model for it. In the
Models folder, create a new
CheckBoxList folder, add the
CheckBoxList_Model.cs class to it:
public class CheckBoxList_Model { public IDictionary<string, int> items; public CheckBoxList_Settings settings; }
Let's change the Partial View call in our
Index view, passing the added model as data:
@Html.Partial("~/Views/Partial/Controls/CheckBoxList.cshtml", new CheckBoxList_Model { items = Model.Languages, settings = new CheckBoxList_Settings { cbl_Name = "SelectedCheckBoxListItems", cbl_Layout = Layoutt.Table, cbl_Direction = Direction.Horizontal, cbl_RepeatColumns = RepeatColumns.FiveColumns } })
and define this model as the model for our Partial View in
/Views/Partial/Controls/CheckBoxList.cshtml , calling and passing values to our control:
@model CheckBoxList_Model @Html.CheckBoxList(Model.items, Model.settings)
This completes the structure of the application. Consider its main points:

The good news is that we are finally done with the structure of our application, and the bad, we haven't written much about our CheckBoxList yet.
CheckBoxList implementation
To begin with, we describe the general algorithm for constructing our element:
1. Selection of a framing HTML tag (table, div) based on the
cbl_Layout parameter.
2. Determination of the number of iterations of the collection based on the
cbl_RepeatColumns parameter and the number of records in the collection.
3. Formation of the condition for selecting elements from the collection based on the sequence number of the current iteration:
3.1. If the output is carried out horizontally, and the number of columns = 4, then the first row should consist of the first 4 elements of the collection going in order, the second of the following 4, and so on:
[0 1 2 3] - 0 [4 5 6 7] - 1 [8 9 10 11]
3.2. If the output is carried out vertically, the number of columns = 4 and the total of elements 24, then the first line should consist (!) Of every sixth element of the collection, the second of iteration number + 6, etc .:
[0 6 12 18] - 0 [1 7 13 19] - 1 [2 8 14 20] [3 9 15 21] [4 10 16 22] [5 11 17 23]
3.3. Do not forget when calculating the number of iterations (number of elements / number of columns) about the remainder of the division! = 0. In this case, we need extra iteration to display the entire collection.
[0 1 2 3] - 0 [4 5 6 7] - 1 [8 9 10 11] [12 13 - -]
4. Forming a string of suitable for the condition of the sample at each iteration, depending on the framing tag (div, table):
4.1. For a div, this will only be hyphenated at the end of the line.
4.2. For table, this is:
4.2.1. at the beginning of the line. label in the process of sorting items. at the end of the line.
5. Close the framing tag and return the generated string to the view.
Since the main idea of the article is not the code and algorithms, I will give only a part of the functions - you can see the rest by downloading the project at the end of the article, stopping perhaps a bit more at the formation of the CheckBox markup:
Generating HTML CheckBox and Model Binding markup
In general terms, this is the simplest code to which an element of the collection and settings of our control is passed. Perhaps the main thing in this code is that the name attribute is assigned the name of our entire CheckBoxList. This is done so that in the future, when retrieving data on the server side in our
Index method of the
DemoController controller
, we can get the selected values in the form of a formed array - I will show this later with an example later.
public static string GenerateHtmlMarkup_CheckBox(KeyValuePair<string, int> item, CheckBoxList_Settings settings) { TagBuilder tagBuilder = new TagBuilder("input"); tagBuilder.MergeAttribute("type", "checkbox"); tagBuilder.MergeAttribute("name", settings.cbl_Name); tagBuilder.MergeAttribute("value", item.Value.ToString()); return tagBuilder.ToString(TagRenderMode.SelfClosing); }
HTML Label Formation
public static string GenerateHtmlMarkup_Label(KeyValuePair<string, int> item) { TagBuilder tagBuilder = new TagBuilder("label"); tagBuilder.SetInnerText(item.Key); return tagBuilder.ToString(TagRenderMode.Normal); }
Calculating the number of iterations:
int iMod = items.Count % (int)settings.cbl_RepeatColumns; int iterationsCount = items.Count / (int)settings.cbl_RepeatColumns + (iMod == 0 ? 0 : 1);
Selection condition for each iteration:
foreach (KeyValuePair<string, int> item in items.Where((item, index) => settings.cbl_Direction == Direction.Horizontal ? index / (int)settings.cbl_RepeatColumns == i : (index - i) % iterationsCount == 0))
where i is the iteration number.
When outputting horizontally, we take elements whose indices when divided by the number of columns == i:
[0 1 2 3] [4 5 6 7] [8 9 10 11]
When outputting vertically, we take elements, the remainder of dividing the index of which and the current iteration by the number of iterations == 0:
[0 6 12 18] [1 7 13 19]
Health check
Let's take a look at the 3 CheckBoxLists as an example, what we got in the end:
<h2>Index</h2> @using (Html.BeginForm()) { @Html.Partial("~/Views/Partial/Controls/CheckBoxList.cshtml", new CheckBoxList_Model { items = Model.Languages, settings = new CheckBoxList_Settings { cbl_Name = "SelectedCheckBoxListItems", cbl_Layout = Layoutt.Table, cbl_Direction = Direction.Horizontal, cbl_RepeatColumns = RepeatColumns.FiveColumns }, htmlAttributes = new { @cellpadding = "0", @cellspacing = "0" } }) <br /> @Html.Partial("~/Views/Partial/Controls/CheckBoxList.cshtml", new CheckBoxList_Model { items = Model.Languages, settings = new CheckBoxList_Settings { cbl_Name = "SelectedCheckBoxListItemsTwo", cbl_Layout = Layoutt.Flow, cbl_Direction = Direction.Vertical, cbl_RepeatColumns = RepeatColumns.ThreeColumns } }) <br /> @Html.Partial("~/Views/Partial/Controls/CheckBoxList.cshtml", new CheckBoxList_Model { items = Model.Languages, settings = new CheckBoxList_Settings { cbl_Name = "SelectedCheckBoxListItemsThree", cbl_Layout = Layoutt.Flow, cbl_Direction = Direction.Horizontal, cbl_RepeatColumns = RepeatColumns.TwoColumns } }) <br /> <input type="submit" value="send" /> }

In order to get the selected values, we need to add 3 arrays with the names that we pass to our CheckBoxLists in the model of our
ViewModel_Index :
public class ViewModel_Index { public IDictionary<string, int> Languages { get; set; } public int[] SelectedCheckBoxListItems { get; set; } public int[] SelectedCheckBoxListItemsTwo { get; set; } public int[] SelectedCheckBoxListItemsThree { get; set; } }
and also add the
Index controller method that handles post requests:
[HttpPost] public ActionResult Index(ViewModel_Index model) {
Putting a breakpoint, look at the result:

If you are absolutely indifferent to duplication of code, model of representations and maintenance, then you can also just call CheckBoxList directly in the view and get the selected values on the server directly in the array (however, you should not forget that the name CheckBoxList and the accepted parameter on the server should be the same):



Well, now that we have completely finished what we planned, the last remains. Consider expanding our control with new properties on the example of
object htmlAttributes .
Extend CheckBoxList
In order to pass a new property to the code of our element, we have to go from the constructor to the initial call (or vice versa, as someone like more). It all starts with our constructor in
Controls.cs - we add a new parameter object htmlAttributes:
public static MvcHtmlString CheckBoxList(this HtmlHelper helper, IDictionary<string, int> items, CheckBoxList_Settings settings, object htmlAttributes)
Our constructor is called in Partial View with the name
CheckBoxList.cshtml . Add a new property for the CheckBoxList model:
public class CheckBoxList_Model { public IDictionary<string, int> items; public CheckBoxList_Settings settings; public object htmlAttributes; }
And pass it when called in our Partial View:
Before:
@model CheckBoxList_Model @Html.CheckBoxList(Model.items, Model.settings)
After:
@model CheckBoxList_Model @Html.CheckBoxList(Model.items, Model.settings, Model.htmlAttributes)
It remains to add the property initialization itself in the
Index view and add the code of our element:
@Html.Partial("~/Views/Partial/Controls/CheckBoxList.cshtml", new CheckBoxList_Model { items = Model.Languages, settings = new CheckBoxList_Settings { cbl_Name = "SelectedCheckBoxListItemsThree", cbl_Layout = Layoutt.Table, cbl_Direction = Direction.Vertical, cbl_RepeatColumns = RepeatColumns.FourColumns }, htmlAttributes = new { @border = "3", style = "color: Grey; border-style:dashed;" } })
Frame selection code (table, div):
public static TagBuilder GenerateHtmlMarkup_OuterTag(Layoutt cbl_Layout, IDictionary<string, object> htmlAttributes) { ... TagBuilder tagBuilder = new TagBuilder(htmlTag); tagBuilder.MergeAttributes(htmlAttributes); ... }
As you can see below, everything works:

Conclusion
As a conclusion, I would like to understand what we did after all:
- We considered the possibility of creating, using and extending our own Html-based control of ASP .NET MVC helpers;
- Created a project framework for further development of controls;
- Implemented the structure of a web application with the division of responsibilities of each functional unit;
- We learned about the site, with which there is something to compete when writing your own controls.
List of materials
1.
http://mvc.devexpress.com/Editors/CheckBoxList2.
How to handle checkboxes in ASP.NET MVC forms3.
Project Source Code