網頁

2014年5月13日 星期二

ASP.NET MVC - CheckBoxList 與 ValidationMessage (ASP.NET MVC 5 with Bootstrap3)

在上個月,有位網友在一篇文章下面寫了這樣的留言,留言的文章是「ASP.NET MVC - 修改 CheckBoxList、增加 RadioButtonList」,

image

因為從四月一直到五月初,我都一直在瘋狂忙碌的狀態,除了幾篇簡短的練習題文章之外,我實在抽不出時間來好好研究這個問題,所以一直延宕到現在才有時間來研究一下這個問題。

這篇文章就來跟大家說明怎麼讓 CheckBoxList 也可以有 Validation 的功能。

 


CheckBoxList 的相關文章:

ASP.NET MVC 擴充HtmlHelper 加入 CheckBoxList 功能 – 1

ASP.NET MVC 擴充HtmlHelper 加入 CheckBoxList 功能 - 2

jQuery 取得CheckBoxList裡項目有被選取(Checked=true)的值

ASP.NET MVC - 修改 CheckBoxList、增加 RadioButtonLis

 

我稍微看了一下我之前所寫的 CheckBoxList 與 RadioButtonList,發現到還真的是沒有對前端驗證有做任何的支援,所以就著手進行修改,不過並無法作到像內建的 Html Helper 所能達到的效果,內建的 Html Helper 只需要在 Model 類別裡對 Property 增加 DataAnnotation Attrinbute 就可以在頁面上有驗證的功能與效果,而礙於原本的 CheckBoxList 的限制,所以我盡量作到讓開發人員在 Model 定義時可以不用改變原本使用 DataAnnotation Attribute 的方式,另外在檢視頁面裡的使用也不需要有太大的改變。

 

開發環境:Visual Studio 2013 Update 2, ASP.NET MVC 5.1.2, EntityFramework 6.1.0

在 Visual Studio 2012 使用 ASP.NET MVC 4 與 EF 5.0 同樣可以使用。

 

首先,我定義了一個 「Foo.cs」Model

image

 

建立 FooController.cs 與內容,那個 CategoryService 不是什麼了不起的東西,就只是一個取得分類資料的類別,

image

 

建立 View (~/Views/Foo/Index.cshtml)

image

一個基本的頁面

image

這個基本頁面是有驗證與顯示驗證訊息的功能

image

接著就是要來修改頁面上的分類,要修改為 CheckBoxList,不過這次不會直接把 CheckBoxList 直接用在 Index.cshtml 裡,而是另外建立 EditorTemplate,如此一來就可以在 EditorFor 這個 Html Helper 裡指定 EditorTemplate 與資料,原本頁面不需要做大幅度的變動,而且之後還可以重複使用。

 

在建立 CheckBoxList 的 EditorTemplate 之前,先來建立 CheckBoxList 的相關程式,

image

Position.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
 
namespace WebApplication1.Infrastructure.Enums
{
    public enum Position
    {
        Horizontal,
        Vertical
    }
}

CheckBoxListExtensions.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using WebApplication1.Infrastructure.Enums;
 
namespace WebApplication1.Infrastructure.Extensions
{
    public static class CheckBoxListExtensions
    {
        #region -- CheckBoxList (Horizontal) --
        /// <summary>
        /// CheckBoxList.
        /// </summary>
        /// <param name="htmlHelper">The HTML helper.</param>
        /// <param name="name">The name.</param>
        /// <param name="listInfo">SelectListItem.</param>
        /// <returns></returns>
        public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper,
            string name,
            IEnumerable<SelectListItem> listInfo)
        {
            return htmlHelper.CheckBoxList(name, listInfo, (IDictionary<string, object>)null, 0);
        }
 
