(19/30)大家一起學Blazor:圖片上傳

(19/30)大家一起學Blazor:圖片上傳

在大部分的網站中,上傳圖片也是很重要的功能,今天我們就來操作下。

最後更新 2021/12/21 下午10:04
StrayaWorker
預計閱讀 5 分鐘
分類
Blazor
專題
一起學Blazor系列
標籤
.NET C# ASP.NET Core Blazor

在大部分的網站中,上傳圖片也是很重要的功能,今天我們就來操作一下。

(注意:這是用 Blazor Server 的方式,但最好不要上傳太多檔案,所以限制上傳 4 張照片的話就會提示,畢竟這些事都是在伺服器上做,負擔太大,微軟也建議用 .NET Core Web API 的方式操作)

我們先建立一個 Component FileUpload

下面程式碼為 FileUpload.razor,使用 Blazor 提供的 Component <InputFile>multiple 代表可以上傳多個檔案

@page "/FileUpload"

<div>
    <div>
        <InputFile OnChange="OnChange" multiple></InputFile>
    </div>
    <div>
        <MyButton value="Submit" class="btn btn-primary" type="submit" @onclick="OnSubmit"></MyButton>
    </div>
</div>
@if (ImageList.Count > 0)
{
    <table>
        <tr>
            @foreach (var img in ImageList)
            {
                <td>
                    <img src="@img" width="150" height="150"/>
                </td>
            }
        </tr>
    </table>
}

下面程式碼為 FileUpload.razor.cs,這裡用 partial class

using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.JSInterop;
using System.Text.Json;

namespace BlazorServer.Shared;

public partial class FileUpload
{
	private JsInteropClasses? _jsClass;

	// 取得 `<InputFile>` 的檔案內容
	public IReadOnlyList<IBrowserFile>? ImageFiles;

	public List<string> ImageList = new();
	public string? ImageSrc;
	[Inject] protected IJSRuntime Js { get; set; }

	/// <summary>
	///     用以判斷 runtime 期間在什麼環境執行
	/// </summary>
	[Inject]
	protected IWebHostEnvironment? Env { get; set; }

	protected override Task OnInitializedAsync()
	{
		_jsClass = new JsInteropClasses(Js);
		return base.OnInitializedAsync();
	}

	public async Task OnChange(InputFileChangeEventArgs e)
	{
		ImageList = new List<string>();
		const string format = "image/jpeg";

		// 取得檔案
		ImageFiles = e.GetMultipleFiles();
		foreach (var file in ImageFiles)
		{
			// 將圖片內容轉換成指定類型及最大尺寸
			var imageFile = await file.RequestImageFileAsync(format, 1200, 675);

			// 利用 Stream 讀取圖片內容
			await using var fileStream = imageFile.OpenReadStream();

			// 將 Stream 讀取到記憶體中,如果沒有確定要上傳不要這麼做,以免浪費記憶體
			await using var memoryStream = new MemoryStream();
			await fileStream.CopyToAsync(memoryStream);
			ImageSrc = $"data:{format};base64,{Convert.ToBase64String(memoryStream.ToArray())}";

			// 以 Data URI 的方式將圖片呈現
			ImageList.Add(ImageSrc);
		}
	}

	public async Task OnSubmit()
	{
		// 將提示訊息變成 ViewModel
		var sweetConfirm = new SweetConfirmViewModel
		{
			RequestTitle = "是否確定上傳圖片?",
			ResponseTitle = "上傳成功"
		};
		var jsonString = JsonSerializer.Serialize(sweetConfirm);
		var result = await _jsClass!.Confirm(jsonString);
		if (result && ImageFiles != null && ImageFiles.Any())
		{
			long maxFileSize = 1024 * 1024 * 15;

			// 指定圖片儲存路徑
			var folder = $@"{Env!.WebRootPath}\images";
			foreach (var file in ImageFiles)
			{
				// 使用 Stream 將檔案儲存到指定路徑
				await using var stream = file.OpenReadStream(maxFileSize);

				// 如果資料夾不存在先建立
				Directory.CreateDirectory(folder);

				var path = $@"{Env.WebRootPath}\images\{file.Name}";

				// 建立檔案
				var fs = File.Create(path);

				// 將圖片 Stream 複製到檔案中
				await stream.CopyToAsync(fs);

				// Stream 用完記得關閉
				stream.Close();
				fs.Close();
			}
		}
	}
}

