Jak korzystać z dobrodziejstw .NetCore w aplikacjach natywnych (Wpf, Console, Xamarin, WinForms)

Jak korzystać z dobrodziejstw .NetCore w aplikacjach natywnych (Wpf, Console, Xamarin, WinForms)

Wstęp

Jak już raz wejdziesz w internetowy świat .NET i później przyjdzie ci coś zrobić na konsoli albo innej aplikacji natywnej (Wpf, Winforms, Xamarin), to nagle się okazuje, że brakuje rzeczy. Nie ma dependency injection (trzeba pobierać np. starego, dobrego Autofaca), konfiguracji z appsettings i wielu innych mechanizmów, do których nas internetowy .NetCore przyzwyczaił.

Ale to nie znaczy, że nie można ich tam w prosty sposób umieścić.

Większość przykładów zrobimy na konsoli, bo jest najprościej. Jednak na koniec pokażę też przykłady w innych typach aplikacji.

W artykule o wstrzykiwaniu zależności pokazałem jak dodać DI do aplikacji konsolowej. Dzisiaj zrobimy pełen pakiet.

Co to Host?

Na początku stwórz nową aplikację konsolową. Po staremu to klasa Program ze statyczną metodą Main. W .NET6 to po prostu jedna linijka w pliku Program.cs:

Console.WriteLine("Hello, World!");

I tutaj dzieje się wszystko. Możesz wypisywać komunikaty na konsoli, możesz pobierać dane od użytkownika, tworzyć obiekty, zwalniać je itd. Jednym zdaniem – to jest serce całej aplikacji (po staremu – metoda Main z klasy Program). Można powiedzieć, że metoda Main jest w pewnym sensie hostem dla Twojej aplikacji.

A gdyby teraz przenieść zarządzanie tym wszystkim do innej klasy? I tak powstała klasa Host, która implementuje interfejs IHost. Host zajmuje się całym cyklem życia aplikacji. Zajmuje się konfiguracją, wstrzykiwaniem zależności, zwalnianiem zasobów, tworzeniem ich itd.

Żeby jej użyć, przede wszystkim musisz pobrać pakiet z NuGet: Microsoft.Extensions.Hosting.

Jednak, żeby utworzyć hosta, musisz posłużyć się HostBuilderem

Co to HostBuilder?

To budowniczy (w sensie wzorca projektowego) dla klasy Host. Najprostszy sposób na utworzenie Hosta to:

using Microsoft.Extensions.Hosting;

var hostBuilder = new HostBuilder();
var host = hostBuilder.Build();
host.Start();

Mamy tutaj utworzenie hosta i uruchomienie aplikacji (Start).

Jednak czym by był budowniczy, gdyby budował tylko takiego prymitywnego hosta? Zwykłym pijakiem spod bramy…

Budowanie hosta

Możesz zbudować domyślnego hosta na skróty w taki sposób:

using Microsoft.Extensions.Hosting;

var hostBuilder = Host.CreateDefaultBuilder();
var host = hostBuilder.Build();
await host.Run();

Cała magia zadzieje się w metodzie CreateDefaultBuilder. Ta metoda utworzy i zwróci ci domyślnego buildera, który ma już oprogramowane czytanie konfiguracji z appsettings, zmiennych środowiskowych i linii poleceń, utworzenie dependency injection i rejestrację podstawowych klas. W tym – domyślnej klasy Host.

Można powiedzieć, że to jest cała podstawa tego, co byś chciał mieć. Nic więcej, nic mniej.

Teraz możesz zadać pytanie – co stanie się w momencie wywołania host.Run()?

Obiekt Host poszuka zarejestrowanych obiektów implementujących IHostedService. I po kolei na każdym z nich wywoła metodę StartAsync. W tym przypadku oczywiście nie znajdzie takich obiektów, ponieważ nie utworzyłeś ich. Zatem praca od razu się zakończy. Wniosek z tego taki, że musisz stworzyć przynajmniej jedną klasę implementującą IHostedService.

IHostedService

Od tej pory pomyśl o swojej aplikacji konsolowej jako o „hoście”. Twoja główna aplikacja (po staremu metoda Main) ma za zadanie utworzyć i uruchomić obiekty IHostedService. I to w tych obiektach będzie dział się cały „prawdziwy” program. Stwórzmy taką klasę:

internal class MainApplication : IHostedService
{
    public Task StartAsync(CancellationToken cancellationToken)
    {
        Console.Write("Jak masz na imię? ");
        string name = Console.ReadLine();

        Console.WriteLine($"Cześć {name}!");

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        //ta metoda zostanie wykonana na zakończenie Twojego programu
    }
}

Teraz musimy zarejestrować gdzieś, ten IHostedService:

var hostBuilder = Host.CreateDefaultBuilder()
    .ConfigureServices(services =>
    {
        services.AddSingleton<IHostedService, MainApplication>();
    });

var host = hostBuilder.Build();
host.Run();

Jeśli chciałbyś ten kod porównać do starej, dobrej konsoli, to wyglądałoby to mniej więcej tak:

public static void Main(string[] args)
{
    var app = new MainApplication();
    app.StartAsync(CancellationToken.None).Wait();

    app.StopAsync(CancellationToken.None).Wait();
}

