Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to "DRY up" C# attributes in Models and ViewModels?

This question was inspired by my struggles with ASP.NET MVC, but I think it applies to other situations as well.

Let's say I have an ORM-generated Model and two ViewModels (one for a "details" view and one for an "edit" view):

Model

public class FooModel // ORM generated
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string EmailAddress { get; set; }
    public int Age { get; set; }
    public int CategoryId { get; set; }
}

Display ViewModel

public class FooDisplayViewModel // use for "details" view
{
    [DisplayName("ID Number")]
    public int Id { get; set; }

    [DisplayName("First Name")]
    public string FirstName { get; set; }

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

    [DisplayName("Email Address")]
    [DataType("EmailAddress")]
    public string EmailAddress { get; set; }

    public int Age { get; set; }

    [DisplayName("Category")]
    public string CategoryName { get; set; }
}

Edit ViewModel

public class FooEditViewModel // use for "edit" view
{
    [DisplayName("First Name")] // not DRY
    public string FirstName { get; set; }

    [DisplayName("Last Name")] // not DRY
    public string LastName { get; set; }

    [DisplayName("Email Address")] // not DRY
    [DataType("EmailAddress")] // not DRY
    public string EmailAddress { get; set; }

    public int Age { get; set; }

    [DisplayName("Category")] // not DRY
    public SelectList Categories { get; set; }
}

Note that the attributes on the ViewModels are not DRY--a lot of information is repeated. Now imagine this scenario multiplied by 10 or 100, and you can see that it can quickly become quite tedious and error prone to ensure consistency across ViewModels (and therefore across Views).

How can I "DRY up" this code?

Before you answer, "Just put all the attributes on FooModel," I've tried that, but it didn't work because I need to keep my ViewModels "flat". In other words, I can't just compose each ViewModel with a Model--I need my ViewModel to have only the properties (and attributes) that should be consumed by the View, and the View can't burrow into sub-properties to get at the values.

Update

LukLed's answer suggests using inheritance. This definitely reduces the amount of non-DRY code, but it doesn't eliminate it. Note that, in my example above, the DisplayName attribute for the Category property would need to be written twice because the data type of the property is different between the display and edit ViewModels. This isn't going to be a big deal on a small scale, but as the size and complexity of a project scales up (imagine a lot more properties, more attributes per property, more views per model), there is still the potentially for "repeating yourself" a fair amount. Perhaps I'm taking DRY too far here, but I'd still rather have all my "friendly names", data types, validation rules, etc. typed out only once.

like image 213
devuxer Avatar asked Feb 15 '10 21:02

devuxer


People also ask

How long does it take for boobs to dry up?

Some people may stop producing over just a few days. For others, it may take several weeks for their milk to dry up completely. It's also possible to experience let-down sensations or leaking for months after suppressing lactation.

How long does it take for milk to dry up if not breastfeeding?

It usually gets better after several days. Over time, your body will stop making milk if you don't breastfeed or pump. This can take up to several weeks. You can take steps at home to decrease your discomfort and help your breasts stop making milk.


2 Answers

I'll assume that your doing this to take advantage of the HtmlHelpers EditorFor and DisplayFor and don't want the overhead of ceremoniously declaring the same thing 4000 times throughout the application.

The easiest way to DRY this up is to implement your own ModelMetadataProvider. The ModelMetadataProvider is what is reading those attributes and presenting them to the template helpers. MVC2 already provides a DataAnnotationsModelMetadataProvider implementation to get things going so inheriting from that makes things really easy.

To get you started here is a simple example that breaks apart camelcased property names into spaces, FirstName => First Name :

public class ConventionModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {
        var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);

        HumanizePropertyNamesAsDisplayName(metadata);

        if (metadata.DisplayName.ToUpper() == "ID")
            metadata.DisplayName = "Id Number";

        return metadata;
    }

    private void HumanizePropertyNamesAsDisplayName(ModelMetadata metadata)
    {
        metadata.DisplayName = HumanizeCamel((metadata.DisplayName ?? metadata.PropertyName));
    }

    public static string HumanizeCamel(string camelCasedString)
    {
        if (camelCasedString == null)
            return "";

        StringBuilder sb = new StringBuilder();

        char last = char.MinValue;
        foreach (char c in camelCasedString)
        {
            if (char.IsLower(last) && char.IsUpper(c))
            {
                sb.Append(' ');
            }
            sb.Append(c);
            last = c;
        }
        return sb.ToString();
    }
}

Then all you have to do is register it like adding your own custom ViewEngine or ControllerFactory inside of Global.asax's Application Start:

ModelMetadataProviders.Current = new ConventionModelMetadataProvider();

Now just to show you I'm not cheating this is the view model I'm using to get the same HtmlHelper.*.For experience as your decorated ViewModel:

    public class FooDisplayViewModel // use for "details" view
    {
        public int Id { get; set; }

        public string FirstName { get; set; }

        public string LastName { get; set; }

        [DataType("EmailAddress")]
        public string EmailAddress { get; set; }

        public int Age { get; set; }

        [DisplayName("Category")]
        public string CategoryName { get; set; }
    }
like image 135
John Farrell Avatar answered Oct 20 '22 06:10

John Farrell


Declare BaseModel, inherit and add another properties:

public class BaseFooViewModel
{
    [DisplayName("First Name")]
    public string FirstName { get; set; }

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

    [DisplayName("Email Address")]
    [DataType("EmailAddress")]
    public string EmailAddress { get; set; }
}

public class FooDisplayViewModel : BaseFooViewModel
{
    [DisplayName("ID Number")]
    public int Id { get; set; }
}

public class FooEditViewModel : BaseFooViewModel

EDIT

About categories. Shouldn't edit view model have public string CategoryName { get; set; } and public List<string> Categories { get; set; } instead of SelectList? This way you can place public string CategoryName { get; set; } in base class and keep DRY. Edit view enhances class by adding List<string>.

like image 21
LukLed Avatar answered Oct 20 '22 06:10

LukLed