代码改变世界

有关注册DataItem的一些可能被忽视的事情

2007-04-18 15:05 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

在UpdatePanel对页面进行部分刷新时注册一些Data Item是ASP.NET AJAX的特点之一。我们可以在服务器端为某个控件注册一个字符串甚至是一个对象,然后在客户端将将其取回。但是现在我希望向您展示一些您可能会忽视的事情。

让我们先从最基本的用法开始。在一个异步回送过程中,我们可以调用RegisterDataItem方法将一个字符串与一个UpdatePanel绑定起来:

ScriptManager.GetCurrent(this.Page).RegisterDataItem(this.UpdatePanel1, "DataItem");

在客户端,如果我们监听pageLoading,pageLoaded或者endRequest事件,我们就可以使用下面的代码,使用控件的ClientID将这个字符串取回:

Sys.WebForms.PageRequestManager.getInstance().add_pageLoading(
    function(sender, e)
    {
        var dataItem = e.get_dataItems()["<%= this.UpdatePanel1.ClientID %>"]; // "DataItem"
        // more implementations...
    });

RegisterDataItem方法还有一个重载的版本会接受一个额外的参数,其作用是使用一个布尔值来表明我们注册的dataItem是不是已经被序列化为JSON。但是这里就出了一个问题——甚至在官方文档中的示例也是不正确的。

下面的代码选自官方文档中的示例:

protected void Page_Load(object sender, EventArgs e)
{
    if (ScriptManager1.IsInAsyncPostBack)
    {
        System.Web.Script.Serialization.JavaScriptSerializer json =
            new System.Web.Script.Serialization.JavaScriptSerializer();
        ScriptManager1.RegisterDataItem(Label1, DateTime.Now.ToString());
        ScriptManager1.RegisterDataItem(Label2, json.Serialize("more data"), true);
    }
}

这个示例的确能够正常工作,但是它事实上掩盖了一个问题。请注意,序列化之后的Data Item会被JavaScript内置的eval方法解释执行:

function Sys$WebForms$PageRequestManager$_onFormSubmitCompleted(sender, eventArgs)
{   
    //...

    for (i = 0; i < dataItemJsonNodes.length; i++) {
        var dataItemJsonNode = dataItemJsonNodes[i];
        this._dataItems[dataItemJsonNode.id] = eval(dataItemJsonNode.content);
    }

    //...
}

在官方文档的示例中,字符串“more data”会被序列化为“"more data"”(请注意多出的双引号),这样从eval方法中得到的结果就是“more data”,这正是我们注册的内容。这个再正常不多了,不是吗?猜猜看如果我们将一个序列化的对象注册到客户端时会发生什么事情呢?我为此写了一个示例:

//  the definition of Person class
public class Person
{
    public string Name;
}


// the code to register a Person object
Person person = new Person();
person.Name = "Jeffz";

JavaScriptSerializer serializer = new JavaScriptSerializer();
ScriptManager.GetCurrent(this.Page).RegisterDataItem(
    this.UpdatePanel1, serializer.Serialize(person), true);

当我们进行异步更新时就会抛出异常。似乎这是因为服务器端注册的表示Person对象的JSON字符串“{"Name":"Jeffz"}”无法被直接传递进入eval方法。具体的原因关乎语法方面的问题,可以在ECMAScript Specification中找到解释,因此我在这里就不多作解释了。那么,请注意我下面的代码:

ScriptManager.GetCurrent(this.Page).RegisterDataItem(
    this.UpdatePanel1, "(" + serializer.Serialize(person) + ")", true);

现在,我们的代码就能正常工作了,于是我们就可以在客户端将Person对象取出:

Sys.WebForms.PageRequestManager.getInstance().add_pageLoading(
    function(sender, e)
    {
        var person = e.get_dataItems()["<%= this.UpdatePanel1.ClientID %>"];
        alert(person.Name); // "Jeffz"
    });

这样看来,这个问题也就可以使用这个方法来解决了。不过为什么ASP.NET AJAX会使用eval方法,而不是定义在Microsoft AJAX Library中的deserialize方法来反序列化一个对象呢?我们可以发现,在deserialize方法中,一个表达式被传递给eval方法之前会被自动加上括号:

Sys.Serialization.JavaScriptSerializer.deserialize = function (data)
{
    try
    {    
        var exp = data.replace(...);
        return eval('(' + exp + ')');
    }
    catch (e)
    {
         throw Error.argument('data', Sys.Res.cannotDeserializeInvalidJson);
    }
}

我猜想,会不会是因为编写部分刷新相关代码的开发人员并不完全了解Microsoft AJAX Library中的功能呢?不过可能性最大的原因则是这些代码没有被完整地Review和测试过。事实上这并不是ASP.NET AJAX最终版本中唯一的bug,其他的还包括那个著名的“跨域名frame的拒绝访问错误”和StringBuilder的Bug

嗯,我们问题也看够了,就把目光移向别处吧。尽管逻辑上一个Data Item应该是一个包含信息的对象,但是请注意我们是使用eval方法来“反序列化”一个JSON字符串的。这意味着我们事实上可以将任意的合法表达式发送到客户端,然后它就会被正确执行。请看下面的示例:

ScriptManager.GetCurrent(this.Page).RegisterDataItem(
    this.UpdatePanel1, "var __f = function(){alert('Hello World!');}; __f;", true);

Sys.WebForms.PageRequestManager.getInstance().add_pageLoading(
    function(sender, e)
    {
        e.get_dataItems()["<%= this.UpdatePanel1.ClientID %>"]();
    });

我在上面代码中将一个函数注册为一个Data Item并且在客户端将其执行了。如果您深入了解UpdatePanel局部刷新的过程,您会注意到您在服务器端注册的JavaScript代码只有在页面更新结束后才会生效,也就是说,我们无法在pageLoading事件被触发时执行这些代码。我会在之后的文章中针对这样的设计以及如何使用work arounds来解决这个问题进行详细的探讨。这些work arounds就是基于Data Item注册的使用方式的,因为表示Data Item的“JSON字符串”在pageLoading触发之前就被解释执行。

 

English Version