WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

Web API にファイルアップロード

by WebSurfer 9. March 2023 10:23

.NET Framework 版の ASP.NET Web API はファイルアップロードの受信に対応してません。正確に言うと multipart/form-data 形式で送信されてきたデータのフォーマッターが実装されていません。

Web API にファイルアップロード

なので、アクションメソッドの引数を Post(HttpPostedFileBase file) というようして、それに multipart/form-data 形式でファイルを送信すると UnsupportedMediaTypeException がスローされ、

"メディアの型 'multipart/form-data' のコンテンツから型 'HttpPostedFileBase' のオブジェクトを読み取るために使用可能な MediaTypeFormatter がありません。"

・・・というエラーになります。

その対処方法を書きます。基本的には Microsoft のドキュメント「ASP.NET Web APIでの HTML フォーム データの送信: ファイルのアップロードとマルチパート MIME」に書いてあった方法です。

例えば以下のような View で HTML5 fetch API を使ってファイルをアップロードするとします。

<form id="form1" method="post" enctype="multipart/form-data">
    <input name="caption" type="text" value="Summer Vacation" />

    <input type="file" name="file" />
</form>

<button type="button" id="button1" class="btn btn-primary">
    アップロード
</button>

<div id="result1"></div>

@section Scripts {
    <script type="text/javascript">
        let resultDiv, uploadButton;

        window.addEventListener('DOMContentLoaded', () => {
            uploadButton = document.getElementById("button1");
            resultDiv = document.getElementById("result1");
            uploadButton.addEventListener('click', uploadFile);
        });

        const uploadFile = async () => {
            let fd = new FormData(document.getElementById("form1"));
            const param = {
                method: "POST",
                body: fd
            }
            const response = await fetch("/api/FileUpload", param);
            if (response.ok) {
                const message = await response.text();
                resultDiv.innerText = message;
            } else {
                resultDiv.innerText = "アップロード失敗";
            }
        };
    </script>
}

送信されるデータは以下のように multipart/form-data 形式になります。

------WebKitFormBoundaryoasm5HIwy0wijTSo
Content-Disposition: form-data; name="caption"

Summer Vacation
------WebKitFormBoundaryoasm5HIwy0wijTSo
Content-Disposition: form-data; name="file"; filename="0305Result.jpg"
Content-Type: image/jpeg

それを .NET Framework 版の ASP.NET Web API のアクションメソッドで取得してサーバーのフォルダにファイルとして書き込むコードを書いてみました。以下の通りです。

説明はコメントに書きましたのでそれを見てください。手抜きでスミマセン。

using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Web;
using System.Web.Http;
using System.Threading.Tasks;
using System.Collections.Specialized;
using System.Text;
using System.Net.Http.Headers;

