在前面的几篇文章中,已经在控制台和界面实现了属性值的笛卡尔乘积,这是商品模块中的一个难点。本篇就来实现在ASP.NET MVC4下商品模块的一个小样。与本篇相关的文章包括:
1、ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积01, 在控制台实现
2、ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现
3、再议ASP.NET MVC中CheckBoxList的验证
4、ASP.NET MVC在服务端把异步上传的图片裁剪成不同尺寸分别保存,并设置上传目录的尺寸限制
5、ASP.NET MVC异步验证是如何工作的01,jQuery的验证方式、错误信息提示、validate方法的背后
6、ASP.NET MVC异步验证是如何工作的02,异步验证表单元素的创建
7、ASP.NET MVC异步验证是如何工作的03,jquery.validate.unobtrusive.js是如何工作的
8、MVC批量更新,可验证并解决集合元素不连续控制器接收不完全的问题
9、MVC扩展生成CheckBoxList并水平排列
本篇主要包括:
□ 商品模块小样简介
□ 领域模型和视图模型
□ 控制器和视图实现
商品模块小样简介
※ 界面
○ 类别区域,用来显示产品类别,点击选择某个类别,在"产品属性"区域出现该类别下的所有属性,以及属性值,对于单选的属性值用Select显示,对于多选的属性值用CheckBoxList显示。
○ 产品描述,表示数据库中产品表中的字段,当然实际情况中,这里的字段更多,比如上传时间,是否通过,产品卖点,等等。
○ 产品属性,只有点击选择产品类别,这里才会显示
○ 定价按钮,点击这个按钮,如果"产品属性"区域中有CheckBoxList项,"产品SKU与定价"区域会出现关于属性值、产品价格的SKU组合项;如果"产品属性"区域中没有CheckBoxList项,"产品SKU与定价"区域只出现一个有关价格的input元素。另外,每次点击定价按钮,出现提交按钮,定价按钮隐藏。
○ 产品SKU与定价:这里要么呈现属性值、价格的SKU项,要么只出现一个有关价格的input元素
※ 点击类别项,在"产品属性"区域包括CheckBoxList
○ 点击类名中的"家电"选项,在"产品属性"区域中出现属性及其值,有些属性值以Select呈现,有些属性值以CheckBoxList呈现
○ 点击属性行后面的"删除行"直接删除属性行
※ 点击类别项,在"产品属性"区域包括CheckBoxList,点击"定价"按钮
点击"定价"按钮,如果每组的CheckBoxList中没有一项被选中,会在属性行后面出现错误提示。在"产品SKU与定价"区域不会出现内容。
※ 点击类别项,在"产品属性"区域包括CheckBoxList,点击"定价"按钮,再点击CheckBoxList选项,某些错误提示消失
点击CheckBoxList中的某项,该属性行后面的错误提示消失。在"产品SKU与定价"区域还是不会出现内容。
※ 点击类别项,在"产品属性"区域包括CheckBoxList,如果所有的CheckBoxList至少有一项被选中,点击"定价"按钮
○ 会把所有的选中属性值进行笛卡尔乘积显示到"产品SKU与定价"区域
○ 出现"提交"按钮
○ 如果有关价格的input验证不通过会出现异步验证错误信息
○ 与有关价格的input一起渲染的还有一个隐藏域,用来存放该SKU项的属性值Id,以便和价格一起被保存到数据库
※ 点击类别项,在"产品属性"区域不包括CheckBoxList
当选择类别中的"家具"项,在"产品属性"区域中的属性值只是以Select来呈现。
※ 点击类别项,在"产品属性"区域不包括CheckBoxList,点击"定价"按钮
如果"产品属性"区域中只有Select元素,点击"定价"按钮,在"产品SKU与定价"区域只出现有关价格的input,并且带异步验证,同时还出现提交按钮。
※ 在控制器提交产品的方法中打断点,点击"提交"按钮
在界面提交的包括:
在控制器方法中收到了所有的提交:
领域模型和视图模型
有关产品类别的领域模型:
public class Category{public int Id { get; set; }public string Name { get; set; }}
有关属性的领域模型:
public class Prop{public int Id { get; set; }public string Name { get; set; }public int CategoryId { get; set; }public short InputType { get; set; }public Category Category { get; set; }}
以上,InputType
属性对应InputTypeEnum
的枚举项,会依据此属性加载不同的视图(Select或CheckBoxList)。
public enum InputTypeEnum{//下拉选框PropDropDownList = 0,//复选框PropCheckBoxList = 1}
有关属性值的领域模型:
public class PropOption{public int Id { get; set; }public string RealValue { get; set; }public int PropId { get; set; }public Prop Prop { get; set; }}
在产品提交页,和产品有关包括:产品类别、产品本身的描述、属性及属性值(属性值有些以Select显示,有些以CheckBoxList显示)、属性值和价格的SKU组合项。提炼出有关产品的一个视图模型:
public class ProductVm{public ProductVm(){this.PropOptionDs = new List<PropOptionVmD>();this.ProductSKUs = new List<ProductSKUVm>();this.PropOptionCs = new List<PropOptionVmC>();}public int Id { get; set; }[Required(ErrorMessage = "必填")]public int CategoryId { get; set; }[Required(ErrorMessage = "必填")][Display(Name = "产品编号")][MaxLength(10, ErrorMessage = "最大长度10")]public string Code { get; set; }[Required(ErrorMessage = "必填")][Display(Name = "产品名称")][MaxLength(10, ErrorMessage = "最大长度10")]public string Name { get; set; }public List<PropOptionVmD> PropOptionDs { get; set; }public List<PropOptionVmC> PropOptionCs { get; set; }public List<ProductSKUVm> ProductSKUs { get; set; }}
以上,
○ PropOptionDs
表示以Select显示属性值的、有关属性和属性值的集合
○ PropOptionCs
表示以CheckBoxList显示属性值的、有关属性和属性值的集合
○ ProductSKUs
表示SKU项的集合
PropOptionVmD
视图模型用来显示每一个属性名,该属性下的属性值是以Select呈现:
public class PropOptionVmD{public int Id { get; set; }public int PropId { get; set; }public string PropName { get; set; }[Required(ErrorMessage = "必填")]public int PropOptionId { get; set; }}
以上,
○ PropId
用来表示属性Id,在界面中是以隐藏域存在的,会被传给服务端
○ PropName
表示属性名,在界面中显示属性的名称
○ PropOptionId
表示界面中被选中的属性值Id
PropOptionVmC
视图模型也用来显示每一个属性名,该属性下的属性值以CheckBoxList呈现:
public class PropOptionVmC{public int Id { get; set; }public int PropId { get; set; }public string PropName { get; set; }public string PropOptionIds { get; set; }}
ProductSKUVm
视图模型用来显示SKU项中的价格部分:
public class ProductSKUVm{[Display(Name = "价格")][Required(ErrorMessage = "必填")][Range(typeof(Decimal), "0", "9999", ErrorMessage = "{0} 必须是数字介于 {1} 和 {2}之间.")]public decimal Price { get; set; }public string OptionIds { get; set; }}
以上,
○ Price
用来显示SKU项中的价格
○ OptionIds
用来存放SKU项中的所有属性值编号,以逗号隔开,在界面中以隐藏域存在
控制器和视图实现
□ HomeController
当呈现Home/Index.cshtml视图的时候,HomeController应该提供一个方法,把所有的类别放在SelectListItem集合中传给前台,并返回一个有关产品视图模型强类型视图。
当在界面上点击类别选项,HomeController应该有一个方法接收类别的Id,把该类别下所有的属性Id以Json格式返回给前台。
当在界面上接收到一个属性Id集合,需要遍历属性Id集合,把每个属性Id传给控制器,HomeController应该有一个方法接收属性Id,在方法内部根据InputType来决定显示带Select的视图,还是带CheckBoxList的视图。
当点击界面上的"定价"按钮,可能需要对属性值进行笛卡尔乘积,可能不需要,因此,HomeController应该提供2个方法,一个方法用来渲染出需要笛卡尔乘积的视图,另一个方法用来渲染不需要笛卡尔乘积的视图。
当点击界面上的"提交"按钮,HomeController应该提供一个提交产品的方法,该方法接收的参数是有关产品的视图模型。
public class HomeController : Controller{public ActionResult Index(){//把类别封装成SelectListItem集合传递到前台var categories = Database.GetCategories();var result = from c in categoriesselect new SelectListItem() {Text = c.Name, Value = c.Id.ToString()};ViewData["categories"] = result;return View(new ProductVm());}//添加产品[HttpPost]public ActionResult AddProduct(ProductVm productVm){if (ModelState.IsValid){//TODO:各种保存return Json(new { msg = true });}else{//把类别封装成SelectListItem集合传递到前台var categories = Database.GetCategories();var result = from c in categoriesselect new SelectListItem() { Text = c.Name, Value = c.Id.ToString() };ViewData["categories"] = result;return RedirectToAction("Index", productVm);}}//根据分类返回分类下的所有属性Id[HttpPost]public ActionResult GetPropIdsByCategoryId(int categoryId){var props = Database.GetPropsByCategoryId(categoryId);List<int> propIds = props.Select(p => p.Id).ToList();return Json(propIds);}//显示属性和属性项的部分视图public ActionResult AddPropOption(int propId){var prop = Database.GetProps().Where(p => p.Id == propId).FirstOrDefault();var propOptions = Database.GetPropOptionsByPropId(propId);if (prop.InputType == (short) InputTypeEnum.PropDropDownList){PropOptionVmD propOptionVmD = new PropOptionVmD();propOptionVmD.PropId = propId;propOptionVmD.PropName = prop.Name;ViewData["propOptionsD"] = from p in propOptionsselect new SelectListItem() { Text = p.RealValue, Value = p.Id.ToString() };return PartialView("_AddPropOptionD", propOptionVmD);}else{PropOptionVmC propOptionVmC = new PropOptionVmC();propOptionVmC.PropId = propId;propOptionVmC.PropName = prop.Name;ViewData["propOptionsC"] = from p in propOptionsselect new SelectListItem() {Text = p.RealValue, Value = p.Id.ToString()};return PartialView("_AddPropOptionC", propOptionVmC);}}//当在前台界面上勾选CheckBoxList选项,点击"定价"按钮,就把PropAndOption集合传到这里[HttpPost]public ActionResult DisplaySKUs(List<PropAndOption> propAndOptions){try{//属性值分组var groupValues = (from v in propAndOptionsgroup v by v.PropIdinto grpselect grp.Select(t => Database.GetOptionValueById(t.PropOptionId))).ToList();//属性值Id分组var groupIds = (from i in propAndOptionsgroup i by i.PropIdinto grepselect grep.Select(t => t.PropOptionId.ToString())).ToList();//属性值分组后进行笛卡尔乘积IEnumerable<string> values;values = groupValues.First();groupValues.RemoveAt(0);groupValues.ForEach(delegate(IEnumerable<string> ele){values = (from v in valuesfrom e in eleselect v + " " + e).ToList();});//属性值Id分组后进行笛卡尔乘积IEnumerable<string> ids;ids = groupIds.First();groupIds.RemoveAt(0);groupIds.ForEach(delegate(IEnumerable<string> ele){ids = (from i in idsfrom e in eleselect i + "," + e).ToList();});//把笛卡尔积后的集合传递给前台ViewData["v"] = values;ViewData["i"] = ids;}catch (Exception){throw;}return PartialView("_ShowSKUs");}//不涉及属性值的笛卡尔乘积public ActionResult ShowSKUsWithoutCombination(){ViewData["v"] = null;ViewData["i"] = null;return PartialView("_ShowSKUs");}}
□ Home/Index.cshtml视图
当初次显示界面的时候,需要把"提交"按钮隐藏,把"定价"按钮显示。
当点击类别下拉框的时候:
1、清空属性区域
2、清空SKU区域
3、隐藏"定价"按钮,显示"提交"按钮
4、把类别Id异步传给控制器
5、遍历从控制器异步传回的属性Id的集合,把属性Id传给控制器,发送异步请求,返回有关产品属性和属性值的强类型部分视图,并追加到界面"产品属性"区域
当点击"定价"按钮:
1、可能"产品属性"区域有CheckBoxList
1.1 判断每组CheckBoxList必须至少有一被勾选
1.2 遍历每个属性行,遍历每个被勾选的项,组成类似{ propId: pId, propOptionId: oId }的数组
1.3 把{ propId: pId, propOptionId: oId }的数组以json格式传给控制器
1.4 异步返回的部分视图追加到界面的"产品SKU与定价"区域,并给动态加载内容实施异步验证
2、可能"产品属性"区域没有CheckBoxList
2.1 异步加载显示SKU组合的部分视图,只显示一个有关价格的input元素
勾选"产品属性"区域的CheckBoxList:
1、检查每组CheckBoxList是否满足条件,即至少有一项被选中
2、隐藏"定价"按钮,显示"提交"按钮
点击"产品属性"区域中,每行的"删除行"按钮,删除当前属性行。
@model MvcApplication1.Models.ProductVm@{ViewBag.Title = "Index";Layout = "~/Views/Shared/_Layout.cshtml";}<div id="wrapper">@using (Html.BeginForm("AddProduct", "Home", FormMethod.Post, new { id = "addForm" })){<fieldset><legend>类别</legend><div id="categories">@Html.DropDownListFor(m => m.CategoryId, ViewData["categories"] as IEnumerable<SelectListItem>, "==选择类别==")@Html.ValidationMessageFor(m => m.CategoryId)</div></fieldset><br /><fieldset><legend>产品描述</legend><div id="description">@Html.LabelFor(m => m.Name)@Html.TextBoxFor(m => m.Name)@Html.ValidationMessageFor(m => m.Name)<br /><br />@Html.LabelFor(m => m.Code)@Html.TextBoxFor(m => m.Code)@Html.ValidationMessageFor(m => m.Code)</div></fieldset><br /><fieldset><legend>产品属性</legend><ul id="props"></ul></fieldset><br /><input type="button" id="displaySKU" value="定价" /><br /><fieldset><legend>产品SKU与定价</legend><ul id="skus"></ul></fieldset><input type="button" id="up" value="提交" />}</div>@section scripts{<script src="~/Scripts/jquery.validate.min.js"></script><script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script><script src="~/Scripts/dynamicvalidation.js"></script><script type="text/javascript">$(function () {//提交按钮先隐藏直到点击定价按钮再显示showPriceHideUp();//点击类别下拉框$(‘#CategoryId‘).change(function () {changeCategory();});//点击定价按钮显示SKU项,以表格显示,属性名称 属性名称 价格,//定价按钮消失,提交按钮出现//对每组CheckBoxList进行验证,保证至少有一个选项勾选$(‘#displaySKU‘).on("click", function () {if ($(‘#props‘).find(‘.c‘).length) { //判断属性和属性值区域有没有包含CheckBoxList的li,存在if (checkCblist()) { //如果所有CheckBoxList组都至少有一项被勾选//遍历所有的CheckBoxList的选中项,一个属性Id带着1个或多个属性项Idvar propAndOptions = [];//遍历所有包含CheckBoxList的li$.each($(‘#props‘).find(‘.c‘), function () {//从隐藏域中获取属性Id <input type="hidden" value="" id=‘h_v‘ class=‘h_v‘>var pId = $(this).find(‘input[type=hidden]‘).val();//遍历每个li中被选中的CheckBox$.each($(this).find("input:checked"), function () {//获取选中值var oId = $(this).val();propAndOptions.push({ propId: pId, propOptionId: oId });});});//异步提交PropAndOption集合$.ajax({cache: false,url: ‘@Url.Action("DisplaySKUs", "Home")‘,contentType: ‘application/json; charset=utf-8‘,dataType: "html",type: "POST",data: JSON.stringify({ ‘propAndOptions‘: propAndOptions }),success: function (data) {$(‘#skus‘).html(data);$.each($(‘.s‘), function (index) {$.validator.unobtrusive.parseDynamicContent(this, "#addForm");});hidePriceShowUp();},error: function (jqXhr, textStatus, errorThrown) {alert("出错了 ‘" + jqXhr.status + "‘ (状态: ‘" + textStatus + "‘, 错误为: ‘" + errorThrown + "‘)");}});} else {return;}} else {//判断属性和属性值区域有没有包含CheckBoxList的li,不存在$.ajax({cache: false,url: ‘@Url.Action("ShowSKUsWithoutCombination", "Home")‘,dataType: "html",type: "GET",success: function (data) {$(‘#skus‘).html(data);$.validator.unobtrusive.parseDynamicContent(‘.s‘, "#addForm");hidePriceShowUp();},error: function (jqXhr, textStatus, errorThrown) {alert("出错了 ‘" + jqXhr.status + "‘ (状态: ‘" + textStatus + "‘, 错误为: ‘" + errorThrown + "‘)");}});}});//删除属性属性值行$(‘#props‘).on(‘click‘, ‘.delRow‘, function() {$(this).parent().parent().remove();});//点击任意CheckBoxList中的选项,定价按钮出现,提交按钮隐藏$(‘#props‘).on("change", "input[type=checkbox]", function () {//验证checkCblist();showPriceHideUp();});//点击提交$(‘#up‘).on("click", function () {if (checkCblist) {if ($(‘#addForm‘).valid()) {$.ajax({cache: false,url: ‘@Url.Action("AddProduct", "Home")‘,type: ‘POST‘,dataType: ‘json‘,data: $(‘#addForm‘).serialize(),success: function (data) {if (data.msg) {alert(‘提交成功‘);}},error: function (xhr, status) {alert("添加失败,状态码:" + status);}});}} else {alert("属性值必须勾选");}});});//点击类别下拉框function changeCategory() {//获取选中的值var selectedValue = $(‘#CategoryId option:selected‘).val();//如果确实选中if ($.trim(selectedValue).length > 0) {//清空属性和属性项区域$(‘#props‘).empty();//清空SKU区域$(‘#skus‘).empty();showPriceHideUp();//异步请求属性和属性项$.ajax({url: ‘@Url.Action("GetPropIdsByCategoryId", "Home")‘,data: { categoryId: selectedValue },type: ‘post‘,cache: false,async: false,dataType: ‘json‘,success: function (data) {if (data.length > 0) {$.each(data, function (i, item) {$.get("@Url.Action("AddPropOption", "Home")", { propId: item }, function (result) {$(‘#props‘).append(result);});});}}});}}//隐藏定价按钮 显示提交按钮function hidePriceShowUp() {//隐藏定价按钮$(‘#displaySKU‘).css("display", "none");//显示提交按钮$(‘#up‘).css("display", "block");}//显示定价按钮 隐藏提交按钮function showPriceHideUp(parameters) {$(‘#displaySKU‘).css("display", "block");$(‘#up‘).css("display", "none");}//检查每组CheckBoxList,如果没有一个选中,报错function checkCblist() {var result = false;//遍历每组li下的checkboxlist,如果没有一个选中,报错$(‘#props li‘).each(function () {if ($(this).find("input:checked").length == 0) {$(this).find(‘.err‘).text("至少选择一项").css("color", "red");} else {$(this).find(‘.err‘).text("");result = true;}});return result;}</script>}
以上,关于给为动态加载内容实施验证的dynamicvalidation.js
文件,详细参考这里。
//对动态生成内容客户端验证(function ($) {$.validator.unobtrusive.parseDynamicContent = function (selector, formSelector) {$.validator.unobtrusive.parse(selector);var form = $(formSelector);var unobtrusiveValidation = form.data(‘unobtrusiveValidation‘);var validator = form.validate();$.each(unobtrusiveValidation.options.rules, function (elname, elrules) {if (validator.settings.rules[elname] == undefined) {var args = {};$.extend(args, elrules);args.messages = unobtrusiveValidation.options.messages[elname];//edit:use quoted strings for the name selector$("[name=‘" + elname + "‘]").rules("add", args);} else {$.each(elrules, function (rulename, data) {if (validator.settings.rules[elname][rulename] == undefined) {var args = {};args[rulename] = data;args.messages = unobtrusiveValidation.options.messages[elname][rulename];//edit:use quoted strings for the name selector$("[name=‘" + elname + "‘]").rules("add", args);}});}});};})(jQuery);
以上,当点击产品类别,搜集"产品属性"区域中的勾选项,组成{ propId: pId, propOptionId: oId }
数组的时候,这里的propId
和propOptionId
键必须和PropAndOption
中的属性吻合,因为在控制器方法中,接收的是List
类型。
public class PropAndOption{public int PropId { get; set; }public int PropOptionId { get; set; }}
□ _AddPropOptionD.cshtml部分视图
当点击界面上的类别选项,相应属性下的属性值以Select显示,即单选,就来加载这里的视图,并呈现到界面中的"产品属性"区域。
@using MvcApplication1.Extensions@model MvcApplication1.Models.PropOptionVmD@using (Html.BeginCollectionItem("PropOptionDs")){<li><span>@Model.PropName:</span><span>@Html.DropDownListFor(m => m.PropOptionId, ViewData["propOptionsD"] as IEnumerable<SelectListItem>)</span><span>@Html.ValidationMessageFor(m => m.PropOptionId)</span><span>@Html.HiddenFor(m => m.PropId)</span><span><a href="javascript:void(0)" class="delRow">删除行</a></span></li>}
其中,Html.BeginCollectionItem("PropOptionDs")
根据导航属性生成满足批量上传条件的表单元素,详细介绍在这里。
public static class CollectionEditingHtmlExtensions{//目标生成如下格式//<input autocomplete="off" name="FavouriteMovies.Index" type="hidden" value="6d85a95b-1dee-4175-bfae-73fad6a3763b" />//<label>Title</label>//<input class="text-box single-line" name="FavouriteMovies[6d85a95b-1dee-4175-bfae-73fad6a3763b].Title" type="text" value="Movie 1" />//<span class="field-validation-valid"></span>public static IDisposable BeginCollectionItem<TModel>(this HtmlHelper<TModel> html, string collectionName){//构建name="FavouriteMovies.Index"string collectionIndexFieldName = string.Format("{0}.Index", collectionName);//构建Guid字符串string itemIndex = GetCollectionItemIndex(collectionIndexFieldName);//构建带上集合属性+Guid字符串的前缀string collectionItemName = string.Format("{0}[{1}]", collectionName, itemIndex);TagBuilder indexField = new TagBuilder("input");indexField.MergeAttributes(new Dictionary<string, string>(){{"name", string.Format("{0}.Index", collectionName)},{"value", itemIndex},{"type", "hidden"},{"autocomplete", "off"}});html.ViewContext.Writer.WriteLine(indexField.ToString(TagRenderMode.SelfClosing));return new CollectionItemNamePrefixScope(html.ViewData.TemplateInfo, collectionItemName);}private class CollectionItemNamePrefixScope : IDisposable{private readonly TemplateInfo _templateInfo;private readonly string _previousPrfix;//通过构造函数,先把TemplateInfo以及TemplateInfo.HtmlFieldPrefix赋值给私有字段变量,并把集合属性名称赋值给TemplateInfo.HtmlFieldPrefixpublic CollectionItemNamePrefixScope(TemplateInfo templateInfo, string collectionItemName){this._templateInfo = templateInfo;this._previousPrfix = templateInfo.HtmlFieldPrefix;templateInfo.HtmlFieldPrefix = collectionItemName;}public void Dispose(){_templateInfo.HtmlFieldPrefix = _previousPrfix;}}/// <summary>////// </summary>/// <param name="collectionIndexFieldName">比如,FavouriteMovies.Index</param>/// <returns>Guid字符串</returns>private static string GetCollectionItemIndex(string collectionIndexFieldName){Queue<string> previousIndices = (Queue<string>)HttpContext.Current.Items[collectionIndexFieldName];if (previousIndices == null){HttpContext.Current.Items[collectionIndexFieldName] = previousIndices = new Queue<string>();string previousIndicesValues = HttpContext.Current.Request[collectionIndexFieldName];if (!string.IsNullOrWhiteSpace(previousIndicesValues)){foreach (string index in previousIndicesValues.Split(‘,‘)){previousIndices.Enqueue(index);}}}return previousIndices.Count > 0 ? previousIndices.Dequeue() : Guid.NewGuid().ToString();}}
□ _AddPropOptionC.cshtml部分视图
当点击界面上的类别选项,相应属性下的属性值以CheckBoxList显示,即多选,就来加载这里的视图,并呈现到界面中的"产品属性"区域。
@using MvcApplication1.Extensions@model MvcApplication1.Models.PropOptionVmC@using (Html.BeginCollectionItem("PropOptionCs")){<li class="c"><span>@Model.PropName:</span><span>@Html.CheckBoxList("PropOptionIds",ViewData["propOptionsC"] as IEnumerable<SelectListItem>,null, 10)<span class="err"></span></span><span>@Html.HiddenFor(m => m.PropId)</span><span><a href="javascript:void(0)" class="delRow">删除行</a></span></li>}
其中,CheckBoxList是基于HtmlHelper
的扩展方法,用来呈现水平或垂直分布的CheckBoxList,详细介绍在这里。
using System.Collections.Generic;using System.Linq;using System.Text;namespace System.Web.Mvc{public static class InputExtensions{#region 水平方向CheckBoxList/// <summary>/// 生成水平方向的CheckBoxList/// </summary>/// <param name="htmlHelper"></param>/// <param name="name">name属性值</param>/// <param name="htmlAttributes">属性和属性值的键值对集合</param>/// <param name="number">每行显示的个数</param>/// <returns></returns>public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper,string name,IEnumerable<SelectListItem> listInfo,IDictionary<string, object> htmlAttributes,int number){//name属性值必须有if (string.IsNullOrEmpty(name)){throw new ArgumentException("必须给CheckBoxList一个name值", "name");}//数据源SelectListItem的集合必须有if (listInfo == null){throw new ArgumentNullException("listInfo", "List<SelectListItem>类型的listInfo参数不能为null");}//数据源中必须有数据if (!listInfo.Any()){throw new ArgumentException("List<SelectListItem>类型的listInfo参数必须有数据", "listInfo");}//准备拼接var sb = new StringBuilder();//每行CheckBox开始数数var lineNumber = 0;//遍历数据源foreach (var info in listInfo){lineNumber++;//创建type=checkbox的inputvar builder = new TagBuilder("input");//tag设置属性if (info.Selected){builder.MergeAttribute("checked", "checked");}builder.MergeAttributes(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));//创建checkbox的显示值var lableBuilder = new TagBuilder("label");lableBuilder.MergeAttribute("for", string.Format("{0}_{1}", name, info.Value));lableBuilder.InnerHtml = info.Text;sb.Append(lableBuilder.ToString(TagRenderMode.Normal));//如果设置的每行数量刚好被当前数量整除就换行if (lineNumber == 0 || (lineNumber % number == 0)){sb.Append("<br />");}}return MvcHtmlString.Create(sb.ToString());}/// <summary>/// 重载,不包含属性和属性值键值对的集合/// </summary>/// <param name="htmlHelper"></param>/// <param name="name">name的属性值</param>/// <param name="listInfor">SelectListItem集合类型的数据源</param>/// <returns></returns>public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper,string name,IEnumerable<SelectListItem> listInfor){return htmlHelper.CheckBoxList(name, listInfor, null, 5);}#endregion#region 垂直方向CheckBoxListpublic static MvcHtmlString CheckBoxListVertical(this HtmlHelper htmlHelper,string name,IEnumerable<SelectListItem> listInfo,IDictionary<string, object> htmlAttributes,int columnNumber = 1){//name属性值不能为nullif (string.IsNullOrEmpty(name)){throw new ArgumentException("必须给CheckBoxList的name属性赋值","name");}//数据源不能为nullif (listInfo == null){throw new ArgumentNullException("listInfo","List<SelectListItem>类型的listInfo参数不能为null");}//数据源中必须有数据if (!listInfo.Any()){throw new ArgumentException("List<SelectListItem>类型的参数listInfo必须有数据","listInfo");}//数据源数据项的数量var dataCount = listInfo.Count();//得到行数var rows = Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(dataCount) / Convert.ToDecimal(columnNumber)));//创建divvar wrapBuilder = new TagBuilder("div");wrapBuilder.MergeAttribute("style", "float:left; line-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 listInfo){var builder = new TagBuilder("input");if (info.Selected){builder.MergeAttribute("checked", "checked");}builder.MergeAttributes(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}}
□ _ShowSKUs.cshtml部分视图
当点击界面上的"定价"按钮,就来加载这个视图,如果属性值笛卡尔乘积为null,那就只显示一个有关价格的input元素。如果确实存在属性值笛卡尔乘积,就遍历这些SKU项显示出来,并且,每遍历一次,就去加载有关产品价格的强类型部分视图。
@if (ViewData["v"] == null){<li><span class="s">@{ProductSKUVm productSkuVm = new ProductSKUVm();productSkuVm.OptionIds = "";Html.RenderPartial("_SKUDetail", productSkuVm);}</span></li>}else{string[] values = (ViewData["v"] as IEnumerable<string>).ToArray();string[] ids = (ViewData["i"] as IEnumerable<string>).ToArray();for (int i = 0; i < values.Count(); i++){<li><span>@values[@i]</span><span class="s">@{ProductSKUVm productSkuVm = new ProductSKUVm();productSkuVm.OptionIds = ids[@i];Html.RenderPartial("_SKUDetail", productSkuVm);}</span></li>}}
□ _SKUDetail.cshtml强类型部分视图
作为_ShowSKUs.cshtml部分视图的子部分视图,用来显示产品价格相关的强类型部分视图。
@using MvcApplication1.Extensions@model MvcApplication1.Models.ProductSKUVm@using (Html.BeginCollectionItem("ProductSKUs")){@Html.TextBoxFor(m => m.Price)@Html.ValidationMessageFor(m => m.Price)@Html.HiddenFor(m => m.OptionIds)}
□ 模拟数据库存储的Database类
展开 public class Database { #region 关于分类 public static List<Category> GetCategories() { return new List<Category>() { new Category(){Id = 1, Name = "家电"}, new Category(){Id = 2, Name = "家具"} }; } #endregion #region 关于属性 public static List<Prop> GetProps() { var category1 = GetCategories().Where(c => c.Id == 1).FirstOrDefault(); var category2 = GetCategories().Where(c => c.Id == 2).FirstOrDefault(); return new List<Prop>() { new Prop(){Id = 1, Name = "重量",InputType = (short)InputTypeEnum.PropDropDownList,CategoryId = category1.Id, Category = category1}, new Prop(){Id = 2, Name = "尺寸",InputType = (short)InputTypeEnum.PropCheckBoxList,CategoryId = category1.Id, Category = category1}, new Prop(){Id = 3, Name = "颜色",InputType = (short)InputTypeEnum.PropCheckBoxList,CategoryId = category1.Id, Category = category1}, new Prop(){Id = 4, Name = "成份",InputType = (short)InputTypeEnum.PropDropDownList,CategoryId = category2.Id, Category = category2} }; } public static List<Prop> GetPropsByCategoryId(int categoryId) { return GetProps().Where(p => p.CategoryId == categoryId).ToList(); } #endregion #region 关于属性值 public static List<PropOption> GetPropOptions() { var prop1 = GetProps().Where(p => p.Id == 1).FirstOrDefault(); //重量 var prop2 = GetProps().Where(p => p.Id == 2).FirstOrDefault(); //尺寸 var prop3 = GetProps().Where(p => p.Id == 3).FirstOrDefault(); //颜色 var prop4 = GetProps().Where(p => p.Id == 4).FirstOrDefault(); //成份 return new List<PropOption>() { //重量的属性值 new PropOption(){Id = 1, Prop = prop1, PropId = prop1.Id, RealValue = "5kg"}, new PropOption(){Id = 2, Prop = prop1, PropId = prop1.Id, RealValue = "10kg"}, //尺寸的属性值 new PropOption(){Id = 3, Prop = prop2, PropId = prop2.Id, RealValue = "10英寸"}, new PropOption(){Id = 4, Prop = prop2, PropId = prop2.Id, RealValue = "12英寸"}, new PropOption(){Id = 5, Prop = prop2, PropId = prop2.Id, RealValue = "15英寸"}, //颜色的属性值 new PropOption(){Id = 6, Prop = prop3, PropId = prop3.Id, RealValue = "玫红色"}, new PropOption(){Id = 7, Prop = prop3, PropId = prop3.Id, RealValue = "圣诞白"}, new PropOption(){Id = 8, Prop = prop3, PropId = prop3.Id, RealValue = "宇宙光"}, new PropOption(){Id = 9, Prop = prop3, PropId = prop3.Id, RealValue = "绚烂橙"}, //成份的属性值 new PropOption(){Id = 10, Prop = prop4, PropId = prop4.Id, RealValue = "实木"}, new PropOption(){Id = 11, Prop = prop4, PropId = prop4.Id, RealValue = "橡木"}, }; } //根据属性Id获取所有属性值 public static List<PropOption> GetPropOptionsByPropId(int propId) { return GetPropOptions().Where(p => p.PropId == propId).ToList(); } //根据属性值Id获取属性值 public static string GetOptionValueById(int optionId) { return (GetPropOptions().Where(p => p.Id == optionId).FirstOrDefault()).RealValue; } #endregion }
结束!