I'm looking for a solution for a while and all solutions are hacky. All needs for variable length list with unordered indexer is change current CollectionValidator to:
- adding access to collection within CollectionValidator
- adding current index of item which is being validated
-
adding possibility to assign different indexer (indexer field from validated item)
I have no time to create full blown solution so I create next hacky solution but in nicer way.
For the rest of mvc code refer to this:
http://ivanz.com/2011/06/16/editing-variable-length-reorderable-collections-in-asp-net-mvc-part-1/
Shortcut extension method
public static IRuleBuilderOptions<T, IEnumerable<TProperty>> ValidateCollection<T, TProperty>(
this IRuleBuilder<T, IEnumerable<TProperty>> ruleBuilder,
Expression<Func<TProperty, object>> indexerProperty,
Action<CollectionItemsValidator<T, TProperty>.Context> result)
{
return ruleBuilder.SetValidator(new CollectionItemsValidator<T, TProperty>(result, indexerProperty));
}
Validator
public class CollectionItemsValidator<T, TProperty> : NoopPropertyValidator
{
private readonly Action<Context> _result;
private readonly Expression<Func<TProperty, object>> _indexerProperty;
public CollectionItemsValidator(Action<Context> result, Expression<Func<TProperty, object>> indexerProperty)
{
_result = result;
_indexerProperty = indexerProperty;
}
public override IEnumerable<ValidationFailure> Validate(PropertyValidatorContext context)
{
context.MessageFormatter.AppendPropertyName(context.PropertyDescription);
var items = context.PropertyValue as IEnumerable<TProperty>;
if (items == null) return null;
var properties = items as TProperty[] ?? items.ToArray();
if (!properties.Any()) return null;
var ctx = new Context(context, _indexerProperty);
int index = 0;
foreach (var item in properties)
{
ctx.SetItem(item, index);
_result(ctx);
index = index + 1;
}
return ctx.Errors;
}
public class Context
{
private readonly PropertyValidatorContext _context;
private readonly Func<TProperty, object> _indexerProperty;
private readonly IList<ValidationFailure> _errors;
public Context(PropertyValidatorContext context, Expression<Func<TProperty, object>> indexerProperty)
{
_context = context;
_indexerProperty = indexerProperty.Compile();
_errors = new List<ValidationFailure>();
Model = (T)context.Instance;
}
public void AddError(Expression<Func<TProperty, object>> property, string message)
{
var propertyName = property.GetPath();
var newChain = new PropertyChain(_context.ParentContext.PropertyChain);
newChain.Add(_context.Rule.Member);
newChain.AddIndexer(_indexerProperty(Item));
newChain.Add(propertyName);
_errors.Add(new ValidationFailure(newChain.ToString(), message));
}
public IEnumerable<ValidationFailure> Errors { get { return _errors; } }
internal void SetItem(TProperty item, int index)
{
Item = item;
ItemIndex = index;
}
public int ItemIndex { get; private set; }
public TProperty Item { get; private set; }
public T Model { get; private set; }
}
}
Usage example:
public class Model
{
public IEnumerable<Point> Points {get;set;}
}
public class Point
{
public string Index {get;set;}
public DateTime? ReturnDepartureHour {get;set;}
}
RuleFor(x => x.Points).ValidateCollection(
x => x.Index, // <- assign indexer
c =>
{
if (c.ItemIndex != 0 && !c.Item.ReturnDepartureHour.HasValue) // validation ReturnDepartureHour but only if item is not at index zero
{
c.AddError(x => x.ReturnDepartureHour, "Row {0} - return deprature hour is required".Fmt(c.ItemIndex));
}
});
Feel free to comment/use :)
Regards
Darek