WebSurfer's Home

Filter by APML

Blazor Web App で HttpClient を使ってファイルアップロード

by WebSurfer 2. January 2025 14:42

Blazor Web App から HttpCLient を使ってファイルアップロードする方法を書きます。ベースとしたのは Visual Studio 2022 のテンプレートを使って、ターゲットフレームワークを .NET 9.0 に、 [Interactive render mode] を [WebAssembly] に、 [Interactive location] を [Per page/component] に設定して作成したプロジェクトです。設定が異なる場合は下の説明の中には当てはまらない事がありますので注意してください。

Blazor Web App で HttpClient を使ってファイルアップロード

JavaScript の fetch を使ってアップロードする例は先の記事「Blazor Web App でファイルアップロード」に書きました。それに比べて、HttpClient を使う場合は、Program.cs での HttpClient の登録が必要なこと、本来ブラウザでは使えない HttpClient を使うことの違和感などが気になりました。それでも Blazor では JavaScript より HttpClient を使うのが本筋のようです。

この記事の例では、ファイルのアップロード先は外部 Web API または本 Blazor アプリのプロジェクト内に実装した Web API としました。その Web API のアクションメソッドに Blazor Web App の Razor コンポーネントから HttpClient を使ってファイルを送信します。(実際にはブラウザから送信されますので、HttpClient から生成された WebAssembly が送信しているのだろうと思いますが、具体的にどうなっているのかは分かりません)

Visual Studio でプロジェクトを作成すると、下のソリューションエクスプローラーの画像の構成となります。青枠と赤枠の中のフォルダ / ファイルはこの記事を書く際に追加したものです。青枠がプロジェクト内に作成した Web API とファイル保存用のフォルダ、赤枠がファイルをアップロードする Razor コンポーネントです。

プロジェクトの構成

この記事の例は、Microsoft のドキュメント「クライアント側のレンダリング (CSR) を使用するサーバーへのファイルのアップロード」に該当します。それを読んですべて分かれば良いのですが、自分は読んでも分からないことがありました。

なので、この記事に、Microsoft のドキュメントを読んでも分からなかったことを調べて書くとともに、実装を基本的な部分のみに簡略化したコードを備忘録として載せることにしました。

(1) InputFile クラスの使用

html の <input type="file"> を使った場合は、ユーザーが選択したファイルのデータを HttpClient の C# のコードに渡すことができません (裏ワザとかがあるかもしれませんが)。

InputFile クラスを使用すると、ユーザーがファイルの選択を完了したときに発生する OnChange イベントのハンドラの引数に InputFileChangeEventArgs オブジェクト が渡され、ユーザーが選択したファイルのデータを HttpClient の C# のコードで取得できます。

それゆえ、アップロードするのに HttpClient を使う場合は、InputFile クラスを使用するほか選択肢はなさそうです。

(2) Razor コンポーネントの配置場所

上のソリューションエクスプローラーの画像に示すように、メインプロジェクトとクライアントプロジェクト (.Client) の 2 つのプロジェクトが作られます。そのどちらにも Razor コンポーネントを配置できるのですが、今回のファイルをアップロードする Razor コンポーネントはどちらに配置すればいいのでしょうか?

答えはクライアントプロジェクト (.Client) 側です。上のソリューションエクスプローラーの画像の赤枠で示した FileUpload.razor がそれです。理由は以下の通りです。

Microsoft のドキュメント「ASP.NET Core Blazor プロジェクトの構造」に "対話型 WebAssembly または対話型自動レンダリング モードを使用するコンポーネントは、.Client プロジェクトに配置する必要があります" と書いてあります。

「対話型」の対話というのはユーザーとの対話を意味します。例えばテンプレートで自動生成されるコンポーネント Counter.razor のようにユーザーのクリックに応じてカウント値が変わっていくものが該当するようです。InputFile クラスの OnChange イベントで処理を行うのも対話に含まれるようです。

実は、最初 FileUpload.razor はメインプロジェクトに配置していたのですが、OnChange のハンドラが動かず、その解決に半日ほどハマったのは内緒です。(笑)

(3) Program.cs で HttpClient サービスの追加

