Rozdział 11. Podzespoły .NET.

Adam Boduch

Pojęcie „podzespół .NET” zostało wprowadzone w rozdziale 2. Podzespoły są fizycznymi plikami zapisanymi w formacie PE. Mogą być plikami wykonywalnymi (z rozszerzeniem .exe) albo współużytkowanymi bibliotekami .dll. Tak więc każdy program tworzony w środowisku .NET jest podzespołem.

Czasami można się spotkać z określeniem komponenty .NET.

Nim rozpocznę omawianie podzespołów, pragnę przybliżyć Ci tematykę modeli COM, które również stanowiły komponenty przenośne w środowisku Win32.

W dalszej części tego rozdziału nauczysz się pisać własne podzespoły oraz przeprowadzać komunikację pomiędzy poszczególnymi aplikacjami.

1 Czym jest COM
     1.1 Kontrolka w rozumieniu COM
     1.2 Odrobinę historii
     1.3 ActiveX
     1.4 DCOM
2 Podstawowy podzespół
     2.5 Deasembler .NET
3 Komponenty .NET
     3.6 Przygotowanie komponentu w Delphi
     3.7 Przygotowanie komponentu C#
          3.7.1 Kompilacja z poziomu linii poleceń
     3.8 Zalety stosowania podzespołów
4 Budowa podzespołu
5 Atrybuty podzespołu
6 Mechanizm refleksji
     6.9 Funkcja GetType
     6.10 Klasa System.Type
     6.11 Ładowanie podzespołu
     6.12 Przykład działania — program Reflection
          6.12.2 Odczyt atrybutów z podzespołu
     6.13 Własne atrybuty
          6.13.3 Deklaracja własnego atrybutu
          6.13.4 Odczyt wartości atrybutu
          6.13.5 Użycie atrybutu
7 Aplikacje .NET Framework SDK
     7.14 Global Assembly Cache Tool
     7.15 WinCV
     7.16 Narzędzie konfiguracji .NET Framework
     7.17 PEVerify — narzędzie weryfikacji
8 PInvoke
     8.18 Użycie funkcji Win32 API
     8.19 Użycie atrybutu DLLImport
9 Podsumowanie

Czym jest COM

Pierwowzorem idei komponentów .NET jest COM (ang. Component Object Model), COM+, DCOM. COM zapewniał niezależność języka programowania, lecz .NET wprowadza dodatkowo jego integrację oraz niezależność od platformy.

Rozwinięciem angielskiego skrótu COM jest Component Object Model (obiektowy model komponentów). Jest to specyfikacja firmy Microsoft, która w założeniu dotyczy tworzenia obiektów wielokrotnego użytku, niezależnych od języka programowania.

Aby zrozumieć ActiveX, trzeba poznać COM — postaram się zwięźle wytłumaczyć to pojęcie. Otóż firma Microsoft stworzyła model obiektów, które mogą być wykorzystywane w każdym środowisku programistycznym Windows. Wynikiem powstania obiektu COM jest kontrolka — plik binarny z rozszerzeniem .ocx. Kontrolka taka może być wykorzystywana zarówno w Delphi, jak i w językach Visual C++, C++ Builder czy Visual Basic.

Na razie obiekty COM działają jedynie w różnych środowiskach Windows — wykorzystanie ich poza tym systemem jest niemożliwe.

Kontrolka w rozumieniu COM

Na tym etapie będę używał słowa „kontrolka” w znaczeniu obiektu COM. Można powiedzieć, że obiekty COM są takim uniwersalnym komponentem podobnym do biblioteki DLL. Raz utworzona kontrolka może być wykorzystywana wiele razy, przez wielu programistów oraz w różnych środowiskach programowania. Jeżeli ktoś już napisał kontrolkę spełniającą daną funkcję, to powtórne tworzenie takiego samego elementu nie ma sensu. Przykładem może być przeglądarka WWW. Napisanie programu analizującego kod HTML jest niezwykle czasochłonnym i żmudnym zadaniem. Niekiedy jednak w tworzonym programie konieczne staje się wyświetlenie jakiegoś dokumentu w formie strony WWW. Dzięki technologii COM i ActiveX (o ActiveX opowiem nieco później) możemy zaimportować udostępnione przez twórców przeglądarki obiekty COM i wykorzystać je w Delphi w bardzo prosty sposób.

Odrobinę historii

COM jest technologią stosunkowo nową, bo powstałą kilka lat temu. Wprowadzenie jej miało na celu zapewnienie jednolitego standardu komunikowania się, tak aby np. (by jeszcze raz posłużyć się wcześniejszym przykładem) programiści mogli korzystać z możliwości przeglądania stron WWW w swoich aplikacjach. Firma Microsoft wyszła naprzeciw tym potrzebom i utworzyła moduł obiektów (COM), który umożliwia udostępnianie innym aplikacjom swoich metod.

ActiveX

ActiveX jest technologią opartą na COM. Pozwala na tworzenie kontrolek .ocx lub .dll. W rzeczywistości ActiveX to obiekt COM, tyle że posiadający własny interfejs (okna, kontrolki itp.). Tak więc mogliśmy tworzyć kontrolki ActiveX, wykorzystując np. Delphi oraz jego zalety projektowania wizualnego. Można było korzystać ze wszystkich komponentów i, ogólnie rzecz biorąc, projektowanie było łatwiejsze niż w przypadku zwykłych obiektów COM.

Dodatkowo ActiveX pozwala na wygenerowanie kodu umożliwiającego umieszczenie aplikacji na stronie WWW.

Platforma .NET jest następczynią COM, która zakłada integralność pomiędzy programami. Do tej pory programiści mogli budować osobne kontrolki, które później dawało się wykorzystywać w innych aplikacjach. Wiązało się to z rejestracją tej kontrolki i dodawaniem odpowiednich wpisów w rejestrze Windows. W .NET komunikacja między aplikacjami będzie ułatwiona — dany program będzie mógł dziedziczyć po klasie z innego, obsługiwać jego wyjątki itp.

DCOM

DCOM jest akronimem słów Distributed Component Object Model. Technologia ta, również opracowana przez firmę Microsoft, zakłada możliwość komunikowania się pomiędzy poszczególnymi kontrolkami COM za pośrednictwem internetu. Ta technologia również została uznana za przestarzałą w stosunku do platformy .NET.

Podstawowy podzespół

Jak już wiesz, podzespoły zawierają przestrzenie nazw, a te z kolei — kolejne klasy, struktury czy typy wyliczeniowe. Wszystkie aplikacje .NET wykorzystują podzespół mscorlib.dll. Podzespół ten zawiera nie tylko wszystkie podstawowe typy wykorzystywane w tej platformie, ale także klasę bazową Exception oraz wiele innych ważnych elementów.

Plik mscorelib.dll znajduje się w katalogu, w którym zainstalowane jest środowisko .NET Framework. Na moim komputerze jest to C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll.

Deasembler .NET

Deasembler jest programem działającym odwrotnie niż asembler — przekształca formę binarnej aplikacji do kodu pośredniego IL. Wraz z pakietem .NET Framework SDK jest dostarczany deasembler, który umożliwia analizę kodu binarnego PE i dokonanie konwersji do kodu IL.

SDK to skrót od Software Developer Kit. Pakiet .NET Framework SDK zawiera narzędzia, dokumentację oraz wiele przydatnych rzeczy, które mogą przydać się każdemu programiście .NET. Pakiet ten nie jest wymagany do prawidłowego funkcjonowania aplikacji .NET. Możesz go ściągnąć za darmo ze strony http://msdn.microsoft.com/netframework/downloads/updates/default.aspx.

Deasembler, o którym mowa, znajduje się w katalogu C:\Program Files\Microsoft.NET\SDK\v2.0\Bin (w zależności od konfiguracji) pod nazwą Ildasm.exe (rysunek 11.1).

csharp11.1.jpg
Rysunek 11.1. Deasembler w trakcie działania

