Lokalizowanie komunikatów błedów w ASP.NET MVC

Gdy w 2008 czy tam 2009 roku Microsoft wynalazł wzorzec MVC 1, firma ta jako jedną z większych zalet nowego frameworka podawała odróżniającą go od WebFormsów elastyczność i możliwość łatwej konfiguracji. (Pojechanie po innym swoim produkcie to swoją drogą jest świetny chwyt marketingowy.) Zdawałoby się zatem, że lokalizowanie standardowych komunikatów błędów powinno być banalne. No i w sumie jest, gdy już się potrafi to zrobić.

Sposób pierwszy

Najczęściej proponowanym na StackOverflow rozwiązaniem tego problemu jest umieszczenie pliku zasobów z odpowiednimi kluczami w katalogu App_GlobalResources oraz ustawienie DefaultModelBinder.ResourceClassKey = "Messages";. To rozwiązanie ma dwie wady:

  1. Korzysta z App_GlobalResources, co jest dość oldskulowe.
  2. Nie działa.

Działało kiedyś, ostatnio chyba w wersji 2 lub 3 ASP.NET MVC, zeznania nie są spójne. W każdym razie odpada.

Sposób drugi

Kolejnym sposobem jest utworzenie własnego atrybutu dziedziczącego ze standardowego atrybutu i nadpisanie w nim komunikatu. Coś w tym stylu:

1 public class MyAwesomeRequiredAttribute: RequiredAttribute
2 {
3     public MyAwesomeRequiredAttribute()
4     {
5         this.ErrorMessage = "Podaj wartość albo idź na frytki";
6     }
7 }

To pewno działa - nie sprawdzałem, bo nie widzę za bardzo sensu w zmuszaniu użytkowników Pizzy do nauki nowego zestawu atrybutów. Do podstawowych zastosowań te z System.ComponentModel.DataAnnotations są całkiem niezłe. Jeśli kiedykolwiek stwierdzę, że nie wystarczają, to raczej przejdę na Fluent Validation niż napiszę coś swojego.

Sposób trzeci

Na szczęście w ASP.NET 4.0 wprowadzono nowy twór: RequiredAttributeAdapter.

Jego podstawowe i polecane użycie wygląda np. tak:

1 public class PizzaRequiredAttributeAdapter : RequiredAttributeAdapter
2 {
3   public PizzaRequiredAttributeAdapter(ModelMetadata metadata, ControllerContext context, RequiredAttribute attribute) 
4          : base(metadata, context, attribute)
5   {
6     attribute.ErrorMessageResourceType = typeof(Errors);
7     attribute.ErrorMessageResourceName = "ValueIsRequired";
8   }
9 }

Istnieje cały zestaw takich adapterów dla poszczególnych standardowych atrybutów walidacji:

  • MaxLengthAttributeAdapter
  • MinLengthAttributeAdapter
  • RangeAttributeAdapter
  • RegularExpressionAttributeAdapter
  • RequiredAttributeAdapter
  • StringLengthAttributeAdapter

Tak utworzony adapter trzeba oczywiście gdzieś zarejestrować, np. w Global.asax albo w metodzie wołanej przez WebActivatora po starcie aplikacji. Kod rejestrujący wygląda tak:

1 DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredAttribute), typeof(PizzaRequiredAttributeAdapter));

Proste prawda? Aż za bardzo, dlatego nie działa.

Sposób trzeci edycja druga

Problem powstanie, gdy spróbujemy nadpisać ten nasz domyślny, pobrany z zasobów komunikat błędu, podając wartość ErrorMessage w kodzie modelu:

1 [Display(Name = "Login"), Required(ErrorMessage = "Podaj login i nie cwaniakuj!")]
2 public string Login { get; set; }

Po uruchomieniu aplikacja rzuci pięknym wyjątkiem: >Either ErrorMessageString or ErrorMessageResourceName must be set, but not both.

Pochodzi on z klasy System.ComponentModel.DataAnnotations.ValidationAttribute. Musiałem aż zdebugować ten kod, aby dojść o co chodzi i czemu tak się dzieje. Tak wygląda ten fragment, warto zwrócić uwagę zwłaszcza na komentarze. Wynika z tego, że w dwóch zupełnie róznych sytuacjach zostanie rzucony ten sam wyjątek. Dobre rozwiązanie, w Microsofcie zaoszczędzlii w ten sposób jakieś 50 bajtów na zasobach tekstowych, ktoś pewno dostał solidną premię.

 1 string localErrorMessage = this.ErrorMessage;
 2 bool resourceNameSet = !string.IsNullOrEmpty(this._errorMessageResourceName);
 3 bool errorMessageSet = !string.IsNullOrEmpty(this._errorMessage);
 4 bool resourceTypeSet = this._errorMessageResourceType != null;
 5 bool defaultMessageSet = !string.IsNullOrEmpty(this._defaultErrorMessage);
 6 
 7 // The following combinations are illegal and throw InvalidOperationException:
 8 //   1) Both ErrorMessage and ErrorMessageResourceName are set, or
 9 //   2) None of ErrorMessage, ErrorMessageReourceName, and DefaultErrorMessage are set.
10 if ((resourceNameSet && errorMessageSet) || !(resourceNameSet || errorMessageSet || defaultMessageSet)) {
11     throw new InvalidOperationException(DataAnnotationsResources.ValidationAttribute_Cannot_Set_ErrorMessage_And_Resource);
12 }

Gdybym to ja projektował tę klasę, wyszedłbym z założenia, że ręcznie wpisany tekst ma nadpisać tekst domyślny, a nie rzucać wyjątkiem. No, ale rozumiem też, że rzucenie wyjątkiem w jakiś tam sposób zabezpiecza programistę przed pomyłką, więc niech już będzie. I tak nie mam na to wpływu, więc nie pozostaje nic innego jak poprawić własny kod. Oto finalna wersja:

 1 public class PizzaRequiredAttributeAdapter : RequiredAttributeAdapter
 2 {
 3     public PizzaRequiredAttributeAdapter(ModelMetadata metadata, ControllerContext context, RequiredAttribute attribute)
 4         : base(metadata, context, attribute)
 5     {
 6         if (string.IsNullOrEmpty(attribute.ErrorMessage) && string.IsNullOrEmpty(attribute.ErrorMessageResourceName))
 7         {
 8             attribute.ErrorMessage = Errors.ValueIsRequired;
 9         }
10     }
11 }

Dodatkowym plusem tego rozwiązania jest brak magic stringów.


1 Oczywiście to nie Microsoft wynalazł MVC, ale mało kto o tym wie, a poza tym ja chcę wygrać Xboxa.

Opublikowano: