今年初分享過一個用 Response.Flush 在 ASP.NET MVC 實現即時進度回報的極簡風做法,但 ASP.NET Core 網站架構不同己無法適用。不管在什麼平台,極簡風永遠是我的最愛,所以,ASP.NET Core 版的簡易進度回報寫法來了!

ASP.NET Core 移除了 Reponse.Flush() 方法,最接近的替代方案是 Response.Body.FlushAsync(),配合 Response.Body.WriteAsync(),因此,WebForm / ASP.NET MVC 時代的 Reponse.Write() + Response.Flush() 概念稍加修改便能在 ASP.NET Core 重現。

繼續用昨天的 scanimage 當範例。

假設我想從 ASP.NET Core 呼叫 scanimage,透過 BeginErrorReadLine() 從 StandardError 接收進度回報並即時顯示在網頁上,讓使用者能看到 15.1%, 36.6%, 57.7%... 的數字跳動,正規做法可以考慮用 SignalR、WebSocketServer-Event實現即時資訊更新,但在小型網站或 Coding4Fun 專案,我更喜歡寫幾行程式就把它搞定。

先看執行結果:(註:為了方便在 Windows 測試,我寫了一個 scanimage 模擬器,故進度為 0.5 秒固定增加 5%)

接著來看程式碼。呼叫 scanimage 的部分我包成 ScanService 了,Scan() 方法要傳入一個 Action<string> Callback 函式接收 stderr 傳回的掃描進度資訊。由於進度數字要刷新在同一行,我寫了兩個 JavaScript 函式 printMessage()、updateProgress() 分別處理添加一行新訊息或更新進度訊息,在接收到 scanimage 回傳文字後,透過 Reponse.Body.WriteAsync() 輸出 <script>printMessage(...)或updateProgress(...)</script>,再呼叫 Reponse.Body.FlushAsync() 立即送出,但這裡有個眉角,即使呼叫了 FlushAsync(),Kestrel 會等累積到 1024 Bytes 後才真的送出,類似行為之前 IE 遇過,這次用同樣手法解決 - 將輸出內容補空白到 1024 個字元。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using M225dwScan.Models;

namespace M225dwScan.Controllers
{
    public class ScanController : Controller
    {
        private readonly ILogger<ScanController> _logger;
        private readonly ScanService scanSvc;

        public ScanController(ILogger<ScanController> logger, ScanService scanSvc)
        {
            _logger = logger;
            this.scanSvc = scanSvc;
        }

        public async Task Index(string mode, string resolution, string source,
            string format, int top = 0, int left = 0, int width = 100, int height = 100)
        {
            var body = Response.Body;
            Action<string, bool> print = async (msg, padding) => {
                if (padding) msg = msg.PadRight(1024, ' ');
                var data = System.Text.Encoding.UTF8.GetBytes(msg); 
                await body.WriteAsync(data, 0, data.Length);
                await body.FlushAsync();
            };

            print(@"
<html>
<head>
    <style>html,body{font-size:9pt;}</style>
</head>
<body></body>
<script>
function updateProgress(msg) {
    var p = document.getElementById('progress');
    if (!p) {
        p = document.createElement('div');
        p.setAttribute('id','progress');
        document.body.appendChild(p);
    }
    p.innerText = msg;
}
function printMessage(msg) {
    var m = document.createElement('div');
    m.innerText = msg;
    document.body.appendChild(m);
}
</script>", false);
            Func<object, string> toJson = o => System.Text.Json.JsonSerializer.Serialize(o);
            var img = await scanSvc.Scan(mode, resolution, source, format, top, left, width, height,
            (msg) => //回報進度 Callback,msg 為 scanimage stderr 顯示內容
            {
                if (msg.StartsWith('\r')) //以 \r 起首時為進度數字
                    print($"<script>updateProgress({toJson(msg.TrimStart('\r'))});</script>", true);
                else 
                    print($"<script>printMessage({toJson(msg)});</script>", true);
            });

            print("</html>", false);
        }
    }
}

雖稱不上嚴謹有效率,但簡單有效,短短幾行搞定,在想快速實現即時回報的場合不失為一個好選擇。

Example of using Response.Body.FlushAsync() to report progress realtime in ASP.NET Core.


Comments

Be the first to post a comment

Post a comment