Program jest dość prosty w użyciu. Wystarczy z menu File wybrać Open i wskazać plik, który następnie zostanie otwarty przez deasembler. Rysunek 11.1 przedstawia zawartość podzespołu mscorlib.dll.

Aby obejrzeć kod IL danego fragmentu aplikacji, wystarczy kliknąć odpowiednią pozycję. Przykładowy fragment takiego kodu IL wyświetlonego przez deasembler pokazano na rysunku 11.2.

csharp11.2.jpg
Rysunek 11.2. Podgląd kodu IL

Pewnie chciałbyś teraz zapytać, w jaki sposób można zabezpieczyć się przed deasemblacją programu. Prawda jest taka, że nie można. Nikt nie będzie jednak w stanie przekształcić kodu IL do rzeczywistego kodu źródłowego C#, więc nie ma powodów do zmartwień. Jedyne ryzyko jest takie, że każdy może dowiedzieć się, z jakich modułów korzystał projektant. Może także poznać strukturę klas aplikacji.

W menu File znajduje się ciekawa pozycja Dump, która umożliwia zapisanie zrzutu (kodu IL i metadanych) do pliku tekstowego.

Komponenty .NET

Główną zaletą .NET Framework jest niezależność od platformy oraz od języka. W rozdziale 2. wspominałem o technologii Common Type System (wspólny system typów). Pisałem wówczas, iż dzięki CTS możliwe jest komunikowanie się pomiędzy poszczególnymi podzespołami.

W środowisku Win32 możliwe jest podzielenie aplikacji na mniejsze jednostki. W tym celu stosuje się biblioteki DLL. Biblioteki DLL mogą eksportować funkcje i procedury, które z kolei mogą być wykorzystywane w aplikacji EXE. Innym rozwiązaniem jest zastosowanie kontrolek COM. Użycie takiej kontrolki w programie także nie jest łatwe, gdyż przed wykorzystaniem obiekt COM musi zostać zarejestrowany.

W .NET sytuacja wygląda zupełnie inaczej: poszczególne podzespoły mogą wykorzystywać się wzajemnie. Podzespół A może odwoływać się do metody z klasy, która znajduje się w podzespole B. W przeciwieństwie do bibliotek DLL, można korzystać z całych klas umieszczonych w danym podzespole (a nie tylko z procedur i funkcji).

W tej części rozdziału zaprezentuję, w jaki sposób aplikacja C# może wykorzystywać komponent napisany w Delphi [#]_.

Delphi jest obiektowym, profesjonalnym językiem programowania, stworzonym przez firmę Borland. Umożliwia projektowanie aplikacji zarówno na platformę Win32, jak i .NET. Zdaję sobie sprawę, że tematyka programowania w Delphi może Cię nie interesować i możesz nie znać tego języka. Dlatego prezentowane tutaj przykłady kodów źródłowych umieszczone są w formie źródłowej oraz skompilowanej na płycie CD dołączonej do książki.

Przygotowanie komponentu w Delphi

Nasz przykładowy program może być bardzo prosty, nie musi wykonywać żadnych wyspecjalizowanych działań. Odgrywa jedynie rolę demonstracyjną.

Nie jest w tej chwili istotne, czy nasza aplikacja będzie podzespołem .exe, czy .dll — jest skompilowana do kodu IL, więc można ją wykorzystać w ten sam sposób.

Na listingu 11.1 znajduje się przykładowy program zawierający klasę Vehicle. Program jest prosty, zawiera kilka metod, które wyświetlają określony tekst na konsoli. Jak już wspominałem, nie jest to program zbyt użyteczny, bowiem jedynie demonstruje współdziałanie komponentów .NET.

Listing 11.1. Przykładowy podzespół napisany w Delphi

library Assembly;

{$APPTYPE CONSOLE}

{ Copyright (c) 2004 by Adam Boduch }

type
  Vehicle = class
  private
    procedure TurnLeft;
    procedure TurnRight;
    procedure Breaks;
  public
    procedure SendMsg(const Message : String);
  end;


{ Vehicle }

procedure Vehicle.Breaks;
begin
  Console.WriteLine('Włączam hamulce.');
end;

procedure Vehicle.SendMsg(const Message: String);
begin
{ sprawdzenie, jaki parametr został przekazany do procedury }
  if Message = 'Left' then TurnLeft
  else if Message = 'Right' then TurnRight
  else if Message = 'Breaks' then Breaks
  else Console.WriteLine('Nieprawidłowa komenda.');
end;

procedure Vehicle.TurnLeft;
begin
  Console.WriteLine('Skręcam w lewo.');
end;

procedure Vehicle.TurnRight;
begin
  Console.WriteLine('Skręcam w prawo.');
end;

begin
{ empty }
end.

Jeżeli masz na swoim komputerze zainstalowane środowisko Delphi 8 lub nowsze, możesz skompilować w nim poniższy kod. Dla wszystkich tych, którzy nie mają zainstalowanego środowiska Delphi, na płycie CD dołączonej do książki zamieściłem wersję skompilowaną.

Parametr przekazany do metody SendMsg() decyduje o tym, jaka procedura zostanie wywołana z klasy Vehicle.

Przygotowanie komponentu C#

Przede wszystkim uruchom środowisko Visual C# Express Edition i utwórz nowy projekt aplikacji konsolowej. W oknie Solution odnajdź gałąź References, kliknij ją prawym przyciskiem myszy i wybierz Add Reference. Dzięki oknu Add Reference możemy dodać do programu odwołanie do innego komponentu .NET lub kontrolki COM. Nas interesuje zakładka Browse — pozwala ona wskazać skompilowany podzespół, do którego zostanie utworzone odwołanie.

Odszukaj i wskaż skompilowany podzespół Assembly.dll, który możesz znaleźć na płycie CD dołączonej do książki. Po dodaniu nowa pozycja zostanie wyświetlona w oknie Solution. Teraz czas na wykorzystanie klasy Vehicle z naszego podzespołu. Rozwiązanie znajduje się na listingu 11.2.

Listing 11.2. Program korzystający z podzespołu

using System;
using Assembly;

/* Copyright (c) 2004 by Adam Boduch */

class MainClass
{
    public static void Main()
    {
        try
        {
            Vehicle Car = new Vehicle();  // utworzenie obiektu
            String S; // deklaracja łańcucha

            Console.WriteLine("Przykładowa aplikacja korzystająca z podzespołu Assembly");
            Console.WriteLine("Podaj komendę do wysłania:");
            Console.WriteLine("   -- Left");
            Console.WriteLine("   -- Right");
            Console.WriteLine("   -- Breaks");
            Console.WriteLine();
            Console.Write("Komenda: ");

            S = Console.ReadLine(); // pobranie komendy
            Car.SendMsg(S); // przesłanie do podzespołu

        }
        catch (Exception e)
        {
            Console.WriteLine(e.ToString());
        }

        Console.Read();
    }
}

Zwróć uwagę, iż przy pomocy instrukcji using musimy włączyć do programu odpowiednią przestrzeń nazw. Dalsza część kodu powinna być dla Ciebie jasna. Rysunek 11.3 prezentuje działanie takiego programu.

csharp11.3.jpg
Rysunek 11.3. Prezentacja możliwości wykorzystania podzespołu

Kompilacja z poziomu linii poleceń

Oczywiście do kompilacji takiej aplikacji można również użyć kompilatora csc.exe. Ja skorzystałem ze środowiska Visual C# Express Edition jedynie ze względu na wygodę.

Aby kompilacja takiego programu przy pomocy kompilatora csc.exe przebiegła bez żadnych przeszkód, należy kompilować w ten sposób:

csc /r:C:\katalog\Assembly.dll /out:C:\demo.exe C:\katalog\demo.cs