namespace WebAPI.Controllers
{
    public class FileUploadController : ApiController
    {
        // アクションメソッドは引数無しにする。フォーマッタを呼び出さ
        // ずアクションメソッド内で要求本文を処理するため
        public async Task<HttpResponseMessage> Post()
        {
            // 要求にマルチパート MIME メッセージが含まれているか確認
            if (!Request.Content.IsMimeMultipartContent())
            {
                throw new HttpResponseException(
                               HttpStatusCode.UnsupportedMediaType);
            }

            // アプリケーションルート直下に UploadedFiles というフォル
            // ダを設け、そこにアップロードされてきたファイルを書き込
            // む。以下のコードでそのフォルダの物理パスを取得
            string root = HttpContext.Current.Server
                                     .MapPath("~/UploadedFiles");

            // アップロードされたファイルにファイルストリームを割り当て
            var provider = new MultipartFormDataStreamProvider(root);

            // マルチパート MIME メッセージのすべてのファイル部分を
            // 抽出し、上の root で指定されたフォルダに書き込む。
            // ファイル名は ReadAsMultipartAsync が自動的に一意に
            // なるように生成する
            await Request.Content.ReadAsMultipartAsync(provider);

            // ReadAsMultipartAsync メソッドが完了すると、FileData
            // プロパティで MultipartFileData オブジェクトのコレ
            // クションを取得できる。MultipartFileData オブジェクト
            // からファイルに関する情報を取得できる
            foreach (MultipartFileData file in provider.FileData)
            {
                // ReadAsMultipartAsync メソッドが自動的に生成した
                // ファイル名をフルパスで取得。UploadedFiles フォル
                // ダにはその名前でアップロードされてきたファイルが
                // 書き込まれている
                string source = file.LocalFileName;

                // 要求ヘッダに含まれるファイル名を取得。取得した
                // 文字列は何故か " で囲われるのでそれを除去
                var name = file.Headers.ContentDisposition
                                       .FileName.Replace("\"", "");
                string dist = root + "\\" + name;

                // ReadAsMultipartAsync メソッドが生成したファイル
                // 名を要求ヘッダに含まれるファイル名に書き換える。
                // 同名のファイルが存在する場合は先に削除する
                File.Delete(dist);
                File.Move(source, dist);
            }

            // <input name="caption" type="text" /> で送信されて
            // きたデータは以下のようにして取得できる
            NameValueCollection col = provider.FormData;
            foreach (string s in col.AllKeys)
            {
                Trace.WriteLine($"Key: {s}, Value: {col[s]}");
            }

            string result = "アップロード完了";

            // .NET Framework 版の Web API は JSON を返すのが基本
            // らしく、アクションメソッドの戻り値の型を string にし
            // て return result; とすると送信される文字列は " で囲
            // われてしまうので以下のようにする
            return new HttpResponseMessage
            {
                Content = new StringContent(result,
                                            Encoding.UTF8,
                                            "text/plain")
            };
        }
    }
}

何か見落としがあるような気もしますが、自分が検証した限りでは期待通りに動きました。

ちなみに、ASP.NET MVC5 および .NET 6.0 で作った ASP.NET Core Web API では multipart/form-data 形式に対応するフォーマッターが組み込まれているので、上記のようなことをする必要はありません。そちらの方向に進んだ方が正解かもしれません。

Tags: ,

Web API

img 要素の画像データを取得してアップロード

by WebSurfer 1. February 2023 14:30

下の画像のように html の img 要素の src 属性に url が指定されてブラウザ上に表示されている状態で、その画像データを JavaScript で取得し、さらにサーバーにアップロードする方法を書きます。

img 要素の画像をアップロード

img 要素から直接画像デ���タを JavaScript で取得する方法は無さそうです。少なくとも自分は見つけられませんでした。なので、img 要素の画像を一旦 canvas に描画し、canvas から画像データを取得するようにしました。

なお、この記事の方法は src 属性の url が同一オリジンになっている場合に限りますので注意してください。

クロスドメインになっている場合は、canvas から画像データを取得する toDataURL メソッドまたは toBlob メソッドで DOMException がスローされます。エラーメッセージは、例えば Chrome で toDataURL メソッドを使った場合、以下のようになります。

DOMException: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.

以下にコード例を載せます。ASP.NET Core MVC の View のものですが、html とJavaScript のコードはそのまま使えるはずです。

コードのコメントに書いた「その1」は toDataURL メソッドを使って画像データを DataURL 形式の文字列として取得し JSON 文字列として送信するものです。データは BASE64 でエンコードされるのでバイナリ形式よりサイズが約 1.3 倍大きくなることに注意してください。

「その2」は toBlob メソッドを使って画像データをバイナリ形式で取得し multipart/form-data 形式で送信するものです。toBlob メソッドは非同期で動き、バイナリデータは引数のコールバックに渡されることに注意してください。

@{
    ViewData["Title"] = "UploadImage";
}

<h1>UploadImage</h1>

<input id="button1" type="button" value="Upload DataUrl" />
<input id="button2" type="button" value="Upload Blob" />
<br />
<img id="image1" src="/images/911GT2_2.jpg" alt="" />
<div id="result"></div>

