3

Trying to implement data validation and error handling on a simple contact form. When I add the check for ModelState.IsValid I'm in a chicken and egg situation. I have looked at other similar questions and am just not getting this. Moving from Web Forms to MVC and struggling. Trying to toggle HTML elements based on what's happening - success/error message, etc. RIght now, not even the validation is working.

Right now I'm just trying to get server-side validation working but would welcome advice on how to add client-side validation also; for example, is it necessary to use jQuery for this or is there something baked in?

View:

@using (Html.BeginForm("Contact", "Home", FormMethod.Post))
{
    if (ViewData["Error"] == null && ViewData["Success"] == null)
    {
        <h3>Send us an email</h3>
        Html.ValidationSummary(true);
        <div class="form-group">
            <label class="sr-only" for="contact-email">Email</label>
            <input type="text" name="email" placeholder="Email..."   
                class="contact-email" id="contact-email">
        </div>
        <div class="form-group">
            <label class="sr-only" for="contact-subject">Subject</label>
            <input type="text" name="subject" placeholder="Subject..."
                class="contact-subject" id="contact-subject">
        </div>
        <div class="form-group">
             <label class="sr-only" for="contact-message">Message</label>
             <textarea name="message" placeholder="Message..." 
                 class="contact-message" id="contact-message"></textarea>
        </div>
        <button type="submit" class="btn">Send it</button>
        <button type="reset" class="btn">Reset</button>
    }   
    else if (ViewData["Error"] == null && ViewData["Success"] != null)
    {
        <h4>We will get back to you as soon as possible!</h4>
        <p>
            Thank you for getting in touch with us. If you do not hear 
            from us within 24 hours, that means we couldn't contact you 
           at the email provided. In that case, please feel free to call 
           us at (xxx) xxx-xxxx at any time.
        </p>
    }
    else if (ViewData["Error"] != null)
    {
        <h3>Oops!</h3>
        <p>
            We apologize. We seem to be having some problems.
        </p>
        <p>
            Please come back and try again later. Alternatively, 
            call us anytime at (xxx) xxx-xxxx.
        </p>
    } 
}

Model:

public class ContactModel
{
    [Required(ErrorMessage = "Email address is required")]
    [EmailAddress(ErrorMessage = "Invalid Email Address")]
    public string Email { get; set; }

    [Required(ErrorMessage = "Subject is required")]
    public string Subject { get; set; }

    [Required(ErrorMessage = "Message is required")]
    public string Message  { get; set; }
}

Controller:

[HttpPost]        
public ActionResult Contact(ContactModel contactModel)
{
    if (ModelState.IsValid)
    {
        try
        {
            MailMessage message = new MailMessage();

            using (var smtp = new SmtpClient("mail.mydomain.com"))
            {
                // Standard mail code here

                ViewData["Success"] = "Success";
             }
         }
         catch (Exception)
         {
             ViewData["Error"] 
                 = "Something went wrong - please try again later.";
                    return View("Error");
         }
     }
     return View();
}

Error View:

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Error</title>
</head>
<body>
    <hgroup>
        <h1>Error.</h1>
        <h2>An error occurred while processing your request.</h2>
    </hgroup>
</body>
</html>

UPDATE - 05/09/2017

Per Guruprasad's answer, if ModelState.IsValid evaluates to false, then no validation error messages are being reported on the form.

Note I had to change the AddModelError signature to not use the "Extension ex" parameter:ModelState.AddModelError("Error", "Server side error occurred"); as I do not want system errors being reported to users.

Note also that at this point I am only trying out validation on the server side (have yet to work through client-side validation). I have updated the Contact.cshtml view as follows as no model errors were being displayed - I have included the Bootstrap .has-error and .help-block CSS rules for the validation errors:

