views:

583

answers:

2

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!)

A: 

Are you trying to do this all on one page? Or one method? I am not really sure how you want to do this so here is my shot if I understand you right.

So for example you could do something like this for the birthdate one.

In your view

<%= Html.DropDownList("Birthday")%>
<%= Html.DropDownList("BirthMonth")%>
<%= Html.DropDownList("BirthYear")%>

In your controller somewhere have something like this. Maybe you could combine it all into one loop or something like that.

List<int> month = new List<int>()
for(int i = 0; i < 12, i++)
{
    month.add(i + 1);
}

ViewData["BirthMonth"] = new SelectList(month);


int days = DateTime.DaysInMonth(Year, Month);

    // do a another for loop add it to viewsData =  ViewData["Birthday"] .
    // do a another for loop for years and add it do another viewdata = ViewData["BirthYear"].

So what I am getting at is your can do some stuff on the server to get the dates that you want and just add it through ViewData into the dropdown lists.

chobo2
Hi chobo2, thanks for the input but this isn't really going in the right direction. I'm looking for a way to bind to a datetime property on a model and still validate, etc. Link in above comment should give a better idea of what I'm trying to accomplish.
Steve C
+3  A: 

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.

jfar
Thanks for this jfar, it's certainly given me something to work with however I'm more aiming towards to splitting the actual datetime object in to its component inputs (a seperate input for year, month and day for example) rather than multiple full datetime instances. Do you think this is still the right way to go? How would go you about adding client side validation as part of this solution?
Steve C
Well right. You'll just have to make your own DateTime template which will split up the UI. As far as client side validation goes simply include the javascript in your custom DateTime.ascx template.
jfar
Ok, I have it working with one of the combinations I was after (month and year only in two seperate inputs) so a massive thank you for your help and I'll flag this as answered. One thing I am wondering now is I have it so that a custom validationattribute takes a CardDate object (int month and int year) and tries to form a valid date. if it fails it correctly returns false stating that the carddate is invalid. One thing, is it possible from within the attribute to make both fields within the cardtype object invalid so they show in an invalid state on the view? Hope that made sense!
Steve C
Hmm, I have another issue. If I don't apply the require attribute to the cardtype property then leaving them blank should not raise an error but the customattribute doesn't know this and raises the error. Do you know how to access the dataannotations from within the custom attributes so I can check this before raising an error?(sorry about all of the questions!)
Steve C