How to retrieve GroupName data annotation from ModelMetadata - asp.net-mvc-3

The DisplayAttribute in System.ComponentModel.DataAnnotations has a GroupName property, which allows you to logically group fields together in a UI control (e.g. a property grid in WPF/WinForms).
I am trying to access this metadata in an ASP.NET MVC3 application, essentially to create a property grid. If my model looks like this:
public class Customer
{
[ReadOnly]
public int Id { get;set; }
[Display(Name = "Name", Description = "Customer's name", GroupName = "Basic")]
[Required(ErrorMessage = "Please enter the customer's name")]
[StringLength(255)]
public string Name { get;set; }
[Display(Name = "Email", Description = "Customer's primary email address", GroupName = "Basic")]
[Required]
[StringLength(255)]
[DataType(DataType.Email)]
public string EmailAddress { get;set; }
[Display(Name = "Last Order", Description = "The date when the customer last placed an order", GroupName = "Status")]
public DateTime LastOrderPlaced { get;set; }
[Display(Name = "Locked", Description = "Whether the customer account is locked", GroupName = "Status")]
public bool IsLocked { get;set; }
}
and my view looks like this:
#model Customer
<div class="edit-customer">
#foreach (var property in ViewData.ModelMetadata.Properties.Where(p => !p.IsReadOnly).OrderBy(p => p.Order))
{
<div class="editor-row">
#Html.DevExpress().Label(settings =>
{
settings.AssociatedControlName = property.PropertyName;
settings.Text = property.DisplayName;
settings.ToolTip = property.Description;
}).GetHtml()
<span class="editor-field">
#Html.DevExpress().TextBox(settings =>
{
settings.Name = property.PropertyName;
settings.Properties.NullText = property.Watermark;
settings.Width = 200;
settings.Properties.ValidationSettings.RequiredField.IsRequired = property.IsRequired;
settings.ShowModelErrors = true;
}).Bind(ViewData[property.PropertyName]).GetHtml()
</span>
</div>
}
</div>
then the form is laid out very nicely based on the metadata, with labels, tooltips, watermarks etc all pulled out of the model's metadata; but, I would like to be able to group the items together, for instance in a <fieldset> per group. Does anyone know how to get the GroupName out of the metadata, short of writing an extension method for ModelMetadata?

GroupName is not parsed by the DataAnnotationsModelMetadataProvider. So there's no way to get it right off the ModelMetadata object, even with an extension method.
You could implement your own provider that extends the existing one to add support for GroupName, which Brad Wilson explains in his blog.
You could also write your own attribute instead of using Display(GroupName = ) and implement the IMetadataAware interface to add the groupname to ModelMetadata.AdditionalValues.

You can also use this Extension Method:
public static class ModelMetadataExtensions
{
public static T GetPropertyAttribute<T>(this ModelMetadata instance)
where T : Attribute
{
var result = instance.ContainerType
.GetProperty(instance.PropertyName)
.GetCustomAttributes(typeof(T), false)
.Select(a => a as T)
.FirstOrDefault(a => a != null);
return result;
}
}
Then
var display= this.ViewData.ModelMetadata
.GetPropertyAttribute<DisplayAttribute>();
var groupName = display.Groupname;

An alternative could be to use the ShortName property of the DisplayAttribute class. It is exposed by the ModelMetadata class as the ShortDisplayName property.
This isn't exactly what you're looking for, but it will allow you to avoid creating another attribute class...and on top of that, you can take advantage of the localization ability of the DisplayAttribute.

Related

MVC3 DropDownListFor not populating selectedValue

