しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET MVC 3 と File API を利用してアップロード前に検証を行う

ASP.NET MVC でアップロードされたファイルを検証する - しばやん雑記 でアップロードされたファイルの拡張子とファイルサイズを検証する属性を実装しましたが、いちいちアップロードが必要なので時間がかかりユーザビリティ的によくありませんでした。

ファイルサイズや拡張子といった情報は、IE 以外で実装されている File API を使えばJavaScript から取得できるので、今回はそれを利用した検証属性を作成しました。

それではサクッと独自の検証属性を作成しましょう。作業としては前回作成した UploadFile 検証属性に IClientValidatable を実装するだけです。

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class UploadFileAttribute : ValidationAttribute, IClientValidatable
{
    public UploadFileAttribute()
    {
        ErrorMessage = "ファイルの検証に失敗しました。";
    }

    public int MaxLength { get; set; }

    public string Extensions { get; set; }

    public override bool IsValid(object value)
    {
        var postedFile = value as HttpPostedFileBase;

        if (postedFile == null)
        {
            return true;
        }

        if (postedFile.ContentLength > MaxLength)
        {
            return false;
        }

        var ext = Path.GetExtension(postedFile.FileName).Replace(".", "");

        if (!string.IsNullOrEmpty(Extensions) && !Extensions.Split(';').Any(p=>p == ext))
        {
            return false;
        }

        return true;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ValidationType = "file",
            ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
        };

        rule.ValidationParameters["maxlength"] = MaxLength;
        rule.ValidationParameters["extensions"] = Extensions;

        yield return rule;
    }
}

今回はコメントを書く気力がなかったです。GetClientValidationRules を追加しただけで、このメソッドはクライアントサイド検証を作成した時と殆ど同じような処理です。ValidationParameters に必要な追加情報を入れるのをお忘れずに。

それでは jQuery validate にクライアントサイドで実行される検証メソッドを登録しましょう。今回は 2 つのパラメータが必要だったので、プリミティブなメソッドを呼び出して登録しました。

// 登録用のヘルパー関数
function setValidationValues(options, ruleName, value) {
    options.rules[ruleName] = value;
    if (options.message) {
        options.messages[ruleName] = options.message;
    }
}

// file 検証を登録
jQuery.validator.addMethod("file", function (value, element, param) {
    if (value == false) {
        return true;
    }

    // File API を使って選択されたファイルの情報を取得
    var file = element.files[0];

    // 拡張子を取得
    var index = file.name.lastIndexOf(".");
    var ext = file.name.substring(index + 1);

    // extensions が指定されているときだけ検証する
    if (param.extensions && param.extensions.indexOf(ext) == -1) {
        return false;
    }

    // maxlength が指定されている時だけ検証する
    if (param.maxlength && file.size > param.maxlength) {
        return false;
    }

    return true;
});

// maxlength と extensions の二つのパラメータを持つ file 検証を登録
jQuery.validator.unobtrusive.adapters.add("file", ["maxlength", "extensions"], function (options) {
    var maxlength = options.params.maxlength,
        extensions = options.params.extensions;

    setValidationValues(options, "file", { maxlength: maxlength, extensions: extensions });
});

今回はパラメータが二つ必要なので前回よりは行数が少し多いです。setValidationValues は jQuery validate の中に含まれているのですが、関数スコープで宣言されていたので仕方なく持ってきました。

そして最後に input タグを追加するのですが、控えめな JavaScript を使うためには適切に属性として情報を埋め込む必要があります。情報自体は Html.GetUnobtrusiveValidationAttributes メソッドを使うと取得できます。

しかしメタデータなどが関連し、Razor を使っても埋め込むのが非常に面倒だったので、HTML ヘルパーとして切り出しました。

public static class FileExtensions
{
    public static MvcHtmlString File<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
    {
        if (expression == null)
        {
            throw new ArgumentNullException("expression");
        }

        var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
        var name = ExpressionHelper.GetExpressionText(expression);

        var fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);

        var tagBuilder = new TagBuilder("input");
        tagBuilder.MergeAttribute("type", "file");
        tagBuilder.MergeAttribute("name", fullName, true);

        tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata));

        return MvcHtmlString.Create(tagBuilder.ToString(TagRenderMode.SelfClosing));
    }
}

超駆け足でしたがこれで準備はすべて完了しましたので、動作検証を前回と同じように拡張子が txt で 64KB 以下のファイルを許可するように設定し、Firefox 3.6 で行いました。

まずは DLL ファイルを指定しました。

通信が発生することなくエラーメッセージが表示されているので、クライアントサイドで検証が行われていることが分かりますね。

残りはサンプルソースを置いておくので各自で試してください!眠いのですよ…。

MvcApplication4.zip 直