Month: May 2013

Create a dropdown list from an Enum in Asp.Net MVC

A common situation you can find in MVC, is to have an Enum in your model and want that rendered as a dropdown list in the browser.

If you are lucky enough to be using MVC5.1 (or higher), I’d recommend you look at this post that shows how to do this using the new Enum Support that’s part of MVC5.1. If not, then read on.

There are a number of ways of solving this, Currently on StackoverFlow there are 20 answers to this question, but none of them consider what I consider the more elegant solution of using templating.

For anyone not familiar with templating, I’d recommend reading Brad Wilson’s excellent blog on it. The following post is how I like to do it, using Asp.Net MVC with C# and the Razor view engine.

For a model that looks like this

public class PersonViewModel
{
   [Display(Name="First Name")]
   public string FirstName { get; set; }

   [Display(Name = "Last Name")]
   public string LastName { get; set; }

   [Display(Name = "Airline Preference")]
   public AirlinePreference? AirlinePreference { get; set; }
}

With our Enum defined as:

  public enum AirlinePreference
    {
        BritishAirways,
        EasyJet,
        RyanAir,
        Virgin
    }

Wouldn’t it be cool to have view that looks like this

Person Final Page

And for the view to have just one line to render the form content:

@Html.EditorForModel()

This post explains one way of achieving this.

The model should be easy enough to read. The controller action is also, so boring, it’s barely worth mentioning, but here it is:

[HttpGet]
public ActionResult Add()
{
    var viewModel = new PersonViewModel();

    return View(viewModel);
}

Enum Template

Out of the box, MVC will render a textbox if the model type is type string and your View for that element is @Html.EditorFor(x => x.FirstName) (for our model). However, it doesn’t know how to render an Enum, so defaults it to being a textbox. For our model, this would result in a view like this

DefaultEditPage

We need to provide a template for MVC to handle Enums in order for it to render them as a dropdown list. We could provide a template whose name matches our Enum (AirlinePreference) and in that view render our dropdown. We could also supply a UIHint attribute on our model property, to help MVC find our template. I prefer to alter the string template, to look for a property type of Enum and render all Enums in my models by the same piece of code without having to be explicit about it with each Enum in each model. This helps have a consistent way of rendering an Enum the same way on all pages with the least amount of code.

We need to provide the string template, and its important we put it in the right folder. As I want all Enums to use this by default, I’m putting mine here:

/Views/Shared/EditorTemplates/

String.cshtml template

@using MVCProject.Extensions

@{
    var type = Nullable.GetUnderlyingType(ViewData.ModelMetadata.ModelType) ?? ViewData.ModelMetadata.ModelType;

    @(typeof (Enum).IsAssignableFrom(type) ? Html.EnumDropDownListFor(x => x) : Html.TextBoxFor(x => x))
}

The first line gets the type of our property, taking care to cater for a value that may be null. The second line will render a EnumDropDownListFor for an Enum or a TextBoxFor for a (non) Enum, e.g. a string.

EnumDropDownListFor

EnumDropDownListFor is an HtmlHelper extension. Here it is

public static IHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> html, Expression<Func<TModel, TEnum>> expression)
{
    var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);

    var enumType = Nullable.GetUnderlyingType(metadata.ModelType) ?? metadata.ModelType;

    var enumValues = Enum.GetValues(enumType).Cast<object>();

    var items = from enumValue in enumValues
                select new SelectListItem
                {
                    Text = enumValue.ToString(),
                    Value = ((int)enumValue).ToString(),
                    Selected = enumValue.Equals(metadata.Model)
                };

    return html.DropDownListFor(expression, items, string.Empty, null);
}

First couple of lines determine the enum type (taking care of the null case too). Then we get an array of Enum values and cast it into a enumerable collection of objects. From here we can easily convert that to an enumerable collection of SelectListItems with a linq query. Once we have the collection of SelectListItems, it’s passed into the DropDownListFor helper to render it as a select list for the client.

In the browser our page now looks like

Person Edit Page, non formattedMuch improved, but not very user friendly to see BrittishAirways, displayed with no space in between.

Format DropDownList Values

We could add a Description Attribute or a Display Attribute to the enum values and update the linq query to display that instead of enumValue.ToString().
However, I’m going to go with a convention over code style approach and use a resx file to store the value of what we want to render to the screen. Also, as we are using a resx file to store the value that get’s displayed to the user we can easily make this support more than one language.
Let’s create a resx file and call it Enums.resx. And add some values in that look like:

EnumsSo, here our convention is the names of the resources follow the pattern
[Enum Name]_[Enum Value]

The final thing to do is update the EnumDropDownFor extension to use these resource values, instead of the enum value.

public static IHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> html, Expression<Func<TModel, TEnum>> expression)
{
    var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);

    var enumType = Nullable.GetUnderlyingType(metadata.ModelType) ?? metadata.ModelType;

    var enumValues = Enum.GetValues(enumType).Cast<object>();

    var items = from enumValue in enumValues
                select new SelectListItem
                {
                    Text = GetResourceValueForEnumValue(enumValue),
                    Value = ((int)enumValue).ToString(),
                    Selected = enumValue.Equals(metadata.Model)
                };

    return html.DropDownListFor(expression, items, string.Empty, null);
}

