Asp.Net MVC 3.0 Model Localization With RegularExp

2019-07-31 15:36发布

问题:

I've written a custom error message localization logic in my custom DataAnnotationsModelMetadataProvider class. It's working just fine with build-in StringLengthAttribute or RequiredAttribute validation error messages. But i am having trouble with my custom derived RegularExpressionAttribute classes. The logic i am using is something like below :

public class AccountNameFormatAttribute : RegularExpressionAttribute {
    public AccountNameFormatAttribute()
      : base(Linnet.Core.Shared.RegExPatterns.AccountNamePattern) {

    }   

    public override string FormatErrorMessage(string name) {
      return string.Format("{0} field must contain only letters, numbers or | . | - | _ | characters.", name);
    }
}

public class SignUpViewModel {
    [AccountNameFormat()]
    [StringLength(16, MinimumLength = 3)]
    [Required]
    [DisplayName("Account Name")]
    public string AccountName { get; set; }

    [Required]
    [DisplayName("Password")]
    [StringLength(32, MinimumLength = 6)]
    [DataType(System.ComponentModel.DataAnnotations.DataType.Password)]
    public string Password { get; set; }

    // .... and other properties, quite similar ... //
}

public class MvcDataAnnotationsModelValidatorProvider : DataAnnotationsModelValidatorProvider {

    protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes) {
      MyMvcController myMvcController = context.Controller as MyMvcController; /* custom mvc controller, that contains methods for wcf service activations and common properties. */
      if (myMvcController == null) {
        return base.GetValidators(metadata, context, attributes);
      }
      List<Attribute> newAttributes = new List<Attribute>();
      foreach (Attribute att in attributes) {
        if (att.GetType() != typeof(ValidationAttribute) && !att.GetType().IsSubclassOf(typeof(ValidationAttribute))) {
          // if this is not a validation attribute, do nothing.
          newAttributes.Add(att);
          continue;
        }
        ValidationAttribute validationAtt = att as ValidationAttribute;
        if (!string.IsNullOrWhiteSpace(validationAtt.ErrorMessageResourceName) && validationAtt.ErrorMessageResourceType != null) {
          // if resource key and resource type is already set, do nothing.
          newAttributes.Add(validationAtt);
          continue;
        }
        string translationKey = "MvcModelMetaData.ValidationMessages." + metadata.ModelType.Name + (metadata.PropertyName != null ? "." + metadata.PropertyName : string.Empty) + "." + validationAtt.GetType().Name;
        string originalText = validationAtt.FormatErrorMessage("{0}"); /* non-translated default english text */

        // clonning current attiribute into a new attribute 
        // not to ruin original attribute for later usage 
        // using Activator.CreateInstance and then mapping with AutoMapper inside..
        var newAtt = this.CloneValidationAttiribute(validationAtt);  


        // fetching translation from database via WCF service...
        // At this point, i can see error strings are always translated.
        // And it works perfect with [Required], [StringLength] and [DataType] attributes.
        // But somehow it does not work with my AccountNameFormatAttribute on the web page, even if i give it the translated text as expected..
        // Even if its ErrorMessage is already set to translated text, 
        // it still displays the original english text from the overridden FormatErrorMessage() method on the web page. 
        // It is the same both with client side validation or server side validation.
        // Seems like it does not care the ErrorMessage that i manually set.
        newAtt.ErrorMessage = myMvcController.Translations.GetTranslation(translationKey, originalText);

        newAttributes.Add(newAtt);
      }
      IEnumerable<ModelValidator> result = base.GetValidators(metadata, context, newAttributes);
      return result;
    }

    private ValidationAttribute CloneValidationAttiribute(ValidationAttribute att) {
      if (att == null) {
        return null;
      }
      Type attType = att.GetType();
      ConstructorInfo[] constructorInfos = attType.GetConstructors();
      if (constructorInfos == null || constructorInfos.Length <= 0) {
        // can not close..
        return att;
      }
      if (constructorInfos.Any(ci => ci.GetParameters().Length <= 0)) {
        // clone with no constructor paramters.
        return CloneManager.CloneObject(att) as ValidationAttribute;
      }

      // Validation attributes that needs constructor paramters...
      if (attType == typeof(StringLengthAttribute)) {
        int maxLength = ((StringLengthAttribute)att).MaximumLength;
        return CloneManager.CloneObject(att, maxLength) as StringLengthAttribute;
      }

      return att;
    }
}

public class CloneManager {

    public static object CloneObject(object input) {
      return CloneObject(input, null);
    }

