在MVC4專案使用JSONP

by vivid 9. 十月 2013 03:37

.NET Magazine國際中文電子雜誌
者:許薰尹
稿:張智凱
文章編號:N131014101
出刊日期:2013/10/09

有些網站透過JSONP來分享資料給其它網站應用程式,特別是透過用戶端JavaScript或jQuery程式碼來進行存取。JSONP可以設計跨來源資源共享(Cross-Origin Resource Sharing,CORS)的程式,不過預設ASP.NET MVC網站應用程式並不直接支援JSONP,需要額外進行一些設計,本篇文章將介紹如何在ASP.NET MVC加上JSONP呼叫。

第一步先建立ASP.NET MVC專案,從Visual Studio 2012開發工具 -「File」-「New」-「Project」,在「New Project」對話盒中選取程式語言,例如本範例選擇「Visual C#」,從「Web」分類中,選取「ASP.NET MVC 4 Web Application」,為專案取一個名稱,然後按下「OK」鍵,請參考下圖所示:

clip_image002

圖 1:ASP.NET MVC 4 Web Application。

建立一個ASP.NET MVC 4應用程式「Empty」範本專案,請參考下圖所示:

clip_image004

圖 2:建立一個ASP.NET MVC 4應用程式「Empty」範本專案。

從「Solution Explorer」視窗,選取專案下「Models」目錄,按滑鼠右鍵,選取「Add」-「New Item」加入一個新項目,選取「ADO.NET Entity Data Model」,使用「Model.edmx」當作名稱,按「Add」按鈕,請參考下圖所示:

clip_image006

圖 3:加入「ADO.NET Entity Data Model」。

在「Entity Data Model Wizard」視窗中,選擇模型內容為「Generate from database」,然後選「Next」按鈕,請參考下圖所示:

clip_image008

圖 4:「Generate from database」。

接著設定資料連接,點選「New Connection」開啟「Connection Properties」視窗,在「Connection Properties」視窗,設定「Data Source」為「Microsoft SQL Server」;「Server Name」則輸入您的資料庫伺服器名稱,本文使用「.\Sqlexpress」;「Select or enter a database name」則選取「Pubs」資料庫,然後按下「OK」按鈕,請參考下圖所示:

clip_image010

圖 5:設定資料來源。

選「Next」按鈕,勾選「employee」與「publishers」資料表,以及「Pluralize or singularize generated object names」然後按下「Finish」按鈕,請參考下圖所示:

clip_image012

圖 6:勾選「employee」與「publishers」資料表。

Publishers與employee資料表有一對多的關聯,參考下圖所示,Visual Studio 2012工具會產生一個edmx檔案,描述Entity 類別的相關資訊,請參考下圖所示:

clip_image014

圖 7:Publishers與employee資料表有一對多的關聯。

JsonpResult

若要讓用戶端JavaScript或jQuery程式碼以JSONP方式和伺服端程式碼互動,用戶端需要定義一個回呼函式,如MyCallback,來接收伺服端傳送過來的JSON資料,伺服端必需送出以下格式的JavaScript Function到用戶端,回呼函式的名稱通常都是由用戶端提供,最後再將JSON物件傳入MyCallback當參數:

MyCallback(
{ "pub_id":"0877",
"pub_name":"Binnet \u0026 Hardley",
"city":"Washington",
"state":"DC",
"country":"USA",
"employees":[]
}
);

MVC的ActionResult中有一個JsonResult類別,代表JavaScript Object Notation (JSON)資料,通常搭配AJAX技術使用。我們可以繼承此類別來實作JSONP預期傳送的資料。在目前的專案之中加入一個類別,從Visual Studio 2012開發工具-「Solution Explorer」-你的專案目錄上方按滑鼠右鍵,從快捷選單選擇「Add」-「New Item」選取「Class」,設定名稱為「JsonpResult」,然後按下「Add」按鈕,請參考下圖所示:

clip_image016

圖 8:加入JsonpResult類別。

修改JsonpResult類別程式碼,繼承JsonResult類別,加入一個Callback屬性存放回呼函式的名稱;JData屬性用來存放要送到用戶端的JSON資料,另外新增一個建構函式,以傳入要序列化的publisher物件:

namespace JSONPDemo1 {
  public class JsonpResult : JsonResult {
    public string Callback { get; set; }
    public object JData { get; set; }
    public JsonpResult( object data ) {
      JData = data;
    }
    public JsonpResult( ) {
      JsonRequestBehavior = JsonRequestBehavior.AllowGet;
    }
    public override void ExecuteResult( ControllerContext context ) {
      var ctx = context.HttpContext;
      Callback = ctx.Request [ "callback" ];

      JavaScriptSerializer serializer = new JavaScriptSerializer( );
      var dataToSend = serializer.Serialize( JData );
      ctx.Response.Write( string.Format( "{0}({1});" , Callback , dataToSend ) );
    }
  }
}


