首页 > 代码库 > ASP.NET Web Forms 的 DI 應用範例

ASP.NET Web Forms 的 DI 應用範例

跟 ASP.NET MVC 與 Web API 比起來,在 Web Forms 應用程式中使用 Dependency Injection 要來的麻煩些。這裡用一個範例來說明如何注入相依物件至 Web Forms 的 ASPX 頁面。

使用的開發工具與類別庫:

  • Visual Studio 2013
  • .NET Framework 4.5
  • Unity 3.5.x


問題描述

基於測試或其他原因,希望 ASPX 網頁只依賴特定服務的介面,而不要依賴具象類別。 

假設首頁 Default.aspx 需要一個傳回「Hello World!」字串的服務,而我們將此服務的介面命名為 IHelloService。以下為此服務的介面與實作類別:

public interface IHelloService{    string Hello(string name);} public class HelloService : IHelloService{    public string Hello(string name)    {        return "Hello, " + name;    }}

 

Default.aspx 的 code-behind 類別大概會像這樣: 

public partial class Default : System.Web.UI.Page{    public IHelloService HelloService { get; set; }     protected void Page_Load(object sender, EventArgs e)    {        // 在網頁上輸出一段字串訊息。訊息內容由 HelloService 提供。        Response.Write(this.HelloService.Hello("DI in ASP.NET Web Forms!"));    }}

 

問題來了:Page 物件是由 ASP.NET Web Forms 框架所建立的,我們如何從外界動態注入 IHelloService 物件呢?


解法

一般而言,我們建議儘量採用 Constructor Injection 來注入相依物件,可是此法很難運用在 Web Forms 的 Page 物件上。一個便宜行事的解法是採用 Mark Seemann 所說的「私生注入」(Bastard Injection),像這樣:

public partial class Default : System.Web.UI.Page{    public IHelloService HelloService { get; set; }     public Default()    {        // 透過一個共用的 Container 物件來解析相依物件。        this.HelloService = AppShared.Container.Resolve<IHelloService>();    }     protected void Page_Load(object sender, EventArgs e)    {        // 在網頁上輸出一段字串訊息。訊息內容由 HelloService 提供。        Response.Write(this.HelloService.Hello("DI in ASP.NET Web Forms!"));    }}

 

此解法的一個問題是,你必須在每一個 ASPX 頁面的 code-behind 類別中引用 DI 容器的命名空間,而這樣就變成到處都依賴特定的 DI 容器了。我們希望盡可能把呼叫 DI 容器的程式碼集中寫在少數幾個地方就好。


接下來的實作步驟會利用一個 HTTP handler 來攔截 Page 物件的建立程序,以便在 Page 物件建立完成後,立刻以 Property Injection 的方式將 Page 物件需要的服務給注入進去。

實作步驟

Step 1:建立新專案

建立一個新的 ASP.NET Web Application 專案,目標平台選擇 .NET Framework 4.5,專案名稱命名為:WebFormsDemo。

專案範本選擇 Empty,然後在 Add folder and core references for 項目上勾選「Web Forms」。

專案建立完成後,透過 NuGet 管理員加入 Unity 套件。

Step 2:註冊型別

在應用程式的「組合根」建立 DI 容器並註冊相依型別。這裡選擇在 Global_asax.cs 的 Application_Start 方法中處理這件事:

public class Global : System.Web.HttpApplication{    protected void Application_Start(object sender, EventArgs e)    {        var container = new UnityContainer();        Application["Container"] = container; // 把容器物件保存在共用變數裡         // 註冊型別        container.RegisterType<IHelloService, HelloService>();    }}

 

Step 3:撰寫 HTTP Handler


在專案根目錄下建立一個子目錄:Infrastructure,然後在此目錄中加入一個新類別:UnityPageHandlerFactory.cs。程式碼:

public class UnityPageHandlerFactory : System.Web.UI.PageHandlerFactory{    public override IHttpHandler GetHandler(HttpContext context, string requestType, string virtualPath, string path)    {        Page page = base.GetHandler(context, requestType, virtualPath, path) as Page;        if (page != null)        {            var container = context.Application["Container"] as IUnityContainer;            var properties = GetInjectableProperties(page.GetType());             foreach (var prop in properties)            {                try                {                    var service = container.Resolve(prop.PropertyType);                    if (service != null)                    {                        prop.SetValue(page, service);                    }                }                catch                {                    // 沒辦法解析型別就算了。                }            }        }        return page;    }     public static PropertyInfo[] GetInjectableProperties(Type type)    {        var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);        if (props.Length == 0)        {            // 傳入的型別若是由 ASPX 頁面所生成的類別,那就必須取得其父類別(code-behind 類別)的屬性。            props = type.BaseType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);        }        return props;    }       }

程式說明:

  • ASP.NET Web Forms 框架會呼叫此 handler 物件的 GetHandler 方法來建立 Page 物件。
  • 在 GetHandler 方法中,先利用父類別來建立 Page 物件,然後緊接著進行 Property Injection 的處理。首先,從 Application["Container"] 中取出上一個步驟所建立的 DI 容器,接著找出目前的 Page 物件有宣告哪些公開屬性,然後利用 DI 容器來逐一解析各屬性的型別,並將建立的物件指派給屬性。
  • 靜態方法 GetInjectableProperties 會找出指定型別所宣告的所有公開屬性,並傳回呼叫端。注意這裡只針對「Page 類別本身所宣告的公開屬性」來進行 Property Injection,這樣就不用花時間在處理由父類別繼承而來的數十個公開屬性。


Step 4:註冊 HTTP Handler

在 web.config 中註冊剛才寫好的 HTTP handler:

<configuration>  <system.web>    <compilation debug="true" targetFramework="4.5" />    <httpRuntime targetFramework="4.5" />  </system.web>   <system.webServer>    <handlers>      <add name="UnityPageHandlerFactory" path="*.aspx" verb="*" type="WebFormsDemo.Infrastructure.UnityPageHandlerFactory"/>    </handlers>  </system.webServer></configuration>

 

基礎建設的部分到此步驟已經完成,接著就是撰寫各個 ASPX 頁面。

Step 5:撰寫測試頁面

在專案中加入一個新的 Web Form,命名為 Default.aspx。然後在 code-behind 類別中宣告相依服務的屬性,並且在其他地方呼叫該服務的方法。參考以下範例:

public partial class Default : System.Web.UI.Page{    public IHelloService HelloService { get; set; }     protected void Page_Load(object sender, EventArgs e)    {        // 在網頁上輸出一段字串訊息。訊息內容由 HelloService 提供。        Response.Write(this.HelloService.Hello("DI in ASP.NET Web Forms!"));    }}

 

你可以看到,ASPX 網頁並不需要引用 Unity 容器的命名空間,因為注入相依物件的動作已經由基礎建設預先幫你處理好了。


Step 6:執行看看

執行時,瀏覽器應該會顯示一行訊息:「Hello, DI in ASP.NET Web Forms!」

Happy coding!