2011年6月30日木曜日

全角数値を半角数値に変換するModelBinder

拝啓、まゆきっつぁん

いつも、The Shodoにて筆の練習をさせていただいてます。ただ、いつまで経っても筆ぺんでは上手にかけるようになれません。本物の習字道具を使わないと練習の成果が出ないのでしょうか?The Fudepenで練習すると効果が期待できるかもしれないですね。是非、考慮いただければと思います。

先日のmvcConf @:Japan懇親会での一件について。ふと、思い出したので書いてみました。あの時はValueProviderがどうのこうのという話になったような気がしないでもないですが、ValueProviderではレイヤ低すぎて型は意識されてないのダメですね。

こんな感じでいかがでしょうか?整数に限定してますが、応用すると他にもいろいろできると思います。

public class IntegralModelBinder : DefaultModelBinder
{
  private readonly List<Type> _integralTypes = new List<Type>
  {
    typeof (sbyte),
    typeof (byte),
    typeof (char),
    typeof (short),
    typeof (ushort),
    typeof (int),
    typeof (uint),
    typeof (long),
    typeof (ulong)
  };
  private const string WideIntegrals = "1234567890一二三四五六七八九零壱弐参肆伍陸柒捌玖零";
  private const string NarrowIntegrals = "123456789012345678901234567890";

  protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
  {
    if (!_integralTypes.Contains(propertyDescriptor.PropertyType) || value != null)
    {
      base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
      return;
    }

    var providerResult = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);
    value = providerResult.AttemptedValue;

    var narrow = string.Join("",(value + "").Select(c => WideIntegrals.Contains(c) ? NarrowIntegrals[WideIntegrals.IndexOf(c)] : c));
    var converter = TypeDescriptor.GetConverter(propertyDescriptor.PropertyType);
    try
    {
      value = converter.ConvertFrom(narrow);
      bindingContext.ModelState.Remove(propertyDescriptor.Name);
    }
    catch{}

    base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
  }
}

試しに以下のようなモデルを定義してみました。

    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public DateTime? Birthday { get; set; }
        public byte Rank { get; set; }
    }

AgeとRankが整数型なので処理対象となります。Global.asaxでDefaultBinder(ModelBinders.Binders.DefaultBinder = new IntegralModelBinder();)を差し替えて実行した結果は↓こんな感じになります。

mb1

@model ZenBinder.Models.Person
@{
    ViewBag.Title = "ホーム ページ";
}

<h2>@ViewBag.Message</h2>

@using (Html.BeginForm())
{
    @Html.EditorForModel()

    <button type="submit">送信</button>
}

まずはフォームを出すでしょう。簡単にEditorForModelを使います。

mb2

普通に半角だけで試して送信してみると、ちゃんと動きます。同じものを全角にしてみても結果は同じになります。

mb3

変換出来ない場合はDefaultModelBinderの挙動になります。十とか百とか千も変換するためのマッピングを用意すれば、もう少しオシャレさが増すかもしれないです。

mb4

いかがでしょう。ケータイでの入力に是非応用してみてください。