@using (Html.BeginForm("Contact", "Home", FormMethod.Post))
{
    <h3>Send us an email</h3>
    Html.ValidationSummary(true);
    <div class="form-group has-error">
        <label class="sr-only" for="contact-email">Email</label>
        @Html.TextBoxFor(m => m.Email, new { type = "text", name = "email",
            placeholder = "Email..", @class = "contact-email" })
        @Html.ValidationMessageFor(model => model.Email, String.Empty, 
            new { @class="help-block" })
    </div>
    <div class="form-group has-error">
        <label class="sr-only" for="contact-subject">Subject</label>
        @Html.TextBoxFor(m => m.Subject, new { type = "text", 
            name = "subject", 
            placeholder = "Subject..", @class = "contact-subject" })
        @Html.ValidationMessageFor(model => model.Subject, String.Empty, 
            new { @class = "help-block" })
    </div>
    <div class="form-group has-error">
        <label class="sr-only" for="contact-message">Message</label>
        @Html.TextAreaFor(m => m.Message, new { name = "message", 
            placeholder = "Message..", @class = "contact-message" })
        @Html.ValidationMessageFor(model => model.Message, String.Empty, 
            new { @class = "help-block" })
    </div>
    <button type="submit" class="btn">Send it</button>
    <button type="reset" class="btn">Reset</button>

    if (ViewData["Success"] != null)
    {
        <h4>We will get back to you as soon as possible!</h4>
        <p>
            Thank you for getting in touch with us. If you do not hear 
            from us within 24 hours, that means we couldn't contact you 
            at the email provided. In that case, please feel free to 
            call us at (xxx) xxx-xxxx at any time.
        </p>
    }
}
IrishChieftain
  • 15,108
  • 7
  • 50
  • 91
  • Have you added the jquery validation scripts? – Bat_Programmer Jan 17 '17 at 04:23
  • Yes, as part of the project template. It's version 1.11.1 - should I update? – IrishChieftain Jan 17 '17 at 11:04
  • 1
    Did you debug n see what error was existing when you had `ModelState.IsValid` as false? Also, if you do not want system errors to be reported to user, then you can configure that in web.config files where you can have your own custom views for different error codes.. Also on the other hand, Please look at `ExceptionFilter` which you can extend to display different custom views for different error types.. Let me know if you face any issues on this.. – Guruprasad J Rao May 10 '17 at 04:19
  • Thanks for the feedback, I got the model errors to show after adding the @Html.ValidationMessageFor's. – IrishChieftain May 10 '17 at 06:23

1 Answers1

10

There are multiple things you need to understand here. Let me go point by point.

  • Its good to know that you have your model designed, but how your view gets to know that it has a model to bind for itself and when posting the form contents, how would server comes to know that, there is a model to be received. So on the first instance, you need to construct your view binding the model. To bind a model in a view, you need to first get a reference/declare it at the top, letting view know that, ok, here is a model for you to generate my view.

  • Well, you have ValidationSummary to true, then I would suggest that, instead of using ViewData to pass error message, you can use ModelState.AddModelError and let ValidationSummary take care of that. As a side note, you might also want to take care of this issue and you can resolve the same with answers mentioned in the same post. If you are not using or do not want to use Html.ValidationSummary, then you can stick to your current view.

  • Now, to display Success message, you can either use TempData or ViewData and follow the same structure as you have in your view now. Here is one more post to let you work on that.

  • Last and most important on View part is binding model properties to View elements. Use Razor View extension helpers to generate View for your model. You have @Html.TextBoxFor,@Html.TextAreaFor etc., You also have @Html.TextBox, @Html.TextArea which is not for binding model properties, but just to generate a plain HTML view. You can add other html properties within these helpers as shown in the updated view below. I would suggest to dig down more on the overloads available for these helpers.

So here is your updated view.

@model SOTestApplication.Models.ContactModel   @*getting model reference*@

@using (Html.BeginForm("Contact", "Home", FormMethod.Post))
{
    <h3>Send us an email</h3>
    Html.ValidationSummary(true);
    <div class="form-group">
        <label class="sr-only" for="contact-email">Email</label>
        @Html.TextBoxFor(m => m.Email, new { type = "text", name = "email", placeholder = "Email..", @class = "contact-email" })
        @*Usage of helpers and html attributes*@
    </div>
    <div class="form-group">
        <label class="sr-only" for="contact-subject">Subject</label>
        @Html.TextBoxFor(m => m.Subject, new { type = "text", name = "subject", placeholder = "Subject..", @class = "contact-subject" })
    </div>
    <div class="form-group">
        <label class="sr-only" for="contact-message">Message</label>
        @Html.TextAreaFor(m => m.Message, new { name = "message", placeholder = "Message..", @class = "contact-message" })
    </div>
    <button type="submit" class="btn">Send it</button>
    <button type="reset" class="btn">Reset</button>
}
if (ViewData["Success"] != null)
{
    <h4>We will get back to you as soon as possible!</h4>
    <p>
        Thank you for getting in touch with us. If you do not hear
        from us within 24 hours, that means we couldn't contact you
        at the email provided. In that case, please feel free to call
        us at (xxx) xxx-xxxx at any time.
    </p>
}

Controller Side validation

Not much to say on this part as it looks good. But based on few of my points above, I would suggest you to add ModelState.AddModelError instead of using ViewData for error messages. Eliminate your if conditions in view, so that contact form remains, even after postback. Now if you want to persist the values after server side validation, then just pass back the model to your view in your post method. Updated Controller would be:

[HttpPost]
public ActionResult Contact(ContactModel contactModel)
{
     if (ModelState.IsValid)
     {
          try
          {
              MailMessage message = new MailMessage();
              using (var smtp = new SmtpClient("mail.mydomain.com"))
              {
                  // Standard mail code here
                  ViewData["Success"] = "Success";
              }
          }
          catch (Exception)
          {
              ModelState.AddModelError("Server side error occurred", ex.Message.ToString());
          }
     }
     return View(contactModel); //this will persist user entered data on validation failure
}

Client Side Validation