@section Scripts {
    <script type="text/javascript">
        //<![CDATA[
        let myImage, uploadDataUrl, uploadBlob, myCanvas, resultDiv;

        // DOMContentLoaded イベントのリスナ設定
        window.addEventListener('DOMContentLoaded', () => {
            uploadDataUrl = document.getElementById("button1");
            uploadBlob = document.getElementById("button2");
            myImage = document.getElementById("image1");
            resultDiv = document.getElementById("result");
            myCanvas = document.createElement("canvas");

            uploadDataUrl.addEventListener('click', uploadDataUrlImage);
            uploadBlob.addEventListener('click', uploadBlobImage);
        });

        // その1
        // 上の img 要素の画像を canvas に描画、canvas の画像データ
        // を DataURL 形式の文字列として取得し JSON 文字列を組み立
        // て、それを fetch API を使ってサーバーに送信。
        const uploadDataUrlImage = async () => {
            const context = myCanvas.getContext('2d');
            myCanvas.setAttribute('width', myImage.width);
            myCanvas.setAttribute('height', myImage.height);
            context.drawImage(myImage, 0, 0);
            const dataUrl = myCanvas.toDataURL("image/jpeg");
            const params = {
                method: "POST",
                body: '{"imgBase64":"' + dataUrl + '"}',
                headers: { 'Content-Type': 'application/json' }
            }
            const response = await fetch("/api/Canvas", params);
            if (response.ok) {
                const message = await response.text();
                resultDiv.innerText = message;
            } else {
                resultDiv.innerText = "アップロード失敗";
            }
        }

        // その2
        // img 要素の画像を canvas に描画、canvas の画像データを 
        // Blob 形式で取得し、それを fetch API を使ってサーバー
        // に送信。
        const uploadBlobImage = async () => {
            const context = myCanvas.getContext('2d');
            myCanvas.setAttribute('width', myImage.width);
            myCanvas.setAttribute('height', myImage.height);
            context.drawImage(myImage, 0, 0);

            const data = await getBlobFromCanvas();

            const formData = new FormData();
            formData.append("postedFile", data);
            const param = {
                method: "POST",
                body: formData
            }
            const response = await fetch("/api/Upload", param);
            if (response.ok) {
                const message = await response.text();
                resultDiv.innerText = message;
            } else {
                resultDiv.innerText = "アップロード失敗";
            }
        }

        // toBlob メソッドは非同期で動き、結果の Blob データは引数
        // のコールバックに渡されるので、結果は以下のように Promise
        // にラップして返す
        const getBlobFromCanvas = () => {
            return new Promise((resolve) => {
                myCanvas.toBlob((blob) => resolve(blob), "image/jpeg");
            });
        };

        //]]>
    </script>
}

上のコードで送信された画像データを受け取るサーバー側のコードは、「その1」の Data Url 形式のデータを JSON 文字列で受け取る場合は先の記事「canvas の画像をアップロード (その 2)」の下の方に例がありますので見てください。

「その2」のコードでは以下の Fiddler でのキャプチャ画像のようにバイナリデータが multipart/form-data 形式で送信されます。

Blob を maultipart/form-data 形式で送信

サーバー側のコードはそれを受け取って処理できるように書く必要があります。ASP.NET Core Web API でのコード例を下に載せておきます。

