Как получить доступ к другим значениям атрибута из пользовательского атрибута ValidationAttribute в приложении asp.net mvc 2? - PullRequest
3 голосов
/ 15 сентября 2010

Я использую asp.net mvc 2 с C # и DataAnnotations.

Ситуация такова: у меня есть пользовательский элемент управления, который строго типизирован для класса модели. Этот элемент управления отображается в представлении несколько раз, но с различными значениями свойств, такими как заголовки (например, все вопросы: вопрос 1, вопрос 2, вопрос 3 и т. Д.). Я могу написать собственный класс проверки, который проверяет этот объект в целом, но проблема в том, что я не могу получить конкретные теги Html.ValidationMessage (...) для отображения. Ошибки проверки отображаются только в сводке вверху представления, и я хочу, чтобы они отображались как вверху, так и рядом с конкретным элементом управления, который не прошел проверку.

Я пытался создать пользовательские классы проверки, подобные приведенным ниже, для проверки по каждому свойству, но проблема в том, что мне нужны два значения модели: Рейтинг и Заголовок. Фактические проверки бизнеса выполняются по рейтингу, но свойство Заголовок указывает пользователю, какой элемент управления на экране неверен.

    [AttributeUsage(AttributeTargets.Property)]
    public class RatingValidation : ValidationAttribute
     {

      public string Heading { get; private set; }  
      private readonly string ErrorRatingInvalid = "Rating for {0} is invalid. Rating is required and must be between 1 and 5.";

        public override string FormatErrorMessage(string name)
        {
            String errorMsg = ErrorRatingInvalid;
            return String.Format(CultureInfo.CurrentUICulture,
                                 errorMsg, 
                                 Heading);
        }

      public override bool IsValid(object value)
      {   
       bool isValidResult = true;

       if (value == null)
        return false;

        //Trying to do something like the following. This doesn't work because the 
        //attribute is applied to a property so only that property value is passed.
        //In this case, the type of myRatingObject would likely be the same as the 
        //property validated.
        var myRatingObject = TypeDescriptor.GetProperties(value);  
        this.Heading = myRatingObject.Heading;

        if( myRatingObject.Rating < 1 || myRatingObject.Rating > 5)
            isValidResult = false;

       return isValidResult;
      }
     }

Пример класса моей модели:

    public class MyModel
    {
        public MyModel()
        {
            //this.IsEditable = true;
        }        
        public String Heading { get; set; }
        [RatingValidation()]
        public int Rating { get; set; }
    }

Мысли

РЕДАКТИРОВАТЬ 1

Я хотел написать еще немного кода, чтобы лучше объяснить, почему создание отдельных валидаторов классов в этом случае не будет решением.

Напомним, что вся эта проверка входит в настраиваемое частичное представление со строгой типизацией. Точнее говоря, это шаблон редактирования. Основная модель будет иметь список модели того же типа, что и это частичное представление. В целях примера я пересмотрел некоторые из приведенного выше кода, чтобы лучше это проиллюстрировать. Я оставляю код выше этого редактирования нетронутым, поэтому обсуждение, ведущее к этому редактированию, имеет смысл и позволяет увидеть, где начался разговор и в каком направлении он шел.

Простите за код, так как демонстрационное приложение для тестирования это довольно многословно. Надеюсь, это лучше объясняет мою проблему.

    [RatingsValidationAttribute()]
    public class PersonRating
    {
        public String Heading { get; set; }
        public int Index { get; set; }
        public int Rating { get; set; }
    }

    public class Person
    {
        public String FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
        public List<PersonRating> Ratings { get; set; }
    }       

В моем основном представлении у меня есть:

        <%= Html.EditorFor(m => m.Ratings) %>

Элемент управления Ratings View выглядит следующим образом:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ModelBindingResearch.Models.PersonRating>" %>
<%= Html.HiddenFor(m=>m.Heading) %>
<%= Html.HiddenFor(m=>m.Index) %>
 <%= Html.DisplayTextFor(model => model.Heading) %>
 :
 <%= Html.TextBoxFor(model => model.Rating) %>
 <%= Html.ValidationMessageFor(model => model.Rating, "*")%>
 <%= Html.ValidationMessageFor(m=>m) %>
 <br />         

