0

In my class, I have a property for a file attachment like so...

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; }
}

I don't see anything wrong with my regex, but I always get ModelState.IsValid false. This seems pretty trivial and simple regex, am I missing something? Do I need to write my own custom validation?

I'm populating AttachmentTrace via a regular input of type file:

<div class="editor-label">
    @Html.LabelFor(model => model.AttachmentTrace)
</div>
<div class="editor-field">
    @Html.TextBoxFor(model => model.AttachmentTrace, new { type = "file" })
    @Html.ValidationMessageFor(model => model.AttachmentTrace)
</div>

The action method is just a regular action:

public ActionResult Create(Certificate certificate, HttpPostedFileBase attachmentTrace, HttpPostedFileBase attachmentEmail)
    {
        if (ModelState.IsValid)
        {
            // code ...
        }
        return View(certificate);
    }
sammydc
  • 514
  • 1
  • 4
  • 12
  • how are you populating AttachmentTrace? – Forty-Two Mar 12 '13 at 19:34
  • Forty-Two, I populate it via a regular input of type file (just added some code above). Thanks. – sammydc Mar 12 '13 at 20:16
  • Well, the way you are doing that I would guess that the value of AttachmentTrace is not what you expect (the file name) but rather something like "System.Web.HttpPostedFileWrapper" instead. – Forty-Two Mar 12 '13 at 20:47
  • What does your Action method look like? – Erik Funkenbusch Mar 12 '13 at 20:48
  • You could simplify that extension bit as `[xX][lL][sS][xX]?`, or better yet, use the inline case-insensitive modifier: `(?i)^.*\.xlsx?$`. – Kenneth K. Mar 12 '13 at 20:59
  • Mystere Man, the action method is just a regular action (I put the code above). Thanks for that improved regex Kenneth. Forty-Two, that's also what I suspected. I looked at it closely and I see that the client-side validation succeeds, but as soon as the form is posted, I get the error message as I specified in my model validation. – sammydc Mar 12 '13 at 21:10
  • What happens if you accommodate arbitrary whitespace at the end of the string? `@"^.*\.(xlsx|xls|XLSX|XLS)\w*$` – w.brian Mar 12 '13 at 21:16
  • @sammydc - You are posting the data, correct? does the data get populated in the field, but still fails validation? – Erik Funkenbusch Mar 12 '13 at 22:44
  • After much investigation on this issue, I think there is a problem with the property type of 'string' for the AttachmentTrace. It should be 'HttpPostedFileBase', but the problem now is that when I change the type to HttpPostedFileBase, the code-first migration gives me this error message: "EntityType 'HttpPostedFileBase' has no key defined. Define the key for this EntityType." I'm thinking of going to the route of creating a separate ViewModel just to be able to specify a type of 'HttpPostedFileBase'. – sammydc Mar 12 '13 at 22:58
  • @sammydc - that's why you're not supposed to use entities in your view. You use view models, which do not have to conform to the requirements of your data model. – Erik Funkenbusch Mar 12 '13 at 23:03
  • @MystereMan, thanks for the reminder. I realize that now, this application I'm building is a simple quick app that I didn't want to complicate things. But looks like, I'm gonna have to do that now. – sammydc Mar 12 '13 at 23:15
  • @sammydc - I find that I am invariably bitten like that if I take shortcuts.. – Erik Funkenbusch Mar 12 '13 at 23:19
  • @MystereMan, yup, I agree. Most of the time you can get away with it. But sometimes you get bit quite painfully. In my case, I'm almost done with my dev, but just one todo item, which is supposedly to fix the regex for this validation. But as it turned out, I still have a lot more code to write and to test. Anyways, I got this working now, finally getting ModelState.IsValid true, yay! I will post the answer to this in a moment. Thank you all for your input! – sammydc Mar 12 '13 at 23:22

1 Answers1

1

Ok, here's the solution I found. I'm sure there are other solutions out there. First a little background, because my application uses EF code-first migration, specifying a HttpPostedFileBase property type in my model, produces this error when adding migration:

One or more validation errors were detected during model generation: System.Data.Entity.Edm.EdmEntityType: : EntityType 'HttpPostedFileBase' has no key defined. Define the key for this EntityType. \tSystem.Data.Entity.Edm.EdmEntitySet: EntityType: EntitySet 'HttpPostedFileBases' is based on type 'HttpPostedFileBase' that has no keys defined.

So I really had to stick with using a string type for the AttachmentTrace property.

The solution is to employ a ViewModel class like this:

public class CertificateViewModel {
    // .. other properties
    [Required]
    [FileTypes("xls,xlsx")]
    public HttpPostedFileBase AttachmentTrace { get; set; }
}

Then create a FileTypesAttribute like so, I borrowed this code from this excellent post.

public class FileTypesAttribute : ValidationAttribute {
    private readonly List<string> _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));
    }
}

In the controller Action, I needed to make a change to use the ViewModel instead, then map it back to my Entity using AutoMapper (which is excellent by the way):

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.CreateMap<HttpPostedFileBase, string>().ConvertUsing(new HttpPostedFileBaseTypeConverter());
            Mapper.CreateMap<CreateCertificateViewModel, Certificate>();
            Certificate myCert = Mapper.Map<CreateCertificateViewModel, Certificate>(certificate);
            // other code ...
        }
        return View(myCert);
    }

For the AutoMapper, I created my own TypeConverter for the HttpPostedFileBase as follows:

public class HttpPostedFileBaseTypeConverter : ITypeConverter<HttpPostedFileBase, string> {

    public string Convert(ResolutionContext context) {
        var fileBase = context.SourceValue as HttpPostedFileBase;
        if (fileBase != null) {
            return fileBase.FileName;
        }
        return null;
    }
}

That's it. Hope this helps out others who may have this same issue.

sammydc
  • 514
  • 1
  • 4
  • 12