diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj b/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj index c146034bc9..2ac49e2f93 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj @@ -71,6 +71,7 @@ + @@ -83,6 +84,7 @@ + diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ClientModelValidationContext.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ClientModelValidationContext.cs index 6b6fe5ef75..3569cabbf5 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ClientModelValidationContext.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ClientModelValidationContext.cs @@ -4,11 +4,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { public class ClientModelValidationContext { - public ClientModelValidationContext([NotNull] ModelMetadata metadata) + public ClientModelValidationContext([NotNull] ModelMetadata metadata, + [NotNull] IModelMetadataProvider metadataProvider) { ModelMetadata = metadata; + MetadataProvider = metadataProvider; } public ModelMetadata ModelMetadata { get; private set; } + + public IModelMetadataProvider MetadataProvider { get; private set; } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/CompareAttributeAdapter.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/CompareAttributeAdapter.cs new file mode 100644 index 0000000000..f2123f7679 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/CompareAttributeAdapter.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class CompareAttributeAdapter : DataAnnotationsModelValidator + { + public CompareAttributeAdapter([NotNull] CompareAttribute attribute) + : base(new CompareAttributeWrapper(attribute)) + { + } + + public override IEnumerable GetClientValidationRules( + [NotNull] ClientModelValidationContext context) + { + var errorMessage = ((CompareAttributeWrapper)Attribute).FormatErrorMessage(context); + var clientRule = new ModelClientValidationEqualToRule(errorMessage, + FormatPropertyForClientValidation(Attribute.OtherProperty)); + return new [] { clientRule }; + } + + private static string FormatPropertyForClientValidation(string property) + { + return "*." + property; + } + + private sealed class CompareAttributeWrapper : CompareAttribute + { + public CompareAttributeWrapper(CompareAttribute attribute) + : base(attribute.OtherProperty) + { + // Copy settable properties from wrapped attribute. Don't reset default message accessor (set as + // CompareAttribute constructor calls ValidationAttribute constructor) when all properties are null to + // preserve default error message. Reset the message accessor when just ErrorMessageResourceType is + // non-null to ensure correct InvalidOperationException. + if (!string.IsNullOrEmpty(attribute.ErrorMessage) || + !string.IsNullOrEmpty(attribute.ErrorMessageResourceName) || + attribute.ErrorMessageResourceType != null) + { + ErrorMessage = attribute.ErrorMessage; + ErrorMessageResourceName = attribute.ErrorMessageResourceName; + ErrorMessageResourceType = attribute.ErrorMessageResourceType; + } + } + + public string FormatErrorMessage(ClientModelValidationContext context) + { + var displayName = context.ModelMetadata.GetDisplayName(); + return string.Format(CultureInfo.CurrentCulture, + ErrorMessageString, + displayName, + GetOtherPropertyDisplayName(context)); + } + + private string GetOtherPropertyDisplayName(ClientModelValidationContext context) + { + // The System.ComponentModel.DataAnnotations.CompareAttribute doesn't populate the OtherPropertyDisplayName + // until after IsValid() is called. Therefore, by the time we get the error message for client validation, + // the display name is not populated and won't be used. + var metadata = context.ModelMetadata; + var otherPropertyDisplayName = OtherPropertyDisplayName; + if (otherPropertyDisplayName == null && metadata.ContainerType != null) + { + var otherProperty = context.MetadataProvider + .GetMetadataForProperty(() => metadata.Model, + metadata.ContainerType, + OtherProperty); + if (otherProperty != null) + { + return otherProperty.GetDisplayName(); + } + } + + return OtherProperty; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelClientValidationEqualToRule.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelClientValidationEqualToRule.cs new file mode 100644 index 0000000000..10d946699b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelClientValidationEqualToRule.cs @@ -0,0 +1,18 @@ +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// Represents client side validation rule that determines if two values are equal. + /// + public class ModelClientValidationEqualToRule : ModelClientValidationRule + { + private const string EqualToValidationType = "equalto"; + private const string EqualToValidationParameter = "other"; + + public ModelClientValidationEqualToRule([NotNull] string errorMessage, + [NotNull] object other) + : base(EqualToValidationType, errorMessage) + { + ValidationParameters[EqualToValidationParameter] = other; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs index 7d4618123b..97d038e5a3 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs @@ -63,7 +63,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Host var value = _resourceManager.GetString(name); System.Diagnostics.Debug.Assert(value != null); - + if (formatterNames != null) { for (var i = 0; i < formatterNames.Length; i++) diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Microsoft.AspNet.Mvc.ModelBinding.Test.kproj b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Microsoft.AspNet.Mvc.ModelBinding.Test.kproj index a9be9a260d..d4f3dc8e0b 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Microsoft.AspNet.Mvc.ModelBinding.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Microsoft.AspNet.Mvc.ModelBinding.Test.kproj @@ -19,6 +19,7 @@ + @@ -36,8 +37,10 @@ + + diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Properties/Resources.Designer.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..a127d68320 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Properties/Resources.Designer.cs @@ -0,0 +1,46 @@ +// +namespace Microsoft.AspNet.Mvc.ModelBinding.Test +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.Mvc.ModelBinding.Test.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// Comparing {0} to {1}. + /// + internal static string CompareAttributeTestResource + { + get { return GetString("CompareAttributeTestResource"); } + } + + /// + /// Comparing {0} to {1}. + /// + internal static string FormatCompareAttributeTestResource(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("CompareAttributeTestResource"), p0, p1); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Resources.resx b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Resources.resx new file mode 100644 index 0000000000..c393030af5 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Resources.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Comparing {0} to {1}. + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/CompareAttributeTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/CompareAttributeTest.cs new file mode 100644 index 0000000000..9fb88dcc7a --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/CompareAttributeTest.cs @@ -0,0 +1,106 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNet.Testing; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class CompareAttributeAdapterTest + { + [Fact] + [ReplaceCulture] + public void ClientRulesWithCompareAttribute_ErrorMessageUsesDisplayName() + { + // Arrange + var metadataProvider = new DataAnnotationsModelMetadataProvider(); + var metadata = metadataProvider.GetMetadataForProperty(() => null, typeof(PropertyDisplayNameModel), "MyProperty"); + var attribute = new CompareAttribute("OtherProperty"); + var context = new ClientModelValidationContext(metadata, metadataProvider); + var adapter = new CompareAttributeAdapter(attribute); + + // Act + var rules = adapter.GetClientValidationRules(context); + + // Assert + var rule = Assert.Single(rules); + Assert.Equal("'MyPropertyDisplayName' and 'OtherPropertyDisplayName' do not match.", rule.ErrorMessage); + } + + [Fact] + [ReplaceCulture] + public void ClientRulesWithCompareAttribute_ErrorMessageUsesPropertyName() + { + // Arrange + var metadataProvider = new DataAnnotationsModelMetadataProvider(); + var metadata = metadataProvider.GetMetadataForProperty(() => null, typeof(PropertyNameModel), "MyProperty"); + var attribute = new CompareAttribute("OtherProperty"); + var context = new ClientModelValidationContext(metadata, metadataProvider); + var adapter = new CompareAttributeAdapter(attribute); + + // Act + var rules = adapter.GetClientValidationRules(context); + + // Assert + var rule = Assert.Single(rules); + Assert.Equal("'MyProperty' and 'OtherProperty' do not match.", rule.ErrorMessage); + } + + [Fact] + public void ClientRulesWithCompareAttribute_ErrorMessageUsesOverride() + { + // Arrange + var metadataProvider = new DataAnnotationsModelMetadataProvider(); + var metadata = metadataProvider.GetMetadataForProperty(() => null, typeof(PropertyNameModel), "MyProperty"); + var attribute = new CompareAttribute("OtherProperty") + { + ErrorMessage = "Hello '{0}', goodbye '{1}'." + }; + var context = new ClientModelValidationContext(metadata, metadataProvider); + var adapter = new CompareAttributeAdapter(attribute); + + // Act + var rules = adapter.GetClientValidationRules(context); + + // Assert + var rule = Assert.Single(rules); + Assert.Equal("Hello 'MyProperty', goodbye 'OtherProperty'.", rule.ErrorMessage); + } + + [Fact] + public void ClientRulesWithCompareAttribute_ErrorMessageUsesResourceOverride() + { + // Arrange + var metadataProvider = new DataAnnotationsModelMetadataProvider(); + var metadata = metadataProvider.GetMetadataForProperty(() => null, typeof(PropertyNameModel), "MyProperty"); + var attribute = new CompareAttribute("OtherProperty") + { + ErrorMessageResourceName = "CompareAttributeTestResource", + ErrorMessageResourceType = typeof(Test.Resources), + }; + var context = new ClientModelValidationContext(metadata, metadataProvider); + var adapter = new CompareAttributeAdapter(attribute); + + // Act + var rules = adapter.GetClientValidationRules(context); + + // Assert + var rule = Assert.Single(rules); + Assert.Equal("Comparing MyProperty to OtherProperty.", rule.ErrorMessage); + } + + private class PropertyDisplayNameModel + { + [Display(Name = "MyPropertyDisplayName")] + public string MyProperty { get; set; } + + [Display(Name = "OtherPropertyDisplayName")] + public string OtherProperty { get; set; } + } + + private class PropertyNameModel + { + public string MyProperty { get; set; } + + public string OtherProperty { get; set; } + } + } +} \ No newline at end of file