Lokalizacja Enum

Wprowadzenie

Typów wyliczeniowych używamy do określenia wartości z góry określonego zbioru. Dzięki nim ułatwiamy sobie tworzenie kodu poprzez nadanie nazwy dla wartości. Nazwa enuma musi spełniać rygory składni takich języków jak C#. Dlatego potrzebujemy czegoś, co wyświetli ładny napis w interfejsie użytkownika. W tym wpisie drogi czytelniku, pokaże Ci jak to zrobić małym nakładem pracy oraz przy okazji wspierać wiele języków. Zobacz jak lokalizować enumy w aplikacji .NET.

Status zamówienia

W przykładzie zajmiemy się zamówieniami oraz ich statusami. Prosta aplikacja ASP.NET MVC, którą przygotowałem wyświetli listę zamówień, gdzie jednym z pól będzie status zamówienia, który jest właśnie typem wyliczeniowym:

Zamówienie może być w jednym z trzech statusów. W aplikacji chcielibyśmy wyświetlać status zamówienia w języku polskim i najlepiej, aby nie trzeba było przy tym pisać za każdym razem dużej ilości kodu.

Użycie view modeli

W swoich aplikacjach wykorzystuje view modele to przekazywania danych do widoków (praktycznie w każdym typie aplikacji z tego korzystam). Zakładam, że widoki w aplikacji są w miarę proste i nie chce mieć w nim za dużo logiki szczególnie, że widoki ciężko jest przetestować. Dlatego view model przekazany do widoku zawiera już właściwy napis dla enumu. View model dla zamówienia wygląda tak:

Jak widać na listingu właściwość Status w view modelu jest typu string. Jest to o tyle istotne, że dzięki temu za chwilę automapper wykona automatyczną konwersję, która zamieni enuma na jego ładny napis.

Atrybut EnumDescription

Poszczególne napisy dla enumów przechowuje w plikach Resource. Dzięki czemu mogę tłumaczyć je i wpierać wiele języków w aplikacji. Dla każdego enum tworze dedykowany plik resource np. OrderStatusResources. Używam wartości enuma jako nazw elementów w resource:

OrderStatusResources

Następnie enum jest udekorowany moim własnym atrybutem (EnumDescription), w którym przekazywany jest plik resource, w którym są napisy:

Kiedyś zamiast własnego atrybutu korzystałem z atrybutu Display z data annotations. Niestety tamten atrybut nie może być podpięty pod typ i trzeba go definiować dla każdego elementu w typie. Takie podejście na dłuższą metę jest uciążliwe, dlatego teraz stosuje swój atrybut, który jest używany tylko raz dla typu.

ToDisplayString – extension method

Do wyciągania przetłumaczonego napisu wykorzystuje extension method (ToDisplayString):

Metoda sprawdza w pierwszej kolejności, czy enum jest udekorowany atrybutem EnumDescription i jeśli tak, to szuka w przekazanym pliku resourców wpisu o takie samej nazwie, jak wartość enuma. Zwraca go, gdy znajdzie. Gdy nie, zwraca nazwę enuma. Użycie tej metody jest później bardzo proste:

W tym miejscu dodaje również logowanie informacji (w przykładzie tego nie ma) o brakujących resourcach dla enuma. Aby przez przypadek nie zapomnieć ich dodać.

Taka implementacja ToDisplayString nie jest efektywna. Korzystanie z reflekcji przy każdym wywołaniu powoduje niepotrzebny narzut. Pod koniec miesiąca planuje dodać wpis, w którym wrócę do tej metody i zastanowimy się, co można zrobić lepiej.

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 20 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

Automapper oraz TypeConverter

Na początku artykułu obiecałem, że użycie tego mechanizmu będzie wiązało się z mały nakładem pracy. Posłużymy się do tego automapperem, w którym możemy zdefiniować konwerter dla typów, który automatycznie będzie zmieniał enuma na napis. Użyjemy do tego metody ToDisplayString. Plusem tego rozwiązania jest to, że nie będziemy musieli później pisać kodu, który będzie wywoływał metodę ToDisplayString. Wszystko zrobi za nas automapper. Jedynie będziemy musieli zapewnić, że view model używa typ string dla właściwości, aby automatyczna konwersją mogła mieć miejsce.

Tworzenie konwertera typów jest proste. Wystarczy utworzyć nową klasę oraz zaimplementować generyczny interfejs ITypeConverter. W naszym przypadku będzie to wywołanie metody ToDisplayString:

Mając już konwerter typów rejestrujemy go w profilu, aby był wykorzystywany do konwersji enuma na stringa:

I to już wszystko. Nie musimy już w innych mapowaniach (np. Order => OrderViewModel) dodawać informacji o sposobie zamiany enuma na string. Wszystko zrobi za nas automapper.

Poniżej rejestracja mapowania dla zamówienia. Jak widać nie określam w tym miejscu jak mapować status.

Przykład