Powyższe polecenie odniesie oczekiwany efekt, pod warunkiem że plik Assembly.dll oraz demo.cs znajdują się w katalogu C:\katalog. Przełącznik /r: określa podzespół, z jakim zostanie skompilowany nasz program. Dzięki temu kompilator rozpoznaje typ Vehicle w kodzie C#. Z kolei przełącznik /out: określa ścieżkę, w której zostanie umieszczona wersja wynikowa naszego programu.

Jeżeli kompilator nie wyświetli żadnego błędu, można spróbować uruchomić aplikację demo.exe.

Zalety stosowania podzespołów

W poprzednich przykładach zaprezentowałem sposób na wykorzystanie możliwości danego podzespołu z poziomu drugiego programu. Taki przykład powinien zobrazować Ci pewną zaletę platformy .NET, a mianowicie niezależność. Jak widzisz, jeden program (podzespół) został napisany w języku Delphi, a drugi — w C#. Niezależnie od tego bez żadnych problemów mogliśmy wykorzystać klasę Vehicle. W ten sposób skonstruowana jest cała biblioteka klas .NET Framework. Plik mscorelib.dll jest skompilowanym podzespołem zawierającym setki klas. Programując na platformie .NET, możemy ten plik włączyć do swojej aplikacji i wykorzystywać znajdujące się w nim klasy.

Będąc programistom, możesz pisać swoje aplikacje w języku C#, a następnie sprzedawać. Wyobraź sobie, że napisałeś program zawierający klasę służącą do szyfrowania tekstu. Jest ona tak rewolucyjna, że bez problemów znajdujesz kupców na swój program. Nie chcesz jednak udostępniać swoich kodów źródłowych, sprzedajesz więc wersję skompilowaną. Firma, która kupiła od Ciebie ów program, może wykorzystywać klasy zawarte w podzespole, nie znając przy tym zawartości kodu! Takiej firmie wystarczy jedynie dokumentacja klasy. Przy pomocy mechanizmu dziedziczenia oraz polimorfizmu firma, która kupiła od Ciebie podzespół, może napisać nową klasę, dziedziczącą po Twojej, znajdującej się w osobnym podzespole!

Inną zaletą dzielenia aplikacji na kilka podzespołów jest podział funkcjonalności. Możesz podzielić aplikację na kilka mniejszych modułów według ich funkcjonalności. Przypuszczam, że już niedługo będziesz (jeżeli jeszcze nie jesteś) zawodowym programistą. Możesz pisać aplikacje i udostępniać je na zasadach licencji shareware. W każdej aplikacji zamieszczasz formularz służący do rejestracji programu i uzyskiwania klucza produktu. Po co ten sam kod kopiować do wielu programów? Nie lepiej przenieść go do osobnego podzespołu, który będziemy ładować, kiedy będzie potrzebny? Dzielenie aplikacji na kilka modułów (podzespołów) ma też inną zaletę. Jeżeli w jednym z modułów wykryjesz błąd, możesz udostępnić swoim klientom poprawkę w postaci jednego podzespołu, a nie całej aplikacji.

Możesz uniemożliwić innym podzespołom dziedziczenie lub wręcz wykorzystywanie Twoich klas lub ich elementów. Należy wówczas deklarować elementy z użyciem modyfikatora dostępu internal.

Budowa podzespołu

Powiedzieliśmy sobie, że podzespół to nie tylko pośredni kod IL. W rzeczywistości jego zawartość można podzielić na cztery elementy:

*kod pośredni IL,
*zasoby aplikacji,
*manifest,
*metadane.

Te wszystkie elementy są umieszczane w jednym pliku wykonywalnym (.exe lub .dll).
Kod pośredni jest zapisany w języku IL, który przypomina nieco Asemblera. Wspominałem o nim w rozdziale 4. i wydaje mi się, że to pojęcie nie wymaga dalszego wyjaśnienia.

O zasobach wspominałem w poprzednim rozdziale. Zasoby to pliki dźwiękowe, graficzne, tekstowe oraz inne pliki wykonywalne. Zasoby mogą być umieszczone w zewnętrznym pliku, ale najczęściej są umieszczane w aplikacji wykonywalnej.

Metadane to informacje o kodzie źródłowym. Są one generowane w trakcie kompilacji i opisują klasy, metody czy inne typy wykorzystywane w kodzie. Dzięki metadanym jesteśmy w stanie stwierdzić, jaka jest zawartość danego podzespołu, lepiej wykorzystać jego funkcjonalność. Wykonaj małe doświadczenie. W kodzie źródłowym kliknij prawym przyciskiem nazwę jakiejś klasy biblioteki FCL. Z menu podręcznego wybierz pozycję Go to definition. Na podstawie metadanych zawartych w podzespole środowisko Visual C# Express Edition jest w stanie wyświetlić informację o zawartości danej klasy czy przestrzeni nazw.

Manifest to informacje o metadanych podzespołu. Manifest zawiera informacje odnośnie do podzespołu, jego relacji z innymi komponentami .NET. W szczególności jest to:

*Nazwa atrybutu — ciąg znaków określający nazwę podzespołu.
*Wersja podzespołu — numer wersji podzespołu.
*Lokalizacja — informacje o lokalizacji lub o języku interfejsu podzespołu.
*Ścisła kontrola nazw — publiczny klucz podzespołu. O tym opowiem w dalszej części rozdziału.
*Lista plików — informacje o plikach wykorzystywanych w projekcie.

Atrybuty podzespołu

W podzespołach można zadeklarować określone atrybuty opisujące dany podzespół. Mogą one być później odczytywane przez inne programy w celu weryfikacji numeru wersji aplikacji oraz nazwy producenta. Innymi słowy, atrybuty są narzędziem programisty, który może decydować o zawartości manifestu.
Jeżeli masz otwarty projekt aplikacji C#, nieważne, czy Windows Forms, czy konsolowy, przejdź do okna Solution w gałęzi Properties, wybierz pozycję AssemblyInfo.cs i kliknij ją:

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following 
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("AssemblyApp")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("AssemblyApp")]
[assembly: AssemblyCopyright("Copyright ©  2006")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible 
// to COM components. If you need to access a type in this assembly from 
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("a0bf0a73-54d8-4b7b-9472-3a2c0980654f")]

// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version 
//      Build Number
//      Revision
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

Zawartość tego modułu jest również kompilowana wraz z pozostałym kodem. Moduł ten zawiera informacje o naszej aplikacji w formie atrybutów. Te informacje zostaną skompilowane do kodu pośredniego IL.

Jak widzisz, umieszczanie atrybutów w kodzie programu jest dość specyficzne. Atrybuty umieszczane są w nawiasach kwadratowych, z użyciem słowa określającego przeznaczenie atrybutu (assembly):

[assembly: nazwa atrybutu]

Atrybuty są zwykłymi klasami, które dziedziczą po Attribute (która to zadeklarowana jest w przestrzeni System.Reflection). Przykładowo, deklaracja klasy AssemblyFileVersionAttribute w przestrzeni System.Reflection wygląda następująco:

public sealed class AssemblyFileVersionAttribute : Attribute
{
    public AssemblyFileVersionAttribute(string version);
    public string Version { get; }
}

W przypadku tego atrybutu do klasy należy przekazać jeden parametr w postaci typu string.
Jeżeli chcesz, możesz zmienić zawartość atrybutów, m.in. podając prawidłową nazwę aplikacji oraz wersję czy autora.

Mechanizm refleksji

Powiedziałem już, że dzięki metadanym mamy możliwość odczytania informacji na temat elementów umieszczonych w podzespole. Pokazywałem, jak to zrobić, korzystając z opcji Go to definition w menu podręcznym środowiska Visual C# Express Edition.

Mechanizm refleksji umożliwia odczytanie informacji odnośnie do metadanych. Z tego mechanizmu korzysta m.in. środowisko Visual C# Express Edition przy odczytywaniu informacji z podzespołu. Mamy możliwość przeglądania zawartości biblioteki klas FCL lub jakiegokolwiek innego podzespołu przy pomocy okna Object Browser (rysunek 11.4).

