情報応用演習Ⅰ(2024)

【T10b】簡易ブログソフトウェアの作成 Part.Ⅳ ~ ファイルのアップロード/ダウンロード(4/7)

プロジェクトタイプ(注意: 本文参照)
プロジェクト名T10b
ソリューション名PIT10
注意
  • 本ページの作業内容は 前のページまでの続き になっていることに注意せよ.
    • 先に前のページまでをすべて読み,指示されている作業を済ませてから本ページを読むこと.
    • プロジェクトの作成作業については準備作業を参照せよ.

10b-4. UploadAttachmentアクションとビューの作成

これらを定義したうえで,GET用およびPOST用のUploadAttachmentアクションメソッドを作成しよう. Articlesコントローラーに__に示す内容を追記する.

Articlesコントローラーの追記内容(UploadAttachmentアクション(GET用))
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// UploadAttachment アクション(GET用),
[Authorize]
public async Task<IActionResult> UploadAttachment(int? id) 
{
    if (id == null) 
    {
        return NotFound();
    }

    var article = await _context.Articles.FindAsync(id);
    if (article == null) 
    {
        return NotFound();
    }

    BlogUser? currentUser = await _userManager.GetUserAsync(User);
    if (!await IsModifiableAsync(currentUser, article))
    {
        return Forbid();
    }

    ViewBag.Article = article; // 記事の情報をViewBagに入れておく(表示に使う)

    return View(new UploadFile());
}
Articlesコントローラーの追記内容(UploadAttachmentアクション(POST用))
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// UploadAttachment アクション(POST用)
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadAttachment(int? id, UploadFile uploadFile) 
{
    if (id == null) 
    {
        return NotFound();
    }

    var article = await _context.Articles.FindAsync(id);

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

    BlogUser? currentUser = await _userManager.GetUserAsync(User);
    if (!await IsModifiableAsync(currentUser, article))
    {
        return Forbid();
    }

    ViewBag.Article = article;

    // ↑ここまではGET用の UploadAttachment() メソッドと同じ

    if (ModelState.IsValid) 
    {                
        var attachment = new Attachment()                      // 「添付ファイル」オブジェクトを生成する.
        {                                                      //
            ArticleId = article.ArticleId,                     //    その添付ファイルを関連付ける記事のId
            Filename = uploadFile.File.FileName,               //    ファイル名
            ContentType = uploadFile.File.ContentType,         //    コンテントタイプ
            Uploaded = DateTime.Now,                           //    アップロードされた日付
        };                                                     //

        _context.Add(attachment);                              // データベースに
        await _context.SaveChangesAsync();                     // 反映する.

        var saveFilePath = GetAttachmentFilePath(attachment);  // 添付ファイルを保存するパスを取得する
                
        using (var strm = System.IO.File.Create(saveFilePath)) // アップロードされたファイルを,
        {                                                      // ↑で取得したファイルが示す場所に
            await uploadFile.File.CopyToAsync(strm);           // 保存する.
        }//using                                               //

        return RedirectToAction(nameof(Edit), new { id = article.ArticleId }); // 編集画面に遷移する.
    }//if

    return View(uploadFile);
}

両メソッドとも,処理の前半はEditアクションなどと違いはない.ログイン中のユーザーがその記事に対して編集を行う権限を 持っているかを確認しているのみである.

POST用のUploadAttachmentアクションも,データベースへの保存処理(40行目のawait _context.SaveChangesAsync())までは, 今まで見てきたようなモデルクラスの追加の処理と違いはない.実際のファイルの保存処理を行っているのは42~47行目である. ここではアップロードされたファイルの物理的な保存先を決定し,そこに実際にファイルを保存している.このために, リスト10b-3-9で定義したGetAttachmentFilePath()メソッドを使用している.

次にこのUploadAttachmentアクションのためのビューを作成しよう. Views/Articles/UploadAttachment.cshtml を_のように記述する.

Views/Articles/UploadAttachment.cshtmlの内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@model UploadFile

@{
    ViewData["Title"] = "ファイル添付";

    if (Model is null) throw new ArgumentNullException(nameof(Model)); // ad-hoc! 非null保証のための回避策

    var article = (Article)ViewBag.Article; // アクション側で保存した記事オブジェクトを取得する.
}

<p>
  <a asp-action="Details" asp-route-id="@article.ArticleId">記事:@article.Title</a>にファイルを添付します.
</p>