HttpClient サービスの追加をメインプロジェクとクライアントプロジェクト (.Client) 両方の Program.cs で行う必要があります。

上に紹介した Microsoft のドキュメントに書いてある通り、メインプロジェクトの Program.cs に、

// IHttpClientFactory および関連するサービスを追加
builder.Services.AddHttpClient();

クライアントプロジェクト (.Client) の Program.cs に、

// Web API に対する POST 要求のための HttpClient の登録
builder.Services.AddScoped(sp =>
    new HttpClient 
    { 
        BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
    });

・・・を追加します。(注: 後者のコードは「Blazor WebAssembly スタンドアロン アプリ」のテンプレートを使って作成した Blazor WebAssembly プロジェクトの Prpgram.cs には最初から含まれています)

でも、考えてみると HttpClient を使うのはクライアントプロジェクト (.Client) の FileUpload.razor なのに、なぜメインプロジェクトの Program.cs でも追加しなければならないのかが腑に落ちません。

Microsoft のドキュメントには "クライアント側コンポーネントはサーバーでプリレンダリングされるため、HttpClient サービスをメインプロジェクトに追加する必要があります。対話型コンポーネントではプリレンダリングが既定で有効になっています" と書いてありました。そう言われても、その仕組みは全く理解できていませんが。(汗)

上のクライアントプロジェクト (.Client) の Program.cs での設定例では、HttpClient の BaseAddres プロパティを設定していますがその理由を書きます。

builder.HostEnvironment.BaseAddress というのは Blazor アプリのベースアドレスで、この記事の例では https://localhost:44340/ となります。

Microsoft のドキュメント「HttpClient.BaseAddress プロパティ」に "相対 URI を使用して HttpRequestMessage を送信すると、メッセージ Uri が BaseAddress プロパティに追加され、絶対 URI が作成されます" と書かれています。

なので、Blazor アプリ内の Web API (上のソリューションエクスプローラーの画像の青枠がそれ) にファイルを送信する場合は、上のコードのように BaseAddress プロパティを設定しておくと、FileUpload.razor の HttpClient のコードで URI を指定する際、相対 URI を使うことができるようになります。

外部 Web API にファイルを送信する場合は、FileUpload.razor の HttpClient のコードで絶対 URI を指定しますが、その場合はそれが HttpClient の BaseAddress に追加されるということはなく、コードで指定した絶対 URI がそのまま使用されます (ドキュメントにはそのことは書いてないですが検証して確認)。

HttpClient のコードで常に絶対 URI を指定するのであれば上のような HttpClient の BaseAddress の設定は不要で、以下のようにすれば良いです。

// FileUpload.razor の Http.PostAsync(url, content) の url に
// http から始まる絶対 URI を指定するのであれば以下で良い
builder.Services.AddScoped(sp => new HttpClient());

(4) FileUpload.razor

ユーザーがファイルを選択してアップロードする razor コンポーネント (FileUpload.razor) のサンプルコードは以下の通りです。Blazor Web App はデフォルトでは static rendering になるそうで、@rendermode InteractiveWebAssembly を設定しないと対話型にならないので注意してください。

@page "/fileupload"
@rendermode InteractiveWebAssembly
@using System.Net.Http.Headers
@inject HttpClient Http

<h3>Upload File</h3>

<InputFile OnChange="OnInputFileChange" multiple />
<br />
<p>@result</p>

@code {
    // 外部 Web API の URI
    // private string url = "https://localhost:44366/FileUpDownload/multiple";

    // Blazor アプリのプロジェクト内に作成した Web API の URI
    // Program.cs での HttpClient の登録で BaseAddress に本 Blazor 
    // アプリのベースアドレスを設定してあるので相対 URI を使用可
    private string url = "/FileSave";

    private string result = string.Empty;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        using var content = new MultipartFormDataContent();

        // GetMultipleFiles メソッドの引数 maximumFileCount は
        // デフォルトで 10。超えると例外がスローされる
        foreach (var file in e.GetMultipleFiles())
        {
            // OpenReadStream メソッドの引数 maxAllowedSize は
            // デフォルトで 500KB。超えると例外がスローされる
            var streamContent = new StreamContent(file.OpenReadStream());
            streamContent.Headers.ContentDisposition =
                new ContentDispositionHeaderValue("form-data")
                    {
                        // Name は Web API の引数名に合わせる
                        Name = "postedfiles",  
                        FileName = Path.GetFileName(file.Name)
                    };
            streamContent.Headers.ContentType =
                new MediaTypeHeaderValue(file.ContentType);

            content.Add(streamContent);
        }

        HttpResponseMessage response = await Http.PostAsync(url, content);

        // Web API からの応答のコンテンツの文字列を取得
        result = await response.Content.ReadAsStringAsync();
    }
}

