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之路- 點部落

 

以上

沒有留言:

張貼留言

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。