Na githubie (https://github.com/danielplawgo/EnumLocalization) znajduje się przykład pokazujący w praktyce ten mechanizm. W przykładzie jest jedna testowa akcja (http://localhost:53393/orders), która zwraca listę zamówień, gdzie właśnie wykorzystałem opisywany mechanizm. Jak widać w samym kontrolerze nie ma logiki mapowania enuma na string, wszystko robi za nas automaper, a kod samej akcji jest bardzo prosty:

Podsumowanie

Wyświetlanie enumów w interfejsie użytkownika nie musi być skomplikowane i czasochłonne. Wystarczy wykorzystać automapera, aby zrobił to za nas. Wykorzystując pliki resourców otwieramy sobie możliwość tłumaczenie później aplikacji, co też jest istotne.

Jak podoba Ci się to rozwiązanie? Używasz czegoś podobne? A może lepszego? Daj znać w komentarzu.

5 thoughts on “Lokalizacja Enum

  • Pingback: dotnetomaniak.pl
  • Niestety Twoje pomysły to próba wymyślania koła od nowa.
    1.Rozbijanie plików resource tylko dla kilku wartości zabija to co w Resource-ach jest najistotniejsze, czyli wydajność i prostotę. Tutaj polecam zapoznać się z tym czym właściwie są resource i jak działają m.in. jak działa w nich cache.
    2.Atrybut na wartościach enum-a są po to, żeby można je niezależnie konfigurować, ponieważ „słownik tłumaczeń” jest w zasadzie „słownikiem” i w ten sposób mamy pewność w kodzie, że dany klucz się nie powtarza, albo, że używamy danego klucza wielokrotnie świadomie.
    2.ToDisplayString przez to, że korzysta z refleksji jest strasznie niewydajne i w przypadku błędów dostajemy błąd z klasy z której jej nie oczekujemy. Nie mówiąc już o tym, że są tutaj błędy np. Twój atrybut można nadać wielokrotnie, a i tak weźmiemy pierwszą wartość.

    • Hej, dzięki za komentarz!

      A co masz na myśli o wymyślaniu koła od nowa? Chodzi Ci, że jest już coś gotowego, co można użyć? Kilka lat temu, kiedy potrzebowałem stworzyć taki automatyczny mechanizm nic niestety gotowego nie było.

      Oczywiście można też to zorganizować trochę inaczej. Sam kiedyś w niektórych aplikacjach używałem podobnego mechanizmu tylko, że miałem jeden plik resourców dla wszystkich enumów, z którego metoda ToDispalyString wyciągała wartość już bez użycia refekcji. Ale niestety takie podejście na dłuższą metę było problematyczne, w szczególności, gdy aplikacja była z budowana z modułów, które były dynamiczne ładowane z wykorzystaniem MEFa, gdzie klient mógł dodawać własne rozszerzenia do aplikacji. Więc nie dało się korzystać z jednego pliku resources dla enumów.

      A mógłbyś rozwinąć kwestię z punkut 1? Przyznam się szczerze, że jak kiedyś interesowałem się tematem, to właśnie ludzie zalecali, aby korzystać z wielu plików resource (np. https://stackoverflow.com/questions/8792180/1-resource-file-vs-many lub https://stackoverflow.com/questions/19910926/multiple-resource-files-versus-single-resource-file). U nas w projektach duże pliki resourców średnio się sprawdzały. Masz rację, że jeden plik będzie wydajniejszy ponieważ tylko raz go się wczytuje z dysku. A nie jest tak, że mechanizm cache spowoduje, że kolejne odczytanie wartości będzie już bez tego narzutu? W wolnej chwili z ciekawości sprawdzę jaki faktycznie jest narzut.

      Co do punktu 2 to myślę, że to zależy jak organizujesz sobie jako całoś korzystaniem z resourców. Jak pisałem w wpisie, sam kiedyś korzystałem z atrybutu Display na każdej wartości w enum, ale takie rozwiązaniem powodowało, że w definicji enuma było sporo zduplikowanego kodu, który w przypadku, gdy mam jeden plik resources per enum był nie do końca potrzebny. np.:

      enum OrderStatus
      {
      [Display(ResourceType = typeof(OrderStatusResources), Name = „Created”)]
      Created,
      [Display(ResourceType = typeof(OrderStatusResources), Name = „Processed”)]
      Processed
      }

      Masz rację, atrybuty na poziomie wartości są bardziej elastyczne, gdy na przykład korzystasz z jednego pliku resources, gdzie wartości są współdzielone przez różne elementy aplikacji.

      Natomiast co do metody ToDisplayString to masz rację, że w tym wydaniu nie jest efektywna. Dzięki Twojemu komentarzowy zauważyłem, że podczas edycji wpisu uciekł mi jeden akapit związany właśnie z tą kwestią. Pod koniec miesiąca planuje dodać wpis na temat BenchmarkDotNet, gdzie właśnie chciałem użyć tej metody jako przykładu do analizy wydajności.

      Jeszcze raz wielkie dzięki zakomentarz!

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *