Walidacja warunkowa ASP.NET MVC

129

Jak używać adnotacji danych do warunkowej walidacji modelu?

Na przykład, powiedzmy, że mamy następujący model (osoba i starszy):

public class Person
{
    [Required(ErrorMessage = "*")]
    public string Name
    {
        get;
        set;
    }

    public bool IsSenior
    {
        get;
        set;
    }

    public Senior Senior
    {
        get;
        set;
    }
}

public class Senior
{
    [Required(ErrorMessage = "*")]//this should be conditional validation, based on the "IsSenior" value
    public string Description
    {
        get;
        set;
    }
}

I następujący widok:

<%= Html.EditorFor(m => m.Name)%>
<%= Html.ValidationMessageFor(m => m.Name)%>

<%= Html.CheckBoxFor(m => m.IsSenior)%>
<%= Html.ValidationMessageFor(m => m.IsSenior)%>

<%= Html.CheckBoxFor(m => m.Senior.Description)%>
<%= Html.ValidationMessageFor(m => m.Senior.Description)%>

Chciałbym być warunkowym wymaganym polem właściwości "Senior.Description" na podstawie wyboru właściwości "IsSenior" (true -> wymagane). Jak zaimplementować walidację warunkową w ASP.NET MVC 2 z adnotacjami danych?

Peter Stegnar
źródło
1
Niedawno zadałem podobne pytanie: stackoverflow.com/questions/2280539/…
Darin Dimitrov
Jestem zmieszany. SeniorObiekt zawsze jest starszy, więc dlaczego IsSenior mogą być fałszywe w tym przypadku. Czy nie potrzebujesz tylko, aby właściwość „Person.Senior” miała wartość null, gdy Person.IsSeniorjest fałszywa. Albo dlaczego nie realizować IsSeniorwłasności w następujący sposób: bool IsSenior { get { return this.Senior != null; } }.
Steven
Steven: „IsSenior” tłumaczy się na pole wyboru w widoku. Kiedy użytkownik zaznaczy checkBox "IsSenior", wtedy pole "Senior.Description" staje się obowiązkowe.
Peter Stegnar
Darin Dimitrov: Cóż, w pewnym sensie, ale nie do końca. Widzisz, jak możesz osiągnąć to, że komunikat o błędzie jest dołączony do określonego pola? Jeśli przeprowadzisz walidację na poziomie obiektu, pojawi się błąd na poziomie obiektu. Potrzebuję błędu na poziomie nieruchomości.
Peter Stegnar

Odpowiedzi:

152

Istnieje znacznie lepszy sposób dodawania reguł walidacji warunkowej w MVC3; niech twój model odziedziczy IValidatableObjecti zaimplementuj Validatemetodę:

public class Person : IValidatableObject
{
    public string Name { get; set; }
    public bool IsSenior { get; set; }
    public Senior Senior { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) 
    { 
        if (IsSenior && string.IsNullOrEmpty(Senior.Description)) 
            yield return new ValidationResult("Description must be supplied.");
    }
}

Przeczytaj więcej w artykule Wprowadzenie do ASP.NET MVC 3 (wersja zapoznawcza 1) .

viperguynaz
źródło
2
Niestety Microsoft umieścił to w złej warstwie - walidacja jest logiką biznesową, a ten interfejs znajduje się w bibliotece DLL System.Web. Aby z tego skorzystać, musisz uzależnić swoją warstwę biznesową od technologii prezentacji.
NightOwl888
7
zrobisz, jeśli go wdrożysz
viperguynaz
4
falconwebtech.com/post/… - @viperguynaz to nie działa
Smit Patel
1
@RayLoveless powinno być powołanie ModelState.IsValid- nie nazywając Weryfikuj bezpośrednio
viperguynaz
1
Wiem, że to starszy wątek, ale do zakończenia - @viperguynaz - aby powiązać błąd z określoną właściwością (np. W przypadku wyświetlania błędu walidacji powiązania modelu po stronie klienta), użyj zamiast tego przeciążonej metody. "yield return new ValidationResult (" ErrorMessage. ", new [] {" PutNameOfPropertyHere "}); Bez tego błąd walidacji jest ogólny i wymagałby wyświetlenia etykiety ostrzegawczej typu catch-all w widoku.
user3280560
63