Класс атрибута проверки:

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
        public class RatingsValidationAttribute : ValidationAttribute
        {
            public RatingsValidationAttribute()
            {

            }

            public int Rating { get; private set; }        
            public string Heading { get; set; }        

            private readonly string ErrorRatingInvalid = "Rating for {0} is invalid. Rating is required and must be between 1 and 5.";

            public override string FormatErrorMessage(string name)
            {
                String errorMsg = ErrorRatingInvalid;
                return String.Format(CultureInfo.CurrentUICulture,
                                     errorMsg,
                                     Heading);
            }

            public override bool IsValid(object value)
            {

                bool isValidResult = true;
                PersonRating personRating = (value as PersonRating);

                try
                {                
                    PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value);
                    Heading = personRating.Heading;

                    if (personRating.Rating < 1 ||      //Rating must be b/t 1 & 5
                        personRating.Rating > 5)
                    {
                        isValidResult = false;
                    }
                }
                catch (Exception e)
                {
                    //log error
                }

                return isValidResult;
            }
        }    

Моя тестовая модель связующего. Это еще ничего не делает. Это просто исследовательский. Обратите внимание, что переменная o содержит полную объектную модель и все значения. Этот код сильно заимствует из: ASP.NET MVC - Пользовательское сообщение проверки для типов значений

    public class TestModelBinder : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            Object o = base.BindModel(controllerContext, bindingContext);
            Object obj = bindingContext.Model;
            Person p = (Person)o;
            bindingContext.ModelState.AddModelError("FirstName", "Custom exception thrown during binding for firstname.");
            return o;
        }

        protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, object value)
        {
            base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
            String propertyName = propertyDescriptor.Name;
            Object propertyValue = value; 
        }

        private bool IsFormatException(Exception e)
        {
            if (e == null)
                return false;
            else if (e is FormatException)
                return true;
            else
                return IsFormatException(e.InnerException);
        }
    }           

My Home Controller (только методы, которые я добавил для представления Create):

    public ActionResult Create()
    {
        return View(getPerson());
    }

    [HttpPost]
    public ActionResult Create(Person p)
    {
        return View(p);
    }

    private Person getPerson()
    {
        Person p = new Person();
        Address a = new Address();
        PersonRating pr1 = new PersonRating();
        PersonRating pr2 = new PersonRating();
        PersonRating pr3 = new PersonRating();
        pr1.Heading = "Initiative";
        pr1.Rating = 5;
        pr1.Index = 1;
        pr2.Heading = "Punctuality";
        pr2.Rating = 5;
        pr1.Index = 2;
        pr3.Heading = "Technical Knowledge";
        pr3.Rating = 5;
        pr3.Index = 3;

        a.Street = "555 Somewhere Dr";
        a.City = "City";
        a.State = "AL";
        p.FirstName = "Jason";
        p.LastName = "Rhevax";
        p.Age = 30;
        p.PersonAddress = a;
        p.Ratings.Add(pr1);
        p.Ratings.Add(pr2);
        p.Ratings.Add(pr3);
        return p;
    }