Po takiej prostej konfiguracji masz już:

  • ustawienie ContentRootPath na katalog aplikacji
  • wczytanie konfiguracji hosta ze zmiennych środowiskowych z prefixem DOTNET_
  • wczytanie konfiguracji z linii poleceń
  • wczytanie konfiguracji z plików appsettings i sekretów
  • wczytanie konfiguracji ze zmiennych środowiskowych i linii poleceń
  • dodanie domyślnego mechanizmu logowania
  • no i rzecz jasna Dependency Injection (w tym z automatu zarejestrowane IHostEnvironment)

Run, RunAsync, Start, StartAsync? WTF?

Jak się zapewne zdążyłeś zorientować, Host (a właściwie interfejs IHost) zawiera 4 metody do startowania aplikacji: Run, RunAsync, Start, StartAsync. Nie rozróżniając na synchroniczne i asynchroniczne mamy dwie – Run i Start. Czym się różnią? Poniżej „aplikacja” zrozum jako „wszystkie zarejestrowane klasy IHostedServices„.

Run

To jest extension method. W kodzie frameworka wygląda ona tak:

host.RunAsync().GetAwaiter().GetResult();

Uruchamia aplikację i blokuje aktualny wątek do momentu zakończenia aplikacji (wszystkie IHostedServices się skończą).

RunAsync

To również jest extension method. Ona wywołuje StartAsync i czeka na zakończenie aplikacji:

await host.StartAsync(token).ConfigureAwait(false);
await host.WaitForShutdownAsync(token).ConfigureAwait(false);

Na koniec wywołuje Dispose (lub DisposeAsync) na rzecz hosta.

Start

Wywołuje StartAsync analogicznie jak Run wywołuje RunAsync:

host.StartAsync().GetAwaiter().GetResult();

StartAsync

Startuje hosta asynchronicznie. Działa w taki sposób:

  • loguje na konsoli start aplikacji
  • wywołuje metodę WaitForAsync na rzecz IHostLifetime
  • wywołuje asynchronicznie StartAsync na wszystkich zarejestrowanych obiektach IHostedService
  • powiadamia IHostLifetime, że aplikacja się rozpoczęła

Czyli widać z tego, że wszystko sprowadza się i tak do StartAsync. Metody Start i Run są tylko pomocnicze. RunAsync dodatkowo czeka na zakończenie aplikacji przez IHostLifetime. Dokładną różnicę między StartAsync, a RunAsync, zobaczysz po zakończeniu aplikacji konsolowej.

Jeśli użyjesz RunAsync, to domyślny IHostLifetime zakończy aplikację tylko przy zamknięciu okna konsoli lub wciśnięciu Ctrl+C.

Jeśli użyjesz StartAsync, aplikacja zostanie zakończona normalnie, ale w oknie konsoli będziesz mógł przeczytać dodatkowe informacje zanim je zamkniesz.

IHostLifetime jest domyślnie ustawiany w CreateDefaultBuilder. Domyślnie jest to ConsoleLifetime. Oczywiście nic nie stoi na przeszkodzie, żebyś stworzył własną implementację IHostLifetime i np. odłożył faktyczne uruchomienie programu do jakiegoś momentu. Jednak to nie jest o tym artykuł 🙂

Środowisko

Pamiętaj, że jeśli w taki sposób tworzysz hosta, środowiskiem będzie Production. .NET uzna to za środowisko produkcyjne, ponieważ nigdzie nie znalazł zmiennej ASPNETCORE_ENVIRONMENT. Oczywiście, jeśli taką zmienną środowiskową masz wpisaną na swojej maszynie lub przekażesz ją w parametrach, to zadziała i środowisko będzie takie jakie sobie wpiszesz. Możesz też na sztywno posłużyć się metodą UseEnvironment:

var hostBuilder = Host.CreateDefaultBuilder()
    .ConfigureServices(services =>
    {
        services.AddSingleton<IHostedService, MainApplication>();
    })
    .UseEnvironment("Development");

Możesz też zrobić sobie plik launchSettings.json – analogicznie jak w internetowej wersji. Np:

{
  "ENVIRONMENT": "Development"
}

Ten plik jednak nie jest automatycznie wczytywany (w przeciwieństwie do appsettings), więc musisz go dodać do konfiguracji ręcznie:

var hostBuilder = Host.CreateDefaultBuilder()
    .ConfigureServices(services =>
    {
        services.AddSingleton<IHostedService, MainApplication>();
    })
    .ConfigureHostConfiguration(config =>
    {
        config.AddJsonFile("launchSettings.json");
    });

Zwróć uwagę na dwie rzeczy:

  • w takim przypadku w launchSettings.json zmienne środowiskowe nie powinny być prefixowane. Dlatego jest ENVIRONMENT, zamiast ASPNETCORE_ENVIRONMENT
  • plik launchSettings.json powinien się znaleźć w katalogu wynikowym aplikacji. A więc upewnij się, że w jego właściwościach zaznaczysz opcję Copy to Output Directory na Copy always lub Copy if newer.

Teraz możesz sterować swoim środowiskiem z tego pliku. Możesz oczywiście wpisywać tam wszelkie zmienne środowiskowe, jakie Ci się zamarzą.

Konfiguracja

W powyższym kodzie cała konfiguracja aplikacji powinna odbyć się w ConfigureServices. Tak jak w przykładzie rejestruję MainApplication:

var hostBuilder = Host.CreateDefaultBuilder()
    .ConfigureServices(services =>
    {
        services.AddSingleton<IHostedService, MainApplication>();
    })
    .ConfigureHostConfiguration(config =>
    {
        config.AddJsonFile("launchSettings.json");
    });

Jeśli jednak tak jak ja – w kodzie musisz mieć porządek, przenieś to do innej klasy lub metody. Na początek stwórz prostą klasę Startup – powinieneś znać ją z internetowej wersji .NETCore. Różnica jest taka, że w tej klasie nie definiujemy middleware pipeline, bo go po prostu nie ma:

internal class Startup
{
    IConfiguration config;
    ILogger<Startup> logger;

    public Startup(IConfiguration config, ILogger<Startup> logger)
    {
        this.config = config;
        this.logger = logger;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IHostedService, MainApplication>();
    }
}

W konstruktorze wstrzykuję konfigurację i loggera. Zwróć uwagę, że te obiekty zostały zarejestrowane w pierwszym kroku budowania hosta – ConfigureHostConfiguration w metodzie CreateDefaultBuilder.

A teraz swojego hosta stwórz w taki sposób:

var hostBuilder = Host.CreateDefaultBuilder()
    .ConfigureServices(services =>
    {
        services.AddSingleton<Startup>();
        var provider = services.BuildServiceProvider();

        var startup = provider.GetRequiredService<Startup>();
        startup.ConfigureServices(services);
    })
    .ConfigureHostConfiguration(config =>
    {
        config.AddJsonFile("launchSettings.json");
    });

Spójrz, co się stało w ConfigureServices.

Najpierw zarejestrowałem naszą klasę Startup. Równie dobrze mógłbym ją utworzyć przez new, ale tak jest bardziej elegancko i możesz wstrzyknąć do konstruktora klasy Startup wszystko, co zostało zarejestrowane przed BuildServiceProvider (5 linijka)

Następnie w linijce 5 budujemy service providera. Powinieneś już wiedzieć (przynajmniej z tego artykułu), że to jest obiekt, który zwróci zarejestrowane wcześniej serwisy.

Teraz małe wyjaśnienie. ServiceProvider może być budowany kilka razy na podstawie jednego IServiceCollection. Dlatego w tym momencie otrzymamy dostęp do serwisów zarejestrowanych wcześniej i możemy pobrać obiekt klasy Startup. Co więcej, możemy dalej posługiwać się IServiceCollection i przekazać go jako parametr do metody ConfigureServices (linijka 8).

Na sam koniec ServiceProvider zostanie znowu utworzony (to się dzieje podczas tworzenia hosta) i wszystkie serwisy rejestrowanie w klasie Startup również będą dostępne.

Jak dodać appsettings?

Domyślna konfiguracja odczytuje pliki appsettings z katalogu aplikacji. Czyli teoretycznie niczego nie musisz robić. Zwłaszcza jeśli masz tylko jeden plik appsettings.json – do tego powinieneś dążyć na środowisku produkcyjnym. Upewnij się tylko, że plik znajduje się w katalogu wynikowym aplikacji (właściwość pliku Copy to Output Directory ustaw na Copy always lub Copy if newer).

Jest też druga możliwość. Dodanie appsettings do zasobów aplikacji i wczytywanie ich stamtąd. Ale to nie jest artykuł o tym, niedługo coś na ten temat skrobnę.

UWAGA!

Pamiętaj, że nie powinieneś w pliku appsettings przechowywać ŻADNYCH sekretów aplikacji. Wszelkie hasła, dostępy do kont i inne wrażliwe dane muszą być przechowywane w sposób bezpieczny. Pamiętaj, że nawet jeśli umieścisz plik appsettings.json w zasobach aplikacji, to nie ma problemu dla użytkownika, żeby sobie go wyekstrahować i przeczytać.

Przykład w WPF

Teraz, jak obiecałem na początku, pokażę Ci kilka przykładów innych niż aplikacja konsolowa. Na początek aplikacja WPF.

O ile w aplikacji konsolowej musiałeś stworzyć implementację IHostedService (bo gdzieś ten program musi być), to w WPF nie musisz już tego robić. Wystarczy, że odpalisz główne okno aplikacji.

Oczywiście przede wszystkim musisz zacząć od małej konfiguracji aplikacji – żeby nie uruchamiała okna głównego, tylko zdarzenie OnStartup w klasie Application. W tym celu w pliku App.xaml ustaw:

<Application x:Class="Wpf.App"
             Startup="Application_Startup"
             ShutdownMode="OnMainWindowClose">

Metoda Application_Startup zostanie uruchomiona podczas uruchamiania aplikacji. Jeśli tego nie zrobisz, automatycznie uruchomione zostanie okno główne.

Jeśli chodzi o ShutdownMode to gdy nie ustawisz tej wartości, aplikacja po zamknięciu okna głównego nigdy się nie zakończy. Dlatego też ustaw to. Następnie skonfiguruj .NET.

W konstruktorze klasy App utwórz hosta:

IHost host;

public App()
{
	host = Host.CreateDefaultBuilder(Environment.GetCommandLineArgs()) //przekaż linię poleceń
		.ConfigureServices((ctx, services) =>
		{
			s.AddSingleton<Startup>();
            var tempServices = services.BuildServiceProvider();

            var startup = tempServices.GetRequiredService<Startup>();
           startup.ConfigureServices(services);
		}).Build();
}

W następnym kroku możesz uruchomić hosta w Application_Startup:

private void Application_Startup(object sender, StartupEventArgs e)
{
	host.Start();
    MainWindow = host.Services.GetRequiredService<MainWindow>();
    MainWindow.Show();
}

