现存问题以及解决方案:在ASP.NET AJAX中从客户端向服务器端传送DataTable

摘要

在《现存问题以及解决方案:在ASP.NET AJAX客户端得到服务器端的DataTable》这篇文章中,我给出了一个在ASP.NET AJAX中从服务器端得到客户端DataTable的方法,以及相应的示例程序。Jeffrey Zhao更加聪明地对此进行了改进,从根本上解决了从服务器到客户端传送DataTable的问题。

然而,这也仅仅解决了这个问题的一半而已。从客户端向服务器端发送DataTable仍然无法实现,这部分的问题要比前一部分更加严重。本文就将分析其中的原因,并给出解决方案。

本文包括如下内容:

  1. 异常重现——第一个异常:客户端JSON序列化时发生循环引用造成堆栈溢出
  2. 解决第一个异常——破坏循环引用
  3. 异常重现——第二个异常:服务器端Deserialize()方法抛出异常
  4. 解决第二个异常——简单实现Deserialize()方法
  5. 完成后的示例程序
  6. 示例代码下载
  7. 参考文献

 

异常重现——第一个异常:客户端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

 

参考文献

  1. http://forums.asp.net/thread/1442553.aspx
  2. http://forums.asp.net/thread/1251349.aspx
  3. 深入Atlas系列:综合示例(1) - 调用服务器端方法时直接获得客户端具体类型
  4. 深入Atlas系列:Web Sevices Access in Atlas示例(7) - 编写JavaScriptConverter处理含有循环引用的类型

 

写作随想

  1. 本文内容很多,解决问题的方法也比较麻烦
  2. ASP.NET AJAX还是需要很长时间的完善阿
  3. Windows Live Writer的Bug简直多到不能忍受!!
  4. 分析问题的方法,永远是最重要的
  5. 胃疼…………
posted on 2006-11-17 22:00  Dflying Chen  阅读(7048)  评论(12编辑  收藏  举报