65.9K
CodeProject 正在变化。 阅读更多。
Home

使用ASP.NET、WCF和jQuery开发小部件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (25投票s)

2009年8月3日

CPOL

18分钟阅读

viewsIcon

146598

downloadIcon

3234

本文演示了如何使用ASP.NET、jQuery和WCF来开发小部件——可嵌入到其他HTML页面中的可移植代码块。

引言

我将从一些定义和要解决问题的描述开始。

现代Web的常见问题之一是如何嵌入别人的代码片段,有时这些片段会被重新包装成不同的展示形式,并显示在你自己的页面上。我们经常在门户页面上看到横幅广告,看到显示您百货公司或医生办公室地址的谷歌地图。我们看到像iGoogle这样的页面,您可以根据自己的需求通过选择和排列时钟、每日天气、Gmail、cnn.com新闻、股票行情指示器以及许多其他工具来定制页面。

我们遵循维基百科的说法,将这些小片段称为Web小部件

您的小部件通常驻留在别人的页面上。在本文中,我将把这个页面称为Mashup

小部件和Mashup的概念类似于PortletPortal的概念;主要区别在于,Portlet聚合到Portal页面通常发生在Portal服务器上;而小部件聚合到Mashup通常由[客户端]浏览器执行。有关Mashup和Portal的更多信息,请参阅维基百科。在本文中,我们只演示“客户端”方法,因为它更具可伸缩性,并且是现代Web开发中的首选。

现在,我们有了小部件,以及承载该小部件的Mashup页面。假设您想使用ASP.NET和C#开发一个小部件,并希望为他人提供轻松将其小部件包含在其Mashup页面中的能力。而Mashup页面不一定是用ASP.NET开发的,您也不一定能控制开发该页面所使用的技术。如果您是LAMP开发者,网络上有很多相关信息,但当涉及到ASP.NET时,信息就没那么多了。

本文将演示使用ASP.NET开发此类小部件的两种技术:`IFRAME`,以及使用JavaScript和jQuery将小部件动态嵌入到Mashup页面。

背景

在撰写本文时,我进行了广泛的网络研究,并且有一些非常好的文章描述了您在开发小部件时可能遇到的问题以及可能的解决方案。我将在下面回顾一下。

在开发小部件时,最具挑战性的问题之一是所谓的“跨域通信”。简单来说,如果您的Mashup页面和小部件在同一台服务器上,一切都非常简单。当Mashup在一台服务器上(例如,http://mymashup.com),而您的小部件驻留在另一台服务器上(例如,http://mywidgets.com)时,情况就会变得更加复杂。

因此,如果您有一个简单的场景(您可以控制Mashup和小部件环境),您会怎么做?网络上有一些很好的文章,我将引用其中一些(请记住,有些文章假设Mashup和小部件都是用ASP.NET开发的,有些允许使用混合技术)。

有一份很棒的PowerPoint演示文稿,列出了所有已知的小部件开发技术(请记住,我们要克服的最具挑战性的问题是“跨域发布”)

IFRAME:优点和缺点

IFRAME方法与其他客户端技术独立,因为它不涉及“跨域通信”问题(即使小部件托管在完全不同的服务器上)。这是最简单的技术,但也有其局限性。

简单来说,`IFRAME`是宿主(Mashup)页面上的一个矩形区域,小部件加载到其中。在这种情况下,小部件是一个功能齐全的网页,但显示在该矩形区域内。由于它是一个独立的页面,它可以使用ASP.NET开发,您不受限于使用ASP.NET回发和AJAX(ScriptManager等)。

使用`IFRAME`时,我们需要记住,它基本上就像将一个不同的浏览器窗口嵌入到Mashup页面中,并且可能会对Mashup页面产生不良影响(例如,如果页面上显示一个`DIV`弹出窗口或模态对话框,并且它与`IFRAME`重叠,则对话框的一部分将不会显示)。另外,请注意`IFRAME`资源消耗较大,因此具有多个`IFRAME`的父页面可能速度较慢。

如果Mashup页面的所有者非常关注安全性,担心小部件中运行的恶意脚本,那么IFRAME方法是首选,因为小部件的脚本对宿主页面的访问权限有限,因此不会对嵌入它的页面造成太大损害。

此外,如果小部件的所有者希望控制其小部件的布局和样式,则此方法是首选。由于`IFRAME`本质上是一个独立的网页,Mashup的CSS脚本无法对小部件造成太大损害。

作为小部件开发者,如果您仍然希望Mashup开发者能够自定义您的部件样式,您将需要接受一个`Style ID`(一个标识您已开发和支持的样式之一的ID)或CSS的完整路径作为您小部件的GET参数(而在小部件中,您需要生成一个适当的链接命令,将相应的CSS直接加载到您的`IFRAME`中)。

让我们用一个例子来演示IFRAME方法。

IFRAME:示例

例如,让我们开发一个简单的计算器小部件,它接受两个参数并计算它们的总和。首先创建一个新的ASP.NET项目,添加一个新的ASP.NET页面,并将必要的控件放置在上面。我们将有一个链接按钮来生成回发。`OnClick`事件非常简单(请参阅*Widgets*项目中的*Calculator*文件夹)。

//C#:
protected void lbResult_Click(object sender, EventArgs e)
{
    int arg1 = 0;
    int arg2 = 0;
    int.TryParse(txtArg1.Text, out arg1);
    int.TryParse(txtArg2.Text, out arg2);
    txtResult.Text = (arg1 + arg2).ToString();
}

我们希望为小部件提供几种备选样式,因此我们将向小部件传递一个额外的“GET”参数来设置我们小部件的主题。

以下是我们如何在Mashup页面中包含该小部件(请参阅*CalculatorTest.aspx*)。

<!-- HTML: -->
<IFRAME id="calc1" name="calc1" 
   style="width: 200px; height: 100px;" 
   frameborder="0" scrolling="no" 
   src="https:///widgets/calculator?theme=theme1" />

如您所见,我们的IFRAME小部件接受一个名为`theme`的附加参数。在演示示例中,由于我们拥有同一台服务器上的小部件和宿主项目,我们将`localhost`用作小部件宿主的名称;在生产环境中,`SRC`应指向实际托管小部件的宿主。

我们将通过这种方式实现控件的样式设置。

  1. 为了处理`theme`参数,您必须使您的小部件继承自特殊的`ThemedPage`类。
  2. `ThemedPage`有一个`Css`函数——我们将使用这个辅助函数来“修复”CSS文件和图片的地址。这是该函数的实现。
  3. //C#:
    
    protected string Css(string url)
    {
        string theme = Request["theme"];
        return !string.IsNullOrEmpty(theme) ? string.Format(url, "themes/" + theme) : "";
    }
  4. 在*Widgets*项目中,我们将创建一个*Themes*文件夹,并在其下创建两个名为*Theme1*和*Theme2*的文件夹,每个文件夹都将有一个*default.css*文件。
  5. 我们将在计算器的ASPX文件中使用这个`Css`函数来引用正确的CSS文件。
  6. <!-- HTML: -->
    <link rel='stylesheet' type='text/css' href="<%= Css("../{0}/default.css") %>" />

    您将看到,在运行时,这将生成到CSS文件的实际引用(相对于计算器ASPX文件)。

    <!-- HTML: -->
    <link rel='stylesheet' type='text/css' href="../themes/theme1/default.css" />

就是这样!现在我们可以使用具有两种不同主题的计算器小部件(主题由小部件所有者维护)。由于我们使用的是`IFRAME`,宿主页面的所有者无法破坏格式。通过这种方法,您还可以获得创建使用回发、AJAX等的复杂小部件的好处。

asp-net-wcf-widget-1.png

请参阅配套的存档中的完整示例。

我将快速说明一下为什么我选择不使用标准的ASP.NET主题功能,而是实现了类似的东西。我本可以使用它,这可能会使代码稍微简化一些(我们只需根据`themes`参数重新分配小部件的`Theme`属性,将所有主题放入标准的*App_Themes*文件夹而不是自定义的*Themes*文件夹,我们甚至不必担心将CSS链接包含到我们的页面中——ASP.NET会自动为我们处理)。我选择不这样做是因为我不喜欢ASP.NET将*所有*主题目录中的CSS文件引用添加到*所有*页面。当您有许多不同小部件的CSS文件时,这似乎有点不方便——所有小部件都会链接所有可用的CSS文件。我决定我宁愿自己控制。

嵌入式小部件:背景、优点和缺点

虽然IFRAME方法非常简单明了,但该方法存在一些缺点;主要是它速度慢,[在浏览器上]资源消耗大,并且不能让Mashup页面的所有者以他/她想要的方式对小部件进行样式设置。

替代方法是将您的小部件动态加载到别人的页面中,并将其嵌入到一个空的`DIV`或`SPAN`标签中。小部件将成为Mashup页面不可分割的一部分。所有父CSS脚本都会影响小部件,Mashup页面的所有者可以根据需要轻松更改小部件的样式。在开发此类小部件时,建议不在HTML中提供任何样式(将所有样式留给Mashup页面)。通过这种方法,由宿主负责正确格式化小部件。

这听起来是个好方法。那么,有什么问题呢?

有很多问题。

  1. 首先,小部件不应该有任何`HEAD`或`BODY`标签,因为它现在是宿主页面的一部分,而页面上只能有一个`HEAD`和一个`BODY`标签。
  2. 您可以在小部件中有一个`FORM`标签(页面可以有多个表单),但您应该忘记向您的ASP.NET表单进行POST。如果您尝试这样做,您的ASP.NET小部件代码将获得控制权,结果,您将被从宿主页面重定向(这可能不是您想要的)。
  3. 这实际上是此方法的一个缺点:在这种情况下,小部件可以更广泛地控制整个页面,并且很容易破坏它:通过提供糟糕的HTML格式、使用恶意JavaScript、从宿主页面重定向等。因此,Mashup页面所有者和小部件所有者之间应该存在更高的信任关系。

    底线:您应该忘记那些会POST的服务器端控件。

  4. 您不能使用Microsoft AJAX库(或者至少不能直接使用)。请记住,您应该只有一个`ScriptManager`,它位于您的`FORM`标签内。这是必要的,以便Microsoft AJAX JavaScript库可以将其自己的JavaScript代码嵌入到您的页面中。现在,想象一下将多个小部件嵌入到一个父页面中,每个小部件都有自己的`FORM`标签和自己的`ScriptManager`。这将不起作用(也许有一种方法可以使其工作,我没有努力尝试,您将在稍后看到原因)。当然,您可能可以从您的小部件中取出`ScriptManager`,然后要求页面所有者以某种方式(如何?)将其Microsoft AJAX脚本嵌入到他/她的页面中的某个位置,位于小部件之上。这可能可行,但它会引入页面宿主所有者可能不喜欢的另一个依赖项。
  5. 这种方法比IFRAME方法复杂得多。

现在,如果您不能使用Microsoft AJAX,但仍希望您的小部件具有交互性并通过用户点击来更新自己,该怎么办?

我将演示一种技术,该技术使用jQuery向WCF服务发出JSONP请求,该服务实现业务逻辑并提供结果,然后这些结果用于更新小部件的UI。

那么,让我重申一下开发此小部件所使用的所有技术,以及我为什么使用它们。

  1. JSONP。如前所述,使用此方法时,我们要处理的最具挑战性的问题是克服“跨域发布”问题。简单来说,我们想从与Mashup页面托管不同的主机获取一些HTML(我们的小部件),并将其嵌入到Mashup页面中——浏览器会阻止此类更新。JSONP是一个众所周知的技巧,可以克服这种浏览器限制。
  2. JavaScript。多年来,我一直害怕JavaScript,但现在Internet Explorer(开发人员工具,IE8中的F12;IE7中需要单独安装)和Firefox(Firebug)等优秀工具,以及出色的流量分析器Fiddler,终于使JavaScript开发变得愉快——您拥有一个非常强大的IDE(几乎就像Visual Studio)用于客户端开发。
  3. jQueryjQuery是一个非常强大的面向对象的客户端JavaScript库。实际上,学习这个库是我最终决定转向JavaScript的决定性因素。由于这个库已经支持JSONP,因此这是合乎逻辑的选择。
  4. WCF。WCF是一种面向对象的方式来调用提供业务逻辑的服务。对于简单的小部件(就像我们在这里开发的),我们可以使用一个更简单的方法(只需将参数传递给“GET”),但如果您想开发一个具有非平凡业务逻辑和高交互性的复杂小部件,WCF非常有用。
  5. JSON。不要将JSONP与JSON混淆:JSON是在向服务器发出请求时打包参数的一种方式,而JSONP是克服跨域发布问题的一种技巧。我们知道另一种著名的技术,用于打包参数并向服务器发出调用——SOAP和XML。显然,当涉及到JavaScript和浏览器时,JSON更受欢迎,因为它更轻量级(在解析方面,在最终网络数据包大小方面)。因此,我们将开发一个接受JSON参数并返回JSONP结果的WCF服务。

现在,这里有一些很棒的文章,它们展示了完整图景的部分内容,并且可能值得一读,以更深入地了解如何使用这些技术、在什么上下文中等等。

嵌入式小部件:示例

对于嵌入式小部件,最棘手的部分是使其能够更新宿主(Mashup)页面。JavaScript存在一个限制,您只能通过向提供页面的同一宿主发出请求来更新页面的部分。简单地说,如果Mashup位于http://www.mymashup.com,没有特殊技巧,它只能更新位于同一宿主www.mymashup.com上的页面部分。

在实际场景中,我们希望Mashup页面位于一个服务器上,而小部件从另一个服务器(例如,http://www.mywidgets.com)下载并嵌入到Mashup页面中。如前所述,我们将使用JSONP来克服下载我们的小部件和发出调用以更新我们小部件部分时的这种限制。此外,我们将使用jQuery和JSONP向WCF后端服务发出请求,以响应用户点击来更新小部件的部分。

以下是您需要做的事情来实现该小部件。

  1. 实现小部件UI。虽然我们可以使用ASP.NET控件,但由于我们不使用回发功能,在此示例中,我们将使用纯HTML控件(请参阅*Calculator2/default.aspx*)。
  2. <!-- HTML: -->
    
    <div id="calculator2">
        <input id="txtArg1" />
        <span id="lblSign">+</span>
        <input id="txtArg2" />
        <a id="lbResult">=</a>
        <span id="result" />
        <span id="error" />
    </div>
  3. 实现将用于下载我们控件的JSONP服务。该服务将这样使用(尝试在浏览器中输入此URL):我们将小部件的URL传递给服务,服务将返回一段JavaScript代码——一个函数调用,其中小部件的内容作为字符串参数传递。
  4. https:///Widgets/Service.ashx?downloadurl=
       http%3A///Widgets/Calculator2&method=jsonp1249181681232&_=1249181681315

    对此服务的调用结果将如下所示。

    //JavaScript:
    
    jsonp1249181681232( "\r\n\r\n\u003c!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 
        Transitional//EN\" 
        \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"\u003e\r\n\r\n\u003chtml 
        xmlns=\"http://www.w3.org/1999/xhtml\" 
        \u003e\r\n\u003chead\u003e\u003ctitle\u003e\r\n\r\n
        \u003c/title\u003e\u003c/head\u003e\r\n\u003cbody\u003e\r\n   
        \u003cform name=\"form1\" method=\"post\" action=\"default.aspx\" 
        id=\"form1\"\u003e\r\n\u003cinput type=\"hidden\" name=\"__VIEWSTATE\" 
        id=\"__VIEWSTATE\" 
        value=\"/wEPDwULLTE2MTY2ODcyMjlkZIrWmIrloRWErn6elOCzG8cGl8eN\" /\u003e\r\n\r\n   
        \u003cdiv id=\"calculator2\"\u003e\r\n       
        \u003cinput id=\"txtArg1\" /\u003e\r\n       
        \u003cspan id=\"lblSign\"\u003e+\u003c/span\u003e\r\n       
        \u003cinput id=\"txtArg2\" /\u003e\r\n       
        \u003ca id=\"lbResult\"\u003e=\u003c/a\u003e\r\n       
        \u003cspan id=\"result\" /\u003e\r\n       
        \u003cspan id=\"error\" /\u003e\r\n   
        \u003c/div\u003e\r\n   
        \u003c/form\u003e\r\n\u003c/body\u003e\r\n\u003c/html\u003e\r\n" );

    这就是JSONP的思路——JSONP调用将返回特定的JavaScript代码(函数调用),该代码现在可以在浏览器上下文中执行。控件的HTML内容作为函数的参数传递,并且不被视为从不同主机下载的内容,因此我们可以轻松地将其“嵌入”到Mashup页面中。

    `jsonp1249181681232`是jQuery自动生成的JavaScript函数的名称。该函数将在JSONP调用结束时被调用,并将实际结果(HTML)传递给我们的回调函数,该回调函数实现了将小部件的HTML代码“嵌入”到宿主页面中(请参阅下一段)。

  5. 现在,当我们有了JSONP服务,我们就可以调用来下载小部件了。
  6. //JavaScript:
    
    loadhtml: function(container, urlraw, callback) {
        var urlselector = (urlraw).split(" ", 1);
        var url = urlselector[0];
        var selector = urlraw.substring(urlraw.indexOf(' ') + 1, urlraw.length);
        private.container = container;
        private.callback = callback;
        private.jsonpcall('Service.ashx', ['downloadurl', escape(url)],
            function(msg) {
                // gets the contents of the Html in the 'msg'
                // todo: apply selector
                private.container.html(msg);
                if ($.isFunction(private.callback)) {
                    private.callback();
                }
            });
    },

    将被自动生成的函数(`jsonp1249181681232`)调用的回调将完成将我们小部件的HTML代码嵌入到Mashup页面中的工作。

    //JavaScript:
    
    function(msg) {
        // gets the contents of the Html in the 'msg'
        // todo: apply selector
        private.container.html(msg);
    }

    其中`container`是Mashup页面中将要嵌入控件的`DIV`。

    `loadhtml`函数的使用方法如下。

    loadhtml(container, 'https:///Widgets/Calculator2', 
             this.Calculator2_"en-us">Init);

    基本上,我们将我们小部件的URL传递给这个函数,函数将嵌入适当的JSONP调用,一旦下载和嵌入完成,它将调用`Calculator2_Init`函数,该函数将初始化我们的小部件。

  7. 实现JavaScript小部件初始化函数(`Calculator2_Init`)。
  8. //JavaScript:
    
    // wire widget after it's loaded
    Calculator2_Init: function() {
    
        // this function is OnClick event for the link
        $('a#lbResult').click(function() {
            var btn = $(this);
            var widget = $('div#calculator2');
            widget.find('*').addClass("processing");
            widget.find("span#error").html("");
            var arg1 = widget.find('input#txtArg1')[0].value;
            var arg2 = widget.find('input#txtArg2')[0].value;
            private.jsonpcall("Calculator2/Service.svc/Sum",
                ["arg1", arg1, "arg2", arg2],
                function(result) {
                    if (result.Error == null) {
                        widget.find("span#result").html(result.Value);
                    } else {
                        widget.find("span#result").html("Error");
                        widget.find("span#error").html(result.Error);
                    }
                    widget.find('*').removeClass("processing");
                });
            return false;
        });
    
        // initializing the widget.
        // nothing for now.
    }

    基本上,这里发生的是我们为`lbResult`元素分配了一个`click`事件。当用户单击链接时,将触发该事件。该事件从输入元素(`arg1`和`arg2`)获取值,然后向我们的`Calculator2` WCF服务发出WCF JSONP调用(调用其`Sum`函数)。

    这里最重要的代码是。

    //JavaScript:
    
    private.jsoncall("Calculator2/Service.svc/Sum", 
       ["arg1", arg1, "arg2", arg2], <OnComplete>);

    此函数调用WCF JSONP服务*Calculator2/Service.svc*,调用`Sum`函数,并传递两个参数(`arg1`和`arg2`)。

    `Sum`函数在后端实现。它将执行计算,并在计算完成后,我们将结果值赋给`SPAN`*result*。

    //JavaScript:
    
    widget.find("span#result").html(result.Value);
  9. 实现WCF `Calculator2`服务。实现WCF服务包括以下步骤。
    • 定义服务的服务契约。契约包括`Sum`函数的定义。
    • //C#:
      
      [ServiceContract]
      public interface ICalculator2
      {
          [OperationContract]
          [WebGet(ResponseFormat = WebMessageFormat.Json)]
          [JSONPBehavior(callback = "method")]
          Result Sum(string arg1, string arg2);
      }
    • 为我们的自定义类型`Result`定义数据契约。
    • //C#:
      
      [DataContract]
      public class Result
      {
          public Result() { }
      
          [DataMember]
          public string Error;
      
          [DataMember]
          public string Value;
      }

      虽然我们可以只返回数字(`int`或`string`值),但此实现将可能的服务异常传回UI。

    • 实现服务。
    • //C#:
      
      // Service Implementation
      public class Calculator2 : ICalculator2
      {
          public Calculator2() { }
      
          public Result Sum(string arg1, string arg2)
          {
              Result result = new Result();
              try
              {
                  int iarg1 = 0;
                  int iarg2 = 0;
                  int.TryParse(arg1, out iarg1);
                  int.TryParse(arg2, out iarg2);
                  result.Value = (iarg1 + iarg2).ToString();
              }
              catch (Exception ex)
              {
                  result.Error = ex.Message;
              }
              return result;
          }
      }
    • 在*web.config*中公开服务。
    • <!-- web.config: -->
      
      <!-- WCF configuration >>> -->
      <system.serviceModel>
          <!-- WCF services -->
          <services>
            <service name="Widgets.Calculator2">
              <endpoint address=""
                        binding="customBinding"
                        bindingConfiguration="jsonpBinding"
                        behaviorConfiguration="Calculator2_Behavior"
                        contract="Widgets.ICalculator2"/>
            </service>
          </services>
          <behaviors>
            <endpointBehaviors>
              <behavior name="Calculator2_Behavior">
                <webHttp/>
              </behavior>
            </endpointBehaviors>
          </behaviors>
          <bindings>
            <customBinding>
              <binding name="jsonpBinding">
                <jsonpMessageEncoding/>
                <httpTransport manualAddressing="true"/>
              </binding>
            </customBinding>
          </bindings>
          <extensions>
            <bindingElementExtensions>
              <add name="jsonpMessageEncoding"
                   type="Microsoft.Jsonp.JsonpBindingExtension, Widgets, 
                          Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
            </bindingElementExtensions>
          </extensions>
      </system.serviceModel>
      <!-- <<< WCF configuration -->

      此服务使用自定义绑定(`jsonpBinding`)。有关此绑定的实现,请参阅配套的存档。我从Microsoft的*WCFWFCardSpace*示例中获取了自定义JSONP绑定实现(请参阅*Microsoft.Jsonp.JsonpBindingExtension*类)。

  10. 现在我们准备使用我们的小部件了。
    • 在页面上预留一些空间,供小部件嵌入(请参阅*Calculator2Test.aspx*)。
    • <!-- HTML: -->
      <div id="calc"></div>
    • 包含JavaScript代码以下载必要的库(jQuery)和我们的代码以及所有小部件的JavaScript(*calculator2.js*)。
    • //JavaScript:
      
      <script type="text/javascript" 
        src="https:///widgets/js/jquery-1.3.2.min.js"></script>
      <script type="text/javascript" 
        src="https:///widgets/js/calculator2.js"></script>
      <script type="text/javascript">
         $(document).ready(function() {
             Widgets.Calculator2($("div#calc"), 'localhost');
         });
      </script>

      与IFRAME示例一样,您需要将*localhost*替换为实际托管小部件的宿主。

      `Calculator2`的实现基于我们之前实现的函数。

      //JavaScript:
      
      // load widget into 'container' from 'host'
      Calculator2: function(container, host) {
          private.host = host;
          private.loadhtml(container, 'http://' + private.host + '/Widgets/Calculator2',
                           private.Calculator2_Init);
      }

      基本上,我们使用`loadhtml`下载我们的小部件,然后初始化它。

就是这样!恭喜您!您已经使用JSONP、WCF和ASP.NET实现了您的第一个嵌入式小部件。

asp-net-wcf-widget-2.png

历史

  • 2009年7月8日 - 初始版本。
© . All rights reserved.