csharp11.4.jpg
Rysunek 11.4. Okno Object Browser

Okno Object Browser można wywołać, wybierając z menu View pozycję Other Windows/Object Browser.

Korzystając z przycisku ... znajdującego się obok listy rozwijanej Browse, mamy możliwość wyboru podzespołu, z którego dane zostaną wyświetlone w oknie. Rysunek 11.4 przedstawia informacje o metadanych podzespołu Assembly.dll (napisanego w Delphi).

Po zaznaczeniu wybranej pozycji w prawym oknie wyświetlona zostanie lista elementów danej klasy wraz z ewentualnymi parametrami metod.

Okno Object Browser umożliwia odczytywanie informacji z wybranego podzespołu i równocześnie stanowi prosty i szybki sposób na poznanie budowy programu. Istnieje możliwość zastosowania mechanizmu refleksji w naszej aplikacji dzięki przestrzeni nazw System.Reflection, która jest udostępniana przez pakiet .NET Framework.

Mechanizm refleksji pozwala na odczyt dowolnego podzespołu .NET. Nie jest więc istotne, czy program został napisany w C#, Delphi czy w C++. Po prostu program napisany w Delphi będzie zawierał więcej informacji, gdyż kompilator Delphi dodatkowo włącza w aplikację wykonywalną zawartość przestrzeni Borland.Delphi.System.

Funkcja GetType

Każda klasa .NET posiada metodę GetType(), która jest dziedziczona po klasie głównej — System.Object. Owa metoda zwraca rezultat w postaci klasy System.Type. Dzięki niej można odczytywać takie informacje jak nazwa podzespołu, jego wersja, a także przestrzeń adresowa, w jakiej się on znajduje. Tym zajmiemy się nieco później — teraz pozostaniemy jedynie przy odczytaniu nazwy danego typu:

using System;

namespace FooApp
{
    class Program
    {
        static void Main(string[] args)
        {
            int I = 10;
            double D = 10.5;

            Console.WriteLine("Zmienna I jest typu: " + I.GetType().ToString());
            Console.WriteLine("Zmienna D jest typu: " + D.GetType().ToString());

            Console.Read();

        }
    }
}

Typy int czy double w C# są odpowiednikami typów .NET, również są klasami, zatem posiadają metodę GetType(), która zwraca informacje o obiekcie. W powyższym przykładzie użyłem metody ToString() do znakowego przedstawiania typu zmiennej. Działanie takiego programu spowoduje wyświetlenie na ekranie tekstu:

Zmienna I jest typu System.Int32
Zmienna D jest typu System.Double

Klasa System.Type

Jak powiedziałem wcześniej, metoda GetType() zwraca informacje w postaci obiektu System.Type, z którego można wyczytać więcej informacji na temat konkretnego obiektu .NET.

Na formularzu WinForms umieśćmy teraz komponent ListBox oraz Button. Nazwijmy je, odpowiednio, lbInfo oraz btnGetInfo (nazwy komponentów nie odgrywają większej roli). Zdarzenie Click przycisku powinno wyglądać tak jak na listingu 11.3.

Listing 11.3. Wykorzystanie klasy System.Type

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace WinForms
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
        }

        private void btnGetInfo_Click(object sender, EventArgs e)
        {
            System.Type MyType;

            MyType = sender.GetType();

            lbInfo.Items.Add("Nazwa klasy: " + MyType.FullName);
            lbInfo.Items.Add("Przestrzeń nazw: " + MyType.Namespace);
            lbInfo.Items.Add("Nazwa podzespołu: " + MyType.Assembly.FullName);
            lbInfo.Items.Add("Wersja podzespołu: " + MyType.Assembly.GetName().Version.ToString());
            lbInfo.Items.Add("Nazwa pliku: " + MyType.Module.Name);
        }
    }
}

Po naciśnięciu przycisku aplikacja pobiera informację na temat komponentu Button. Wyświetla m.in. pełną nazwę klasy (System.Windows.Forms.Button) oraz podzespołu.

Należy zapamiętać, że nazwa, jaką nadaje się komponentowi, nie ma większego znaczenia dla działania programu. Nie można jednak przepisywać bezmyślnie kodu z tej książki, gdyż w poszczególnych przypadkach nazwa komponentu może się różnić od tej, którą ja nadałem. W takiej sytuacji program nie zostanie skompilowany.

Na samym początku do zadeklarowanej zmiennej MyType jest przypisywany rezultat działania metody GetType(). Od tego momentu zmienna MyType zawiera informację na temat przycisku. Następnie informacje te są umieszczane w komponencie typu ListBox.

Ładowanie podzespołu

Program przedstawiony w poprzednim przykładzie pobierał informacje o komponencie typu Button. Jest to dość proste, lecz niezbyt przydatne. Okno Object Browser umożliwia odczytanie informacji na temat dowolnego podzespołu. Aby to wykonać, można wykorzystać klasę System.Reflection.Assembly.

Dla przypomnienia: dla kompilatora nie ma znaczenia, czy zadeklarowana zmienna będzie typu Assembly czy System.Reflection.Assembly.

Załadowanie podzespołu wiąże się zaledwie z jedną linią kodu — wywołaniem metody LoadFrom() z klasy System.Reflection.Assembly (w skrócie będę nazywał tę klasę po prostu Assembly):

System.Reflection.Assembly AFile;
AFile = System.Reflection.Assembly.LoadFrom("Assembly.exe");

Taki zapis umożliwia następnie odczytanie klas znajdujących się w podzespole oraz ich właściwości, zdarzeń itd.

Przykład działania — program Reflection

Rysunek 11.5 prezentuje przykładowy program Reflection, który analizuje zawartość podzespołu. Zadaniem aplikacji jest wyświetlanie listy typów zawartych w podzespole wraz z ich właściwościami, polami, metodami oraz zdarzeniami.

csharp11.5.jpg
Rysunek 11.5. Przykładowy program Reflection

Na rysunku 11.5 program odczytuje zawartość biblioteki Assembly.dll, którą opisywałem w trakcie prezentowania możliwości wspólnego modelu programowania.

Interfejs aplikacji składa się z komponentów TreeView (komponent nazwałem tvAssembly), Button oraz niewidoczny OpenFileDialog (przypomnę — służy on do wyświetlania standardowego okna Windows Otwórz plik). Komponent TreeView został użyty z uwagi na możliwość tworzenia gałęzi, co daje wrażenie hierarchicznej struktury.

W programie wykorzystałem również komponent ProgressBar. Pokazuje on postęp w trakcie analizowania i wczytywania podzespołu.

Cały kod źródłowy został przedstawiony na listingu 11.4. Najpierw mu się przyjrzyj, później przeczytasz opis jego budowy.