Najpierw uruchamiamy hosta. Potem pobieramy zarejestrowany serwis MainWindow -> w taki sposób, mając hosta, możesz pobrać każdy zarejestrowany serwis (łącznie z konfiguracją).

Gdy masz już MainWindow – po prostu pokazujesz je.

A jak rejestrujesz MainWindow? W tym przykładzie użyłem analogicznej klasy Startup jak w sekcji wyżej, po prostu:

internal class Startup
{
    IConfiguration config;
    ILogger<Startup> logger;

    public Startup(IConfiguration config, ILogger<Startup> logger)
    {
        this.config = config;
        this.logger = logger;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<MainWindow>();
    }
}

W aplikacji WinForms zrobisz to analogicznie.

Przykład w Xamarin

Oczywiście z Xamarin jest trochę więcej roboty. Chociaż w MAUI ma to już być w standardzie. Yeah!

Ale póki co, spójrzmy na Xamarin.

W związku z tym, że Xamarin może mieć tak naprawdę kilka aplikacji (np. Android i iPhone) jest trochę więcej kombinacji.

Na początek spójrz do projektu Xamarin (tego współdzielonego) do pliku App.xaml.cs. To jest klasa aplikacji. Dodaj tam konstruktor:

public readonly IHost host;
public App(Action<HostBuilderContext, IServiceCollection> configureNativeServicesAction)
{
	host = Host.CreateDefaultBuilder(Environment.GetCommandLineArgs())
		.ConfigureServices((ctx, b) =>
        {
			b.AddSingleton<Startup>();
			b.AddSingleton<App>(this);

			configureNativeServicesAction?.Invoke(ctx, b);
        })
		.ConfigureServices((ctx, b) =>
        {
			var tempServices = b.BuildServiceProvider();
			var startup = tempServices.GetRequiredService<Startup>();
			startup.ConfigureServices(this, b);
        })
		.Build();

	InitializeComponent();

	MainPage = new NavigationPage(new MainPage());
}

W konstruktorze przyjmujesz akcję do rejestracji pewnych natywnych serwisów, których być może używasz. Pisząc „natywne” mam na myśli takie, że na aplikacji Androidowej i na aplikacji iPhone się różnią. Ich implementacje są związane z urządzeniem.

W linii 10 wywołujesz tę akcję. Następnie wszystko jest analogiczne jak przy WPF z wyjątkiem tworzenia głównego okna.

Następnie w tym samym projekcie (Xamarin) stwórz sobie klasę do inicjalizacji aplikacji:

public static class AppInitializer
{
    static App theApp;
    static bool isInitialized = false;
    public static void Init(Action<HostBuilderContext, IServiceCollection> configureNativeServicesAction)
    {
        if (isInitialized)
            return;

        theApp = new App(startupAssembly, configureNativeServicesAction);         
    }
    
    public static App GetApplication()
    {
        return theApp;
    }
}

Klasa App to oczywiście klasa z pliku App.xaml.cs z projektu Xamarina z odpowiednim konstruktorem.

Na koniec użyj tego w aplikacji natywnej, np. Android. W metodzie OnCreate klasy MainActivity uruchom to wszystko:

protected override void OnCreate(Bundle savedInstanceState)
{
    AppInitializer.Init(ConfigureNativeServices);
    LoadApplication(AppInitializer.GetApplication());
}

void ConfigureNativeServices(HostBuilderContext ctx, IServiceCollection services)
{
  //tutaj zarejestruj serwisy natywne
}

No i to wszystko.

Zakończenie

Zachęcam Cię do poeksperymentowania z HostBuilderem jak i samym Hostem. Możesz naprawdę sporo tym wyciągnąć. I wcale nie musisz posługiwać się metodą CreateDefaultBuilder. Możesz skonfigurować wszystko sam krok po kroku, używając dostępnych metod.

Dziękuję Ci za przeczytanie tego tekstu. Jeśli czegoś nie rozumiesz lub znalazłeś błąd w artykule, koniecznie daj znać w komentarzu. No i jak zwykle zachęcam do zostawienia swojego adresu poniżej – dzięki temu nie ominie Cię żadna dawka wiedzy 🙂

Podziel się artykułem na:
Tłumaczenie aplikacji desktopowych (WinForms i WPF)

Tłumaczenie aplikacji desktopowych (WinForms i WPF)

Wstęp

To jest kolejny artykuł z serii o globalizacji i lokalizacji. Jeśli nie czytałeś poprzednich, koniecznie to nadrób. W tym artykule opisuję TYLKO jak ładować tłumaczenia w aplikacjach WinForms i WPF. Poprzedni artykuł – Tłumaczenie aplikacji cz. 3 – jak to ogarnąć? – daje całą podstawę.

Tłumaczenia w WinForms

W WinForms nie ma żadnego zmyślnego sposobu na ładowanie tłumaczeń. Po prostu każdemu przyciskowi, labelowi itd musisz zmienić TEXT w runtime. Chyba, że wymyślisz swój własny sposób, który zadziała automatycznie. Ale chyba więcej z tym nerwów niż pożytku.

Generalnie robisz to dokładnie tak samo, jak robiłbyś to w konsoli – dokładnie tak jak opisane w artykule podstawowym.

Pamiętaj, żeby domyślnie nadawać teksty w języku angielskim – wtedy jeśli czegoś nie przetłumaczysz, użytkownicy zobaczą teksty w tym właśnie języku.

Tłumaczenia w WPF

