MVC Core新增了ViewComponent的概念,直接強行理解爲視圖組件,用於在頁面上顯示可重用的內容,這部份內容包括邏輯和展現內容,並且定義爲組件那麼其一定是能夠獨立存在而且是高度可重用的。html
其實從概念上講,在ASP.NET的歷史版本中早已有實現,從最開始的WebForm版本就提供了ascx做爲用戶的自定義控件,以控件的方式將獨立的功能封裝起來。到了後來的MVC框架,提供了partial view,可是partial view就是提供視圖的重用,業務數據仍是依賴於action提供。git
MVC還要一個更加獨立的實現方式就是用Child Action,所謂Child Action就是一個Action定義在Controller裏面,而後標記一個[ChildAction]屬性標記。我在早期作MVC項目的時候,感受到Child Action的功能很不錯,將一些每一個頁面(但不是全部頁面)須要用到的小部件都作成Child Action,好比登陸信息,左側菜單欄等。一開始以爲不錯,後來發現了嚴重性能問題結果被吊打。問題的元兇是Child Action執行的時候會把Controller的那一套生命週期的環節再執行一遍,好比OnActionExecuting,OneActionExecuted等,也就是說致使了不少無用的重複操做(由於當時OnActionExecuting也被用得很氾濫)。以後覆盤分析,Child Action執行動做的時候就比如在當前ActionExcuted以後再開出一個分支去執行其餘任務,致使了Controller執行過程又嵌套了一個Controller執行過程,嚴重違背了扁平化的思想(當時公司的主流設計思想),因此後來都沒用Child Action。github
簡單回顧了過去版本的實現,咱們來看看MVC Core的ViewComponenet。從名稱定義來看,是要真的把功能數據和頁面都獨立出來,要否則配不上組件二字。ViewComponent獨立於其所在的View頁面和Action,更不會跟當前的Controller有任何的瓜葛,也就是說不是Child Action了。固然ViewComponent也能夠重用父頁面的數據或者從而後用本身的View。固然ViewComponent是不能獨立使用的,必須在一個頁面內被調用。後端
接下來看看ViewComponent的幾種建立方式框架
首先準備一個項目,這裏使用基於Starter kit項目項目模板建立一個用於練習的項目前後端分離
(該項目模板能夠在這裏下載https://marketplace.visualstudio.com/items?itemName=junwenluo.ASPNETMVCCoreStarterKit)異步
運行後的初始界面是這樣的async
方式一 建立POCO View Component函數
POCO估計寫過程序的都知道是什麼東西,能夠簡單理解爲一個比較純粹的類,不依賴於任何外部框架或者包含附加的行爲,純粹的只用CLR的語言建立。性能
用POCO方式建立的ViewComponent類必須用ViewComponent結尾,這個類能定義在任何地方(只要能被引用到),這裏咱們新建一個ViewComponents目錄,而後新建以下類
public class PocoViewComponent { private readonly IRepository _repository; public PocoViewComponent(IRepository repository) { _repository = repository; } public string Invoke() { return $"{_repository.Cities.Count()} cities, " + $"{_repository.Cities.Sum(c => c.Population)} people"; } }
這個類很簡單,就是提供一個Invoke方法,而後返回城市的數量和總人口信息。
而後在頁面中應用,咱們把調用語句放在_Layout.cshtml頁面中
@await Component.InvokeAsync("Poco")
將右上角的City Placeholder替換掉,那麼運行以後能夠看到右上角的內容輸出了咱們預期的內容
那麼這個就是最簡單的實現ViewComponent的方式,從這個簡單的例子能夠看到咱們只須要提供一個約定的Invoke方法便可。
從這個簡單的Component能夠看到有如下3個優點
1 ViewComponent支持經過構造函數注入參數,也就是支持常規的依賴注入。有了依賴注入的支持,那麼Component就有了本身
獨立的數據來源
2 支持依賴注入意味着能夠進行獨立的單元測試
3 因爲Component的實現的高度獨立性,其能夠被應用於多處,固然不會跟任何一個Controller綁定(也就不會有ChildAction帶來的麻煩)
方式二 基於ViewComponentBase建立
基於POCO方式建立的ViewComponent的好處是簡單,不依賴於其餘類。若是基於ViewComponentBase建立,那麼就有了一個上下文,畢竟也是一個基礎類,總會提供一些幫助方法吧,就跟Controller一個道理。
下面是基於ViewComponent做爲基類建立一個ViewComponent
public class CitySummary : ViewComponent { private readonly IRepository _repository; public CitySummary(IRepository repository) { _repository = repository; } public string Invoke() { return $"{_repository.Cities.Count()} cities, " + $"{_repository.Cities.Sum(c => c.Population)} people"; } }
咋一看跟用POCO的方式沒有什麼區別,代碼拷貝過來就能用,固然輸出的內容也是一致的。
固然這個例子只是證實這種實現方式,還沒體現出繼承了ViewComponentBase的好處。下面來了解一下這種方式的優點
1.實際項目中使用deViewComponent哪有這麼簡單,僅僅輸出一行字符串。若是遇到須要輸出很複雜的頁面,那豈不是要拼湊很複雜的字符串,這樣不優雅,更不用說單元測試,先後端分離這樣的高大上的想法。因此ViewComponentBase就提供了一個相似Controller那樣的解決方法,提供了幾個內置的ResultType,然你返回到結果符合面向對象的思想,一次過知足上述的要求,主要有如下三種結果類型
a.ViewVIewComponentResult
能夠理解爲Controller的ViewResult,就是結果是經過一個VIew視圖來展現
b.ContentViewComponentResult
相似於Controller的ContentResult,返回的是Encode以後的文本結果
c.HtmlContentViewComponentResult
這個用於返回包含Html字符串的內容,也就是說這些Html內容須要直接顯示,而不是Encode以後再顯示。
有了上述的理解,藉助ViewComponentBase提供的一些基類方法就能夠輕鬆實現顯示一個複雜的視圖,跟Controller相似。
下面咱們改進一下CitySummary,改爲輸出一個ViewModel,並經過獨立的View去定義Html內容。
public class CitySummary : ViewComponent { private readonly IRepository _repository; public CitySummary(IRepository repository) { _repository = repository; } public IViewComponentResult Invoke() { return View(new CityViewModel { Cities = _repository.Cities.Count(), Population = _repository.Cities.Sum(c => c.Population) }); } }
注意Invoke的實現代碼,其中使用了View的方法。
這個View方法的實現邏輯相似Controller的View,可是尋找View頁面的方式不一樣,其尋找頁面文件的路徑規則以下
/Views/{CurrentControllerName}/Components/{ComponentName}/Default.cshtml
/Views/Shared/Components/{ComponentName}/Default.cshtml
根據這規則,在View/Shared/目錄下建立一個Components目錄,而後再建立CitySummary目錄,接着新建一個Default.cshtml頁面
@model CityViewModel <table class="table table-condensed table-bordered"> <tr> <td>Cities:</td> <td class="text-right"> @Model.Cities </td> </tr> <tr> <td>Population:</td> <td class="text-right"> @Model.Population.ToString("#,###") </td> </tr> </table>
儘管頁面比較簡單,可是比起以前拼字符串的方式更增強大了,下面是應用後右上角的變化效果
這就是使用View的方式,其餘兩種結果類型的使用方式跟Controller的相似。
除了調用View方法以外,經過ViewComponentBase還能夠得到當前請求的上下文信息,好比路由參數。
好比讀取請求id,而後加載對應Country的數據
public IViewComponentResult Invoke() { var target = RouteData.Values["id"] as string; var cities = _repository.Cities.Where( city => target == null || Compare(city.Country, target, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); return View(new CityViewModel { Cities = cities.Count(), Population = cities.Sum(c => c.Population) }); }
固然也能夠經過方法參數的形式傳入id,好比咱們能夠在頁面調用的時候傳入id參數,那麼Invoke方法能夠改爲以下
public IViewComponentResult Invoke(string target) { target = target ?? RouteData.Values["id"] as string; var cities = _repository.Cities.Where( city => target == null || Compare(city.Country, target, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); return View(new CityViewModel { Cities = cities.Count(), Population = cities.Sum(c => c.Population) }); }
而後在界面調用的時候
@await Component.InvokeAsync("CitySummary", new { target = "USA" }),傳入target參數。
上面介紹的都是同步執行的ViewComponent,接下來咱們來看看支持異步操做的ViewComponent。
下面咱們建立一個WeatherViewComponent,獲取城市的天氣,這獲取天氣經過異步的方式從外部獲取。
在Components文件夾建立一個CityWeather文件夾,而後建立一個Default.cshtml文件,內容以下
@model string <img src="http://@Model"/>
這個頁面只是顯示一個天氣的圖片,具體的值經過服務端返回。
而後在ViewComponents目錄新建一個CityWeather類,以下
public class CityWeather : ViewComponent { private static readonly Regex WeatherRegex = new Regex(@"<img id=cur-weather class=mtt title="".+?"" src=""//(.+?.png)"" width=80 height=80>"); public async Task<IViewComponentResult> InvokeAsync(string country, string city) { city = city.Replace(" ", string.Empty); using (var client = new HttpClient()) { var response = await client.GetAsync($"https://www.timeanddate.com/weather/{country}/{city}"); var content = await response.Content.ReadAsStringAsync(); var match = WeatherRegex.Match(content); var imageUrl = match.Groups[1].Value; return View("Default", imageUrl); } } }
這個ViewComponent最大的特別之處是,它從外部獲取城市的天氣信息,這個過程使用的async的方法,異步從http下載獲得內容後,解析返回當前天氣的圖片。
對於每個城市咱們均可以調用這個ViewComponent,在城市列表中增長一列顯示當前的天氣圖片
最後一種建立方式:混雜在Controller中
聽名字就以爲不對勁了,難道又回到ChildAction的老路。其實不是,先看看定義。
就是說將ViewComponent的Invoke方法定義在Controller中,Invoke的方法簽名跟以前兩種方式相同。
那麼這麼作的目的其實是爲了某些代碼的共用,很少說先看看代碼如何實現。
在HomeController加入以下方法
public IViewComponentResult Invoke() => new ViewViewComponentResult { ViewData = new ViewDataDictionary<IEnumerable<City>>(ViewData, _repository.Cities) };
這個Invoke方法就是普通的ViewComponent必須的方法,最關鍵是重用了這個Controller裏面的_repository,固然實際代碼會更有意義些。
而後給HomeController加入以下屬性標記
[ViewComponent(Name = "ComboComponent")]
這裏使用了ViewComponent這個屬性標記在Controller上,一看就知道這是用來標記識別ViewComponent的。
接着建立視圖,在Views/Shared/Components/下建立一個ComboComponent目錄,並建立一個Default.cshtml文件
@model IEnumerable<City> <table class="table table-condensed table-bordered"> <tr> <td>Biggest City:</td> <td> @Model.OrderByDescending(c => c.Population).First().Name </td> </tr> </table>
而後調用跟其餘方式同樣,按名稱去Invoke
@await Component.InvokeAsync("ComboComponent")
小結
OK,以上就是ViewComponent的三種建立方式,都比較簡單易懂,推薦使用方式二。
示例代碼:https://github.com/shenba2014/AspDotNetCoreMvcExamples/tree/master/CustomViewComponent