        /// <summary>
        /// CheckBoxList.
        /// </summary>
        /// <param name="htmlHelper">The HTML helper.</param>
        /// <param name="name">The name.</param>
        /// <param name="listInfo">SelectListItem.</param>
        /// <param name="htmlAttributes">The HTML attributes.</param>
        /// <returns></returns>
        public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper,
            string name,
            IEnumerable<SelectListItem> listInfo,
            object htmlAttributes)
        {
            return htmlHelper.CheckBoxList(
                name, 
                listInfo, 
                (IDictionary<string, object>)new RouteValueDictionary(htmlAttributes), 
                0);
        }
 
        /// <summary>
        /// CheckBoxList.
        /// </summary>
        /// <param name="htmlHelper">The HTML helper.</param>
        /// <param name="name">The name.</param>
        /// <param name="listInfo">The list info.</param>
        /// <param name="htmlAttributes">The HTML attributes.</param>
        /// <param name="number">每個Row的顯示個數.</param>
        /// <returns></returns>
        public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper,
            string name,
            IEnumerable<SelectListItem> listInfo,
            IDictionary<string, object> htmlAttributes,
            int number)
        {
            if (String.IsNullOrEmpty(name))
            {
                throw new ArgumentException("必須給這些 CheckBoxList 一個 Tag Name", "name");
            }
            if (listInfo == null)
            {
                throw new ArgumentNullException("listInfo", "必須要給List<SelectListItem> listInfo");
            }
            if (!listInfo.Any())
            {
                throw new ArgumentException("List<SelectListItem> listInfo 至少要有一組資料", "listInfo");
            }
 
            var sb = new StringBuilder();
            var lineNumber = 0;
 
            foreach (SelectListItem info in listInfo)
            {
                lineNumber++;
 
                TagBuilder builder = new TagBuilder("input");
                if (info.Selected)
                {
                    builder.MergeAttribute("checked", "checked");
                }
                builder.MergeAttributes<string, object>(htmlAttributes);
                builder.MergeAttribute("type", "checkbox");
                builder.MergeAttribute("value", info.Value);
                builder.MergeAttribute("name", name);
                builder.MergeAttribute("id", string.Format("{0}_{1}", name, info.Value));
                sb.Append(builder.ToString(TagRenderMode.Normal));
 
                var labelBuilder = new TagBuilder("label");
                labelBuilder.MergeAttribute("for", string.Format("{0}_{1}", name, info.Value));
                labelBuilder.InnerHtml = info.Text;
                sb.Append(labelBuilder.ToString(TagRenderMode.Normal));
 
                if (number == 0 || (lineNumber % number == 0))
                {
                    sb.Append("<br />");
                }
            }
            return MvcHtmlString.Create(sb.ToString());
        }
        #endregion
 
        #region -- CheckBoxListVertical --
        /// <summary>
        /// Checks the box list vertical.
        /// </summary>
        /// <param name="htmlHelper">The HTML helper.</param>
        /// <param name="name">The name.</param>
        /// <param name="listInfo">The list info.</param>
        /// <param name="htmlAttributes">The HTML attributes.</param>
        /// <param name="columnNumber">The column number.</param>
        /// <returns></returns>
        public static MvcHtmlString CheckBoxListVertical(this HtmlHelper htmlHelper,
            string name,
            IEnumerable<SelectListItem> listInfo,
            IDictionary<string, object> htmlAttributes,
            int columnNumber = 1)
        {
            if (String.IsNullOrEmpty(name))
            {
                throw new ArgumentException("必須給這些 CheckBoxList 一個 Tag Name", "name");
            }
            if (listInfo == null)
            {
                throw new ArgumentNullException("listInfo", "必須要給 List<CheckBoxListInfo> listInfo");
            }
            var selectListItems = listInfo as SelectListItem[] ?? listInfo.ToArray();
            if (!selectListItems.Any())
            {
                throw new ArgumentException("List<CheckBoxListInfo> listInfo 至少要有一組資料", "listInfo");
            }
 
            var dataCount = selectListItems.Count();
 
            // calculate number of rows
            var rows = Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(dataCount) / Convert.ToDecimal(columnNumber)));
            if (dataCount <= columnNumber || dataCount - columnNumber == 1)
            {
                rows = dataCount;
            }
 
            var wrapBuilder = new TagBuilder("div");
            wrapBuilder.MergeAttribute("style", "float: left; light-height: 25px; padding-right: 5px;");
 
            var wrapStart = wrapBuilder.ToString(TagRenderMode.StartTag);
            var wrapClose = string.Concat(wrapBuilder.ToString(TagRenderMode.EndTag), " <div style=\"clear:both;\"></div>");
            var wrapBreak = string.Concat("</div>", wrapBuilder.ToString(TagRenderMode.StartTag));
 
            var sb = new StringBuilder();
            sb.Append(wrapStart);
 
            var lineNumber = 0;
 
            foreach (var info in selectListItems)
            {
                var builder = new TagBuilder("input");
                if (info.Selected)
                {
                    builder.MergeAttribute("checked", "checked");
                }
                builder.MergeAttributes<string, object>(htmlAttributes);
                builder.MergeAttribute("type", "checkbox");
                builder.MergeAttribute("value", info.Value);
                builder.MergeAttribute("name", name);
                builder.MergeAttribute("id", string.Format("{0}_{1}", name, info.Value));
                sb.Append(builder.ToString(TagRenderMode.Normal));
 
                var labelBuilder = new TagBuilder("label");
                labelBuilder.MergeAttribute("for", string.Format("{0}_{1}", name, info.Value));
                labelBuilder.InnerHtml = info.Text;
                sb.Append(labelBuilder.ToString(TagRenderMode.Normal));
 
                lineNumber++;
 
                if (lineNumber.Equals(rows))
                {
                    sb.Append(wrapBreak);
                    lineNumber = 0;
                }
                else
                {
                    sb.Append("<br/>");
                }
            }
            sb.Append(wrapClose);
            return MvcHtmlString.Create(sb.ToString());
        }
        #endregion
 
        #region -- CheckBoxList (Horizonal, Vertical) --
        /// <summary>
        /// Checks the box list.
        /// </summary>
        /// <param name="htmlHelper">The HTML helper.</param>
        /// <param name="name">The name.</param>
        /// <param name="listInfo">The list info.</param>
        /// <param name="htmlAttributes">The HTML attributes.</param>
        /// <param name="position">The position.</param>
        /// <param name="number">Position.Horizontal則表示每個Row的顯示個數, Position.Vertical則表示要顯示幾個Column</param>
        /// <returns></returns>
        public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper,
            string name,
            IEnumerable<SelectListItem> listInfo,
            IDictionary<string, object> htmlAttributes,
            Position position = Position.Horizontal,
            int number = 0)
        {
            if (String.IsNullOrEmpty(name))
            {
                throw new ArgumentException("必須給這些 CheckBoxList 一個 Tag Name", "name");
            }
            if (listInfo == null)
            {
                throw new ArgumentNullException("listInfo", "必須要給List<SelectListItem> listInfo");
            }
            var selectListItems = listInfo as SelectListItem[] ?? listInfo.ToArray();
            if (!selectListItems.Any())
            {
                throw new ArgumentException("List<SelectListItem> listInfo 至少要有一組資料", "listInfo");
            }
 
            var sb = new StringBuilder();
            var lineNumber = 0;
 
            switch (position)
            {
                case Position.Horizontal:
 
                    foreach (var info in selectListItems)
                    {
                        lineNumber++;
                        sb.Append(CreateCheckBoxItem(info, name, htmlAttributes));
 
                        if (number == 0 || (lineNumber % number == 0))
                        {
                            sb.Append("<br />");
                        }
                    }
                    break;
 
                case Position.Vertical:
 
                    var dataCount = selectListItems.Count();
 
                    // 計算最大顯示的列數(rows)
                    var rows = Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(dataCount) / Convert.ToDecimal(number)));
                    if (dataCount <= number || dataCount - number == 1)
                    {
                        rows = dataCount;
                    }
 
                    var wrapBuilder = new TagBuilder("div");
                    wrapBuilder.MergeAttribute("style", "float: left; light-height: 25px; padding-right: 5px;");
 
                    var wrapStart = wrapBuilder.ToString(TagRenderMode.StartTag);
                    var wrapClose = string.Concat(wrapBuilder.ToString(TagRenderMode.EndTag), " <div style=\"clear:both;\"></div>");
                    var wrapBreak = string.Concat("</div>", wrapBuilder.ToString(TagRenderMode.StartTag));
 
                    sb.Append(wrapStart);
 
                    foreach (var info in selectListItems)
                    {
                        lineNumber++;
                        sb.Append(CreateCheckBoxItem(info, name, htmlAttributes));
 
                        if (lineNumber.Equals(rows))
                        {
                            sb.Append(wrapBreak);
                            lineNumber = 0;
                        }
                        else
                        {
                            sb.Append("<br/>");
                        }
                    }
                    sb.Append(wrapClose);
                    break;
            }
 
            return MvcHtmlString.Create(sb.ToString());
        }
 
        /// <summary>
        /// Creates the check box item.
        /// </summary>
        /// <param name="info">The info.</param>
        /// <param name="name">The name.</param>
        /// <param name="htmlAttributes">The HTML attributes.</param>
        /// <returns></returns>
        internal static string CreateCheckBoxItem(
            SelectListItem info, 
            string name, 
            IDictionary<string, object> htmlAttributes)
        {
            var sb = new StringBuilder();
 
            var labelBuilder = new TagBuilder("label");
            labelBuilder.MergeAttribute("for", string.Format("{0}_{1}", name, info.Value));
            sb.Append(labelBuilder.ToString(TagRenderMode.StartTag));
 
            var builder = new TagBuilder("input");
            if (info.Selected)
            {
                builder.MergeAttribute("checked", "checked");
            }
            builder.MergeAttributes<string, object>(htmlAttributes);
            builder.MergeAttribute("type", "checkbox");
            builder.MergeAttribute("value", info.Value);
            builder.MergeAttribute("name", name);
            builder.MergeAttribute("id", string.Format("{0}_{1}", name, info.Value));
            sb.Append(builder.ToString(TagRenderMode.Normal));
 
            sb.AppendFormat(" {0}",info.Text);
            sb.Append("</label>");
 
            return sb.ToString();
        }
        #endregion
    }
 
}

 

