起因

在测试一个例子时发现的问题,这个示例实现的功能是刷新页面也能保持表格锁定列的状态,先看下页面的完成效果:

<http://core.fineui.com/#/GridLockColumn/SaveToDB>

 

测试中发现,几乎相同的代码:

* 在 FineUIMvc(Net Framework)下没有问题:
http://mvc.fineui.com/#/GridLockColumn/SaveToDB
<http://mvc.fineui.com/#/GridLockColumn/SaveToDB>
* 但是在 FineUICore(Net Core)下就失效了,刷新页面后锁定列状态丢失:
http://core.fineui.com/#/GridLockColumn/SaveToDB
<http://core.fineui.com/#/GridLockColumn/SaveToDB>
 

这个例子使用了 Session 来保存表格的锁定状态,先来看下页面视图的定义:
@(F.Grid().IsFluid(true).CssClass("blockpanel").Title("表格").ShowHeader(true
).ShowBorder(true).ID("Grid1").DataIDField("Id").DataTextField("Name"
).AllowColumnLocking(true) .Columns( F.RowNumberField(),
F.RenderField().HeaderText("姓名").DataField("Name").Width(100).EnableLock(true
).Locked(true), F.RenderField().HeaderText("性别").DataField("Gender"
).FieldType(FieldType.Int).RendererFunction("renderGender").Width(80
).EnableLock(true), F.RenderField().HeaderText("入学年份").DataField("EntranceYear"
).FieldType(FieldType.Int).Width(100).EnableLock(true),
F.RenderCheckField().HeaderText("是否在校").DataField("AtSchool"
).RenderAsStaticField(true).Width(100).EnableLock(true),
F.RenderField().HeaderText("所学专业").DataField("Major").RendererFunction("
renderMajor").Width(300).EnableLock(true), F.RenderField().HeaderText("分组"
).DataField("Group").RendererFunction("renderGroup").Width(80).EnableLock(true
), F.RenderField().HeaderText("注册日期").DataField("LogTime"
).FieldType(FieldType.Date).Renderer(Renderer.Date).RendererArgument("yyyy-MM-dd
").Width(100).EnableLock(true) ).Listener("columnlock", "onGridColumnLock"
).Listener("columnunlock", "onGridColumnUnlock")
.DataSource(DataSourceUtil.GetDataTable()) )
 

在客户端事件 columnlock 和 columnunlock 中,会将锁定列的状态改变回发到后台:
function onGridColumnLock(event, columnId) { // 触发后台事件
F.doPostBack('@Url.Action("Grid1_ColumnLockUnlock")', { type: 'lock', columnId:
columnId }); }function onGridColumnUnlock(event, columnId) { // 触发后台事件
F.doPostBack('@Url.Action("Grid1_ColumnLockUnlock")', { type: 'unlock',
columnId: columnId }); }
 

后台会将列状态信息保存到 Session 中(实际项目中是要保存到数据库中的):
[HttpPost] [ValidateAntiForgeryToken] public ActionResult
Grid1_ColumnLockUnlock(string type, string columnId) { // 模拟操作数据库中的数据 List<
string> lockedColumns = GetLockedColumns(); if (type == "lock") { if (!
lockedColumns.Contains(columnId)) { lockedColumns.Add(columnId); } }else if
(type =="unlock") { if (lockedColumns.Contains(columnId)) {
lockedColumns.Remove(columnId); } }return UIHelper.Result(); } private static
readonly string KEY_FOR_DATASOURCE_SESSION = "GridLockColumn.SaveToDB"; //
模拟在服务器端保存数据// 特别注意:在真实的开发环境中,不要在Session放置大量数据,否则会严重影响服务器性能 private List<string>
GetLockedColumns() {if (Session[KEY_FOR_DATASOURCE_SESSION] == null) {
Session[KEY_FOR_DATASOURCE_SESSION]= new List<string>() { }; } return (List<
string>)Session[KEY_FOR_DATASOURCE_SESSION]; }
当然,上面对 Session 的操作是在 FineUIMvc(ASP.NET MVC) 中的代码,也就是运行在 .Net  Framework 下的代码。

 

FineUICore(ASP.NET Core)中的代码稍微不同,如下所示:
[HttpPost] [ValidateAntiForgeryToken] public IActionResult
Grid1_ColumnLockUnlock(string type, string columnId) { // 模拟操作数据库中的数据 List<
string> lockedColumns = GetLockedColumns(); if (type == "lock") { if (!
lockedColumns.Contains(columnId)) { lockedColumns.Add(columnId); } }else if
(type =="unlock") { if (lockedColumns.Contains(columnId)) {
lockedColumns.Remove(columnId); } }return UIHelper.Result(); } private static
readonly string KEY_FOR_DATASOURCE_SESSION = "GridLockColumn.SaveToDB"; //
模拟在服务器端保存数据// 特别注意:在真实的开发环境中,不要在Session放置大量数据,否则会严重影响服务器性能 private List<string>
GetLockedColumns() {if (HttpContext.Session.GetObject<List<string
>>(KEY_FOR_DATASOURCE_SESSION) ==null) {
HttpContext.Session.SetObject(KEY_FOR_DATASOURCE_SESSION,new List<string>() {
}); }return HttpContext.Session.GetObject<List<string>>
(KEY_FOR_DATASOURCE_SESSION); }
 

上面是保存状态的逻辑,而刷新页面后,会从Session中读取保存的列锁定状态:
// GET: GridLockColumn/SaveToDB public ActionResult Index() { LoadData();
return View(); } private void LoadData() { ViewBag.LockedColumns =
GetLockedColumns(); }
然后,在页面视图中,将保存的列锁定状态设置到表格上,如下所示:
@{ Grid grid1 = F.GetControl<Grid>("Grid1"); List<string> lockedColumns =
ViewBag.LockedColumnsas List<string>; if (lockedColumns.Count > 0) { foreach
(GridColumn columnin grid1.Columns) { RenderBaseField field = column as
RenderBaseField;if (field == null) { continue; } if
(lockedColumns.Contains(field.ColumnID) ||
lockedColumns.Contains(field.DataField)) { field.Locked= true; } } } }
 