<form asp-action="UploadAttachment" enctype="multipart/form-data">
  @* 添付ファイルのための入力欄 *@
  <label asp-for="@Model.File"></label>:
  <input asp-for="@Model.File" />
  <span asp-validation-for="@Model.File"></span>
  <br />

  <input type="submit" value="アップロード" />
</form>

<a asp-action="Edit" asp-route-id="@article.ArticleId">編集に戻る</a>

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

1行目ではリスト10b-3-7で定義したUploadFileクラスをモデルクラスとして指定している.

15行目では通常通り asp-action タグヘルパーを form 要素に指定しているが, enctype 属性を指定している点が異なる. 通常のフォームはこの属性にはapplication/x-www-form-urlencodedが指定されているものとみなされるが, ファイルをアップロードする際は_のようにmultipart/form-dataを指定する必要がある.

18行目ではUploadFileオブジェクトのFileプロパティ,すなわちIFormFileインターフェース型のプロパティを asp-for タグヘルパーを使用して input 要素に関連付けている.

さらに図10b-3-2で示した通り,記事の個別表示画面(Details)と記事編集画面(Edit)にも 手を加えておこう.ArticlesコントローラーのDetailsアクションとEditアクション(GET用)に, それぞれ__に示す内容を適用しよう.

Articlesコントローラーの変更内容(Detailsアクション(GETのみ))
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// GET: Articles/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null || _context.Articles == null)
    {
        return NotFound();
    }

    var article = await _context.Articles
        .Include(a => a.BlogUser)
        .Include(a => a.Attachments) // Include()を使ってナビゲーションプロパティを使用できるようにする.
        .FirstOrDefaultAsync(m => m.ArticleId == id);
    if (article == null)
    {
        return NotFound();
    }

    BlogUser? currentUser = await _userManager.GetUserAsync(User);
    if (!await IsReadableAsync(currentUser, article)) return Forbid();

    ViewBag.IsModifiable = currentUser != null && await IsModifiableAsync(currentUser, article);

    return View(article);
}
Articlesコントローラーの変更内容(Editアクション(GET用))
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// GET: Articles/Edit/5
[Authorize]
public async Task<IActionResult> Edit(int? id)
{
    if (id == null || _context.Articles == null)
    {
        return NotFound();
    }

    var article = await _context.Articles
        .Include(a => a.BlogUser)
        .Include(a => a.Attachments) // Include()を使ってナビゲーションプロパティを使用できるようにする.
        .FirstOrDefaultAsync(m => m.ArticleId == id);
    if (article == null)
    {
        return NotFound();
    }            

    BlogUser? currentUser = await _userManager.GetUserAsync(User);
    if (!await IsModifiableAsync(currentUser, article)) return Forbid();

    return View(article);
}

どちらのメソッドでも,指定された記事IDからArticleクラスのオブジェクトを得る方法を修正して,ビュー側で ナビゲーションプロパティAttachmentsを使用できるように.Include()メソッドを呼び出している.

これに対応して各々のビューも変更しておこう. Views/Articles フォルダの Details.cshtml と Edit.cshtml に,それぞれ__に示す通りに 追記しよう.

Views/Articles/Details.cshtmlの追記内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@model T10b.Models.Article
 
@{
    if (Model is null) throw new ArgumentNullException(nameof(Model)); // ad-hoc! 非null保証のための回避策
 
    ViewData["Title"] = Model.Title; 
 
    var isModifiable = (bool)ViewBag.IsModifiable; 
}
 
<p> @(Model.BlogUser?.Nickname ?? Model.BlogUser?.UserName) による投稿</p>
 
<dl>
  <dt>@Html.DisplayNameFor(m => m.Created)</dt>
  <dd>@Html.DisplayFor(m => m.Created)</dd>
 
  <dt>@Html.DisplayNameFor(m => m.Modified)</dt>
  <dd>@Html.DisplayFor(m => m.Modified)</dd>
</dl>
 
<pre class="article">@Model.Body</pre>

@* 添付ファイルの一覧の表示 *@
@if (Model.Attachments != null && Model.Attachments.Count != 0)
{
    <h5>添付ファイル</h5>
    <ul>
      @foreach(var f in Model.Attachments)
        {
            <li>
              <a asp-action="GetAttachment" asp-route-id="@f.AttachmentId">@f.Filename</a>
            </li>
        }
    </ul>
}

<a asp-action="Index">一覧に戻る</a>
 
@if(isModifiable)
{
    <span>| </span><a asp-action="Edit" asp-route-id="@Model.ArticleId">編集</a>
    <span>| </span><a asp-action="Delete" asp-route-id="@Model.ArticleId">削除</a>
}
Views/Articles/Edit.cshtmlの追記内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@model T10b.Models.Article
 
@{
    ViewData["Title"] = "記事編集";
 
    if (Model is null) throw new ArgumentNullException(nameof(Model)); // ad-hoc! 非null保証のための回避策
}
 
<form asp-action="Edit">
  <label asp-for="@Model.Title"></label>  <input asp-for="@Model.Title" />
  <br />
 
  <label asp-for="@Model.Body"></label><br />
  <textarea asp-for="@Model.Body" rows="10" cols="70"></textarea>
  <br />
 
  <label asp-for="@Model.PublicationState"></label>  <select asp-for="@Model.PublicationState" asp-items="Html.GetEnumSelectList<PublicationStateType>()"></select>
  <br />
 
  <input type="hidden" asp-for="@Model.ArticleId" />
  <input type="hidden" asp-for="@Model.Created" />    
  <input type="hidden" asp-for="@Model.BlogUserId" />

  <input type="submit" value="修正" />
  
  @* 「ファイル添付」ページへのリンク *@
  <a asp-action="UploadAttachment" asp-route-id="@Model.ArticleId">ファイル添付</a>
 
  <div asp-validation-summary="All"></div>
</form>

@* 添付ファイルの一覧の表示(「削除」のリンク付き) *@
@if (Model.Attachments != null && Model.Attachments.Count != 0)
{
    <h5>添付ファイル</h5>
    <ul>
      @foreach(var f in Model.Attachments)
        {
            <li>
              <a asp-action="GetAttachment" asp-route-id="@f.AttachmentId">@f.Filename</a>
              (<a asp-action="DeleteAttachment" asp-route-id="@f.AttachmentId">削除</a>)
            </li>
        }
    </ul>
}
 
<a asp-action="Details" asp-route-id="@Model.ArticleId">詳細に戻る</a>
 
@section Scripts
{
    @{ await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

_Detailsのビューでは,添付ファイルが1件でも添付されていれば それを23~35行目でリストとして表示している._Editのビューでも同様に, 34~47行目で添付ファイルのリストを表示している.Detailsと異なるのは,削除のためのリンク(43行目)も 表示している点である.

Editのビューでは,これらに加えて「修正」ボタンの横にUploadAttachmentアクションへのリンクを 表示するようにしている(29行目).

ここまで書けたら実行してみよう.プロジェクトを起動して「ログイン」のリンクをクリックし適当な 通常ユーザー (admin以外のユーザー)としてログインしよう(__).記事の一覧画面(Index)に移動してその中から, ログイン中のユーザーが編集可能な記事を見つけて「編集」のリンクをクリックする. すると記事の編集画面(Edit)に遷移するはずである(__). さらにその画面で「修正」ボタンの横の「ファイル添付」のリンクをクリックする. するとファイル添付画面(UploadAttachment)に遷移するはずである(__). 何でもよいのでアップロードする適当なファイルを用意しよう._の例ではWordファイルを使用している. この画面で「参照」のボタンを押すとファイル選択ダイアログが開くので,前述のファイルを選択しよう (__). そして「アップロード」のボタンを押すと記事の編集画面に遷移して,ページ下部に添付ファイルのリストが表示されるはずである (__). このアップロードしたファイルのファイル名を控えておこう

実行結果

この状態で,いったん実行を停止し「ソリューションエクスプローラー」のプロジェクト名を右クリックして, 「エクスプローラーでフォルダを開く」をクリックする(_).「bin」フォルダ→「Debug」フォルダ →「net8.0」フォルダと辿っていくと,「Attachments」フォルダが作成されていることが分かるはずである(__). さらにこの「Attachments」フォルダを開くと,ファイル名がAttachmentクラスの主キー,つまりは 拡張子なしの数値になっているファイル が作られているはずである(_). このファイル名が数値になっているファイルをその場にコピーし,ファイル名を先ほど控えておいたファイル名に付け替えて開いてみよう. ファイルの内容が確かに先ほどアップロードしたファイルと同じものであることが確認できるはずである(__).

実行結果
Last updated on 2024-06-18
Published on 2024-06-18

Powered by Hugo. Theme by TechDoc. Designed by Thingsym.