I feel like I'm taking crazy pills. I have a dropdownlist for a view that reads from our database all of the wine producers we have. I want to set the selectedValue to a particular ID driven by the referring page. I can see it picks up the selectedValue in debug, I see the selected value populated (906 for this example), but it doesn't set the dropdownlist to the correct value when the page is rendered, it always defaults to 1 for the default value. I've tried creating the selectList in razor as opposed to my controller, but nothing works. Any help on this would be appreciated, I'm guessing it is something small.
Controller:
if (User.IsInRole("admin"))
{
if (ID != 0)
{
ViewBag.ProducerSelect = new SelectList(db.Producers.OrderBy(p => p.Name), "ProducerID", "Name", ID);
}
else
{
ViewBag.ProducerSelect = new SelectList(db.Producers.OrderBy(p => p.Name), "ProducerID", "Name");
}
}
View:
if (User.IsInRole("producereditor"))
{
<h3>#ViewBag.ProducerName</h3>
}
else
{
<div class="editor-label">
#Html.LabelFor(m => m.Wine.ProducerID, "Producer")
</div>
<div class="editor-field">
#Html.DropDownListFor(m => m.Wine.ProducerID, ViewBag.ProducerSelect as SelectList)
</div>
}
Tried the below but no success:
ViewBag.ProducerSelect = new SelectList(from p in db.Producers
orderby p.Name
select new { p.ProducerID, p.Name }
, "ProducerID", "Name", ID);
If you want to preselect an item, You set that value to your ProducerId property.
var yourViewModelObj=new YourViewModel;
yourViewModelObj.Wine.ProducerId=906; //or whatever value you want
return View(yourViewModelObj);
Suggestion : For better code readablity/Maintenance, Try to avoid ViewBag / ViewData and use a ViewModel to pass the data.
I would add a Property to my ViewModel to hold the Collection of Producers
public class WineViewModel
{
//Other Existing properties also
public IEnumerable<SelectListItem> Producers{ get; set; }
public string SelectedProducer { get; set; }
}
Then in yout GetAction method, you can set the value like this, If you want to set one select option as the default selected one.
public ActionResult CreateWine()
{
var vm=new WineViewModel();
//The below code is hardcoded for demo. you mat replace with DB data.
vm.Producers= new[]
{
new SelectListItem { Value = "1", Text = "Prodcer A" },
new SelectListItem { Value = "2", Text = "Prodcer B" },
new SelectListItem { Value = "3", Text = "Prodcer C" }
};
//Now let's set the default one's value
vm.SelectedProducer = "2";
return View(vm);
}
And in your Strongly typed View,
#Html.DropDownListFor(x => x.SelectedProducer,
new SelectList(Model.Producers, "Value", "Text"), "Select Producer")
The HTML Markup generated by above code will have the HTML select with the option with value 2 as selected one.
I figured this out. I had ViewModel.wine.ProducerID elsewhere on the page in a hidden field, and that defaults to 1, so I just assigned that to passed in value, and it worked great. I knew it was something like that. Thanks!
User a ViewModel ex WineViewModel
public class WineViewModel
{
public Wine Wine { get; set; }
public SelectList PProducerList { get; set; }
public WineViewModel() { }
public WineViewModel(Wine wine)
{
this.Wine = wine;
}
}
Try the following in your controller
var model = new WineViewModel( selectwine);
model.ProjectTypeList = new SelectList( from p in db.Producers
orderby p.Name
select new { p.ID, p.Name }, "ID", "Name")
notice how I am exclusively declaring which is the ID and which is the Value in my SelectList
Then in your view do
#Html.DropDownListFor(model => model.Wine.ProducerID, Model.ProjectTypeList)

In ASP.NET MVC3, how do I manually apply validation on generated properties

