読者です 読者をやめる 読者になる 読者になる

KatsuYuzuのブログ

.NET の開発をメインとした日記です。

カスタムモデルバインダーのすゝめ #aspnetjp

この記事はASP.NET Advent Calendar 2015 - Qiitaに参加しています。17日の担当です。前日は id:minato128 さんでした。

カスタムモデルバインダーのすゝめ

f:id:KatsuYuzu:20151217023340p:plain
ASP.NET MVCではモデルバインダーという仕組みで、リクエストの値をモデルに紐づけてくれます。自分でFormの中身やクエリーストリングを見たり、パースしたりしなくてもいい素晴らしい仕組みです。お手軽さ、直感さ、大好き。
そして、もちろんカスタマイズできます。和暦 "H27/12/17" を DateTime 型にしたいなんて闇もモデルバインダーで済ませられれば捗ります。

_________________________ 
    <○√ 
     ∥    ←モデルバインダー
      くく 

作成方法

  1. DefaultModelBinder を継承
  2. BindModel をオーバーライド
  3. カスタムモデルバインダーの登録

DefaultModelBinder を継承 ~ BindModel をオーバーライド

DefaultModelBinder を継承して BindModel をオーバーライドするだけです。その中でモデルステート用の設定を呼び出す等の多少のお作法がある程度。
今回は "1,000" というカンマ区切りの数値文字列を数値型で受け取れるようにしてみます。*1

DefaultModelBinder を継承

数値型の部分をジェネリックにして、型制約を適当に。*2

/// <summary>
/// 1000 の桁で区切られた数字のブラウザー要求を数値型に対応付けます。
/// </summary>
/// <typeparam name="T">対応付ける数値型。</typeparam>
public class ThousandsSeparatorNumberBinder<T>
    : DefaultModelBinder
    where T : struct, IConvertible
{}
BindModel をオーバーライド

多少のお作法を守りながら変換するだけ。唐突に登場している Parser は後述。

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
    var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

    if (valueProviderResult == null)
    {
        return base.BindModel(controllerContext, bindingContext);
    }

    // モデルステートの設定
    bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

    // 値の文字列
    var valueString = valueProviderResult.AttemptedValue;

    // 以下、変換処理

    if (string.IsNullOrWhiteSpace(valueString))
    {
        return null;
    }

    try
    {
        return Parser(valueString);
    }
    catch (Exception ex)
    {
        bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
        return null;
    }
}
カンマ区切りの数値文字列を数値型に変換

int.ParseNumberStyles フラグを渡すことで簡単に実現できます。ただ、数値型は複数あるのでそこは頑張ります。パース時のオーバーフロー例外を期待するとか考慮するとそれぞれきっちり。

/// <summary>
/// 送信された文字列の変換に使うパーサー。
/// </summary>
private static readonly Func<string, object> Parser;

/// <summary>
/// パーサーを初期化します。
/// </summary>
static ThousandsSeparatorNumberBinder()
{
    // short, long は int と同様、float, double は decimal と同様なので割愛
    var parsers = new Dictionary<Type, Func<string, object>>
    {
        {
            typeof(int), 
            (string value) => int.Parse(value, NumberStyles.Integer | NumberStyles.AllowThousands)
        },
        {
            typeof(decimal), 
            (string value) => decimal.Parse(value, NumberStyles.Number)
        }
    };

    Parser = parsers[typeof(T)];
}

カスタムモデルバインダーの登録

アプリケーション全体、または、コントローラーのアクションで登録します。今回はプリミティブな型向けなのでアプリケーション全体で登録するのが良いかと思います。

// モデルバインダーの登録
// グローバルで一括登録するならキー型は nullable も忘れずに!
// カスタムバインダー側は nullable でなくておk。
// コントローラーやアクションでの個別利用なら適宜どうぞ。
ModelBinders.Binders.Add(typeof(int), new ThousandsSeparatorNumberBinder<int>());
ModelBinders.Binders.Add(typeof(int?), new ThousandsSeparatorNumberBinder<int>());

結果

やったね!
f:id:KatsuYuzu:20151217023340p:plain

バトンタッチ

qiita.com

*1:和暦の話は面倒なのでいつか。

*2:いつも思うんですが数値型制約とか欲しいよ!