private static string GetResourceValueForEnumValue<TEnum>(TEnum enumValue)
{
    var key = string.Format("{0}_{1}", enumValue.GetType().Name, enumValue);

    return Enums.ResourceManager.GetString(key) ?? enumValue.ToString();
}

We’ve added the static method GetResourceValueForEnumValue, which simply creates a key, based on our convention of [Enum Name]_[Enum Value] and then uses the ResourceManager to lookup the corresponding value from the Enums resource.

Finally, earlier I mentioned that I was using @Html.EditorForModel to create the form in the view. You could also call @Html.EditorFor(x => x.AirlinePreference) if you need to have more control over where to place the dropdown on your form. You can also explicitly call the EnumDropDownListFor of course.

All source code available to download here:
https://github.com/paulthecyclist/EnumBlog

 

Log All Asp.Net MVC Errors

Last year I wrote a post, Catch All WCF Errors, showing how you can implement  IErrorHandler to log and process all errors in a WCF service. This post is how to achieve this in a Asp.Net MVC web site.

HandleErrorAttribute

If you are using MVC3 or greater, your default project is already setup to use the HandleErrorAttribute.

In the global.asax file, you should see a call to RegisterGlobalFilters. This will call out to a static method for registering global filters for our MVC application. By default it will look like this

public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
        }
    }

What does the HandleError filter do?

When registered globally it handles all errors that may occur in your application. It then  redirects them to a view called Error, located in the shared views folder. It will only do this if <customErrors> is turned on in the web.config

What doesn’t HandleError do?

  • It doesn’t log errors.
  • It only handles http errors, of 500.

IExceptionFilter

 public interface IExceptionFilter
        {
            void OnException(ExceptionContext filterContext);
        }

This interface exposes just one method, OnException, which is called when ever an unhandled exception is thrown.

All exception filters implement this filter, including the HandleErrorAttribute. By applying the HandleErrorAttribute filter at a global level, all errors will be handled by it. So, in order to extend our application to also log errors, you could either create a class that inherits from the HandleErrorAttribute and override its OnException method to also perform logging, or you could create another filter that implements IExceptionFilter and register it globally. I prefer the latter option, as I think its implementation is cleaner.

Create an Exception Logger

public class ExceptionLoggingFilter : IExceptionFilter
    {
        private readonly ILog _logger;

        public ExceptionLoggingFilter(ILog logger)
        {
            _logger = logger;
        }

        public virtual void OnException(ExceptionContext filterContext)
        {
            _logger.Error(filterContext.Exception);
        }

        public interface IExceptionFilter
        {
            void OnException(ExceptionContext filterContext);
        }
    }

The ExceptionLoggingFilter above, uses log4Net as its logger and very simply will log all exceptions that occur in a MVC web application.

To register it we need to update the RegisterGlobalFilters method, to look like this

public class FilterConfig
{
   public static void RegisterGlobalFilters(GlobalFilterCollection filters, ILog logger)
   {
      filters.Add(new HandleErrorAttribute());
      filters.Add(new ExceptionLoggingFilter(logger));
   }
}

As you can see we are now adding our ExceptionLoggingFilter to the GlobalFilters collection and passing an instance of the log4Net logger into the RegisterGlobalFilters method.

Finally, our Application_Start method needs a minor update to register log4Net and pass it into the RegisterGlobalFilters method

protected void Application_Start()
{
   XmlConfigurator.Configure();
   var logger = LogManager.GetLogger(&quot;MyLogger&quot;);

   FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters, logger);

   ... other registration
}

All this wiring up can of course be done neater by using an IoC container of your choice.

What about those 404s?

Yup, they won’t get logged using the above code, as IExceptionFilter is only concerned with errors in the application. In order to log a 404 error you either need to do something ugly, such as write some code in the Application_Error or use a third party tool like Elmah.

Elmah

This is a good, solid solution for a lot of people who just want something that works without  having to understand what an IExceptionFilter is.
As it uses HttpModules it can also log/handle HttpError codes other than 500. From the project website:

ELMAH (Error Logging Modules and Handlers) is an application-wide error logging facility that is completely pluggable. It can be dynamically added to a running ASP.NET web application, or even all ASP.NET web applications on a machine, without any need for re-compilation or re-deployment.

Once ELMAH has been dropped into a running web application and configured appropriately, you get the following facilities without changing a single line of your code:

  • Logging of nearly all unhandled exceptions.
  • A web page to remotely view the entire log of recoded exceptions.
  • A web page to remotely view the full details of any one logged exception, including colored stack traces.
  • In many cases, you can review the original yellow screen of death that ASP.NET generated for a given exception, even with customErrorsmode turned off.
  • An e-mail notification of each error at the time it occurs.
  • An RSS feed of the last 15 errors from the log.

Summary

This post was about showing you how to have complete control over logging in your MVC web application by implementing IExceptionFilter.