建立 ~/Views/Shared/EditorTemplates/CheckBoxList.cshtml

@using WebApplication1.Infrastructure.Enums
@using WebApplication1.Infrastructure.Extensions
@{
    var require = false;
    object validationMessage = string.Empty;
 
    var validationAttributes = Html.GetUnobtrusiveValidationAttributes("");
    if (validationAttributes.ContainsKey("data-val")
        &&
        validationAttributes.ContainsKey("data-val-required"))
    {
        require = true;
        if (!validationAttributes.TryGetValue("data-val-required", out validationMessage))
        {
            validationMessage = "This field is required.";
        }
        validationAttributes.Add("required", "required");
    }
 
    var tagName = ViewData["TagName"] == null
        ? "CheckBoxList"
        : (string)ViewData["TagName"];
 
    var checkboxItems = ViewData["CheckBoxItems"] == null
        ? new List<SelectListItem>()
        : (IEnumerable<SelectListItem>)ViewData["CheckBoxItems"];
 
    var position = ViewData["Position"] == null
        ? Position.Horizontal
        : (Position)ViewData["Position"];
 
    var numbers = 0;
    if (ViewData["Numbers"] == null)
    {
        numbers = 1;
    }
    else if (!int.TryParse(ViewData["Numbers"].ToString(), out numbers))
    {
        numbers = 1;
    }
}
 
