请别埋没了URL Routing
本文做法不甚妥當(dāng),更好的做法請參考:《對Action方法的參數(shù)進(jìn)行雙向轉(zhuǎn)化》
實(shí)現(xiàn)分析
既然Model Binder機(jī)制有著明顯的缺陷,那么我們又該如何處理這樣的問題呢?
我們再來回顧一下目前問題:對于從URL中表現(xiàn)出來的參數(shù),我們可以把URL Routing捕獲到的數(shù)據(jù)使用Model Binder進(jìn)行轉(zhuǎn)化(例如上例中的DateTimeModelBinder);但是如果我們在生成URL時直接提供復(fù)雜參數(shù),則框架只會把它簡單的ToString后放入U(xiǎn)RL。這是因?yàn)槟切┡cURL有關(guān)的HTML Helper會將數(shù)據(jù)交給URL Ruoting組件來生成URL,而Route規(guī)則在生成URL時不知道一個復(fù)雜對象該如何轉(zhuǎn)變?yōu)閁RL,因此……
慢著,你剛才說,把數(shù)據(jù)“交給URL Routing組件來生成URL”?URL Routing不是解析URL用的嗎?為什么還負(fù)責(zé)“生成”URL?沒錯,與Model Binder不同,URL Routing的工作職責(zé)是“雙向”的。它既負(fù)責(zé)從URL中提取RouteData,也負(fù)責(zé)根據(jù)Route生成一個URL——可惜微軟沒有對URL Routing給出足夠的資料,有相當(dāng)多的朋友沒有意識到這一點(diǎn)。
可惡的微軟。
既然問題的原因是Model Binder的“單向性”,那么如果存在一個“雙向”的Model Binder就應(yīng)該可以解決問題。例如,我們可以繼承現(xiàn)有的IModelBinder接口進(jìn)行擴(kuò)展,那么至少從解析URL到執(zhí)行Action方法這個流程中所有的功能都不需要任何額外工作。可惜,這種做法對于大多數(shù)HTML Helper來說,我們就必須定義新的擴(kuò)展,才能利用所謂的“雙向Model Binder”。不過其實(shí)我們可以有更好的解決方案——成本低廉,通用性強(qiáng)。既然上次提到了傳說中的“Model Binder強(qiáng)迫癥”,那么我們現(xiàn)在就把目光移到Model Binder以外的地方。
您一定已經(jīng)猜到我們要從哪里入手了。沒錯,就是URL Routing。關(guān)于這方面,大名鼎鼎的Scott Hanselman同學(xué)提出將DateTime類型進(jìn)行分割,也就是將一個DateTime切成年、月、日多個部分進(jìn)行表示。這個做法老趙頗不贊同,無論從易用性還是通用性等角度來看,這種做法都是下下之策。說實(shí)話,這樣的做法其實(shí)并沒有跳出框架既有功能給定的圈子,它只是通過“迎合框架”來滿足自己的需求,而不是讓框架為我們的需求服務(wù)。
那么,我們來分析一下URL Routing組件的運(yùn)作方式吧,這是必要的預(yù)備工作:
- 首先,應(yīng)用程序?yàn)镽outeCollection類型的RouteTable.Routes集合添加一些Route規(guī)則,每個規(guī)則即為一個RouteBase對象。RouteBase是一個抽象類型,其中包含兩個抽象方法,GetRouteData和GetVirtualPath。
- 在捕獲URL中數(shù)據(jù)的時候,URL Routing組件將調(diào)用RouteTable.Routes.GetRouteData方法來獲得一個RouteData對象。簡單來說,它會依次調(diào)用每個RouteBase對象的GetRouteData方法,直到得到第一個不為null的RouteData對象。
- 在生成URL時,URL Routing組件將調(diào)用RouteTable.Routes.GetVirtualPath方法來獲得一個VirtualPathData對象。簡單來說,它會依次調(diào)用每個RouteBase對象的GetVirtualPath方法,直到得到第一個不為null的VirualPathData對象。
顯然,光有RouteBase抽象類型是不足以提供任何有用功能的。因此URL Routing框架還提供了一個具體的Route類型供大家使用。說起Route類,它的功能可謂非常強(qiáng)大。我們在使用ASP.NET MVC框架時用到的MapRoute方法,其實(shí)就是在向RouteTable.Routes集合中添加Route對象。而其中的URL占位符,默認(rèn)值,約束等功能,實(shí)際上完全由Route對象實(shí)現(xiàn)了。多么強(qiáng)大的Route類型!如果想要寫一個足以匹敵,并且包含額外功能的RouteBase實(shí)現(xiàn)可不是一件容易的事情。幸好我們生活在面向?qū)ο蟮拿篮檬澜缰?#xff0c;“復(fù)用”是我們手中威力非凡的利器。如果我們基于現(xiàn)有的Route類型進(jìn)行擴(kuò)展,那么大部分的工作我們彈指間便可完成。
現(xiàn)有的Route只能從URL中提取字符串類型的數(shù)據(jù),同時也只能把任何對象作為字符串來生成URL。而我們將要構(gòu)造RouteBase實(shí)現(xiàn),就要彌補(bǔ)這一缺陷,讓Route規(guī)則能夠直接從URL中提取出復(fù)雜對象,并且知道如何將一個復(fù)雜對象轉(zhuǎn)化為一個URL。有了前者,RouteData就能包含復(fù)雜類型的對象,以此應(yīng)對Action方法的參數(shù)自然不是問題;有了后者,我們只需要提供一個強(qiáng)類型的復(fù)雜對象,Route規(guī)則也能順利地將其轉(zhuǎn)化為可以識別的URL——多么美好。
Route Formatter
那么解析字符串,或生成URL的職責(zé)由誰來完成呢?于是我們定義一個IRouteFormatter來負(fù)責(zé)這件事情:
public interface IRouteFormatter {bool TryParse(object value, out object output);bool TryToString(object value, out string output); }TryParse方法負(fù)責(zé)將一個對象轉(zhuǎn)化為我們需要的復(fù)雜類型對象,而TryToString則將一個復(fù)雜類型對象轉(zhuǎn)化為字符串(即URL)。兩個方法都返回一個布爾值,以表示這次轉(zhuǎn)化是否合法。您可能會發(fā)現(xiàn),TryToString輸出的是一個string,而TryParse……他接受的是一個object類型的參數(shù),這是怎么回事呢?原因在于Route規(guī)則中的“默認(rèn)值”設(shè)置。在Route規(guī)則中我們可以為RouteData中的某個“字段”設(shè)定默認(rèn)值,這樣即使URL中無法捕獲到這個字段,它也可以出現(xiàn)在RouteData中。從URL中捕獲得到的自然是一個字符串,但是默認(rèn)值則可以設(shè)為任意類型的對象。因此Formatter需要可以接受一個object參數(shù),并設(shè)法將其轉(zhuǎn)化為我們需要的復(fù)雜類型。
是不是有點(diǎn)繞?請繼續(xù)看下去,您會了解它的作用的。雖說TryParse需要接受一個object參數(shù),但是在大多數(shù)情況下,我們更多是要處理強(qiáng)類型。因此我們不妨再定一個RouteFormatter抽象類,方便強(qiáng)類型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<>類接受一個范型參數(shù),并且準(zhǔn)備兩個強(qiáng)類型的抽象方法讓子類實(shí)現(xiàn)。至于接口中的兩個類型,它們會處理一部分邏輯——主要是類型判斷——只在合適的時候?qū)⒉僮鹘唤o范型方法來實(shí)現(xiàn)。TryToString方法樸實(shí)無華,而TryParse方法相對較為有趣,它會首先判斷value參數(shù)的類型,如果已經(jīng)符合當(dāng)前的范型類型,則直接將其轉(zhuǎn)化后返回。這就是為了“默認(rèn)值”而進(jìn)行的處理,例如用戶準(zhǔn)備了一個DateTime類型的默認(rèn)值,并被Route規(guī)則采納了,則我們的RouteFormatter<DateTime>就會將其直接返回,不做任何轉(zhuǎn)化。
為了解決目前提出的問題,我們會編寫一個DateTimeFormatter,它接受一個Format參數(shù)表示日期的格式:
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需要直接實(shí)現(xiàn)IRouteFormatter接口呢?有。之前提到TryParse方法將在value參數(shù)符合范型T的情況下直接返回“通過”,如果某個Route Formatter不支持這條判斷,則自然無法繼承于RouteFormatter<>類型。例如下面的RegexFormatter,將使用正則表達(dá)式對某個字段的值進(jìn)行約束。在我們的RouteBase實(shí)現(xiàn)中,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的關(guān)鍵在于Try方法。Try方法首先判斷value參數(shù)是否為一個字符串,如果是,則使用正則表達(dá)式進(jìn)行驗(yàn)證。當(dāng)且僅當(dāng)value為字符串并滿足指定的正則表達(dá)式時,RegexFormatter才表示“通過”。
FormatRoute實(shí)現(xiàn)
FormatRoute便是我們RouteBase抽象類的實(shí)現(xiàn),它提供了Route類的所有功能,并可以為每個字段設(shè)置一個Route Formatter對象,以此對這個字段進(jìn)行轉(zhuǎn)換或約束。之前提到,我們會將主要功能委托給現(xiàn)有Route類型,這樣可以大大簡化我們的工作量。因此,我們會在FormatRoute中包含一個Route類型的對象,此外還會保留所有字段與其Route Formatter的映射關(guān)系。請看如下構(gòu)造函數(shù):
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的關(guān)鍵方法便是GetRouteData和GetVirtualPath。有了Route類型的輔助,這兩個方法其實(shí)非常簡單。如下:
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對象,并調(diào)用Route對象的GetRouteData方法獲取一個RouteData對象。如果RouteData不為null,則遍歷其中的所有字段,如果指定了對應(yīng)的Route Formater,則還需要通過Route Formatter的檢驗(yàn)及轉(zhuǎn)化——沒錯,經(jīng)歷了Route Formatter之后的RouteData中已經(jīng)包含了強(qiáng)類型對象。而GetVirtualPath方法則略有不同,它首先遍歷values參數(shù)中的所有字段,將其中的強(qiáng)類型對象轉(zhuǎn)化為字符串,也就是URL片段,這樣交給Route對象來生成VirtualPathData時,便可以得到正確的URL了。
最后便是FormatRoute的運(yùn)用:
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字段指定了轉(zhuǎn)化用的DateTimeFormatter之外,我們也為controller和action字段提供了負(fù)責(zé)約束的RegexFormatter——這點(diǎn)只是為了演示。更好的做法是直接將URL設(shè)為Demo/Date/{date},并在默認(rèn)值中指定controller和action的值。此外,您也可以使用傳統(tǒng)的方式為字段提供約束,而不是使用RegexFormatter。當(dāng)然,效果幾乎可以說是一模一樣的。
總結(jié)
現(xiàn)在我們完美地解決了之前提出的問題。使用FormatRoute可以輕松地處理URL中特定類型對象的提取,并且可以把特定類型的對象轉(zhuǎn)化為URL的片段。除了日期時間之外,我們還可以轉(zhuǎn)化語言文化,查詢條件等任意復(fù)雜類型。而RouteFormatter對象與Route規(guī)則的分離,使得我們可以對RouteFormatter進(jìn)行獨(dú)立的單元測試,這也是一件十分理想的事情。這下在視圖中,無論是指定Route Values,還是使用強(qiáng)類型的方式,我們都可以正確獲得所需的URL了。如下:
<%= Html.ActionLink("Yesterday", "Date", new { date = date.AddDays(-1) }) %> <span><%= date.ToShortDateString() %></span> <%= Html.ActionLink<DemoController>(c => c.Date(date.AddDays(1)), "Tomorrow") %>那么,從設(shè)計(jì)上講,把數(shù)據(jù)的提取轉(zhuǎn)移到URL Routing上是否合適呢?答案是肯定的。因?yàn)閁RL Routing的職責(zé)原本就是從URL中提取數(shù)據(jù)——任意類型的數(shù)據(jù),以及把數(shù)據(jù)轉(zhuǎn)化為URL,我們現(xiàn)在只是充分利用了URL Routing的功能而已。事實(shí)上,我建議任何使用URL表示的數(shù)據(jù),都把轉(zhuǎn)化的職責(zé)轉(zhuǎn)移到URL Routing這一層,因?yàn)檫@時我們基本上無可避免地需要根據(jù)數(shù)據(jù)來生成URL。一般情況下,我們要盡可能地使用強(qiáng)類型數(shù)據(jù)。那么Model Binder難道就沒有用了嗎?當(dāng)然不是。URL Routing負(fù)責(zé)從URL中提取數(shù)據(jù),而Model Binder則用于從其他方面來獲取參數(shù)。例如POST來的數(shù)據(jù),例如《最佳實(shí)踐》中的Url Referrer參數(shù)。
打開視野,發(fā)揮程序員的敏捷思路,生活就會變得更加美好。
總結(jié)
以上是生活随笔為你收集整理的请别埋没了URL Routing的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 限制edit只能输入数字
- 下一篇: 面试前的准备工作