Here's the situation, I have a list of about 20 properties (called Attributes) that I've defined in my database. This consists of a name, possible values, an optional regex, a boolean that indicates the field is required, etc.
In my ViewModel I get the list of attributes and in my view as List I have a nice EditorTemplate for AttributeViewModel to show them using Steve Sanderson's cool BeginCollectionItem to make sure the post gets bound back to a list of AttributeViewModel (this works just fine).
My AttributeViewModel looks like this:
public class AttributeViewModel
{
public string Description { get; set; }
public IEnumerable<SelectListItem> Values { get; set; }
public string SelectedValue { get; set; }
public byte RenderAs { get; set; }
public int AttributeID { get; set; }
public int ID { get; set; }
public int RegexValidation { get; set; }
public bool IsRequired { get; set; }
}
My View looks like this (edit.cshtml):
#model Company.Services.ViewModels.StaffMemberViewModel
<h2>Edit</h2>
#using (Html.BeginForm())
{
Some fields here, nothing of interest.
#Html.EditorFor(model => model.AttributeValues)
<input type="submit" value="Send" />
}
Here's the interesting bit though, this is my EditorTemplate for AttributeValues:
#using Company.Web.Helpers // This is where "BeginCollectionItem" lives
#model Company.Services.ViewModels.AttributeViewModel
using (Html.BeginCollectionItem("attributes"))
{
<div class="editor-label">
#Model.Description
</div>
<div class="editor-field">
#Html.DropDownListFor(m => m.SelectedValue, new SelectList(Model.Values, "Value", "Text"), "-- Select --")
#Html.HiddenFor(model => model.AttributeID)
</div>
}
What I would like to do is use the IsRequired and RegexValidation to make sure the SelectedValue for each attribute is valid. How would I go about doing so? If possible, I'd really like to take advantage of the MVC3 validation framework and unobtrusive validation like I "normally" would.
I obviously can't dynamically add a RequiredAttribute or a RegularExpressionAttribute as these differ for each of my attribute objects in the list.
This is untested. You may have to play with this to get your desired result.
First, create your custom DataAnnotationsModelValidatorProvider class:
public class MyModelMetadataValidatorProvider : DataAnnotationsModelValidatorProvider
{
internal static DataAnnotationsModelValidationFactory DefaultAttributeFactory = Create;
internal static Dictionary<Type, DataAnnotationsModelValidationFactory> AttributeFactories =
new Dictionary<Type, DataAnnotationsModelValidationFactory>()
{
{
typeof(RequiredAttribute),
(metadata, context, attribute) => new RequiredAttributeAdapter(metadata, context, (RequiredAttribute)attribute)
},
{
typeof(RegularExpressionAttribute),
(metadata, context, attribute) => new RegularExpressionAttributeAdapter(metadata, context, (RegularExpressionAttribute)attribute)
}
};
internal static ModelValidator Create(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute)
{
return new DataAnnotationsModelValidator(metadata, context, attribute);
}
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
{
List<ModelValidator> vals = base.GetValidators(metadata, context, attributes).ToList();
if (metadata.ModelType.Name == "SelectedValue")
{
// get our parent model
var parentMetaData = ModelMetadataProviders.Current.GetMetadataForProperties(context.Controller.ViewData.Model,
metadata.ContainerType);
// get the associated AttributeId
var attributeId = Convert.ToInt32(parentMetaData.FirstOrDefault(p => p.PropertyName == "AttributeId").Model);
// get AttributeViewModel with specified AttributeId from repository
var attributeViewModel = _db.AttributeViewModels.FirstOrDefault(x => x.AttributeId == attributeId);
DataAnnotationsModelValidationFactory factory;
// check if required
if (attributeViewModel.IsRequired)
{
// must be marked as required
var required = new RequiredAttribute();
required.ErrorMessage = attributeViewModel.Description.Trim() +
" is Required";
if (!AttributeFactories.TryGetValue(required.GetType(), out factory))
factory = DefaultAttributeFactory;
vals.Add(factory(metadata, context, required));
}
// check for regex
if (attributeViewModel.RegexValidation > 0)
{
// get regex from repository
var regexValidation = _db.attributeViewModels.
FirstOrDefault(x => x.RegexValidation == attributeViewModel.RegexValidation);
var regex = new RegularExpressionAttribute(regexValidation.Pattern);
regex.ErrorMessage = attributeViewModel.Description.Trim() +
" is not in a valid format";
if (!AttributeFactories.TryGetValue(regex.GetType(), out factory))
factory = DefaultAttributeFactory;
vals.Add(factory(metadata, context, regex));
}
}
return vals.AsEnumerable();
}
}
Then, add the following to Application_Start in Global.asax.cs:
ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new MyModelMetadataValidatorProvider());
Consider using FluentValidation.Net (which is available via NuGet from the following Install-Package FluentValidation.MVC3). It makes any sort of relatively complex data validation far simpler and more intuitive than a declarative style. There is support for client-side validation too.
I hope I am understanding your question correctly. You want to add custom validation attributes, annotation and validation logic to your views?
If so, you want to go to the System.ComponentModel.DataAnnotation namespace. Your validation logic will be placed in a class deriving from ValidationAttribute:
using System.ComponentModel.DataAnnotation;
public class MyValidationAttribute : ValidationAttribute
{
string readonly _validationParameter;
public MyValidationAttribute(string validationParameter)
{
_validationParameter = validationParameter;
}
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
// add validation logic here
if (//not valid)
{
var errorMessage = FormatErrorMessage(validationContext.DisplayName);
return new ValidationResult(errorMessage);
}
return ValidationResult.Success;
}
}
You can apply the attribute to any model property
[Required]
[MyValidationAttribute("parameter", ErrorMessage="Error in {0}")]
public string MyProperty { get; set; }
I hope this helps. See
Professional ASP.NET MVC 3
page 127 for more info.

one property from my ViewModel will not populate ModelMetadata

