miso_soup3 Blog

主に ASP.NET 関連について書いています。

ASP.NET MVC 半角全角の入力値補正

ASP.NET MVC において、自動的に全角→半角にしたり、
両端のスペースを取り除いてくれるようにしよう!という話です。

アプローチ

プロパティ毎にモデルバインダーを切り替えるようにします。
また、モデルは、単純な型でなく、複合型を対象とします。
ざっと以下の通りです。

  • 全角→半角にしたりするモデルバインダーを利用する。
  • 補正したいプロパティに [SmartBind] という属性をつける。
  • [SmartBind] がついている場合だけ、指定したモデルバインダーでバインドされるようにする。
  • プロパティ毎にモデルバインダーを切り替える処理は、DefaultModelBinder を継承して作成。
理由

このアプローチをとった理由についてです。
(ちょっと長くて余計なことも書いてます。)

先述のようなことをしなくても String に対するモデルバインダーを作れば簡単なのですが、
そうすると、アプリケーション全体の String に対して補正されてしまうので、
それは避けたく、属性がついているプロパティだけを対象にするようにしました。

また、ASP.NET MVC には、プロパティ毎にモデルバインダーを設定できるような
拡張ポイントは用意されておりません。
モデルバインダーは、”型”だけを参照して選択されています。(ModelMetaData とかも参照されない)
(ちなみにモデルバインダーの選択方法について、ModelBinderDictionaryクラスの GetBinder メソッドに丁寧にコメントが書かれています。 )

ということで、モデルバインダープロバイダーあたりを弄ってもできないので、
DefaultModelBinder をいじることにしました。

で、DefaultModelBinder は、複合型が指定された時に、
BindProperty メソッドと SetProperty メソッドでプロパティに値を設定します。
2つのメソッドの定義は以下の通りです。

void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)

BindProperty メソッドが先に呼び出され、モデルバインダーでプロパティの値を生成した後、
SetProperty メソッドの引数、object value(右端に定義)にその生成した値が入ります。

なので、モデルバインダーを用意しなくでも、SetProperty メソッド内で、
object value に対して全角半角処理を実行すれば、やりたいことは実現できます。

が、BindProperty メソッド内でモデルバインダーが利用されていることが、
ちょっと気持ち悪い…のと、string はいいけど int に対応するときどうする?
という曖昧な理由から、プロパティ毎にモデルバインダーを設定する、という選択にしました。

(補正はモデルバインダーよりも一歩進んだ概念なので
モデルバインダーではなく補正者ーというオブジェクトでもいいかもしれません。)

実装コード

MVC Property Binder を参考にしました。

SmartStringBinder

String の型に対する、補正を行うモデルバインダーです。
入力された値を、全角→半角に変換し、Trim を行い
String 値を生成します。

public class SmartStringBinder : IModelBinder
{
	public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
	{
		if (bindingContext.ModelType != typeof(string))
			throw new NotSupportedException();

		var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
		if (valueResult == null)
			return null;

		string value = valueResult.AttemptedValue;

		if (value == null)
			return null;

		return SmartProcessing(value);
	}

	private string SmartProcessing(string value)
	{
		return Microsoft.VisualBasic.Strings.StrConv(value, Microsoft.VisualBasic.VbStrConv.Narrow)
			.Trim();
	}
}

Microsoft.VisualBasic ェ・・・

属性 PropertyBinderAttribute

このプロパティは、指定したモデルバインダーを使って
値を生成してほしい、ということを表す属性です。

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class PropertyBinderAttribute : Attribute
{
	public PropertyBinderAttribute(Type binderType)
	{
		BinderType = binderType;
	}

	public Type BinderType { get; set; }
}

先ほど作成した SmartStringBinder をあらかじめ指定しておく
SmartBindAttribute も作っておきます。

public class SmartBindAttribute : PropertyBinderAttribute
{
	public SmartBindAttribute()
		: base(typeof(SmartStringBinder))
	{
	}
}
属性付与

