请别埋没了URL Routing
本文做法不甚妥當,更好的做法請參考:《對Action方法的參數進行雙向轉化》
實現分析
既然Model Binder機制有著明顯的缺陷,那么我們又該如何處理這樣的問題呢?
我們再來回顧一下目前問題:對于從URL中表現出來的參數,我們可以把URL Routing捕獲到的數據使用Model Binder進行轉化(例如上例中的DateTimeModelBinder);但是如果我們在生成URL時直接提供復雜參數,則框架只會把它簡單的ToString后放入URL。這是因為那些與URL有關的HTML Helper會將數據交給URL Ruoting組件來生成URL,而Route規則在生成URL時不知道一個復雜對象該如何轉變為URL,因此……
慢著,你剛才說,把數據“交給URL Routing組件來生成URL”?URL Routing不是解析URL用的嗎?為什么還負責“生成”URL?沒錯,與Model Binder不同,URL Routing的工作職責是“雙向”的。它既負責從URL中提取RouteData,也負責根據Route生成一個URL——可惜微軟沒有對URL Routing給出足夠的資料,有相當多的朋友沒有意識到這一點。
可惡的微軟。
既然問題的原因是Model Binder的“單向性”,那么如果存在一個“雙向”的Model Binder就應該可以解決問題。例如,我們可以繼承現有的IModelBinder接口進行擴展,那么至少從解析URL到執行Action方法這個流程中所有的功能都不需要任何額外工作。可惜,這種做法對于大多數HTML Helper來說,我們就必須定義新的擴展,才能利用所謂的“雙向Model Binder”。不過其實我們可以有更好的解決方案——成本低廉,通用性強。既然上次提到了傳說中的“Model Binder強迫癥”,那么我們現在就把目光移到Model Binder以外的地方。
您一定已經猜到我們要從哪里入手了。沒錯,就是URL Routing。關于這方面,大名鼎鼎的Scott Hanselman同學提出將DateTime類型進行分割,也就是將一個DateTime切成年、月、日多個部分進行表示。這個做法老趙頗不贊同,無論從易用性還是通用性等角度來看,這種做法都是下下之策。說實話,這樣的做法其實并沒有跳出框架既有功能給定的圈子,它只是通過“迎合框架”來滿足自己的需求,而不是讓框架為我們的需求服務。
那么,我們來分析一下URL Routing組件的運作方式吧,這是必要的預備工作:
- 首先,應用程序為RouteCollection類型的RouteTable.Routes集合添加一些Route規則,每個規則即為一個RouteBase對象。RouteBase是一個抽象類型,其中包含兩個抽象方法,GetRouteData和GetVirtualPath。
- 在捕獲URL中數據的時候,URL Routing組件將調用RouteTable.Routes.GetRouteData方法來獲得一個RouteData對象。簡單來說,它會依次調用每個RouteBase對象的GetRouteData方法,直到得到第一個不為null的RouteData對象。
- 在生成URL時,URL Routing組件將調用RouteTable.Routes.GetVirtualPath方法來獲得一個VirtualPathData對象。簡單來說,它會依次調用每個RouteBase對象的GetVirtualPath方法,直到得到第一個不為null的VirualPathData對象。
顯然,光有RouteBase抽象類型是不足以提供任何有用功能的。因此URL Routing框架還提供了一個具體的Route類型供大家使用。說起Route類,它的功能可謂非常強大。我們在使用ASP.NET MVC框架時用到的MapRoute方法,其實就是在向RouteTable.Routes集合中添加Route對象。而其中的URL占位符,默認值,約束等功能,實際上完全由Route對象實現了。多么強大的Route類型!如果想要寫一個足以匹敵,并且包含額外功能的RouteBase實現可不是一件容易的事情。幸好我們生活在面向對象的美好世界中,“復用”是我們手中威力非凡的利器。如果我們基于現有的Route類型進行擴展,那么大部分的工作我們彈指間便可完成。
現有的Route只能從URL中提取字符串類型的數據,同時也只能把任何對象作為字符串來生成URL。而我們將要構造RouteBase實現,就要彌補這一缺陷,讓Route規則能夠直接從URL中提取出復雜對象,并且知道如何將一個復雜對象轉化為一個URL。有了前者,RouteData就能包含復雜類型的對象,以此應對Action方法的參數自然不是問題;有了后者,我們只需要提供一個強類型的復雜對象,Route規則也能順利地將其轉化為可以識別的URL——多么美好。
Route Formatter
那么解析字符串,或生成URL的職責由誰來完成呢?于是我們定義一個IRouteFormatter來負責這件事情:
public interface IRouteFormatter {bool TryParse(object value, out object output);bool TryToString(object value, out string output); }TryParse方法負責將一個對象轉化為我們需要的復雜類型對象,而TryToString則將一個復雜類型對象轉化為字符串(即URL)。兩個方法都返回一個布爾值,以表示這次轉化是否合法。您可能會發現,TryToString輸出的是一個string,而TryParse……他接受的是一個object類型的參數,這是怎么回事呢?原因在于Route規則中的“默認值”設置。在Route規則中我們可以為RouteData中的某個“字段”設定默認值,這樣即使URL中無法捕獲到這個字段,它也可以出現在RouteData中。從URL中捕獲得到的自然是一個字符串,但是默認值則可以設為任意類型的對象。因此Formatter需要可以接受一個object參數,并設法將其轉化為我們需要的復雜類型。
是不是有點繞?請繼續看下去,您會了解它的作用的。雖說TryParse需要接受一個object參數,但是在大多數情況下,我們更多是要處理強類型。因此我們不妨再定一個RouteFormatter抽象類,方便強類型IRouteFormatter對象的編寫:
public abstract class RouteFormatter<T> : IRouteFormatter {public abstract bool TryParse(string value, out T output);public abstract bool TryToString(T value, out string output);bool IRouteFormatter.TryParse(object value, out object output){if (value is T){output = value;return true;}string s = value as string;if (s == null){output = null;return false;}else{T t;var result = this.TryParse(s, out t);output = t;return result;}}bool IRouteFormatter.TryToString(object value, out string output){if (value is T){return this.TryToString((T)value, out output);}else{output = null;return false;}} }RouteFormater<>類接受一個范型參數,并且準備兩個強類型的抽象方法讓子類實現。至于接口中的兩個類型,它們會處理一部分邏輯——主要是類型判斷——只在合適的時候將操作交給范型方法來實現。TryToString方法樸實無華,而TryParse方法相對較為有趣,它會首先判斷value參數的類型,如果已經符合當前的范型類型,則直接將其轉化后返回。這就是為了“默認值”而進行的處理,例如用戶準備了一個DateTime類型的默認值,并被Route規則采納了,則我們的RouteFormatter<DateTime>就會將其直接返回,不做任何轉化。
為了解決目前提出的問題,我們會編寫一個DateTimeFormatter,它接受一個Format參數表示日期的格式:
public class DateTimeFormatter : RouteFormatter<DateTime> {public string Format { get; private set; }public DateTimeFormatter(string format){this.Format = format;}public override bool TryParse(string value, out DateTime output){return DateTime.TryParseExact(value, this.Format, null, DateTimeStyles.None, out output);}public override bool TryToString(DateTime value, out string output){output = value.ToString(this.Format);return true;} }那么有沒有某個Route Formatter需要直接實現IRouteFormatter接口呢?有。之前提到TryParse方法將在value參數符合范型T的情況下直接返回“通過”,如果某個Route Formatter不支持這條判斷,則自然無法繼承于RouteFormatter<>類型。例如下面的RegexFormatter,將使用正則表達式對某個字段的值進行約束。在我們的RouteBase實現中,RegexFormatter便是Route類中“約束”功能的替代品。如下:
public class RegexFormatter : IRouteFormatter {public Regex Regex { get; private set; }public RegexFormatter(string pattern){this.Regex = new Regex(pattern,RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled);}public bool TryParse(object value, out object output){string s;bool result = this.Try(value, out s);output = s;return result;}public bool TryToString(object value, out string output){return this.Try(value, out output);}private bool Try(object value, out string output){var s = value as string;if (s != null && this.Regex.IsMatch(s)){output = s;return true;}else{output = null;return false;}} }RegexFormatter的關鍵在于Try方法。Try方法首先判斷value參數是否為一個字符串,如果是,則使用正則表達式進行驗證。當且僅當value為字符串并滿足指定的正則表達式時,RegexFormatter才表示“通過”。
FormatRoute實現
FormatRoute便是我們RouteBase抽象類的實現,它提供了Route類的所有功能,并可以為每個字段設置一個Route Formatter對象,以此對這個字段進行轉換或約束。之前提到,我們會將主要功能委托給現有Route類型,這樣可以大大簡化我們的工作量。因此,我們會在FormatRoute中包含一個Route類型的對象,此外還會保留所有字段與其Route Formatter的映射關系。請看如下構造函數:
public class FormatRoute : RouteBase {private Route m_route;private IDictionary<string, IRouteFormatter> m_formatters;public FormatRoute(string url,RouteValueDictionary defaults,IDictionary<string, IRouteFormatter> formatters,RouteValueDictionary constaints,RouteValueDictionary dataTokens,IRouteHandler routeHandler){this.m_formatters = formatters;this.m_route = new Route(url,defaults,constaints,dataTokens,routeHandler);}... }RouteBase的關鍵方法便是GetRouteData和GetVirtualPath。有了Route類型的輔助,這兩個方法其實非常簡單。如下:
public override RouteData GetRouteData(HttpContextBase httpContext) {var result = this.m_route.GetRouteData(httpContext);if (result == null) return null;var valuesModified = new Dictionary<string, object>();foreach (var pair in result.Values){var key = pair.Key;IRouteFormatter formatter = null;if (this.m_formatters.TryGetValue(key, out formatter)){object o;if (formatter.TryParse(pair.Value, out o)){valuesModified[key] = o;}else{return null;}}}foreach (var pair in valuesModified){result.Values[pair.Key] = pair.Value;}return result; }public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) {var routeValues = new RouteValueDictionary();foreach (var pair in values){var key = pair.Key;IRouteFormatter formatter = null;if (this.m_formatters.TryGetValue(key, out formatter)){string s;if (formatter.TryToString(pair.Value, out s)){routeValues[key] = s;}else{return null;}}else{routeValues[key] = pair.Value;}}return this.m_route.GetVirtualPath(requestContext, routeValues); }GetRouteData會接受一個HttpContextBase對象,并調用Route對象的GetRouteData方法獲取一個RouteData對象。如果RouteData不為null,則遍歷其中的所有字段,如果指定了對應的Route Formater,則還需要通過Route Formatter的檢驗及轉化——沒錯,經歷了Route Formatter之后的RouteData中已經包含了強類型對象。而GetVirtualPath方法則略有不同,它首先遍歷values參數中的所有字段,將其中的強類型對象轉化為字符串,也就是URL片段,這樣交給Route對象來生成VirtualPathData時,便可以得到正確的URL了。
最后便是FormatRoute的運用:
routes.Add("Demo.Date",new FormatRoute("{controller}/{action}/{date}",new RouteValueDictionary(), // defaultsnew Dictionary<string, IRouteFormatter>{{"controller", new RegexFormatter("Demo")},{"action", new RegexFormatter("Date")},{"date", new DateTimeFormatter("yyyy-MM-dd")}},new RouteValueDictionary(), // constaintsnew RouteValueDictionary(), // data tokensnew MvcRouteHandler()));除了為date字段指定了轉化用的DateTimeFormatter之外,我們也為controller和action字段提供了負責約束的RegexFormatter——這點只是為了演示。更好的做法是直接將URL設為Demo/Date/{date},并在默認值中指定controller和action的值。此外,您也可以使用傳統的方式為字段提供約束,而不是使用RegexFormatter。當然,效果幾乎可以說是一模一樣的。
總結
現在我們完美地解決了之前提出的問題。使用FormatRoute可以輕松地處理URL中特定類型對象的提取,并且可以把特定類型的對象轉化為URL的片段。除了日期時間之外,我們還可以轉化語言文化,查詢條件等任意復雜類型。而RouteFormatter對象與Route規則的分離,使得我們可以對RouteFormatter進行獨立的單元測試,這也是一件十分理想的事情。這下在視圖中,無論是指定Route Values,還是使用強類型的方式,我們都可以正確獲得所需的URL了。如下:
<%= Html.ActionLink("Yesterday", "Date", new { date = date.AddDays(-1) }) %> <span><%= date.ToShortDateString() %></span> <%= Html.ActionLink<DemoController>(c => c.Date(date.AddDays(1)), "Tomorrow") %>那么,從設計上講,把數據的提取轉移到URL Routing上是否合適呢?答案是肯定的。因為URL Routing的職責原本就是從URL中提取數據——任意類型的數據,以及把數據轉化為URL,我們現在只是充分利用了URL Routing的功能而已。事實上,我建議任何使用URL表示的數據,都把轉化的職責轉移到URL Routing這一層,因為這時我們基本上無可避免地需要根據數據來生成URL。一般情況下,我們要盡可能地使用強類型數據。那么Model Binder難道就沒有用了嗎?當然不是。URL Routing負責從URL中提取數據,而Model Binder則用于從其他方面來獲取參數。例如POST來的數據,例如《最佳實踐》中的Url Referrer參數。
打開視野,發揮程序員的敏捷思路,生活就會變得更加美好。
總結
以上是生活随笔為你收集整理的请别埋没了URL Routing的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 限制edit只能输入数字
- 下一篇: 【Vegas原创】添加SQL Serve