Rozwiązałem to obsługując słownik „ModelState” , który jest zawarty w kontrolerze. Słownik ModelState zawiera wszystkie elementy członkowskie, które muszą zostać zweryfikowane.

Oto rozwiązanie:

Jeśli potrzebujesz zaimplementować walidację warunkową na podstawie jakiegoś pola (np. Jeśli A = prawda, wtedy B jest wymagane), zachowując komunikaty o błędach na poziomie właściwości (nie dotyczy to niestandardowych walidatorów, które są na poziomie obiektu), możesz to osiągnąć obsługując „ModelState”, po prostu usuwając z niego niechciane walidacje.

... w jakiejś klasie ...

public bool PropertyThatRequiredAnotherFieldToBeFilled
{
  get;
  set;
}

[Required(ErrorMessage = "*")] 
public string DepentedProperty
{
  get;
  set;
}

... lekcja trwa ...

... W jakiejś akcji kontrolera ...

if (!PropertyThatRequiredAnotherFieldToBeFilled)
{
   this.ModelState.Remove("DepentedProperty");
}

...

Dzięki temu uzyskujemy walidację warunkową, pozostawiając wszystko inne bez zmian.


AKTUALIZACJA:

Oto moja ostateczna implementacja: użyłem interfejsu w modelu i atrybutu akcji, który weryfikuje model, który implementuje wspomniany interfejs. Interfejs przepisuje metodę Validate (ModelStateDictionary modelState). Atrybut w akcji po prostu wywołuje Validate (modelState) w IValidatorSomething.

Nie chciałem komplikować tej odpowiedzi, więc nie wspomniałem o ostatecznych szczegółach implementacji (które na końcu mają znaczenie w kodzie produkcyjnym).

Peter Stegnar
źródło
17
Wadą jest to, że jedna część logiki walidacji znajduje się w modelu, a druga w kontrolerze (-ach).
Kristof Claes
Oczywiście nie jest to konieczne. Podaję tylko najbardziej podstawowy przykład. Zaimplementowałem to z interfejsem na modelu i atrybutem akcji, który waliduje model, który implementuje wspomniany interfejs. Interfejs analizuje metodę Validate (ModelStateDictionary modelState). W końcu wykonujesz całą walidację w modelu. W każdym razie słuszna uwaga.
Peter Stegnar
Podoba mi się prostota tego podejścia w międzyczasie, dopóki zespół MVC nie zbuduje czegoś lepszego po wyjęciu z pudełka. Ale czy Twoje rozwiązanie działa z włączoną weryfikacją po stronie klienta?
Aaron,
2
@Aaron: Cieszę się, że podoba Ci się rozwiązanie, ale niestety to rozwiązanie nie działa z walidacją po stronie klienta (ponieważ każdy atrybut walidacji wymaga implementacji JavaScript). Możesz sobie pomóc atrybutem „Remote”, więc wyemitowane zostanie tylko wywołanie Ajax w celu sprawdzenia poprawności.
Peter Stegnar,
Czy jesteś w stanie rozwinąć tę odpowiedź? To ma sens, ale chcę się upewnić, że jestem kryształem. Mam do czynienia z dokładnie taką sytuacją i chcę ją rozwiązać.
Richard B
37

Miałem wczoraj ten sam problem, ale zrobiłem to w bardzo czysty sposób, który działa zarówno po stronie klienta, jak i po stronie serwera.

Warunek: na podstawie wartości innej właściwości w modelu chcesz ustawić inną właściwość jako wymaganą. Oto kod

public class RequiredIfAttribute : RequiredAttribute
{
    private String PropertyName { get; set; }
    private Object DesiredValue { get; set; }

    public RequiredIfAttribute(String propertyName, Object desiredvalue)
    {
        PropertyName = propertyName;
        DesiredValue = desiredvalue;
    }

    protected override ValidationResult IsValid(object value, ValidationContext context)
    {
        Object instance = context.ObjectInstance;
        Type type = instance.GetType();
        Object proprtyvalue = type.GetProperty(PropertyName).GetValue(instance, null);
        if (proprtyvalue.ToString() == DesiredValue.ToString())
        {
            ValidationResult result = base.IsValid(value, context);
            return result;
        }
        return ValidationResult.Success;
    }
}

Tutaj PropertyName jest właściwością, dla której chcesz ustawić swój warunek DesiredValue to konkretna wartość PropertyName (właściwość), dla której Twoja druga właściwość musi zostać zweryfikowana pod kątem wymaganej