    public static object CloneObject(object input, params object[] constructorParameters) {
      if (input == null) {
        return null;
      }
      Type type = input.GetType();
      if (type.IsValueType) {
        return input;
      }
      ConstructorInfo[] constructorInfos = type.GetConstructors();
      if (constructorInfos == null || constructorInfos.Length <= 0) {
        throw new LinnetException("0b59079b-3dc4-4763-b26d-651bde93ba56", "Object type does not have any constructors.", false);
      }
      if ((constructorParameters == null || constructorParameters.Length <= 0) && !constructorInfos.Any(ci => ci.GetParameters().Length <= 0)) {
        throw new LinnetException("f03be2b9-b629-4a72-b025-c7a87924d9a4", "Object type does not have any constructor without parameters.", false);
      }
      object newObject = null;
      if (constructorParameters == null || constructorParameters.Length <= 0) {
        newObject = Activator.CreateInstance(type);
      } else {
        newObject = Activator.CreateInstance(type, constructorParameters);
      }
      return MapProperties(input, newObject);
    }

    private static object MapProperties(object source, object destination) {
      if (source == null) {
        return null;
      }
      Type type = source.GetType();
      if (type != destination.GetType()) {
        throw new LinnetException("e67bccfb-235f-42fc-b6b9-55f454c705a8", "Use 'MapProperties' method only for object with same types.", false);
      }
      if (type.IsValueType) {
        return source;
      }
      var typeMap = AutoMapper.Mapper.FindTypeMapFor(type, type);
      if (typeMap == null) {
        AutoMapper.Mapper.CreateMap(type, type);
      }
      AutoMapper.Mapper.Map(source, destination, type, type);
      return destination;
    }
}

回答1:

Seems like my logic was actually an odd approach.

I've discovered making custom DataAnnotationsModelValidators for each type of validation attiributes. And then translating the ErrorMessages inside Validate() and GetClientValidationRules() methods.

public class MvcRegularExpressionAttributeAdapter : RegularExpressionAttributeAdapter {
    public MvcRegularExpressionAttributeAdapter(ModelMetadata metadata, ControllerContext context, RegularExpressionAttribute attribute)
      : base(metadata, context, attribute) {
    }

    public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() {
      return MvcValidationResultsTranslation.TranslateClientValidationRules(base.GetClientValidationRules(), this.Metadata, this.ControllerContext, this.Attribute);
    }

    public override IEnumerable<ModelValidationResult> Validate(object container) {
      return MvcValidationResultsTranslation.TranslateValidationResults(base.Validate(container), this.Metadata, this.ControllerContext, this.Attribute);
    }
}

 public class MvcValidationResultsTranslation {
    public static IEnumerable<ModelClientValidationRule> TranslateClientValidationRules(IEnumerable<ModelClientValidationRule> validationRules, ModelMetadata metadata, ControllerContext context, ValidationAttribute validationAttribute) {
      if (validationRules == null) {
        return validationRules;
      }
      MvcController mvcController = context.Controller as MvcController;
      if (mvcController == null) {
        return validationRules;
      }
      if (!string.IsNullOrWhiteSpace(validationAttribute.ErrorMessageResourceName) && validationAttribute.ErrorMessageResourceType != null) {
        // if resource key and resource type is set, do not override..         
        return validationRules;
      }
      string translatedText = GetTranslation(metadata, mvcController, validationAttribute);
      foreach (var validationRule in validationRules) {
        List<string> msgParams = new List<string>();
        msgParams.Add(!string.IsNullOrEmpty(metadata.DisplayName) ? metadata.DisplayName : metadata.PropertyName);
        if (validationRule.ValidationParameters != null) {
          msgParams.AddRange(validationRule.ValidationParameters.Where(p => p.Value.GetType().IsValueType || p.Value.GetType().IsEnum).Select(p => p.Value.ToString()));
        }
        validationRule.ErrorMessage = string.Format(translatedText, msgParams.ToArray());
      }
      return validationRules;
    }    

    public static IEnumerable<ModelValidationResult> TranslateValidationResults(IEnumerable<ModelValidationResult> validationResults, ModelMetadata metadata, ControllerContext context, ValidationAttribute validationAttribute) {
      if (validationResults == null) {
        return validationResults;
      }
      MvcController mvcController = context.Controller as MvcController;
      if (mvcController == null) {
        return validationResults;
      }
      if (!string.IsNullOrWhiteSpace(validationAttribute.ErrorMessageResourceName) && validationAttribute.ErrorMessageResourceType != null) {
        // if resource key and resource type is set, do not override..         
        return validationResults;
      }
      string translatedText = GetTranslation(metadata, mvcController, validationAttribute);
      List<ModelValidationResult> newValidationResults = new List<ModelValidationResult>();
      foreach (var validationResult in validationResults) {
        ModelValidationResult newValidationResult = new ModelValidationResult();
        newValidationResult.Message = string.Format(translatedText, (!string.IsNullOrEmpty(metadata.DisplayName) ? metadata.DisplayName : metadata.PropertyName));
        newValidationResults.Add(newValidationResult);
      }
      return newValidationResults;
    }
}


回答2:

You can use my Griffin.MvcContrib to get easier localization.

  1. Use nuget to download griffin.mvccontrib
  2. Define a string table as described here.
  3. Use the regular [RegularExpression] attribute directly in your view model.

Add this to your string table:

SignUpViewModel_AccountName_RegularExpression "{0} field must contain only letters, numbers or | . | - | _ | characters.

That's it..