Наконец, Create.aspx полностью:

    <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<ModelBindingResearch.Models.Person>" %>

    <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
        Create
    </asp:Content>

    <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

        <h2>Create</h2>

        <% using (Html.BeginForm()) {%>
            <%= Html.ValidationSummary() %>

            <fieldset>
                <legend>Fields</legend>

                <div class="editor-label">
                    <%= Html.LabelFor(model => model.FirstName) %>
                </div>
                <div class="editor-field">
                    <%= Html.TextBoxFor(model => model.FirstName) %>
                    <%= Html.ValidationMessageFor(model => model.FirstName, "*") %>
                </div>

                <div class="editor-label">
                    <%= Html.LabelFor(model => model.LastName) %>
                </div>
                <div class="editor-field">
                    <%= Html.TextBoxFor(model => model.LastName) %>
                    <%= Html.ValidationMessageFor(model => model.LastName) %>
                </div>

                <div class="editor-label">
                    <%= Html.LabelFor(model => model.Age) %>
                </div>
                <div class="editor-field">
                    <%= Html.TextBoxFor(model => model.Age) %>
                    <%= Html.ValidationMessageFor(model => model.Age) %>
                </div>

                <div class="editor-label">
                    <%= Html.LabelFor(model => model.PersonAddress.Street) %>
                </div>
                <div class="editor-field">
                    <%= Html.TextBoxFor(model => model.PersonAddress.Street)%>
                    <%= Html.ValidationMessageFor(model => model.PersonAddress.Street)%>
                </div>

                <div class="editor-label">
                    <%= Html.LabelFor(model => model.PersonAddress.City) %>
                </div>
                <div class="editor-field">
                    <%= Html.TextBoxFor(model => model.PersonAddress.City)%>
                    <%= Html.ValidationMessageFor(model => model.PersonAddress.City)%>
                </div>

                <div class="editor-label">
                    <%= Html.LabelFor(model => model.PersonAddress.State) %>
                </div>
                <div class="editor-field">
                    <%= Html.TextBoxFor(model => model.PersonAddress.State)%>
                    <%= Html.ValidationMessageFor(model => model.PersonAddress.State)%>
                </div>     
                <div>
                    <%= Html.EditorFor(m => m.Ratings) %>
                </div>                               
                <p>
                    <input type="submit" value="Create" />
                </p>
            </fieldset>

        <% } %>

        <div>
            <%= Html.ActionLink("Back to List", "Index") %>
        </div>

    </asp:Content>      

Наконец, я зарегистрировал связыватель модели в методе application_start global.aspx через:

ModelBinders.Binders.Add(typeof(Person), new TestModelBinder());

Итак, проверка ниже работает, но вот проблема. Сообщение проверки, отображаемое классом RatingsValidationAttribute, является правильным, но я хочу, чтобы это сообщение отображалось только в верхней части страницы. На элементе управления я просто хочу, чтобы текстовое поле «Рейтинги» отображало «*». Вы заметите, что следующее

<%= Html.ValidationMessageFor(model => model.Rating, "*")%>

не отображает звездочку. Я полагаю, это потому, что атрибут проверяется на уровне класса. При проверке ModelState в моём модуле связывания ключ, который на самом деле содержит сообщение об ошибке, похож на: «Ratings [0]», что означает отображение этого сообщения, я должен использовать следующее:

 <%= Html.ValidationMessageFor(model => model)%>

Во-первых, это безобразно. Во-вторых, будет проверено несколько проверок для каждого частичного элемента управления, поэтому я хочу, чтобы ошибка в сводке уточняла, в каком заголовке произошла ошибка, и просто используйте звездочки «*» для обозначения элементов управления, в которых есть ошибка.

Я прошу прощения за длину редактирования. Я пытаюсь обрабатывать вопросы, которые я задаю при переполнении стека, таким образом, что практически нет сомнений в том, какое было разрешение. Не стесняйтесь просить больше кода (в случае, если я забыл опубликовать что-то) или любые другие вопросы.

РЕДАКТИРОВАТЬ 3

Общее примечание о создании атрибута проверки на уровне класса: если проверка завершается неудачно и в ModelState добавляется ошибка, имя поля - это имя класса.Поскольку мой проверяемый класс представляет собой список из 3 объектов, которые содержатся в списке в представлении Model, ключи ModelState выглядят примерно так: Model.ListName [index].

Есть ли способ указать ключ, связанный с сообщением об ошибке, для пользовательского класса проверки, который оценивается на уровне класса?Для настраиваемых атрибутов проверки на уровне полей это не проблема.

РЕДАКТИРОВАТЬ 4

В этом сообщении блога http://blog.ceredir.com/index.php/2010/08/10/mvc2-cross-field-validation/, рассматриваются проблемы этого вопроса.,Моя единственная проблема заключается в том, что мне требуется создать 4 класса для каждой проверки, которую я хочу выполнить, что довольно много.Тем не менее, это первый рабочий пример, который показывает способ применения атрибута проверки к полю / свойству и доступа ко всей модели для этого класса.

Теперь моя единственная оставшаяся проблема - попытка перехватить некоторые позадивалидация сцен, которую фреймворк делает для вас.Я говорю о том, что если у вас есть поле int в вашей форме и вы не указываете значение, платформа добавляет сообщение проверки, например:

Поле _____ обязательно для заполнения.(где ______ - имя поля int).

Это также делает нечто подобное, если вы предоставляете текстовое значение для поля int:

Значение 'asdf' недопустимо для ______,

Из-за моей ситуации, когда эти проверки выполняются на модели, которая повторяется несколько раз, мне нужно изменить эти сообщения, чтобы отобразить свойство Heading объекта в сообщениях выше.Приведенные выше сообщения могут измениться следующим образом:

Поле __________ является обязательным.-> Поле {0} для {1} является обязательным.

Значение 'asdf' недопустимо для (имя поля).-> Значение {0} для {1} недопустимо для {2}.-> Значение рейтинга «asdf» недействительно для технических знаний.

Надеюсь, это имело смысл.Я думаю, что я перевела весь этот вопрос в бессмысленный характер.Я мог бы вернуться и попытаться как-то перефразировать.Было бы полезно, если бы я мог получить какую-то обратную связь.

Ответы [ 4 ]

1 голос
/ 24 сентября 2010

Я хотел бы уточнить предложение Джфара. Я не согласен с тем, что мне нужно ждать до MVC 3; тем не менее, его точка зрения на создание пользовательского связующего для модели - ЕДИНСТВЕННЫЙ способ сделать то, что я пытаюсь сделать. Я пробовал и атрибуты уровня поля / свойства и атрибуты уровня класса. Ничего из этого не достаточно для того типа сообщений, которые мне нужны.

Например, вспомните, что у меня есть ситуация, когда у меня есть шаблонный элемент управления, строго типизированный для модели со свойствами Index, Heading и Rating. Таким образом, страница этих элементов управления будет выглядеть примерно так:

Рубрика: Технические знания Рейтинг: [текстовое поле]

Рубрика: Лидерство Рейтинг: [текстовое поле]

Рубрика: Трудовая этика Рейтинг: [текстовое поле]

... и т. Д. И т. Д.

Для моих проверочных сообщений я хотел что-то очень индивидуальное. Оказывается, мои потребности были слишком специфичны для проверки «из коробки» в MVC 2. Мне нужно было сообщения об ошибках в поле «Рейтинг» для ссылки на заголовок, с которым связан рейтинг. Таким образом, проверка будет выглядеть примерно так:

Требуется рейтинг рабочей этики, который должен быть от 1 до 5. Значение рейтинга «asdf» для трудовой этики недопустимо.

Проблема с атрибутами уровня поля заключается в том, что у них не было доступа к значению заголовка. Кроме того, каждый объект на самом деле содержит поле IsEditable, и если значение этого поля будет ложным, я вообще пропускаю проверки.

Проблема с атрибутами уровня класса в два раза. Во-первых, я не могу контролировать ключ, используемый в ModelStateCollection. По умолчанию используется имя класса и индекс объекта на странице. Это дает результат, похожий на: PersonRating [0], PersonRating [1]. Проблема в том, что это означает, что вы можете иметь только 1 сообщение об ошибке при проверке на уровне класса. Если у вас есть 2 атрибута уровня класса, они оба помещаются в ModelStateCollection с одним и тем же ключом. Я не совсем уверен, как это работает, так как я не думаю, что словарь позволит вам сделать это. Возможно, оно молча терпит неудачу или второе сообщение просто перезаписывает первое. В дополнение к этому, мне все еще нужно, чтобы сами поля имели свои изменения CSS для обозначения ошибки. С проверками на уровне класса я не мог понять, как это сделать, потому что ключ не ссылается на поле ... поэтому я не знаю, какое сообщение идет с каким полем, если я не выполняю жесткие проверки строк, что кажется действительно плохим решением.