Listing 11.4. Kod źródłowy programu Reflection

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace Reflection
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
        }

        private void btnLoad_Click(object sender, EventArgs e)
        {
            System.Reflection.Assembly AFile;
            TreeNode ClassTree, PropTree, MethodTree, FieldTree, EventTree;

            // usunięcie wszystkich gałęzi (jeżeli są) 
            tvAssembly.Nodes.Clear();

            // wyświetlenie okna dialogowego 
            openFileDialog1.ShowDialog();

            try
            {
                // pobranie nazwy wybranego podzespołu oraz załadowanie go 
                AFile = System.Reflection.Assembly.LoadFrom(openFileDialog1.FileName);
                Text = "Reflection [" + openFileDialog1.FileName + "]";
            }
            catch
            {
                MessageBox.Show("Nie można załadować podzespołu!");
                return;
            }
            // załadowanie informacji o klasach znajdujących się w podzespole 
            System.Type[] MyType = AFile.GetTypes();            
            
            progressLoad.Maximum = MyType.Length;

            for (int i = 0; i < MyType.Length; i++)
            {
                // utworzenie nowego węzła o nazwie klasy z podzespołu 
                ClassTree = tvAssembly.Nodes.Add(MyType[i].FullName);

                // utworzenie nowej gałęzi o nazwie „Właściwości” 
                PropTree = ClassTree.Nodes.Add("Właściwości");

                for (int subI = 0; subI < MyType[i].GetProperties().Length; subI++)
                {
                    PropTree.Nodes.Add(MyType[i].GetProperties().GetValue(subI).ToString());
                }
                // utworzenie nowej gałęzi o nazwie „Metody” 
                MethodTree = ClassTree.Nodes.Add("Metody");

                for (int subI = 0; subI < MyType[i].GetMethods().Length; subI++)
                {
                    MethodTree.Nodes.Add(MyType[i].GetMethods().GetValue(subI).ToString());
                }
                // utworzenie nowej gałęzi o nazwie „Pola” 
                FieldTree = ClassTree.Nodes.Add("Pola");

                for (int subI = 0; subI < MyType[i].GetFields().Length; subI++)
                {
                    FieldTree.Nodes.Add(MyType[i].GetFields().GetValue(subI).ToString());
                }
                // utworzenie nowej gałęzi o nazwie „Zdarzenia” 
                EventTree = ClassTree.Nodes.Add("Zdarzenia");

                for (int subI = 0; subI < MyType[i].GetEvents().Length; subI++)
                {
                    MethodTree.Nodes.Add(MyType[i].GetEvents().GetValue(subI).ToString());
                }
                progressLoad.Value = i;
            }
            progressLoad.Value = 0;
        }
    }
}

Po załadowaniu wybranego przez użytkownika podzespołu następuje odczytanie znajdujących się w nim typów (metoda GetTypes()) do tablicy MyTypes. Następnie pętla for analizuje po kolei wszystkie elementy, dodając za każdym razem nową pozycję w komponencie TreeView:

ClassTree = tvAssembly.Nodes.Add(MyType[i].FullName);

Tworzenie nowej pozycji w komponencie TreeView przypomina nieco tworzenie nowej linii w kontrolce ListBox. Zmienna ClassTree stanowi niejako „uchwyt”, który będzie wykorzystywany do tworzenia kolejnych odgałęzień.

Kolejnym krokiem w działaniu programu jest odczytanie właściwości, metod, zdarzeń, pól oraz metod każdej z klas. W każdym przypadku proces ten jest niemal identyczny (różne są jedynie nazwy służących do tego funkcji):

PropTree = ClassTree.Nodes.Add("Właściwości");

for (int subI = 0; subI < MyType[i].GetProperties().Length; subI++)
{
     PropTree.Nodes.Add(MyType[i].GetProperties().GetValue(subI).ToString());
}

Na samym początku tworzone jest odgałęzienie Właściwości, do którego będą doklejane kolejne gałęzie zawierające nazwy właściwości. W tym celu kolejna pętla for pobiera nazwy kolejnych właściwości klasy.

Jak zapewne zdążyłeś się zorientować, do pobierania listy właściwości danego typu służy metoda GetProperties(), która również zwraca listę w postaci tablicy.

Odczyt atrybutów z podzespołu

W naszym programie Reflection odczytywaliśmy jedynie drzewo klas i metod danego podzespołu. Teraz zajmiemy się odczytem niestandardowych atrybutów podzespołu, takich jak jego opis, tytuł itp.
W tym celu .NET Framework posiada klasy umożliwiające odczyt np. tytułu podzespołu (klasa AssemblyTitleAttribute). Naszym zadaniem jest załadowanie wybranego podzespołu (to już opisywałem), a następnie wywołanie metody GetCustomAttributes(), która pobierze nazwy wszystkich atrybutów znajdujących się w podzespole i zapisze je do tablicy.

Rozbudujmy więc naszą aplikację Reflection, dodając do niej nową funkcjonalność. Do formularza dodałem kilka komponentów typu TextBox, które będą przechowywać nazwę podzespołu, jego opis, opis praw autorskich oraz wersję. Odpowiada za to prywatna metoda:

private void loadAttributes(System.Reflection.Assembly AFile)
{
    System.Object[] Attrs = AFile.GetCustomAttributes(true);

    for (int i = 0; i < Attrs.Length; i++)
    {
        /*
            W tych instrukcjach następuje sprawdzenie, czy
            dany obiekt należy do szukanej przez nas klasy.
            Pozwala to uniknąć błędów w trakcie działania programu.
            Inaczej mówiąc, instrukcja if sprawdza, czy w podzespole
            znajduje się dany atrybut (przykładowo: „AssemblyTitleAttribute”
         */
         
        if (Attrs[i] is AssemblyTitleAttribute)
        {
            edtTitle.Text = (Attrs[i] as AssemblyTitleAttribute).Title;
        }

        if (Attrs[i] is AssemblyDescriptionAttribute)
        {
            edtDescription.Text = (Attrs[i] as AssemblyDescriptionAttribute).Description;
        }

        if (Attrs[i] is AssemblyCopyrightAttribute)
        {
            edtCopyright.Text = (Attrs[i] as AssemblyCopyrightAttribute).Copyright;
        }

        if (Attrs[i] is ComCompatibleVersionAttribute)
        {
            edtVersion.Text = Convert.ToString(
                (Attrs[i] as ComCompatibleVersionAttribute).MajorVersion) + '.' +
                Convert.ToString((Attrs[i] as ComCompatibleVersionAttribute).MinorVersion);

        }
    }
}

Przed próbą kompilacji programu nie można zapomnieć o włączeniu do programu przestrzeni nazw System.Reflection i System.Runtime.InteropServices.

W powyższym przykładzie posłużyłem się mechanizmem rzutowania za pomocą operatorów is i as:

if (Attrs[i] is AssemblyTitleAttribute)
{
    edtTitle.Text = (Attrs[i] as AssemblyTitleAttribute).Title;
}

Najpierw program sprawdza, czy element tablicy określony zmienną i jest typu AssemblyTitleAttribute. Jeżeli tak, następuje rzutowanie tego elementu tablicy oraz wyciągnięcie wartości właściwości Title. Kontrolka o nazwie edtText w rzeczywistości jest komponentem TextBox umieszczonym na formularzu.

Zastosowanie takiego mechanizmu było konieczne ze względu na to, iż funkcja GetCustomAttributes zwraca elementy w postaci tablicy typu System.Object. Należało więc ustalić, czy rzutowanie na daną klasę (np. AssemblyTitleAttribute) powiedzie się — stąd konieczność użycia operatorów is i as.

Własne atrybuty

Powiedziałem, że atrybuty są danymi opisującymi dane. Oprócz standardowych atrybutów — określających np. prawa autorskie do podzespołu — można zadeklarować własne, które tak samo będą odczytywane przez programy typu Reflection.

Do czego mogą się przydać własne atrybuty? Możliwości są nieograniczone. Przykładowo: umieszczanie w swoich programach takich informacji jak odnośnik do strony WWW zawierającej kod XML danego programu. Gdy wszystkie Twoje aplikacje będą zawierały odpowiedni atrybut z odnośnikiem do pliku XML, to będą mogły zacieśnić współpracę — w pliku XML może znajdować się dodatkowa informacja dotycząca programu.

Inny przykład: notowanie błędów aplikacji. Można oczywiście umieszczać komentarz przy klasie, w której poprawiono kod. Można także użyć atrybutu, np. w taki sposób:

[BugFixedAttribute("Adam", "19-05-2006", "Dodałem metodę")]
[BugFixedAttribute("Janek", "21-06-2006", "Poprawka")]
class Program
{
    static void Main(string[] args)
    {
    }
}

Własny atrybut BugFixAttribute opisuje dane dotyczące błędu — nazwisko programisty, który go naprawił, datę naprawy oraz komentarz. Takie informacje może zawierać także ID błędu. Następnie odpowiedni moduł aplikacji odczyta wszystkie wartości atrybutu TBugFixAttribute, w tym ID błędu, oraz pozwoli na połączenie z bazą danych, która na podstawie ID ujawni więcej informacji na temat poprawki.