using Microsoft.AspNetCore.Mvc;

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

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

        // .NET 6.0 で作ったプロジェクトはnull許容参照型がデフォルトで有効に
        // 設定してあるので、引数を NULL 許容にしておかないと、クライアントでファ
        // イルを選択しないで送信した場合、HTTP 400 Bad Request エラーになって
        //  "The postedFile field is required."というエラーメッセージが返ってくる
        [HttpPost]
        public async Task<string> Post([FromForm] IFormFile? postedFile)
        {
            string result = "";
            if (postedFile != null && postedFile.Length > 0)
            {
                // アップロードされたファイル名を取得
                string filename = System.IO.Path.GetFileName(postedFile.FileName);

                // アプリケーションルートの物理パスを取得
                string contentRootPath = _hostingEnvironment.ContentRootPath;

                string filePath = $"{contentRootPath}\\UploadedFiles\\" +
                    $"{filename}{DateTime.Now.ToString("yyyyMMddHHmmss")}.jpg";

                // フォルダ UploadedFile に画像ファイルを保存
                using (var stream = new FileStream(filePath, FileMode.Create))
                {
                    await postedFile.CopyToAsync(stream);
                }

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

            return result;
        }
    }
}

JavaScript はブラウザ依存なところがありますが、Edge 109.0.1518.70, Chrome 109.0.5414.120, Firefox 109.0, Opera 94.0.4606.76 で期待通り動くことは確認しました。

Tags: , , , ,

Upload Download

画像をアップロードして SQL Server に保存 (CORE)

by WebSurfer 24. November 2021 14:51

画像ファイルをタイトルと説明の文字列と共にアップロードし、サーバ側でサムネイル画像を作成してタイトル・説明・サムネイル画像・オリジナル画像を一式 SQL Server データベースに保存するサンプルを書きます。

一覧の表示

上の画像はアップロードして SQL Server に保存されたタイトル、説明、サムネイル画像を取得して一覧にして表示したものです。(オリジナル画像を表示してないのは一覧表に表示するのは大きすぎるからという理由だけです)

以下に、Entity Framework Code First の機能を使っての SQL Server データベースの作り方、一覧の表示、アップロード、編集、削除機能を実装した ASP.NET Core MVC アプリの作り方を述べます。

(保存先をデータベースではなく Web サーバーの特定のフォルダにファイルとして保存する場合は別の記事「ASP.NET Core MVC でファイルアップロード」に書きましたので、興味がありましたらそちらを見てください)

ベースに使ったプロジェクトは、先の記事「Visual Studio 2022 の ASP.NET MVC アプリ」に書いた Visual Studio 2022 で作成した .NET 6.0 の ASP.NET Core MVC アプリです。それに以下のように機能を追加します。

(1) Model

アップロードの際に Controller と View の間でデータのやり取りをするための View Model と、Entity Framework を使って SQL Server データベースとのデータのやり取りをするエンティティクラスを作ります。この記事では以下のようにしました (ファイル名とか MIME タイプ情報なども保持したいかもしれませんが、それはまた別の機会に)。

#nullable disable

using System.ComponentModel.DataAnnotations;

namespace MvcCore6App.Models
{
    // View Model
    // アップロードの際 Controller/View 間でデータのやり取りをする
    public class FileUploadViewModel
    {
        [Display(Name = "タイトル")]
        [Required(ErrorMessage = "{0} は必須")]
        [StringLength(25, ErrorMessage = "{0} は {1} 文字以内")]
        public string Title { get; set; }

        [Display(Name = "説明")]
        [StringLength(250, ErrorMessage = "{0} は {1} 文字以内")]
        public string Description { get; set; }

        [Display(Name = "ファイル")]
        [Required(ErrorMessage = "{0} は必須")]
        public IFormFile PostedFile { get; set; }
    }

    // エンティティクラス
    // SQL Server データベースとのデータのやり取りをする。また、
    // これをベースに EF Code First でデータベースを生成する
    public class FileEntity
    {
        public int Id { get; set; }

        [Display(Name = "タイトル")]
        [Required]
        [StringLength(25)]
        public string FileName { get; set; }

        [Display(Name = "説明")]
        [StringLength(250)]
        public string Description { get; set; }

        [Display(Name = "サムネイル画像")]
        [Required]
        public byte[] ThumbImage { get; set; }

        [Required]
        public byte[] OriginalImage { get; set; }
    }
}