上のコードでは、ユーザーがファイルの選択を完了すると、ファイルは即 Web API に送信されます。

それを、ユーザーがファイルを選択した後でボタンクリックによりアップロードしたい場合は、OnInputFileChange メソッドでは e.GetMultipleFiles() メソッドで IReadOnlyList<IBrowserFile> オブジェクトを取得してそれを保持するに留め、ボタンクリックのハンドラで保持された IReadOnlyList<IBrowserFile> オブジェクトからファイルデータを取得して送信するのが良さそうです。

(5) Web API

FileUpload.razor から送信されたファイルを受け取って UploadedFiles フォルダに保存する Web API のサンプルコードを下に載せておきます。

この記事のコードでは Microsoft の記事「ASP.NET Core でファイルをアップロードする」に書かれたセキュリティに関する配慮はされていませんので注意してください。例えば「アプリと同じディレクトリツリーに、アップロードしたファイルを保持しないでください」とありますが、アプリケーションルート直下の UploadedFiles というフォルダに、アップロードされたファイルをチェックせず、ユーザーによって指定されたファイル名でそのまま保存するようになっています。

using Microsoft.AspNetCore.Mvc;

namespace BlazorWebAppWASM.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class FileSaveController : ControllerBase
    {
        // 物理パスの取得用
        private readonly IWebHostEnvironment _hostingEnvironment;

        public FileSaveController(IWebHostEnvironment hostingEnvironment)
        {
            this._hostingEnvironment = hostingEnvironment;
        }

        [HttpPost]
        public async Task<IActionResult> MuilipleFiles(List<IFormFile>? postedFiles)
        {
            string result = "Uploaded files: ";
            if (postedFiles != null)
            {
                // アプリケーションルートの物理パスを取得
                // wwwroot の物理パスは WebRootPath プロパティを使う
                string contentRootPath = _hostingEnvironment.ContentRootPath;

                foreach (var postedFile in postedFiles)
                {
                    if (postedFile != null && postedFile.Length > 0)
                    {
                        // アップロードされたファイル名を取得
                        string filename = Path.GetFileName(postedFile.FileName);

                        // アプリケーションルート直下の UploadedFiles フォルダに書き込み
                        string filePath = $"{contentRootPath}\\UploadedFiles\\{filename}";
                        using (var stream = new FileStream(filePath, FileMode.Create))
                        {
                            await postedFile.CopyToAsync(stream);
                        }

                        result += $"{filename} ";
                    }
                }
            }
            else
            {
                result = "postedFiles is null";
            }

            return Content(result);
        }
    }
}

Blazor アプリのプロジェクト内に実装する場合は、上のソリューションエクスプローラーの画像の青枠に示したようにメインプロジェクトに配置します。さらに、メインプロジェクトの Program.cs に下の 2 行を追加します。

// Controller を使用できるようサービスを追加
builder.Services.AddControllers();

// ・・・中略・・・

// 属性でルーティングされたコントローラーをマップ
app.MapControllers();

外部 Web API に上と同様なコードを実装して FileUpload.razor から送信されたファイルを受け取ることもできます。ただし、その場合はクロスドメインになるので、Web API 側のサーバーが CORS 対応している必要があります。

HttpClient を使っていると言っても、結局それは WebAssembly に変換されてブラウザに送信され、ブラウザから Web API に要求が出るので、クロスドメインでの要求は、JavaScript の fetch を使った場合と同様に、CORS 対応がされてないと失敗します。

外部 Web API のサーバーで CORS 対応がされていれば、下の Fiddler でのキャプチャ画像のように #95 でプリフライトリクエストが出て、#96 でファイルが送信されます。

