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