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!
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。