Regex on data annotation behaves differently on sub properties - c#

I have the following model on my website
[Table("UserProfile")]
public class UserProfile
{
[Key]
public int UserId { get; set; }
[DisplayName("E-Mail")]
[MaxLength(60)]
[RegularExpression(#"[a-zA-Z0-9_\\.-]+#([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}", ErrorMessage = "Invalid e-mail")]
public string MailAddress { get; set; }
public UserInfo userInfo{ get; set; }
}
public class UserInfo
{
[Key, ForeignKey("UserProfile")]
public int UserProfileUserId { get; set; }
public virtual UserProfile UserProfile { get; set; }
[UIHint("PhoneNumbers")]
public List<PhoneNumber> PhoneNumbers { get; set; }
[...]
}
public class PhoneNumber
{
public int Id { get; set; }
[MaxLength(20)]
[DisplayName("Téléphone")]
[RegularExpression(#"^[+]?[0-9\s]{2,20}$", ErrorMessage = "Invalid")]
public string Phone { get; set; }
public int UserInfoId { get; set; }
[ForeignKey("UserInfoId")]
public Physician UserInfo { get; set; }
}
When I register a user, I have the following view:
<div class="editor-label">
#Html.LabelFor(model => model.MailAddressTemp)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.MailAddressTemp)
#Html.ValidationMessageFor(model => model.MailAddressTemp)
</div>
And the validation is ok
However, when I try to modify the mail address from another view
<div class="editor-label">
#Html.LabelFor(model => model.UserProfile.MailAddress)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.UserProfile.MailAddress)
#Html.ValidationMessageFor(model => model.UserProfile.MailAddress)
</div>
It doesn't work with the exact same mail address
Can someone explain it to me why regex validation doesn't work on sub properties?
Btw, I do have validation for the phone numbers during the registration and the modification of the account.
Thank you

To make the answer more elaborate:
You can't have client side validation on nested objects.
You can replace DataAnnotations with some third-party validation library like e.g. FluentValidatio.
In general DataAnnotations are used for simple scenarios. In case of more complex scenarios like this with nested objects you could implement IValidatableObject interface.

Related

MVC model binding keeps values NULL

I'm trying to get custom model binding to work, but for some reason the values aren't set. The code seems ligit when comparing it to working code, but still it doesnt bind. I guess it some trivial thing i'm missing.
Custom model:
//Cluster is from Entity Framework
//BaseViewModelAdmin defines:
public List<KeyValuePair<string, string>> MenuItems;
public IPrincipal CurrentUser = null;
public Foundation Foundation; //also from Entity Framework
public class AdminClusterCreateModel : BaseViewModelAdmin
{
public Cluster Item;
public AdminClusterCreateModel()
{
Item = new Cluster();
}
}
The view form looks like:
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>Cluster</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Item.Active)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Item.Active)
#Html.ValidationMessageFor(model => model.Item.Active)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Item.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Item.Name)
#Html.ValidationMessageFor(model => model.Item.Name)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
And the controller:
[HttpPost]
public ActionResult Create(AdminClusterCreateModel model, FormCollection form)
{
if(ModelState.IsValid) //true
{
var test = form["Item.Name"]; //Value is correct from form (EG: Test)
UpdateModel(model); //no error
}
//At this point model.Item.Name = null <--- WHY?
return View(model);
}
Cluster on request
public partial class Cluster
{
public Cluster()
{
this.Team = new HashSet<Team>();
}
public long Id { get; set; }
public System.DateTime Created { get; set; }
public System.DateTime Modified { get; set; }
public bool Active { get; set; }
public long FoundationId { get; set; }
public string Name { get; set; }
public virtual Foundation Foundation { get; set; }
public virtual ICollection<Team> Team { get; set; }
}
DefaultModelBinder works explicitly on 'Properties', not on 'Fields'
Changing public Cluster Item to public Cluster Item {get; set;} in AdminClusterCreateModel should do the trick.
public class AdminClusterCreateModel : BaseViewModelAdmin
{
public Cluster Item {get; set;}
public AdminClusterCreateModel()
{
Item = new Cluster();
}
}
Regards
This is a trivial and a corner case, yet if it might help someone:
If your model has a property, named model this to will cause the DefaultModelBinder to return a null.
public class VehicleModel
{
public string Model { get; set; }
}

Add/Remove related Entities to an objects List<Entity>

Ok first my actual Book and Author Object definitions:
public class Book
{
[Key]
public virtual int BookID { get; set; }
[Required]
[DataType(DataType.Text)]
[MaxLength(100)]
public virtual String Title { get; set; }
[DataType(DataType.MultilineText)]
public virtual String Abstract { get; set; }
[RegularExpression(#"\d{4}")]
public virtual String Year { get; set; }
[Required]
[Display(Name="Genre")]
public virtual int GenreID { get; set; }
public virtual Genre Genre { get; set; }
public virtual List<Author> Authors { get; set; }
public Book()
{
Authors = new List<Author>();
}
}
public class Genre
{
[Key]
public virtual int GenreID { get; set; }
[Required]
[DataType(DataType.Text)]
[MaxLength(30)]
public virtual String Name { get; set; }
public virtual List<Book> Books { get; set; }
}
public class Author
{
[Key]
public virtual int AuthorID { get; set; }
[Required]
[DataType(DataType.Text)]
[MaxLength(50)]
public virtual String Firstname { get; set; }
[Required]
[DataType(DataType.Text)]
[MaxLength(50)]
public virtual String Lastname { get; set; }
public virtual List<Book> Books { get; set; }
public Author()
{
Books = new List<Book>();
}
[NotMapped]
public string Fullname
{
get { return String.Format("{0}, {1}", Lastname, Firstname); }
}
public override string ToString()
{
return Fullname;
}
}
Using EF-Migrations I get a configuration.cs file into which I entered my Data-Seed and then using add-migration to get the first migration to initialize the database EF properly creates the according tables Authors, Books, Genres and last but not least AuthorBooks to hold the many-to-many relations between books and authors.
So far I got my whole demo application working. The only problem I have is that Entity Framework seems to refuse saving the AuthorBooks relations. I've set my app up so that at book-creation/editing I can choose the related Authors via a MultiSelectList-ListBox.
BookControllerViewModel:
public class BookControllerViewModel
{
public Book actualBook { get; set; }
public String redirectUrl { get; set; }
public MultiSelectList Authors { get; set; }
public int[] AuthorIDs { get; set; }
public SelectList Genres { get; set; }
}
The ViewModel contains the book that is subject to the create/edit operation, the MultiSelectList of Authors with proper Selection-Info based on the book and I added int[] AuthorIDs to hold the selected IDs the user would select in the ListBox of the Create/Edit view.
#model BookStoreInternet.ViewModels.BookControllerViewModel
<div class="editor-label">
#Html.LabelFor(model => model.actualBook.Title)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.actualBook.Title)
#Html.ValidationMessageFor(model => model.actualBook.Title)
</div>
<div class="editor-label">
<label>Author#( Model.actualBook.Authors.Count > 1 ? "s" :"" )</label>
</div>
<div class="editor-field">
#Html.ListBox("AuthorIDs", Model.Authors)
#Html.ValidationMessageFor(model => model.actualBook.Authors)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.actualBook.Abstract)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.actualBook.Abstract)
#Html.ValidationMessageFor(model => model.actualBook.Abstract)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.actualBook.Year)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.actualBook.Year)
#Html.ValidationMessageFor(model => model.actualBook.Year)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.actualBook.GenreID, "Genre")
</div>
<div class="editor-field">
#Html.DropDownList("actualBook.GenreID", Model.Genres)
#Html.ValidationMessageFor(model => model.actualBook.GenreID)
</div>
That too works well in that the ViewModel that is posted to the controller-Action
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(BookControllerViewModel bookVM)
{
if (ModelState.IsValid)
{
bookVM.actualBook.Authors = db.Authors.Where(x => bookVM.AuthorIDs.Contains(x.AuthorID)).ToList();
db.Entry(bookVM.actualBook).State = EntityState.Modified;
db.SaveChanges();
return Redirect(bookVM.redirectUrl);
}
bookVM.Authors = new MultiSelectList(db.Authors, "AuthorID", "Fullname", bookVM.actualBook.Authors.Select(x => x.AuthorID));
bookVM.Genres = new SelectList(db.Genres, "GenreID", "Name", bookVM.actualBook.GenreID);
return View(bookVM);
}
is properly filled with the values I would expect. What does not work though is that db.SaveChanges() seems to "silently" refuse to save the authors I added to the book.
Debugging the operation I can see that bookVM.ActualBook.Authors is actually set properly and contains the selected Authors... but the according entries to the Jointable AuthorBooks are not created...
Would anyone have an idea why this doesn't work or maybe even how to make it work?
Thanks in advance!
Thanks to Gerts answers I got this working by modifying the Controller-Action to
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(BookControllerViewModel bookVM)
{
if (ModelState.IsValid)
{
var book = db.Books.Find(bookVM.actualBook.BookID);
book.Update(bookVM.actualBook);
book.Authors.Clear();
foreach(var aid in bookVM.AuthorIDs)
book.Authors.Add(db.Authors.Find(aid));
db.SaveChanges();
return Redirect(bookVM.redirectUrl);
}
bookVM.Authors = new MultiSelectList(db.Authors, "AuthorID", "Fullname", bookVM.actualBook.Authors.Select(x => x.AuthorID));
bookVM.Genres = new SelectList(db.Genres, "GenreID", "Name", bookVM.actualBook.GenreID);
return View(bookVM);
}
and adding a method to the Book model
internal void Update(Book book)
{
Title = book.Title;
Abstract = book.Abstract;
Year = book.Year;
GenreID = book.GenreID;
}
Thanks again to Gert!
Because Book-Author is a many to many association you have to load actualBook.Authors first for the change tracker to be able to notice changes. Then you can add/remove items from it or replace it by a new collection. Setting its state is not necessary.

ASP.NET MVC4 DataAnnotations Validation on Models acting very strange - validating all fields even when I only specify one field to be validated

To keep this real simple, I have a model that has just one Required attribute (just on the Name). My View only has one #Html.ValidationMessageFor that's tied to the Name. When I click Save on the View when nothing is filled in, all fields come back as required. If I fill in the Name field, the remaining fields come back as required. I really need some help figuring out why this is:
public class KeyActive
{
[Key]
public int Pk { get; set; }
[Required(ErrorMessage="Name of filler is required.")]
[Display(Name="Name:")]
public string Name { get; set; }
[Display(Name = "Capsule 00 Pack Stat:")]
public int PackStat00 { get; set; }
[Display(Name = "Capsule 0 Pack Stat:")]
public int PackStat0 { get; set; }
[Display(Name = "Capsule 1 Pack Stat:")]
public int PackStat1 { get; set; }
[Display(Name = "Capsule 3 Pack Stat:")]
public int PackStat3 { get; set; }
public string CreatedBy { get; set; }
public DateTime CreatedDate { get; set; }
public string ModifiedBy { get; set; }
public DateTime ModifiedDate { get; set; }
}
Here's my View:
<div data-role="fieldcontain">
#Html.LabelFor(model => model.Name)
#Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
</div>
<div data-role="fieldcontain">
#Html.LabelFor(model => model.PackStat00)
#Html.EditorFor(model => model.PackStat00)
</div>
<div data-role="fieldcontain">
#Html.LabelFor(model => model.PackStat0)
#Html.EditorFor(model => model.PackStat0)
</div>
<div data-role="fieldcontain">
#Html.LabelFor(model => model.PackStat1)
#Html.EditorFor(model => model.PackStat1)
</div>
<div data-role="fieldcontain">
#Html.LabelFor(model => model.PackStat3)
#Html.EditorFor(model => model.PackStat3)
</div>
Finally, when I fill out the form and click Save, here's what I get:
It's acting like every field is required- when I fill in Name, and leave everything else blank, it won't let me submit it because it seems to think the other fields are also required. I really need some help figuring out if there's another place that I'm not seeing that performs validation, or what I'm doing wrong here!! Thanks.
Could it be because your ViewModel is using int instead of a nullable value:
public Nullable<int> PackStat00
or
public int? PackStat00
Right now, there's no way to store a null value in your view model for those fields.

Metadata in separate file does not display as expected

Hi I am trying to add my metada in separate file in order to keep my models as clean as posiblen but something seems to be wrong becuase now it seems that some properties do not display the validation.Here is my model clasS:
public partial class BookModel
{
public int Id { get; set; }
public string Name { get; set; }
public string Author { get; set; }
public string Description { get; set;}
public DateTime PublicationDate { get; set; }
public int CategoryId { get; set; }
public decimal Price { get; set; }
public string BookUrl { get; set; }
}
Here is my metada partial class:
[MetadataType(typeof(BookModel))]
public partial class BookModelMetada
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Author { get; set; }
[Required]
[DataType(DataType.MultilineText)]
public string Description { get; set; }
[Required]
[DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)]
public DateTime PublicationDate { get; set; }
[Required]
public int CategoryId { get; set; }
[Required]
[DataType(DataType.Currency)]
[DisplayFormat(DataFormatString = "{0:c}")]
public decimal Price { get; set; }
public string BookUrl { get; set; }
}
The wierd field is that the PublicationDate and Price display the validation error but the other properties do not.
What am I doing wrong?
EDIT:
View Code:
<p>
#Html.LabelFor(model => model.Book.Name, "Book Name")
#Html.TextBoxFor(model => model.Book.Name)
</p>
<p>
#Html.LabelFor(model => model.Book.Author)
#Html.TextBoxFor(model => model.Book.Author)
</p>
<p>
#Html.LabelFor(model => model.Book.PublicationDate ,"Publication Date")
#Html.TextBoxFor(model => model.Book.PublicationDate, new { #class="datepicker" })
</p>
<p>
#Html.LabelFor(model => model.Book.Price)
#Html.TextBoxFor(model => model.Book.Price)
</p>
<p>
#Html.LabelFor(model => model.Book.CategoryId, "Select category")
#Html.DropDownListFor(model => model.Book.CategoryId, new SelectList(Model.Categories, "Id", "Name"))
</p>
<p>
#Html.LabelFor(model => model.Book.Description)
#Html.TextAreaFor(model => model.Book.Description)
</p>
<p>
<input type="submit" value="Create" class="link"/>
#Html.ActionLink("Back to List", "Books", "ProductManager", null, new { #class = "link" })
</p>
The problem was that I added [MetadataType(typeof(BookModel))] on the metada class instead of the model class.
There can be one or more causes:
But at first make sure that you included jquery.unobtrusive and jquery.validate in the view or in the layout of the view.
Your View however seems to be unclear, you have to use #Html.LabelFor(model =>model.Book.Author). Please post your full view.

Entity Framework 4.1 - Code First - Cannot update nullabe DateTime values while resolving DbUpdateConcurrencyException

I currently have an entity that I'm trying to edit through my MVC 3 web app. I receive an DbUpdateConcurrencyExceptionwhen trying to perform a the client wins approach I got from MSDN's post Using DbContext in EF 4.1 Part 9: Optimistic Concurrency Patterns. The weird part is that this only happens on this particular entity and there I'm not doing anything different from the other. Also, it only happens when updating from a null to a value. The Properties giving the error when updating from null value to a DateTime value are DispositionLetterDate and DateDisposition.
Class:
public class A22
{
public A22()
{
this.IsArchived = false;
this.A22StatusId = (int)AStatus.Open;
}
[Key]
public int Id { get; set; }
[Required]
[StringLength(100, ErrorMessage="A22 Number cannot exceed 100 characters")]
public string Number { get; set; }
[Display(Name="Manual")]
public int ManualId { get; set; }
[Display(Name="SGMLID")]
public string SGMLId { get; set; }
[Required]
[DataType(DataType.Date)]
[Display(Name="Date Received")]
public DateTime DateReceived { get; set; }
[Display(Name= "Status")]
[EnumDataType(typeof(A22Status))]
public int A22StatusId { get; set; }
[Display(Name="Priority")]
[EnumDataType(typeof(A22Priority))]
public int A22PriorityId { get; set; }
[Display(Name="Providing Disposition")]
public string ProvidingDisposition { get; set; }
[Display(Name="Final Disposition")]
public bool FinalDisposition { get; set; }
[Display(Name="Is Archived")]
public bool IsArchived { get; set; }
[Display(Name="Created By")]
public int CreatedById { get; set; }
[Display(Name="Date Created")]
public DateTime DateCreated { get; set; }
[ConcurrencyCheck]
[Display(Name="Date Modified")]
public DateTime DateModified { get; set; }
[Display(Name = "Disposition Date")]
public DateTime? DateDisposition { get; set; }
[Display(Name = "Date Disposition Letter Sent")]
public DateTime? DispositionLetterDate{ get; set; }
// Virtual Properties
[ForeignKey("CreatedById")]
public virtual User CreatedBy { get; set; }
[ForeignKey("ManualId")]
public virtual Manual Manual { get; set; }
public virtual ICollection<A22Manual> A22ManualsImpacted { get; set; }
public virtual ICollection<A22Task> A22TasksImpacted { get; set; }
public virtual ICollection<A22Comment> Comments { get; set; }
public virtual ICollection<A22HistoryLog> HistoryLogs { get; set; }
}
Controller:
[HttpPost]
public ActionResult Edit(A22 a22)
{
var d = new A22Repository().Find(a22.Id);
var changes = TrackChanges(d, a22);
if (ModelState.IsValid) {
if (!string.IsNullOrEmpty(changes))
{
repository.InsertOrUpdate(a22);
this.repository.AddHistory(a22, changes);
repository.Save();
}
return RedirectToAction("Details", new { id = a22.Id });
} else {
ViewBag.PossibleManuals = d.ManualId == default(int) ? manualRepo.GetManualList() :
manualRepo.GetManualList(d.ManualId);
ViewBag.APriority = repository.GetAPriorityList(d.APriorityId);
ViewBag.AStatus = repository.GetAStatusList(d.APriorityId);
return View();
}
}
}
View:
#model TPMVC.Web.Models.A22
#{
Layout = "~/Views/Shared/_BlendLayoutLeftOnly.cshtml";
ViewBag.Title = "Edit";
}
<h2>Edit A22# #Model.Number</h2>
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.Id)
#Html.HiddenFor(model => model.CreatedById)
#Html.HiddenFor(model => model.DateCreated)
#Html.HiddenFor(model => model.DateModified)
#Html.Partial("_CreateOrEdit")
<div class="newItemLabel">
<strong style="padding-right: 145px;">#Html.LabelFor(model => model.AStatusId)</strong>
#{
Html.Telerik().DropDownList()
.Name("AStatusId")
.BindTo((IEnumerable<SelectListItem>)ViewBag.AStatus)
.HtmlAttributes(new { #style = "width: 200px;" })
.Render();
}
#Html.ValidationMessageFor(model => model.AStatusId)
</div>
<div class="newItemLabel">
<strong style="padding-right: 77px;">#Html.LabelFor(model => model.FinalDisposition)</strong>
#Html.EditorFor(model => model.FinalDisposition)
</div>
<div class="newItemLabel">
<strong style="padding-right: 44px;">#Html.LabelFor(model => model.DateDisposition)</strong>
#{
Html.Telerik().DatePickerFor(model => model.DateDisposition)
.Render();
}
</div>
<div class="newItemLabel">
<strong style="padding-right: 44px;">#Html.LabelFor(model => model.DispositionLetterDate)</strong>
#{
Html.Telerik().DatePickerFor(model => model.DispositionLetterDate)
.Render();
}
</div>
<div class="newItemLabel">
<strong style="padding-right: 110px;">#Html.LabelFor(model => model.IsArchived) </strong>
#Html.EditorFor(model => model.IsArchived)
</div>
<p>
<input type="submit" value="Save" />
</p>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
Thinking it was could have been something with the data annotations, I decided to define the properties with this issue to optional using the Fluid API.
modelBuilder.Entity<A22>().Property(a => a.DateDisposition).IsOptional();
modelBuilder.Entity<A22>().Property(a => a.DispositionLetterDate).IsOptional();
I basically need a fresh pair of eyes to see if I'm missing something. Is there other property that is making it behave this way?
Thanks in advance.
I'm mapping nullable DateTime properties like following without IsOptional methods. Also it works fine with MS SQL and MySQL by me.
this.Property(t => t.CreatedDate).HasColumnName("CreatedDate");
this.Property(t => t.ModifiedDate).HasColumnName("ModifiedDate");
in class derived from EntityTypeConfiguration. I'm using Code First approach.

Categories

Resources