W WPF skorzystamy z ustrojstwa, co się zowie MarkupExtension. Jeśli nie wiesz co to, to odsyłam do netu: „WPF markup extension”, być może kiedyś opiszę ten mechanizm.

W skrócie – to coś, dzięki czemu możesz tworzyć własne tagi XAML. Coś jak {Binding...}

Teraz popatrz na ten fragment kodu:

<GroupBox Header="{app:Localize Receipt}" />

Tutaj widzisz mój markup extension – Localize. Efektem tego kodu będzie pobranie zasobu o kluczu Receipt i wartość tego zasobu będzie widoczna w nagłówku GroupBoxa – w Polsce: „Paragon”, wszędzie indziej: „Receipt”. Super? Ja się jaram 🙂

A teraz zobaczmy, jak coś takiego osiągnąć. Generalnie łatwo, prosto i przyjemnie…

MarkupExtension do tłumaczeń

Stwórz taką klasę najlepiej gdzieś w projekcie WPF:

[ContentProperty("ResId")]
class LocalizeExtension : MarkupExtension
{
    public string ResId { get; set; }
    public LocalizeExtension()
    {

    }

    public LocalizeExtension(string ResId)
    {
        this.ResId = ResId;
    }


    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (string.IsNullOrWhiteSpace(ResId))
            return "<???>";

        string result = LangRes.ResourceManager.GetString(ResId);
        if (string.IsNullOrEmpty(result))
            return $"<? {ResId} ?>";
        else
            return result;
    }
}

Wystarczy napisać klasę, która dziedziczy po MarkupExtension.

W linijce 1 podajesz domyślną właściwość… Tzn. gdybym tego nie zrobił, musiałbym kod w XAML napisać tak:

<GroupBox Header="{app:Localize ResId=Receipt}"/> //zwróć uwagę na obecność ResId

Klasa ma jedną metodę – ProvideValue i to w niej dzieje się cała magia. Po prostu pobieram stringa z zasobów na podstawie przekazanego klucza.

Ja tu sobie zrobiłem taki myk, że w razie jakbym nie dodał jakiegoś tłumaczenia, wtedy zamiast konkretnego stringa (którego nie ma w zasobach) zobaczę nazwę tego klucza w nawiasach ostrych. Dzięki temu wiem, że danego tłumaczenia nie ma w zasobach. Pozwalam sobie na taką nonszalancję, bo sprawdzam każde okienko, które robię.

I to właściwie tyle. Po utworzeniu takiej klasy, możesz skompilować projekt i używać swojego markup extension.

Być może nie wiesz skąd się bierze to app w kodzie:

<GroupBox Header="{app:Localize Receipt}"/>

To po prostu alias na namespace, w którym masz swoją klasę LocalizeExtension. Musisz go oczywiście zadeklarować na początku pliku XAML analogicznie do innych, np:

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:app="clr-namespace:MojaAplikacja">

gdzie MojaAplikacja to namespace, w którym znajduje się Twoja klasa. Oczywiście to nie musi być app. To może być cokolwiek, ale mam nadzieję, że to wiesz.


To wszystko jeśli chodzi o tłumaczenie aplikacji desktopowych. Jeśli masz jakieś inne pomysły, podziel się w komentarzu.

Obrazek komputera w logo tego artykułu dzięki upklyak / Freepik

Podziel się artykułem na:
Gdy okienko jest za małe, czyli WPF i problem z monitorem 4K

Gdy okienko jest za małe, czyli WPF i problem z monitorem 4K

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:

<application xmlns="urn:schemas-microsoft-com:asm.v3">
  <windowsSettings>
    <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
    <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
  </windowsSettings>
</application>

I tyle. Upewnij się, że Twoja aplikacja używa tego pliku. Aby to zrobić:

  • kliknij prawym klawiszem myszy na projekt
  • wejdź w ustawienia (Properties)
  • na karcie Application upewnij sie, że w ComboBox Manifest masz podany swój plik manifestu.

Uruchom apkę… I to tyle.

Podziel się artykułem na:
Różne typy projektów w VisualStudio 2019

Różne typy projektów w VisualStudio 2019

Po pewnych zmianach w Visual Studio 2019, tworzenie projektów może być nieco niezrozumiałe zwłaszcza dla młodszych programistów. Ten artykuł wyjaśnia po kolei typy projektów w VisualStudio i jest kierowany głównie do początkujących.

Jaki utworzyć projekt

Stworzyłem specjalnie na potrzeby tego artykułu prosty i nieidealny algorytm do wyboru typu projektu. Możesz go pobrać stąd

Algorytm wyboru odpowiedniego typu projektu

Co możemy utworzyć

Visual Studio daje nam właściwie nieograniczone możliwości. Pomaga utworzyć projekt na dowolną platformę w konkretnym języku i w wybranej technologii (gdzie można). Tak wygląda okienko tworzenia nowego projektu:

Okienko tworzenia nowego projektu w Visual Studio
Tworzenie nowego projektu

Okienko jest podzielone na dwie główne grupy. Z lewej strony znajduje się lista ostatnich używanych typów projektów. Po prostu możesz kliknąć na to, co już ostatnio tworzyłeś i nie musisz szukać w liście po prawej stronie.

Z prawej strony masz z kolei grupę podzieloną na dwa elementy. Na górze filtr, na dole listę dostępnych typów projektów. One mogą być różne w zależności od tego, jak zainstalowałeś Visual Studio (typy też można dodawać i usuwać z poziomu instalatora: Visual Studio Installer, który znajduje się na Twoim komputerze)