Fiddler でのキャプチャ画像

Tags: , , , ,

Upload Download

Blazor Web App で JavaScript を使ってファイルアップロード

by WebSurfer 5. November 2024 14:45

Visal Studio 2022 の Blazor プロジェクト作成用テンプレートで Blazor Web App を選び、[Interactive render mode] を [Server] に設定して作成した Blazor アプリから、JavaScript を使ってファイルをアップロードする方法を書きます。

テンプレートで Blazor Web App を選択

基本的には先の記事「Blazor WASM でファイルアップロード」とほぼ同じです。違いは、WASM ではなく Web App であること、ファイルを選択する画面とアップロードする画面を分けたこと、ファイルをアップロードするスクリプトは jQuery ajax に替えて fetch を使ったこと、複数のファイルを一度にアップロードできるようにしたことです。

ファイルを選択する画面とアップロードする画面を分ける必要はないのですが、訳あって、画面遷移しても <input type="file" /> から取得できる FileList オブジェクトを画面間で受け渡すことができるかを調べるため分けました。

正確に言うと「受け渡す」のではなくて、Blazor アプリは基本 SPA なので、画面が遷移しても書き変わらない共通部分に情報を保持しておいて、異なる画面で共通に使うということです。

作成したアプリを動かして、Select File 画面でファイルを選択し、

Select File 画面

その後 Upload File 画面に移って[Upload]ボタンをクリックすると、別プロジェクトとして作成した ASP.NET Core Web API にファイルをアップロードするようになっています。

Upload File 画面

下に載せたコードで、Select File 画面でファイルを選択して FileList オブジェクト作成し、それを共通部分にある JavaScript の変数に代入し、Upload File 画面に移ってその変数から FileList オブジェクトを取得して FormData オブジェクトを作成し、ファイルをアップロードすることはできました。

ただし、Select File 画面の <input type="file" /> の DOM は、Upload File 画面ではなくなってしまうので、時間が経つとブラウザのガベージコレクタで FileList オブジェクトもなくなってしまう可能性は否定できません。

検証に使ったブラウザは、Windows 10 64-bit の Chrome 130.0.6723.117、Microsoft Edge 130.0.2849.68、Firefox 132.0.1、Opera 114.0.5282.115 です。


以下にどのようにアプリを作成したかを書きます。

まず、一番上の画像に示したように Visual Studio 2022 のテンプレートを使って Blazor Web App のプロジェクトを作成します。この記事のサンプルでは Interactive render mode は Server に、Interactive location は Per Page/component に設定しました。(Interactive render mode は Auto でも WebAssembly でも OK です)

Razor Component の追加

Select File 画面と File Upload 画面の Razor Component を追加します。コードはそれぞれ以下の通りです。Blazor Web App はデフォルトでは static rendering になるので、下のコード例のように rendermode を指定しないと interactive にならず、スクリプトを Invoke できないことに注意してください。

Select File 画面用

@page "/selectfile"
@rendermode InteractiveServer
@inject IJSRuntime JSRuntime;

<h3>Select File</h3>

<input type="file" name="fileupload" id="fileupload" 
    multiple="multiple" @onchange="Select" />

<div id="result"></div>

@code {
    private async Task Select()
    {
        await JSRuntime.InvokeVoidAsync("select");
    }
}

Upload File 画面用

@page "/uploadfile"
@rendermode InteractiveServer
@inject IJSRuntime JSRuntime;

<h3>Upload File</h3>

<button type="button" class="btn btn-primary" @onclick="Upload">
    Upload
</button>

<div id="result"></div>

@code {
    private async Task Upload()
    {
        await JSRuntime.InvokeVoidAsync("upload");
    }
}

ファイル選択とアップロード用 JavaScript

上の Select File 画面と Upload File 画面でファイル選択とアップロードを行う JavaScript ファイルを作成し、wwwroot フォルダに配置します。名前は任意ですがこの記事では exampleJsInterop.js としています。