補正を行いたいプロパティに、SmartBindAttribute をつけます。

public class Employee
{
	public int Id { get; set; }

	[SmartBind]
	[Required]
	[StringLength(10)]
	public String Name { get; set; }

	[SmartBind]
	public String MailAddress { get; set; }
}

他の検証属性もおまけでつけときました。
ちゃんと補正後に検証が効きます。

DefaultModelBinder をカスタマイズ

プロパティのバインド時、プロパティに PropertyBinderAttribute 属性がついている場合は、
その属性クラスに指定されているモデルバインダーを使用して値を生成するようにします。

所々、元の DefaultModelBinder の処理を参照しています。

public class CustomModelBinder : DefaultModelBinder
{
	protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
	{
		//プロパティバインダー取得
		PropertyBinderAttribute propertyBinderAttribute = TryFindPropertyBinderAttribute(propertyDescriptor);

		//プロパティバインダーの指定がない場合は、デフォルトの動作へ
		if (propertyBinderAttribute == null)
		{
			base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
			return;
		}

		//プロパティバインダーより値をセットする
		string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
		if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey))
		{
			return;
		}

		IModelBinder propertyBinder = CreateBinder(propertyBinderAttribute);
		object originalPropertyValue = propertyDescriptor.GetValue(bindingContext.Model);
		ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
		propertyMetadata.Model = originalPropertyValue;
		ModelBindingContext innerBindingContext = new ModelBindingContext()
		{
			ModelMetadata = propertyMetadata,
			ModelName = fullPropertyKey,
			ModelState = bindingContext.ModelState,
			ValueProvider = bindingContext.ValueProvider
		};
		object newPropertyValue = GetPropertyValue(controllerContext, innerBindingContext, propertyDescriptor, propertyBinder);
		propertyMetadata.Model = newPropertyValue;

		SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
	}

	/// <summary>
	/// プロパティについているPropertyBinderAttribute属性の取得を試みます。
	/// </summary>
	/// <param name="propertyDescriptor"></param>
	/// <returns></returns>
	private PropertyBinderAttribute TryFindPropertyBinderAttribute(PropertyDescriptor propertyDescriptor)
	{
		return propertyDescriptor.Attributes
			.OfType<PropertyBinderAttribute>()
			.FirstOrDefault();
	}

	/// <summary>
	/// プロパティバインダーで指定したモデルバインダーのインスタンスを取得します。
	/// </summary>
	/// <param name="propertyBinderAttribute"></param>
	/// <returns></returns>
	private IModelBinder CreateBinder(PropertyBinderAttribute propertyBinderAttribute)
	{
		return (IModelBinder)DependencyResolver.Current.GetService(propertyBinderAttribute.BinderType);
	}
}
モデルバインダーの登録

さっき作成した DefaultModelBinder をカスタマイズした CustomModelBinder を、
MVC のデフォルトのモデルバインダーとして登録します。

public class BinderConfig
{
	public static void RegisterBinders(ModelBinderDictionary binders)
	{
		binders.DefaultBinder = new CustomModelBinder();
	}
}

一丁前に BinderConfig を用意。

↓ Global.asax.cs にて

public class MvcApplication : System.Web.HttpApplication
{
	protected void Application_Start()
	{
		//AreaRegistration.RegisterAllAreas(); とか
		//...

		BinderConfig.RegisterBinders(ModelBinders.Binders);
	}
}

おしまい

        これで、誰も例のTweetをRTしなくなる
   ∧∧
  (  ・ω・)
  _| ⊃/(___
/ └-(____/
 ̄ ̄ ̄ ̄ ̄ ̄ ̄
        俺はそういうことに幸せを感じるんだ
  <⌒/ヽ-、___
/<_/____/

テストはちゃんと行っておりません。
CustomModelBinder あたりを重点的に、でしょうか。