Visual Studio 2022 で作成した .NET 6.0 の ASP.NET Core MVC アプリでは NULL 許容参照型がプロジェクト全体で有効化されていますので、警告を抑制するため #nullable disable を付与しています。下のコードでも必要に応じてそのようにしています。

(2) コンテキストクラス

Entity Framework を使って Controller と SQL Server データベースとの間でデータをやり取りするためのコンテキストクラスを定義します。この記事では以下のようにしました。

#nullable disable

using MvcCore6App.Models;
using Microsoft.EntityFrameworkCore;

namespace MvcCore6App.Data
{
    public class FileContext : DbContext
    {
        public FileContext(DbContextOptions<FileContext> options) : base(options)
        {
        }

        public DbSet<FileEntity> Files { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<FileEntity>().ToTable("File");
        }
    }

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

上記 (2) で定義したコンテクストクラスのインスタ��スを Controller のコンストラクタ経由で DI できるようにするため、Program.cs に AddDbContext メソッドを使ってサービスの追加を行います。以下のコードで「// これを追加」とコメントした部分です。

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using MvcCore6App.Data;
using MvcCore6App.Areas.Identity.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration
    .GetConnectionString("ApplicationDbContextConnection");

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));

builder.Services.AddDefaultIdentity<ApplicationUser>(options => 
    options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

// これを追加
builder.Services.AddDbContext<FileContext>(options =>
    options.UseSqlServer(connectionString));

// Add services to the container.
builder.Services.AddControllersWithViews();

// ・・・後略・・・

(4) SQL Server データベース作成

上に定義したエンティティクラスとコンテクストクラスをベースに Migration 操作を行って SQL Server データベースを生成します。

SQL Server データベース

具体的には、Visual Studio のパッケージマネージャーコンソールで Add-Migration, Update-Database コマンドを実行すれば、Entity Framework Code First の機能によって、上の画像のようなデータベースが生成されます。

なお、上の (3) で接続文字列を appsettings.json に既存の ASP.NET Identity 用の "ApplicationDbContextConnection" にしていますので、ASP.NET Identity のユーザー情報のストア用のデータベースに File テーブルが追加されます。別のデータベースとして作成したい場合は接続文字列を変更してください。

(5) サムネイル作成用ユーティリティ

オリジナル画像を指定したサイズに縮小したサムネイル画像を作成するユーティリティクラスを定義します。

今回は以前に作成した .NET Framework の Windows アプリ用のコードを流用したのですが、それは Windows OS の GDI+ に依存する System.Drawing 名前空間のグラフィックス機能を利用しています。

.NET Core では特定の OS に依存する機能は Visual Studio のテンプレートで作るプロジェクトには含まれてないようで、利用するには NuGet パッケージ System.Drawing.Common をインストールする必要があります。

System.Drawing.Common

Windows OS の GDI+ に依存するということで Linux 上で動かすと例外が出て動かないとのことですが、それを解決するために libgdiplus というライブラリがあるそうです。(未検証・未確認です)

NuGet パッケージ System.Drawing.Common をインストールしても CA1416 警告が出ますが、#pragma warning disable CA1416 を追記して警告を抑制しました。コードは以下の通りです。

#pragma warning disable CA1416

using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

namespace MvcCore6App.Utils
{
    public class ImageUtils
    {
        const int sizeThumb = 69;   // thumbimage のサイズ(縦横同じ)
        const int sizeLarge = 400;  // largerimage のサイズ(横幅)

        // sizeThumb で指定されたサイズのサムネイルを作る。
        // オリジナルの縦横比は保たれる(高さ or 幅の大きい方が sizeThumb になる)
        public static byte[] MakeThumb(byte[] fullsize)
        {
            // ・・・省略・・・
        }

        // 引数 newWidth, newHeight で指定されたサイズのサムネイルを作る。
        // 縦横で縮小率が異なる場合変形されないよう大きい方をトリミングして縮小
        public static byte[] MakeThumb(byte[] fullsize, int newWidth, int newHeight)
        {
            using (MemoryStream ms1 = new MemoryStream(fullsize))
            using (Image iOriginal = Image.FromStream(ms1))
            {                                
                // オリジナル/サムネイルの縦横のサイズ比
                double scaleW = (double)iOriginal.Width / (double)newWidth;
                double scaleH = (double)iOriginal.Height / (double)newHeight;

                // オリジナル画像をトリミングするための Rectangle 作成
                Rectangle srcRect = new Rectangle();

                if (scaleH == scaleW)  // 縦横同じ⇒トリミングなし
                {
                    srcRect.Width = iOriginal.Width;
                    srcRect.Height = iOriginal.Height;
                    srcRect.X = 0;
                    srcRect.Y = 0;
                }
                else if (scaleH > scaleW) // 縦 > 横 ⇒ 縦のみトリミング
                {
                    srcRect.Width = iOriginal.Width;
                    srcRect.Height = Convert.ToInt32((double)newHeight * scaleW);
                    srcRect.X = 0;
                    srcRect.Y = (iOriginal.Height - srcRect.Height) / 2;
                }
                else   // 縦 < 横 ⇒ 横のみトリミング
                {
                    srcRect.Width = Convert.ToInt32((double)newWidth * scaleH);
                    srcRect.Height = iOriginal.Height;
                    srcRect.X = (iOriginal.Width - srcRect.Width) / 2;
                    srcRect.Y = 0;
                }

                using (Image iThumb = new Bitmap(newWidth, newHeight))
                using (Graphics g = Graphics.FromImage(iThumb))
                {
                    g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                    Rectangle destRect = new Rectangle(0, 0, newWidth, newHeight);
                    g.DrawImage(iOriginal, destRect, srcRect, GraphicsUnit.Pixel);

                    using (MemoryStream ms2 = new MemoryStream())
                    {
                        iThumb.Save(ms2, ImageFormat.Jpeg);
                        return ms2.GetBuffer();
                    }
                }
            }
        }

        // 幅のみ指定してサムネイルを作る。高さは幅と同じ縮小率で縮小。
        public static byte[] MakeThumb(byte[] fullsize, int maxWidth)
        {
            // ・・・省略・・・
        }
    }
}

引数 fullsize のバイト列が有効なイメージ形式でないと Image.FromStream で例外がスローされます。一応 png と jpeg 形式は問題ないのは確認しましたが、その他はチェックはしてないので注意してください。戻り値のサムネイル画像のバイト列は jpeg 形式になります。

(6) Controller / Action Method

SQL Server のレコード一覧の表示、アップロード、編集、削除を行うための Controller のコードは以下のようにしました。

using Microsoft.AspNetCore.Mvc;
using MvcCore6App.Data;
using MvcCore6App.Models;
using MvcCore6App.Utils;
using Microsoft.EntityFrameworkCore;

namespace MvcCore6App.Controllers
{
    public class FileController : Controller
    {
        private readonly FileContext _context;

        public FileController(FileContext context)
        {
            _context = context;
        }

        public async Task<IActionResult> Index()
        {
            var fileContext = _context.Files;
            return View(await fileContext.ToListAsync());
        }

        public async Task<IActionResult> GetThumb(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var file = await _context.Files
                .FirstOrDefaultAsync(m => m.Id == id);

            if (file == null)
            {
                return NotFound();
            }

            return File(file.ThumbImage, "image/jpeg");
        }


        public IActionResult Upload()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Upload(FileUploadViewModel model)
        {
            if (ModelState.IsValid)
            {
                using (var memoryStream = new MemoryStream())
                {
                    await model.PostedFile.CopyToAsync(memoryStream);

                    // Upload the file if less than 2 MB
                    if (memoryStream.Length < 2097152)
                    {
                        var byteArray = memoryStream.ToArray();
                        var file = new FileEntity()
                        {
                            FileName = model.Title,
                            Description = model.Description,
                            ThumbImage = ImageUtils.MakeThumb(byteArray, 70, 70),
                            OriginalImage = byteArray
                        };

                        _context.Files.Add(file);

                        await _context.SaveChangesAsync();
                    }
                    else
                    {
                        ModelState.AddModelError("PostedFile", "サイズは 2MB 以下");
                        return View(model);
                    }
                }

                return RedirectToAction("Index");
            }

            return View(model);
        }

        public async Task<IActionResult> Edit(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var file = await _context.Files.FindAsync(id);
            if (file == null)
            {
                return NotFound();
            }
            return View(file);
        }

        [HttpPost, ActionName("Edit")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> EditPost(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var fileToUpdate = await _context
                                     .Files
                                     .FirstOrDefaultAsync(f => f.Id == id);

            if (fileToUpdate != null)
            {

                if (await TryUpdateModelAsync<FileEntity>(
                    fileToUpdate,
                    "",
                    f => f.FileName, f => f.Description))
                {
                    await _context.SaveChangesAsync();
                    return RedirectToAction("Index");
                }

            }
            return View(fileToUpdate);
        }

        public async Task<IActionResult> Delete(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var file = await _context.Files
                .AsNoTracking()
                .FirstOrDefaultAsync(f => f.Id == id);

            if (file == null)
            {
                return NotFound();
            }

            return View(file);
        }

        // POST: Students/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(int id)
        {
            var file = await _context.Files.FindAsync(id);

            if (file == null)
            {
                return RedirectToAction("Index");
            }

            _context.Files.Remove(file);
            await _context.SaveChangesAsync();
            return RedirectToAction("Index");
            
            
        }
    }
}

Upload メソッドは Microsoft のドキュメント「バッファー モデル バインドを使用して小さいファイルをデータベースにアップロードする」を参考にしました。Action Method と View の間のデータのやり取りにはビューモデル FileUploadViewModel を使用し、それをエンティティクラス FileEntity に移し替えるとともにサムネイル画像を追加し SQL Server データベースに保存しています。

アップロードするファイルの検証については、アップロードする時点でのファイル選択の有無とサーバー側でのサイズのチェックしか行っていません。Microsoft のドキュメント「ASP.NET Core でファイルをアップロードする」の「セキュリティに関する考慮事項」のセクションに書かれたセキュリティ関する配慮はしていませんので注意してください。

先の記事「ファイルアップロード時の検証 (CORE)」に、ASP.NET Core. 3.1 MVC アプリでファイルをアップロードする際に、カスタム検証属性を利用してファイルのサイズとタイプをクライアント側とサーバー側の両方で検証し、検証結果 NG の場合はエラーメッセージを表示する方法を書きましたので、興味があれば見てください。

Edit / EditPost メソッドではタイトルと説明のみ編集して結果を SQL Server データベースに反映するようにしました。画像の差し替えは上のコードではできません (画像を差し替えるなら、削除してから新たに Upload し直した方が良いと思いましたので)。

タイトルと説明のみ変更するため TryUpdateModelAsync メソッドを使っているところに注目してください。よくあるパターンとしては、上の EditPost メソッドの引数に ビューモデル FileUploadViewModel を使ってそれにモデルバインドということをすると思いますが、画像データはアップロードされてこないところが問題です。

上の EditPost メソッドのコードでは、既存のエンティティを読み取り、TryUpdateModel を呼び出してポストされたタイトルと説明からフィールドを更新しています。既存のエンティティの読み取りによって画像データも取得されるので、その上でタイトルと説明だけを変更して SaveChanges を適用するので画像データが消えることはないです。

TryUpdateModelAsync メソッドについては Microsoft のチュートリアルの「HttpPost Edit メソッドの更新」が参考になると思いますので興味があれば見てください。

GetThumb メソッドは、DB からサムネイル画像を取得してダウンロードするためのメソッドです。この記事の��番上の一覧表示の画像ようにサムネイルを表示するために使います。View に img 要素を配置し、その src 属性に GetThumb メソッドを設定することによりサムネイル画像が表示されます。

src 属性に GetThumb メソッドを設定するということは、ブラウザはそこで GetThumb メソッドを呼んでデータベースからデータを取得してくるという動きになることに注意してください。特にこの記事の一番上の画像のように一覧表示する場合はレコードの数だけ GetThumb メソッドが呼ばれることになります。

それを避けるのは、アクションメソッドですべてのデータを Model として View に渡し済みですので、View で Model に含まれる画像のバイト列を Data URL 形式に変換して src 属性に設定することにより可能です。その例は下の「(7) View」のセクションの Index.cshtml に書きます。ただし、そのようにした場合はブラウザはその画像をキャッシュできないことに注意してください。

(7) View

Index.cshtml, Upload.cshtml, Edit.cshtml, Delete.cshtml のコードをその順に以下に記載しておきます。Index.cshtml と Delete.cshtml には上のコントローラーのコードの GetThumb メソッドを使ってサムネイル画像を表示するようにしています。

Index.cshtml

@model IEnumerable<MvcCore6App.Models.FileEntity>

@{
    ViewData["Title"] = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<p>
    <a asp-action="Upload">Uoload New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.FileName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Description)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ThumbImage)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@{
#nullable disable
    foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.FileName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Description)
            </td>
            <td>
                @*以下はコメントアウトして代わりのコードを書いた                
                <img src="/File/GetThumb/@item.Id" 
                    alt="@item.FileName" title="@item.FileName" />*@

                @*アクションメソッドですべてのデータを Model として受け取って
                いるので、Model に含まれる画像のバイト列を Data URL 形式に変
                換して src 属性に設定することにより画像を表示できる。*@
                <img src="data:image/jpeg;base64,@Convert.ToBase64String(item.ThumbImage)" 
                    alt="@item.FileName" title="@item.FileName" />
            </td>
            
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
    }
}
    </tbody>
