I need to add a prefix to the name of form elements that are rendered within a form. I've created a custom attribute to decorate a property that accepts the name of another property whose value will be used for the name prefix.
public class HtmlElementNamePrefixPropertyAttribute : Attribute {
public string PropertyName { get; set; }
public HtmlElementNamePrefixPropertyAttribute(string propertyName) {
PropertyName = propertyName;
}
}
And my custom ModelMetadataProvider:
public class AddressModelMetadataProvider : DataAnnotationsModelMetadataProvider {
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) {
ModelMetadata metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
HtmlElementNamePrefixPropertyAttribute nameAttribute = attributes.OfType<HtmlElementNamePrefixPropertyAttribute>().FirstOrDefault();
if (nameAttribute != null) {
ModelMetadata prefixMetadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor, metadata.ContainerType, nameAttribute.PropertyName);
metadata.PropertyName = string.Format("{0}{1}", prefixMetadata.Model, metadata.PropertyName);
}
return metadata;
}
}
As an example, if I decorate Address1 with HtmlElementNamePrefixAttribute:
[HtmlElementNamePrefix("AddressType")]
public string Address1 { get; set; }
public AddressType AddressType { get; set; }
And in my view render a textbox for Address1:
#Html.TextBoxFor(m => m.Address1)
It should render as (assuming that AddressType == AddressType.Home)
<input name="HomeAddress1" value="123 Street way"/>
I have a few problems:
I'm not sure how to effect the rendered name HTML attribute from AddressModelMetadataProvider or what property in ModelMetadata would allow me to do that. My current attempt was to change the PropertyName property of ModelMetadata, but that doesn't have a setter.
If possible, I don't want to create a new HtmlHelper as this attribute could apply to any type of form element that would be rendered in an Address. I also don't want to create a string EditorTemplate since this scenario only applies to an Address object and its properties.
To give a better understanding of what I'm trying to accomplish and why, let me give a brief explanation of the project and its purpose.
Our application allows users to create "fields". From the end users perspective, "fields" can be a single line textbox, multi line textbox (textarea), chooseMany (which is a group of checkboxes), chooseOne (which is a dropdown), address (which consists of more than one form element and can be of type home, business or other), contact (email, phone, and fax), and others. I've simplified a great deal, but I think this gets the point across.
All this information is stored in the database ("field" values, "field" metadata, which "fields" are on the requested "form", etc.), and at runtime used to configure the "form" the user is requesting (i.e., /forms/123). So the form may have a textbox and an address "field", or maybe a home address "field" and a business address "field". Each "form" is created and configured by an administrative user. These "fields" or rather models, inherit from IDataItem and have their own views (templates) that describe how they should be rendered to the UI.
Because of the dynamic nature of a "form" custom model binding and validation was needed (i.e., custom ValidationAttributes, ValidatorProviders, ModelBinders, etc.). Validation rules and logic are applied dynamically at runtime using custom and standard ValidationAttributes (i.e., RequiredAttribute is used for simple "fields" like a single line textbox or chooseOne). This is done at runtime, because the administrator building the "form" can mark a "field" as required or not (as well as other validation constraints).
Required validation for an address "field" is different, because an address is not considered complete unless all parts of the address are filled out (with the exception of address2 and address3).
For client side validation we're using the standard MVC client validation library, jQuery validate. Here in lies my problem... Error messages are applied by jQuery validate based on the name of the form element. So if address1 is invalid, but there is a home address "field" and a business address "field" on the form then a distinction needs to be made between each form element name, so that the error message can be applied to the correct element. This is the biggest reason why I need to prefix the address form element names with AddressType.
This is a very long-winded simplified explanation, but I think it relays the purpose of the application and why my problem exists.
Related
I am trying to have all the models used for specific searches into one Generic search view.
I need to render only some of the fields in the model, either with something similar to this : (Psudo code)
foreach (string textBoxFieldName in TextBoxFieldNames)
{
Html.Toolbox_TextBoxFor(m => m.textBoxFieldName)
}
or having attributes on the model and checking when that attribute applies e.g.
in the model I'll have something like this :
[AppliedCases("Case1","Case4","Case77")]
[ControlToUse("TextBoxFor")]
public string LastName { get; set; }
And some how in the view will be able to go trough all the properties check that if the CurrentCase is one of the AppliedCases of the Model property and if so then use the Razor magic display it accordingly
My question is this correct approach and if so how do I implement it, looking for some examples with dynamic rendering (if that is the name of this topic)
You could use the overload of Html.Editor that takes the name (as string) of the property you want to render:
var fieldNames = new string[] {"LastName"};
foreach (string fieldName in fieldNames) {
#Html.Editor(fieldName)
}
You can use the UIHint attribute in the ViewModel to influence which editor template shall be used:
[UIHint("MySpecialStringEditor")]
public string LastName { get; set; }
At the shop where I work, we do not use this approach. Rather, we use different explicit Views of the same ViewModel. This allows for more flexibility and is easier to understand and maintain.
I have an EF entity Respondent that is automatically generated by EF from the database).
I had to expand this entity to add some validation rules and attributes to use within my View:
[MetadataType(typeof(RespondentMetadata))]
public partial class Respondent { }
public class RespondentMetadata
{
[Required]
[Display(Name = "First Name")]
public string FirstName { get; set; }
}
Now, in my controller, I need to check if this Respondent object has indeed value in FirstName (without checking the value explicitly, because I may have large number of such properties with various validation rules).
This check can be anywhere not necessary in [HttpPost] action (so, ModelState.IsValid N/A here).
How can I validate the entire entity anywhere in code?
Thanks.
The 'Text-Book-Way' is to add validation to your Model.
then you can make a check like
if (Respondent.IsValid())
{
}
You could use the Validator.ValidateObject method in order to perform validation anywhere in the code, but for that you need to stop using the MetadataTypeAttribute and explicitly associate the metadata class using the TypeDescriptor.AddProviderTransparent method.
Sample:
var respondent = new Respondent();
var provider = new AssociatedMetadataTypeTypeDescriptionProvider(
typeof (Respondent),
typeof (RespondentMetadata));
TypeDescriptor.AddProviderTransparent(provider, typeof (Respondent));
Validator.ValidateObject(
respondent,
new ValidationContext(respondent, null, null));
The Validator class does not seem to honor the attribute so this is the only workaround that I'm aware.
i want to provide a method of changing the labels associated with controls which will be rendered on the page via a lookup to a sql table.
Ideally i want to inject the meta data display field which is then rendered on the page using the helper.
#Html.LabelFor(m => m.city)
Since this would need to be a sql lookup at runtime, i cannot just change the class scaffolding tt template to stamp on a displayname annotation at design time.
I thought of 3 potential methods.
rewrite all the html.helpers i want to use. Problem with this is you would need to replicate all the functionality of the existing helper prior to making the changes.
write a custom data annotation and stamp it on each property in the class i.e.
[MyCustomNameAttribute]
public string city{ get; set; }
then hopefully in the MyCustomNameAttribute class i can find both the linq field i am referring to, a reference to the metadatamodel and a database context using these i can retrieve and replace the DisplayName based on potential name customisations configured by the User. I tried to do this but was unable to find out how the [Display(Name="city")] annotation works in the background.
Modify the entity model backing code to inject the name into the metadatamodel.
Does anyone have any experience of the above?
Cheers
Tim
You have to create a custom ModelMetaDataProvider by extending the DataAnnotationsModelMetadataProvider.
public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(
IEnumerable<Attribute> attributes,
Type containerType,
Func<object> modelAccessor,
Type modelType,
string propertyName)
{
var meta = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
if(meta.DisplayName == null)
{
// TO DO read the display value from database and assign here
meta.DisplayName = ..
}
return meta;
}
}
Then you have to set that in the Global.asax.cs
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ModelMetadataProviders.Current = new CustomModelMetadataProvider();
}
I'm assuming that you want to set only the display name from db but suppose you want to load the complete modelmetadata from db then I would suggest you to create a custom ModelMetadataProvider by implementing the abstract class ModelMetadataProvider.
Hitting the database every time definitely not a good idea so we have to workout for some caching strategy.
We have to hit the database for every new containerType (I guess) and read the metadata information for the container along with all its properties and store in the cache with key as the containerType (this could be a difficult job).
I am working with a ASP.NET MVC4 application. I have created a view model which contains menu items and I can switch languages in page by Resources file.
#region Properties
[Display(Name = "MenuText", ResourceType = typeof(App.App_Resources.Menu))]
public string menuText { get; set; }
public List<MenuItem> menuItems { get; set; }
#endregion
However, I want to get this resource string in my .cshtml file, then I try as following
#model App.Models.MenuViewModel
#Html.LabelFor(model => model.menuText) <- Success
#Html.DisplayForModel("menuText") <- Success
#Model.menuText <- Fail
I inserted a break point and found out that Model contains a property which name is menuText but value is null. And I checked that Html also contains a property Model and its menuText also is null.
However, menuItems has items since I assign objects in constructor.
Why the menuText cannot be initialized and assigned value to it?
Why I can succeed to show the resource string with first two but Model.menuText is null and fail to show anything? What is different between the models in #Html.XXX and #Model?
#Model.menuText retrieves the raw string value stored within the menuText property. Attributes are ignored.
Using LabelFor causes the Display attribute of the property to be examined. The localised string is stored in the attribute, not the property.
Note that I think the Model object/class should not be used to store information for display (that's what ViewData is for), but rather only for round-trip data that is sent from the client to the server.
Maybe that's not exactly the solution i need, but this is what i want to do:
i have a company registration form, and each company needs an administrative user. an administrative user may manage multiple companies, so in the company registration form, you can choose an existing user from a dropdown.
the company view model looks something like this:
public class CompanyViewModel {
[Required]
public string Name { get; set; }
// other properties...
public UserViewModel Administrator { get; set; }
public IEnumerable<UserViewModel> AvailableUsers { get; set; }
}
and the user view model looks like this:
public class UserViewModel {
[Required]
public string UserName { get; set; }
[Required]
public string Password { get; set; }
// other properties...
}
in the company registration view:
<div><input type="radiobutton" name="chooseuser" id="existing"/>Choose an Existing User:</div.
<div>#Html.DropDownListFor(m => m.Administrator.Id, Model.AvailableUsers.Select(u => new SelectListItem { Text = string.Format("{0} - {1} {2}", u.UserName, u.FirstName, u.LastName), Value = u.Id.ToString() }), "<Choose existing user>", new { id = "existingusers" })
</div>
<div><input type="radiobutton" name="chooseuser" id="createnew"/>Create a new User:</div>
<div><label>Username:</label> #Html.EditorFor(m => m.Administrator.UserName)</div>
Through javascript, based on radio button selection, the dropdown list is disabled and the new user form shown, or the new user form is hidden and the dropdown list is enabled.
The problem is in the controller Save action after you press save, ModelState.IsValid is false if an existing user is chosen and no data is filled in on the form. If the user chooses to enter a new user, validation succeeds.
What is the best way to handle this?
One option is to load all data for all users into data structures in javascript, and when the value changes on the existing user dropdown, the hidden "create new" form fields can be populated. But this seems lame since passwords would be sitting the html in plain text. i can get fancier and use ajax for a "create new" form and populate a user id on the original form once the new user is saved, but i'd like to keep it all in one form if possible.
Seems liked i'd ideally be able to load the existing user data from the db and populate the model state in the controller Save action, but writing this code manually (even using reflection) seems sloppy. It would be nice if there was a built in method to do this.
Any ideas?
Thanks.
That's a typical scenario which perfectly illustrates the limitations of declarative validation (a.k.a Data Annotations). In order to handle it you could write a custom validation attribute which will be applied to the CompanyViewModel instead of individual properties and will allow you to perform the validation logic based on which radio button the user choose (btw you will need a property on your view model which will represent the radio button selection). The problem with model validators is that you might have some hard time handling the error highlighting.
That's one of the reasons why I use FluentValidation.NET instead of Data Annotations. This allows me to have the validation logic away from the model and done in an imperative way. It also allows me to have conditional validators which apply based on the values of some properties on the view model (in this case this would be the radio button selection).
You may want to consider a custom Modelbinder.
Here's some sample code from my site - this is part of a checkout page for a shopping cart - the user can enter an address but for US StateCd is sent and for non US StateOrProvince is sent. So we look at the country and remove any model errors for the other property that doesn't apply.
I think this is very similar to what you're describing (that you have two scenarios that need different rules but you want to use the same model).
The important code here is bindingContext.ModelState.Remove(...)which removes the model state and allows IsValid to return true.
public class AddressModelBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
base.OnModelUpdated(controllerContext, bindingContext);
// get the address to validate
var address = (Address)bindingContext.Model;
// remove statecd for non-us
if (address.IsUSA)
{
address.StateOrProvince = string.IsNullOrEmpty(address.StateCd) ? null : CountryCache.GetStateName(address.StateCd);
bindingContext.ModelState.Remove(bindingContext.ModelName + ".StateOrProvince");
}
else
{
address.StateCd = null;
bindingContext.ModelState.Remove(bindingContext.ModelName + ".StateCd");
}
// update country
address.Country = CountryCache.GetCountry(address.CountryCode, true).Name;
// validate US zipcode
if (address.CountryCode == "US")
{
if (new Regex(#"^\d{5}([\-]\d{4})?$", RegexOptions.Compiled).Match(address.ZipOrPostal ?? "").Success == false)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName + ".ZipOrPostal", "The value " + address.ZipOrPostal + " is not a valid zipcode");
}
}
// all other modelbinding attributes such as [Required] will be processed as normal
}
}
Note: You need to register this modelbinder in global.asax. The modelbinding component is smart enough to let you create differnt model binders for any part of your model if it contains different objects.
ModelBinders.Binders[typeof(UI.Address)] = new AddressModelBinder();
Hope this helps. I think this applies to your situation.