From 3e2891d3a3a1206c01b809f07cbf8ef0c4717ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=9F=B3=E5=A4=B4?= Date: Tue, 5 Nov 2024 23:24:26 +0800 Subject: [PATCH] =?UTF-8?q?ReadOnlyEntityController=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E5=85=B1=E6=80=A7=E4=BB=A3=E7=A0=81=EF=BC=8CMVC=E4=B8=8EAPI?= =?UTF-8?q?=E5=85=B1=E7=94=A8=E4=BA=8EReadOnlyEntityController=EF=BC=8C?= =?UTF-8?q?=E5=87=8F=E5=B0=91=E7=BB=B4=E6=8A=A4=E5=B7=A5=E4=BD=9C=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Common/ReadOnlyEntityController.cs | 491 +--------------- .../Common/ReadOnlyEntityController2.cs | 503 ++++++++++++++++ .../Common/ReadOnlyEntityController.cs | 541 +----------------- NewLife.CubeNC/NewLife.CubeNC.csproj | 1 + NewLife.CubeNC/ViewModels/FormField.cs | 17 +- 5 files changed, 510 insertions(+), 1043 deletions(-) create mode 100644 NewLife.Cube/Common/ReadOnlyEntityController2.cs diff --git a/NewLife.Cube/Common/ReadOnlyEntityController.cs b/NewLife.Cube/Common/ReadOnlyEntityController.cs index f8fa6625..040bd4a4 100644 --- a/NewLife.Cube/Common/ReadOnlyEntityController.cs +++ b/NewLife.Cube/Common/ReadOnlyEntityController.cs @@ -16,37 +16,9 @@ namespace NewLife.Cube; /// 只读实体控制器基类 /// -public class ReadOnlyEntityController : ControllerBaseX where TEntity : Entity, new() +public partial class ReadOnlyEntityController : ControllerBaseX where TEntity : Entity, new() { - #region 属性 - /// 实体工厂 - public static IEntityFactory Factory => Entity.Meta.Factory; - - /// 实体改变时写日志。默认false - protected static Boolean LogOnChange { get; set; } - - /// 系统配置 - public SysConfig SysConfig { get; set; } - - /// 当前列表页的查询条件缓存Key - private static String CacheKey => $"CubeView_{typeof(TEntity).FullName}"; - #endregion - #region 构造 - static ReadOnlyEntityController() - { - // 强行实例化一次,初始化实体对象 - var entity = new TEntity(); - } - - /// 构造函数 - public ReadOnlyEntityController() - { - PageSetting.IsReadOnly = true; - - SysConfig = SysConfig.Current; - } - /// 动作执行前 /// public override void OnActionExecuting(Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext filterContext) @@ -72,335 +44,6 @@ public override void OnActionExecuting(Microsoft.AspNetCore.Mvc.Filters.ActionEx } #endregion - #region 数据获取 - /// 搜索数据集 - /// - /// - protected virtual IEnumerable Search(Pager p) - { - var start = p["dtStart"].ToDateTime(); - var end = p["dtEnd"].ToDateTime(); - var key = p["Q"]; - - var whereExpression = Entity.SearchWhereByKeys(key); - if (start > DateTime.MinValue || end > DateTime.MinValue) - { - var masterTime = Factory.MasterTime; - if (masterTime != null) - whereExpression &= masterTime.Between(start, end); - } - - //// 根据模型列设置,拼接作为搜索字段的字段 - //var modelTable = ModelTable; - //var modelCols = modelTable?.GetColumns()?.Where(w => w.ShowInSearch)?.ToList() ?? new List(); - - //foreach (var col in modelCols) - //{ - // var val = p[col.Name]; - // if (val.IsNullOrWhiteSpace()) continue; - // whereExpression &= col.Field == val; - //} - - //添加映射字段查询 - foreach (var item in Factory.Fields) - { - var val = p[item.Name]; - if (!val.IsNullOrWhiteSpace()) - { - whereExpression &= item.Equal(val); - } - } - - return Entity.FindAll(whereExpression, p); - } - - /// 搜索数据,支持数据权限 - /// - /// - protected virtual IEnumerable SearchData(Pager p) - { - // 缓存数据,用于后续导出 - //SetSession(CacheKey, p); - //Session[CacheKey] = p; - - // 数据权限 - var builder = CreateWhere(); - if (builder != null) - { - builder.Data2 ??= p.Items; - p.State = builder; - } - - // 数字型主键,默认降序 - if (PageSetting.OrderByKey && p.Sort.IsNullOrEmpty() && p.OrderBy.IsNullOrEmpty()) - { - var uk = Factory.Unique; - if (uk != null && uk.Type.IsInt()) - { - p.OrderBy = uk.Desc(); - //p.Sort = uk.Name; - //p.Desc = true; - } - } - - return Search(p); - } - - /// 查找单行数据 - /// - /// - protected virtual TEntity Find(Object key) - { - var fact = Factory; - if (fact.Unique == null) - { - var pks = fact.Table.PrimaryKeys; - if (pks.Length > 0) - { - var exp = new WhereExpression(); - foreach (var item in pks) - { - // 如果前端没有传值,则不要参与构造查询 - var val = GetRequest(item.Name); - - // 2021.04.18 添加 - // 表结构没有唯一键,只有联合主键,并且id是其中一个主键, - // 而id作为路由参数,上面从Request中获取到空值, - // 最终导致联合主键的表查询单条数据,只用到名称为非id的主键 - if (val == null && item.Name.EqualIgnoreCase("id")) val = key.ToString(); - - if (val != null) exp &= item.Equal(val); - } - - return Entity.Find(exp); - } - } - - return Entity.FindByKeyForEdit(key); - } - - /// 查找单行数据,并判断数据权限 - /// - /// - protected TEntity FindData(Object key) - { - // 先查出来,再判断数据权限 - var entity = Find(key); - if (entity != null) - { - // 数据权限 - var builder = CreateWhere(); - if (builder != null && !builder.Eval(entity)) throw new InvalidOperationException($"非法访问数据[{key}]"); - } - - return entity; - } - - /// 创建查询条件构造器,主要用于数据权限 - /// - protected virtual WhereBuilder CreateWhere() - { - var exp = ""; - var att = GetType().GetCustomAttribute(); - if (att != null) - { - // 已登录用户判断系统角色,未登录时不判断 - var user = HttpContext.Items["CurrentUser"] as IUser; - user ??= ManageProvider.User; - if (user == null || !user.Roles.Any(e => e.IsSystem) && !att.Valid(user.Roles)) - exp = att.Expression; - } - - // 多租户 - var set = CubeSetting.Current; - if (set.EnableTenant) - { - var ctxTenant = TenantContext.Current; - if (ctxTenant != null && IsTenantSource) - { - var tenant = Tenant.FindById(ctxTenant.TenantId); - if (tenant != null) - { - HttpContext.Items["TenantId"] = tenant.Id; - - if (typeof(TEntity) == typeof(Tenant)) - { - if (!exp.IsNullOrEmpty()) - exp = "Id={#TenantId} and " + exp; - else - exp = "Id={#TenantId}"; - } - else - { - if (!exp.IsNullOrEmpty()) - exp = "TenantId={#TenantId} and " + exp; - else - exp = "TenantId={#TenantId}"; - } - } - } - } - - if (exp.IsNullOrEmpty()) return null; - - var builder = new WhereBuilder - { - Factory = Factory, - Expression = exp, - //Data = Session, - }; - builder.SetData(Session); - //builder.Data2 = new ItemsExtend { Items = HttpContext.Items }; - builder.SetData2(HttpContext.Items.ToDictionary(e => e.Key + "", e => e.Value)); - - return builder; - } - - /// 是否租户实体类 - protected Boolean IsTenantSource => typeof(TEntity).GetInterfaces().Any(e => e == typeof(ITenantSource)); - - /// 获取选中键 - /// - protected virtual String[] SelectKeys => GetRequest("Keys")?.Split(","); - - /// 多次导出数据 - /// - protected virtual IEnumerable ExportData(Int32 max = 0) - { - var set = CubeSetting.Current; - if (max <= 0) max = set.MaxExport; - - // 计算目标数据量 - var p = Session[CacheKey] as Pager; - p = new Pager(p) - { - RetrieveTotalCount = true, - PageIndex = 1, - PageSize = 1, - }; - SearchData(p); - p.PageSize = 20_000; - - //!!! 数据量很大,且有时间条件时,采用时间分片导出。否则统一分页导出 - //if (Factory.Count > 100_000) - if (p.TotalCount > 100_000) - { - var start = p["dtStart"].ToDateTime(); - var end = p["dtEnd"].ToDateTime(); - if (start.Year > 2000 /*&& end.Year > 2000*/) - { - if (end.Year < 2000) end = DateTime.Now; - - // 计算步进,80%数据集中在20%时间上,凑够每页10000 - //var speed = (p.TotalCount * 0.8) / (24 * 3600 * 0.2); - var speed = (Double)p.TotalCount / (24 * 3600); - var step = p.PageSize / speed; - - XTrace.WriteLine("[{0}]导出数据[{1:n0}],时间区间({2},{3}),分片步进{4:n0}秒", Factory.EntityType.FullName, p.TotalCount, start, end, step); - - return ExportDataByDatetime((Int32)step, max); - } - } - - XTrace.WriteLine("[{0}]导出数据[{1:n0}],共[{2:n0}]页", Factory.EntityType.FullName, p.TotalCount, p.PageCount); - - return ExportDataByPage(p.PageSize, max); - } - - /// 分页导出数据 - /// 页大小。默认10_000 - /// 最大行数 - /// - protected virtual IEnumerable ExportDataByPage(Int32 pageSize, Int32 max) - { - // 跳过头部一些页数,导出当前页以及以后的数据 - var p = Session[CacheKey] as Pager; - p = new Pager(p) - { - // 不要查记录数 - RetrieveTotalCount = false, - PageIndex = 1, - PageSize = pageSize - }; - - while (max > 0) - { - if (HttpContext.RequestAborted.IsCancellationRequested) yield break; - if (p.PageSize > max) p.PageSize = max; - - var list = SearchData(p); - - var count = list.Count(); - if (count == 0) break; - max -= count; - - foreach (var item in list) - { - yield return item; - } - - if (count < p.PageSize) break; - - p.PageIndex++; - } - - // 回收内存 - GC.Collect(); - } - - /// 时间分片导出数据 - /// 分片不仅。默认60 - /// 最大行数 - /// - protected virtual IEnumerable ExportDataByDatetime(Int32 step, Int32 max) - { - // 跳过头部一些页数,导出当前页以及以后的数据 - var p = Session[CacheKey] as Pager; - p = new Pager(p) - { - // 不要查记录数 - RetrieveTotalCount = false, - PageIndex = 1, - PageSize = 0, - }; - - var start = p["dtStart"].ToDateTime(); - var end = p["dtEnd"].ToDateTime(); - if (end.Year < 2000) end = DateTime.Now; - - //!!! 前后同一天必须查跨天 - if (start == start.Date && end == end.Date) end = end.AddDays(1); - - var dt = start; - while (max > 0 && dt < end) - { - if (HttpContext.RequestAborted.IsCancellationRequested) yield break; - - var dt2 = dt.AddSeconds(step); - if (dt2 > end) dt2 = end; - - p["dtStart"] = dt.ToFullString(); - p["dtEnd"] = dt2.ToFullString(); - - var list = SearchData(p); - - var count = list.Count(); - //if (count == 0) break; - - foreach (var item in list) - { - yield return item; - } - - dt = dt2; - max -= count; - } - - // 回收内存 - GC.Collect(); - } - #endregion - #region 默认Action /// 多行数据列表 /// @@ -1171,142 +814,12 @@ public virtual ApiResponse Detail([Required] String id) //} #endregion - #region 实体操作重载 - /// 验证实体对象 - /// 实体对象 - /// 操作类型 - /// 是否提交数据阶段 - /// - protected virtual Boolean Valid(TEntity entity, DataObjectMethodType type, Boolean post) - { - if (!ValidPermission(entity, type, post)) - { - switch (type) - { - case DataObjectMethodType.Select: throw new NoPermissionException(PermissionFlags.Detail, "无权查看数据"); - case DataObjectMethodType.Update: throw new NoPermissionException(PermissionFlags.Update, "无权更新数据"); - case DataObjectMethodType.Insert: throw new NoPermissionException(PermissionFlags.Insert, "无权新增数据"); - case DataObjectMethodType.Delete: throw new NoPermissionException(PermissionFlags.Delete, "无权删除数据"); - } - } - - if (post && LogOnChange) - { - // 必须提前写修改日志,否则修改后脏数据失效,保存的日志为空 - if (type == DataObjectMethodType.Delete || - (type == DataObjectMethodType.Update && (entity as IEntity).HasDirty)) - LogProvider.Provider.WriteLog(type + "", entity); - } - - return true; - } - - /// 验证实体对象 - /// 实体对象 - /// 操作类型 - /// 是否提交数据阶段 - /// - protected virtual Boolean ValidPermission(TEntity entity, DataObjectMethodType type, Boolean post) => true; - #endregion - #region 列表字段和表单字段 - private static FieldCollection _ListFields; - /// 列表字段过滤 - protected static FieldCollection ListFields => _ListFields ??= new FieldCollection(Factory, ViewKinds.List); - - //private static FieldCollection _FormFields; - ///// 表单字段过滤 - //[Obsolete] - //protected static FieldCollection FormFields => _FormFields ??= new FieldCollection(Factory, "Form"); - - private static FieldCollection _AddFormFields; - /// 表单字段过滤 - protected static FieldCollection AddFormFields => _AddFormFields ??= new FieldCollection(Factory, ViewKinds.AddForm); - - private static FieldCollection _EditFormFields; - /// 表单字段过滤 - protected static FieldCollection EditFormFields => _EditFormFields ??= new FieldCollection(Factory, ViewKinds.EditForm); - - private static FieldCollection _DetailFields; - /// 表单字段过滤 - protected static FieldCollection DetailFields => _DetailFields ??= new FieldCollection(Factory, ViewKinds.Detail); - - private static FieldCollection _SearchFields; - /// 搜索字段过滤 - protected static FieldCollection SearchFields => _SearchFields ??= new FieldCollection(Factory, ViewKinds.Search); - - /// 获取字段信息。支持用户重载并根据上下文定制界面 - /// 字段类型:1-列表List、2-详情Detail、3-添加AddForm、4-编辑EditForm、5-搜索Search - /// 获取字段列表时的相关模型。对webapi版暂时无效 - /// - protected virtual FieldCollection OnGetFields(ViewKinds kind, Object model) - { - var fields = kind switch - { - ViewKinds.List => ListFields, - ViewKinds.Detail => DetailFields, - ViewKinds.AddForm => AddFormFields, - ViewKinds.EditForm => EditFormFields, - ViewKinds.Search => SearchFields, - _ => ListFields, - }; - return fields.Clone(); - } - /// 获取字段信息。支持用户重载并根据上下文定制界面 /// 字段类型:1-列表List、2-详情Detail、3-添加AddForm、4-编辑EditForm、5-搜索Search /// [AllowAnonymous] [HttpGet] - public virtual List GetFields(ViewKinds kind) - { - var fields = OnGetFields(kind, null); - - //Object data = new { code = 0, data = fields }; - - //return new JsonResult(data); - return fields; - } - - ///// - ///// 实体过滤器,根据模型列的表单显示类型,不显示的字段去掉 - ///// - ///// - ///// - ///// - //protected virtual IDictionary OnFilter(IModel model, ViewKinds kind) - //{ - // if (model == null) return null; - - // var dic = new Dictionary(); - // var fields = OnGetFields(kind, model); - // if (fields != null) - // { - // var names = Factory.FieldNames; - // foreach (var field in fields) - // { - // if (!field.Name.IsNullOrEmpty() && names.Contains(field.Name)) - // dic[field.Name] = model[field.Name]; - // } - // } - - // return dic; - //} - - ///// - ///// 实体列表过滤器,根据模型列的列表页显示类型,不显示的字段去掉 - ///// - ///// - ///// - ///// - //protected virtual IEnumerable> OnFilter(IEnumerable models, ViewKinds kind) - //{ - // if (models == null) yield break; - - // foreach (var item in models) - // { - // yield return OnFilter(item, kind); - // } - //} + public virtual List GetFields(ViewKinds kind) => OnGetFields(kind, null); #endregion } \ No newline at end of file diff --git a/NewLife.Cube/Common/ReadOnlyEntityController2.cs b/NewLife.Cube/Common/ReadOnlyEntityController2.cs new file mode 100644 index 00000000..9b4ea2d1 --- /dev/null +++ b/NewLife.Cube/Common/ReadOnlyEntityController2.cs @@ -0,0 +1,503 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NewLife.Common; +using NewLife.Cube.ViewModels; +using NewLife.Log; +using NewLife.Reflection; +using NewLife.Web; +using XCode; +using XCode.Membership; +using XCode.Model; + +namespace NewLife.Cube; + +/// 只读实体控制器基类 +public partial class ReadOnlyEntityController +{ + #region 属性 + /// 实体工厂 + public static IEntityFactory Factory => Entity.Meta.Factory; + + /// 实体改变时写日志。默认false + protected static Boolean LogOnChange { get; set; } + + /// 系统配置 + public SysConfig SysConfig { get; set; } + + /// 当前列表页的查询条件缓存Key + private static String CacheKey => $"CubeView_{typeof(TEntity).FullName}"; + #endregion + + #region 构造 + static ReadOnlyEntityController() + { + // 强行实例化一次,初始化实体对象 + var entity = new TEntity(); + } + + /// 构造函数 + public ReadOnlyEntityController() + { + PageSetting.IsReadOnly = true; + +#if MVC + PageSetting.EnableTableDoubleClick = CubeSetting.Current.EnableTableDoubleClick; +#endif + + SysConfig = SysConfig.Current; + } + #endregion + + #region 数据获取 + /// 搜索数据集 + /// + /// + protected virtual IEnumerable Search(Pager p) + { + var start = p["dtStart"].ToDateTime(); + var end = p["dtEnd"].ToDateTime(); + var key = p["Q"]; + + var whereExpression = Entity.SearchWhereByKeys(key); + if (start > DateTime.MinValue || end > DateTime.MinValue) + { + var masterTime = Factory.MasterTime; + if (masterTime != null) + whereExpression &= masterTime.Between(start, end); + } + + //// 根据模型列设置,拼接作为搜索字段的字段 + //var modelTable = ModelTable; + //var modelCols = modelTable?.GetColumns()?.Where(w => w.ShowInSearch)?.ToList() ?? new List(); + + //foreach (var col in modelCols) + //{ + // var val = p[col.Name]; + // if (val.IsNullOrWhiteSpace()) continue; + // whereExpression &= col.Field == val; + //} + + //添加映射字段查询 + foreach (var item in Factory.Fields) + { + var val = p[item.Name]; + if (!val.IsNullOrWhiteSpace()) + { + whereExpression &= item.Equal(val); + } + } + + return Entity.FindAll(whereExpression, p); + } + + /// 搜索数据,支持数据权限 + /// + /// + protected virtual IEnumerable SearchData(Pager p) + { + // 数据权限 + var builder = CreateWhere(); + if (builder != null) + { + builder.Data2 ??= p.Items; + p.State = builder; + } + + // 数字型主键,默认降序 + if (PageSetting.OrderByKey && p.Sort.IsNullOrEmpty() && p.OrderBy.IsNullOrEmpty()) + { + var uk = Factory.Unique; + if (uk != null && uk.Type.IsInt()) + { + p.OrderBy = uk.Desc(); + } + } + + return Search(p); + } + + /// 查找单行数据 + /// + /// + protected virtual TEntity Find(Object key) + { + // 分表需要特殊处理 + var fact = Factory; + var shardField = fact.ShardPolicy?.Field; + if (shardField != null) + { + var dt = GetRequest(shardField.Name).ToDateTime(); + if (dt.Year > 2000) + { + var entity = new TEntity(); + entity[fact.Unique.Name] = key; + entity[shardField.Name] = dt; + return FindByKey(entity); + } + } + + return FindByKey(key); + } + + private TEntity FindByKey(Object key) + { + var fact = Factory; + if (fact.Unique == null) + { + var pks = fact.Table.PrimaryKeys; + if (pks.Length > 0) + { + var exp = new WhereExpression(); + foreach (var item in pks) + { + // 如果前端没有传值,则不要参与构造查询 + var val = GetRequest(item.Name); + + // 2021.04.18 添加 + // 表结构没有唯一键,只有联合主键,并且id是其中一个主键, + // 而id作为路由参数,上面从Request中获取到空值, + // 最终导致联合主键的表查询单条数据,只用到名称为非id的主键 + if (val == null && item.Name.EqualIgnoreCase("id")) val = key.ToString(); + + if (val != null) exp &= item.Equal(val); + } + + return Entity.Find(exp); + } + } + + return Entity.FindByKeyForEdit(key); + } + + /// 查找单行数据,并判断数据权限 + /// + /// + protected TEntity FindData(Object key) + { + // 先查出来,再判断数据权限 + var entity = Find(key); + if (entity != null) + { + // 数据权限 + var builder = CreateWhere(); + if (builder != null && !builder.Eval(entity)) throw new InvalidOperationException($"非法访问数据[{key}]"); + } + + return entity; + } + + /// 创建查询条件构造器,主要用于数据权限 + /// + protected virtual WhereBuilder CreateWhere() + { + var exp = ""; + var att = GetType().GetCustomAttribute(); + if (att != null) + { + // 已登录用户判断系统角色,未登录时不判断 + var user = HttpContext.Items["CurrentUser"] as IUser; + user ??= ManageProvider.User; + if (user == null || !user.Roles.Any(e => e.IsSystem) && !att.Valid(user.Roles)) + exp = att.Expression; + } + + // 多租户 + var set = CubeSetting.Current; + if (set.EnableTenant) + { + var ctxTenant = TenantContext.Current; + if (ctxTenant != null && IsTenantSource) + { + var tenant = Tenant.FindById(ctxTenant.TenantId); + if (tenant != null) + { + HttpContext.Items["TenantId"] = tenant.Id; + + if (typeof(TEntity) == typeof(Tenant)) + { + if (!exp.IsNullOrEmpty()) + exp = "Id={#TenantId} and " + exp; + else + exp = "Id={#TenantId}"; + } + else + { + if (!exp.IsNullOrEmpty()) + exp = "TenantId={#TenantId} and " + exp; + else + exp = "TenantId={#TenantId}"; + } + } + } + } + + if (exp.IsNullOrEmpty()) return null; + + var builder = new WhereBuilder + { + Factory = Factory, + Expression = exp, + }; + builder.SetData(Session); + builder.SetData2(HttpContext.Items.ToDictionary(e => e.Key + "", e => e.Value)); + + return builder; + } + + /// 是否租户实体类 + protected virtual Boolean IsTenantSource => typeof(TEntity).GetInterfaces().Any(e => e == typeof(ITenantSource)); + + /// 获取选中键 + /// + protected virtual String[] SelectKeys => GetRequest("Keys")?.Split(","); + + /// 多次导出数据 + /// + protected virtual IEnumerable ExportData(Int32 max = 0) + { + var set = CubeSetting.Current; + if (max <= 0) max = set.MaxExport; + + // 计算目标数据量 + var p = Session[CacheKey] as Pager; + p = new Pager(p) + { + RetrieveTotalCount = true, + PageIndex = 1, + PageSize = 1, + }; + SearchData(p); + p.PageSize = 20_000; + + //!!! 数据量很大,且有时间条件时,采用时间分片导出。否则统一分页导出 + //if (Factory.Count > 100_000) + if (p.TotalCount > 100_000) + { + var start = p["dtStart"].ToDateTime(); + var end = p["dtEnd"].ToDateTime(); + if (start.Year > 2000 /*&& end.Year > 2000*/) + { + if (end.Year < 2000) end = DateTime.Now; + + // 计算步进,80%数据集中在20%时间上,凑够每页10000 + //var speed = (p.TotalCount * 0.8) / (24 * 3600 * 0.2); + var speed = (Double)p.TotalCount / (24 * 3600); + var step = p.PageSize / speed; + + XTrace.WriteLine("[{0}]导出数据[{1:n0}],时间区间({2},{3}),分片步进{4:n0}秒", Factory.EntityType.FullName, p.TotalCount, start, end, step); + + return ExportDataByDatetime((Int32)step, max); + } + } + + XTrace.WriteLine("[{0}]导出数据[{1:n0}],共[{2:n0}]页", Factory.EntityType.FullName, p.TotalCount, p.PageCount); + + return ExportDataByPage(p.PageSize, max); + } + + /// 分页导出数据 + /// 页大小。默认10_000 + /// 最大行数 + /// + protected virtual IEnumerable ExportDataByPage(Int32 pageSize, Int32 max) + { + // 跳过头部一些页数,导出当前页以及以后的数据 + var p = Session[CacheKey] as Pager; + p = new Pager(p) + { + // 不要查记录数 + RetrieveTotalCount = false, + PageIndex = 1, + PageSize = pageSize + }; + + while (max > 0) + { + if (HttpContext.RequestAborted.IsCancellationRequested) yield break; + if (p.PageSize > max) p.PageSize = max; + + var list = SearchData(p); + + var count = list.Count(); + if (count == 0) break; + max -= count; + + foreach (var item in list) + { + yield return item; + } + + if (count < p.PageSize) break; + + p.PageIndex++; + } + + // 回收内存 + GC.Collect(); + } + + /// 时间分片导出数据 + /// 分片不仅。默认60 + /// 最大行数 + /// + protected virtual IEnumerable ExportDataByDatetime(Int32 step, Int32 max) + { + // 跳过头部一些页数,导出当前页以及以后的数据 + var p = Session[CacheKey] as Pager; + p = new Pager(p) + { + // 不要查记录数 + RetrieveTotalCount = false, + PageIndex = 1, + PageSize = 0, + }; + + var start = p["dtStart"].ToDateTime(); + var end = p["dtEnd"].ToDateTime(); + if (end.Year < 2000) end = DateTime.Now; + + //!!! 前后同一天必须查跨天 + if (start == start.Date && end == end.Date) end = end.AddDays(1); + + var dt = start; + while (max > 0 && dt < end) + { + if (HttpContext.RequestAborted.IsCancellationRequested) yield break; + + var dt2 = dt.AddSeconds(step); + if (dt2 > end) dt2 = end; + + p["dtStart"] = dt.ToFullString(); + p["dtEnd"] = dt2.ToFullString(); + + var list = SearchData(p); + + var count = list.Count(); + //if (count == 0) break; + + foreach (var item in list) + { + yield return item; + } + + dt = dt2; + max -= count; + } + + // 回收内存 + GC.Collect(); + } + #endregion + + #region 实体操作重载 + /// 验证实体对象 + /// 实体对象 + /// 操作类型 + /// 是否提交数据阶段 + /// + protected virtual Boolean Valid(TEntity entity, DataObjectMethodType type, Boolean post) + { + if (!ValidPermission(entity, type, post)) + { + switch (type) + { + case DataObjectMethodType.Select: throw new NoPermissionException(PermissionFlags.Detail, "无权查看数据"); + case DataObjectMethodType.Update: throw new NoPermissionException(PermissionFlags.Update, "无权更新数据"); + case DataObjectMethodType.Insert: throw new NoPermissionException(PermissionFlags.Insert, "无权新增数据"); + case DataObjectMethodType.Delete: throw new NoPermissionException(PermissionFlags.Delete, "无权删除数据"); + } + } + + if (post && LogOnChange) + { + // 必须提前写修改日志,否则修改后脏数据失效,保存的日志为空 + switch (type) + { + case DataObjectMethodType.Insert: + case DataObjectMethodType.Delete: + case DataObjectMethodType.Update when (entity as IEntity).HasDirty: + LogProvider.Provider.WriteLog(type + "", entity); + break; + } + } + + return true; + } + + /// 验证实体对象 + /// 实体对象 + /// 操作类型 + /// 是否提交数据阶段 + /// + protected virtual Boolean ValidPermission(TEntity entity, DataObjectMethodType type, Boolean post) => true; + #endregion + + #region 列表字段和表单字段 + private static FieldCollection _ListFields; + /// 列表字段过滤 + protected static FieldCollection ListFields => _ListFields ??= new FieldCollection(Factory, ViewKinds.List); + + //private static FieldCollection _FormFields; + ///// 表单字段过滤 + //[Obsolete] + //protected static FieldCollection FormFields => _FormFields ??= new FieldCollection(Factory, "Form"); + + private static FieldCollection _AddFormFields; + /// 表单字段过滤 + protected static FieldCollection AddFormFields => _AddFormFields ??= new FieldCollection(Factory, ViewKinds.AddForm); + + private static FieldCollection _EditFormFields; + /// 表单字段过滤 + protected static FieldCollection EditFormFields => _EditFormFields ??= new FieldCollection(Factory, ViewKinds.EditForm); + + private static FieldCollection _DetailFields; + /// 表单字段过滤 + protected static FieldCollection DetailFields => _DetailFields ??= new FieldCollection(Factory, ViewKinds.Detail); + + private static FieldCollection _SearchFields; + /// 搜索字段过滤 + protected static FieldCollection SearchFields => _SearchFields ??= new FieldCollection(Factory, ViewKinds.Search); + + /// 获取字段信息。支持用户重载并根据上下文定制界面 + /// 字段类型:1-列表List、2-详情Detail、3-添加AddForm、4-编辑EditForm、5-搜索Search + /// 获取字段列表时的相关模型,可能是实体对象或实体列表,可依次来定制要显示的字段 + /// + protected virtual FieldCollection OnGetFields(ViewKinds kind, Object model) + { + var fields = kind switch + { + ViewKinds.List => ListFields, + ViewKinds.Detail => DetailFields, + ViewKinds.AddForm => AddFormFields, + ViewKinds.EditForm => EditFormFields, + ViewKinds.Search => SearchFields, + _ => ListFields, + }; + fields = fields.Clone(); + + // 表单嵌入配置字段 + if (kind == ViewKinds.EditForm && model is TEntity entity) + { + // 获取参数对象,展开参数,作为表单字段 + foreach (var item in fields.ToArray()) + { + if (item is FormField ef && ef.GetExpand != null) + { + var p = ef.GetExpand(entity); + if (p != null && p is not String) + { + if (!ef.RetainExpand) fields.Remove(ef); + + fields.Expand(entity, p, ef.Name + "_"); + } + } + } + } + + return fields; + } + #endregion +} \ No newline at end of file diff --git a/NewLife.CubeNC/Common/ReadOnlyEntityController.cs b/NewLife.CubeNC/Common/ReadOnlyEntityController.cs index 31485633..4e65f831 100644 --- a/NewLife.CubeNC/Common/ReadOnlyEntityController.cs +++ b/NewLife.CubeNC/Common/ReadOnlyEntityController.cs @@ -29,39 +29,9 @@ namespace NewLife.Cube; /// 只读实体控制器基类 /// -public class ReadOnlyEntityController : ControllerBaseX where TEntity : Entity, new() +public partial class ReadOnlyEntityController : ControllerBaseX where TEntity : Entity, new() { - #region 属性 - /// 实体工厂 - public static IEntityFactory Factory => Entity.Meta.Factory; - - /// 实体改变时写日志。默认false - protected static Boolean LogOnChange { get; set; } - - /// 系统配置 - public SysConfig SysConfig { get; set; } - - /// 当前列表页的查询条件缓存Key - private static String CacheKey => $"CubeView_{typeof(TEntity).FullName}"; - #endregion - #region 构造 - static ReadOnlyEntityController() - { - // 强行实例化一次,初始化实体对象 - var entity = new TEntity(); - } - - /// 构造函数 - public ReadOnlyEntityController() - { - PageSetting.IsReadOnly = true; - - PageSetting.EnableTableDoubleClick = CubeSetting.Current.EnableTableDoubleClick; - - SysConfig = SysConfig.Current; - } - /// 动作执行前 /// public override void OnActionExecuting(ActionExecutingContext filterContext) @@ -83,7 +53,7 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) } } - var title = GetType().GetDisplayName() ?? typeof(TEntity).GetDisplayName() ?? Entity.Meta.Table.DataTable.DisplayName; + var title = GetType().GetDisplayName() ?? typeof(TEntity).GetDisplayName() ?? Factory.Table.DataTable.DisplayName; ViewBag.Title = title; // Ajax请求不需要设置ViewBag @@ -102,7 +72,7 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) var txt = (String)ViewBag.HeaderContent; if (txt.IsNullOrEmpty()) txt = Menu?.Remark; if (txt.IsNullOrEmpty()) txt = GetType().GetDescription(); - if (txt.IsNullOrEmpty()) txt = Entity.Meta.Table.Description; + if (txt.IsNullOrEmpty()) txt = Factory.Table.Description; //if (txt.IsNullOrEmpty() && SysConfig.Current.Develop) // txt = "这里是页头内容,来自于菜单备注,或者给控制器增加Description特性"; ViewBag.HeaderContent = txt; @@ -122,363 +92,6 @@ public override void OnActionExecuted(ActionExecutedContext filterContext) } #endregion - #region 数据获取 - /// 搜索数据集 - /// - /// - protected virtual IEnumerable Search(Pager p) - { - var start = p["dtStart"].ToDateTime(); - var end = p["dtEnd"].ToDateTime(); - var key = p["Q"]; - - var whereExpression = Entity.SearchWhereByKeys(key); - if (start > DateTime.MinValue || end > DateTime.MinValue) - { - var masterTime = Factory.MasterTime; - if (masterTime != null) - whereExpression &= masterTime.Between(start, end); - } - - //// 根据模型列设置,拼接作为搜索字段的字段 - //var modelTable = ModelTable; - //var modelCols = modelTable?.GetColumns()?.Where(w => w.ShowInSearch)?.ToList() ?? new List(); - - //foreach (var col in modelCols) - //{ - // var val = p[col.Name]; - // if (val.IsNullOrWhiteSpace()) continue; - // whereExpression &= col.Field == val; - //} - - //添加映射字段查询 - foreach (var item in Factory.Fields) - { - var val = p[item.Name]; - if (!val.IsNullOrWhiteSpace()) - { - whereExpression &= item.Equal(val); - } - } - - return Entity.FindAll(whereExpression, p); - } - - /// 搜索数据,支持数据权限 - /// - /// - protected virtual IEnumerable SearchData(Pager p) - { - // 缓存数据,用于后续导出 - //SetSession(CacheKey, p); - //Session[CacheKey] = p; - - // 数据权限 - var builder = CreateWhere(); - if (builder != null) - { - builder.Data2 ??= p.Items; - p.State = builder; - } - - // 数字型主键,默认降序 - if (PageSetting.OrderByKey && p.Sort.IsNullOrEmpty() && p.OrderBy.IsNullOrEmpty()) - { - var uk = Factory.Unique; - if (uk != null && uk.Type.IsInt()) - { - p.OrderBy = uk.Desc(); - //p.Sort = uk.Name; - //p.Desc = true; - } - } - - return Search(p); - } - - /// 查找单行数据 - /// - /// - protected virtual TEntity Find(Object key) - { - // 分表需要特殊处理 - var fact = Factory; - var shardField = fact.ShardPolicy?.Field; - if (shardField != null) - { - var dt = GetRequest(shardField.Name).ToDateTime(); - if (dt.Year > 2000) - { - //return fact.AutoShard(dt, dt.AddSeconds(1), () => FindByKey(key)).FirstOrDefault(); - //using var split = fact.CreateShard(dt); - //var unique = fact.Unique; - //if (unique != null) - // return Entity.Find(unique.Equal(key)); - //else - // return FindByKey(key); - - var entity = new TEntity(); - entity[fact.Unique.Name] = key; - entity[shardField.Name] = dt; - return FindByKey(entity); - } - } - - return FindByKey(key); - } - - private TEntity FindByKey(Object key) - { - var fact = Factory; - if (fact.Unique == null) - { - var pks = fact.Table.PrimaryKeys; - if (pks.Length > 0) - { - var exp = new WhereExpression(); - foreach (var item in pks) - { - // 如果前端没有传值,则不要参与构造查询 - var val = GetRequest(item.Name); - - // 2021.04.18 添加 - // 表结构没有唯一键,只有联合主键,并且id是其中一个主键, - // 而id作为路由参数,上面从Request中获取到空值, - // 最终导致联合主键的表查询单条数据,只用到名称为非id的主键 - if (val == null && item.Name.EqualIgnoreCase("id")) val = key.ToString(); - - if (val != null) exp &= item.Equal(val); - } - - return Entity.Find(exp); - } - } - - return Entity.FindByKeyForEdit(key); - } - - /// 查找单行数据,并判断数据权限 - /// - /// - protected TEntity FindData(Object key) - { - // 先查出来,再判断数据权限 - var entity = Find(key); - if (entity != null) - { - // 数据权限 - var builder = CreateWhere(); - if (builder != null && !builder.Eval(entity)) throw new InvalidOperationException($"非法访问数据[{key}]"); - } - - return entity; - } - - /// 创建查询条件构造器,主要用于数据权限 - /// - protected virtual WhereBuilder CreateWhere() - { - var exp = ""; - var att = GetType().GetCustomAttribute(); - if (att != null) - { - // 已登录用户判断系统角色,未登录时不判断 - var user = HttpContext.Items["CurrentUser"] as IUser; - user ??= ManageProvider.User; - if (user == null || !user.Roles.Any(e => e.IsSystem) && !att.Valid(user.Roles)) - exp = att.Expression; - } - - // 多租户 - var set = CubeSetting.Current; - if (set.EnableTenant) - { - var ctxTenant = TenantContext.Current; - if (ctxTenant != null && IsTenantSource) - { - var tenant = Tenant.FindById(ctxTenant.TenantId); - if (tenant != null) - { - HttpContext.Items["TenantId"] = tenant.Id; - - if (typeof(TEntity) == typeof(Tenant)) - { - if (!exp.IsNullOrEmpty()) - exp = "Id={#TenantId} and " + exp; - else - exp = "Id={#TenantId}"; - } - else - { - if (!exp.IsNullOrEmpty()) - exp = "TenantId={#TenantId} and " + exp; - else - exp = "TenantId={#TenantId}"; - } - } - } - } - - if (exp.IsNullOrEmpty()) return null; - - var builder = new WhereBuilder - { - Factory = Factory, - Expression = exp, - //Data = Session, - }; - builder.SetData(Session); - //builder.Data2 = new ItemsExtend { Items = HttpContext.Items }; - builder.SetData2(HttpContext.Items.ToDictionary(e => e.Key + "", e => e.Value)); - - return builder; - } - - /// 是否租户实体类 - public Boolean IsTenantSource => typeof(TEntity).GetInterfaces().Any(e => e == typeof(ITenantSource)); - - /// 获取选中键 - /// - protected virtual String[] SelectKeys => GetRequest("Keys")?.Split(","); - - /// 多次导出数据 - /// - protected virtual IEnumerable ExportData(Int32 max = 0) - { - var set = CubeSetting.Current; - if (max <= 0) max = set.MaxExport; - - // 计算目标数据量 - var p = Session[CacheKey] as Pager; - p = new Pager(p) - { - RetrieveTotalCount = true, - PageIndex = 1, - PageSize = 1, - }; - SearchData(p); - p.PageSize = 20_000; - - //!!! 数据量很大,且有时间条件时,采用时间分片导出。否则统一分页导出 - //if (Factory.Count > 100_000) - if (p.TotalCount > 100_000) - { - var start = p["dtStart"].ToDateTime(); - var end = p["dtEnd"].ToDateTime(); - if (start.Year > 2000 /*&& end.Year > 2000*/) - { - if (end.Year < 2000) end = DateTime.Now; - - // 计算步进,80%数据集中在20%时间上,凑够每页10000 - //var speed = (p.TotalCount * 0.8) / (24 * 3600 * 0.2); - var speed = (Double)p.TotalCount / (24 * 3600); - var step = p.PageSize / speed; - - XTrace.WriteLine("[{0}]导出数据[{1:n0}],时间区间({2},{3}),分片步进{4:n0}秒", Factory.EntityType.FullName, p.TotalCount, start, end, step); - - return ExportDataByDatetime((Int32)step, max); - } - } - - XTrace.WriteLine("[{0}]导出数据[{1:n0}],共[{2:n0}]页", Factory.EntityType.FullName, p.TotalCount, p.PageCount); - - return ExportDataByPage(p.PageSize, max); - } - - /// 分页导出数据 - /// 页大小。默认10_000 - /// 最大行数。默认10_000_000 - /// - protected virtual IEnumerable ExportDataByPage(Int32 pageSize, Int32 max) - { - // 跳过头部一些页数,导出当前页以及以后的数据 - var p = Session[CacheKey] as Pager; - p = new Pager(p) - { - // 不要查记录数 - RetrieveTotalCount = false, - PageIndex = 1, - PageSize = pageSize - }; - - while (max > 0) - { - if (HttpContext.RequestAborted.IsCancellationRequested) yield break; - if (p.PageSize > max) p.PageSize = max; - - var list = SearchData(p); - - var count = list.Count(); - if (count == 0) break; - max -= count; - - foreach (var item in list) - { - yield return item; - } - - if (count < p.PageSize) break; - - p.PageIndex++; - } - - // 回收内存 - GC.Collect(); - } - - /// 时间分片导出数据 - /// 分片不仅。默认60 - /// 最大行数。默认10_000_000 - /// - protected virtual IEnumerable ExportDataByDatetime(Int32 step, Int32 max) - { - // 跳过头部一些页数,导出当前页以及以后的数据 - var p = Session[CacheKey] as Pager; - p = new Pager(p) - { - // 不要查记录数 - RetrieveTotalCount = false, - PageIndex = 1, - PageSize = 0, - }; - - var start = p["dtStart"].ToDateTime(); - var end = p["dtEnd"].ToDateTime(); - if (end.Year < 2000) end = DateTime.Now; - - //!!! 前后同一天必须查跨天 - if (start == start.Date && end == end.Date) end = end.AddDays(1); - - var dt = start; - while (max > 0 && dt < end) - { - if (HttpContext.RequestAborted.IsCancellationRequested) yield break; - - var dt2 = dt.AddSeconds(step); - if (dt2 > end) dt2 = end; - - p["dtStart"] = dt.ToFullString(); - p["dtEnd"] = dt2.ToFullString(); - - var list = SearchData(p); - - var count = list.Count(); - //if (count == 0) break; - - foreach (var item in list) - { - yield return item; - } - - dt = dt2; - max -= count; - } - - // 回收内存 - GC.Collect(); - } - #endregion - #region 默认Action /// 数据列表首页 /// @@ -1252,114 +865,7 @@ private String GetProjectRoot() } #endregion - #region 实体操作重载 - /// 验证实体对象 - /// 实体对象 - /// 操作类型 - /// 是否提交数据阶段 - /// - protected virtual Boolean Valid(TEntity entity, DataObjectMethodType type, Boolean post) - { - if (!ValidPermission(entity, type, post)) - { - switch (type) - { - case DataObjectMethodType.Select: throw new NoPermissionException(PermissionFlags.Detail, "无权查看数据"); - case DataObjectMethodType.Update: throw new NoPermissionException(PermissionFlags.Update, "无权更新数据"); - case DataObjectMethodType.Insert: throw new NoPermissionException(PermissionFlags.Insert, "无权新增数据"); - case DataObjectMethodType.Delete: throw new NoPermissionException(PermissionFlags.Delete, "无权删除数据"); - } - } - - if (post && LogOnChange) - { - // 必须提前写修改日志,否则修改后脏数据失效,保存的日志为空 - switch (type) - { - case DataObjectMethodType.Insert: - case DataObjectMethodType.Delete: - case DataObjectMethodType.Update when (entity as IEntity).HasDirty: - LogProvider.Provider.WriteLog(type + "", entity); - break; - } - } - - return true; - } - - /// 验证实体对象 - /// 实体对象 - /// 操作类型 - /// 是否提交数据阶段 - /// - protected virtual Boolean ValidPermission(TEntity entity, DataObjectMethodType type, Boolean post) => true; - #endregion - #region 列表字段和表单字段 - private static FieldCollection _ListFields; - /// 列表字段过滤 - protected static FieldCollection ListFields => _ListFields ??= new FieldCollection(Factory, ViewKinds.List); - - //private static FieldCollection _FormFields; - ///// 表单字段过滤 - //[Obsolete] - //protected static FieldCollection FormFields => _FormFields ??= new FieldCollection(Factory, "Form"); - - private static FieldCollection _AddFormFields; - /// 表单字段过滤 - protected static FieldCollection AddFormFields => _AddFormFields ??= new FieldCollection(Factory, ViewKinds.AddForm); - - private static FieldCollection _EditFormFields; - /// 表单字段过滤 - protected static FieldCollection EditFormFields => _EditFormFields ??= new FieldCollection(Factory, ViewKinds.EditForm); - - private static FieldCollection _DetailFields; - /// 表单字段过滤 - protected static FieldCollection DetailFields => _DetailFields ??= new FieldCollection(Factory, ViewKinds.Detail); - - private static FieldCollection _SearchFields; - /// 搜索字段过滤 - protected static FieldCollection SearchFields => _SearchFields ??= new FieldCollection(Factory, ViewKinds.Search); - - /// 获取字段信息。支持用户重载并根据上下文定制界面 - /// 字段类型:1-列表List、2-详情Detail、3-添加AddForm、4-编辑EditForm、5-搜索Search - /// 获取字段列表时的相关模型,可能是实体对象或实体列表,可依次来定制要显示的字段 - /// - protected virtual FieldCollection OnGetFields(ViewKinds kind, Object model) - { - var fields = kind switch - { - ViewKinds.List => ListFields, - ViewKinds.Detail => DetailFields, - ViewKinds.AddForm => AddFormFields, - ViewKinds.EditForm => EditFormFields, - ViewKinds.Search => SearchFields, - _ => ListFields, - }; - fields = fields.Clone(); - - // 表单嵌入配置字段 - if (kind == ViewKinds.EditForm && model is TEntity entity) - { - // 获取参数对象,展开参数,作为表单字段 - foreach (var item in fields.ToArray()) - { - if (item is FormField ef && ef.GetExpand != null) - { - var p = ef.GetExpand(entity); - if (p != null && p is not String) - { - if (!ef.RetainExpand) fields.Remove(ef); - - fields.Expand(entity, p, ef.Name + "_"); - } - } - } - } - - return fields; - } - /// 获取字段信息。支持用户重载并根据上下文定制界面 /// 字段类型:1-列表List、2-详情Detail、3-添加AddForm、4-编辑EditForm、5-搜索Search /// @@ -1372,47 +878,6 @@ public virtual ActionResult GetFields(ViewKinds kind) return new JsonResult(data); } - - ///// - ///// 实体过滤器,根据模型列的表单显示类型,不显示的字段去掉 - ///// - ///// - ///// - ///// - //protected virtual IDictionary OnFilter(IModel model, ViewKinds kind) - //{ - // if (model == null) return null; - - // var dic = new Dictionary(); - // var fields = OnGetFields(kind, model); - // if (fields != null) - // { - // var names = Factory.FieldNames; - // foreach (var field in fields) - // { - // if (!field.Name.IsNullOrEmpty() && names.Contains(field.Name)) - // dic[field.Name] = model[field.Name]; - // } - // } - - // return dic; - //} - - ///// - ///// 实体列表过滤器,根据模型列的列表页显示类型,不显示的字段去掉 - ///// - ///// - ///// - ///// - //protected virtual IEnumerable> OnFilter(IEnumerable models, ViewKinds kind) - //{ - // if (models == null) yield break; - - // foreach (var item in models) - // { - // yield return OnFilter(item, kind); - // } - //} #endregion #region 图表 diff --git a/NewLife.CubeNC/NewLife.CubeNC.csproj b/NewLife.CubeNC/NewLife.CubeNC.csproj index 2d863538..76259069 100644 --- a/NewLife.CubeNC/NewLife.CubeNC.csproj +++ b/NewLife.CubeNC/NewLife.CubeNC.csproj @@ -65,6 +65,7 @@ + diff --git a/NewLife.CubeNC/ViewModels/FormField.cs b/NewLife.CubeNC/ViewModels/FormField.cs index 3a532094..bf705420 100644 --- a/NewLife.CubeNC/ViewModels/FormField.cs +++ b/NewLife.CubeNC/ViewModels/FormField.cs @@ -11,26 +11,11 @@ public class FormField : DataField #if MVC /// MVC特有,表单字段的分部视图名称,不要.cshtml后缀。对标_Form_Group,允许针对字段定义视图 public String GroupView { get; set; } +#endif /// 获取扩展字段委托。当前字段所表示的对象,各属性作为表单字段展开 public GetExpandDelegate GetExpand { get; set; } /// 保留扩展字段。默认false,字段被扩展以后,表单上就不再出现原字段 public Boolean RetainExpand { get; set; } -#endif - - #region 方法 - ///// 克隆 - ///// - //public override DataField Clone() - //{ - // var df = base.Clone(); - // if (df is FormField ff) - // { - // ff.GroupView = GroupView; - // } - - // return df; - //} - #endregion } \ No newline at end of file