Deklaracja własnego atrybutu

Aby dany atrybut był rozpoznawany przez kompilator, trzeba w aplikacji utworzyć klasę o nazwie odpowiadającej atrybutowi:

class BugFixedAttribute : Attribute
{
    public BugFixedAttribute(string Programmer, string Date, string Comment)
    {
    }
}

Taka klasa musi dziedziczyć po Attribute. Jak widzisz, klasa BugFixedAttribute posiada konstruktor z trzema parametrami. Jest to ważny element programu, gdyż kiedy użyjemy atrybutu, jego wartości zostaną przypisane do parametrów konstruktora:

[BugFixedAttribute("Adam", "19-05-2006", "Dodałem metodę")] 

Użycie atrybutu w kodzie programu przypomina wywołanie metody klasy. Mamy również możliwość przekazania do niego określonych parametrów oddzielonych znakiem przecinka.

Istnieje możliwość jawnego przypisania parametrów atrybutu do danych właściwości — np.:

[BugFixedAttribute(Programmer = "Janek", Date = "21-06-2006", Comment = "Poprawka")]

Wówczas w klasie BugFixedAttribute należy zadeklarować właściwości Programmer, Date oraz Comment:

class BugFixedAttribute : Attribute
{
private string FProgrammer;
private string FDate;
private string FComment;

public string Programmer
{
    get
    {
        return FProgrammer;
    }
    set
    {
        FProgrammer = value;
    }
}
public string Date
{
    get
    {
        return FDate;
    }
    set
    {
        FDate = value;
    }
}
public string Comment
{
    get
    {
        return FComment;
    }
    set
    {
        FComment = value;
    }
}


public BugFixedAttribute(string AProgrammer, string ADate, string AComment)
{
    FProgrammer = AProgrammer;
    FDate = ADate;
    FComment = AComment;
}
public BugFixedAttribute()
{
}
}

Wydaje mi się, że zapis z konstruktorem jest szybszy i przejrzystszy, aczkolwiek nie jest zbyt czytelny dla programisty nieznającego parametrów atrybutu.

Odczyt wartości atrybutu

W poprzednim przykładzie pokazywałem, w jaki sposób można odczytać atrybuty znajdujące się w innym podzespole. Aby odczytać atrybuty znajdujące się we własnym programie, nie trzeba korzystać z typu Assembly — kod stanie się bardziej przejrzysty po użyciu następującej instrukcji:

System.Object[] Attrs;

Attrs = typeof(Foo).GetCustomAttributes(typeof(BugFixedAttribute), true);

Operator typeof jest tutaj ważny, gdyż zwraca typ danych (w tym przypadku klasy) w postaci zmiennej typu System.Type. Po uzyskaniu typu można wywołać metodę GetCustomAttributes(), pobierającą atrybuty programu.

Pierwszy parametr funkcji, GetCustomAttributes(), informuje o tym, że atrybuty mają być jedynie typu BugFixAttributes. Jeśli mamy tablicę danych typu Object (Attrs), odczyt atrybutów jest taki sam jak w poprzednio prezentowanym przykładzie.

Listing 11.5 zawiera pełny kod źródłowy programu odczytującego atrybuty użyte w programie.

using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace AttrsApp
{
    [AttributeUsage(AttributeTargets.All, AllowMultiple=true)]
    class BugFixedAttribute : Attribute
    {
        private string FProgrammer;
        private string FDate;
        private string FComment;

        public string Programmer
        {
            get
            {
                return FProgrammer;
            }
            set
            {
                FProgrammer = value;
            }
        }
        public string Date
        {
            get
            {
                return FDate;
            }
            set
            {
                FDate = value;
            }
        }
        public string Comment
        {
            get
            {
                return FComment;
            }
            set
            {
                FComment = value;
            }
        }


        public BugFixedAttribute(string AProgrammer, string ADate, string AComment)
        {
            FProgrammer = AProgrammer;
            FDate = ADate;
            FComment = AComment;
        }
        public BugFixedAttribute()
        {
        }
    }

    [BugFixedAttribute("Adam", "19-05-2006", "Dodałem metodę")]
    [BugFixedAttribute(Programmer = "Janek", Date = "21-06-2006", Comment = "Poprawka")]
    class Foo
    {
        static void Bar()
        {
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            System.Object[] Attrs;

            Attrs = typeof(Foo).GetCustomAttributes(typeof(BugFixedAttribute), true);

            for (int i = 0; i < Attrs.Length; i++)
            {
                Console.WriteLine("-------");
                Console.WriteLine("Programista: " + (Attrs[i] as BugFixedAttribute).Programmer);
                Console.WriteLine("Data: " + (Attrs[i] as BugFixedAttribute).Date);
                Console.WriteLine("Komentarz: " + (Attrs[i] as BugFixedAttribute).Comment);
            }
            Console.Read();
        }
    }
}

Rysunek 11.6 prezentuje program w trakcie działania.

csharp11.6.jpg
Rysunek 11.6. Odczyt atrybutów użytych w programie

Użycie atrybutu

Przypatrz się dokładnie listingowi 11.5. Klasa BugFixedAttribute została opatrzona atrybutem AttributeUsage:

[AttributeUsage(AttributeTargets.All, AllowMultiple=true)]

Takie użycie oznacza, że nasza klasa BugFixedAttribute będzie mogła być użyta w połączeniu z dowolnym elementem programu. Parametr AllowMultiple informuje, że nasz atrybut BugFixedAttribute może być użyty w wielu miejscach aplikacji. Wartość false spowodowałaby błąd kompilacji: Duplicate 'BugFixedAttribute' attribute w przypadku wielokrotnego użycia tego samego atrybutu.

Aplikacje .NET Framework SDK

Pakiet .NET Framework Software Developer Kit zawiera aplikacje przydatne każdemu programiście .NET. Pakiet ten nie jest wymagany do prawidłowego działania aplikacji, lecz może się okazać przydatny, gdybyśmy chcieli np. wykorzystać deasemblera .NET czy inne programy związane z tą platformą. Chciałbym w tym miejscu wspomnieć o kilku użytecznych aplikacjach dostępnych w pakiecie SDK.

Więcej informacji na temat omawianych tu programów (np. dodatkowe parametry czy opcje) można znaleźć w dokumentacji .NET Framework.

Spod adresu http://msdn.microsoft.com/netframework/downloads/updates/default.aspx możesz ściągnąć najnowszy pakiet .NET Framework SDK.

Global Assembly Cache Tool

Obiekty COM należało zarejestrować przed użyciem, co powodowało tworzenie odpowiednich wpisów w rejestrze. Dopiero później można było użyć takiej kontrolki.

Jeżeli chodzi o komponenty .NET, zabiegi takie nie są konieczne — stosowny przykład zaprezentowałem wcześniej w tym rozdziale, gdzie klasa z aplikacji napisanej w Delphi była wykorzystana w programie C#. W tamtym przykładzie skorzystałem z tzw. komponentów prywatnych (ang. private components).
Oznaczało to, że tylko jedna nasza aplikacja mogła korzystać z owego podzespołu, a przy tym należało uważać na ścieżki (katalogi), w których się ona znajdowała. Przykładowo, w opisywanym przykładzie podzespół, z którego korzystamy, zawsze będzie musiał znajdować się w tym samym katalogu co korzystający z niego program.

.NET zakłada możliwość tworzenia podzespołów współużytkowanych (ang. shared components), czyli takich, które raz zarejestrowane zostaną umieszczone w jednym głównym katalogu. Z takiego katalogu będzie mogła korzystać każda aplikacja, która zechce użyć naszego podzespołu. Przestrzeń, w której są rejestrowane takie podzespoły, nazywa się Global Assembly Cache (w skrócie GAC) i znajduje się w katalogu C:\WINDOWS\assembly (w moim przypadku).

Każdy podzespół powinien posiadać opis, nazwę, informację o prawach autorskich oraz — co bardzo ważne — numer wersji. Na jednej maszynie może istnieć kilka takich samych podzespołów o różnych numerach wersji. Program Global Assembly Cache Tool, kryjący się pod nazwą gacutil.exe, umożliwia rejestrację danego podzespołu jako globalnego, współużytkowanego.

Program gacutil jest wywoływany z poziomu wiersza poleceń. Aby zainstalować dany podzespół, należy wywołać program z opcją /i, tak jak to zrobiłem poniżej:

C:\Program Files\Microsoft.NET\SDK\v1.1\Bin>gacutil /i c:\csharpexample\Assembly.dll
Microsoft (R) .NET Global Assembly Cache Utility.  Version 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
Assembly successfully added to the cache

Przed zainstalowaniem podzespołu jako GAC zaleca się wygenerowanie specjalnego klucza, który stanowi o unikalności jego oraz numeru jego wersji. Do tego celu należy użyć narzędzia Strong Name Tool (sn.exe), które wygeneruje odpowiedni plik będący kluczem. Plik ten z kolei należy dołączyć jako atrybut do programu:

C:\Program Files\Microsoft.NET\SDK\v1.1\Bin>sn -k Assembly.snk

Microsoft (R) .NET Framework Strong Name Utility  Version 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Key pair written to Assembly.snk

Program sn.exe także jest aplikacją wywoływaną z poziomu wiersza poleceń. W celu wygenerowania klucza należy uruchomić ją z parametrem –k oraz podać nazwę pliku, do którego klucz zostanie zapisany (tak jak to przedstawiłem powyżej).

Koncepcja strong names jest podobna do 128-bitowego klucza GUID (ang. Globally Unique Identifier) w COM. GUID zapewnia unikalność klucza. Tymczasem wersja oraz nazwa podzespołu nie gwarantują unikalności tak jak klucz. Dlatego też używamy narzędzia sn.exe. Włączenie klucza do podzespołu może nastąpić za sprawą użycia programu al.exe. Można także włączyć klucz bezpośrednio w kodzie programu, używając atrybutu AssemblyKeyFileAttribute.

Wygenerowany tym sposobem plik można skopiować do katalogu z kodem źródłowym programu, a następnie włączyć go, korzystając z atrybutu AssemblyKeyFileAttribute.

[assembly: AssemblyKeyFileAttribute("Assembly.snk")]

Rysunek 11.7 przedstawia podzespół zainstalowany w przestrzeni GAC.

csharp11.7.jpg
Rysunek 11.7. Podzespół zainstalowany w przestrzeni GAC

Zalecane jest odpowiednie nazewnictwo podzespołów w postaci NazwaFirmy.NazwaPodzespołu, np. Boduch.Foo itd. Łatwiej jest się wtedy zorientować, czyjego autorstwa jest podzespół, oraz wprowadza się pewną zasadę nadawania nazw, która powinna być respektowana przez większość programistów.

WinCV

Nazwa programu jest skrótem od słów Windows Class Viewer (ang. podgląd klas). Jest to przydatny i prosty program służący do odnajdywania i podglądu zawartości klas .NET (rysunek 11.8).

csharp11.8.jpg
Rysunek 11.8. Program WinCV w trakcie działania

Głównym elementem programu jest pole, w którym należy wpisać słowo kluczowe do wyszukania. Na liście po lewej stronie zostaną wyświetlone klasy zawierające to słowo kluczowe.

Po zaznaczeniu danej klasy w głównym oknie zostanie wyświetlona lista znajdujących się w niej właściwości, pól, zdarzeń oraz metod wraz z nazwami oraz typem parametrów.

Narzędzie konfiguracji .NET Framework

Narzędzie konfiguracji .NET Framework kryje się pod nazwą mscorcfg.msc. Jest to program, dzięki któremu można zarządzać GAC poprzez dodawanie lub usuwanie wybranych podzespołów. Istnieje możliwość ustawienia wielu opcji uprawnień aplikacji, wersji, jaka ma być ładowana itp.

PEVerify — narzędzie weryfikacji

Narzędzie PEVerify.exe służy do weryfikacji, czy kod MSIL i metadane spełniają warunki bezpiecznego kodu. Aby upewnić się, że dana aplikacja jest bezpieczna, można wywołać PEVerify.exe z poziomu wiersza poleceń:

<codee>C:\Program Files\Microsoft.NET\SDK\v1.1\Bin>PEVerify C:\P6_1.exe

Microsoft (R) .NET Framework PE Verifier Version 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

All Classes and Methods in C:\P6_1.exe Verified


Jeżeli po wywołaniu aplikacji zostanie wyświetlony komunikat <i>All Classes and Methods in C:\P6_1.exe Verified</i>, można być pewnym, że jest ona bezpieczna i zweryfikowana.

.NET a COM
========

Pokazałem, w jaki sposób poszczególne podzespoły mogą komunikować się ze sobą oraz wykorzystywać klasy z innych podzespołów. Jednak cały ten proces odbywał się w obrębie kodu zarządzanego (<i>managed code</i>). W tym podrozdziale zaprezentuję sposób stosowania kodu niezarządzanego (kontrolki COM) w języku C# na .NET.

Import obiektu COM do aplikacji .NET jest dosyć prosty. Obiekty Win32 COM mogą zostać zaimportowane do .NET za pomocą narzędzia o nazwie <i>Tlbimp.exe</i>, dołączonego do .NET Framework SDK.

Zasady importowania obiektów COM do .NET przedstawię na przykładzie kontrolki SAPI (Microsoft Speech API). Najpierw odszukajmy na dysku plik <i>sapi.dll</i>. Potem z poziomu wiersza poleceń trzeba uruchomić program <i>Tlbimp.exe</i>, który zaimportuje kontrolkę COM do .NET:

`tlbimp "C:\Scieżka do pliku\sapi.dll" /verbose /out:C:\Interop.SAPI.dll`

Takie użycie programu spowoduje utworzenie podzespołu <i>Interop.SAPI.dll</i>, który można w pełni wykorzystać w środowisku .NET. Normalne jest, że podczas konwersji na konsoli jest wyświetlana masa komunikatów ostrzegających o możliwej niekompatybilności owej kontrolki z .NET.

Gdy utworzymy podzespół, trzeba skopiować go do katalogu, w którym następnie należy umieścić nowy projekt WinForms. W oknie <i>Solution</i> należy dodać odwołanie do pliku <i>Interop.SAPI.dll</i>. Interfejs aplikacji składa się z komponentu typu `RichTextBox` oraz Button (rysunek 11.9). Po kliknięciu przycisku ciąg znakowy z pola tekstowego zostanie przekazany do biblioteki, co spowoduje wywołanie lektora, który przeczyta tekst.
 