Jeśli teraz zaczniesz wpisywać w filtrze na górze nazwę typu projektu, który chcesz utworzyć, lista automatycznie zacznie się zmieniać. Ja przykładowo wpisałem Console i zobacz, jak wygląda moja lista na obrazku wyżej. Tych projektów jest dużo więcej. Teraz postaram się część z nich omówić:

Add-In

To jest po prostu rozszerzenie (plugin) do Visual Studio. Tak, można pisać własne pluginy. Będę o tym pisał w niedalekiej przyszłości. Tak naprawdę całe Visual Studio składa się z pluginów. Jak widzisz są to projekty VSIX.

VSIX Project

To projekt rozszerzenia VisualStudio z dodanymi już pewnymi plikami i kodami. Takich projektów jest kilka w zależności od języka, w którym piszesz (C#, VisualBasic)

WIdok projektu Add-In

Empty VSIX Project

jest to zupełnie pusty projekt rozszerzenia VisualStudio. Od tego powyżej różni się brakiem kodów i domyślnych plików. Wszystko piszesz od zera. Ma to swoje plusy i minusy.

Widok pustego projektu Add-In

Empty VSIX Project (Community)

to samo, co wyżej z tą różnicą, że zawiera referencje do biblioteki Community.VisualStudio.Toolkit, co ułatwia pisanie rozszerzeń

VSIX Project w/Command (Community)

ten template zawiera to co VSIXProject, dodatkowo posiada referencje do Community.VisualStudio.Toolkit i dodatkowo posiada napisaną już komendę. Ten suffiks w/Command czytaj jako „with command”

Widok projektu Add-In Community

VSIX Project w/Tool Window (Community)

Uuuu, to jest dopiero mocarz. Zawiera wszystko to, co VSIX Project w/Command (Community), ale dodatkowo własne okno narzędziowe (Tool window)

Widok projektu Add-In z okienkiem

Który projekt wybrać do tworzenia rozszerzenia? To oczywiście zależy. Najbardziej obszernym zdecydowanie jest VSIX Project w/Tool Window (Community) i może się wydawać najlepszym. Tam, gdzie używasz okien i komend może to być szybki start. Oczywiście jeśli wcześniej nie tworzyłeś żadnych rozszerzeń, polecam zaczynać od najmniejszego projektu Empty VSIX Project. Nie przytłoczy Cię duża ilość plików na początek i będziesz mógł rozwijać rozszerzenie w swoim tempie, ucząc się powoli nowych rzeczy.

Console

Czym są aplikacje konsolowe?

Aplikacje konsolowe tworzone są na komputery. Często są to jakieś narzędzia dostępne z wiersza poleceń lub proste programy testowe, gdzie szybko chcemy sprawdzić jakieś funkcjonalności.

Konsola

Ale aplikacje konsolowe potrafią być często naprawdę dużymi, poważnymi narzędziami. Czasami posiadają nakładki graficzne, dzięki którym łatwiej je obsługiwać. Zdarzają się też proste gry tekstowe lub z grafiką w postaci ASCII Art.

Typy aplikacji konsolowych

  • Console Application (dostępna w różnych językach: C#, F#, VisualBasic…) – podstawowa aplikacja konsolowa na komputery. Podczas jej tworzenia zostaniesz zapytany o wersję .NetCore, której używać
Wybór framework

Domyślnie w tej wersji Visual Studio aplikacje konsolowe są tworzone w .NetCore. W tym miejscu możesz wybrać wersję, której chcesz używać.

Jest to właściwie najmniejszy template. Domyślnie posiada tylko jeden plik Program.cs z małym fragmentem kodu.

Chociaż długo sam nie potrafiłem tego pojąć, to jednak aplikacje konsolowe są tym typem, od którego powinno zaczynać się naukę programowania. Po prostu jest wtedy dużo prościej pojąć pewne rzeczy.

Blank Node.js console application

są dwie wersje zależne od języka. JavaScript lub TypeScript. Jeśli nie wiesz, czym jest TypeScript, to po prostu JavaScript z typami. Sam TypeScript jest kompilowany później do JavaScript. Generalnie to jest najmniejszy template do tworzenia aplikacji w Node.js

Console App (.Net Framework)

to jest aplikacja konsolowa, zupełnie taka jak domyślna Console Application. Też dostępna w kilku językach. Różnica jest taka, że używając tego templatu, będziesz miał do dyspozycji cały .Net Framework. Ale uwaga, ten typ projektu tworzy aplikacje jedynie dla Windowsa.

Widok projektu konsolowego

Standalone code analysis tool

to służy do tworzenia analizatorów kodu. Jakiś czas temu w VisualStudio pojawiły się takie analizatory kodu, które za pomocą Roslyn potrafią podpowiedzieć Ci kilka rzeczy. Np., że powinieneś dany fragment kodu wywołać w wątku głównym. Albo, że możesz jakiś kod napisać czyściej. Dzięki temu VisualStudio potrafi też automatycznie dodać odpowiednie namespacey do pliku cs. Oczywiście nic nie stoi na przeszkodzie, żeby napisać własne analizatory. Do tego służy ten template.

Jak widzisz mamy tutaj właściwie 4 różne typy aplikacji do wyboru – Kosnola dla Windows, konsola niezależna od systemu (.NetCore)*, konsola w Node.js no i analizator kodu.

Pisząc „konsola niezależna od systemu” mam na myśli, że kiedyś tak może być. Na dzień dzisiejszy aplikacje konsolowe i okienkowe są tworzone tylko pod Windows. Ale w przyszłości może się to zmienić. Wtedy dużo prościej będzie przejść z aplikacji pisanej w .NetCore (lub będzie to w standardzie)

Desktop

To jest to, co lubię najbardziej. Stare, dobre aplikacje na komputery :). Tutaj tworzymy głównie aplikacje okienkowe i wszystko co z nimi powiązane (biblioteki, kontrolki, serwisy), ale możemy też znaleźć templaty do testów jednostkowych.

Opiszę pokrótce niektóre typy:

Windows Forms

  • Windows Forms App – tworzymy aplikację okienkową w technologii WindowsForms. Jedynie dla Windows. Używamy wybranej wersji .NetCore. Dostępne dla C# i VisualBasic
  • Windows Forms Class Library – biblioteka dll, której możemy używać w aplikacjach Windows Forms App. Jedynie dla Windows.
  • Windows Forms Control Library – biblioteka, w której możemy tworzyć dodatkowe kontrolki i używać ich w Windows Forms App. Tutaj też piszemy tylko dla Windows i w .NetCore.
  • Windows Forms * (.NET Framework) – te kilka pozycji jest analogiczne do tych powyżej. Z tą jedynie różnicą, że mamy do dyspozycji pełny .NET Framework

UWAGA! Pamiętaj o tym, że nie możesz mieszać ze sobą technologii .NetCore i .NET Framerowk. To znaczy, że jeśli tworzysz aplikację w .NetCore, to nie będziesz mógł użyć w niej .NET Framework.

WPF (Windows Presentation Foundation)

  • WPF Application – nowsza technologia niż Windows Forms. Używamy kodu XAML do projektowania okienek. Używamy wybranej wersji .NetCore. Dostępne jedynie dla Windows.
  • WPF Class Library – analogicznie jak w Windows Forms. Po prostu dllka, której możemy używać w WPF Application. Używamy wybranej wersji .NetCore i piszemy tylko dla Windows
  • WPF Custom Control Library – analogicznie jak w Windows Forms Control Library. Tworzymy kontrolki dla aplikacji WPF. Używamy wybranej wersji .NetCore i piszemy tylko dla Windows
  • WPF UserControl Library – tutaj mały haczyk. Możemy tworzyć „kontrolki” dziedziczące po UserControl i używać ich w aplikacji WPF. Nie ma to odpowiednika w WindowsForms, ponieważ WPF wymaga nieco innego podejścia. Używamy wybranej wersji .NetCore i piszemy w Windows.
  • WPF Application (.NET Framework) – to samo, co wyżej z tą różnicą, że używasz pełnej wersji .NET Framerowk

UWAGA! Pamiętaj o tym, że nie możesz mieszać ze sobą technologii .NetCore i .NET Framerowk. To znaczy, że jeśli tworzysz aplikację w .NetCore, to nie będziesz mógł użyć w niej .NET Framework.

Universal Windows

To są aplikacje, które mogą działać na wszystkich platformach z Windowsem. Na komputerach, telefonach (z Windowsem), XBox… Generalnie wszędzie tam, gdzie jest zainstalowany Windows. Co ciekawe, sam możesz tworzyć swoje urządzenia z odpowiednim chipem i tworzyć aplikacje, które będą na nim pracowały.

Te aplikacje też wykorzystują XAML do tworzenia okienek, ale robi się je zupełnie inaczej niż aplikacje w WPF. Mają zupełnie inny cykl życia i inne kontrolki.

Windows Service

To są usługi, które pracują w systemie. Po prostu takie aplikacje bez okienek, które pracują cały czas w tle.

Mobile

W tej kategorii znajdziemy templaty do tworzenia aplikacji mobilnych. Głównie w technologii Xamarin. Nie będę się tu za bardzo wysilał, bo też nie ma nad czym. Opiszę tylko w kilku słowach, czym jest Xamarin.

Xamarin

Wszystkie aplikacje oznaczone jako Xamarin, są pisane w technologii Xamarin. No tak, a masło jest z masła. Generalnie Xamarin to taka technologia, która próbuje połączyć ze sobą różne platformy. Android, iOS, Universal Windows i inne. Celem, jaki temu przyświeca jest to, żeby jak największa część kodu była współdzielona pomiędzy te wszystkie aplikacje. Zarówno część wizualna (Xamarin.Forms) jak i cała logika.

Mimo, że technologia ma już kilka lat, to jednak cały czas (maj 2021) są z nią jakieś problemy i czasami pewne rzeczy trzeba napisać inaczej niż by się chciało. Ale cały czas jest rozwijana i ulepszana. I faktycznie z roku na rok wygląda to coraz lepiej.

Co do reszty templatów (np. Photo Editing Extension) nie będę się rozpisywał, bo nie miałem z nimi do czynienia. Można się domyślić, że jeśli chcesz napisać aplikację na telewizor, użyjesz odpowiedniego templatu z oznaczeniem tvOS (chyba, że piszesz na Androida). Jeśli interesuje Cię coś konkretnego z tej listy poza Xamarin, odsyłam do google 🙂

Other

U siebie mam tylko jeden typ – Blank Solution. I mogłoby paść pytanie – po co komu pusta solucja z samym tylko plikiem sln? Trochę jest to przeszłość, a czasami tak po prostu zaczyna się większe projekty. Pozwala Ci to od początku panować całkowicie nad strukturą katalogów i dodatkowych plików, których chcesz używać.

Web

No i doszliśmy do momentu, w którym mamy aplikacje typowo Webowe. Czyli to, co dzisiaj zdecydowanie jest na topie. Visual Studio umożliwia nam tu utworzenie kilku typów:

ASP.NET Core Empty

To jest pusta aplikacja webowa. Możesz z niej zrobić WebAPI, stronę internetową lub co tylko dusza zapragnie w webie. Podczas tworzenia zostaniesz zapytany o pewne ustawienia:

Wybór framework
  • TargetFramework – podajesz tutaj, jakiego frameworka chcesz używać. Zazwyczaj wybierzesz .NetCore 3.1 lub .NET 5
  • Configure for HTTPS – dzisiaj świat na wysokim miejscu stawia bezpieczeństwo. Również w sieci. Jeśli zaznaczysz tę opcję, projekt będzie skonfigurowany do pracy z protokołem HTTPS. Podczas pierwszego debugowania takiego projektu, Visual Studio utworzy i zainstaluje dla Ciebie specjalny certyfikat (Self Signed Certificate), a przeglądarki podczas pierwszego uruchomienia ostrzegą Cię, że ten certyfikat jest niebezpieczny i nie pochodzi z zaufanego źródła. Nie jest to w tym momencie istotne. Ważne, że gdy opublikujesz taką aplikację w sieci, musisz mieć na serwerze poprawny certyfikat. Dzisiaj można go otrzymać za darmo od Let’s Encrypt. Serwer, na którym mam swoje aplikacje – HostedWindows umożliwia utworzenie takiego certyfikatu i automatyczne przedłużenie go, za pomocą jednego kliknięcia.
  • Enable Docker – jeśli nie wiesz, czym jest docker, to moje wyjaśnienie nic Ci nie powie. Jeśli wiesz, to wiesz do czego służy ta opcja 🙂

A czysta aplikacja wygląda tak:

Widok projektu webowego

ASP.NET Core WebAPI

To jest template, który wykorzystasz konkretnie do utworzenia restowego WebAPI. Jeśli nie wiesz, czym jest restowe WebAPI, to bardzo krótko mówiąc…

Wyobraź sobie, że masz aplikację, w której chcesz wyświetlić wszystkich swoich znajomych z Facebooka. Facebook wystawia takie WebAPI (nie jest to restowe, ale dla przykładu załóżmy, że jest) i teraz możesz się z nim skomunikować i zapytać go o listę swoich znajomych, wywołując taki adres: https://api.facebook.com/friends

Ten adres jest tzw. „endpointem”. Podany przeze mnie endpoint nie istnieje w rzeczywistości, został wymyślony na potrzeby artykułu. Ale tak to mniej więcej działa – aplikacja wywołuje odpowiedni adres (endpoint), a serwer, jeśli Twoje uprawnienia na to pozwolą, odpowiada w jakiś ustalony sposób. Najczęściej odpowiedź jest w formie JSON, rzadziej XML.

Podczas tworzenia projektu tego typu, zostaniesz poproszony o dodatkowe informacje:

Wybór framework

Większość jest analogicznie jak wyżej z jedną małą różnicą.

  • Authentication Type – w projekcie WebAPI możesz i powinieneś w jakiś sposób autentykować i autoryzować swoich użytkowników. Ten artykuł zdecydowanie nie jest miejscem na wyjaśnienia co, jak i dlaczego, bo zagadnienie jest dość obszerne. Wkrótce o tym napiszę. Jeśli nie wiesz o co chodzi, to zostaw na None.

Projekt WebAPI nieco różni się od pustego:

Widok projektu WebAPI

Jak widzisz doszedł folder Controllers, zawierający kontrolery i kilka innych plików. Kod też jest inny. Zatem, jeśli chcesz stworzyć WebAPI, to zdecydowanie zacznij od tego projektu. Własne proste WebAPI dzięki temu można stworzyć w 10 minut (co dzisiaj udowodniłem komuś :)).