</table>

Upload.cshtml

@model MvcCore6App.Models.FileUploadViewModel

@{
    ViewData["Title"] = "Upload";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Upload</h1>

<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Upload" enctype="multipart/form-data" method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Description" class="control-label"></label>
                <input asp-for="Description" class="form-control" />
                <span asp-validation-for="Description" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="PostedFile" class="control-label"></label>
                <input asp-for="PostedFile" type="file" class="form-control" />
                <span asp-validation-for="PostedFile" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Edit.cshtml

@model MvcCore6App.Models.FileEntity

@{
    ViewData["Title"] = "Edit";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Edit</h1>

<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="FileName" class="control-label"></label>
                <input asp-for="FileName" class="form-control" />
                <span asp-validation-for="FileName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Description" class="control-label"></label>
                <input asp-for="Description" class="form-control" />
                <span asp-validation-for="Description" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Delete.cshtml

@model MvcCore6App.Models.FileEntity

@{
    ViewData["Title"] = "Delete";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Delete</h1>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>FileEntity</h4>
    <hr />
    <dl class="row">
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.FileName)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.FileName)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Description)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Description)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.ThumbImage)
        </dt>
        <dd class = "col-sm-10">
            @{
                if (Model != null)
                {
                    <img src="/File/GetThumb/@Model.Id" 
                        alt="@Model.FileName" 
                        title="@Model.FileName" />
                }
            }
        </dd>
    </dl>
    
    <form asp-action="Delete">
        <input type="hidden" asp-for="Id" />
        <input type="submit" value="Delete"
            class="btn btn-danger" /> |
        <a asp-action="Index">Back to List</a>
    </form>
</div>

Tags: , , , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  June 2023  >>
MoTuWeThFrSaSu
2930311234
567891011
12131415161718
19202122232425
262728293012
3456789

View posts in large calendar