Custom Data Annotations for Complex Models - asp.net-mvc

I have a view model like this:
public class Event
{
public string Name { get; set; }
[DateRangeValidator]
public DateTimeSpan DateRange { get; set; }
}
And this contains another class called DateTimeSpan that looks like this:
public class DateTimeSpan
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
}
I want to enable jQuery unobtrusive validation on the client side so I have written a custom data annotation validator (it inherits from ValidationAttribute and implements IClientValidatable) but it only seems to work if I apply the annotation to the Start and End properties of the DateTimeSpan class, and not if I apply it to the DateRange property of the Event class.
This is in my view:
#Html.LabelFor(x => x.DateRange.Start, "Start Date:") #Html.ValidationMessageFor(x => x.DateRange.Start)
#Html.TextBoxFor(x => x.DateRange.Start)
#Html.LabelFor(x => x.DateRange.End, "End Date:") #Html.ValidationMessageFor(x => x.DateRange.End)
#Html.TextBoxFor(x => x.DateRange.End)
ASP.NET MVC 3 will only inject the unobtrusive JavaScript data-* attributes into the HTML if the annotation is added to the Start and End properties, is there a way to make it work if the property is applied to the DateRange property instead?
I don't want my domain model class (DateRange, i.e. non view-model classes) to have to implement IClientValidatable because then I have to reference System.Web.Mvc in the domain model project.
Edit: Not sure if it's relevant but the DateRangeValidator attribute does checks to make sure the end date occurs after the start date etc.

Related

Unobtrusive validation on multiple models with same property names

I have a view containing multiple partial views bind to different models.
#model MyApp.ViewModels.ParentViewModel
#Html.Partial("_PartialView1", Model.PartialView1)
#Html.Partial("_PartialView2", Model.PartialView2)
Unobtrusive validation works, problem is, the models for the views have properties with the same name.
public class ClassA
{
public int SomeProperty { get; set; }
}
public class ClassB
{
public int SomeProperty { get; set; }
}
public class ParentViewModel
{
public int ClassA PartialView1 { get; set; }
public int ClassB PartialView2 { get; set; }
}
Since both properties have the same name, their html name attributes are the same too.
If ClassA.SomeProperty has an error, same error is shown in ClassB.SomeProperty.
Is there a way to have proper validation without changing the property names?
Do not use partials (which result in duplicate name attributes without the correct prefix, and which cannot be bound to you model when you submit the form).
The correct approach is to use an EditorTemplate. Rename _PartialView1.cshtml to ClassA.cshtml and move it to the /Views/Shared/EditorTemplates folder (ditto for _PartialView2 which needs to renamed to ClassB.cshtml - i.e. to match the name of the class). Then in the main view its
#model MyApp.ViewModels.ParentViewModel
....
#Html.EditorFor(m => m.PartialView1)
#Html.EditorFor(m => m.PartialView2)
Your html will now generate the correct name attributes
<input name="PartialView1.SomeProperty" .... />
<input name="PartialView2.SomeProperty" .... />
and the associated #Html.ValidationMessageFor() will also match up correctly
Side note: You can also solve this using a partial by passing the prefix as additional ViewData as per this answer, but the correct approach is to use an EditorTemplate
Unless they are in separate forms I don't think it is possible without giving them a different name.

MVC ignores DataType on overriden model property when DisplayFor is used

I'm wondering if anyone can confirm this behavior or if I've done something wrong.
Normally when you specify the DataType(DataType.MultilineText) attribute, and do something like #Html.DisplayFor(m => m.Body) MVC will use the MultilineText.cshtml in the DisplayTemplates folder. That does not seem to work when the DataType attribute is applied to an overriden property as in the code below. Now if I move the attribute to the property in the abstract class it MVC does use the MultilineText.cshtml display template.
public abstract class PostBase
{
[Required]
public virtual string Body { get; set; }
}
public class MessagePost : PostBase
{
[StringLength(500), DataType(DataType.MultilineText)]
public override string Body
{
get { return base.Body; }
set { base.Body = value; }
}
}
What's the Model in declared in your view? The abstract or child?
It uses reflection to read the attribute based on the model declared so:
#model PostBase
#Html.DisplayFor(m => m.Body)
Will work differently to
#model MessagePost
#Html.DisplayFor(m => m.Body)
the first of these will apply the [Required] only. It's bound to a PostBase model (doesn't know or care what the child class), so when it reflects the PostBase class; this only has [Required] on that property. So it never looks for the MultilineText.cshtml, why would it? It's not got MultilineText on it.
The second one will apply [StringLength(500), DataType(DataType.MultilineText)] and [Required]. The attributes are combined for inherited classes so when it reflects the class it'll see both attributes.
This view should use the template as required. I'm guessing this doesn't work for you though as I'm presuming the inheritance is there for a reason?

Asp.net mvc how to use htmlhelper to generate complex type?

