ASP.NET MVC Extending Framework 藉由實作擴充讓開發更為優雅
2023-02-05
筆記 ASP.NET MVC 如何藉由擴充開發框架所提供的 Interface 以及既有 Class,來達到優雅簡潔的需求達成以及易於維護的架構。
說明
本次的筆記來自 Alex Wolf 在 Improving Your MVC Applications with 10 Extension Points 課程中的教學內容。
Views
HTML Helpers
在 Keeping Your Razor Code Clean with HTML Helpers 的課程模組中,介紹兩種 Helper 方式來簡化重複的內容,包含使用 inline Razor Helpers
以及 HTML Helper Using Extensino Methods
的方式來 Reuse 需要在 View 當中重複使用的程式碼以及邏輯。
@helper DisplayList(List<string> data)
{
<ol>
@foreach (var item in data)
{
<li>@item</li>
}
</ol>
}
使用 inline Razor Helper 的好處是容易建立,可以快速用於消除重複的 HTML 語法,但缺點是重複使用的可能較低,受限於在相同的 cshtml 當中使用。
可以藉由將 Razor Helper 獨立儲存在 cshtml 檔案當中,並且儲存於 App_Code
資料夾,這樣在其他 cshtml 當中,也可以重複使用,提升重複使用的可能性。例如儲存在 App_Code/MyHelper.cshtml
,可以透過 @MyHelper.DisplayList(...)
的方式使用。
Infrastructure/HtmlHelperExtensions.cs
public static class HtmlHelperExtensions
{
public static MvcHtmlString DisplayList(this HtmlHelper html, List<string> data)
{
var ulBuilder = new TagBuilder("ul");
foreach (var item in data)
{
var liBuilder = new TagBuilder("li");
liBuilder.SetInnerText(item);
ulBuilder.InnerHtml += liBuilder.ToString();
}
return new MvcHtmlString(ulBuilder.ToString());
}
}
可以在 Views/Web.config
加入 HelperExtensions 的命名空間,如此在使用擴充的 Helper 時就不用重複輸入命名空間。
而除了 Helper 的方式外,也可以透過 Partial View 以及 ChildAction 的方式來重複使用 View 的內容與邏輯,而取決於如果是簡單的 Html 元素搭配資料重現,適合使用 Helper;如果是複雜的區塊 (Sections) 包含較多的 Html 組合以及資料,則適合使用 Partial View 以及 ChildAction 的方式。
Custom View Engine
在 Enabling Theme Support with a Custom View Engine 的課程模組中,示範如何客製化 View 的主題引擎,能夠在 Web.config 調整主題的選擇,讓應用程式對應使用不同風格的 cshtml 進行呈現。
原理是利用客製 View Engine,由此客製的 View Engine 決定如何尋找與回應對應的 cshtml,而如果沒有找到相符的 cshtml,則會使用第二順序的 View Engine 進行尋找。
ThemeEngine.cs
public class ThemeViewEngine : RazorViewEngine
{
public ThemeViewEngine(string activeThemeName)
{
ViewLocationFormats = new[]
{
"~/Views/Themes/" + activeThemeName + "/{1}/{0}.cshtml",
"~/Views/Themes/" + activeThemeName + "/Shared/{0}.cshtml"
};
PartialViewLocationFormats = new[]
{
"~/Views/Themes/" + activeThemeName + "/{1}/{0}.cshtml",
"~/Views/Themes/" + activeThemeName + "/Shared/{0}.cshtml"
};
AreaViewLocationFormats = new[]
{
"~Areas/{2}/Views/Themes/" + activeThemeName + "/{1}/{0}.cshtml",
"~Areas/{2}/Views/Themes/" + activeThemeName + "/Shared/{0}.cshtml"
};
AreaPartialViewLocationFormats = new[]
{
"~Areas/{2}/Views/Themes/" + activeThemeName + "/{1}/{0}.cshtml",
"~Areas/{2}/Views/Themes/" + activeThemeName + "/Shared/{0}.cshtml"
};
}
}
完成 Custom View Engine 的設計後,接著必須在 Global.asax 進行註冊,並且將 Custom View Engine 的順序設定為最優先 (index 0):
Global.asax
if (!string.IsNullOrEmpty(ConfigurationManager.AppSettings["ActiveTheme"]))
{
var activeTheme = ConfigurationManager.AppSettings["ActiveTheme"];
ViewEngines.Engines.Insert(0, new ThemeViewEngine(activeTheme));
};
而在 Web.config 中調整 ActiveTheme 即可以讓 Controller 在回應 Action 所需要使用的 View 時,優先至 Custom View Engine 所定義的路徑進行 cshtml 檔案尋找,如沒有才會回到以往的尋找路徑 🙂
<configuration>
<appSettings>
<add key="ActiveTheme" value="Vertical"/>
</appSettings>
</configuration>
Custom Action Results
在 Improving Application Responses with Custom Action Results 的課程模組中,說明如何客製化 Action Results,例如回應 XML 或者是 CSV 檔。
要客製化 Action Results,可以藉由繼承 ActionResult
類別,並且覆寫 ExecuteResult
方法來達成。
Infrastructure/XMLResult.cs
public class XMLResult : ActionResult
{
private object _data;
public XMLResult(object data)
{
_data = data;
}
public override void ExecuteResult(ControllerContext context)
{
XmlSerializer serializer = new XmlSerializer(_data.GetType());
var response = context.HttpContext.Response;
response.ContentType = "text/xml";
serializer.Serialize(response.Output, _data);
}
}
接著就可以在 Controller 當中使用:
public ActionResult XMLService()
{
...
return new XMLResult(vmApplicants);
}
而許多開發情境下,繼承 ASP.NET MVC 既有的 ActionResult 省去重複的底層實作會讓開發與維護更為輕鬆,例如回應 CSV 檔案,可以利用既有的 FileResult 去覆寫 ExecuteResult
方法:
Infrastructure/CSVResult.cs
public class CSVResult : FileResult
{
private IEnumerable _data;
public CSVResult(IEnumerable data, string fileName) : base("text/csv")
{
_data = data;
FileDownloadName = fileName;
}
protected override void WriteFile(HttpResponseBase response)
{
var builder = new StringBuilder();
var stringWriter = new StringWriter(builder);
foreach (var item in _data)
{
var properties = item.GetType().GetProperties();
foreach (var prop in properties)
{
stringWriter.Write(GetValue(item, prop.Name));
stringWriter.Write(", ");
}
stringWriter.WriteLine();
}
response.Write(builder);
}
public static string GetValue(object item, string propName)
{
return item.GetType().GetProperty(propName).GetValue(item, null).ToString() ?? "";
}
}
Model
Extending Validation
在 Extending Validation to Improve Data Integrity 的課程模組中,說明除 ASP.NET MVC 預設的 Validation Attributes
外,也可以利用在 Model 加入時做 IValidatableObject
的方式,來達到資料 Model 的驗證,Validation Attributes
的優點是驗證的邏輯關注點分離、易於重複使用;IValidatableObject
則是易於設計跨屬性的資料驗證邏輯。
BirthdateValidator.cs 範例說明如何客製化 Validation Attributes
。
Infrastructure\BirthdateValidator.cs
public class BirthdateValidator : ValidationAttribute
{
public BirthdateValidator()
{
ErrorMessage =
"Please enter a valid birth date.";
}
public override bool IsValid(object value)
{
DateTime enteredDate;
if (DateTime.TryParse(value.ToString(), out enteredDate))
{
return enteredDate <= DateTime.Now.AddYears(-18);
}
else
{
return false;
}
}
}
EmploymentSummary.cs 則是以 ViewModel 結合 IValidatableObject
的方式進行資料驗證,
ViewModels\EmploymentSummary.cs
public class EmploymentSummary : IValidatableObject
{
public EmploymentSummary()
{
PrimaryEmployer = new EmploymentVM();
PreviousEmployer = new EmploymentVM();
}
public EmploymentVM PrimaryEmployer { get; set; }
public EmploymentVM PreviousEmployer { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
if (PrimaryEmployer.StartDate > DateTime.Now.AddYears(-3))
{
if (PreviousEmployer.EmploymentType != "Unemployed")
{
if (string.IsNullOrEmpty(PreviousEmployer.Employer))
{
results.Add(new ValidationResult("Previous employer is required."));
}
if (string.IsNullOrEmpty(PreviousEmployer.Position))
{
results.Add(new ValidationResult("Previous position is required."));
}
}
}
return results;
}
}
Custom Model Binders
在 Extending Data Binding with Custom Model Binders 的課程模組中,介紹如何客製 Model Binder,並以處理 Client 端 XML 格式的 InputStream 為例。
ASP.NET MVC 的 Action Invoker 在 Controller 處理傳入的參數時,會依序使用 Model Binder 檢查是否可以處理對應的參數,如果不行就換下一個 Model Binder 直到 Default Model Binder 都無法處理則報錯為止;除了可以將直接客製化的 Model Binder 加入順序以外,也可以藉由客製化 Model Binder Provider 的方式來達到特定時機使用特定的 Model Binder。
示範如何客製 Model Binder ,搭配分別建立類別實作 IModelBinder
以及 IModelBinderProvider
介面。
public interface IModelBinder
{
object BindModel(ControllerContext controllerContext, ModelBindingContenxt bindingContext);
}
public interface IModelBinderProvider
{
IModelBinder object GetBinder(Type model);
}
Infrastructure/XMLModelBinder.cs
public class XmlModelBinder : IModelBinder
{
public object BindModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
try
{
var modelType = bindingContext.ModelType;
var serializer = new XmlSerializer(modelType);
var inputStream = controllerContext.HttpContext.Request.InputStream;
return serializer.Deserialize(inputStream);
}
catch(Exception ex) {
bindingContext.ModelState.AddModelError("", "The item could not be serialized");
return null;
}
}
}
Infrastructure/XMLModelBinderProvider.cs
public class XmlModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
var contentType = HttpContext.Current.Request.ContentType;
if (string.Compare(contentType, @"text/xml",
StringComparison.OrdinalIgnoreCase) != 0)
{
return null;
}
return new XmlModelBinder();
}
}
接著到 Global.asax 進行 Model Binder Provider 的註冊:
Global.asax
protected void Application_Start()
{
ModelBinderProviders.BinderProviders.Add(new XmlModelBinderProvider());
}
相關參考:保哥的 ASP.NET MVC 開發心得分享 (25):ModelBinder 與 ValueProvider 的用途
Custom Value Providers
在 Improving Data Availability with Custom Value Providers 的課程模組中,示範如何藉由 Value Provider 將 Http Request Headers 的資料讀入 Model 之中,方便在 Controller 使用。
Custom Value Provider 是除了重新實作 IModelBinder 或者是覆寫既有 ModelBinder BindModel
, BindProperty
以外,同樣可以達成資料 Binding 的擴充方式。
public interface IValueProvider
{
bool ContainsPrefix(string prefix);
ValueProviderResult GetValue(string key);
}
Infrastructure/HttpValueProvider.cs
public class HttpValueProvider : IValueProvider
{
private readonly NameValueCollection _headers;
private readonly string[] _headerKeys;
public HttpValueProvider(NameValueCollection httpHeaders)
{
_headers = httpHeaders;
_headerKeys = _headers.AllKeys;
}
public bool ContainsPrefix(string prefix)
{
return _headerKeys.Any(h => h.Replace("-", "")
.Equals(prefix, StringComparison.OrdinalIgnoreCase));
}
public ValueProviderResult GetValue(string key)
{
var header = _headerKeys.FirstOrDefault(
h => h.Replace("-", "").Equals(key, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(_headers[header]))
{
return new ValueProviderResult(
_headers[header],
_headers[header],
CultureInfo.CurrentCulture);
}
return null;
}
}
public class HttpValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
return new HttpValueProvider(controllerContext.HttpContext.Request.Headers);
}
}
接著著在 Global.asax 進行 Value Provider Factory 的註冊。
Global.asax
protected void Application_Start()
{
ValueProviderFactories.Factories.Insert(0, new HttpValueProviderFactory());
}
從 ASP.NET MVC 的原始碼,我們可以觀察各種預設的 Value Provider 順序,從而 Model Binder 在繫結模型時,取得資料的優先順序。
ValueProviderFactories.cs
public static class ValueProviderFactories
{
private static readonly ValueProviderFactoryCollection _factories = new ValueProviderFactoryCollection()
{
new ChildActionValueProviderFactory(),
new FormValueProviderFactory(),
new JsonValueProviderFactory(),
new RouteDataValueProviderFactory(),
new QueryStringValueProviderFactory(),
new HttpFileCollectionValueProviderFactory(),
new JQueryFormValueProviderFactory()
};
public static ValueProviderFactoryCollection Factories
{
get { return _factories; }
}
}
- Value Provider
- 提供網站資料的提供程式,它可以從不同的來源(例如表單、請求查詢字符串、請求頭)中獲取資料;從請求中擷取參數值,並將其填入 ValueProviderResult 物件中。
- Model Binder Provider
- 選擇適當的 Model Binder 的提供程式,根據模型類型和資料來源選據對應的 Model Binder;
- Model Binder
- 綁定模型對象的系統,它使用 Value Provider 中的資料來填充模型對象;將 ValueProviderResult 值與 MyInputModel 參數物件繫結。
Controller
Action Filters
在 Organizing Application Flow with Action Filters 的課程模組中
Authentication Filtes
在 Customizing Security Using Authentication Filters 的課程模組中
Exception Filters
在 Improving Error Handling with Custom Exception Filters 的課程模組中
Action Method Selectors
在 Influencing Action Method Execution Using Custom Selectors 的課程模組中