Powiedz, że masz następujące rzeczy

public class User
{
    public UserType UserType { get; set; }

    [RequiredIf("UserType", UserType.Admin, ErrorMessageResourceName = "PasswordRequired", ErrorMessageResourceType = typeof(ResourceString))]
    public string Password
    {
        get;
        set;
    }
}

Na koniec zarejestruj adapter dla swojego atrybutu, aby mógł przeprowadzić walidację po stronie klienta (umieściłem go w global.asax, Application_Start)

 DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredIfAttribute),typeof(RequiredAttributeAdapter));
Dan Hunex
źródło
To był pierwotny punkt wyjścia miroprocessordev.blogspot.com/2012/08/…
Dan Hunex
Czy istnieje równoważne rozwiązanie w asp.net mvc2? ValidationResult, ValidationContext klasy nie są dostępne w asp.net mvc2 (.NET Framework 3.5)
User_MVC
2
Działa to tylko po stronie serwera, jak stwierdza linkowany blog
Pakman
2
Udało mi się to uruchomić po stronie klienta z MVC5, ale w kliencie uruchamia walidację bez względu na DesiredValue.
Geethanga
1
@Dan Hunex: W MVC4 nie udało mi się poprawnie działać po stronie klienta i uruchamia walidację bez względu na wartość DesiredValue. Wszelkie pls pomocy?
Jack
35

Używam tego niesamowitego obiektu nuget, który wykonuje dynamiczne adnotacje ExpressiveAnnotations

Możesz zweryfikować dowolną logikę, o jakiej marzysz:

public string Email { get; set; }
public string Phone { get; set; }
[RequiredIf("Email != null")]
[RequiredIf("Phone != null")]
[AssertThat("AgreeToContact == true")]
public bool? AgreeToContact { get; set; }
Korayem
źródło
3
Biblioteka ExpressiveAnnotation jest najbardziej elastycznym i ogólnym rozwiązaniem spośród wszystkich odpowiedzi. Dzięki za udostępnienie!
Sudhanshu Mishra
2
Waliłem głowę, próbując znaleźć rozwiązanie na porządny dzień. Ekspresyjne adnotacje wydają się być rozwiązaniem dla mnie!
Caverman
Biblioteka ExpressiveAnnotation jest niesamowita!
Doug Knudsen
1
Obsługuje również klienta!
Nattrass
1
Brak wsparcia dla .NET Core i nie wygląda na to, żeby tak się stało.
gosr
18

Możesz wyłączyć walidatory warunkowo, usuwając błędy z ModelState:

ModelState["DependentProperty"].Errors.Clear();
Pavel Chuchuva
źródło
6

Obecnie istnieje platforma, która wykonuje tę warunkową walidację (wśród innych przydatnych walidacji adnotacji danych) po wyjęciu z pudełka: http://foolproof.codeplex.com/

W szczególności przyjrzyj się walidatorowi [RequiredIfTrue ("IsSenior")]. Umieszczasz to bezpośrednio na właściwości, którą chcesz zweryfikować, dzięki czemu uzyskujesz pożądane zachowanie błędu walidacji powiązanego z właściwością „Senior”.

Jest dostępny jako pakiet NuGet.

bojingo
źródło
3

Musisz walidować na poziomie osoby, a nie na poziomie Senior, lub Senior musi mieć odniesienie do osoby nadrzędnej. Wydaje mi się, że potrzebujesz mechanizmu samooceny, który definiuje walidację na osobie, a nie na jednej z jej właściwości. Nie jestem pewien, ale wydaje mi się, że DataAnnotations nie obsługuje tego po wyjęciu z pudełka. To, co możesz zrobić, stwórz swój własny Attribute, pochodzący z ValidationAttributetego, może być ozdobiony na poziomie klasy, a następnie utwórz niestandardowy walidator, który również pozwoli na działanie walidatorów na poziomie klasy.

Wiem, że Validation Application Block obsługuje samoocenę zaraz po wyjęciu z pudełka, ale VAB ma dość stromą krzywą uczenia się. Niemniej jednak, oto przykład użycia VAB:

[HasSelfValidation]
public class Person
{
    public string Name { get; set; }
    public bool IsSenior { get; set; }
    public Senior Senior { get; set; }