I have a complex type License as a view model.
public class License
{
public string Name { get; set; }
// Other Properties
public List<Function> Functions { get; set; }
}
public class Function
{
public string Name { get; set; }
// Other Properties
public List<Unit> Units { get; set; }
}
public class Unit
{
public string Name { get; set; }
// Other Properties
}
Both the Function's view template and Unit's view template are dynamiclly rendered. So the html looks like this:
<!-- LicenseView -->
#model License
#Html.TextBoxFor(m => m.Name) // this is OK
#for(int i=0; i<Model.Functions.Count; i++)
{
#Html.Partial(Model.Functions[i].Name, Model.Functions[i])
}
and the FunctionView may look like this
#model Function
#Html.TextBoxFor(m => m.Name) // the generated html element's name is just 'Name'
#for(int i=0; i < Model.Units.Count; i++)
{
#Html.Partial(Model.Units[i].Name, Model.Units[i])
}
and this is UnitView
#model Unit
#Html.TextBoxFor(m => m.Name) // the generated html element's name is just 'Name'
So my question is, what should I do the make the Name attribute correct?
Thanks a lot
The only change you need to make in the above code is to use Editor instead of partial view.
So basically all you code will look similar to the following
#model License
#Html.TextBoxFor(m => m.Name)
// Editor will take care of the repetition and u don't need to explicitly pass in the name
// Since the model already have the attribute
#Html.EditorFor(Model.Functions)
Then create your editor template folder, "EditorTemplates", under "Shared" folder and name your view file as "Function"
Do the same for Unit class and you will get what you want.
As #Jack said... you can do this using Editors instead of PartialViews.
BUT... if you really want to use PartialViews, you can do it, but the model to pass should be the top one (License). This way is similar of what David Jessee proposed, but splitting the one view in several.
Pardon me for guessing at the problem, but are you asking for the DisplayName attribute?
It will define how the html helpers display your field lables
public class License
{
[DisplayName("License Name")]
public string Name { get; set; }
// Other Properties
public List<Function> Functions { get; set; }
}
public class Function
{
[DisplayName("Fun Name")]
public string Name { get; set; }
// Other Properties
public List<Unit> Units { get; set; }
}
public class Unit
{
[DisplayName("Unit Name")]
public string Name { get; set; }
// Other Properties
}
be sure to have
using System.ComponentModel;
in your model code.
If you want to be able to create all of the inputs for a complex object graph and have the entire graph be reconstituted by the model binder, the easiest way to approach it is to create a single view or partial view that renders the entire graph:
#for(int i=0;i<Functions.Length;i++){
#for(int j=0;j<Units.Length;j++){
#Html.EditorFor(Functions[i].Length[j].Unit)
}
}
The other option would be to find a way to pass the index of your element to the partial views for each leaf on your object graph.
Granted, a lot of people dont like the idea of rendering a complex model inside of a single view. However, your other option is to make the smaller child views for Units, etc. be dependent on having additional data either injected or provided by the context. 6 of one, half dozen of the other. Just about every time I've done the "academically correct" approach of making exactly one view or partial view for each type in an object graph, I ended up with a whole bunch of views that were not reusable to begin with and the only advantage I got was the ability to say, "Look! Lots of small files.....that are totally dependent on each other...why did I do that?"

How to format date in lambda expression in Razor View?

I have a View on which I need to display a date formatted in "dd/MM/yyyy".
Actually it is displayed as: #Html.LabelFor(model => model.DtNews) and I don't know where and how I can put the Format() function.
Data is retrieved from the database using EF. How can I do it?
#Html.LabelFor(model => model.DtNews)
#Html.EditorFor(model => model.DtNews)
and on your view model you could use the [DisplayFormat] attribute
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:dd/MM/yyyy}")]
public DateTime DtNews { get; set; }
Now you're gonna tell me that this class is generated by the EF framework to which I would respond to you: YOU SHOULD NOT USE YOUR EF AUTOGENERATED MODELS IN YOUR VIEWS. You should define and use view models. The view models are classes specifically tailored to the requirements of your views. For example in this particular view you have a requirement to format the dates in a particular way: perfect fit for view models:
public class MyViewModel
{
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:dd/MM/yyyy}")]
public DateTime DtNews { get; set; }
}
then your controller action could query your repository and fetch the domain model (EF autogenerated entity) and map it to a view model. Then it will pass this view model to the view.
I'd just throw a buddy class on your model.DtNews
A buddy class will decorate your existing model
[MetadataType(NewsMetadata)]
public partial class News // this is the same name as the News model from EF
{ /* ... */ }
/* Metadata type */
public class NewsMetadata
{
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:dd/MM/yyyy}")]
public DateTime DtNews { get; set; }
}
Try this. it work for me.
#Model.DtNews.Value.ToString("dd-MM-yyyy")
#Html.LabelFor(model => model.DtNews.ToString("dd/MM/yyyy"))
^should do the trick.
you could also use editor/display templates as discussed here.
If DtNews is a DateTime, then try this:
#Html.LabelFor(model => model.DtNews.ToString("dd/MM/yyyy"));
Use This
#Html.TextBoxFor(m => m.MktEnquiryDetail.CallbackDate, "{0:dd/MM/yyyy}")

DataAnnotations on public fields vs properties in MVC

Why don't DataAnnotations work on public fields? Example:
namespace Models
{
public class Product
{
[Display(Name = "Name")]
public string Title; // { get; set; }
}
}
public ActionResult Test()
{
return View(new Models.Product() { Title = "why no love?" });
}
#Html.LabelFor(m => m.Title) // will return 'Title' if field, or 'Name' if property
#Html.DisplayFor(m => m.Title)
If Title is a field, then the Display attribute seems to have no effect. If Title is changed to a property, it works as expected as displays "Name".
It would seem easy in this example to just change to a property, but I am trying to use the types from F# where they get compiled to a class with fields and not properties.
This was tested in ASP.NET 4 and MVC RC 3.
The reason why DataAnnotations do not work with fields is because the reflection-like mechanism that is used to retrieve the attributes (TypeDescriptor) only supports properties.
While it would not be easy, we could look into making this work with fields if there is enough demand.

Resources