As far as this portion is considered, you have few more things to set up in your application.

  • You need to add Html.EnableClientValidation(true); and Html.EnableUnobtrusiveJavaScript(true); to your application. There are various possible ways to add this. You can add this on Web.config file under appSettings for global implication Or you can add this in particular view as mentioned in below updated View example.

Global Implication in Web.Config ex:

<appSettings>
  <add key="ClientValidationEnabled" value="true" />
  <add key="UnobtrusiveJavaScriptEnabled" value="true" />
</appSettings>
  • If you have noticed your BundleConfig.cs file under App_Start directory, you would have seen below entries created by default. These are the jquery stuffs responsible for your Client Side validation.

jQuery and jQueryVal entries

bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                    "~/Scripts/jquery-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
                    "~/Scripts/jquery.unobtrusive*",
                    "~/Scripts/jquery.validate*"));
  • Next Step is to add reference to these files/use @section Scripts to render these bundles either in _Layout.cshtml or in any specific view. When you include this in _Layout.cshtml. these scripts/bundles are rendered wherever you use this layout with other views. So basically, its your call on where to render these.

For example here, I would render these in Contact.cshtml view soon after adding reference to model.

@section Scripts
{
    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/jqueryval")
}
  • One Last thing to make this work here is that you need to use @Html.ValidationMessageFor razor extension and let MVC do the binding of error messages on particular properties. Also for these error messages to be displayed in the View, you need to specify ErrorMessage for each property in your model as you are doing it now with Required(ErrorMessage=... for each properties in model. There are more to know about these stuffs if you explore it in detail.

Your updated view with proper validations added.

@model SOTestApplication.Models.ContactModel
@section Scripts
{
    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/jqueryval")
}
@using (Html.BeginForm("Contact", "Contacts", FormMethod.Post))
{

    <h3>Send us an email</h3>
    Html.ValidationSummary(true);
    Html.EnableClientValidation(true);
    Html.EnableUnobtrusiveJavaScript(true);
    <div class="form-group">
        <label class="sr-only" for="contact-email">Email</label>
        @Html.TextBoxFor(m => m.Email, new { type = "text", name = "email", placeholder = "Email..", @class = "contact-email" })
        @Html.ValidationMessageFor(m => m.Email)
    </div>
    <div class="form-group">
        <label class="sr-only" for="contact-subject">Subject</label>
        @Html.TextBoxFor(m => m.Subject, new { type = "text", name = "subject", placeholder = "Subject..", @class = "contact-subject" })
        @Html.ValidationMessageFor(m => m.Subject)
    </div>
    <div class="form-group">
        <label class="sr-only" for="contact-message">Message</label>
        @Html.TextAreaFor(m => m.Message, new { name = "message", placeholder = "Message..", @class = "contact-message" })
        @Html.ValidationMessageFor(m => m.Message)
    </div>
    <button type="submit" class="btn">Send it</button>
    <button type="reset" class="btn">Reset</button>
    if (ViewData["Success"] != null)
    {
        <h4>We will get back to you as soon as possible!</h4>
        <p>
            Thank you for getting in touch with us. If you do not hear
            from us within 24 hours, that means we couldn't contact you
            at the email provided. In that case, please feel free to call
            us at (xxx) xxx-xxxx at any time.
        </p>
    }
}

Hope I have clarified most of your doubts with these points. Happy Coding.. :)

Community
  • 1
  • 1
Guruprasad J Rao
  • 29,410
  • 14
  • 101
  • 200
  • 2
    Very, very thorough answer. Thanks so much for the effort. Will attempt this later today :) – IrishChieftain Jan 17 '17 at 11:05
  • @IrishChieftain Sure please.. Happy coding.. :) – Guruprasad J Rao Jan 17 '17 at 12:26
  • 1
    Hi Guruprasad, I had to update the question as your solution wasn't working for me - still working on server-side only. – IrishChieftain May 09 '17 at 19:31
  • I got the success message rendering but the form fields (populated) are still appearing on post back, above the success message? I took the if(ViewDate[]) code block out of the using block (per your example) and pre-pended it with "@". I'm still getting the populated form fields rendering with the success message on post back... – IrishChieftain May 13 '17 at 01:06
  • Just `null` the respective model buddy or just initialize with `new model()` once it is `success`.. As in, after `ViewData["Success"] = "Success";` just add `contactModel=new ContactModel()`.. that's it.. – Guruprasad J Rao May 13 '17 at 05:41
  • 1
    That had no effect, but I got it working by putting an if/else around the entire using{} block in the View. Thanks for your help! :) – IrishChieftain May 13 '17 at 10:44
  • 1
    Anytime buddy.. Happy coding.. :) – Guruprasad J Rao May 13 '17 at 11:19
  • This reply was useful, but still I have a problem when the fields in the form are dinamyc, or let´s say, if some fields are part of a partial view, and others are inside the form. Even If I don't want to validate partial view, the form submits instead of validating (but I can see validation messages though) – Yogurtu Nov 16 '18 at 20:50