@Html.CheckBoxList(
    tagName,
    checkboxItems,
    new RouteValueDictionary(validationAttributes),
    position,
    numbers)
 
@Html.ValidationMessage(tagName, 
    "", 
    new
    {
        @class = "text-danger", 
        data_valmsg_for = tagName
    })

在 CheckBoxList.cshtml 裡,一開始要先抓出放在 Model 類別裡標示在 Property 的 DataAnnotation Attributes 資訊,當需要驗證以及標示為必選的時候,就必須在 CheckBox 裡再加入 required 的 Html Attribute,並且還要抓出必選條件的 ErrorMessage 內容。

然後有定義四個參數是從 ViewData 裡取得資料,分別是 TagName, CheckBoxItems, Position, Numbers,因為使用了 EditorTemplate 後,要傳送資料進去就要以下的多載方法(下面的程式),將資料放到 additionalViewdata 裡,然後 CheckBoxList.cshtml 再從 ViewData 拿到資料。

public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, object additionalViewData)
{
    return html.TemplateFor<TModel, TValue>(expression, templateName, null, DataBoundControlMode.Edit, additionalViewData);
}

image

而 CheckBoxList.cshtml 最後所使用的  Html.ValidationMessage() 需要特別去指定 data_valmsg_for 的名稱,如果不指定的話,將會 Redenr 為「Categories.Categories」,如此一來就會無法正確的顯示驗證訊息,另外就是 ValidationMessage 的訊息內容也設定為空字串,如果有設定的話,將會直接就顯示在頁面上,所以就要設定為空字串,之後再到 Index.cshtml 裡另外使用前端程式去加入驗證訊息。

 

