ASP.NET MVC Core的ViewComponent


MVC Core新增了ViewComponent的概念,直接強行理解為視圖組件,用於在頁面上顯示可重用的內容,這部分內容包括邏輯和展示內容,而且定義為組件那么其必定是可以獨立存在並且是高度可重用的。

其實從概念上講,在ASP.NET的歷史版本中早已有實現,從最開始的WebForm版本就提供了ascx作為用戶的自定義控件,以控件的方式將獨立的功能封裝起來。到了后來的MVC框架,提供了partial view,但是partial view就是提供視圖的重用,業務數據還是依賴於action提供。

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。

簡單回顧了過去版本的實現,我們來看看MVC Core的ViewComponenet。從名稱定義來看,是要真的把功能數據和頁面都獨立出來,要不然配不上組件二字。ViewComponent獨立於其所在的View頁面和Action,更不會跟當前的Controller有任何的瓜葛,也就是說不是Child Action了。當然ViewComponent也可以重用父頁面的數據或者從然后用自己的View。當然ViewComponent是不能獨立使用的,必須在一個頁面內被調用。

接下來看看ViewComponent的幾種創建方式

首先准備一個項目,這里使用基於Starter kit項目項目模板創建一個用於練習的項目

(該項目模板可以在這里下載https://marketplace.visualstudio.com/items?itemName=junwenluo.ASPNETMVCCoreStarterKit

運行后的初始界面是這樣的

image

方式一 創建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替換掉,那么運行之后可以看到右上角的內容輸出了我們預期的內容

image

那么這個就是最簡單的實現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>

盡管頁面比較簡單,但是比起之前拼字符串的方式更加強大了,下面是應用后右上角的變化效果

image

這就是使用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,在城市列表中增加一列顯示當前的天氣圖片

image

 

最后一種創建方式:混雜在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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM