Wraz z nadejściem Visual Studio 2022 (o moich testach tego środowiska możesz przeczytać tutaj) przyszła nowa wersja C#. To już dziesiąta odsłona. Co tym razem przygotowali ludziska z Microsoftu?
Namespace na cały plik
W C# wszystko musi być w jakimś namespace (przestrzeni nazw):
namespace Test
{
class Class1
{
}
}
Teraz wprowadzono możliwość zdefiniowania namespace w obrębie całego pliku:
namespace Test;
class Class1
{
}
Wszystko, co znajdzie się w tym pliku, trafi do przestrzeni Test. Jednak w tak skonstruowanym pliku może być tylko jedna przestrzeń robocza. Jeśli chcesz mieć ich więcej, musisz to zrobić po staremu. Jakimś plusem tego jest mniej wcięć. Tzn. wcięcia klasy zaczynają się od początku (od pierwszego znaku). Jednak…
Moje wrażenia? Jak dla mnie – zupełnie zbędne, zwłaszcza że domyślny template tworzy pliki po staremu. Póki co nie skorzystałem z tego i wątpię, że skorzystam.
Globalne usingi
Brzmi jak mniej kodu w Twoich plikach cs. Co prawda, zawsze można było usingi ukryć w dyrektywie region, np:
#region "Usingi"
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
#endregion
jednak i tak w każdym pliku trzeba było je dodawać. Do tej pory działały tylko lokalnie – w zakresie pliku, w którym zostały „użyte”. Teraz możemy posłużyć się globalnym usingiem, np:
global using System.Linq;
Jeśli taką linijkę wpiszemy w JAKIMKOLWIEK pliku cs, będziemy mogli korzystać z LINQ we wszystkich plikach w tym projekcie.
Usingi statyczne
Ale to nie wszystko… Petarda nadchodzi powoli… Możemy korzystać ze wszystkich form using, jakie znaliśmy wcześniej, np:
global using static System.Console
Jeśli nie znałeś wcześniej znaczenia using static System.Console, to już tłumaczę. Żeby wypisać coś na konsoli, normalnie odwołujemy się do metody przez klasę:
using System;
public void Main()
{
Console.WriteLine("Siemka");
}
Dzięki statycznemu usingowi możemy pominąć klasę, do której należy STATYCZNA metoda i wywołać ją bezpośrednio, jak gdyby była standardowa:
using static System.Console;
public void Main()
{
WriteLine("Siemka");
}
Możemy to zrobić z dowolną statyczną publiczną metodą. Globalny statyczny using daje taką możliwość w obrębie całego projektu. Czyli raz wywołany:
global using static System.Console;
umożliwia nam operacje na konsoli w każdym pliku cs w danym projekcie bez bezpośredniego odwołania się do klasy Console.
Typedef w C#
global using zadziała też do aliasów, np:
global using Generics = System.Collections.Generic;
Ale co jest dla mnie petardą… możemy wreszcie zrobić coś na zasadzie typedef z C++. BAAAARDZO mi tego brakowało w C#:
global using IntList = System.Collections.Generic.List<int>;
i IntList będzie widoczny jako typ w całym projekcie! Niesamowite. Do tej pory jedyne, co mogliśmy zrobić, żeby „zasymulować” coś takiego to utworzenie nowej klasy dziedziczącej po List<int>, co nie było ani zbyt sensowne, ani miłe.
Dyrektywy global using możesz umieścić w dowolnym pliku cs w danym projekcie. Ale najbardziej radosnym miejscem do tego wydaje się plik Program.cs (App.cs), chociaż można też utworzyć zupełnie nowy plik o nazwie np. "usings.cs" i tam umieścić wszystkie globalne usingi.
Moje wrażenia? Pomocne, zwłaszcza jak masz duże grupy takich samych usingów w kilku plikach. Można stworzyć alias na konkretny typ (typedef), co jest niesamowite i bardzo mi tego brakowało.
Czasami jednak w głowie pojawia mi się pytanie: „Czy ten using może/powinien być globalny?” i to działa na minus, bo zaczynam się zastanawiać nad pierdołami. Ale myślę, że to kwestia bardziej zachłyśnięcia się tą opcją i po pewnym czasie to minie.
Stałe w interpolowanych stringach
Spójrz na taki kod:
[Obsolete("Zamiast metody Foo, użyj Bar")]
static void Foo()
{
}
Wcześniej nie można było użyć interpolowanych stringów w takich przypadkach. Teraz już można:
Mała zmiana, ale cieszy. Czasami tego brakowało. Oczywiście powyższy przykład jest mało użyteczny, ale łapiesz kontekst. Jest jeszcze więcej zmian w interpolowanych stringach, które po prostu zoptymalizowały ich działanie.
Wygodniejsze lambdy
Lambda jako var
Do tej pory, żeby zdefiniować lambdę, trzeba było posłużyć się wyrażeniem lub zadeklarować lambdę jako Action<> lub Func<>, np:
Func<string, int> l = (string s) => { return s.Length; };
int len = l("abc");
teraz można lambdę zadeklarować jako var:
var l = (string s) => { return s.Length; };
int len = l("abc");
Konkretny typ lambdy zostanie za nas „odgadnięty”. Dokładnie tak jakbyśmy się tego spodziewali przy słowie var.
Lambdy można też przypisać do typu object lub Delegate a także Expression:
ponieważ metoda Foo może przyjąć różne argumenty (jest przeciążona). W tym przypadku zmienna f nie będzie reprezentowała wszystkich przeciążeń. Po prostu kompilator nie jest w stanie wykminić, którą wersję chcemy użyć. W takim przypadku trzeba posłużyć się starym kodem:
Action<string> fs = Foo;
Action<int> fi = Foo;
Różne typy zwracane
Czasem jest tak, że nie wiadomo, co powinna zwrócić lambda. W takim wypadku zawsze stosowaliśmy typ object, jako typ zwracany, np:
Func<bool, object> f = (bool b) => b ? "string" : 0;
To był jedyny możliwy zapis. Niestety kompilator nie potrafi odgadnąć typu zwracanego z takiego kodu:
var f = (bool b) => b ? "string" : 0;
chociaż nie wiem dlaczego z automatu nie uznaje tego za object. Ale można mu to teraz podpowiedzieć:
var f = object (bool b) => b ? "string" : 0;
Bardzo mi to przypomina definiowanie wskaźnika na funkcję w C++
Atrybuty
W wersji 10 dołożono też możliwość oznaczania lambdy atrybutem! Np:
var l = [MyAttribute](bool b) => { return !b; };
Moje wrażenia. Super, że można definiować lambdy z użyciem var. To naprawdę przydatne. Jeśli chodzi o atrybuty przy lambdach… nigdy nie miałem potrzeby, żeby to stosować, więc nie potrafię znaleźć teraz użytecznego przykładu. A szukałem i kombinowałem. Największe nadzieje pokładałem w atrybucie Conditional, ale z jakiegoś powodu nie chce on działać z lambdami, chociaż powinien działać każdy atrybut, który targetuje w metody.
Struktury i rekordy
W nowej odsłonie C# znajdziemy też kilka drobnych zmian związanych mocniej lub słabiej ze strukturami. Wszystkie zdecydowanie na PLUS. Zacznijmy od:
Konstruktor bez parametrów!
Do tej pory każda struktura posiadała domyślny konstruktor bezparametrowy. Nie można było stworzyć takiego samemu. Taki kod po prostu się nie kompilował:
struct Point
{
public int X { get; init; }
public int Y { get; init; }
public Point()
{
X = 0;
Y = 0;
}
}
A domyślny niejawny konstruktor wszystkie pola inicjalizował domyślnymi wartościami. Czyli np. stringa nullem. Powiem szczerze, nie wiem czemu tak to działało. Na szczęście w wersji 10 można już utworzyć samemu konstruktor bezparametrowy tak jak powyżej.
Record Struct
W C#9 zawitały typy rekordowe. Jeśli nie wiesz, czym są… Rekordy to w skrócie coś łączące ze sobą struktury i klasy. Rekordy są traktowane jako typ referencyjny, natomiast porównywane są wartościowo. Mają wbudowane operatory porównań. Więcej do poczytania tutaj: https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/records
W C#10 doszły record struct. Moje testy tego tworu były dość krótkie, natomiast widzę, że mogą świetnie zająć miejsce tupli. Można je zdefiniować np. w taki sposób:
readonly record struct Point(int X, int Y);
Tak! Tylko jedna linijka! A co mamy? Mamy dwie właściwości – X i Y. Mamy możliwość porównywania takich rekordów (porównywane są WARTOŚCI poszczególnych składników, nawet jeśli mamy stringa – porównana będzie wartość). Słówko readonly oznacza, że taki rekord jest immutable – niezmienny.
Kopiowanie struktur i rekordów
Kod powie więcej niż 1024 słowa:
readonly record struct Point(int X, int Y);
...
void Foo()
{
Point pt = new(0, 0);
Point pt2 = new Point();
var pt3 = pt2 with { Y = 5 };
}
Spójrz, co się stało. Najpierw utworzyliśmy jeden rekord (5) – pt. Potem drugi – pt2 z wartościami (0, 0). Następnie utworzyliśmy trzeci – pt3. A właściwie słowo with spowodowało skopiowanie rekordu pt2 ze zmienioną jedną wartością.
Rozszerzony wzorzec właściwości
Czyli Extended property pattern. To takie nowe cudo, które może nieco skrócić kod. Ciężko mi w tym momencie określić, czy to dobre rozwiązanie, na pewno trzeba się do niego przyzwyczaić. Już tłumaczę o co chodzi. Załóżmy, że mamy takie struktury:
readonly record struct Point(int X, int Y);
public struct Address
{
public string City { get; init; } = "<unknown>";
}
public record struct Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
public Address Address { get; init; }
}
Teraz utwórzmy w jakiś sposób obiekt Person:
object obj = new Person
{
FirstName = "Adam",
LastName = "Jachocki",
Address = new Address { City = "Łódź" }
};
I sprawdźmy, czy taki obiekt jest osobą o imieniu Adam. Najpierw po staremu:
if (obj is Person && ((Person)obj).FirstName == "Adam")
Console.WriteLine("Tak, to Adam");
A po nowemu (używając extended property pattern) możemy zrobić to milej:
if (obj is Person { FirstName: "Adam"})
Console.WriteLine("Tak, to Adam");
Ale co ciekawe, możemy łączyć warunki. Łączenie to koniunkcja, czyli „and”:
if (obj is Person { FirstName: "Adam", Address: { City: "Łódź" }})
Console.WriteLine("Tak, to Adam");
To jest taki cukier składniowy. Z przyzwyczajenia nie miałem okazji go jeszcze użyć, ale często takie sprawdzenia robię w kodzie GUI i dla mnie wydaje się naprawdę fajną sprawą. Chociaż osobiście muszę się do tego trochę przekonać.
To tyle nowości jeśli chodzi o C#10. Moimi ulubieńcami są globalne usingi, możliwość definiowania lambdy jako var no i jednolinijkowe rekordy. A co Wam się najbardziej z tego podoba? Dajcie znać w komentarzu.
Zazwyczaj tego nie robię. Gdy wychodzi nowy Visual, zazwyczaj czekam około pół roku zanim go zainstaluję i zacznę używać. Po prostu chcę uniknąć głupich problemów „wieku dziecięcego”. Tym razem jednak było inaczej. Z jakiegoś powodu nie mogłem doczekać się Visual Studio 2022. Zainstalowałem sobie nawet wersję Preview, ale w sumie nie używałem jej. W końcu wyszło… Poczułem silną wewnętrzną potrzebę przetestowania tego ustrojstwa i jak najszybszego używania go… W tym artykule dzielę się swoimi pierwszymi spostrzeżeniami. Opisuję wersję 17.0.0 Community
Sprzęt
Być może sprzęt, na którym go testuje ma jakieś znaczenie (na pewno ma w pewnych sytuacjach), dlatego postanowiłem przedstawić Wam go
Testowałem też na innym kompie – 2 spore monitory (z czego jeden 4K) i 16 rdzeni (dokładnej konfiguracji nie pamiętam)
Instalacja
Tutaj nie ma nowości. Pobrany instalator musiał najpierw ściągnąć kilka rzeczy, co zajęło mu około 30 sekund. Następnie uruchomił się już znany Visual Studio Installer, w którym wybrałem takie składniki:
ASP.NET Core, ASP.NET
Node.js
Aplikacje mobilne (Xamarin)
Programowanie aplikacji klasycznych .NET
Magazynowanie i przetwarzanie danych
Programowanie rozszerzeń Visual Studio
Niektóre „podskładniki” jak np. środowisko uruchomieniowe .NET 5 miałem już zainstalowane, więc czas instalacji będzie nieco krótszy.
W każdym razie, wszystko zajęło 11,12 GB. A całkowity czas instalacji to około 11 minut. Całkiem nieźle, biorąc pod uwagę, że AKTUALIZACJA VS2019 z wersji 16.11.5 na 16.11.6 zajęła prawie 7 i pół minuty.
Otwieranie solucji
Widziałem jak MS chwalił się szybkością otwierania solucji z ponad 100 projektami. Na pokazie trwało to naprawdę szybko. Żeby niczego nie zaburzyć, postanowiłem pierwsze otwarcie przeprowadzić bez żadnego kodu. Podejrzewam, że podczas pierwszego uruchamiania VS robi więcej rzeczy niż zazwyczaj.
Otwarcie solucji z 36 projektami trwało 50 sekund. Przy czym otwarcie tej samej solucji na VS2019 trwało 45 sekund. ALE! Najwidoczniej VS2022 optymalizuje sobie w jakiś sposób pracę. Drugie otwarcie tej samej solucji w VS2022 (po restarcie środowiska) zajęło już tylko 30 sekund. Więc faktycznie coś w tym jest.
Wygląd
Wygląd różni się nieco od VS2019. Są inne ikonki, inne czcionki… Wszystko to ponoć ma sprawić przyjemniejszą i bardziej skupioną pracę. Myślę, że ocena tego jest dość indywidualna, a ja jeszcze nie spędziłem przy tym środowisku tyle czasu, żeby móc to ocenić.
Co się jednak rzuca w oczy na początek, to czcionka w edytorze kodu. I tu mam mieszane uczucia. Na dużym monitorze 4K wygląda to całkiem nieźle. Natomiast na moim laptopie z ekranem 17″… było mi jakby za ciasno. Dlatego zmieniłem na starą Consolas. Ale to też kwestia dość indywidualna.
Pierwszy problem…
Na jednym komputerze wszystko poszło podejrzanie sprawnie. Jednak na drugim nie mogłem uruchomić aplikacji .NetCore WPF w środowisku. Ciągle dostawałem błąd:
The target process exited without raising a CoreCLR started event.Ensure that the target process is configured to use .NET Core. This may be expected if the target process did not run on .NET Core
Przeszukałem Internety wzdłuż i wszerz i dupa. Restarty, reinstalacje .NetCore, odinstalowywanie starszych wersji… nic nie dawało rady. Wygląda na to, że podczas pierwszej instalacji czegoś nie dopatrzyłem, co było potrzebne dla mojej aplikacji. Gdy doinstalowałem kilka rzeczy później nadal nic nie działało. Musiałem ostatecznie wybrać opcję „Napraw” z VsInstallera. I wtedy wszystko zaczęło magicznie działać.
Z czego wynika ten problem? W pierwszej wersji VS2019 był to faktycznie jakiś problem ze środowiskiem. Tutaj ciężko mi powiedzieć. Być może środowisko nie ogarnęło czegoś w 100% podczas doinstalowywania modułów.
Hot Reload
Podczas prezentacji niesamowicie jarali się mechanizmem HotReload, który ponoć w tej wersji jest przełomowy. I faktycznie – pokazywali Hot Reload w projekcie w C++, co mnie zupełnie zbiło z tropu. Postanowiłem to sprawdzić na jakimś projekcie MFC…
C++ i MFC
No niestety. Wygląda na to, że MFC jest zbyt starą technologią, żeby wspierało HotReload. Niestety można się było tego spodziewać. Więcej nie testuję C++, chociaż na prezentacji faktycznie to działało (oczywiście nie pokazywali tego na MFC, tylko na jakiejś prostej gierce).
WPF
Na pierwszy rzut oka naprawdę bardzo fajnie to działa. Można robić zmiany zarówno w XAML jak i w kodzie C# (!) i HotReload naprawdę daje radę. Co więcej, nie trzeba nawet zapisywać tych zmian! Wszystko dzieje się na bieżąco. Można zmieniać nawet style, a także user controlsy, co kiedyś było upierdliwe i to działa… ale… no właśnie.
Podczas zmian w globalnych stylach w pliku App.xaml coś się pieprzy i zmiany nie są widoczne. Co więcej, cała aplikacja jakby znalazła się w jakimś dziwnym stanie zawieszenia. Chociaż MS sam mówił, że przy niektórych zmianach w kodzie trzeba jednak zrestartować aplikację. Może ma to jakiś związek.
Co więcej, świetne jest okienko Preview (Debug -> Windows -> XAML Live Preview). Pokazuje aktualne okno uruchomionej aplikacji. Świetnie się sprawdza w przypadku jednego monitora, bo nie trzeba przełączać się między Visualem i aplikacją. Okienko Preview jest po prostu zadokowane. Zmiany są widoczne zarówno w tym oknie, jak i w aplikacji. Okienko preview ma też kilka dodatkowych rzeczy w stylu linijek znanych z programów graficznych. Bardzo to może wspomóc ustawianie elementów.
Dodatkowo jest w stanie pokazać w jakim pliku znajduje się jak element:
Okienko XAML Live Preview wraz z fragmentem XAML
Ogólne wrażenie: PLUS. Gdyby nie ten problem z plikiem App.xaml, byłbym niesamowicie zachwycony. Ale nadal – czapki z głów. Naprawdę dobrze działający mechanizm.
Xamarin
HotReload w Xamarinie zazwyczaj działało mi „od święta”. Głównie jednak nie działało, co było bardzo frustrujące i zabierało mnóstwo czasu. Dlatego z wielkim polskim przekąsem na twarzy podszedłem do tego tematu…
No cóż… Dupy nie urywa. HotReload w Xamarinie „potrafi” działać w dwóch trybach (od pewnej wersji). Może odświeżać tylko to, co się zmieniło lub całą stronę.
Okienko Preview udało mi się pokazać tylko w trybie „odświeżania samych zmian”. Natomiast w trybie odświeżania całej strony w ogóle nie działało. Ale to nie jest najgorsze. Tryb odświeżania tylko zmian w ogóle nie chciał działać. Udało się za to uruchomić HotReload w trybie odświeżania całej strony. Nie działa to jednak tak super jak w WPF. Tutaj jest jeszcze trochę pracy do zrobienia.
Jednak, podczas moich testów, HotReload w trybie odświeżania całego ekranu działał cały czas.
Ogólne wrażenie: mieszane… bardziej negatywne. Coś tam działa. Ale kiedy przestanie?
ASP.NET MVC
Niestety tutaj nie działa. Ale MS pisze o tym na swoich stronach. Więc tutaj trzeba posługiwać się starymi metodami. Podobnie jak…
BLAZOR!
Technologia, w której ostatnio się podkochuję. Obiecali, k… obiecali! Obiecali, że będzie Hot Reload! No i jest… Podobno. Gdy używa się .NET6. Aplikacja w Blazor, którą gdzieś tam sobie robię na boku jest na .NET5 i ma dla mnie aktualnie dość niski priorytet. W końcu zmigruję ją do .NET6 i wtedy sprawdzę. Póki co… Smutek…
Designer
Tutaj właściwie napiszę tylko o WPF. W Xamarinie standardowo nie ma takiego pojęcia jak designer, w corowych aplikacjach webowych też nie. Designer w WinForms właściwie się nie zmienił, dlatego opisuję to dla WPF.
Designer w WPF zyskał kilka nowych i fajnych elementów. Przede wszystkim można zmienić niektóre właściwości kontrolki, zaznaczając ją, a następnie klikając żaróweczkę, która się pojawia:
Może i pierdoła, ale naprawdę ułatwia życie.
Ale największy życioułatwiacz dla mnie to element „d” w XAML. Na pewno nie raz miałeś taką sytuację, że musiałeś zaprojektować jakiś element znajdujący się na jakiejś liście. I co wtedy się robi? Trzeba stworzyć jakieś fake’owe dane, żeby elementy były widoczne na liście w design time i generalnie potem trzeba pamiętać, żeby powrócić do prawdziwych danych… Nie jest to zbyt eleganckie rozwiązanie.
Tutaj mamy coś takiego jak wersja w design time (stąd „d”). Kod powie więcej niż 1000 słów:
Jak widzisz mamy tutaj właściwości Text i d:Text. FontWeight i d:FontWeight. Te prawdziwe właściwości (Text i FontWeight) działają normalnie – w runtime, czyli gdy aplikacja jest uruchomiona. Jeśli jednak jesteś w design time (aplikacja jest projektowana), działają właściwości z elementem d:. Czyli wszystko, co ma d: zostanie użyte w czasie projektowania aplikacji. Dla mnie jest to niesamowita opcja.
Wyszukiwanie plików
Zawsze niesamowicie denerwowało mnie wyszukiwanie plików w solution explorerze. Działało to bardzo topornie, a czasami pokazywało strasznie długą listę, na której trzeba było znaleźć to, co się chce. Właściwie pozostawało używanie thirdparty pluginów…
W wersji 2022 wreszcie działa to lepiej. Szukanie jest zdecydowanie szybsze i pokazuje wyniki, które bardziej pasują do tego, co chcesz znaleźć.
Podpowiedzi w kodzie – sztuczna inteligencja
Nie mówię tutaj o Intellisense. W tej wersji wprowadzili sztuczną inteligencję, która podpowiada Ci, co powinieneś napisać. Niedługo dojdzie do tego, że będziemy programować wciskając tylko tabulator 😉
Działa to tak, że Microsoft przeanalizował mnóstwo kodów, przede wszystkim na GitHubie i na tej podstawie nauczył sieć neuronową… programowania. To oczywiście zbyt duże słowo. Ale faktycznie – sztuczna inteligencja w jakiś sposób rozpoznaje kontekst w jakim się znajduje i w jakiś szatański sposób potrafi przewidzieć, co chcesz napisać. Testowałem to na swoich kodach. W miejscach, gdzie klasy były raczej unikalne i konteksty raczej też. No nie były to miejsca „typowe”. I ku mojemu wielkiemu zdziwieniu – działa całkiem nieźle. I nawet nie przeszkadza.
To co widzisz na obrazku powyżej to oczywiście dość „typowe” miejsce, ale wierz mi, że mechanizm jest naprawdę ciekawy. Podczas moich testów kilka razy naprawdę mnie zaskoczył.
MAUI
A gdzie MAUI? Cytują klasyka: „W d… bede grał w gre”. Na obietnicach się skończyło. Chłopaki i dziewczyny nie wyrobili się z MAUI na tą wersję, chociaż można już testować PREVIEW. Jest szansa, że w wersji produkcyjnej pojawi się w przeciągu 6 miesięcy.
Super! Dostałeś nowy monitor 4k! Nic tylko szaleć. Odpalasz swój program napisany w WPF i… gówno widzisz. Wszystko jest za małe. Jak to się dzieje? Przecież obiecali, że WPF ogarnia DPI, czy coś tam i nie trzeba już nic robić…
No tak. Niby ogarnia, ale nie ogarnia zmiany DPI przy kilku monitorach. To automatycznie robią aplikacje pisane w UWP. Natomiast w WPF trzeba zrobić mały, prosty myk. Ale spokojnie, nie musisz przepisywać aplikacji, stosować jakiś ViewBoxów, czy skomplikowanych obliczeń. Wszystko zostanie załatwione w pliku manifestu.
Windows 10 od wersji 1703 używa czegoś takiego jak Per-Monitor v2 awarness mode. Po krótce chodzi o to, że potrafi rozpoznać, kiedy okno aplikacji jest przesuwane na monitor z inną rozdzielczością. Teraz musimy poinformować naszą aplikację WPF, że też to potrafi:
Rozwiązanie
1. Utwórz plik manifestu (jeśli używasz domyślnego) lub otwórz jeśli już go masz.
Aby utworzyć plik manifestu:
kliknij prawym klawiszem myszy na projekt
wybierz Add -> New Item
odnajdź plik manifestu (wpisz w okienko do szukania: manifest)
2. Jeśli utworzyłeś nowy plik manifestu, to prawdopodobnie już wszystko masz. Wystarczy znaleźć fragment <application xmlns="urn:schemas-microsoft-com:asm.v3"> i go odkomentować.
3. Powinieneś w pliku manifestu mieć taki fragment:
Jest to druga część większego artykułu, więc jeśli jesteś faktycznie zainteresowany pisaniem aplikacji wielojęzycznych, przeczytaj najpierw o globalizacji.
Wstęp
Zanim zabierzemy się za tłumaczenie naszej aplikacji, dobrze jest zrobić coś w rodzaju przeglądu lokalizacji. Coś jak code-review, tylko skupiamy się na elementach globalizacyjnych. To znaczy, że w pierwszym etapie trzeba sprawdzić, czy wszystkie wytyczne z artykułu o globalizacji są spełnione. Jeśli nie – poprawiamy. O tym jest ten artykuł.
Adresy, telefony, nazwiska…
Sprawdź też wszystkie adresy, telefony itd. Te elementy różnie wyglądają w różnych państwach. Niestety czasami wymusza to stosowanie różnych typów modeli lub jakiegoś super modelu. Elementy, na które musisz popatrzeć, to np:
adresy
numery telefonów
rozmiary papieru (!)
jednostki miar (!)
Z niewielką pomocą przychodzi tutaj klasa RegionInfo z namespace System.Globalization. Daje ona kilka przydatnych informacji. Spójrz na jej właściwości:
CurrencyEnglishName – angielska nazwa waluty (np. „polish zloty”)
CurrencyNativeName – lokalna nazwa waluty (np. „złoty polski”)
CurrencySymbol – no to chyba wiadomo 😉
ISOCurrencySymbol – określenie nazwy waluty. Np. CurrencySymbol będzie „€”, a ISO to „EUR”. Dla polski to będzie PLN.
IsMetric – zwraca true, jeśli dany region posługuje się jednostkami metrycznymi. Jeśli ma imperialne, wtedy jest false.
UWAGA! Wiele osób jest przekonanych, że Wielka Brytania posługuje się jednostkami imperialnymi. Nie jest to do końca prawda. Jakiś czas temu zmieniono to oficjalnie na jednostki metryczne, jednak duża część ludzi z przyzwyczajenia używa jeszcze jednostek imperialnych.
Klasa RegionInfo posiada jeszcze kilka innych właściwości, jednak nie uważam ich jako super przydatnych podczas lokalizacji. Można je wyciągnąć innymi metodami.
Aplikacje, które mocno polegają na jednostkach miar (np. AutoCAD) dają swoim użytkownikom możliwość wyboru, czy stosować imperialne jednostki, czy metryczne. I myślę, że to jest dobry pomysł. Pod tym względem traktowałbym klasę RegionInfo jako pomocniczą, a nie jak ostateczny wyznacznik (chociażby ze względu na Anglików).
Przetestuj aplikację
W tym momencie powinieneś zacząć testować aplikację na różnych komputerach, różnych wersjach językowych systemu operacyjnego, posługując się globalnymi danymi. Pomoże Ci to wychwycić pozostałe potencjalne problemy, jak np:
serializacja danych (w szczególności dat i liczb zmiennoprzecinkowych)
wyświetlanie danych zgodnie z regułami
problemy z sortowaniem i porównywaniem stringów
I teraz pewnie powiesz – „Gościu, to ile ja mam mieć komputerów, żeby to wszystko przetestować? Przecież nie będę swojego brudził”. Zgadza się. Jeśli chodzi o testowanie desktopa, wystarczy Ci jeden komputer i wirtualne maszyny na nim. Do tego możesz użyć HyperV lub Oracle Virtual Box. Wszystkie te rozwiązania są darmowe.
Jeśli chodzi o testowanie aplikacji internetowych, to jest prościej. Wystarczy zmienić język w przeglądarce.
Chociaż polecam Ci tak, czy inaczej testy na wirtualnej maszynie ze względu na to, że można pozmieniać ustawienia regionalne w samym systemie. Wtedy dokładnie zobaczysz, co się dzieje z Twoją aplikacją.
Jeśli wszystko jest już gotowe, zapraszam do ostatniego artykułu z serii – lokalizacja.
Jeśli trafiłeś tu tylko po to, żeby dowiedzieć się, jak walidować złożony obiekt, zobacz końcówkę artykułu.
Kiedyś napisałem co nieco o walidacji w .NetCore MVC. Możesz te artykuły przeczytać tu i tu. Powinieneś je przeczytać, ponieważ mówią również o podstawach walidacji jako takiej (w tym o data annotation validation, którym będziemy się tu posługiwać).
Wchodząc w świat Blazor, bardzo szybko będziesz chciał tworzyć formularze i je walidować. Na szczęście w Blazor jest to chyba jeszcze prostsze niż w Razor.
Pierwszy formularz
Blazor ma wbudowany komponent, który nazywa się EditForm. Jak można się domyślić, jest to po prostu formularz. Ma on kilka ciekawych możliwości, w tym artykule skupimy się jednak tylko na walidacji.
Na początek stwórzmy bardzo prosty model – Customer. Tę klasę utworzymy oczywiście w osobnym pliku Customer.cs.
public class Customer
{
[Required]
[StringLength(50, MinimumLength = 5)]
public string Name { get; set; }
}
Jest klasa z jednym polem i dodanymi adnotacjami:
Pole jest wymagane
Musi mieć długość minimum 5 znaków i maksimum 50.
Teraz utwórzmy w pliku .razor stronę z formularzem:
Spójrz, co mamy w sekcji code. Tworzymy nowy obiekt dla naszego modelu. To jest ważne, ponieważ ten obiekt powiążesz z formularzem.
Teraz spójrz wyżej na komponent EditForm. To jest właściwie najbardziej podstawowa konstrukcja formularza. Mamy właściwość Model, która określa OBIEKT powiązany (zbindowany) z formularzem. Pamiętaj, że to jest obiekt utworzony w sekcji code, a nie nazwa klasy.
Dalej mamy zdarzenie OnValidSubmit. Przypisujemy tutaj metodę, która wykona się po wysłaniu formularza, gdy będzie on prawidłowy. Z tego wynika, że ta metoda nie zostanie wykonana, jeśli formularz będzie zawierał błędy (są inne zdarzenia, które do tego służą).
Dalej w formularzu masz komponent InputText, który określa powiązanie z odpowiednim polem modelu – za pomocą @bind-value – czyli typowy binding z Blazor.
No i na koniec mamy guzik do wysłania formularza – pamiętaj, że w formularzu możesz mieć TYLKO JEDEN guzik typu submit.
Dodajemy walidację do strony
Przede wszystkim trzeba powiedzieć formularzowi, żeby walidował model za pomocą DataAnnotations (można to zrobić inaczej, ale w tym artykule skupiamy się na prostych podstawach). W tym celu trzeba mu dorzucić komponent DataAnnotationsValidator:
Właściwie to załatwia sprawę, formularz może być już walidowany. Sprawdź to. Jeśli spróbujesz wysłać pusty formularz, wymagane pole zaświeci się na czerwono.
Komunikaty błędów
Pewnie chciałbyś uzyskać jakiś komunikat błędu? Można to zrobić na kilka sposobów. Najprościej – dodaj komponent ValidationSummary – czyli podsumowanie walidacji.
Możesz go dodać w dowolnym miejscu formularza – tutaj pojawią się po prostu komunikaty o błędach. Uruchom teraz i zobacz, co się stanie, gdy podasz nieprawidłowe dane:
Widzimy opis błędu i podświetlone konkretne pole. Przy czym zwróć uwagę, że opis błędu wskazuje konkretnie na pole o nazwie „Name” – to zostało wzięte z modelu. Można to zmienić. Wszystkie rodzaje inputów (a jest ich kilka) w EditForm mają właściwość DisplayName. Dodaj ją:
<InputText id="name" @bind-Value="Customer.Name" DisplayName="imię i nazwisko"/>
To jest po prostu przyjazna nazwa pola dla walidacji. Niestety w mojej wersji Blazor (wrzesień 2021) ta właściwość nie chce działać. Ale nic to. I tak w prawdziwym świecie posłużymy się odpowiednią adnotacją w modelu:
public class Customer
{
[Required(ErrorMessage = "Musisz wypełnić imię i nazwisko")]
[StringLength(50, MinimumLength = 5, ErrorMessage = "Imię i nazwisko musi być dłuższe niż 5 znaków i krótsze niż 50")]
public string Name { get; set; }
}
W artykule o własnej walidacji w .NetCore pisałem też o tym, jak zrobić wymagany checkbox. Np do akceptacji regulaminu. Wtedy trzeba było się nieźle nakombinować. Dzisiaj jest dużo prościej i zdecydowanie bardziej przyjaźnie. Spójrz na model:
public class Customer
{
[Required(ErrorMessage = "Musisz wypełnić imię i nazwisko")]
[StringLength(50, MinimumLength = 5, ErrorMessage = "Imię i nazwisko musi być dłuższe niż 5 znaków i krótsze niż 50")]
public string Name { get; set; }
[Required]
[Range(typeof(bool), "true", "true", ErrorMessage = "Musisz zaakceptować plitykę prywatności")]
public bool PrivacyAgreed { get; set; }
}
Nie wiem jak Ty, ale ja nie lubię widzieć całej listy rzeczy, które zostały źle wprowadzone. Lubię, jak każdy błąd jest wyświetlony przy konkretnym polu. Można to zrobić bardzo prosto.
Błędy przy konkretnych polach
W tym momencie możesz pozbyć się komponentu ValidationSummary. To on właśnie pokazuje pełną listę błędów. Pamiętaj jednak, żeby zostawić DataAnnotasionsValidator, który odpowiada za walidację.
Teraz do każdego walidowanego pola możesz dodać komponent ValidationMessage:
Oczywiście te komponenty możesz umieścić gdziekolwiek w formularzu. Za pomocą właściwości For określasz, do którego pola się odnoszą. Zwróć uwagę, że przekazujesz tam lambdę, a nie nazwę pola
Wszystko fajnie, tylko że brzydko. A co, jeśli chcielibyśmy trochę popracować nad wyglądem komunikatów? Można to zrobić. W standardowym projekcie znajduje się plik site.css (w katalogu wwwroot/css). Jak sobie popatrzysz do środka, zobaczysz taki styl:
.validation-message {
color: red;
}
To właśnie odpowiada za wygląd komunikatów. Możemy to zmienić, np:
Co ma każdy klient? Każdy klient ma swój adres. Stwórzmy teraz bardzo prosty model adresu:
public class Address
{
[Required(ErrorMessage = "Musisz podać nazwę ulicy")]
public string StreetName { get; set; }
[Required(ErrorMessage = "Musisz podać numer ulicy")]
public string StreetNumber { get; set; }
[Required(ErrorMessage = "Musisz podać miasto")]
public string City { get; set; }
}
I dodajmy go do modelu klienta, a także do formularza:
public class Customer
{
[Required(ErrorMessage = "Musisz wypełnić imię i nazwisko")]
[StringLength(50, MinimumLength = 5, ErrorMessage = "Imię i nazwisko musi być dłuższe niż 5 znaków i krótsze niż 50")]
public string Name { get; set; }
[Required]
[Range(typeof(bool), "true", "true", ErrorMessage = "Musisz zaakceptować plitykę prywatności")]
public bool PrivacyAgreed { get; set; }
public Address Address { get; set; } = new Address();
}