Convert String to Enum on http request and set automatically - c#

I can always go and do what is mentioned here. Using Enum.TryParse
However What I am doing is passing a string to the request. Then created a local variable on my controller of the Enum type so that I can pass the Enum type to my service method to perform task. I feel like if I can directly convert it to the type of Enum on the request's set property, that would be great.
This is what I have currently:
public enum SearchSortType
{
None,
Rating,
Date,
Etc
}
[DataContract]
public class MyRequest
{
/// <summary>
/// The field to order on.
/// </summary>
/// <value>
/// The order by.
/// </value>
[DataMember(Name = "sortOn")]
public string SortOn { get; set; }
}
what I want is following. I will keep posting String from the request.
[DataContract]
public class MyRequest
{
/// <summary>
/// The field to order on.
/// </summary>
/// <value>
/// The order by.
/// </value>
[DataMember(Name = "sortOn")]
public SearchSortType SortOn { get; set; }
}
Is it possible to keep passing string and get it converted on the set property ( or any other way to the enum so that I don't have to create a local variable and convert and then use it?

You can write your own model binder that will accept string.
public class EnumModelBinder : DefaultModelBinder
{
/// <summary>
/// Fix for the default model binder's failure to decode enum types when binding to JSON.
/// </summary>
protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
var propertyType = propertyDescriptor.PropertyType;
if (propertyType.IsEnum)
{
var providerValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (null != providerValue)
{
var value = providerValue.RawValue;
if (null != value)
{
var valueType = value.GetType();
if (!valueType.IsEnum)
{
return Enum.ToObject(propertyType, value);
}
}
}
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
Then simply register it in your Global.asax file.
protected override void OnApplicationStarted()
{
base.OnApplicationStarted();
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
// Register your new model binder
ModelBinders.Binders.DefaultBinder = new EnumModelBinder();
}

Enum is a distinct type that consists of a set of named constants called the enumerator list. You can pass the enum in the request and have an enum property on the web api with the same name and .net model binder will automatically bind it to the enum. When the enum is sent in the request it will be send as an Int.

Related

Access model class instance from a custom AdditionalMetadataAttribute (asp.net mvc 5)

I have the following situation - I need to write a custom additional metadata attribute, that based on another property value (from the same model), adds a value to the AdditionalValues dictionary. Right now, my issue is that I'm not able to access the model instance inside my attribute class.
[AttributeUsage(AttributeTargets.Property)]
public class ExtendedAdditionalMetadataAttribute : Attribute, IMetadataAware
{
#region Private properties
private string extraFieldToCheck { get; set; }
private string extraFieldValueToCheck { get; set; }
private string fieldToBeAdded { get; set; }
private string fieldValueToBeAdded { get; set; }
#endregion
#region Constructor
public ExtendedAdditionalMetadataAttribute(string extraFieldToCheck, string extraFieldValueToCheck,
string fieldToBeAdded, string fieldValueToBeAdded)
{
this.extraFieldToCheck = extraFieldToCheck;
this.extraFieldValueToCheck = extraFieldValueToCheck;
this.fieldToBeAdded = fieldToBeAdded;
this.fieldValueToBeAdded = fieldValueToBeAdded;
}
#endregion
public void OnMetadataCreated(ModelMetadata metadata)
{
// HOW TO GET THE MODEL CLASS INSTANCE???
// metadata.ContainerType is correct by metadata.Container is null.
}
}
As you see from the code comments, inside OnMetadataCreated I need to access the Model class instance but, though ContainerType is correct, the Container property is NULL.
Can you please help me by giving me a hint regarding this issue?
THANK YOU IN ADVANCE!
Evdin
LATER EDIT
Considering that I haven't gave to much explanations, I will also paste here an example on how I would like to use this attribute on a model class:
/// <summary>
/// Gets or sets the IsAccountCreated
/// </summary>
/// <value>The IsAccountCreated.</value>
[UIHint("FormFieldStringTemplate")]
[ExtendedAdditionalMetadata("IsExternalAccount", "true", "ReadOnly", "true")]
public override Boolean IsAccountCreated { get; set; }
/// <summary>
/// Gets or sets the IsAccountEnabled
/// </summary>
/// <value>The IsAccountEnabled.</value>
[Display(Name = "Este cont activ?")]
[UIHint("FormFieldStringTemplate")]
[ExtendedAdditionalMetadata("IsExternalAccount", "true", "ReadOnly", "true")]
public override Boolean IsAccountEnabled { get; set; }
/// <summary>
/// Gets or sets the IsExternalAccount
/// </summary>
/// <value>The IsExternalAccount.</value>
[Display(Name = "Este cont extern?")]
[UIHint("FormFieldStringTemplate")]
[AdditionalMetadata("ReadOnly", "true")]
public override Boolean IsExternalAccount { get; set; }
Later & Later Edit
Though the response given by #stephen-muecke is more then simple and acceptable in current situation, for the sake of programming challenge I've looked for other options and I found the following possibility: implementing a custom DataAnnotationsModelMetadataProvider class. In few simple words - it works and I'm able to obtain the model class instance BUT only if the model class is a simple class, otherwise there are many drawbacks - for example if you have a Model class and you use it in your view then it's ok but if you have a class inside another class (a model inside a viewmodel) that this approach is not usable anymore.
Thank you again #stephen-muecke!
Since you seem to need access to multiple properties of the model, the attribute should target class (AttributeTargets.Class) and be applied to the model, not a property. This might mean you need to add another property that is the name of the property you were trying to apply this to. Note metadata.ContainerType only gives you the type, not this instance so you can only get the default value of its properties.
Edit
If the attributes need to be applied to multiple properties in the model, then you cannot access the container in OnMetadataCreated because metadata is created from the innermost properties out so the model's metadata has not yet been created.
Based on OP's comments, a better solution would be to create a custom html helper. For example to generate a textbox that is readonly based on the value of another property
namespace MyHelpers.Html
{
public static class ReadOnlyHelpers
{
public static MvcHtmlString ReadOnlyTextBoxIf<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, bool isReadOnly)
{
object attributes = isReadOnly ? new { #readonly = "readonly" } : null;
return InputExtensions.TextBoxFor(helper, expression, attributes);
}
}
}
and use in your view as
#Html.ReadOnlyTextBoxIf(m => m.SomeTextProperty, Model.SomeBooleanValue)
Creating a 'Readonly' checkbox is a little more difficult because the readonly attribute has no affect with a checkbox. In order to prevent user interaction you need to disable it but that means the value wont post back
public static MvcHtmlString ReadOnlyCheckBoxIf<TModel>(this HtmlHelper<TModel> helper, Expression<Func<TModel, bool>> expression, bool isReadOnly)
{
if (isReadOnly)
{
// If you want to 'visually' render a checkbox (otherwise just render a div with "YES" or "NO")
ModelMetadata metaData = ModelMetadata.FromLambdaExpression(expression, helper.ViewData);
StringBuilder html = new StringBuilder();
// Add a hidden input for postback
html.Append(InputExtensions.HiddenFor(helper, expression).ToString());
// Add a visual checkbox without name so it does not post back
TagBuilder checkbox = new TagBuilder("input");
checkbox.MergeAttribute("type", "checkbox");
checkbox.MergeAttribute("disabled", "disabled");
if ((bool)metaData.Model)
{
checkbox.MergeAttribute("checked", "checked");
}
html.Append(checkbox.ToString());
return MvcHtmlString.Create(html.ToString());
}
else
{
// return normal checkbox
return InputExtensions.CheckBoxFor(helper, expression);
}
}
and use in your view as
#Html.ReadOnlyCheckBoxIf(m => m.IsAccountCreated, Model.IsExternalAccount)

MVC Sending collection and referenced object to a view from BeginForm?

My question is: How to pass Collections and Referenced object types without writing so many HiddenFors?
This is my model:
public partial class AddressObject
{
public int ID { get; set; }
public string KeyNumber { get; set; }
public int AddressID { get; set; }
public int ObjectTypeID { get; set; }
public double ResidentalArea { get; set; }
public short ResidentsNumber { get; set; }
public Nullable<short> TuristBedsNumber { get; set; }
public virtual Address Address { get; set; }
public virtual ObjectType ObjectType { get; set; }
public virtual ICollection<TrashCan> TrashCans { get; set; }
public virtual ICollection<ObjectOwner> ObjectOwners { get; set; }
}
This is my View:
#Html.HiddenFor(model => model.Address.CityID);
#Html.HiddenFor(model => model.Address.Hood);
#Html.HiddenFor(model => model.Address.ID);
#Html.HiddenFor(model => model.Address.Number);
#Html.HiddenFor(model => model.Address.Region);
#Html.HiddenFor(model => model.Address.Street);
#Html.HiddenFor(model => model.Address.City.Addresses);
#Html.HiddenFor(model => model.Address.City.ID);
#Html.HiddenFor(model => model.Address.City.Name);
#Html.HiddenFor(model => model.Address.City.PostalCode);
#Html.HiddenFor(model => model.AddressID);
#Html.HiddenFor(model => model.ObjectOwners);
#Html.HiddenFor(model => model.ObjectType.ID);
#Html.HiddenFor(model => model.ObjectType.Type);
#Html.HiddenFor(model => model.ObjectTypeID);
#Html.HiddenFor(model => model.TrashCans);
#Html.HiddenFor(model => model.TuristBedsNumber);
I don't want to write everything for Address. I just want to pass Address. My collection is ObjectOwners. I want to do the same thing. Solution exists?
EDIT: I have Controller and method in in it ActionResult(AddressObject addressObject). Inside that controller, I'm calling a unitOfWork with repository for saving that entity. My
HiddenFors are wrapped with #HtmlBeginForm("Save", "AddressObject"). When I pass my model to controller, my Address object AddressObject.Address is null = Count 0. I want to be able to pass whole object with my referenced objects and collection without writing hiddenfors for all properties of referenced object or collection.
EDIT2:
I have master detail scenario(that's not matter), textboxes are binded to AddressObject for my View's model. So I have #Html.TextBoxFor(m => m.AddressID) for example. The problem is, when I change for example AddressObject.TurisBedsNumber, every referenced object or collection become null and because of that my AddressObject is not persistent when it is passed from View to the Controller back with my updated properties. I want my references and other properties be untouched as they were before Updating. I've tried with Mvc Futures and Serializing whole object and my object and its collections and referenced objects are ok. The problem is, when I deserialize my object, that object is not updated with new TuristBedNumber properties; it's old value. I want to know how to save state of my collection and other objects. I can save my state with HiddenFor helper (too many properties to write) or I could get AddressObject or Collection from my repository and update it in my controller; again, too many properties. I want to be able to say "hey you collection and my referenced objects, you'll not change no matter what". I want to serialize them whole, but only them.
Someone asked for controller but it is common:
public ActionResult(AddressObject addressObject) { unitOfWork.Update(addressObject) }
Yes I have ValidState etc....
The following is a helper class I use for creating hidden inputs for properties. If the property is a complex type, its called recursively to create hidden inputs for each property of the complex type. In your case you would use #Html.HiddenInputFor(m => m.Address) to generate all 17 inputs. Be sure to add the namespace in your web.config.
using System;
using System.Collections;
using System.Linq.Expressions;
using System.Text;
using System.Web.Mvc;
using Sandtrap.Web.Extensions;
namespace Sandtrap.Web.Html
{
/// <summary>
///
/// </summary>
public static class HiddenInputHelper
{
/// <summary>
/// Returns the html for a hidden input(s) of a property.
/// </summary>
/// <typeparam name="TModel"></typeparam>
/// <typeparam name="TValue"></typeparam>
/// <param name="helper"></param>
/// <param name="expression"></param>
/// <param name="includeID">
/// A value indicating the the 'id' attribute should be rendered for the input.
/// </param>
/// <remarks>
/// If the property is a complex type, the methods is called recursively for each property
/// of the type. Collections and complex types with null value (except those with the
/// Required attribute) are ignored.
/// </remarks>
public static MvcHtmlString HiddenInputFor<TModel, TValue>
(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, bool includeID = false)
{
string name = ExpressionHelper.GetExpressionText(expression);
ModelMetadata metaData = ModelMetadata.FromLambdaExpression(expression, helper.ViewData);
StringBuilder html = new StringBuilder();
return MvcHtmlString.Create(HiddenInput(metaData, name, includeID));
}
/// <summary>
/// Returns the html for a hidden input(s) of a property defined by its metadata.
/// The string is not html-encoded.
/// </summary>
/// <param name="metaData">
/// The metadata of the property.
/// </param>
/// <param name="name">
/// The name of the property (rendered as the 'name' attribute).
/// </param>
/// <param name="includeID">
/// A value indicating the the 'id' attribute should be rendered for the input.
/// </param>
/// <remarks>
/// If the property is a complex type, the methods is called recursively for each property
/// of the type. Collections and complex types with null value (except those with the
/// Required attribute) are ignored.
/// </remarks>
public static string HiddenInputForMetadata(ModelMetadata metaData, string name, bool includeID = false)
{
return HiddenInput(metaData, name, includeID);
}
#region .Helper methods
/// <summary>
/// Returns the html for a hidden input(s) of a property.
/// </summary>
/// <param name="metaData">
/// The property metadata.
/// </param>
/// <param name="name">
/// The name of the property (rendered as the 'name' attribute).
/// </param>
/// <param name="includeID">
/// A value indicating the the 'id' attribute should be rendered for the input.
/// </param>
private static string HiddenInput(ModelMetadata metaData, string name, bool includeID)
{
StringBuilder html = new StringBuilder();
if (metaData.ModelType.IsArray && metaData.Model != null)
{
// Primarily for database time stamps, this need to called before checking IsComplexType
// otherwise an endless loop is created
html.Append(HiddenInput(name, Convert.ToBase64String(metaData.Model as byte[]), includeID));
}
else if (metaData.IsComplexType)
{
foreach (ModelMetadata property in metaData.Properties)
{
if (property.IsCollection() && !property.ModelType.IsArray)
{
// This would just render the Count and Capacity property of List<T>
continue;
}
if (property.Model == null && property.ModelType != typeof(string) && !property.IsRequired)
{
// Ignore complex types that are null and do not have the RequiredAttribute
continue;
}
// Recursive call to render a hidden input for the property
string prefix = string.Format("{0}.{1}", name, property.PropertyName);
html.Append(HiddenInput(property, prefix, includeID));
}
}
else
{
html.Append(HiddenInput(name, metaData.Model, includeID));
}
return html.ToString();
}
/// <summary>
/// Returns the html for a hidden input.
/// </summary>
/// <param name="name">
/// The name of the property.
/// </param>
/// <param name="value">
/// The value of the property.
/// </param>
/// <param name="includeID">
/// A value indicating the the 'id' attribute should be rendered for the input.
/// </param>
/// <returns></returns>
private static string HiddenInput(string name, object value, bool includeID)
{
TagBuilder input = new TagBuilder("input");
input.MergeAttribute("type", "hidden");
if (includeID)
{
input.MergeAttribute("id", HtmlHelper.GenerateIdFromName(name));
}
input.MergeAttribute("name", name);
input.MergeAttribute("value", string.Format("{0}", value));
return input.ToString();
}
#endregion
}
}
The following extension method is also required
public static bool IsCollection(this ModelMetadata metaData)
{
if (metaData.ModelType == typeof(string))
{
return false;
}
return typeof(IEnumerable).IsAssignableFrom(metaData.ModelType);
}

How to set ExportMetaData with multiple values as well as single w/ custom attribute?

I have the following ExportMetaData attributes set on my class:
[Export(typeof(IDocumentViewer))]
[ExportMetadata("Name", "MyViewer")]
[ExportMetadata("SupportsEditing", true)]
[ExportMetadata("Formats", DocFormat.DOC, IsMultiple = true)]
[ExportMetadata("Formats", DocFormat.DOCX, IsMultiple = true)]
[ExportMetadata("Formats", DocFormat.RTF, IsMultiple = true)]
I also have a supporting interface:
public interface IDocumentViewerMetaData {
/// <summary>
/// Gets the format.
/// </summary>
/// <value>The format.</value>
IEnumerable<DocFormat> Formats { get; }
/// <summary>
/// Gets the name of the viewer
/// </summary>
/// <value>The name.</value>
string Name { get; }
/// <summary>
/// Gets a value indicating whether this viewer supports editing
/// </summary>
/// <value><c>true</c> if [supports editing]; otherwise, <c>false</c>.</value>
bool SupportsEditing { get; }
}
And of course my ImportMany:
[ImportMany(typeof(IDocumentViewer))]
public IEnumerable<Lazy<IDocumentViewer, IDocumentViewerMetaData>> _viewers { get; set; }
What I would like to do is use a strongly-typed attribute class instead of using the ExportMetaData attribute. I have not figured out a way to do this while also supporting single values (Name, SupportsEditing, in the example above).
I envision doing something similiar the following (or whatever is suggested as best):
[Export(typeof(IDocumentViewer))]
[DocumentViewerMetadata(Name = "MyViewer")]
[DocumentViewerMetadata(SupportsEditing = true)]
[DocumentViewerMetadata(Format = DocFormat.DOC)]
[DocumentViewerMetadata(Format = DocFormat.DOCX)]
I am fairly certain that there IS a way to do this, I just haven't found the right way to connect the dots. :)
You can subclass the ExportAttribute with your own implementation, and decorate it with a MetadataAttribute to allow MEF to use its properties to project the metadata proxy it uses during composition:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property),
MetadataAttribute]
public class ExportDocumentViewerAttribute : ExportAttribute, IDocumentViewerMetadata
{
public ExportDocumentViewer(string name, bool supportsEditing, params DocFormat[] formats)
: base(typeof(IDocumentViewer))
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException("Export requires a name", "name");
Name = name;
SupportsEditing = supportsEditing;
Formats = formats ?? Enumerable.Empty<DocFormat>();
}
public string Name { get; private set; }
public bool SupportsEditing { get; private set; }
public IEnumerable<DocFormat> Formats { get; private set; }
}
[ExportDocumentViewer("Word", true, DocFormat.DOC, DocFormat.DOCX)]
public WordDocumentViewer : IDocumentViewer
{
// Stuff
}
Note you don't actually need to decorate it with your IDocumentViewerMetadata contract, as MEF will project it regardless, I just prefer to so that I know if I make changes to the metadata contract, that my custom export attribute conforms.

c# LINQ to Entities: unsupported type

in a solution I've got a interface class IUser and, in another project, a class implementing User implementing IUser.
Executing Linq queries on IUser objects throws me the following exception:
The specified type member 'Username' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.
Can you please help me to solve this? Best Regards
Sample query(Users() returns IQueryable<IUser>:
var c= (from user in bank.Users()
where user.Username == "anUser"
select user);
here below some code:
//the interface
public interface IUser
{
string Username { get; }
}
// the implementation
public partial class User : IUser
{
public string Bankname
{
get
{
return bank_name;
}
}
// the partial class generated from the entity model
[EdmEntityTypeAttribute(NamespaceName="Database1Model", Name="User")]
[Serializable()]
[DataContractAttribute(IsReference=true)]
public partial class User : EntityObject
{
#region Factory Method
/// <summary>
/// Create a new User object.
/// </summary>
/// <param name="user_name">Initial value of the user_name property.</param>
/// <param name="pass_hash">Initial value of the pass_hash property.</param>
/// <param name="is_admin">Initial value of the is_admin property.</param>
/// <param name="bank_name">Initial value of the bank_name property.</param>
/// <param name="is_enabled">Initial value of the is_enabled property.</param>
public static User CreateUser(global::System.String user_name, global::System.String pass_hash, global::System.Boolean is_admin, global::System.String bank_name, global::System.Boolean is_enabled)
{
User user = new User();
user.user_name = user_name;
user.pass_hash = pass_hash;
user.is_admin = is_admin;
user.bank_name = bank_name;
user.is_enabled = is_enabled;
return user;
}
#endregion
#region Primitive Properties
/// <summary>
/// No Metadata Documentation available.
/// </summary>
[EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
[DataMemberAttribute()]
public global::System.String user_name
{
get
{
return _user_name;
}
set
{
if (_user_name != value)
{
Onuser_nameChanging(value);
ReportPropertyChanging("user_name");
_user_name = StructuralObject.SetValidValue(value, false);
ReportPropertyChanged("user_name");
Onuser_nameChanged();
}
}
}
private global::System.String _user_name;
partial void Onuser_nameChanging(global::System.String value);
partial void Onuser_nameChanged();
.
.
.
.
}
Your UserName property doesn't have a Set accessor.
If your property doesn't implement a set accessor, the Entity Framework can't set the value of the property in that entity :)
You will need to define a setter for UserName, this could be done in your concrete class.
i got through this using .ToArray().AsQueryable(); on my IQueryable result..., in the example right into the method bank.Users().
Thanks for your help anyway
Best Regards

How can I tell the Data Annotations validator to also validate complex child properties?

Can I automatically validate complex child objects when validating a parent object and include the results in the populated ICollection<ValidationResult>?
If I run the following code:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ConsoleApplication1
{
public class Person
{
[Required]
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
[Required]
public string Street { get; set; }
[Required]
public string City { get; set; }
[Required]
public string State { get; set; }
}
class Program
{
static void Main(string[] args)
{
Person person = new Person
{
Name = null,
Address = new Address
{
Street = "123 Any St",
City = "New York",
State = null
}
};
var validationContext = new ValidationContext(person, null, null);
var validationResults = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(person, validationContext, validationResults);
Console.WriteLine(isValid);
validationResults.ForEach(r => Console.WriteLine(r.ErrorMessage));
Console.ReadKey(true);
}
}
}
I get the following output:
False
The Name field is required.
But I was expecting something similar to:
False
The Name field is required.
The State field is required.
I offered a bounty for a better child object validation solution but didn't get any takers, ideally
validating child objects to an arbitrary depth
handling multiple errors per object
correctly identifying the validation errors on the child object fields.
I'm still surprised the framework doesn't support this.
Issue - Model Binder Order
This is, unfortunately, the standard behavior of Validator.TryValidateObject which
does not recursively validate the property values of the object
As pointed out in Jeff Handley's article on Validating Object and Properties with the Validator, by default, the validator will validate in order:
Property-Level Attributes
Object-Level Attributes
Model-Level implementation IValidatableObject
The problem is, at each step of the way...
If any validators are invalid, Validator.ValidateObject will abort validation and return the failure(s)
Issue - Model Binder Fields
Another possible issue is that the model binder will only run validation on objects that it has decided to bind. For example, if you don't provide inputs for fields within complex types on your model, the model binder won't need to check those properties at all because it hasn't called the constructor on those objects. According to Brad Wilson's great article on Input Validation vs. Model Validation in ASP.NET MVC:
The reason we don't "dive" into the Address object recursively is that there was nothing in the form that bound any values inside of Address.
Solution - Validate Object at the same time as Properties
One way to solve this problem is to convert object-level validations to property level validation by adding a custom validation attribute to the property that will return with the validation result of the object itself.
Josh Carroll's article on Recursive Validation Using DataAnnotations provides an implementation of one such strategy (originally in this SO question). If we want to validate a complex type (like Address), we can add a custom ValidateObject attribute to the property, so it is evaluated on the first step
public class Person {
[Required]
public String Name { get; set; }
[Required, ValidateObject]
public Address Address { get; set; }
}
You'll need to add the following ValidateObjectAttribute implementation:
public class ValidateObjectAttribute: ValidationAttribute {
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
var results = new List<ValidationResult>();
var context = new ValidationContext(value, null, null);
Validator.TryValidateObject(value, context, results, true);
if (results.Count != 0) {
var compositeResults = new CompositeValidationResult(String.Format("Validation for {0} failed!", validationContext.DisplayName));
results.ForEach(compositeResults.AddResult);
return compositeResults;
}
return ValidationResult.Success;
}
}
public class CompositeValidationResult: ValidationResult {
private readonly List<ValidationResult> _results = new List<ValidationResult>();
public IEnumerable<ValidationResult> Results {
get {
return _results;
}
}
public CompositeValidationResult(string errorMessage) : base(errorMessage) {}
public CompositeValidationResult(string errorMessage, IEnumerable<string> memberNames) : base(errorMessage, memberNames) {}
protected CompositeValidationResult(ValidationResult validationResult) : base(validationResult) {}
public void AddResult(ValidationResult validationResult) {
_results.Add(validationResult);
}
}
Solution - Validate Model at the Same time as Properties
For objects that implement IValidatableObject, when we check the ModelState, we can also check to see if the model itself is valid before returning the list of errors. We can add any errors we want by calling ModelState.AddModelError(field, error). As specified in How to force MVC to Validate IValidatableObject, we can do it like this:
[HttpPost]
public ActionResult Create(Model model) {
if (!ModelState.IsValid) {
var errors = model.Validate(new ValidationContext(model, null, null));
foreach (var error in errors)
foreach (var memberName in error.MemberNames)
ModelState.AddModelError(memberName, error.ErrorMessage);
return View(post);
}
}
Also, if you want a more elegant solution, you can write the code once by providing your own custom model binder implementation in Application_Start() with ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());. There are good implementations here and here
I also ran into this, and found this thread. Here's a first pass:
namespace Foo
{
using System.ComponentModel.DataAnnotations;
using System.Linq;
/// <summary>
/// Attribute class used to validate child properties.
/// </summary>
/// <remarks>
/// See: http://stackoverflow.com/questions/2493800/how-can-i-tell-the-data-annotations-validator-to-also-validate-complex-child-pro
/// Apparently the Data Annotations validator does not validate complex child properties.
/// To do so, slap this attribute on a your property (probably a nested view model)
/// whose type has validation attributes on its properties.
/// This will validate until a nested <see cref="System.ComponentModel.DataAnnotations.ValidationAttribute" />
/// fails. The failed validation result will be returned. In other words, it will fail one at a time.
/// </remarks>
public class HasNestedValidationAttribute : ValidationAttribute
{
/// <summary>
/// Validates the specified value with respect to the current validation attribute.
/// </summary>
/// <param name="value">The value to validate.</param>
/// <param name="validationContext">The context information about the validation operation.</param>
/// <returns>
/// An instance of the <see cref="T:System.ComponentModel.DataAnnotations.ValidationResult"/> class.
/// </returns>
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var isValid = true;
var result = ValidationResult.Success;
var nestedValidationProperties = value.GetType().GetProperties()
.Where(p => IsDefined(p, typeof(ValidationAttribute)))
.OrderBy(p => p.Name);//Not the best order, but at least known and repeatable.
foreach (var property in nestedValidationProperties)
{
var validators = GetCustomAttributes(property, typeof(ValidationAttribute)) as ValidationAttribute[];
if (validators == null || validators.Length == 0) continue;
foreach (var validator in validators)
{
var propertyValue = property.GetValue(value, null);
result = validator.GetValidationResult(propertyValue, new ValidationContext(value, null, null));
if (result == ValidationResult.Success) continue;
isValid = false;
break;
}
if (!isValid)
{
break;
}
}
return result;
}
}
}
You will need to make your own validator attribute (eg, [CompositeField]) that validates the child properties.

Categories

Resources