ASP.NET Core Web App

To jest typowa strona internetowa. Frontend tworzy się w RazorPages. A całość po utworzeniu wygląda tak:

Widok projektu RazorPages

ASP.NET Core App (Model-View-Controller)

To jest już aplikacja webowa, używająca wzorca MVC. Front tworzy się w RazorViews. Generalnie jest to bardzo podobne do RazorPages z pewnymi niuansami. Same dwa templaty (ten i poprzedni) są do siebie bardzo podobne i czasem jest ciężko stwierdzić, który będzie lepszy.

Generalnie zasada jest taka, że do prostych stron używamy raczej Razor Pages (ASP.NET Core App tego pierwszego), natomiast do bardziej skomplikowanych używamy MVC. Dodatkowo ten projekt może zawierać WebAPI. Ale osobiście radziłbym nie mieszać.

Utworzony pusty projekt wygląda tak:

Widok projektu Razor MVC

ASP.NET Web Application (.NET Framework)

To jest stary typ aplikacji webowych, który został zastąpiony .NET Core. Niemniej jednak mnóstwo aplikacji jest w tym utrzymywanych i rozwijanych. Odradzam używanie tego templatu do nowych programów.


No to tyle jeśli chodzi o podstawowe typy projektów. Jest ich jeszcze sporo, dodatkowe przychodzą z innymi pluginami, ale ich nazwy raczej same się opisują.

Jeśli znalazłeś w tekście błąd lub masz problem, podziel się w komentarzu

Podziel się artykułem na: