ASP.NET Web Forms 的 DI 應用範例
文章目錄
跟 ASP.NET MVC 與 Web API 比起來,在 Web Forms 應用程式中使用 Dependency Injection 要來得麻煩些。這裡用一個範例來說明如何注入相依物件至 Web Forms 的 ASPX 頁面。
使用的開發工具與類別庫:
問題描述
基於測試或其他原因,希望 ASPX 網頁只依賴特定服務的介面,而不要依賴具象類別。
假設首頁 Default.aspx 需要一個傳回「Hello World!」字串的服務,而我們將此服務的介面命名為 IHelloService。以下為此服務的介面與實作類別:
Default.aspx 的 code-behind 類別大概會像這樣:
問題來了:Page 物件是由 ASP.NET Web Forms 框架所建立的,我們如何從外界動態注入 IHelloService 物件呢?
解法
一般而言,我們建議儘量採用 Constructor Injection 來注入相依物件,可是此法很難運用在 Web Forms 的 Page 物件上。一個便宜行事的解法是採用 Mark Seemann 所說的「私生注入」(Bastard Injection),像這樣:
此解法的一個問題是,你必須在每一個 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 方法中處理這件事:
Step 3:撰寫 HTTP Handler
在專案根目錄下建立一個子目錄:Infrastructure,然後在此目錄中加入一個新類別:UnityPageHandlerFactory.cs。程式碼:
程式說明:
Step 4:註冊 HTTP Handler
在 web.config 中註冊剛才寫好的 HTTP handler:
基礎建設的部分到此步驟已經完成,接著就是撰寫各個 ASPX 頁面。
Step 5:撰寫測試頁面
在專案中加入一個新的 Web Form,命名為 Default.aspx。然後在 code-behind 類別中宣告相依服務的屬性,並且在其他地方呼叫該服務的方法。參考以下範例:
你可以看到,ASPX 網頁並不需要引用 Unity 容器的命名空間,因為注入相依物件的動作已經由基礎建設預先幫你處理好了。
Step 6:執行看看
執行時,瀏覽器應該會顯示一行訊息:「Hello, DI in ASP.NET Web Forms!」
Happy coding!
2014-08-16 補充:
感謝對岸網友提醒我,Unity 已經有提供 BuildUp 方法,不用像本文範例那樣,還得自己寫個 GetInjectableProperties 來處理 Property Injection(但 HTTP handler 仍是少不了的)。
But(聽說人生最厲害的就是這個 BUT),如果採用 Unity 的 BuildUp 方法,就得在許多類別裡面用到 Unity 的 DependencyAttribute 來裝飾你的屬性。這麼一來,對 Unity 容器的依賴更深。如果你覺得將來不可能改用別的 DI 框架,使用 Unity 的 BuildUp 的確是個不錯的辦法。
使用的開發工具與類別庫:
- 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!
2014-08-16 補充:
感謝對岸網友提醒我,Unity 已經有提供 BuildUp 方法,不用像本文範例那樣,還得自己寫個 GetInjectableProperties 來處理 Property Injection(但 HTTP handler 仍是少不了的)。
But(聽說人生最厲害的就是這個 BUT),如果採用 Unity 的 BuildUp 方法,就得在許多類別裡面用到 Unity 的 DependencyAttribute 來裝飾你的屬性。這麼一來,對 Unity 容器的依賴更深。如果你覺得將來不可能改用別的 DI 框架,使用 Unity 的 BuildUp 的確是個不錯的辦法。