I'm experiencing very odd behavior in the way an ASP.NET MVC3 view model is emitted -- for one field, ModelMetadata is not propagated. I'm using the templated helpers after Brad Wilson, though updated for Razor. Here's my view model:
public class FamilyBaseViewModel : ViewModelBase
{
[Display(Order = 10)]
public string FamilyName { get; set; }
[Display(Order = 30)]
[StringLength(50, ErrorMessage = "Street name can only be 50 characters long.")]
public string Street { get; set; }
}
public class FamilyPrivateViewModel : FamilyBaseViewModel
{
[Display(Name = "Date Started", Order = 20)]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:d}")]
[DataType(DataType.Date)]
public DateTime? DateStarted { get; set; }
}
The object.cshtml template runs through the properties and uses Html.Display to show them:
// object.cshtml
<ol>
#foreach (var prop in
ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForDisplay
&& !ViewData.TemplateInfo.Visited(pm)
&& pm.ModelType != typeof(System.Data.EntityState)))
{
<li>
#Html.Display(prop.PropertyName)
</li>
}
</ol>
In the above scenario, all three fields have the right descriptors in the object.cshtml call (prop.DisplayName, prop.TemplateHint), but when the first property -- FamilyName -- is passed to String.cshtml, the ViewData.ModelMetadata is not populated at all. As a result, the template can't display a label (except "String"), nor assign the ID of the control, etc.
Street and DateStarted are emitted normally, with the ID and all. So I'm completely at a loss as to why the one property would fail to set the ViewData properties -- nor do I know how to step through past the Html.Display call to see what might be happening.
Any ideas for a next place to look?
So the problem was in the controller action, which for unrelated reasons used "FamilyName" for a ViewData value:
ViewBag.FamilyName = familyName;
And this caused all heck to break loose in the mapping of model fields with the same name -- that is, ModelMetadata will not propagate. So, the lesson is: don't give ViewData dictionary items keys with the same name as a field in your view model.

validate dropdown field in asp.net mvc 3 razor

code is:
#using (Html.BeginForm("Register", "User", FormMethod.Post, new { id = "RegisterForm" }))
{
#Html.DropDownList("StateId", new SelectList(Model.States, "StateId", "StateName"),
"--Select an option--", new { #tabindex = "11" })
}
i need required field validation for dropdown
Have you tried using Data Annotations in your model to mark the property as required?
[Required(ErrorMessage = "You must select a State")]
Are you exposing the StateId as a part of the Model? If so that's where you should set the Required Attribute, like so:
[Required(ErrorMessage = "You must select a State")]
public int StateId { get; set; }

Form for a different model than the view page in ASP.NET MVC 3

I have Results page that contains an signup form. I'm trying to use client-side validation on the email model, but I can't because the model type is different than the type I'm posting.
class Results
{
... // some results data to display
}
class EmailSignup
{
public virtual int Id { get; set; }
[Required(ErrorMessage = "Please enter your email.")]
[DataType(DataType.EmailAddress)]
[RegularExpression(#"^(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+#((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6}$", ErrorMessage = "Please enter a valid email address.")]
public virtual string Email { get; set; }
}
Results.cshtml
#model MyApp.Results
[display results]
...
#using (Html.BeginForm("SubmitEmail", "AnalysisResults", FormMethod.Post))
{
<fieldset>
#Html.TextBoxFor( model => model.???? )
</fieldset>
}
The type I want the user to submit is EmailSignup, not Results.
Move the form to a partial view that takes an EmailSignup model.
This can be done quite easily. You just have to do it like this:
var contactModel = new ContactModel();
#Html.TextBoxFor(m => contactModel.Title)
#Html.ValidationMessageFor(m => contactModel.Title)
The validation works like a charm.
I have find out 2 more ways
Override the Name attribute for TextBoxFor and set it as the property name.
var formModel = new ForgotPasswordFormModel();
#Html.TextBoxFor(m => formModel.UsernameOrEmail, new { Name = "UsernameOrEmail" })
Specify the same exact model name as the post method parameter.
var formModel = new ForgotPasswordFormModel();
#using (Html.BeginForm("ChangePassword", "LoginSurface")
{
#Html.TextBoxFor(m => formModel.UsernameOrEmail)
}
...
public virtual ActionResult ChangePassword(ForgotPasswordFormModel formModel)
You could create another HtmlHelper like this
var emailSignupHtml = new HtmlHelper<EmailSignup>(Html.ViewContext, new ViewDataContainer<EmailSignup>(new EmailSignup()));
and use it like this
#emailSignupHtml.TextBoxFor(m => m.Email)
For the ViewDataContainer I use following helper class
public class ViewDataContainer<TModel> : ViewDataDictionary<TModel>, IViewDataContainer
{
public ViewDataContainer(TModel model) : base (model)
{
ViewData = new ViewDataDictionary(model);
}
public ViewDataDictionary ViewData { get; set; }
}
I guess you can also try #HTML.Action("EmaialSignup")
and your controller will have a Function calling the partial view
if you have to render multiple model bounded View in this view

Resources