ViewComponent 是 ASP.NET Core 新加入的網頁元件架構,類似前端框架都會支援的自訂網頁元素,Vue.jsAngularReact都有,允許在 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:

  1. class 註明 [Microsoft.AspNetCore.Mvc.ViewComponent] Attribute
  2. 類別名稱為 ***ViewComponent 以 ViewComponent 結尾
  3. 類別繼承 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 只能處理純文字,不怎麼實用。接著我們來做一些改良:

  1. 將查詢縣市改為參數
  2. 顯示內容改採 HTML 形式,並使用獨立 cshtml 決定排版

首先調整 SimpleWeatherService:

  1. 查詢方法更名為 GetWeatherFromOpenDataApi(),查詢地區改為參數由外部傳入
  2. 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, (羞)打了十幾年,一時改不了

Post a comment