window.select = () => {
    let fileUpload = document.getElementById("fileupload");
    fileList = fileUpload.files;

    let str = "<ul>";
    for (let i = 0; i < fileList.length; i++) {
        str += `<li>${fileList[i].name}, ${fileList[i].type}, ${fileList[i].size}</li>`
    }
    str += "</ul>";

    let result = document.getElementById("result");
    result.innerHTML = str;
}

window.upload = async () => {   
    if (fileList) {
        let resultDiv = document.getElementById("result");

        let fd = new FormData();
        for (let i = 0; i < fileList.length; i++) {
            fd.append("postedfiles", fileList[i])
        }

        const param = {
            method: "POST",
            body: fd
        }
        const response = await fetch("送信先 Web API の url", param);
        if (response.ok) {
            const message = await response.text();
            resultDiv.innerText = message;
        } else {
            resultDiv.innerText = "アップロード失敗";
        }
    }
}

App.razor にコードを追加

上の Select File 画面の JavaScript で <input type="file" /> から取得した FileList オブジェクトへの参照を保持するスクリプト fileList = null; と、wwwroot に配置した外部スクリプトファイル exampleJsInterop.js をダウンロードできるよう、下の画像の赤枠で囲ったコードを追加します。

App.razor にコードを追加

Blazor は基本 Single Page Application なので、追加した部分は遷移で書き換えられることはなく、すべての画面で共有されます。

ファイル受信用 Web API

上の JavaScript により送信されたファイルファイルを受信するため、別プロジェクトとして CORS 対応した Web API を作成しています。そのアクションメソッドのコードを下に載せておきます。

using Microsoft.AspNetCore.Mvc;

namespace WebApi.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class FileUpDownloadController : ControllerBase
    {
        // 物理パスの取得用
        private readonly IWebHostEnvironment _hostingEnvironment;

        public FileUpDownloadController(IWebHostEnvironment hostingEnvironment)
        {
            this._hostingEnvironment = hostingEnvironment;
        }

        [HttpPost("multiple")]
        public async Task<IActionResult> MuilipleFiles(List<IFormFile>? postedFiles)
        {
            string result = "アップロードされたファイル: ";
            if (postedFiles != null)
            {
                // アプリケーションルートの物理パスを取得
                // wwwroot の物理パスは WebRootPath プロパティを使う
                string contentRootPath = _hostingEnvironment.ContentRootPath;

                foreach (var postedFile in postedFiles)
                {
                    if (postedFile != null && postedFile.Length > 0)
                    {
                        // アップロードされたファイル名を取得
                        string filename = Path.GetFileName(postedFile.FileName);

                        // アプリケーションルート直下の UploadedFiles フォルダに書き込み
                        string filePath = $"{contentRootPath}\\UploadedFiles\\{filename}";
                        using (var stream = new FileStream(filePath, FileMode.Create))
                        {
                            await postedFile.CopyToAsync(stream);
                        }

                        result += $"{filename} ";
                    }
                }
            }
            else
            {
                result = "ファイルアップロードに失敗しました";
            }

            return Content(result);
        }
    }
}

Tags: , , ,

Upload Download

Blazor WASM で jQuery ajax を使ってファイルアップロード

by WebSurfer 28. October 2022 14:12

Blazor Web Assembly (WASM) アプリで、html の form 要素及び input type="file" 要素と jQuery ajax を使ってファイルをアップロードする方法を書きます。

Blazor WASM でファイルアップロード

(.NET 5.0 以降であれば、InputFile クラスを利用できるそうですので、そちらを使う方がいいかもしれません。詳細は Microsoft のドキュメント「ASP.NET Core Blazor ファイルのアップロード」を見てください)

jQuery / JavaScript を使ってファイルのアップロードはできますので、基本ブラウザである Blazor WASM のクライアント側からでも同様にファイルのアップロードはできます。

その jQuery / JavaScript のコードを Blazor WASM のクライアント側に組み込んで呼び出すことで、Blazor WASM アプリとしてファイルのアップロード機能を持たせることが可能です。(以下に概略を書きますが、詳細は Microsoft のドキュメント「ASP.NET Core Blazor で .NET メソッドから JavaScript 関数を呼び出す」を見てください)

なお、input type="submit" ボタンを form 要素内に配置して、それをクリックするというのは NG です。ファイルはアップロードされますが、返ってきた応答でブラウザの画面が書き換えられてしまうので。