至此,整个流程全部完成。问题是,几乎一模一样的代码,为什么在 .Net Framework 下一切正常,而 .Net Core 下却出问题了?

 

溯源

经过代码调试,我们发现,在 .Net Core 下将状态保存到 Session 中后,再去 Session 中检查却不存在!

后来才发现,我们过于相信引用类型了,请看如下代码:
// 模拟操作数据库中的数据 List<string> lockedColumns = GetLockedColumns(); if (type == "
lock") { if (!lockedColumns.Contains(columnId)) { lockedColumns.Add(columnId);
} }else if (type == "unlock") { if (lockedColumns.Contains(columnId)) {
lockedColumns.Remove(columnId); } }
有过面向对象编程经验的同学都知道,lockedColumns实际上是Session中的一个对象引用,因此下面对此对象的 Add 和 Remove
操作会直接改变 Session 中的对象。

为什么 .Net Core 下,这个逻辑就失效了?

 

我第一个想到的是深拷贝,莫非下面的代码返回了一个 Session 对象的深拷贝?
HttpContext.Session.GetObject<List<string>>(KEY_FOR_DATASOURCE_SESSION)
 

转到 GetObject 方法的定义,我却发现自己的忘性有多大,却原来 GetObject 是自己很久之前定义的一个扩展方法,.Net
Core本身并没有定义这个方法,我们来看一眼:
using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using System; using
System.Collections.Generic;using System.Text; namespace FineUICore { ///
<summary> /// Session扩展 /// </summary> public static class SessionExtension {
/// <summary> /// 设置Session对象 /// </summary> /// <typeparam
name="T"></typeparam> /// <param name="session"></param> /// <param
name="key"></param> /// <param name="obj"></param> public static void
SetObject<T>(this ISession session, string key, T obj) { session.SetString(key,
JsonConvert.SerializeObject(obj)); }/// <summary> /// 获取Session对象 /// </summary>
/// <typeparam name="T"></typeparam> /// <param name="session"></param> ///
<param name="key"></param> /// <returns></returns> public static T GetObject<T>(
this ISession session, string key) { T result = default(T); var value =
session.GetString(key);if(!String.IsNullOrEmpty(value)) { result =
JsonConvert.DeserializeObject<T>(value); } return result; } } }
 

为什么 Session 中保存个对象还要通过JSON字符串中转?

原来 .Net Core 中原生只提供了在 Session 中保存字符串和 byte 数组的支持,想要保存复杂类型,只能自己写扩展方法了。

而这个扩展方法 GetObject 返回的Session对象的确像是一个深度拷贝的对象,因此对于它的 Add 和 Remove 并不会影响 Session
中实际存储的 JSON字符串。

 

至此,问题已经很明朗了,我们再来复习下 ASP.NET Core 中使用 Session 的步骤:

1. 首先在 Startup.cs 中添加 Session 服务
public void ConfigureServices(IServiceCollection services) {
services.AddDistributedMemoryCache(); services.AddSession();// FineUI 和 MVC 服务
services.AddFineUI(Configuration); services.AddMvc(options=> { //
自定义模型绑定(Newtonsoft.Json) options.ModelBinderProviders.Insert(0, new
JsonModelBinderProvider()); }); }public void Configure(IApplicationBuilder app,
IHostingEnvironment env) { app.UseStaticFiles(); app.UseSession();// FineUI 和
MVC 中间件(确保 UseFineUI 位于 UseMvc 的前面) app.UseFineUI(); app.UseMvc(); }
 

2. 控制器中使用 HttpContext.Session.SetString 来保存字符串
HttpContext.Session.SetString("StartedTime", "Started time:" +
DateTime.Now.ToString());var startedTime = HttpContext.Session.GetString("
StartedTime");
 

如果我们看下 SetString 的定义,会知道甚至这个方法也是通过 Microsoft.AspNetCore.Http 里面定义的扩展方法提供的:



 

解决

知道了根本原因,再去修正 FineUICore(ASP.NET Core)下的这个问题就简单多了。

在控制器方法中,修改完 lockedColumns 对象后,需要显式的保存到 Session 中,如下所示:
[HttpPost] [ValidateAntiForgeryToken] public IActionResult
Grid1_ColumnLockUnlock(string type, string columnId) { // 模拟操作数据库中的数据 List<
string> lockedColumns = GetLockedColumns(); if (type == "lock") { if (!
lockedColumns.Contains(columnId)) { lockedColumns.Add(columnId); } }else if
(type =="unlock") { if (lockedColumns.Contains(columnId)) {
lockedColumns.Remove(columnId); } }
HttpContext.Session.SetObject(KEY_FOR_DATASOURCE_SESSION, lockedColumns);return
UIHelper.Result(); }
 

喜欢三石和他的文章,就加入[三石和他的朋友们] <http://fineui.com/fans/>知识星球,可以下载
FineUICore(基础版),下载后永久商用,可运行于Linux,macOS,Windows。