Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC 2 Custom editor templates for splitting datetime fields

In the site I am building I need to have datetime properties split into different combinations depending on the property. Examples:

Member view has date of birth property which needs to be shown on the view as seperate day/month/year dropdowns.

A credit card view has an expiry date property which needs to be shown as seperate month/year dropdowns.

An excursion view has a time only property where seperate hours and minutes are needed as textboxes.

Each of these scenarios require validation and ideally client side validation as well.

I've looked at various options such as custom binding, custom attributes and am now looking at custom editor templates but so far I have had little luck in finding suitable solutions.

It seems like a common task but searching the net has shown little which covers everything (esp with the validation element).

So my question is has anyone else managed to accomplish the above?

(fingers crossed!)

like image 656
Steve C Avatar asked Jan 25 '10 17:01

Steve C


1 Answers

Ok, I'm going to try and get you 90% of the way there. This is actually a huge and complex part of MVC 2 and almost impossible to answer in just this answer box.

Now first you should go to Brad Wilsons blog and read in depth on how to customize the default MVC 2 templates. That should give you a much clearer understanding off all the moving parts.

http://bradwilson.typepad.com/blog/2009/10/aspnet-mvc-2-templates-part-1-introduction.html

Now I'll start a simple example of how to create a contrived appointment view model where we want to make sure the values supplied don't go back in time. Don't pay attention to the attributes right now, we'll get there.

Here is the ViewModel I'm using:

public class AppointmentViewModel
{
    [Required]
    public string Name { get; set; }

    [CantGoBackwardsInTime]
    public DateRange DateRange { get; set; }
}

public class DateRange
{
    public DateTime Start { get; set; }
    public DateTime End { get; set; }

    [Required]
    public int Price { get; set; }
}

And I've added this to the default HomeController ( nothing fancy ):

   public ActionResult Appointment()
    {
        return View(new AppointmentViewModel());
    }

    [HttpPost]
    public ActionResult Appointment(AppointmentViewModel appointment)
    {
        return View(appointment);
    }

And here is my View:

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
 Appointment
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <h2>Add Appointment</h2>
     <%= Html.ValidationSummary() %>
    <% using( Html.BeginForm()) { %>
    <%= Html.EditorForModel() %>
    <input type="submit" value="Save Changes" />
    <%} %>
</asp:Content>

Step 1: Setting the Stage

The first thing you want to do is grab the "default templates" from the blog entry. The important one in this case is the one that will sit in /Views/Shared/EditorTemplates/Object.asxc Object.ascx is the keystone to the whole operation. All Html.Editor***** methods will call this eventually.

Now the first piece of default functionality we have to change is this line inside of Object.ascx

<% if (ViewData.TemplateInfo.TemplateDepth > 1) { %>
    <%= ViewData.ModelMetadata.SimpleDisplayText%>
<% }

What thats saying is "don't display any nested complex types" and we don't want that. Change that > 1 to a > 2. Now view models in your object graph will have templates created for them instead of just creating placeholder text.

Just keep everything else default for now.

*Step 2: Overriding Templates**

If you read the blog entries hopefully you'll understand now how the Editor*** and Display methods will automatically call the templates in View/Shared/EditorTemplates and DisplayTemplates. Think of them as calling Html.RenderPartial("TYPENAME", MyType ) they aren't but its close enough in concept.

So if you run the solution this far and go to the correct url you'll notice that MVC 2 will call Object.ascx twice, once for your AppointmentViewModel and again for the property DateRange. Out of the box is just renders the same collection of form fields.

Lets say we want to make our template surround our DateRange editor with a red bordered box. What we want to do is short circut MVC 2 to call a custom DateTime.ascx template instead of Object.ascx and that is as easy as adding our own template in View/Shared/EditorTemplates/DateRange.ascx. In this case I've just taken what was generated by Object.ascx working with our DateRange model and just pasted the code into a new DateRange.ascx:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<div style="border: 1px solid #900">
    <div class="editor-label"><label for="DateRange">DateRange</label></div>            
        <div class="editor-field">            
            <div class="editor-label"><label for="DateRange_Start">Start</label>
        </div>


        <div class="editor-field">
            <input class="text-box single-line" id="DateRange_Start" name="DateRange.Start" type="text" value="" />            
        </div>

        <div class="editor-label"><label for="DateRange_End">End</label></div>

        <div class="editor-field">
            <input class="text-box single-line" id="DateRange_End" name="DateRange.End" type="text" value="" />            
        </div>

        <div class="editor-label"><label for="DateRange_Price">Price</label></div>

        <div class="editor-field">
            <input class="text-box single-line" id="DateRange_Price" name="DateRange.Price" type="text" value="" />

        </div>       
    </div>
</div>

Wala!

Now when you run the solution you should see a red box around our DateRange. The rest of the customizations are up to you! You could add jQuery datepicker boxes. In your case you could put both the fields in a single div so they line up horizontaly. The sky is the limit at this point.

Step 3: Validation:

Validation works pretty much just the way you'd expect. A [Required] attribute inside your DateRange type works just the same as any other validation attribute.

Now you see I made a can't go backwards in time attribute which I've put on the DateRange property of AppointmentViewModel. All you have to do to create these type specific validation attributes is inherit and implement the base ValidationAttribute:

public class CantGoBackwardsInTime : ValidationAttribute
{
    public override string FormatErrorMessage(string name)
    {
        return "Your date range can't go backwards in time";
        //return base.FormatErrorMessage(name);
    }

    public override bool IsValid(object value)
    {
        if (!(value is DateRange))
            throw new InvalidOperationException("This attributes can only be used on DateRange types!");

        var dateRange = value as DateRange;

        return dateRange.End > dateRange.Start;
    }
}

Now if you add this and decorate your property you should see the error message provided in the custom CantGoBackwardsInTime validation attribute.

I'll update and clear up more of this if you have any problems but this should get you started and on your way. ( thought I could bang this out before sleeping ) Just a warning: The new Editor for pieces of MVC 2 are the most awesome thing in the world and have huge potential to give MVC 2 super RAD capabilities; yet there is little to know information besides Brad Wilsons blog. Just keep at it and don't be afraid to peek at the MVC 2 source code if you need too.

like image 55
John Farrell Avatar answered Oct 21 '22 20:10

John Farrell