為了方便,NavMenu.razor 加上路由可跳轉這個 Component

<div class="nav-item px-3">
  <NavLink class="nav-link" href="FileUpload" Match="NavLinkMatch.All">
    <span class="bi bi-card-image h4 p-2 mb-0" aria-hidden="true"></span> File
    Upload
  </NavLink>
</div>

建立新的 ViewModelSweetConfirm 可以通用

namespace BlazorServer.ViewModels;

public class SweetConfirmViewModel
{
	public string? ResponseTitle { get; set; }

	public string? ResponseText { get; set; }

	public string? RequestTitle { get; set; }

	public string? RequestText { get; set; }
}

再把 _Layout.cshtmlSweetConfirm 修改一下

function SweetConfirm(jsonString) {
    // 這裡要 parse 才能正常傳回來
    var arg = JSON.parse(jsonString);
    return new Promise((resolve) => {
        swal({
            title: arg.RequestTitle,
            text: arg.RequestText,
            icon: "warning",
            buttons: ["取消", "確定"],
            dangerMode: true
        }).then((willDelete) => {
            resolve(willDelete);
            if (willDelete) {
                swal(arg.ResponseTitle, arg.ResponseText, "success");
            }
        });

    });
}

既然這邊改了,PostBase.razor.csDeletePost 也要修改

protected async Task DeletePost()
{
    // 改成 ViewModel
    var sweetConfirm = new SweetConfirmViewModel
    {
        RequestTitle = $"是否確定刪除日誌 {Post!.Title}?",
        RequestText = "這個操作不可恢復",
        ResponseTitle = "刪除成功",
        ResponseText = "日誌被刪除了"
    };
    var jsonString = JsonSerializer.Serialize(sweetConfirm);
    var result = await _jsClass.Confirm(jsonString);
    if (result)
    {
        var deleted = await PostRepository!.DeletePost(Post.Id);
        if (deleted.IsSuccess)
            await GetPostId.InvokeAsync(Post!.Id);
        else
            await _jsClass.Alert(deleted.Message!);
    }
}

JsInteropClasses.csConfirm() 改成 JSON 字串

public async ValueTask<bool> Confirm(string jsonString)
{
    var confirm = await _js.InvokeAsync<object?>("SweetConfirm", jsonString);
    if (confirm == null)
        return false;
    return bool.TryParse(confirm.ToString(), out var result) && result;
}

可以看到圖片上傳成功了

引用:

  1. ASP.NET Core Blazor file uploads
  2. Upload Files Using InputFile Component In Blazor
  3. What scope does a using statement have without curly braces
  4. BrowserFileExtensions.RequestImageFileAsync(IBrowserFile, String, Int32, Int32) 方法
  5. Day 26:Blazor WebAssembly 上傳檔案

注意:本文程式碼透過 .NET 6 + Visual Studio 2022 重構,可點擊原文連結與重構後程式碼比較學習,謝謝閱讀,支持原作者

繼續探索

延伸閱讀

更多文章
同分類 / 同標籤 2021/12/25

(29/30)大家一起學Blazor:Blazor單元測試

開發一個系統最無聊的過程大概就是解決 Bug 了,尤其是那種嘗試對 null 物件取值的錯誤(`Object reference not set to an instance of an object.`),這應該是大部分人剛踏入程式設計領域最常碰到的問題,為了從枯燥的解決 Bug 過程解脫,這篇就來介紹`單元測試`。

繼續閱讀
同分類 / 同標籤 2021/12/25

(28/30)大家一起學Blazor:Policy-based authorization

之前有說到`ASP.NET Core Identity` 使用的是基於`Claim` 的驗證,其實`ASP.NET Core Identity` 有不同類型的授權方式,最簡單的`登入授權`、`角色授權`、`Claim 授權`,但上述幾種都是以一種方式實現:原則授權(`Policy-based authorization`)。

繼續閱讀