jQuery ajax を使ってのファイルのアップロード機能をどのように実装するかの具体例を以下に書いておきます。

ベースとする Blazor WASM ソリューションは、Visual Studio 2022 のテンプレートを使って、フレームワーク .NET 6.0 で、[ASP.NET Core でホストされた(h)]にチェックを入れて作成したものを使います。ソリューション内に Client, Server, Shared という 3 つのプロジェクトができるはずです。

(1) ファイルアップロード用の Razor ページ

Client プロジェクトの Pages フォルダに以下の Razor ページを追加します。

@page "/fileupload"
@inject IJSRuntime JSRuntime;

<h3>File Upload by jQuery Ajax</h3>

<form id="form1" method="post" enctype="multipart/form-data">
    <input type="file" name="postedfile" />
</form>

<button type="button" class="btn btn-primary" @onclick="UploadFile">
    アップロード
</button>

<div id="result"></div>

@code {
    private async Task UploadFile()
    {
        await JSRuntime.InvokeVoidAsync("uploadFile");
    }
}

Shared フォルダの NavMenu.razor に上の fileupload ページへのリンクを追加し、メニューからリンクをクリックしてアクセスできるようにします。

(2) ファイルアップロード用の js ファイル

ファイルをアップロードする jQuery / JavaScript コードのコードを含んだ js ファイルを作成します。コードは以下の通りです。js ファイルの名前は任意ですがここでは exampleJsInterop.js としておきます。

window.uploadFile = () => {
    // FormData オブジェクトの利用
    var fd = new FormData(document.getElementById("form1"));

    $.ajax({
        url: '/Upload',
        method: 'post',
        data: fd,
        processData: false, // jQuery にデータを処理させない
        contentType: false  // contentType を設定させない
    }).done(function (response) {
        $("#result").empty;
        $("#result").text(response);
    }).fail(function (jqXHR, textStatus, errorThrown) {
        $("#result").empty;
        $("#result").text('textStatus: ' + textStatus +
            ', errorThrown: ' + errorThrown);
    });
}

(3) js ファイルの配置

上の exampleJsInterop.js と jQuery.js を、Client プロジェクトの wwwroot フォルダ下に js フォルダを作って、その中に配置します。

js ファイルの配置

(4) index.html に script タグを追加

Client プロジェクトの wwwroot フォルダ下の index.html に、exampleJsInterop.js と jQuery.js がダウンロードされるよう、script タグを追加します。

Index.html に script タグを追加

(5) Server プロジェクトに Controller 追加

Server プロジェクトの Controllers フォルダにアップロードされたファイルを受け取って必要な処置をする Controller を追加します。

具体例は以下のコードを見てください。Web API の Controller としています。名前をステップ (2) の jQuery / JavaScript のコードにある url: '/Upload' と合わせる必要があることに注意してください。

using Microsoft.AspNetCore.Mvc;

namespace BlazorWasmCoreHosted.Server.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class UploadController : ControllerBase
    {
        // .NET 6.0 で作ったプロジェクトは「Null 許容」オプションが「有効化」に
        // 設定してあるので、引数も NULL 許容にしておかないと、クライアントでファ
        // イルを選択しないで送信した場合、HTTP 400 Bad Request エラーになって
        //  "The postedFile field is required."というエラーメッセージが返ってくる
        [HttpPost]
        public string Post([FromForm] IFormFile? postedFile)
        {
            string result = "";
            if (postedFile != null && postedFile.Length > 0)
            {
                // アップロードされたファイル名を取得
                string filename = System.IO.Path.GetFileName(postedFile.FileName);

                // アップロードされたファイルを処理(省略)

                result = $"{filename} ({postedFile.ContentType}) - " +
                    $"{postedFile.Length} bytes アップロード完了";
            }
            else
            {
                result = "ファイルアップロードに失敗しました";
            }

            return result;
        }
    }
}

アプリを実行してファイルアップロードに成功するとこの記事の一番上にある画像のようになります。

Tags: , , ,

Upload Download

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  January 2025  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
272829303112
3456789

View posts in large calendar