这两天需要实现一个动态表单设计,面对着属性的不确定,要能够容纳不同的属性进来,之前也接触过这方面的设计,但是没有设计好,导致问题太多,这一次参考一些前辈们的经验后,再次尝试一番,通过动态设计表结构,以达到任务要求。
一、常用动态表结构设计方式
1、动态修改表,适应变化。
2、预留字段实现动态表结构(伪动态)。
3、将动态属性全部保存在一个字段中,xml或是json格式保存(版本号+通用列)。
4、表结构和表数据分离,xml形式分别保存表结构和表数据。
5、横向表转纵向表(属性字段行存储)。
对于这几种方式,或许不同的选用适用不同的形式,我选择了最后一种来实现我现有的设计,这种方式个人感觉更加灵活,可以更方便的扩展属性(适合的才是最好的)。
二、横向表转纵向表初步设计
首先看下横向表的设计,如果采用横向表,因为业务的需要,要容纳好几种行业的信息进来,这样一来整张表的字段数将会非常多,从设计或是维护角度来讲,这都是一个棘手的芋头,因此传统的横向表设计不能满足现有的需求了。
按照横向表转纵向表的思路对表结构进行更改,通过设置成键值对形式,得到如下表结构。
在这里可能有一个情况得想清楚了,我们每一次增加一条记录的时候,记录内的信息是作为同一批添加进来的,反过来,当我从数据库中取出数据时,也应该需要把同一批的记录信息取出来,因此在上面的设计中再加入一个GroupId用来区分同一批次的数据,而至于属性的重复量很大,之后将进行优化处理。
现在看到这个结构时,对于动态扩充属性来讲,已经是达到了我的预期了,对于数据库设计时,将检测指标及限值均设置成字符串的,分组号我采用时间戳的形式进行存储,当然也可以采用其它更为稳妥的方式,如Guid或是自定义ID等。
对于好多检测指标名称出现重复情况,我将这部分单独抽出来一张表用于存储检测指标属性,需要注意的是此处的名称需要在某个检测项目编号下唯一,该部分属性先在界面上呈现,其次呈现对应的数据,如果某列增加或删除了,对应展示行也就空着了一个数据或是消失了一个数据单元,而对于改了指标名称或是对外的展示名称,都不会影响数据的存储,通过默认值可以使得有些常用值不要再二次输入,减少工作量。
在具体检测限值中完成对检测指标的关联,加入一列完成外键关联,同时对原有表内存在的列可以进行优化,因为这些信息都在检测指标中存在了,不必要的数据冗余还是不存在为好。单从现在的表结构来看,当我们按照在增加一些额外的属性时,可以做到不要去修改表结构,而只需要对表内数据进行管理即可。
三、代码实现过程
此处我采用Asp.Net Core MVC并利用Razor语法,更为方便的完成表单展示工作,当然对于这部分工作,采用js或模板等等都是可以快速完成的。
1、首先对于界面添加检测指标的设计,遵循普通的表单设计方式即可,此处增加了两个隐藏元素,为适用于编辑场景而存在,此处快速略过提交到后台并保存到数据库的过程,可能需要在后台验证提交的名称的唯一性。
2、对于增加具体的检测记录,需要先读取到整个检测项目下的所有检测指标,然后实现生成表单的过程,按照如下的思路一步一步实现:
对于第一步,从数据库获取指定检测项目的检测指标,该步可以直接利用提供的id做一次查询即可得到相应的指标集合。然后在前端循环输出时,利用Razor语法完成动态渲染Html,生成label和input元素,依照之前设计检测指标时的name唯一,可以在此处设计表单时指定name属性。
接下来可以完成表单的输入工作了,并提交到后台完成保存到数据库中,如我此处,新增记录时,保存到数据库前,先生成分组号,以此来区分这些指标下的数据是一个批次的,然后完成保存到数据库的过程。
public async Task ConvertTableToEvaluationLimitValues(TestItemCode_EvaluationStandardSubItem assignTestItemCode, DictionaryevaluationLimitValues){ var groupId = DateTimeHelper.GetTimeStamp(); var evaluationIndexes = assignTestItemCode.EvaluationStandardSubItem.EvaluationStandard.EvaluationIndexes.ToList(); foreach (var item in evaluationLimitValues) { var evaluationIndex = evaluationIndexes.Where(e => e.Name == item.Key).FirstOrDefault(); if (evaluationIndex == null) continue; var evaluationLimitValue = new EvaluationLimitValue(evaluationIndex.Id, assignTestItemCode.Id, item.Value, groupId); await _evaluationLimitValueRepository.InsertAsync(evaluationLimitValue); }}
对于这部分的设计,做一点更改也适用于更新操作,但是得注意到,更新表单时,表单上展示的检测指标可能存在增加或是删除的情形,因此对于存在的记录我们可以展示出来,不存在的则留空,当提交到数据库时,需要做一次比对过程,对那部分增加的检测指标需要保存到数据库中,当然对于已有的检测指标也存在变更的可能,因此需要做一次判断,当有变更时更新,没有时不处理。
public async Task UpdateEvaluationLimitValues(TestItemCode_EvaluationStandardSubItem assignTestItemCode, string groupId, DictionaryevaluationLimitValues){ //获取当前的检测指标 var evaluationIndexes = assignTestItemCode.EvaluationStandardSubItem.EvaluationStandard.EvaluationIndexes.ToList(); //目标分组已存在的检测限值 var results = await _evaluationLimitValueRepository.GetAll() .Where(e => e.TestItemCode_EvaluationStandardSubItemId == assignTestItemCode.Id && e.GroupId == groupId) .Include(e => e.EvaluationIndex).ToListAsync(); //已存在的检测限值对应于检测指标名称列表 var existedEvaluationLimitValueNameList = results.Select(r => r.EvaluationIndex.Name).ToList(); //需新增的检测限值记录 var addEvaluationLimitValueList = evaluationLimitValues.Keys.Except(existedEvaluationLimitValueNameList).ToList(); foreach (var key in addEvaluationLimitValueList) { var evaluationIndexId = evaluationIndexes.Where(e => e.Name == key).FirstOrDefault().Id; var evaluationLimitValue = new EvaluationLimitValue(evaluationIndexId, assignTestItemCode.Id, evaluationLimitValues[key], groupId); await _evaluationLimitValueRepository.InsertAsync(evaluationLimitValue); //移除记录 evaluationLimitValues.Remove(key); } //更新已有检测限值记录值 foreach (var key in evaluationLimitValues.Keys) { var editEvaluationLimitValues = results.Where(r => r.EvaluationIndex.Name == key && r.LimitValue != evaluationLimitValues[key]).FirstOrDefault(); if (editEvaluationLimitValues != null) { editEvaluationLimitValues.LimitValue = evaluationLimitValues[key]; await _evaluationLimitValueRepository.UpdateAsync(editEvaluationLimitValues); } }}
3、完成检测记录的表格展示,此处需要遵循一个原则,就是先展示检测指标,也就是先展示属性列,其次展示数据值,只有相应的属性列存在,展示的数据值才有意义,通过指定的编号Id获取相应的属性集合并展示在前端,利用Razor语法循环输出th元素,来产生表格行头。
@foreach (var item in Model) { @Html.Raw(item.DisplayName) }
表格展示完毕时便是数据开始呈现的时机,通过获取检测限值中的记录,注意这里的记录会有多条存在的,我们需要将纵向表结构转换成横向的json格式,用于前端读取,注意这里得把分组号加入进来,这是属于同一批次的标识。
通过纵向转横向,可以得到字典类型的list集合,然后再返回前序列化成json格式,便是前端需要的格式。
public async Task
>> ConvertEvaluationLimitValuesToTable(Guid assignedTestItemCodeId){ //分组后的评价限值 var results = await _evaluationLimitValueRepository.GetAll() .Where(e => e.TestItemCode_EvaluationStandardSubItemId == assignedTestItemCodeId) .Include(e => e.EvaluationIndex) .GroupBy(e => e.GroupId).ToListAsync(); List > convertResultList = new List >(); foreach (var result in results) { Dictionary tempResultList = new Dictionary { { EvaluationLimitValue.GetGroupIdName(), result.ElementAt(0).GroupId }//增加分组号GroupId }; foreach (var item in result) { tempResultList.Add(item.EvaluationIndex.Name, item.LimitValue); } convertResultList.Add(tempResultList); } return convertResultList;}
四、设计实现效果
1、实现动态增加属性列,尽管没有太丰富的功能,但是已经满足我现有的需求了,或许还能依据此得到更复杂的表单属性列设计。
2、增加表单具体值,依据增加的属性列完成相应值填写。
3、再次增加属性列后增加表单具体值,实现属性列的增加删除和修改后,仍然可以适用而无需手动修改表结构。
至此,动态表单的简单设计工作已经完成,过程较为简单,没有融入更多的比如多选,单选、数值型的设计,纯字符类型设计工作。
2019-05-27,望技术有成后能回来看见自己的脚步