ASP.NET Core ViewComponent 練習
2 |
ViewComponent 是 ASP.NET Core 新加入的網頁元件架構,類似前端框架都會支援的自訂網頁元素,Vue.js、Angular、React都有,允許在 HTML 用 <my-component-name></my-component-name>
這類標籤直接插入自訂元素。
這篇用顯示目前天氣當題材,練習寫個 ViewComponent。資料來源採用氣象局的一般天氣預報-今明36小時天氣預報 OpenData API,用 Facebook 帳號註冊拿到 apiKey,串 URL 用 HTTP GET 就能取回以下 JSON:
我寫了一個小類別簡單從 JSON 裡取出指定縣市當前 12 小時的天氣狀態及最低與最高氣溫,順便練習用.NET Core 的新玩具 - System.Text.Json 解析 JSON:(註:為避免程式複雜模糊焦點,我用了較簡單但不標準的程式寫法,建議做法補充於 TODO 註解)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
namespace MvcLab.Models
{
public class SimpleWeatherService
{
//TODO 宜改用 HttpClientFactory https://blog.darkthread.net/blog/httpclient-sigleton/
static readonly HttpClient httpClient = new HttpClient();
//TODO 實務應用時,URL 應移至 appSettings.json
static readonly string openDataApiUrl = "https://opendata.cwb.gov.tw/fileapi/v1/opendataapi/F-C0032-001?Authorization=...api_key...&downloadType=WEB&format=JSON";
public class WeatherData
{
public string Status { get; set; }
public string MaxTemp { get; set; }
public string MinTemp { get; set; }
}
public WeatherData GetTaipeiWeatherFromOpenDataApi()
{
//TODO 應改為 async/await
var json = httpClient.GetAsync(openDataApiUrl).Result.Content.ReadAsStringAsync().Result;
//https://blog.darkthread.net/blog/httpclient-sigleton/
using (var doc = JsonDocument.Parse(json,
new JsonDocumentOptions { AllowTrailingCommas = true }))
{
var taipeiData = doc
.RootElement
.GetProperty("cwbopendata")
.GetProperty("dataset")
.GetProperty("location")
.EnumerateArray()
.Single(o => o.GetProperty("locationName").GetString() == "臺北市")
.GetProperty("weatherElement")
.EnumerateArray();
Func<string, string> readParameterName =
(elemName) =>
taipeiData.Single(o => o.GetProperty("elementName").GetString() == elemName)
.GetProperty("time").EnumerateArray().First()
.GetProperty("parameter").GetProperty("parameterName").GetString();
return new WeatherData
{
Status = readParameterName("Wx"),
MaxTemp = readParameterName("MaxT"),
MinTemp = readParameterName("MinT")
};
}
}
}
}
接著寫一個 ViewComponent 顯示台北天氣。一般會在專案建立 ViewComponents 資料夾,統一擺放 ViewComponent 類別程式,新增一個 TaipeiWeatherViewComponent:
using MvcLab.Models;
namespace MvcLab.ViewComponents
{
[Microsoft.AspNetCore.Mvc.ViewComponent]
public class TaipeiWeatherViewComponent : Microsoft.AspNetCore.Mvc.ViewComponent
{
public string Invoke()
{
//TODO: 應改用 DI https://blog.darkthread.net/blog/aspnet-core-di-notes/
var svc = new SimpleWeatherService();
var data = svc.GetTaipeiWeatherFromOpenDataApi();
return $"現在台北天氣:{data.Status} /氣溫:{data.MinTemp}° - {data.MaxTemp}°";
}
}
}
ASP.NET Core 依據三種特徵判斷型別為 ViewComponent:
- class 註明 [Microsoft.AspNetCore.Mvc.ViewComponent] Attribute
- 類別名稱為 ***ViewComponent 以 ViewComponent 結尾
- 類別繼承 Microsoft.AspNetCore.Mvc.ViewComponent
三個條件只要滿足任一即可,這裡則做好做滿,三種都示範。ViewComponent 包含一個 Invoke() 方法傳回 string 作為顯示文字。
要使用 ViewComponent,在 .cshtml 最上方加入 @addTagHelper *, MvcLab
。此處 MvcLab 為組件名稱,我的專案叫 MvcLab 會編譯成 MvcLab.dll。這個宣告告訴 ASP.NET Core 載入 MvcLab.dll 中的所有 ViewComponent。至於要插入天氣顯示文字的地方則可寫成 <vc:taipei-weather></vc:taipei-weather>
,vc: 開頭表示為 ViewComponent,而 taipei-weather 由類別名稱 TaipeiWeatherViewComponent 自動轉換來的,輸入時 Visaul Studio 會提示,不用擔心敲錯。
修改 Index.cshtml,在 Welcome 標題上方加入天氣顯示:
@addTagHelper *, MvcLab
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<vc:taipei-weather></vc:taipei-weather>
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
成功!
以上示範的 Invoke() 傳回型別為 string 只能處理純文字,不怎麼實用。接著我們來做一些改良:
- 將查詢縣市改為參數
- 顯示內容改採 HTML 形式,並使用獨立 cshtml 決定排版
首先調整 SimpleWeatherService:
- 查詢方法更名為 GetWeatherFromOpenDataApi(),查詢地區改為參數由外部傳入
- WeatherData 類別加入 ZoneName 屬性
SimpleWeatherService 修改如下:
//...略...
public class WeatherData
{
public string ZoneName { get; set; }
public string Status { get; set; }
public string MaxTemp { get; set; }
public string MinTemp { get; set; }
}
public WeatherData GetWeatherFromOpenDataApi(string zoneName)
{
var json = httpClient.GetAsync(openDataApiUrl).Result.Content.ReadAsStringAsync().Result;
//https://blog.darkthread.net/blog/httpclient-sigleton/
using (var doc = JsonDocument.Parse(json,
new JsonDocumentOptions { AllowTrailingCommas = true }))
{
var taipeiData = doc
.RootElement
.GetProperty("cwbopendata")
.GetProperty("dataset")
.GetProperty("location")
.EnumerateArray()
//TODO: 省略比對不到縣市名稱之錯誤處理
.Single(o => o.GetProperty("locationName").GetString() == zoneName)
.GetProperty("weatherElement")
.EnumerateArray();
Func<string, string> readParameterName =
(elemName) =>
taipeiData.Single(o => o.GetProperty("elementName").GetString() == elemName)
.GetProperty("time").EnumerateArray().First()
.GetProperty("parameter").GetProperty("parameterName").GetString();
return new WeatherData
//...略...
我另外寫了一個 WeatherBlockViewComponent,Invoke() 方法增加 zoneName 參數,傳回型別以 IViewComponentResut 取代 string。ViewComponent 也像 Controller 一樣可呼叫 View() 以 Views 目錄相對位置的 .cshtml 當樣版呈現 Model 資料。我打算將查詢取得的氣象資料 Model 物件 (WeatherData) 當成參數傳給 View,呈現方式交由 View 全權做主,實現觀注點分離:
using Microsoft.AspNetCore.Mvc;
using MvcLab.Models;
namespace MvcLab.ViewComponents
{
public class WeatherBlockViewComponent : ViewComponent
{
public IViewComponentResult Invoke(string zoneName)
{
var svc = new SimpleWeatherService();
var data = svc.GetWeatherFromOpenDataApi(zoneName);
return View(data);
}
}
}
WeatherBlockViewComponent 預設 cshtml 要放在 Views/Shared/Components/WeatherBlock/Default.cshtml:
其內容如下:
@model MvcLab.Models.SimpleWeatherService.WeatherData
<div style="border: 1px solid #ddd; width: 120px; padding: 6px">
<fieldset>
<legend>
@Model.ZoneName
</legend>
<div>
<div style="font-size: 1.2em; color: cadetblue">
@Model.Status
</div>
<div style="color: darkslategrey">
@Model.MinTemp° ~ @Model.MaxTemp°
</div>
</div>
</fieldset>
</div>
若 ViewComponent Invoke() 包含輸入參數,Visual Studio 會主動提示參數名稱:
試著在 Index.cshtml 放上三個天氣區塊,成功!
功能完成了,但程式留有一些不符合 ASP.NET Core 實務規範的設計,例如:URL 寫死在程式裡、未使用 async 與 DI,下一篇我們再來看看如何改善它。
Tutorial of using ViewComponent in the ASP.NET Core web project.
Comments
# by Matol
WeatherBlock/Default.aspx 這邊是習慣性打上aspx嗎XD
# by Jeffrey
to Matol, (羞)打了十幾年,一時改不了