因為預設為了安全性考量,不能使用HTTP GET來回傳JSON資料,因此在JsonpResult的建構函式設定JsonRequestBehavior[User1] 為「AllowGet」開放存取。最後改寫ExecuteResult方法,從Request物件取得查詢字串中指定的回呼函式名稱,放到Callback屬性。然後利用JavaScriptSerializer類別的Serialize方法,將publisher物件序列化成JSON格式,再轉成JSONP格式回傳。

加入控制器

從「Solution Explorer」-專案-「Controllers」目錄上方按滑鼠右鍵,從快捷選單選取「Add」-「Controller」,接著便會出現「Add Controller」對話盒,為控制器取一個名稱,例如本範例中的「HomeController」,請參考下圖所示:

clip_image018

圖 9:加入控制器。

加入JsonData方法,此方法接收一個id參數,指明想要查詢的publisher編號,我們再利用Entity Framework查詢資料庫中篩選條件相同的publisher資料,再利用JsonpResult將資料轉換成JSONP格式:

public ActionResult JsonData( string id ) {
       pubsEntities db = new pubsEntities( );
       publisher data = db.publishers.First<publisher>( p => p.pub_id == id );
       return new JsonpResult( data );
     }

 

建立View

預設控制器中包含一個Index方法,讓我們為它建立對應的檢視。將滑鼠游標停留在HomeController Index方法上方按滑鼠右鍵,然後選取快捷選單上的「Add View」選項,請參考下圖所示:

clip_image020

圖 10:建立View。

在「Add View」對話盒設定檢視的名稱為「Index」。清除勾選所有核取方塊然後按下「Add」按鈕,請參考下圖所示:

clip_image022

圖 11:建立Index View。

安裝jQuery

接著在專安裝jQuery函式庫以便撰寫用戶端程式碼,在Visual Studio 2012工具-「Solution Explorer」-點選專案,按滑鼠右鍵,從快捷選單選取「Manage NuGet Package」,然後從線上搜尋jQuery,點選「Install」按鈕安裝,請參考下圖所示:

clip_image024

圖 12:安裝jQuery。

JSONP請求

在Index View中加入以下程式碼,以發出JSONP請求:

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name = "viewport" content = "width=device-width" />
    <title> Index </title>
</head>
<body>
    <div id="result">
    </div>
    <script src="~/Scripts/jquery-2.0.3.min.js"> </script>
    <script>
        function MyCallback( data ) {

            var result = " Receive from server : "
                + "<br/> publisher Id : " + data.pub_id
                + "<br/> publisher Name : " + data.pub_name
                + "<br/> City : " + data.city
                + "<br/> State : " + data.state
                + "<br/> Country : " + data.country

            $("#result").html( result );


        }
    </script>

    <script src="http://localhost:7503/home/JsonData?id=0877&callback=MyCallback"></script>
</body>
</html>


用戶端利用<script>標籤參考到伺服端HomeController的JsonData方法,並利用查詢字串指明回呼函式的名稱為「MyCallback」,以及傳入想查詢的publisher id 為「0877」。MyCallback函式則可從data變數取得JSON物件,我們便可利用屬性語法取得publisher的pub_id、pub_name、city、state與country等資料。

測試執行

在Visual Studio 2012按F5執行這個網站應用程式,得到一個例外錯誤如下:

 

序列化 'System.Data.Entity.DynamicProxies.publisher_ FA097D739FFA272389F67AA7BA7FF37EC8867D8E1C682219278A70F05326E533' 型別的物件時偵測到循環參考。

我們可以利用一個簡單的方式解決使用Entity Framework存取資料而造成的循環參考問題:就是要求Entity Framework不要為你建立Proxy物件。Proxy物件是為了偵測Cache中資料異動情形而自動建立的。我們可以利用部分類別來變更Entity Framework預設的行為。

在目前的專案之中Models目錄下加入一個類別,從Visual Studio 2012開發工具-「Solution Explorer」-你的專案-「Models」目錄上方按滑鼠右鍵,從快捷選單選擇「Add」-「New Item」,選取「Class」,設定名稱為「pubsEntities」,然後按下「Add」按鈕,在類別中加入一個建構函式,傳入proxyCreationEnabled布林值,並設定給Configuration.ProxyCreationEnabled屬性來指明是否要建立Proxy物件:

 