Сообщение в блоге, на которое я ссылался ранее, предоставляет решение, но оно требует слишком много кода и усилий для каждого атрибута и кажется просто излишним.

В дополнение ко всему этому, аннотации данных MVC имеют встроенные основные валидации, чтобы спасти вас от себя. Это включает в себя вызов обязательного валидатора, если тип вашей модели не обнуляется. Внутренние проверки MVC также выполняют проверку данных, чтобы убедиться, что вы не пытаетесь передать строковое значение в тип int. В обоих этих случаях я не видел действенного способа исправить сообщение проверки без тщательной проверки текста проверки.

Итак, в конце я написал свой собственный механизм связывания для этого объекта. Хотя это не рекомендуемый способ, я утешаюсь тем, что это действительно единственное жизнеспособное решение в моем случае из-за внутренних проверок, встроенных в инфраструктуру MVC. Моя модель переплета выглядела примерно так:

        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            Object o = base.BindModel(controllerContext, bindingContext);
            string ratingKey = bindingContext.ModelName + ".Rating";            
            PersonRating pr = (PersonRating)o;
            ValueProviderResult ratingVpr = controllerContext.
                                        Controller.
                                            ValueProvider.
                                                GetValue(ratingKey);
            String ratingVal = ratingVpr.AttemptedValue;
            String ratingErrorMessage = getRatingModelErrorMessage(
                                            ratingKey,
                                            ratingVal,
                                            pr);

            if (!String.IsNullOrEmpty(ratingErrorMessage))
            {
                bindingContext.ModelState[ratingKey].Errors.Clear();
                bindingContext.ModelState.AddModelError(ratingKey, ratingErrorMessage);
            }

            return o;

                         }

Метод getRatingModelErrorMessage - это пользовательский метод, который выполняет проверки в поле Rating объекта PersonRating и возвращает строку, представляющую сообщение об ошибке. Если строка имеет значение null, метод getRatingModelErrorMessage не вернул ошибку.

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

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

1 голос
/ 15 сентября 2010

Вы можете использовать атрибут класса для достижения этой цели:

[AttributeUsage(AttributeTargets.Class)]
public class RatingValidation : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        var model = (MyModel)value;
        // TODO: here you have the model so work with the 
        // Rating and Heading properties to perform your 
        // validation logic

        return true;
    }
}

А вы модель:

[RatingValidation]
public class MyModel
{
    public String Heading { get; set; }
    public int Rating { get; set; }
}

Или даже лучше: используйте FluentValidation , который прекрасно интегрируется с ASP.NET MV C.

1 голос
/ 21 сентября 2010

Лучше сформулировать этот вопрос в целом:

Как получить отчет об ошибке, отображаемый для сводки проверки для данного поля формы, с отдельным и отдельным сообщением, отображаемым рядом с полем, для которого применяется проверка?

Пример:

Поле: Рейтинг
Сводное сообщение: «Требуется оценка технических знаний, и она должна быть от 1 до 5». Сообщение о проверке поля: "*"

Надеюсь, это иллюстрирует то, что я пытаюсь сделать. Судя по результатам исследования, кажется, что это невозможно с текущей средой аннотаций данных, если я не разверну хотя бы одного помощника html: создаю свой собственный ValidationSummary или мой собственный помощник ValidationMessage ...

1 голос
/ 15 сентября 2010

Ждите MVC 3. Я серьезно.

Опции настраиваемых атрибутов валидации для всего класса сейчас довольно скудны без каких-либо действительно хороших точек расширяемости.Вы в значительной степени застряли в создании пользовательского ModelBinder, который затем может добавлять значения в ModelState, чтобы делать что-либо сложное с атрибутами проверки.

Используйте атрибуты так же, как вы, а затем определите, какие типы запрашиваются из подшивки, Reflect для поиска атрибутов, а затем при необходимости подтвердите / добавьте в состояние модели.

MVC 3 исправляет эту проблему, но до тех пор, пока вы застряли, создаете свою собственную папку.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...