Joost
Joost

Reputation: 394

Return current page with an error message

I have an MVC application where the business layer throws an exception in case of errors. For example when someone tries to register with an email adress that is already registered in our database. For specific exceptions, I want to re-render the current view (with the data filled in by the user), and show an error message on top of the page.

Instead of adding try/catch statements to all controller actions, I've created a class that is derived from HandleErrorAttribute, to catch all exceptions on a central place. In this class, I render the current view by returning a new ViewResult, and copying the ViewData:

public class CustomHandleErrorAttribute : HandleErrorAttribute
{
  public override void OnException(ExceptionContext filterContext)
  {
     filterContext.Result = ShowErrorMessage("Error occured", filterContext);
     filterContext.ExceptionHandled = true;

     base.OnException(filterContext);
  }

  private static ActionResult ShowErrorMessage(string message, ExceptionContext filterContext)
  {
     filterContext.Controller.ViewBag.ErrorMessage = message;
     return new ViewResult { ViewData = filterContext.Controller.ViewData };
  }
}

Unfortunately, the ViewData doesn't contain the complete model. For example, fields that are marked as disabled, are not posted back to the controller. So when I render the view again, some fields will be empty.

Am I missing something here? Or should I go in a different direction to show the current page again, with an error message?

Upvotes: 1

Views: 2842

Answers (2)

Shyju
Shyju

Reputation: 218812

You should not be using Exceptions for control flow! That is a bad practice.

For your specific use case,(when someone tries to register with an email which was already registered), You can create a custom validation attribute and use the Model validation framework available in MVC.

public class UniqueEmail : ValidationAttribute
{
    public override string FormatErrorMessage(string name)
    {
        return $"This email ({name}) is already registered";
    }
    protected override ValidationResult IsValid(object objValue,ValidationContext context)
    {
        var email = objValue as string;

        int userId= Int32.MinValue;
        var idProperty = context.ObjectType.GetProperty("Id");
        if (idProperty != null)
        {
            var idValue = idProperty.GetValue(context.ObjectInstance, null);
            if (idValue != null)
            {
                userId = (int) idValue;
            }
        }

        var userRepository = new UserRepository();

        var e = userRepository.GetUser(email);

        if (e!=null && userId!=e.Id)
        {
            return new ValidationResult(FormatErrorMessage(email));
        }
        return ValidationResult.Success;
    }
}

Here in this validation attribute, i read the value of the property on which this attribute is applied (in your case, it will be email), query the database to see whether we have a record exist with that email and if yes, we will compare the Id property value of that user record is different than the current view model's Id property value (this is to handle editing of the user record).

In the above code, I am using UserRepository which has a GetUser method to check whether we already have a record with this email in the system. Just update that part with however you want to check it with your data access code.

Now all you have to do is, decorate your view model with this attribute

public class  CreateUserVm
{
   public int Id { set;get;}

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

   public string Name { set;get; }
}

And in your HttpPost action method, simply check ModelState is valid before saving data

[HttpPost]
public ActionResult Create(CreateUserVm model)
{
   if(ModelState.IsValid) 
   {
      // all good. Save
      return RedirectToAction("Index");     
   }
   return View(model);
}

When model validation fails, it will render the validation errors in your view, assuming you have the validation helpers in your view.

@using (Html.BeginForm("Create", "Home", FormMethod.Post))
{
    @Html.ValidationSummary("", true)

    @Html.TextBoxFor(f => f.Email)
    @Html.ValidationMessageFor(model => model.Email)

   @Html.HiddenFor(a=>a.Id)
   <input type="submit"  />
}

Upvotes: 0

Grizzly
Grizzly

Reputation: 5953

You should definitely go in a different direction for handling these kind of situations. MVC already provides a much easier way to display errors to the user via the ModelState.

I'm going to provide an example which is 100% an assumption of what you might have.. since zero code was provided in the question as to how your controller and view currently look.

For example:

In your controller (I assume you're talking about the Create ActionResult).

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Property1, Property2, Property3, EmailAddress")] Object myObject)
{
    if(ModelState.IsValid)
    {
        if(db.TableName.Any(x => x.Email.Equals(myObject.EmailAddress, StringComparison.CurrentCultureIgnoreCase)
        {
            ModelState.AddModelError("EmailAddress", "This Email Already Exists!");
            return View(myObject);
        }

        /* otherwise continue */
    }
}

Then you said that you want to display that error message at the top of the view.

So, in your view you have a line that looks like this:

@Html.ValidationSummary(true, "", new { @class = "text-danger" })

Typically, the first parameter is always true because by default you have lines like this:

@Html.ValidationMessageFor(model => model.EmailAddress, "", new { @class = "text-danger" })

that reside under their corresponding text boxes. So, if you want to display errors at the top of the page, make sure that the ValidationSummary line is at the top of your form, and change true to false.

Let me know if this helps!

Upvotes: 1

Related Questions