提高生产性工具(五) - 数据的过滤器和图形化(适用于 MVC5 + MongoDB)
在下面流水账似的文章之前,先将一些感悟说一下。
1.如果一个系统对于某个功能在至少三个地方使用的话,必须将其抽象提炼出来,而且时间点最好是大规模测试之前。
2.提炼出来的功能,如果品质做得好,整个系统的品质也提高不少。
3.提炼出一个泛用的功能需要高度的技巧很时间,但是对于后期维护,应对需求变更是非常有好处的,甚至可以通过仅仅修改模型的特性就完成需求变更。
4.任何一个三年以上的CSharper都可以写一个性能和品质不错的功能,但是要将一个功能做到泛用,具有高度的可扩展性,需要10年左右的经验,大龄程序员应该多关注业务,多思考如何做个泛用功能,不是被多变的需求牵着鼻子走。泛用的东西,先苦后甜。
5.设计模式不是背书,在于灵活应用,设计模式不是死的,有时候,有些需求需要模式之间的组合,以及一些模式的变体。
关于MongoDB
MongoDB的好处是
你可以摆脱固定数据表字段的约束。
原生天然支持ORM(阶层结构的数据库,就是为了OOP对象而生的)。
某些方面拥有比SQL更灵活的Query。
MongoDB的坏处是
如果你删除一个字段,或者修改一个字段,在序列化的时候就会出错(系统发现一个在新的实体中不存在的属性。。)。而且不想Relation那样,可以通过一个SQL语句就改变表结果,后期维护会有一些额外成本。
以下内容可以忽略:
随着大量数据的采集,数据的过滤和可视化将越来越多的出现在实际需求中.
如何制作一个泛用的数据过滤器和可视化设定器,则是一个新的课题.
这里数据库使用的是MongoDB,由于MongoDB在Query上的一些特点,数据过滤变得十分简单了.
假设我们有一张名为人才储备的表格,上面有很多字段,哪些字段是可以用来做过滤的,过滤器的类型是什么,我们在写实体代码的时候,其实可以使用 Attribute 进行标注,然后在运行时,通过读取这些标记,就可以获得可用的过滤器列表.
using System; namespace BussinessLogic.Entity { [AttributeUsage(AttributeTargets.Property)] public class FilterItemAttribute : Attribute { /// <summary> /// 数据形式枚举 /// </summary> public enum StructType { SingleMasterTable = 0, MultiMasterTable = 5, SingleEnum = 10, MultiEnum = 15, Datetime = 20, Boolean = 25, SingleMasterTableWithGrade =30, MultiMasterTableWithGrade = 35, SingleCatalogMasterTable = 40, MultiCatalogMasterTable = 45 } /// <summary> /// 数据形式 /// </summary> public StructType MetaStructType { get; set; } /// <summary> /// 元数据类型 /// </summary> public Type MetaType { get; set; } } }
如果某个字段是可以多选的,可选值是从MasterTable来的,它就是MultiMasterTable,单选的,则是Single。如果可选值是来自于枚举的,则MasterTable变为Enum。
下面将这些特性作用于具体的实体类
using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using MongoDB.Bson.Serialization.Attributes; using System; namespace BussinessLogic.Entity { /// <summary> /// 人才储备 /// </summary> public partial class TalentInfo : CompanyTable { #region "model" /// <summary> /// 姓名 /// </summary> [DisplayName("姓名")] [Required] public string Name { get; set; } /// <summary> /// 英语名 /// </summary> [DisplayName("英语名")] public string EnglishName { get; set; } /// <summary> /// 生日 /// </summary> [DisplayName("生日")] [Required] [DataType(DataType.Date)] [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] [BsonDateTimeOptions(Kind = DateTimeKind.Local)] public DateTime BirthDay { get; set; } /// <summary> /// 出生地 /// </summary> [DisplayName("出生地")] public string BornIn { get; set; } /// <summary> /// 常住地 /// </summary> [DisplayName("常住地")] [Required] public string Location { get; set; } /// <summary> /// 手机 /// </summary> [DisplayName("手机")] [Required] [DataType(DataType.PhoneNumber)] [RegularExpression(@"^1[3458][0-9]{9}$", ErrorMessage = "手机号格式不正确")] public string Mobile { get; set; } /// <summary> /// 电子邮件 /// </summary> [DisplayName("电子邮件")] [Required] [DataType(DataType.EmailAddress)] public string Email { get; set; } /// <summary> /// 大学 /// </summary> [DisplayName("大学")] [Required] public string University { get; set; } /// <summary> /// 专业 /// </summary> [DisplayName("专业")] [Required] public string Major { get; set; } /// <summary> /// 学位 /// </summary> [DisplayName("学位")] [UIHint("Enum")] public AcademicType Academic { get; set; } /// <summary> /// 海外工作背景 /// </summary> [DisplayName("海外工作背景")] [FilterItem(MetaStructType = FilterItemAttribute.StructType.Boolean)] public bool OverseaWork { get; set; } /// <summary> /// 海外教育背景 /// </summary> [DisplayName("海外教育背景")] [FilterItem(MetaStructType = FilterItemAttribute.StructType.Boolean)] public bool OverseaEdu { get; set; } /// <summary> /// 行业背景 /// </summary> [DisplayName("行业背景")] public List<string> IndustryBackgroundList { get; set; } /// <summary> /// 上一家公司 /// </summary> [DisplayName("上一家公司")] public string PreEmp { get; set; } /// <summary> /// 上一家公司行业 /// </summary> [DisplayName("上一家公司行业")] public string PerInd { get; set; } /// <summary> /// 招聘渠道 /// </summary> [DisplayName("招聘渠道")] public string Channel { get; set; } /// <summary> /// 语言 /// </summary> [DisplayName("语言")] [FilterItem(MetaStructType = FilterItemAttribute.StructType.MultiMasterTableWithGrade, MetaType = typeof(M_Language))] public List<ItemWithGrade> LanguageList { get; set; } /// <summary> /// 技能 /// </summary> [DisplayName("技能")] [FilterItem(MetaStructType = FilterItemAttribute.StructType.MultiCatalogMasterTable , MetaType = typeof(M_Skill))] public List<string> SkillList { get; set; } /// <summary> /// 等级 /// </summary> [DisplayName("等级")] [UIHint("Enum")] [Required] public CommonGrade TalentRank { get; set; } /// <summary> /// 评价 /// </summary> [DisplayName("评价")] public string Evaluate { get; set; } /// <summary> /// 评价 /// </summary> [DisplayName("备注")] public string Comment { get; set; } /// <summary> /// 数据集名称 /// </summary> public override string GetCollectionName() { return "TalentInfo"; } /// <summary> /// 数据集名称静态字段 /// </summary> public static string CollectionName = "TalentInfo"; /// <summary> /// 数据主键前缀 /// </summary> public override string GetPrefix() { return string.Empty; } /// <summary> /// 数据主键前缀静态字段 /// </summary> public static string Prefix = string.Empty; /// <summary> /// Mvc画面的标题 /// </summary> [BsonIgnore] public static string MvcTitle = "人才储备"; #endregion } }
系统将自动通过反射机制获得FilterItemAttribute特性,根据特性来生成过滤器项目(filterItem)。
由于是动态的,所以,很容易的增加和修改过滤器项目(filterItem)。
过滤器项目(filterItem)也分多种类型,例如一个布尔型过滤器项目,则负责诸如 “过滤出全部具有 海外工作经验 的人”这样的任务。
using MongoDB.Driver; using MongoDB.Driver.Builders; namespace BussinessLogic.Entity { /// <summary> /// 布尔型的过滤器 /// </summary> public class FilterItemBoolean : FilterItemBase { /// <summary> /// 是否 /// </summary> public bool YesOrNo; public FilterItemBoolean(string _FieldName) { FieldName = _FieldName; IsActive = false; } /// <summary> /// 获得Query /// </summary> /// <returns></returns> public override IMongoQuery GetQuery() { return Query.EQ(FieldName, YesOrNo); } } }
每个过滤器,一旦属性设定完成,MongoDB的查询条件则也可以自动获得了。例如上面代码的 Query.EQ(FieldName, YesOrNo)
表示一个查询条件 :记录的名为 FieldName的元素的值为 YesOrNo(EQ表示等于)。
多个子条件可以组合成一个真正的过滤器(FilterSet)。当然,由于过滤器项目的过滤条件是自动获得的,所以整个过滤器的过滤条件也是很容易直接获得的。
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver; using System.ComponentModel; using MongoDB.Driver.Builders; using System; using System.Collections.Generic; using InfraStructure.DataBase; namespace BussinessLogic.Entity { /// <summary> /// 过滤器中心 /// </summary> public class FilterSetCenter : FilterSetBase { #region "model" /// <summary> /// 数据集名称 /// </summary> public override string GetCollectionName() { return "FilterSetCenter"; } /// <summary> /// 数据集名称静态字段 /// </summary> public static string CollectionName = "FilterSetCenter"; /// <summary> /// 数据主键前缀 /// </summary> public override string GetPrefix() { return string.Empty; } /// <summary> /// 数据主键前缀静态字段 /// </summary> public static string Prefix = string.Empty; /// <summary> /// Mvc画面的标题 /// </summary> [BsonIgnore] public static string MvcTitle = "过滤器中心"; /// <summary> /// 设定DisplayName /// </summary> public void SetDisplayName() { // 考虑到DisplayName和FieldName的关联性 // 以及DisplayName可能会修改名称 var type = Type.GetType(ModelName); for (int i = 0; i < FilterItems.Count; i++) { FilterItems[i].DisplayName = EasyQuery.GetDisplayName(FilterItems[i].FieldName, type); } } /// <summary> /// /// </summary> /// <param name="ModelName"></param> /// <param name="CompanyId"></param> /// <param name="AccountCode"></param> /// <returns></returns> public static List<MasterWrapper> GetFilterWrapperList<T>(string CompanyId, string AccountCode) { var FilterMaster = new List<MasterWrapper>(); var CompanyIdQuery = EasyQuery.CompanyIdQuery(CompanyId); var AccountIdQuery = EasyQuery.AccountCodeQuery(AccountCode); var ModelNameQuery = Query.EQ("ModelName", typeof(T).FullName); var FilterQuery = Query.And(CompanyIdQuery, AccountIdQuery, ModelNameQuery); var FilterSetList = InfraStructure.DataBase.Repository.GetRecList<FilterSetCenter>(FilterSetCenter.CollectionName, FilterQuery); foreach (var filter in FilterSetList) { FilterMaster.Add(new MasterWrapper { Code = filter.Code, Rank = int.Parse(filter.Code), Name = filter.Name, Description = filter.Description }); } return FilterMaster; } /// <summary> /// 获得查询 /// </summary> /// <returns></returns> public IMongoQuery GetQuery() { IMongoQuery FilterItemQuery = EasyQuery.CompanyIdQuery(CompanyId); foreach (var item in FilterItems) { if (item.IsActive) { switch (item.GetType().Name) { case "FilterItemList": FilterItemList filterItemList = (FilterItemList)item; if (filterItemList.Itemlist.Count > 0) { FilterItemQuery = Query.And(FilterItemQuery, filterItemList.GetQuery()); } break; case "FilterItemWithGradeList": FilterItemWithGradeList filterItemWithGradeList = (FilterItemWithGradeList)item; if (filterItemWithGradeList.Itemlist.Count > 0) { FilterItemQuery = Query.And(FilterItemQuery, filterItemWithGradeList.GetQuery()); } break; default: FilterItemQuery = Query.And(FilterItemQuery, item.GetQuery()); break; } } } return FilterItemQuery; } #endregion } }
GetQuery()方法负责通过 MongoDB的 Query.And 方法将子条件组合在一起。当然,现在默认所有过滤器之间是And连接,稍加修改之后也可以用Or连接。
过滤器配合UI之后,可以动态的生成如下的HTML画面。完全不需要任何多余的代码。
下面这样一段代码就可以构成任意的过滤器编辑界面了。
当然这里需要大约 500行的UI自动生成代码的支持。
@model FilterSetCenter @{ ViewBag.Title = ""; if (Model.Code == ConstHelper.NewRecordCode) { ViewBag.Title = "创建-" + FilterSetCenter.MvcTitle; } else { ViewBag.Title = "编辑-" + FilterSetCenter.MvcTitle; } Layout = "~/Views/Shared/_DashBoardForMin.cshtml"; } @using (Html.BeginForm()) { <div class="form-horizontal"> <div class="form-group"> @Html.LabelFor(model => model.Name, new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Description, new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Description, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Description, "", new { @class = "text-danger" }) </div> </div> @foreach (FilterItemBase item in Model.FilterItems) { <div class="form-group"> @Html.Label(item.DisplayName, new { @class = "control-label col-md-2" }) <div class="col-md-10"> @(FilterHelper.GetFilter(item, ViewBag.CompanyId)) </div> </div> } <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <input type="submit" value="保存" class="btn btn-success" /> @Html.ActionLink("返回列表", "Index", null, new { @class = "btn btn-default" }) </div> </div> </div> }
UI辅助代码基本上完成的是类似于 html.EditFor这样的功能,也就是对于HTML代码的一个封装。例如,下面的代码生成一个面板。上面画面例子中,技能的面板也就是这样生成的。当然,整个画面由多个这样的UI辅助代码的共同协作才可以完成。一个方法错了,整个画面就可能出问题。但是如果可以控制每个方法的品质,则使用这些方法堆砌出来的界面的品质也是有保证的。
/// <summary> /// 获得一个面板 /// </summary> /// <param name="id"></param> /// <param name="strTitle"></param> /// <param name="strContent"></param> /// <returns></returns> public static MvcHtmlString GetPanel(string id, string strTitle, string strContent) { var html = "<div class=\"panel panel-info\">" + Environment.NewLine; html += "<div class=\"panel-heading\">" + Environment.NewLine; html += "<h3 class=\"panel-title\">" + Environment.NewLine; html += "<a data-toggle=\"collapse\" data-parent=\"#accordion\" href=\"#collapse" + id + "\">" + Environment.NewLine; html += strTitle + Environment.NewLine; html += "</a>" +Environment.NewLine; html += "</h3>" + Environment.NewLine; html += "</div>" + Environment.NewLine; html += "<div id=\"collapse" + id + "\" class=\"panel-collapse collapse out\">" + Environment.NewLine; html += strContent + Environment.NewLine; html += "</div>" + Environment.NewLine; html += "</div>" + Environment.NewLine; return new MvcHtmlString(html); }