接1的内容,那么有这么一个需求!
需求分析
需要修改某一个配置的时候
1.从对应的api中读取消息,消息内容为Json格式的
2.基于当前的Json渲染成表单提供给管理端的客户呈现
3.管理端的用户可以基于这个表单的内容进行修改,然后提交
4.服务端基于用户提交的信息,提交给IIS Administration以便更新到IIS的网站中!
所以我们需要一个东西,可以动态编辑Json内容的东西!!!
关键点在于这个编辑非开发也能玩,当然我们也可以提供2个版本,一个表单版本,一个字符串版本
方案一
由上面的步骤可知,1获取到的数据是变动的,或者你可以说可以把几种的格式都创建成表然后入库
只入库表的结构,获取到数据后和表的结构相互结合,然后使用PasteForm提供给用户进行更新!
不要在意CRUD的规范一个表一个API,在这个需求中完全可以使用动态Route进行单API处理!
{
"title":"标题",
"desc":"描述信息",
"value":97.5
}
以上的方案你会可见的发现,将会有非常多的工作量,可能增加的表不下100个!!!
方案二
结合现有的PasteForm框架知识,是否也可以搞一个动态的表单,理想的状态有
1.无论来的Json数据是咋样的,都给他在表单中显示
2.可以针对性质的一些字段进行配置,也就是结合方案一的内容做一个表结构的信息
3.区别于方案一的是,方案一比如有100个结构表,你得先创建100个表及对应得CRUD,而这里得动态是可以不出创建的
比如有
{
"title":"标题",
"desc":"描述信息",
"value":97.5
}
其实在文章1中还有一个隐藏的知识点就是Json不是单层级的,比如
{
"name": "iis_temp",
"id": "UYZcrUKt9eaMiolk-rH_GA",
"status": "started",
"auto_start": "true",
"pipeline_mode": "integrated",
"managed_runtime_version": "",
"enable_32bit_win64": "false",
"queue_length": "1000",
"start_mode": "OnDemand",
"cpu": {
"limit": "0",
"limit_interval": "5",
"action": "NoAction",
"processor_affinity_enabled": "false",
"processor_affinity_mask32": "0xFFFFFFFF",
"processor_affinity_mask64": "0xFFFFFFFF"
},
"process_model": {
"idle_timeout": "20",
"max_processes": "1",
"pinging_enabled": "true",
"ping_interval": "30",
"ping_response_time": "90",
"shutdown_time_limit": "90",
"startup_time_limit": "90",
"idle_timeout_action": "Terminate"
},
"identity": {
"identity_type": "ApplicationPoolIdentity",
"username": "",
"load_user_profile": "true"
},
"recycling": {
"disable_overlapped_recycle": "false",
"disable_recycle_on_config_change": "false",
"log_events": {
"time": "true",
"requests": "true",
"schedule": "true",
"memory": "true",
"isapi_unhealthy": "true",
"on_demand": "true",
"config_change": "true",
"private_memory": "true"
},
"periodic_restart": {
"time_interval": "1740",
"private_memory": "0",
"request_limit": "0",
"virtual_memory": "0",
"schedule": []
}
},
"rapid_fail_protection": {
"enabled": "true",
"load_balancer_capabilities": "HttpLevel",
"interval": "5",
"max_crashes": "5",
"auto_shutdown_exe": "",
"auto_shutdown_params": ""
},
"process_orphaning": {
"enabled": "false",
"orphan_action_exe": "",
"orphan_action_params": ""
}
}
上面只是二级的,还有一些是Array格式的,更加负责了,还可能有多级,不止二级!
结合上面的消息,所以选择的是方案二
实现单元
结合上面的内容,我的习惯是搞一个测试项目,然后做单元测试,在一个现有的项目中要添加比较大的模块的时候,
我的步骤就是先梳理大脉络,大需求,大方向
然后拆分成小的功能点
整理下有难点的,不太确定的功能点
搞一个案例项目,实现后进行单元测试下
结合以上的内容,由于也用到了贴代码PasteForm框架的内容,所以我打算在PasteTemplate案例项目上实现这个功能!
动态分析
按照方案二的说明,比如有以下内容
{
"va":"aabbcc",
"score":98,
"avg":67.5
}
按照默认的显示应该是这样的
如果是一些简单的单词还好,有一些单词就不是这么好识别了,比如如下的
{
"title":"这个是标题",
"desc":"描述信息",
"value":97.5
}
我们希望他的显示是如下的,比如title这个字段
结合以上的内容,所以我们知道了这个动态的意思,就是如果我配置中有这个比如title字段的信息,则在表单中对应的显示
如果没有则使用JSON的字段直接显示(一般为英文字母)
以上是单元的内容的需求分析,那么还有一个需求,就是多层级的JSON,比如
{
"name":"abc",
"age":19,
"actions":[
{
"va":"aabbcc",
"score":98,
"avg":67.5
}
],
"info":{
"title":"标题",
"desc":"描述信息",
"value":97.5
}
}
这个时候的显示,我希望他能分组,否则会有一大堆,
有一个关键点在于名称重复,比如name和info.name最终显示都是name,如果全显示可能非常长!
对于用户来说很容易搞混,实现后大概是这样的
注意看上图,按照JSON的内容,做了分组,上面的info或者actions是可以收缩的,如下
结合以上的需求,我设定添加2个表,一个动态表,一个动态字段!
动态表
直接上代码,创建CodeFirst
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Entities;
namespace PasteTemplate.dynamicmodels
{
/// <summary>
/// 动态表
/// </summary>
[Index(nameof(Code), IsUnique = true)]
public class DynamicTable : Entity<int>
{
/// <summary>
/// 代码 示例:userInfo
/// </summary>
[MaxLength(32)]
public string Code { get; set; }
/// <summary>
/// 名称 示例:用户信息
/// </summary>
[MaxLength(16)]
public string Name { get; set; }
/// <summary>
/// 说明 示例:修改用户基本信息
/// </summary>
[MaxLength(128)]
public string Desc { get; set; }
/// <summary>
/// 状态
/// </summary>
public bool IsEnable { get; set; } = true;
/// <summary>
/// 空字符 空字符串移除对象
/// </summary>
public bool RemoveEmptyStr { get; set; }
/// <summary>
/// 数值0 数值为0移除对象
/// </summary>
public bool RemoveZeroNumber { get; set; }
/// <summary>
/// 特性
/// </summary>
public string AttributeJson { get; set; }
/ <summary>
/ 数据提交地址
/ </summary>
//[MaxLength(256)]
//public string PostUrl { get; set; }
}
}
注意上面的特性是使用贴代码PasteForm的特性的JSON内容
其他的字段信息就没啥可说的了!
动态字段
一个动态表可以包含很多个字段,每个字段有不一样的信息,比如标题,说明,是否必填,长度,特性规则等!
using System.ComponentModel.DataAnnotations;
using Volo.Abp.Domain.Entities;
namespace PasteTemplate.dynamicmodels
{
/// <summary>
/// 动态字段信息
/// </summary>
public class DynamicField : Entity<int>
{
/// <summary>
/// 动态表
/// </summary>
public int TableId { get; set; }
/// <summary>
/// 字段名称 CreateDate
/// </summary>
[MaxLength(64)]
public string Name { get; set; } = "";
/// <summary>
/// 字段类型 System.DateTime
/// </summary>
[MaxLength(64)]
public string DataType { get; set; } = "";
/// <summary>
/// 默认值
/// </summary>
public string DefaultValue { get; set; }
/// <summary>
/// 字段中文 创建日期
/// </summary>
[MaxLength(16)]
public string Title { get; set; } = "";
/// <summary>
/// 描述
/// </summary>
[MaxLength(128)]
public string Placeholder { get; set; } = "";
/// <summary>
/// 限定 最大长度
/// </summary>
public int Maxlength { get; set; } = 0;
/// <summary>
/// 必填
/// </summary>
public bool Required { get; set; } = false;
/ <summary>
/ 排序
/ </summary>
//public int Sort { get; set; } = 0;
/// <summary>
/// 特性串
/// </summary>
public string AttributesJson { get; set; }
/// <summary>
/// 状态
/// </summary>
public bool IsEnable { get; set; } = true;
}
}
以上两个cs创建后,我们使用贴代码提供的代码生成器PasteBuilder执行下
然后就会创建对应的Dto AppService等文件,由于我新弄了一个模块,所以要把没引入的namespace处理下,重新生成!
然后稍微改造下对应的Dto,比如DynamicTable
主要是上面的,给他搞一个菜单
然后是字段信息中的表ID,给搞一个从url的参数中获取
以上信息处理后,记得add-migration一下,然后启动项目,到管理页面后,新建一个权限,如下
然后刷新下页面,可以打开对应的菜单
一开始打开,表格内时空白的,没有数据,你可以去创建下!
到此对于动态表的添加就算完成,或者说是对JSON的字段的附加说明的内容!
逻辑代码实现
结合上的内容,那么流程应该是这样的,用户打开一个页面,然后这个页面载入要修改的JSON的数据,以表单的形式呈现,如果有配置字段的附加信息,则把附加信息也展示(就是附加信息结合JSON的值显示)
1.你没办法post一大串数据到一个页面吧!
2.JSON本身数据是动态的,所以数据提供的时候,得给他一个定义,这就是table_code的由来
实现API
按照上面的分析,我们先实现几个接口,一个是读取JSON内容的,一个是测试的,如下
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using PasteFormHelper;
using PasteTemplate.Application.Contracts;
using PasteTemplate.Handler;
namespace PasteTemplate.dynamicmodels
{
/// <summary>
///
///</summary>
//[TypeFilter(typeof(RoleAttribute), Arguments = new object[] { "data", "view" })]
public class DynamicHelperAppService : PasteTemplateAppService
{
private IAppCache _appCache => LazyServiceProvider.LazyGetRequiredService<IAppCache>();
/// <summary>
///
///</summary>
public DynamicHelperAppService() : base()
{
}
/// <summary>
/// 基于datakey读取对应的模型数据和对象数据
/// </summary>
/// <returns></returns>
[HttpGet]
public async Task<DynamicModel> Read(string datakey)
{
if (String.IsNullOrEmpty(datakey))
{
throw new PasteCodeException("传递有效的参数datakey");
}
var _modelStr = await _appCache.GetAsync(datakey);
if (String.IsNullOrEmpty(_modelStr))
{
throw new PasteCodeException("缓存数据有误,无法继续执行!");
}
var _model = JsonSerializer.Deserialize<DynamicModel>(_modelStr);
//从数据库读取对应的信息
if (!String.IsNullOrEmpty(_model.Code) || _model.TableId != 0)
{
var _table = await _dbContext.DynamicTable
.WhereIf(_model.TableId != 0, x => x.Id == _model.TableId)
.WhereIf(!String.IsNullOrEmpty(_model.Code), x => x.Code == _model.Code)
.AsNoTracking()
.FirstOrDefaultAsync();
if (_table != null && _table != default)
{
_model.Table = ObjectMapper.Map<DynamicTable, DynamicTableModel>(_table);
//二次加工
_model.Title = _table.Name;
if (!String.IsNullOrEmpty(_table.AttributeJson))
{
_model.Attributes = JsonSerializer.Deserialize<List<VoloModelAttribute>>(_table.AttributeJson);
}
var _fields = await _dbContext.DynamicField
.Where(x => x.TableId == _table.Id && x.IsEnable)
.OrderBy(x => x.Id)
.AsNoTracking()
.ToListAsync();
if (_fields != null && _fields.Count > 0)
{
_model.Properties = ObjectMapper.Map<List<DynamicField>, List<DynamicFieldModel>>(_fields);
//二次加工
foreach (var _field in _model.Properties)
{
if (!String.IsNullOrEmpty(_field.DefaultValue))
{
switch (_field.DataType)
{
case "Int32":
{
if (int.TryParse(_field.DefaultValue, out var _val))
{
_field.Value = _val;
}
}
break;
case "Int64":
{
if (long.TryParse(_field.DefaultValue, out var _val))
{
_field.Value = _val;
}
}
break;
case "Double":
{
if (double.TryParse(_field.DefaultValue, out var _val))
{
_field.Value = _val;
}
}
break;
case "DateTime":
{
if (_field.DefaultValue == "now")
{
_field.Value = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
else if (_field.DefaultValue == "today")
{
_field.Value = DateTime.Now.ToString("yyyy-MM-dd 00:00:00");
}
else if (_field.DefaultValue == "month")
{
_field.Value = DateTime.Now.ToString("yyyy-MM-01 00:00:00");
}
else
{
if (DateTime.TryParse(_field.DefaultValue, out var _val))
{
_field.Value = _val;
}
}
}
break;
default:
{
_field.Value = _field.DefaultValue;
}
break;
}
}
if (!String.IsNullOrEmpty(_field.AttributesJson))
{
_field.Attributes = JsonSerializer.Deserialize<List<VoloModelAttribute>>(_field.AttributesJson);
}
}
}
}
}
return _model;
}
/// <summary>
/// 提交修改后的JsonString
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<string> Update(string datakey)
{
if (_httpContext.Request.Body != null)
{
using var reader = new StreamReader(_httpContext.Request.Body);
var bodystr = await reader.ReadToEndAsync();
reader.Close();
reader.Dispose();
return bodystr;
}
else
{
throw new PasteCodeException("提交数据有误,无法输出");
}
}
/// <summary>
/// 上传一个json对象,获取datakey
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<string> Test(string code = "", int tableid = 0)
{
var _key = Guid.NewGuid().ToString().Replace("-", "");
var _model = new DynamicModel { };
_model.DataKey = _key;
_model.Code = code;
_model.TableId = tableid;
var _httpContext = base._httpContext;
if (_httpContext.Request.Body != null)
{
using var reader = new StreamReader(_httpContext.Request.Body);
var bodystr = await reader.ReadToEndAsync();
reader.Close();
reader.Dispose();
if (!String.IsNullOrEmpty(bodystr))
{
var _jobj = System.Text.Json.JsonSerializer.Deserialize<JsonElement>(bodystr);
_model.Data = _jobj;
}
}
var _modelStr = JsonSerializer.Serialize(_model);
await _appCache.SetAsync(_key, _modelStr, 3600 * 4);
return _key;
}
}
}
上面的数据中的DynamicModel内容如下
/// <summary>
///
/// </summary>
public class DynamicModel
{
/// <summary>
/// 数据key 必填
/// </summary>
public string DataKey { get; set; }
/// <summary>
/// Table.Code 二选一,如果有模型数据的话
/// </summary>
public string Code { get; set; }
/// <summary>
/// Table.Id 二选一,如果有模型数据的话
/// </summary>
public int TableId { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Title { get; set; }
/// <summary>
/// 模型自己的属性规则列表
/// </summary>
public List<VoloModelAttribute> Attributes { get; set; }
/// <summary>
/// 表信息 可能为null
/// </summary>
public DynamicTableModel Table { get; set; }
/// <summary>
/// 字段信息 字段格式信息,可能为null
/// </summary>
public List<DynamicFieldModel> Properties { get; set; }
/// <summary>
/// System.Text.Json 必填表示Json的数据
/// </summary>
public JsonElement Data { get; set; }
}
主要的意思就是,提交JSON信息的生产者需要知道当前的JSON是哪个的信息(table_id/table_code),然后是数据本身(Data)
这里要注意,由于Data是动态的,这里涉及到JSON的序列化和反序列化,这个和AppService的默认处理方式有关!
就是在AppService中,如果你写的返回值为Object,返回到前端往往是一个JSONString,其实这里是有一个中间件在转格式输出的,目前有System.Text.Json和Newtonsoft的方式,2边需要一致,否则会造成数据丢失的可能!
比如你提交的数据为
{
"info":{
"title":"标题",
"desc":"描述信息",
"value":97.5
}
}
前端接收到的为
{
"info":{
"title":[],
"desc":[],
"value":[]
}
}
惊不惊喜!!!
到此,完成了大概动态表单的API部分,实现的内容有
1.搞一个动态表,动态字段,用于承载配置,针对JSON的某一些字段做补充,或者说是所有字段
2.实现JSON数据的暂存,生产者获得JSON数据后,加工下存储在一个key中,给前端,前端基于这个key进行对应的数据的读取!
这个数据是经过加工的,比如说添加了table_code,对应的1中的模型数据,也可能没有配置,所以这里有一个很好的解耦动作!
下一章将介绍动态表单的UI部分的内容,和这章中的动态字段中的Name的关键说法!
下期见… … .