namespace JSONPDemo1.Models {
  public partial class pubsEntities {
    public pubsEntities( bool proxyCreationEnabled )
      : base( "name=pubsEntities" ) {
        this.Configuration.ProxyCreationEnabled = proxyCreationEnabled;
    }
  }
}

 

修改HomeController的JsonData方法,建立pubsEntities物件時,指明不要建立Proxy物件:

 

public ActionResult JsonData( string id ) {
  pubsEntities db = new pubsEntities(false );
  publisher data = db.publishers.First<publisher>( p => p.pub_id == id );
  return new JsonpResult( data );
}

在Visual Studio 2012按F5執行這個網站應用程式,此範例的執行結果,請參考下圖所示:

clip_image026

圖 13:JSONP範例。

取回Master-Details資料

接著讓我們來試著回傳有Master-Details關係的資料。修改HomeController的JsonData方法,叫用Include方法,將publisher相關聯的employees資料查詢回來,並包裝成JSONP格式回傳:

 

public ActionResult JsonData( string id ) {
      pubsEntities db = new pubsEntities( false );
      publisher data = db.publishers.Include( "employees" ).First<publisher>( p => p.pub_id == id );
      return new JsonpResult( data );
    }

 

不過,在Visual Studio 2012按F5執行這個網站應用程式,執行一樣會有序列化錯誤:

序列化 'JSONPDemo1.Models.publisher' 型別的物件時偵測到循環參考。

例外錯誤的畫面如下:

clip_image028

圖 14:循環參考錯誤。

使用Json.NET套件

我們可以改用Json.NET來取代JavaScriptSerializer類別進行序列化動作。在Visual Studio 2012工具-「Solution Explorer」-點選專案,按滑鼠右鍵,從快捷選單選取「Manage NuGet Package」,然後從線上搜尋「Json.NET」,點選「Install」按鈕安裝,請參考下圖所示:

clip_image030

圖 15:使用Json.NET套件。

使用Json.NET不必特別去管Proxy建立問題,你可以註解掉前文提及為pubsEntities類別撰寫的部分類別程式碼。另外我們可以為employee部分類別加入Metatadata,參考以下範例程式碼,利用Json.NET提供的JsonIgnore Attribute,指明不要序列化Employee類別的publisher屬性,來解決循環序列化的問題:

 

namespace JSONPDemo1.Models {
  //public partial class pubsEntities {
  //  public pubsEntities( bool proxyCreationEnabled )
  //    : base( "name=pubsEntities" ) {
  //      this.Configuration.ProxyCreationEnabled = proxyCreationEnabled;
  //  }
  //}
  [MetadataType( typeof( employeeMetadata ) )]
  public partial class employee {
    private class employeeMetadata {
       [JsonIgnore] 
      public virtual publisher publisher { get; set; }
    }
  }
}

 

接著修改JsonpResult類別,改用Json.NET提供的JsonConvert類別的SerializeObject方法進行序列化:

 

public class JsonpResult : JsonResult {
    public string Callback { get; set; }
    public object JData { get; set; }
    public JsonpResult( object data ) {
      JData = data;
    }
    public JsonpResult( ) {
      JsonRequestBehavior = JsonRequestBehavior.AllowGet;
    }
    public override void ExecuteResult( ControllerContext context ) {
      var ctx = context.HttpContext;
      Callback = ctx.Request [ "callback" ];
      var dataToSend = JsonConvert.SerializeObject( JData );
      ctx.Response.Write( string.Format( "{0}({1});" , Callback , dataToSend ) ); 
    }
  }

 

修改HomeController,建立pubsEntities物件時,建構函式不再需要傳入參數指定是否建立Proxy物件:

 

public ActionResult JsonData( string id ) {
       pubsEntities db = new pubsEntities(  );
       publisher data = db.publishers.Include( "employees" ).First<publisher>( p => p.pub_id == id );
       return new JsonpResult( data );
     }

 

最後修改Index View,利用一個迴圈取回publisher相關聯的employee資料,呈現在<ul>清單之中:

 

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name = "viewport" content = "width=device-width" />
    <title> Index </title>
</head>
<body>
    <div id = "result">
    </div>
    <script src="~/Scripts/jquery-2.0.3.min.js"></script>
    <script>
        function MyCallback( data ) {

            var result = " Receive from server : "
                + "<br/> publisher Id : " + data.pub_id
                + "<br/> publisher Name : " + data.pub_name
                + "<br/> City : " + data.city
                + "<br/> State : " + data.state
                + "<br/> Country : " + data.country

            $("#result").html( result );

            var ul = $("<ul></ul>");           

            for (var i = 0; i < data.employees.length; i++) {
                ul.append("<li>" + data.employees[i].fname + " , " + data.employees[i].lname + "</li>");           
            }
   
            ul.appendTo("#result");

        }
    </script>

    <script src="http://localhost:7503/home/JsonData?id=0877&callback=MyCallback"></script>
