Как DataAnnotationsModelBinder работает с пользовательскими моделями представления? - PullRequest
5 голосов
/ 04 мая 2009

Я пытаюсь использовать DataAnnotationsModelBinder для использования аннотаций данных для проверки на стороне сервера в ASP.NET MVC.

Все работает нормально, если мой ViewModel - это простой класс с непосредственными свойствами, такими как

public class Foo
{
    public int Bar {get;set;}
}

Однако DataAnnotationsModelBinder вызывает NullReferenceException при попытке использовать комплекс ViewModel, такой как

public class Foo
{
    public class Baz
    {
        public int Bar {get;set;}
    }

    public Baz MyBazProperty {get;set;}
}

Это большая проблема для представлений, которые отображают более одного объекта LINQ, потому что я действительно предпочитаю использовать пользовательские ViewModel с несколькими объектами LINQ вместо нетипизированных массивов ViewData.

У DefaultModelBinder нет этой проблемы, поэтому похоже на ошибку в DataAnnotationsModelBinder. Есть ли обходной путь к этому?

Редактировать: Возможный обходной путь, конечно, состоит в том, чтобы представить свойства дочернего объекта в классе ViewModel следующим образом:

public class Foo
{
    private Baz myBazInstance;

    [Required]
    public string ExposedBar
    {
        get { return MyBaz.Bar; }
        set { MyBaz.Bar = value; }
    }

    public Baz MyBaz
    {
        get { return myBazInstance ?? (myBazInstance = new Baz()); }
        set { myBazInstance = value; }
    }

    #region Nested type: Baz

    public class Baz
    {
        [Required]
        public string Bar { get; set; }
    }

    #endregion
}

#endregion

Но я бы предпочел не писать весь этот дополнительный код. DefaultModelBinder отлично работает с такими высокотехнологичными устройствами, так что я полагаю, что DataAnnotationsModelBinder тоже должно.

Второе редактирование: Похоже, что это действительно ошибка в DataAnnotationsModelBinder. Тем не менее, есть надежда, что это может быть исправлено до выхода следующей версии ASP.NET MVC Framework. См. эту ветку форума для более подробной информации.

Ответы [ 2 ]

8 голосов
/ 14 мая 2009

Я столкнулся с точно такой же проблемой сегодня. Как и вы, я не привязываю свой View напрямую к моей модели, но использую промежуточный класс ViewDataModel, который содержит экземпляр модели и любые параметры / конфигурации, которые я хотел бы отправить в представление.

Я закончил тем, что изменил BindProperty в DataAnnotationsModelBinder, чтобы обойти NullReferenceException, и мне лично не понравилось, что свойства были связаны только в том случае, если они действительны (см. Причины ниже).

protected override void BindProperty(ControllerContext controllerContext,
                                         ModelBindingContext bindingContext,
                                         PropertyDescriptor propertyDescriptor) {
    string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);

    // Only bind properties that are part of the request
    if (bindingContext.ValueProvider.DoesAnyKeyHavePrefix(fullPropertyKey)) {
        var innerContext = new ModelBindingContext() {
            Model = propertyDescriptor.GetValue(bindingContext.Model),
            ModelName = fullPropertyKey,
            ModelState = bindingContext.ModelState,
            ModelType = propertyDescriptor.PropertyType,
            ValueProvider = bindingContext.ValueProvider
        };

        IModelBinder binder = Binders.GetBinder(propertyDescriptor.PropertyType);
        object newPropertyValue = ConvertValue(propertyDescriptor, binder.BindModel(controllerContext, innerContext));
        ModelState modelState = bindingContext.ModelState[fullPropertyKey];
        if (modelState == null)
        {
            var keys = bindingContext.ValueProvider.FindKeysWithPrefix(fullPropertyKey);
            if (keys != null && keys.Count() > 0)
                modelState = bindingContext.ModelState[keys.First().Key];
        }
        // Only validate and bind if the property itself has no errors
        //if (modelState.Errors.Count == 0) {
            SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
            if (OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, newPropertyValue)) {

                OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
            }
        //}

        // There was an error getting the value from the binder, which was probably a format
        // exception (meaning, the data wasn't appropriate for the field)
        if (modelState.Errors.Count != 0) {
            foreach (var error in modelState.Errors.Where(err => err.ErrorMessage == "" && err.Exception != null).ToList()) {
                for (var exception = error.Exception; exception != null; exception = exception.InnerException) {
                    if (exception is FormatException) {
                        string displayName = GetDisplayName(propertyDescriptor);
                        string errorMessage = InvalidValueFormatter(propertyDescriptor, modelState.Value.AttemptedValue, displayName);
                        modelState.Errors.Remove(error);
                        modelState.Errors.Add(errorMessage);
                        break;
                    }
                }
            }
        }
    }
}

Я также изменил его, чтобы оно всегда связывало данные свойства независимо от того, действительно ли оно или нет. Таким образом, я могу просто передать модель обратно в представление без сброса недопустимых свойств.

Отрывок контроллера

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(ProfileViewDataModel model)
{
    FormCollection form = new FormCollection(this.Request.Form);
    wsPerson service = new wsPerson();
    Person newPerson = service.Select(1, -1);
    if (ModelState.IsValid && TryUpdateModel<IPersonBindable>(newPerson, "Person", form.ToValueProvider()))
    {
        //call wsPerson.save(newPerson);
    }
    return View(model); //model.Person is always bound no null properties (unless they were null to begin with)
}

Мой класс Model (Person) происходит из веб-службы, поэтому я не могу напрямую на них накладывать атрибуты. Я решил это следующим образом:

Пример с вложенными аннотациями данных

[Validation.MetadataType(typeof(PersonValidation))]
public partial class Person : IPersonBindable { } //force partial.

public class PersonValidation
{
    [Validation.Immutable]
    public int Id { get; set; }
    [Validation.Required]
    public string FirstName { get; set; }
    [Validation.StringLength(35)]
    [Validation.Required]
    public string LastName { get; set; }
    CategoryItemNullable NearestGeographicRegion { get; set; }
}

[Validation.MetadataType(typeof(CategoryItemNullableValidation))]
public partial class CategoryItemNullable { }

public class CategoryItemNullableValidation
{
    [Validation.Required]
    public string Text { get; set; }
    [Validation.Range(1,10)]
    public string Value { get; set; }
}

Теперь, если я привяжу поле формы к [ViewDataModel.]Person.NearestGeographicRegion.Text & [ViewDataModel.]Person.NearestGeographicRegion.Value, ModelState начнет их корректную проверку, и DataAnnotationsModelBinder также правильно их связывает.

Этот ответ не окончательный, это результат того, что я почесал голову сегодня днем. Он не был должным образом протестирован, несмотря на то, что он прошел модульные тесты в проекте , который начал Брайан Уилсон, и большую часть моих собственных ограниченных испытаний. Для истинного закрытия по этому вопросу я хотел бы услышать Брэд Уилсон мысли по этому решению.

3 голосов
/ 14 июня 2009

Исправление этой проблемы простое, как отметил Мартин.

В методе BindProperty вы найдете следующую строку кода:

if (modelState.Errors.Count == 0) {

Должен быть изменен на:

if (modelState == null || modelState.Errors.Count == 0) {

Мы намереваемся включить поддержку DataAnnotations в MVC 2, которая будет включать DataAnnotationsModelBinder. Эта функция будет частью первой ОСАГО.

...