I was having issues with model validation using RegularExpression as indicated in my class below, the property AttachmentTrace is a file attachment that is uploaded during form post.
public class Certificate { [Required] // TODO: Wow looks like there's a problem with using regex in MVC 4, this does not work! [RegularExpression(@"^.*\.(xlsx|xls|XLSX|XLS)$", ErrorMessage = "Only Excel files (*.xls, *.xlsx) files are accepted")] public string AttachmentTrace { get; set; } }All the while I thought there's something wrong with my Regex. But after looking closely on this issue, I found out that what is being validated on the server side is the string "System.Web.HttpPostedFileWrapper", and not the actual filename. That's the reason why ModelState.IsValid returns a false everytime, no matter how right the regex is. But I cannot simply switch the property type of string to HttpPostedFileBase in my model, because I'm using EF code-first migration, which will result into an unpleasant error message when adding a migration.
So the solution to this is to employ a ViewModel, instead of the Entity directly:
public class CertificateViewModel { // .. other properties [Required] [FileTypes("xls,xlsx")] public HttpPostedFileBase AttachmentTrace { get; set; } }The next step is to create a custom ValidationAttribute for the FileTypes:
public class FileTypesAttribute : ValidationAttribute { private readonly ListIn the controller Action, use the ViewModel instead of the Entity, then map the ViewModel back to the Entity via AutoMapper:_types; public FileTypesAttribute(string types) { _types = types.Split(',').ToList(); } public override bool IsValid(object value) { if (value == null) return true; var postedFile = value as HttpPostedFileBase; var fileExt = System.IO.Path.GetExtension(postedFile.FileName).Substring(1); return _types.Contains(fileExt, StringComparer.OrdinalIgnoreCase); } public override string FormatErrorMessage(string name) { return string.Format("Invalid file type. Only {0} are supported.", String.Join(", ", _types)); } }
public ActionResult Create(CertificateViewModel certificate, HttpPostedFileBase attachmentTrace, HttpPostedFileBase attachmentEmail) { if (ModelState.IsValid) { // Let's use AutoMapper to map the ViewModel back to our Certificate Entity // We also need to create a converter for type HttpPostedFileBase -> string Mapper.CreateMapFor the AutoMapper, create a TypeConverter for HttpPostedFileBase:().ConvertUsing(new HttpPostedFileBaseTypeConverter()); Mapper.CreateMap (); Certificate myCert = Mapper.Map (certificate); // other code ... } return View(myCert); }
public class HttpPostedFileBaseTypeConverter : ITypeConverterI know, it's a lot of work, but this will ensure the file extension validation will be done correctly. But wait, this is not yet complete, I need to also make sure that the client side validation will work for this and that will be the topic of my next post.{ public string Convert(ResolutionContext context) { var fileBase = context.SourceValue as HttpPostedFileBase; if (fileBase != null) { return fileBase.FileName; } return null; } }
No comments:
Post a Comment