    [SelfValidation]
    public void ValidateRange(ValidationResults results)
    {
        if (this.IsSenior && this.Senior != null && 
            string.IsNullOrEmpty(this.Senior.Description))
        {
            results.AddResult(new ValidationResult(
                "A senior description is required", 
                this, "", "", null));
        }
    }
}
Steven
źródło
„Musisz walidować na poziomie osoby, a nie na poziomie Senior” Tak, jest to opcja, ale tracisz możliwość dodawania błędu do konkretnego pola, które jest wymagane w obiekcie Senior.
Peter Stegnar
3

Miałem ten sam problem, potrzebowałem modyfikacji atrybutu [Wymagane] - make pole wymagane w zależności od żądania http. Rozwiązanie było podobne do odpowiedzi Dana Hunexa, ale jego rozwiązanie nie działało poprawnie (patrz komentarze). Nie używam dyskretnej weryfikacji, po prostu MicrosoftMvcValidation.js po wyjęciu z pudełka. Tutaj jest. Zaimplementuj swój atrybut niestandardowy:

public class RequiredIfAttribute : RequiredAttribute
{

    public RequiredIfAttribute(/*You can put here pararmeters if You need, as seen in other answers of this topic*/)
    {

    }

    protected override ValidationResult IsValid(object value, ValidationContext context)
    {

    //You can put your logic here   

        return ValidationResult.Success;//I don't need its server-side so it always valid on server but you can do what you need
    }


}

Następnie musisz zaimplementować niestandardowego dostawcę, aby używać go jako adaptera w pliku global.asax

public class RequreIfValidator : DataAnnotationsModelValidator <RequiredIfAttribute>
{

    ControllerContext ccontext;
    public RequreIfValidator(ModelMetadata metadata, ControllerContext context, RequiredIfAttribute attribute)
       : base(metadata, context, attribute)
    {
        ccontext = context;// I need only http request
    }

//override it for custom client-side validation 
     public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
     {       
               //here you can customize it as you want
         ModelClientValidationRule rule = new ModelClientValidationRule()
         {
             ErrorMessage = ErrorMessage,
    //and here is what i need on client side - if you want to make field required on client side just make ValidationType "required"    
             ValidationType =(ccontext.HttpContext.Request["extOperation"] == "2") ? "required" : "none";
         };
         return new ModelClientValidationRule[] { rule };
      }
}

I zmodyfikuj swój global.asax linią

DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredIfAttribute), typeof(RequreIfValidator));

i oto jest

[RequiredIf]
public string NomenclatureId { get; set; }

Główną zaletą dla mnie jest to, że nie muszę kodować niestandardowego walidatora klienta, jak w przypadku dyskretnej weryfikacji. działa tak samo, jak [Wymagane], ale tylko w przypadkach, w których chcesz.

Legowisko
źródło
Ta część dotycząca rozszerzania DataAnnotationsModelValidatorbyła dokładnie tym, co chciałem zobaczyć. Dziękuję Ci.
twip
0

Typowe zastosowanie do warunkowego usuwania błędu ze stanu modelu:

  1. Utwórz warunkową pierwszą część akcji kontrolera
  2. Wykonaj logikę, aby usunąć błąd z ModelState
  3. Wykonaj resztę istniejącej logiki (zwykle walidacja stanu modelu, a następnie wszystko inne)

Przykład:

public ActionResult MyAction(MyViewModel vm)
{
    // perform conditional test
    // if true, then remove from ModelState (e.g. ModelState.Remove("MyKey")

    // Do typical model state validation, inside following if:
    //     if (!ModelState.IsValid)

    // Do rest of logic (e.g. fetching, saving

W twoim przykładzie zachowaj wszystko tak, jak jest i dodaj logikę sugerowaną do akcji kontrolera. Zakładam, że Twój ViewModel przekazany do akcji kontrolera ma obiekty Person i Senior Person z danymi wypełnionymi w nich z interfejsu użytkownika.

Jeremy Ray Brown
źródło
0

Używam MVC 5, ale możesz spróbować czegoś takiego:

public DateTime JobStart { get; set; }

[AssertThat("StartDate >= JobStart", ErrorMessage = "Time Manager may not begin before job start date")]
[DisplayName("Start Date")]
[Required]
public DateTime? StartDate { get; set; }

W twoim przypadku powiedziałbyś coś w rodzaju „IsSenior == true”. Następnie wystarczy sprawdzić walidację akcji wysyłania.


źródło