モデルはちょっと複雑です。
いくつかのプロパティを持つUserViewModel
があり、そのうちの2つはHomePhone
とWorkPhone
です。タイプPhoneViewModel
の両方。 PhoneViewModel
には、CountryCode
、AreaCode
、およびNumber
のすべての文字列があります。 CountryCode
をオプションにしたいのですが、AreaCode
とNumber
は必須です。
これはうまくいきます。私の問題は、UserViewModel
WorkPhone
は必須であり、HomePhone
は必須ではないということです。
とにかく、Require
プロパティに属性を設定することで、PhoneViewModel
のHomeWork
属性を無効にできますか?
私はこれを試しました:
[ValidateInput(false)]
しかし、それはクラスとメソッドのためだけです。
コード:
public class UserViewModel
{
[Required]
public string Name { get; set; }
public PhoneViewModel HomePhone { get; set; }
[Required]
public PhoneViewModel WorkPhone { get; set; }
}
public class PhoneViewModel
{
public string CountryCode { get; set; }
public string AreaCode { get; set; }
[Required]
public string Number { get; set; }
}
[アイデアをより明確にするために2012年5月24日に更新]
これが正しいアプローチかどうかはわかりませんが、概念を拡張して、より一般的で再利用可能なアプローチを作成できると思います。
ASP.NET MVCでは、検証はバインド段階で行われます。サーバーにフォームを投稿する場合、DefaultModelBinder
は、リクエスト情報からモデルインスタンスを作成し、検証エラーをModelStateDictionary
に追加するものです。
あなたの場合、バインディングがHomePhone
で発生する限り、検証が起動し、私は多くのことを行うことができないと思いますこれについては、カスタム検証属性または同様の種類を作成します。
私が考えているのは、(市外局番、国コード、数値または空)の形式で使用できる値がない場合、HomePhone
プロパティのモデルインスタンスをまったく作成しないことです。バインディングを制御します。検証を制御します。そのために、カスタムモデルバインダーを作成する必要があります。
カスタムモデルバインダーでは、プロパティがHomePhone
であるかどうか、フォームにそのプロパティの値が含まれているかどうか、含まれていない場合はチェックしています。プロパティをバインドしないでください。HomePhone
の検証は行われません。簡単に言うと、HomePhone
の値はUserViewModel
ではnullになります。
public class CustomModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.Name == "HomePhone")
{
var form = controllerContext.HttpContext.Request.Form;
var countryCode = form["HomePhone.CountryCode"];
var areaCode = form["HomePhone.AreaCode"];
var number = form["HomePhone.Number"];
if (string.IsNullOrEmpty(countryCode) && string.IsNullOrEmpty(areaCode) && string.IsNullOrEmpty(number))
return;
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
最後に、カスタムモデルバインダーをglobal.asax.csに登録する必要があります。
ModelBinders.Binders.Add(typeof(UserViewModel), new CustomModelBinder());
これで、UserViewModelをパラメーターとして受け取るアクションができました。
[HttpPost]
public Action Post(UserViewModel userViewModel)
{
}
カスタムモデルバインダーが機能し、フォームはHomePhone
の-areacode、countrycode、numberの値を投稿しません。検証エラーは発生せず、userViewModel.HomePhone
無効です。フォームがこれらのプロパティの値のいずれかを少なくとも投稿する場合、検証は期待どおりにHomePhone
に対して行われます。
私は動的な注釈を行うこの驚くべきnugetを使用しています: ExpressiveAnnotations
これまでは不可能だったことができるようになります。
[AssertThat("ReturnDate >= Today()")]
public DateTime? ReturnDate { get; set; }
あるいは
public bool GoAbroad { get; set; }
[RequiredIf("GoAbroad == true")]
public string PassportNumber { get; set; }
@diegoが述べたように、これは文字列でコードを書くのは恐ろしいかもしれませんが、コンパイルエラーを探すすべての検証をユニットテストするために使用するものは次のとおりです。
namespace UnitTest
{
public static class ExpressiveAnnotationTestHelpers
{
public static IEnumerable<ExpressiveAttribute> CompileExpressiveAttributes(this Type type)
{
var properties = type.GetProperties()
.Where(p => Attribute.IsDefined(p, typeof(ExpressiveAttribute)));
var attributes = new List<ExpressiveAttribute>();
foreach (var prop in properties)
{
var attribs = prop.GetCustomAttributes<ExpressiveAttribute>().ToList();
attribs.ForEach(x => x.Compile(prop.DeclaringType));
attributes.AddRange(attribs);
}
return attributes;
}
}
[TestClass]
public class ExpressiveAnnotationTests
{
[TestMethod]
public void CompileAnnotationsTest()
{
// ... or for all assemblies within current domain:
var compiled = Assembly.Load("NamespaceOfEntitiesWithExpressiveAnnotations").GetTypes()
.SelectMany(t => t.CompileExpressiveAttributes()).ToList();
Console.WriteLine($"Total entities using Expressive Annotations: {compiled.Count}");
foreach (var compileItem in compiled)
{
Console.WriteLine($"Expression: {compileItem.Expression}");
}
Assert.IsTrue(compiled.Count > 0);
}
}
}
私はmodelBinderには行きません。カスタムValidationAttributeを使用します:
public class UserViewModel
{
[Required]
public string Name { get; set; }
public HomePhoneViewModel HomePhone { get; set; }
public WorkPhoneViewModel WorkPhone { get; set; }
}
public class HomePhoneViewModel : PhoneViewModel
{
}
public class WorkPhoneViewModel : PhoneViewModel
{
}
public class PhoneViewModel
{
public string CountryCode { get; set; }
public string AreaCode { get; set; }
[CustomRequiredPhone]
public string Number { get; set; }
}
その後:
[AttributeUsage(AttributeTargets.Property]
public class CustomRequiredPhone : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
ValidationResult validationResult = null;
// Check if Model is WorkphoneViewModel, if so, activate validation
if (validationContext.ObjectInstance.GetType() == typeof(WorkPhoneViewModel)
&& string.IsNullOrWhiteSpace((string)value) == true)
{
this.ErrorMessage = "Phone is required";
validationResult = new ValidationResult(this.ErrorMessage);
}
else
{
validationResult = ValidationResult.Success;
}
return validationResult;
}
}
はっきりしない場合は説明しますが、一目瞭然だと思います。
いくつかの観察:バインディングが単純なファイル以上のものである場合、次のコードは問題を引き起こします。オブジェクトにネストされたオブジェクトがあり、それをスキップして、一部のファイルがネストされたオブジェクトにバインドされていないことが原因である場合があります。
考えられる解決策は
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
{
if (!propertyDescriptor.Attributes.OfType<RequiredAttribute>().Any())
{
var form = controllerContext.HttpContext.Request.Form;
if (form.AllKeys.Where(k => k.StartsWith(string.Format(propertyDescriptor.Name, "."))).Count() > 0)
{
if (form.AllKeys.Where(k => k.StartsWith(string.Format(propertyDescriptor.Name, "."))).All(
k => string.IsNullOrWhiteSpace(form[k])))
return;
}
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
どうもありがとう Altaf Khatri