I have the following EditorTemplate
#model ESG.Web.Models.FileInfo // <-- Changed to BaseFileInfo
#{
ViewBag.Title = "FileInfoEditorTemplate";
}
<fieldset>
<table class="fileInfoEdit">
<tr>
<td>Base Directory:</td>
<td>#Html.EditorFor(model => model.Directory)</td>
<td>#Html.ValidationMessageFor(model => model.Directory)</td>
</tr>
<tr>
<td>Filename:</td>
<td>#Html.EditorFor(model => model.Filename)</td>
<td>#Html.ValidationMessageFor(model => model.Filename)</td>
</tr>
</table>
</fieldset>
which corresponds to this ViewModel
public class FileInfo
{
[Display(Name = "Directory")]
[Required(ErrorMessage="Please specify the base directory where the file is located")]
public string Directory { get; set; }
[Display(Name = "File Name")]
[Required(ErrorMessage = "Please specify the name of the file (Either a templated filename or the actual filename).")]
public string Filename { get; set; }
}
What I want to do is reuse the above EditorTemplate but customise the ErrorMessage based on the context the FileInfo class is used in. I can have a standard file name eg abc.txt or a 'templated' file name eg abc_DATE.txt where DATE will be replaced with some user specified date. I want an appropriate error message in each case. In essence, the only difference should be the Annotations. (I think this the key, but am not sure how to tackle this, thus my convoluted approach!)
I have tried creating an abstract Base view model and then deriving a standard file and templated FileInfo classes. I change the declaration on the current EditorTemplate to
`#model ESG.Web.Models.BaseFileInfo`
and use it like
#Html.EditorFor(model => model.VolalityFile, "FileInfoEditorTemplate")`
where model.VolalityFile is a TemplatedFileInfo. The values are correctly displayed on the Edit page, however, there is no client-side validation when fields are not correctly filled. My initial guess is that this has something to do with the abstract class definition (not having any annotations on the fields).
public abstract class BaseFileInfo
{
public abstract string Directory { get; set; }
public abstract string Filename { get; set; }
}
// eg of derived class
public class TemplatedFileInfo : BaseFileInfo
{
[Display(Name = "File Name")]
[Required(ErrorMessage = "Please specify the name of a templated file eg someFileName_DATE.csv")]
public override string Filename { get; set; }
[Display(Name = "Directory")]
[Required(ErrorMessage="Please specify the base templated directory where the file is located")]
public override string Directory { get; set; }
}
This is the only way I could think of to tackle my requirement, thus the question - How to validate ViewModels derived from an abstract class? However, if there is another more feasible way to achieve this, please advise.
I had a similar situation where I had to change the message based the value of another property.
I made the whole validation server side and without the annotations.
Then I just added the ModelErrors this way:
ModelState.AddModelError("YourPropertyName", "The Error Message you want to show.);
It's a bit extra work and overkill maybe but it did the trick for me.
Related
I'm using entity framework and MVC (both CORE if that matters) to make a site. Using the model and tying it directly to the view is fine all the CRUD actions work, everything is lovely.
I wanted to use a couple of pages to access the model so the site looked better, so split the controls out onto separate views and added a corresponding viewmodel for each, so my project looks like this
-Model
--CustomerModel
-ViewModel
--CustomerNameVM
--CustomerAddressVM
-View
--CustomerNameView
--CustomerAddressView
The CustomerModel has a number of properties
Forename
Surname
Address
Postcode
with Forename and Surname in the CustomerNameVM and Address and Postcode in CustomerAddressVM. Surname is defined as [Required] in the model but not in CustomerNameVM which I believe is the correct way to do it.
I'm struggling to get the model loaded into the viewmodel and then trying to save it when I'm editing the address details in CustomerAddressView because it errors when I try and save as the viewmodel doesn't contain Surname (from the model), so it's null and therefore the [Required] criteria isn't being met.
I've tried a few methods of trying to get past this like :-
Jeffrey Palermo's Onion Architecture
Repositories
domain models
amongst others which all end up with the same problem, I can't save the Address as the Surname is null.
How do I ignore validation criteria for the properties of the model that aren't being referenced in the viewmodel?
Or how do I load and reference only those properties of the model that are present in viewmodel?
Edit
For those who keep asking for code, which codeset? I've tried 30 of them now, none of which do the job. Which one of these do you want? I'm trying to get a general idea of how this is supposed to work as none of the methods, documentation and associated examples function.
Here's a starter for 10, it's unlike the other 29 codesets but it's code and probably the shortest.
The controller
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Step2Address(int? id, [Bind("CustomerID,txtAddress,txtPostcode")] VMAddress VMAddress) {
if (ModelState.IsValid) {
//the saving code
}
return View(VMAddress);
}
the model
public class clsCustomer {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int CustomerID { get; set; }
public string Forename { get; set; }
[Required]
public string Surname { get; set; }
public string Address { get; set; }
public string Postcode { get; set; }
the viewmodel
public class VMAddress {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int CustomerID { get; set; }
public string Address { get; set; }
public string Postcode { get; set; }
}
the view
#model theProject.Models.VMStep2Contact
<form asp-action="Step2Address">
<input type="hidden" asp-for="ComplaintID" />
<input asp-for="txtAddress"/>
<input asp-for="txtPostcode"/>
<input type="submit" value="Save" />
the context
public class ContextCustomer : DbContext {
public ContextCustomer(DbContextOptions<ContextCustomer> options) : base(options) {
}
public DbSet<clsCustomer> Customer{ get; set; }
}
Clicking "Save" on the webpage calls the controller straight away, which hits the first line if (ModelState.IsValid) and as the Surname isn't set and is [Required] the ModelState is not valid so no save is attempted.
I don't actually understand what the problem is, and without code, it's impossible to say what you might be doing wrong. Generally speaking, you shouldn't have any issues since you're using view models.
A view model is, of course, not saved directly to the database, so it has to be mapped over to an actual entity class that you will be saving. In the case of an edit, you should retrieve all relevant entities from the database, map any posted values onto the appropriate properties on those entities, and then save those entities back to the database. If you're doing this, presumably, the customer model should already contain the Surname and other required properties, and you'd only be modifying/setting the address properties.
If you're doing a create, then, simply you can't just take address information. You need the name as well, so for this, you'd need to pass a view model that contains at least all required fields, such that you have all the information you need to actually save the entity.
If you're trying to do a multi-step form, where you collect all the information over multiple posts, then you simply must persist the posted data somewhere other than the database until you have enough of it to actually save an entity to the database. This is usually accomplished via Session/TempData.
My viewmodel inherits from a class that inherits from an abstract class that has a property with a [Required] attribute, but the rule doesn't appear in the DOM and unobtrusive validation doesn't catch the error.
The display attribute goes through fine, but the validation DOM attributes are not added to the textarea
my view has this:
#model FormPersonView
....
#Html.TextAreaFor(m => m.Description)
#Html.ValidationMessageFor(m => m.Description)
my code has this:
public class FormPersonView : Person
{
//View related stuff
.....
.....
}
public class Person : BasePerson
{
//Person related stuff - validation for these work!
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
}
public abstract class BasePerson
{
//Base person stuff - validation for this doesn't work!
public string Id { get; set; }
[Required]
[Display("Short description of the person")]
public string Description { get; set; }
}
Why does it work with one level of inheritance but not two? It does work on the server side.
Had exactly this problem. While defining the view, the model comes as a type you defined #model FormPersonView. Data annotations will only work on that specific type, even if you have derived properties from children, their data annotations won't be engaged.
The solution that I came up with in my project was to define editor templates for types I needed data annotations to work properly and then calling #EditorFor on those models. Then and only then were data annotations operating as expected.
Hope this help you.
Thanks for any thoughts.
while I am working my way through some custom validationAttributes, I have come across a problem which should be simple, but has me stumped.
An authorized user will have a UserProfile which includes a key to the site they work in. This site is a record set within a database. 1 field in this site record set is a regular expression which denotes what would be a valid format for a field in a completely separate table. The data entered into this other table will be common to all registered users, but a particular field relates to the ID format used at their institution.
Is there a clean way I can dynamically add a regular expression validator to a property?
Thank you as always.
This is what I came up with, but keen to know if there are better solutions:
Naming conventions are to allow automapper to flatten the model (each StudyCentre has a many to 1 relationship with the RecordSystem (some systems share the patient indexing system)
Mapper.CreateMap<StudyCentre, ParticipantRegistration.StudyCentreViewData>();
As a nested class within the ViewModel for an indidual TrialParticipant
public StudyCentreViewData ViewData { get; set; }
public class StudyCentreViewData
{
public string Abbreviation { get; set; }
public string RecordSystemName { get; set; }
public string RecordSystemHospitalNoRegEx { get; set; }
public string RecordSystemNotationDescription { get; set; }
public IDictionary<string, object> HospitalNoRegEx()
{
return DynamicClientValidation.RegEx(errorMessage:String.Format("{0} must consist of {1}",
RecordSystemName,
RecordSystemNotationDescription),
regExPattern: RecordSystemHospitalNoRegEx);
}
}
The other properties (such as StudyCentre.Abbreviation are for the labels)
The function RegEx is simply:
public static class DynamicClientValidation
{
public static IDictionary<string, object> RegEx(string errorMessage, string regExPattern)
{
var returnVal = new Dictionary<string, object>(3);
returnVal.Add("data-val-regex", errorMessage);
returnVal.Add("data-val-regex-pattern", regExPattern);
returnVal.Add("data-val", "true");
return returnVal;
}
}
The Controller sets up the viewmodel like so:
model.ViewData = Mapper.Map<StudyCentre, ParticipantRegistration.StudyCentreViewData>(_studyCentre.GetCentreByUser(_currentUserName));
and in the view (LabelDetailsfor is a custom helper):
<div class="editor-label">
#Html.LabelDetailsFor(model => model.HospitalID,Model.ViewData.Abbreviation + " ID", Model.ViewData.RecordSystemName)
</div>
<div class="editor-field">
#Html.TextBoxFor(model => model.HospitalID, Model.ViewData.HospitalNoRegEx())
#Html.ValidationMessageFor(model => model.HospitalID)
</div>
I have 2 models, employee and person:
public class Employee
{
[Key]
public int Id { get; set; }
public int? PersonId { get; set; }
[ForeignKey("PersonId")]
public virtual Person Person { get; set; }
}
public class Person
{
public IList<PhoneNumber> PhoneNumbers { get; set; }
public int Id { get; set; }
public string FName { get; set; }
public string LName { get; set; }
public Person()
{
PhoneNumbers = new List<PhoneNumber>
{
new PhoneNumber()
};
}
}
Editor Template for Phone:
#Html.TextBoxFor(x => x.Number)
#Html.DropDownListFor(m => m, new SelectList(Enum.GetNames(typeof (WebMVC.Core.Common.PhoneType))))
To reduce clutter, I removed the other (non-pertinent) properties.
The difficulty I am having is while in the Employee Create(), I can bind the person FName & LName, I cannot bind the PhoneNumbers collection.
I know about the 2008 Haack blog but I do not think it mirrors this situation.
Does anyone know a solution to bind the person phone numbers collection in the employee's Create()?
I'm not exactly sure if PhoneNumber is a custom class that you created, or one that is built into the framework. But if you're having problems with MVC3 mapping posted data to the Employee class like you specified, you might want to look at creating a custom binding. Keep in mind that if your editor template code is incorrect this wont really matter, so I would take a look at that using fiddler first.
Here are a few good sites to get you started, I found them all on SO at one point.
http://odetocode.com/blogs/scott/archive/2009/04/27/6-tips-for-asp-net-mvc-model-binding.aspx
http://odetocode.com/blogs/scott/archive/2009/05/05/iterating-on-an-asp-net-mvc-model-binder.aspx
http://www.singingeels.com/Articles/Model_Binders_in_ASPNET_MVC.aspx
Creating a custom binder gives you complete control over the way that MVC parses your posted model data and populates the object. There are 2 main functions that most people override, CreateModel and BindModel. BindModel is the function you will most likely want to override if this is the way you would like to go.
I don't know what the html from the editor template looks like, but to bind to a collection of custom types it should look something like this:
<input name="[0].Number">
<input name="[0].PhoneType">
<input name="[1].Number">
<input name="[1].PhoneType">
<input name="[2].Number">
<input name="[2].PhoneType">
I'll let the code do the talking here, I have something like this:
class Problem
{
public string Title { get; set; }
public string Description { get; set; }
public virtual IList<Symptom> Symptoms { get; set; }
}
class Symptom
{
public string Comments { get; set; }
public virtual Category Category { get; set; }
}
class Category
{
public string Name { get; set; }
}
I have a modal that allows users to add a list of symptoms on my view. Each symptom being added produces an INPUT that looks like this (where N is the index):
<input type="text" name="Symptom[N].Name" value="#Model.Symptom[N].Name">
<input type="text" name="Symptom[N].Category" value="#Model.Symptom[N].Category">
Once I POST the data to my controller, the model contains a valid list of Symptom (if I add 3, my Product.Symptom list has 3 entities) and the [Comments] of each symptom has persisted, but the [Category] property of each is NULL. What am I doing wrong here? I've tried numerous things but I still end up with NULL as the [Category] for each.
I'm using Entity Framework 4.1 Code First with Fluent API developing in MVC 3 using Razor syntax.
Try this:
<input type="text"
name="Symptom[N].Category.Name"
value="#Model.Symptom[N].Category.Name">
What I think is happening is that it's trying to bind a string to a Category which is invalid. If you want to map the text to the Name property on the Category class, you will need to specify it one level deeper.