![csharp11.9.jpg](//static.4programmers.net/uploads/attachment/4ccd36dd43d43.jpg)
Rysunek 11.9. Przykład użycia kontrolki COM w aplikacji .NET

Użycie kodu z biblioteki COM wiąże się z dodaniem odpowiedniej przestrzeni nazw `Interop.SAPI`. Kod źródłowy aplikacji wykorzystującej tę bibliotekę COM wygląda tak:

```csharp
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Interop.SAPI;

namespace SAPI
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
        }

        private void btnTalk_Click(object sender, EventArgs e)
        {
            SpVoice Voice;

            Voice = new SpVoice();
            Voice.Speak(richTextBox1.Text, SpeechVoiceSpeakFlags.SVSFDefault);
        }
    }
}

PInvoke

.NET jest nową platformą programistyczną. Minie jeszcze sporo czasu, zanim programiści przystosują swoje aplikacje do nowej platformy oraz obdarzą ją zaufaniem. Mimo iż .NET udostępnia dziesiątki klas umożliwiających łatwiejsze programowanie, w niektórych przypadkach nadal konieczne może okazać się wykorzystanie funkcji udostępnionych przez WinAPI.

W wielu przypadkach użycie funkcji Win32 stanie się wręcz niezastąpione, tak więc w tej części rozdziału zajmiemy się wykorzystaniem bibliotek Win32 DLL w aplikacjach .NET.

W tym celu będziemy korzystać z mechanizmu zwanego Platform Invocation Service, czyli Platform Invoke, zwanego w skrócie PInvoke (lub P/Invoke). Mechanizm ów pozwala na importowanie funkcji z bibliotek Win32 DLL za pomocą atrybutu [DllImport].

Dla przypomnienia powiem, iż system Windows udostępnia interfejs programistyczny zwany WinAPI (pisałem o tym w rozdziale 2.), który zawiera setki funkcji możliwych do wykorzystania w naszych aplikacjach. Funkcje te zawarte są w tzw. bibliotekach DLL. Biblioteki DLL to pliki z rozszerzeniem .dll, zawierające skompilowany kod. Nie są to jednak aplikacje wykonywalne (takie jak .exe).

Użycie funkcji Win32 API

Aby skorzystać z funkcji Win32 API w kodzie zarządzanym, należy przede wszystkim utworzyć prototyp funkcji w kodzie zarządzanym. Dla przykładu pokażę sposób użycia funkcji GetUserName(), która znajduje się w bibliotece Advapi32.dll. Zwraca ona nazwę użytkownika zalogowanego w systemie.

Aby utworzyć prototyp funkcji, należy wiedzieć, jak wygląda ich budowa w bibliotekach DLL. Windows był pisany w języku C, stąd też przykłady w dokumentacji WinAPI są zapisane w tym języku. Wiele funkcji zwracało rezultaty do parametru referencyjnego — warto tu wspomnieć chociażby o funkcjach GetUserName czy GetComputerName. Ich deklaracja w C wyglądała tak:

BOOL GetComputerName(
    LPTSTR lpBuffer,    // address of name buffer 
    LPDWORD nSize       // address of size of name buffer 
   );
BOOL GetUserName(
    LPTSTR lpBuffer,    // address of name buffer 
    LPDWORD nSize       // address of size of name buffer 
   );

Dla przykładu pokażę, w jaki sposób pobrać nazwę użytkownika (funkcja GetUserName()) oraz ścieżkę do systemu Windows (funkcja GetWindowsDirectory()). Ich prototyp w kodzie zarządzanym wygląda tak:

[DllImport("kernel32.dll")]
static extern bool GetWindowsDirectory(StringBuilder lpBuffer, ref int nSize);
[DllImport("Advapi32.dll")]
static extern bool GetUserName(StringBuilder lpBuffer, ref int nSize);

Obie metody zostały opatrzone słowem kluczowym extern. Informuje ono kompilator, że metody będą ładowane z bibliotek zewnętrznych. Metody zostały opatrzone atrybutem DLLImport, określającym nazwę biblioteki, z której ładowane będą funkcje. Listing 11.6 zawiera program, który wykorzystuje funkcje Win32API.

Listing 11.6. Przykład użycia funkcji Win32 API

using System;
using System.Text;
using System.Runtime.InteropServices;

namespace PInvoke
{
    class Program
    {
        [DllImport("kernel32.dll")]
        static extern bool GetWindowsDirectory(StringBuilder lpBuffer, ref int nSize);
        [DllImport("Advapi32.dll")]
        static extern bool GetUserName(StringBuilder lpBuffer, ref int nSize);

        static void Main(string[] args)
        {
            StringBuilder Buffer = new StringBuilder(64);
            int nSize = 64;

            GetWindowsDirectory(Buffer, ref nSize);

            Console.WriteLine("Ścieżka do katalogu Windowsa: {0}", Buffer.ToString());

            GetUserName(Buffer, ref nSize);

            Console.WriteLine("Nazwa użytkownika: {0}", Buffer.ToString());
        }
    }
}

Interesujące nas dane (ścieżka do systemu Windows oraz nazwa użytkownika) zostaną przypisane do zmiennej Buffer typu StringBuilder.

Aby wykorzystać funkcje Win32 API, należy znać ich budowę (parametry oraz typy). Tego możesz dowiedzieć się z dokumentacji firmy Microsoft, znajdującej się pod adresem http://msdn.microsoft.com.

Użycie atrybutu DLLImport

Przed skorzystaniem z atrybutu DllImport trzeba dołączyć do programu przestrzeń nazw System.Runtime.InteropServices. Użycie tego atrybutu w najprostszym wydaniu prezentuje poprzedni przykład. Jego budowa jest dość prosta, gdyż w takim przypadku należy podać jedynie nazwę biblioteki DLL:

[DllImport("Nazwa biblioteki DLL")];

Taki atrybut nie posiada żadnych dodatkowych parametrów. Normalnie jest możliwe określenie konwencji wywołania parametrów (cdecl, stdcall itp.), nazwy ładowanej procedury bądź funkcji oraz kodowania ciągów znakowych (Unicode, ANSI String). Parametry atrybutu DllImport można określać tak:

[DllImport("SimpleDLL.dll", CallingConvention = CallingConvention.Stdcall, EntryPoint="About")]

W powyższym przykładzie sprecyzowałem sposób wywołania parametrów funkcji (parametr CallingConvention) oraz określiłem dokładnie nazwę importowanej funkcji (EntryPoint).

Istnieje jeszcze jeden parametr używany tylko w przypadku, gdy w parametrze funkcji lub procedury znajduje się ciąg znaków (String). Tym parametrem jest CharSet, który określa kodowanie:

*Ansi — ciąg znakowy ANSI,
*Unicode — ciągi znakowe Unikodu,
*None — oznacza to samo co parametr Ansi.

W przypadku Win32 API wiele funkcji miało dwie odmiany — jedną z parametrem typu PAnsiChar, a drugą z PWideChar. Dla przypomnienia: wszystkie ciągi znakowe w .NET są zapisane w Unicode, także typ String.

Wspomniałem tutaj o konwencji wywołania. Jest to zaawansowane zagadnienie związane z przekazywaniem wartości do parametrów funkcji, a konkretnie ich umieszczaniem w pamięci. Wyjaśnienie tych pojęć wykracza poza ramy niniejszej publikacji.

Zasadniczo technika PInvoke ma o wiele większe zastosowanie. Ja pokazałem jedynie prosty przykład wywołania funkcji Win32 API, lecz ze względu na różnice pomiędzy systemem Win32 a .NET niekiedy użycie funkcji API może okazać się o wiele trudniejsze. Jeżeli jesteś zainteresowany technologią PInvoke, odsyłam do dokumentacji platformy .NET.

Podsumowanie

Możliwość integracji poszczególnych podzespołów to wielka zaleta platformy .NET, dająca ogromne możliwości. Należy uświadomić sobie, że środowisko .NET Framework to nie tylko biblioteka klas czy biblioteka WinForms, ale również CLS, czyli wspólny język programowania. Dzięki temu nieważne jest, w jakim języku piszemy swoje aplikacje, ponieważ wygenerowany kod pośredni zawsze będzie taki sam, niezależnie od języka. Mam nadzieję, że po przeczytaniu niniejszego rozdziału masz pewną świadomość możliwości płynących ze współdzielenia podzespołów.

.. [#] W tej części rozdziału terminem komponent .NET będę określał podzespół (ang. assembly), czyli zwykłą aplikację skompilowaną do kodu IL i posiadającą rozszerzenie .exe lub .dll.

[[C_Sharp/Wprowadzenie|Spis treści]]

[[C_Sharp/Wprowadzenie/Prawa autorskie|©]] Helion 2006. Autor: Adam Boduch. Zabrania się rozpowszechniania tego tekstu bez zgody autora.

0 komentarzy