在前面的几篇文章中,已经在控制台和界面实现了属性值的笛卡尔乘积,这是商品模块中的一个难点。本篇就来实现在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 categories
                select 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 categories
                             select 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 propOptions
                                           select 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 propOptions
                    select 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 propAndOptions
                                   group v by v.PropId
                                       into grp
                                       select grp.Select(t => Database.GetOptionValueById(t.PropOptionId))).ToList();
                //属性值Id分组
                var groupIds = (from i in propAndOptions
                                group i by i.PropId
                                    into grep
                                    select 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 values
                              from e in ele
                              select v + " " + e).ToList();
                });
                //属性值Id分组后进行笛卡尔乘积
                IEnumerable<string> ids;
                ids = groupIds.First();
                groupIds.RemoveAt(0);
                groupIds.ForEach(delegate(IEnumerable<string> ele)
                {
                    ids = (from i in ids
                           from e in ele
                           select 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个或多个属性项Id
                        var 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 }数组的时候,这里的propIdpropOptionId键必须和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.HtmlFieldPrefix
            public 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的input
                var 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 垂直方向CheckBoxList
        public static MvcHtmlString CheckBoxListVertical(this HtmlHelper htmlHelper,
            string name,
            IEnumerable<SelectListItem> listInfo,
            IDictionary<string, object> htmlAttributes,
            int columnNumber = 1)
        {
            //name属性值不能为null
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentException("必须给CheckBoxList的name属性赋值","name");
            }
            //数据源不能为null
            if (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)));
            //创建div
            var 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}

结束!

转载于:https://www.cnblogs.com/darrenji/p/4110219.html

ASP.NET MVC中商品模块小样相关推荐

  1. 通过源代码研究ASP.NET MVC中的Controller和View(二)

    通过源代码研究ASP.NET MVC中的Controller和View(一) 在开始之前,先来温习下上一篇文章中的结论(推论): IView是所有HTML视图的抽象 ActionResult是Cont ...

  2. 如何在 ASP.NET MVC 中集成 AngularJS(2)

    在如何在 ASP.NET MVC 中集成 AngularJS(1)中,我们介绍了 ASP.NET MVC 捆绑和压缩.应用程序版本自动刷新和工程构建等内容. 下面介绍如何在 ASP.NET MVC 中 ...

  3. 如何在 ASP.NET MVC 中集成 AngularJS

    介绍 当涉及到计算机软件的开发时,我想运用所有的最新技术.例如,前端使用最新的 JavaScript 技术,服务器端使用最新的基于 REST 的 Web API 服务.另外,还有最新的数据库技术.最新 ...

  4. 在Asp.Net MVC中实现RequiredIf标签对Model中的属性进行验证

    在Asp.Net MVC中可以用继承ValidationAttribute的方式,自定制实现RequiredIf标签对Model中的属性进行验证 具体场景为:某一属性是否允许为null的验证,要根据另 ...

  5. ASP.NET MVC中你必须知道的13个扩展点

         ScottGu在其最新的博文中推荐了Simone Chiaretta的文章13 ASP.NET MVC extensibility points you have to know,该文章为我 ...

  6. Asp.net mvc中的Ajax处理

    在Asp.net MVC中的使用Ajax, 可以使用通用的Jquery提供的ajax方法,也可以使用MVC中的AjaxHelper. 这篇文章不对具体如何使用做详细说明,只对于在使用Ajax中的一些需 ...

  7. 在 ASP.NET MVC 中使用 Chart 控件

    在 .NET 3.5 的时候,微软就提供了一个 Chart 控件,网络上有大量的关于在 VS2008 中使用这个控件的文章,在 VS2010 中,这个控件已经被集成到 ASP.NET 4.0 中,可以 ...

  8. 在ASP.NET MVC中使用IIS级别的URL Rewrite

    在ASP.NET MVC中使用IIS级别的URL Rewrite 原文 在ASP.NET MVC中使用IIS级别的URL Rewrite 大约一年半前,我在博客上写过一系列关于URL Rewrite的 ...

  9. ASP.NET MVC中实现多个按钮提交的几种方法

    有时候会遇到这种情况:在一个表单上需要多个按钮来完成不同的功能,比如一个简单的审批功能. 如果是用webform那不需要讨论,但asp.net mvc中一个表单只能提交到一个Action处理,相对比较 ...

  10. 在asp.net mvc中使用PartialView返回部分HTML段

    问题链接: MVC怎样实现异步调用输出HTML页面 该问题是个常见的 case, 故写篇文章用于提示新人. 在asp.net mvc中返回View时使用的是ViewResult,它继承自ViewRes ...

最新文章

  1. Linux虚拟机ip为127.0.0.1的处理
  2. vb.net2019- 机器学习ml.net情绪分析(2)
  3. Python学习:字符串
  4. C语言Prims求最小生成树MST的算法(附完整源码)
  5. C#LeetCode刷题之#40-组合总和 II(Combination Sum II)
  6. HTML5 Canvas 画虚线组件
  7. 电脑运行VirtualBox虚拟机总是提示0x00000000错误的解决方法
  8. Ghost 2.18.3 发布,基于 Markdown 的在线写作平台
  9. Java compiler level does not match the version of the installed Java project fac
  10. BZOJ4832: [Lydsy2017年4月月赛]抵制克苏恩
  11. 更轻量级的Semaphore、AutoResetEvent、ThreadPool
  12. HTML5 的输入类型(input type)
  13. QT5.14.2 官方例子 - 学习系列
  14. 使用spss进行系统聚类分析
  15. 本质与现象:本质与现象
  16. coffe-script
  17. 在ie6下实现position-fixed的效果
  18. EVE LOM正式官宣杨洋成为品牌代言人
  19. 心理学效应:阿基米德与酝酿效应
  20. matlab解方程组方法,第二章解线性方程组的直接方法matlab用法

热门文章

  1. ValueError: only one element tensors can be converted to Python scalars
  2. 设计一个python程序来计算显示通过如图2-7所示的管道_python程序设计习题与答案...
  3. 如何测量智能产品的AI智商水平,论AI的三种智商
  4. 沪牌软件操作开发说明
  5. Java多线程--内存模型(JMM)--详解
  6. jQuery练习t188,从0到1
  7. win10+Ubuntu双系统下如何完美卸载Ubuntu系统
  8. java和mysql实现点餐功能_java+mysql餐馆点餐系统的设计与开发
  9. 基于android点餐系统需求分析,基于Android智能终端的点餐系统设计研究
  10. SEO优化工具,查询死链VisualSEOStudio-2.0.2.3