现存问题以及解决方案:在ASP.NET AJAX中从客户端向服务器端传送DataTable
摘要
在《现存问题以及解决方案:在ASP.NET AJAX客户端得到服务器端的DataTable》这篇文章中,我给出了一个在ASP.NET AJAX中从服务器端得到客户端DataTable的方法,以及相应的示例程序。Jeffrey Zhao更加聪明地对此进行了改进,从根本上解决了从服务器到客户端传送DataTable的问题。
然而,这也仅仅解决了这个问题的一半而已。从客户端向服务器端发送DataTable仍然无法实现,这部分的问题要比前一部分更加严重。本文就将分析其中的原因,并给出解决方案。
本文包括如下内容:
- 异常重现——第一个异常:客户端JSON序列化时发生循环引用造成堆栈溢出
- 解决第一个异常——破坏循环引用
- 异常重现——第二个异常:服务器端Deserialize()方法抛出异常
- 解决第二个异常——简单实现Deserialize()方法
- 完成后的示例程序
- 示例代码下载
- 参考文献
异常重现——第一个异常:客户端JSON序列化时发生循环引用造成堆栈溢出
本文的将接着《现存问题以及解决方案:在ASP.NET AJAX客户端得到服务器端的DataTable》这篇文章中的示例程序继续开发。如果你还没有阅读过,那么请先至少熟悉其中的示例程序。在这篇文章中,我们已经能够在客户端得到一个DataTable,其中客户端的回调函数如下:
function cb_getDataTable(result) { result = parseBetaDataTable(result); var contentBuilder = new Sys.StringBuilder(); for (var i = 0; i < result.get_length(); ++i) { contentBuilder.append("<strong>Id</strong>: "); contentBuilder.append(result.getRow(i).getProperty("Id")); contentBuilder.append(" <strong>Name</strong>: "); contentBuilder.append(result.getRow(i).getProperty("Name")); contentBuilder.append("<br />"); } $get("result").innerHTML = contentBuilder.toString(); }
其中result就是这个客户端DataTable,让我们在该函数外定义一个全局的DataTable,将这个DataTable先保留起来:
var m_myDataTable = null;
修改一下上述回调函数,将result保留在m_myDataTable中:
function cb_getDataTable(result) { result = parseBetaDataTable(result); m_myDataTable = result; var contentBuilder = new Sys.StringBuilder(); //...... }
然后在页面中再添加一个按钮:
<input id="btnSendDataTable" type="button" value="Send DataTable" onclick="return btnSendDataTable_onclick()" />
onclick中指定的事件处理函数定义如下:
function btnSendDataTable_onclick()
{
PageMethods.SendDataTable(m_myDataTable, cb_sendDataTable);
}
可以看到,PageMethods.SendDataTable()即为服务器端名为SendDataTable()的Web Method的客户端代理,我们就通过这个代理将前面保存起来的DataTable(m_myDataTable)发送回了服务器。服务器端SendDataTable()方法的定义如下,注意该方法必须为静态(static),且被 [System.Web.Services.WebMethod]和 [Microsoft.Web.Script.Services.ScriptMethod]两个属性所修饰:
[System.Web.Services.WebMethod] [Microsoft.Web.Script.Services.ScriptMethod] public static void SendDataTable(DataTable myDataTable) { // do anything you like. save it to database or xml file, etc. }
示例程序中我们什么都没做,具体应用中各位朋友可以随心所欲地发挥。我们只要保证DataTable能够发送过去就行了。
返回到客户端JavaScript部分,注意到在调用PageMethods.SendDataTable()时候我们为其指定了一个回调函数,名为cb_sendDataTable(),该JavaScript函数的定义如下:
function cb_sendDataTable(result) { debugger; }
没什么讲的,只要能够顺利执行到回调函数,也就是其中的debugger被hit,那么我们就算是成功了!
这样就完成了本示例程序,运行并点击“Get DataTable”,将顺利得到如下图所示的界面。若出现了异常,请先参考《现存问题以及解决方案:在ASP.NET AJAX客户端得到服务器端的DataTable》这篇文章进行修正。
然后点击“Send DataTable”按钮,将这个DataTable发送回服务器…………………………………………在经历过长时间的等待以及浏览器无响应之后,抛出了Out of stack space异常:
上图右上角的Call Stack中可以看到,同一个函数被调用了无数次——显然发生了循环引用问题。
让我们在btnSendDataTable_onclick() 中加上一个断点,看看这个客户端DataTable到底是怎么回事。关于调试JavaScript,请参考我的这篇文章。
在上图中可以看到,Immediate Window中测试m_myDataTable._rows[0]._owner == m_myDataTable,返回为true。说明确实存在着循环引用:客户端DataTable的每一个Row对象的_owner属性都引用回了该DataTable自身,这也就造成了客户端序列化时无止无休的进行,直至堆栈溢出。
解决第一个异常——破坏循环引用
客户端DataTable的Row对象的_owner属性在传回服务器时似乎没什么用。所以解决这个循环引用问题最好的方式就是,在将客户端DataTable传回服务器之前,清空其每一个Row对象的_owner属性。
编写一个辅助函数prepareSendingDataTable(),接受一个客户端DataTable,返回一个破坏掉循环引用的DataTable:
function prepareSendingDataTable(dataTable) { for (var i = 0; i < dataTable.get_length(); ++i) { dataTable._rows[i]._owner = null; } return dataTable; }
然后修改一下btnSendDataTable_onclick() ,首先调用该辅助函数,然后再发送:
function btnSendDataTable_onclick() { var myDataTable = prepareSendingDataTable(m_myDataTable); PageMethods.SendDataTable(myDataTable, cb_sendDataTable); }
这样以后,第一个异常——客户端JSON序列化时发生循环引用造成堆栈溢出就被搞定了!
异常重现——第二个异常:服务器端Deserialize()方法抛出异常
别高兴得太早了——再次运行示例程序,依次点击“Get DataTable”和“Send DataTable”两个按钮。又出现了如下错误:
打开Fiddler,可以看到如下异常的详细信息:
仍旧是“System.NotSupportedException”异常……
通过某些手段,我们可以知道ASP.NET AJAX中自带的Microsoft.Web.Preview.Script.Serialization.Converters.DataTableConverter中根本没有实现Deserialize()方法,该方法中仅仅是抛出了System.NotSupportedException异常而已……
似乎觉得无语,是么?不过ASP.NET AJAX也有它自己的考虑,毕竟DataTable是一个非常复杂的对象。其中Row、Column、数据类型、更新、删除等各种关系信息非常复杂。实现这个Deserialize()方法确实将是一个非常浩大的工程。
解决第二个异常——简单实现Deserialize()方法
有了问题不能逃避。我这里就简单地实现了一个DataTable的Deserialize()方法,其中忽略了太多太多的复杂东西。仅仅是创建出了最最最最最最最基本的一个DataTable而已,朋友们可以基于这个进行改进:
using System; using System.Data; using System.Configuration; using System.Collections.Generic; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; namespace Dflying.Atlas { /// <summary> /// Simple implementation of DataTable Converter - Deserialize() method. /// </summary> public class DataTableConverter : Microsoft.Web.Preview.Script.Serialization.Converters.DataTableConverter { public override object Deserialize(IDictionary<string, object> dictionary, Type t, Microsoft.Web.Script.Serialization.JavaScriptSerializer serializer) { // there's no rows in the DataTable, return null object. Array rowDicts = (dictionary["_array"] as Array); if (rowDicts.Length == 0) { return null; } DataTable myDataTable = new DataTable(); // get column info foreach (string colKey in (rowDicts.GetValue(0) as IDictionary<string, object>).Keys) { myDataTable.Columns.Add(colKey); } // create and add rows to the DataTable foreach (object rowObj in rowDicts) { IDictionary<string, object> rowDict = rowObj as IDictionary<string, object>; DataRow newRow = myDataTable.NewRow(); foreach (DataColumn column in myDataTable.Columns) { newRow[column.ColumnName] = rowDict[column.ColumnName]; } myDataTable.Rows.Add(newRow); } // done! return myDataTable; } } }
注释非常详细,这里不赘。若您不能完全理解,请参考Jeffrey Zhao的一系列非常精彩的深入文章。若您只想着使用的话,那么也无所谓理解了。
将其放置于App_Code目录下,并修改Web.config,使用我们自己的DataTableConverter:
<jsonSerialization maxJsonLength="500000000"> <converters> <add name="DataTableConverter" type="Dflying.Atlas.DataTableConverter"/> </converters> </jsonSerialization>
千辛万苦之后,终于大功告成!
完成后的示例程序
在public static void SendDataTable(DataTable myDataTable)中加上个断点,再次运行示例程序,依次点击“Get DataTable”和“Send DataTable”两个按钮。如我们所愿,服务器端得到了正确的DataTable:
接下来,客户端回调函数中的debugger也顺利被hit。终于搞定……
示例代码下载
本文的示例程序在此下载:ASPNETAJAXDataTable_Send.zip