zoukankan      html  css  js  c++  java
  • ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现

    在"ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积01, 在控制台实现"中,在控制台应用程序中实现了属性值的笛卡尔乘积。本篇在界面中实现。需要实现的大致如下:

    在界面中勾选CheckBoxList中属性值选项:
    1

     

    把勾选的属性值进行笛卡尔乘积,每行需要填写价格:
    2

     

    我们暂且不考虑这些CheckBoxList是如何显示出来的(在后续有关商品模块的文章中会实现),还需考虑的方面包括:

    1、从CheckBoxList中获取到什么再往控制器传?

     

    对于每行的CheckBoxList来说,可以从选项中拿到属性值的编号,还可以拿到属性的编号,最后我们拿到类似{ propId: 1, propOptionId: 1 },{ propId: 1, propOptionId: 2 }的数组或集合。

     

    2、前台数组和集合如何传递才能确保被控制器接收?

    通过jQuery.ajax方法可以实现,待会实现。

     

    3、在呈现属性值和价格组合的界面是如何呈现的?

    其实是通过部分视图来实现的,而且在一个主部分视图中嵌套了子部分视图。

     

    4、如何对呈现的价格验证呢?

    由于价格是被异步、动态加载到页面的,所以,这里还涉及到如何对异步加载的动态内容进行验证的问题。

     

    关于属性的Model:

        public class Prop
    
        {
    
            public int Id { get; set; }
    
            public string Name { get; set; }
    
        }

     

    关于属性值的Model:

        public class PropOption
    
        {
    
            public int Id { get; set; }
    
            public string RealValue { get; set; }
    
            public int PropId { get; set; }
    
        }

     

    把从前台获取的属性和属性值编号封装到PropAndOption类中,前台向控制器传的就是这个类的集合。

        public class PropAndOption
    
        {
    
            public int PropId { get; set; }
    
            public int propOptionId { get; set; } 
    
        }   

     

    模拟一个数据库存储层,无非就是获取数据等,可忽略。

    展开


    在Home/Index.cshtml视图,把通过$.ajax异步动态加载的、有关属性值及价格笛卡尔乘积的部分视图,追加到页面上的一块区域。大致如下:

    3

     

    1、发出异步请求,把类似{ propId: 1, propOptionId: 1 }的数组传给控制器
    2、控制器根据接收到的
    { propId: 1, propOptionId: 1 }的数组,得到类似"红色 5英寸"的一个IEnumerable<string>类型的集合,通过ViewData传给_DisplaySKUs.cshtml部分视图
    3、在
    _DisplaySKUs.cshtml,遍历类似"红色 5英寸"的一个IEnumerable<string>类型的集合,每遍历一次,再加载有关价格的一个强类型部分视图_SKUDetail.cshtml

    4、最后把_DisplaySKUs.cshtml部分视图动态加载到Home/Index.cshtml视图的某块区域中

     

    Home/Index.cshtml视图。

     

    @{
    
        ViewBag.Title = "Index";
    
        Layout = "~/Views/Shared/_Layout.cshtml";
    
    }
    
    <form id="fm">
    
        <ul id="skus">
    
        </ul>
    
        <input type="submit" value="提交"/>
    
    </form>
    
    @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 () {
    
                var propAndOptions = [];
    
                propAndOptions.push({ propId: 1, propOptionId: 1 });
    
                propAndOptions.push({ propId: 1, propOptionId: 2 });
    
                propAndOptions.push({ propId: 1, propOptionId: 3 });
    
                propAndOptions.push({ propId: 2, propOptionId: 4 });
    
                propAndOptions.push({ propId: 2, propOptionId: 5 });
    
                $.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);
    
                    },
    
                    error: function (jqXhr, textStatus, errorThrown) {
    
                        alert("出错了 '" + jqXhr.status + "' (状态: '" + textStatus + "', 错误为: '" + errorThrown + "')");
    
                    }
    
                });
    
            });
    
        </script>
    
    }
    

    以上,
    { propId: 1, propOptionId: 1 }这个匿名对象的propIdpropOptionId键必须和PropAndOption类的属性对应
    $.ajax方法中,contentType表示传递给控制器的数据类型
    $.ajax方法中,dataType表示返回的数据类型,由于返回的是部分视图,所以这里的类型是html
    ○ 通过
    JSON.stringify方法把{ propId: 1, propOptionId: 1 }类型数组转换成json格式
    $('#skus').html(data)把_DisplaySKUs.cshtml部分视图动态加载到id为skus的区域


    HomeController

     

       public class HomeController : Controller
    
        {
    
            public ActionResult Index()
    
            {
    
                return View();
    
            }
    
            [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("_DisplaySKUs");
    
            }
    
        }
    

    以上,部分视图_DisplaySKUs将会收到2种类型为IEnumerable<string>的集合,一种有关类似"红色 5英寸"属性值集合,一种是类似"1, 2"属性值ID集合,前者用来显示,后者需要被传递到有关价格的Model中,以便随同价格保存到数据库。和价格有关的Model是:

     

        public class SKUVm
    
        {
    
            [Display(Name = "价格")]
    
            [Required(ErrorMessage = "必填")]
    
            [Range(typeof(Decimal), "0", "9999", ErrorMessage = "{0} 必须是数字介于 {1} 和 {2}之间.")]
    
            public decimal Price { get; set; }
    
            public string OptionIds { get; set; }
    
        }
    

     

    其中,OptionIds属性用来保存类似"1, 2"属性值ID,随同价格被保存到数据库。具体如何保存,这里略去,将在后续有关商品模块的文章中实现。 


    _DisplaySKUs.cshtml部分视图


    @{
    
        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">
    
                @{
    
                    SKUVm skuVm = new SKUVm();
    
                    skuVm.OptionIds = ids[@i];
    
                    Html.RenderPartial("_SKUDetail", skuVm);
    
                } 
    
            </span>
    
        </li>
    
    }
    

    以上,遍历所有类似"红色 5英寸"属性值集合,用于显示,由于类似"1, 2"属性值ID集合与类似"红色 5英寸"属性值集合采用同样的算法、逻辑获取到的,所以两者有一一对应关系。在遍历类似"红色 5英寸"属性值集合的同时,把每一个类似"1, 2"属性值传给有关价格的强类型部分视图。

     

    _SKUDetail.cshtml强类型部分视图。

     

    @model MvcApplication3.Models.SKUVm
    
        @Html.TextBoxFor(m => m.Price)
    
        @Html.ValidationMessageFor(m => m.Price)
    
        @Html.HiddenFor(m => m.OptionIds)
    


    大功告成!试着运行一下。wow......真如所愿!

    4

     

    再试下异步验证功能,居然没有?!

    5

     

    why?要知道,所有的异步验证与表单元素中以data-*开头的属性及其值有关。看来,还是有必要查看当前的表单元素。

    6

     

    可是,表单元素中明明已经有了以data-*开头的属性及其值啊?难道jquery.validate.unobtrusive在调用jquery.validatevalidate方法的时候,写法有问题?

     

    validate: function( options ) {
    
    ......  
    
    // check if a validator for this form was already created
    
             
    
    var validator = $.data(this[0], 'validator');
    
             
    
    if ( validator ) {
    
                 
    
      return validator;
    
             
    
    }
    
     
    
    ......

    发现问题了:当调用validate方法的时候,jquery.validate.unobtrusive发现存在data-val="true"的表单元素,就会返回当前的validator对象而不做其它任何事。而实际上,对于动态加载的部分视图,它还没有自己的validator对象。所以,有必要专门针对动态加载的内容写一个$.validator.unobtrusive的扩展。

     

    创建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);
    

    dynamicvalidation.js文件引入Home/Index.cshtml视图页,修改如下:

     

    ……
    
    @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 () {
    
                var propAndOptions = [];
    
                propAndOptions.push({ propId: 1, propOptionId: 1 });
    
                propAndOptions.push({ propId: 1, propOptionId: 2 });
    
                propAndOptions.push({ propId: 1, propOptionId: 3 });
    
                propAndOptions.push({ propId: 2, propOptionId: 4 });
    
                propAndOptions.push({ propId: 2, propOptionId: 5 });
    
                $.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, "#fm");
    
                        });
    
                    },
    
                    error: function (jqXhr, textStatus, errorThrown) {
    
                        alert("出错了 '" + jqXhr.status + "' (状态: '" + textStatus + "', 错误为: '" + errorThrown + "')");
    
                    }
    
                });
    
            });
    
        </script>
    
    }
    

    以上,当异步加载成功,遍历的类名为s的span,即有关价格强类型视图生成的地方,运用扩展方法。

     

    再次运行,并测试异步验证。

    7

     

    亦喜亦忧,喜的是有了异步验证,忧的是虽然只有一个价格验证不通过,所有的价格都验证不通过!

     

    再次查看表单元素:

    8

     

    我们发现:所有有关价格的强类型视图页中,name属性都是price,而以上的$.validator.unobtrusive.parseDynamicContent扩展方法是以name属性为依据的。所以,有必要为每一个有关价格的强类型视图生成不一样的name值。

     

    写一个针对HtmlHelper的扩展方法,目标是生成如下格式:

            //目标生成如下格式
    
            <input autocomplete="off" name="SomePropertion.Index" type="hidden" value="6d85a95b-1dee-4175-bfae-73fad6a3763b" />
    
            <label>Title</label>
    
            <input class="text-box single-line" name="SomePropertion[6d85a95b-1dee-4175-bfae-73fad6a3763b].Title" type="text" value="Movie 1" />
    
            <span class="field-validation-valid"></span>

     

    以上的目的是让每一个有关价格的input元素的name属性值都不一样。

     

        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();
    
            }
    
        }
    

    关于,CollectionEditingHtmlExtensions类,在"MVC批量更新,可验证并解决集合元素不连续控制器接收不完全的问题"中有详细说明。

     

    最后,再次修改_SKUDetail.cshtml这个强类型部分视图。

     

    @using MvcApplication3.Extension
    
    @model MvcApplication3.Models.SKUVm
    
    @using (Html.BeginCollectionItem("ProductSkus"))
    
    {
    
        @Html.TextBoxFor(m => m.Price)
    
        @Html.ValidationMessageFor(m => m.Price)
    
        @Html.HiddenFor(m => m.OptionIds)
    
    }
    

     

    再次运行并测试异步验证,一切正常!

    9

     

    且在每个有关价格的强类型视图部分,有了一个隐藏域,存放这属性值的Id,这些Id可以随价格一起被保存到数据库。

    10

      

     

     

     

  • 相关阅读:
    Django之Form、CSRF、cookie和session
    Django之路由、模板和模型系统
    简约而不简单的Django
    剑指offer(10)矩形覆盖
    剑指offer(9)变态跳台阶
    剑指offer(8)跳台阶
    剑指offer(7)斐波那契数列
    剑指offer(6)旋转数组中的最小数字
    剑指offer(5)用两个栈实现队列
    剑指offer(4)重建二叉树
  • 原文地址:https://www.cnblogs.com/darrenji/p/4108085.html
Copyright © 2011-2022 走看看