ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现

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

在界面中勾选CheckBoxList中属性值选项:
ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现

把勾选的属性值进行笛卡尔乘积,每行需要填写价格:
ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现

我们暂且不考虑这些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; }
    }   

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

展开    public static class Database
{
public static List<Prop> GetProps()
{
return new List<Prop>()
{
new Prop(){Id = 1, Name = "颜色"},
new Prop(){Id = 2, Name = "尺寸"},
};
} public static List<PropOption> GetPropOptions()
{
return new List<PropOption>()
{
new PropOption(){Id = 1, PropId = 1, RealValue = "红色"},
new PropOption(){Id = 2, PropId = 1, RealValue = "蓝色"},
new PropOption(){Id = 3, PropId = 1, RealValue = "黑色"},
new PropOption(){Id = 4, PropId = 2, RealValue = "5.5英寸"},
new PropOption(){Id = 5, PropId = 2, RealValue = "8.5英寸"},
};
} //根据属性项的Id获取属性值
public static string GetOptionValueById(int optionId)
{
return (GetPropOptions().Where(p => p.Id == optionId).FirstOrDefault()).RealValue;
} public static List<PropOption> GEtPropOptionsByPropId(int propId)
{
return GetPropOptions().Where(p => p.PropId == propId).ToList();
}
} Home控制器的Index方法渲染Home/Index.cshtml视图,在这个页面中暂时属性值及价格的笛卡尔乘积。 public class HomeController : Controller
{
public ActionResult Index()
{
return View();
} ......
}

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

ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现

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......真如所愿!

ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现

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

ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现

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

ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现

可是,表单元素中明明已经有了以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,即有关价格强类型视图生成的地方,运用扩展方法。

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

ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现

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

再次查看表单元素:

ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现

我们发现:所有有关价格的强类型视图页中,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)
}

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

ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现

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

ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现

上一篇:基于 HTML5 的 WebGL 技术构建 3D 场景(一)


下一篇:基于 HTML5 的 WebGL 和 VR 技术的 3D 机房数据中心可视化