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:
- Korzysta z
App_GlobalResources
, co jest dość oldskulowe. - 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.