だるろぐ

明日できることは、今日しない。

WebMatrix でファイルのアップロード

f:id:daruyanagi:20120819110408p:plain

今日は「WebMatrix 2」でファイルのアップロードを試してみた。なお、このサンプルは「Empty Sites」テンプレートを元に作成している。

Delault.cshtml

<!DOCTYPE html>

<html lang="ja">
    <head>
        <meta charset="utf-8" />
        <title>マイ サイトのタイトル</title>
    </head>
    <body>
        <form action="~/Upload" method="post"
              enctype="multipart/form-data">
            <input type="file" name="upload" /><br />
            <input type="submit" name="submit" />
        </form>
    </body>
</html>

拡張子を html にすれば、ただの HTML ドキュメントだね! ファイルのアップロードを行うので、 multipart/form-data をつけるのを忘れないように。

f:id:daruyanagi:20120819110559p:plain

Upload.cshtml

アップロード処理を行う cshtml はこんな感じにしてみた。

ほんとは path が存在しなければ例外、 file のサイズが 0 ならば例外、 file が image/*** でなければ例外、といったチェックを入れるのだけれど、ソースが長くなるので割愛している。あと、最初から複数ファイルのアップデートに対応できるように記述している。

@using System.IO

@functions {
    enum Result { Success, Error };
}

@{
    var result = Result.Error;
    var message = "You can use only POST method.";
    var link = string.Empty;

    if (IsPost)
    {
        foreach (var key in Request.Files.AllKeys)
        {
            var file = Request.Files[key];

            try
            {
                const string OUTPUT = "~/Files/";
                var path = Server.MapPath(OUTPUT);

                var src = Path.GetFileName(file.FileName);
                var dst = string.Format(
                    "{0:yyyyMMdd-HHmmssfff}{1}",
                    DateTime.Now, Path.GetExtension(src).ToLower()
                );

                file.SaveAs(Path.Combine(path, dst));

                result = Result.Success;
                message = string.Format(
                    "{0} is uploaded as {1}.", 
                    src, dst
                );
                link = VirtualPathUtility.ToAbsolute(OUTPUT + dst);
            }
            catch (Exception e)
            {
                result = Result.Error;
                message = e.Message;
            }
        }
    }
}

<h1>@result</h1>
<p>@message</p>
if (!string.IsNullOrEmpty(link))
{
    <p><img src="@link" /></p>
}
<p>&raquo; Back to <a href="~/">home</a></p>

基本的には、 Request.Files でファイルを取得し、 SaveAs() で保存するだけ。そのほかはファイル名の決定だのエラー処理だのといったことをしているに過ぎない。

Default.cshtml から画像ファイルを POST すると、

f:id:daruyanagi:20120819111843p:plain

エラーが出たらこんな感じで……

f:id:daruyanagi:20120819111723p:plain

成功したらこんな感じになる。

f:id:daruyanagi:20120819112043p:plain

"~/Files/"フォルダが夢のようになっておるな!

ステップアップ

ヘルパーで楽をしよ……ぅ?

f:id:daruyanagi:20120819114311p:plain

ASP.NET Web Helpers Library という NuGet をインストールすると、複数ファイルのアップロードに対応した Form タグを簡単に生成できる。

@FileUpload.GetHtml()

でも、個人的にはあんまり好きじゃなかったので今回は使わなかった。

<!DOCTYPE html>

@{
    if (IsPost)
    {
        foreach (var key in Request.Files.AllKeys)
        {
            var file = Request.Files[key];

            try
            {
                file.SaveAs(
                   System.IO.Path.Combine(
                       Server.MapPath("~/Files/"),
                       file.FileName)
                );
            }
            catch (Exception e)
            {

            }
        }
    }
}

<html lang="ja">
    <head>
        <meta charset="utf-8" />
        <title>マイ サイトのタイトル</title>
    </head>
    <body>
        @FileUpload.GetHtml()
    </body>
</html>

f:id:daruyanagi:20120819115634p:plain

なんか動的に生成されるノードの名前がカブってるし*1、あんまりよくわかんなかった。

ビューでつかう変数をまとめる

Upload.cshtml のソースコードがなんだか冗長なのは、HTML の出力に使う result、message、link という3つの変数を処理するためだけど、こいつらって匿名クラスでまとめてもいいよね。

@using System.IO

@functions { enum Result { Success, Error }; }

@{
    const string OUTPUT = "~/Files/";
    dynamic model = null;

    if (IsPost)
    {
        foreach (var key in Request.Files.AllKeys)
        {
            var file = Request.Files[key];

            try
            {
                var path = Server.MapPath(OUTPUT);

                var src = Path.GetFileName(file.FileName);
                var dst = string.Format(
                    "{0:yyyyMMdd-HHmmssfff}{1}",
                    DateTime.Now, Path.GetExtension(src).ToLower()
                );

                file.SaveAs(Path.Combine(path, dst));

                model = new {
                    Result = Result.Success,
                    Message = string
                        .Format("{0}'s uploaded as {1}", src, dst),
                    Link = VirtualPathUtility
                        .ToAbsolute(OUTPUT + dst),
                };
            }
            catch (Exception e)
            {
                model = new {
                    Result = Result.Error,
                    Message = e.Message,
                    Link = string.Empty,
                };
            }
        }
    }
    else
    {
        model = new {
            Result = Result.Error,
            Message = "You can use only POST method",
            Link = string.Empty,
        };
    }
}

<h1>@model.Result</h1>
<p>@model.Message</p>
@if (!string.IsNullOrEmpty(model.Link))
{
    <p><img src="@model.Link" /></p>
}
<p>&raquo; Back to <a href="~/">home</a></p>

記述量はかえって多くなったけど、「何かの処理 → 結果(モデル)の生成」という流れが明確になった気がする。この @model っていうのが MVVM の ViewModel じゃない ViewModel という理解でいいんでしょうか。

Ajax には Json で応える

ViewModel を返すことの利点は、可読性だけじゃない。たとえばこんなこともできる。

@if (IsAjax)
{
    // Response.ContentType = "application/json";
    Response.Write(Json.Encode(model));
}
else
{
    <h1>@model.Result</h1>
    <p>@model.Message</p>
    if (!string.IsNullOrEmpty(model.Link))
    {
        <p><img src="@model.Link" /></p>
    }
    <p>&raquo; Back to <a href="~/">home</a></p>
}

f:id:daruyanagi:20120819125626p:plain

Ajax リクエストに Json で応えるなんてことも簡単にできる!

拡張メソッドのお時間です

あとさ、これダサいよね。

foreach (var key in Request.Files.AllKeys)
{
    var file = Request.Files[key];
    :
    :

拡張メソッドを書いて、シンプルにしましょう。

foreach (var file in Request.Files.ToEnumerable())
{
    :
    :

~/App_Code/HttpFileCollectionBaseExtension.cs を作成してこのように書いてみました。

using System.Collections.Generic;
using System.Web;

public static class HttpFileCollectionBaseExtension
{
    public static IEnumerable<HttpPostedFileBase> ToEnumerable(
        this HttpFileCollectionBase target)
    {
        foreach (var key in target.AllKeys)
        {
            yield return target[key];
        }
    } 
}

*1:JavaScriptの不具合かなぁ