</body>
</html>

執行程式,伺服端會送出以下資料到用戶端:

MyCallback({"employees":[{"emp_id":"PMA42628M","fname":"Paolo","minit":"M","lname":"Accorti","job_id":13,"job_lvl":35, "pub_id":"0877","hire_date":"1992-08-27T00:00:00"},{"emp_id":"VPA30890F","fname":"Victoria","minit":"P","lname":"Ashworth","job_id":6,"job_lvl":140, "pub_id":"0877","hire_date":"1990-09-13T00:00:00"},{"emp_id":"H-B39728F","fname":"Helen","minit":" ","lname":"Bennett","job_id":12,"job_lvl":35,"pub_id":"0877","hire_date":"1989-09-21T00:00:00"}, {"emp_id":"L-B31947F","fname":"Lesley","minit":" ","lname":"Brown","job_id":7,"job_lvl":120,"pub_id":"0877","hire_date":"1991-02-13T00:00:00"},{"emp_id":"ARD36773F","fname":"Anabela","minit":"R","lname":"Domingues","job_id":8, "job_lvl":100,"pub_id":"0877","hire_date":"1993-01-27T00:00:00"},{"emp_id":"PHF38899M","fname":"Peter","minit":"H","lname":"Franken","job_id":10,"job_lvl":75, "pub_id":"0877","hire_date":"1992-05-17T00:00:00"},{"emp_id":"PXH22250M","fname":"Paul","minit":"X","lname":"Henriot","job_id":5,"job_lvl":159, "pub_id":"0877","hire_date":"1993-08-19T00:00:00"},{"emp_id":"ENL44273F","fname":"Elizabeth","minit":"N","lname":"Lincoln","job_id":14,"job_lvl":35, "pub_id":"0877","hire_date":"1990-07-24T00:00:00"},{"emp_id":"M-R38834F","fname":"Martine","minit":" ","lname":"Rance","job_id":9,"job_lvl":75,"pub_id":"0877","hire_date":"1992-02-05T00:00:00"},{"emp_id":"DBT39435M","fname":"Daniel","minit":"B","lname":"Tonini","job_id":11,"job_lvl":75," pub_id":"0877","hire_date":"1990-01-01T00:00:00"}],"pub_id":"0877","pub_name":"Binnet & Hardley","city":"Washington","state":"DC","country":"USA"});

此範例的執行結果,請參考下圖所示:

clip_image032

圖 16:取回Master-Details資料。

使用jQuery的$.ajax方法來發出JSONP請求

除了使用<script>標籤來進行JSONP呼叫之外,我們也可以利用jQuery的$.ajax方法來發出JSONP請求,在Index View加入以下程式碼,只要設定dataType屬性為「JSONP」以及jsonpCallback屬性為回呼函式名稱「MyCallback」:

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title> Index </title>
</head>
<body>
       <div id = "result">
    </div>
    <script src = "~/Scripts/jquery-2.0.3.min.js"></script>
        <script>
            function MyCallback( data ) {

                var result = " Receive from server : "
                    + "<br/> publisher Id : " + data.pub_id
                    + "<br/> publisher Name : " + data.pub_name
                    + "<br/> City : " + data.city
                    + "<br/> State : " + data.state
                    + "<br/> Country : " + data.country

                $("#result").html(result);

                var ul = $("<ul></ul>");

                for (var i = 0; i < data.employees.length; i++) {
                    ul.append("<li>" + data.employees[i].fname + " , " + data.employees[i].lname + "</li>");
                }

                ul.appendTo("#result");

            }

   $.ajax({
            type: "GET",
            url: "http://localhost:7817/home/JsonData?id=0877",
            dataType: "jsonp",
            jsonpCallback: "MyCallback"
        });
    </script>
</body>
</html>

Tags:

.NET Magazine國際中文電子雜誌 | ASP.NET MVC | 許薰尹Vivid Hsu | jQuery

評論 (2) -

劉昌賢
劉昌賢 Taiwan
2013/10/11 下午 03:10:12 #

good idea
224新北市瑞芳區深澳電廠台電新村86號

回覆

Vivid
Vivid Taiwan
2013/10/20 下午 12:24:29 #

多謝!!

回覆

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List