修改 Index.cshtml

以下是 ~/Views/Foo/Index.cshtml 的全部內容

@using WebApplication1.Infrastructure.Enums
@model WebApplication1.Models.ViewModels.Foo
 
@{
    ViewBag.Title = "Index";
}
 
<h2>Index</h2>
 
 
@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
 
    <div class="form-horizontal">
        <h4>Foo</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
            </div>
        </div>
 
        <div class="form-group">
            @Html.LabelFor(model => model.Categories, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @*@Html.EditorFor(model => model.Categories, new { htmlAttributes = new { @class = "form-control" } })*@
                @*@Html.ValidationMessageFor(model => model.Categories, "", new { @class = "text-danger" })*@
 
                @Html.EditorFor(model => model.Categories,
                    "CheckBoxList",
                    new
                    {
                        TagName = "Categories",
                        CheckBoxItems = ViewBag.CategoryItems,
                        Position = Position.Vertical,
                        Numbers = 3
                    })  
            </div>
        </div>
 
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}
 
<div>
    @Html.ActionLink("Back to List", "Index")
</div>
 
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
    <script type="text/javascript">
        $(function () {
            $('input[name="Categories"]').rules('add', {
                messages: {
                    required: "請至少選擇一個分類"
                }
            });
        });
    </script>
}

這邊先看「分類」的部份,是在 Html.EditorFor() 裡指定使用 EditorTemplate「CheckBoxList.cshtml」,第二個參數是指定要使用那一個 EditorTemplate,第三個參數就是「additionalViewdata 」分別指定要傳送到 CheckBoxList.cshtml 裡所要用的資料,

image

再來就是前端程式的部份,

image

因為無法使用 ASP.NET MVC 內建的方法直接對 CheckBoxList 產生驗證,所以就在頁面的前端程式加入驗證訊息。

 

執行結果

一開始的執行頁面

image

什麼都不填、不勾選,直接按下「Create」

image

在分類項目裡勾選一個

image

以下為完整的操作過程

20140513_CheckBoxList_MVC5_Bootstrap3

 

以上就是在 ASP.NET MVC 5 與 Bootstrap 3 的情況下設定 CheckBoxList 有前端驗證的操作處理,下一篇文章再來看看 ASP.NET MVC 4 以及沒有使用 Bootstrap 情況下又該如何處理,等下一篇文章寫好之後再將程式放到 GitHub 上面讓大家參考。


 

參考連結

ASP.NET MVC學習筆記(十一)-超好用的Templates - 我的Coding之路- 點部落

 

以上

沒有留言:

張貼留言