Rozdział 8. Podzespoły .NET

Adam Boduch

Pojęcie podzespół .NET, zostało wprowadzone w rozdziale 5. Podzespoły są fizycznymi plikami zapisanymi w formacie PE. Podzespoły 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ć Czytelnikowi tematykę modeli COM, które również stanowiły komponenty przenośne w środowisku Win32.

     1 Czym jest COM?
          1.1 Kontrolka w rozumieniu COM
          1.2 Odrobinę historii
          1.3 ActiveX
          1.4 DCOM
     2 Podstawowe podzespoły
          2.5 Słowo o funkcji ShellExecute
          2.6 Deasembler .NET
     3 Obiektowość .NET
          3.7 Transformacja modułu
     4 Komponenty .NET
          4.8 Przygotowanie komponentu w Delphi
          4.9 Przygotowanie komponentu C#
               4.9.1 Kompilacja programu
          4.10 Włączenie podzespołu w Delphi
     5 Atrybuty podzespołu
          5.11 Korzystanie z programu Reflection
     6 Mechanizm reflection
          6.12 Metadane
          6.13 Funkcja GetType
          6.14 Klasa System.Type
          6.15 Ładowanie podzespołu
          6.16 Przykład — własny program reflection
               6.16.2 Odczyt atrybutów z podzespołu
          6.17 Własne atrybuty
               6.17.3 Deklaracja własnego atrybutu
               6.17.4 Odczyt wartości atrybutu
               6.17.5 Użycie atrybutu
     7 Aplikacje .NET Framework SDK
          7.18 Global Assembly Cache Tool
          7.19 WinCV
          7.20 Narzędzie konfiguracji .NET Framework
          7.21 PEVerify — narzędzie weryfikacji
     8 Test
     9 FAQ
     10 Podsumowanie

W tym rozdziale:
*opowiem, czym jest mechanizm reflection,
*pokażę, w jaki sposób wykorzystywać atrybuty podzespołów,
*opiszę, w jaki sposób podzespoły mogą się ze sobą komunikować,
*zdefiniuję pojęcia COM i ActiveX.

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 integrację języka 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 językach Visual C++, C++ Builder czy Visual Basic.

Na razie obiekty COM są obiektami działającymi 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, dostępny na płaszczyźnie projektowania. Tak więc mogliśmy tworzyć kontrolki ActiveX, wykorzystując Delphi oraz jego zalety projektowania wizualnego. Można było korzystać ze wszystkich komponentów i, ogólnie rzecz biorąc, projektowanie jest łatwiejsze niż w przypadku zwykłych obiektów COM.
Dodatkowo ActiveX pozwala na wygenerowanie kodu umożliwiającego umieszczenie aplikacji na stronie WWW.

Więcej na temat tworzenia kontrolek COM/ActiveX oraz wykorzystywania ich w aplikacjach znajdziesz w rozdziale 13. książki Delphi 7. Kompendium programisty, wydanej nakładem wydawnictwa Helion w 2003 roku.

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 komunikowanie się programów między sobą będzie ułatwione — dany program będzie mógł dziedziczyć po klasie z innej aplikacji, obsługiwać wyjątki innej aplikacji 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łą na rzecz platformy .NET.

Podstawowe podzespoły

Programując w modelu Win32, w swoich aplikacjach wykorzystujemy funkcje zawarte w bibliotekach DLL systemu Windows. Przykładowo, często jest stosowana funkcja ShellExecute, która zawarta jest w bibliotece shell32.dll.

Utwórzmy teraz nowy projekt aplikacji VCL Forms Application — Delphi for Win32.

Do listy modułów (uses) należy dodać moduł ShellAPI. Funkcja ShellExecute służy do uruchamiania zewnętrznych programów. Przykładowo, aby uruchomić Notatnik systemu Windows, wystarczy użyć funkcji ShellExecute w następujący sposób:

  ShellExecute(Handle, 'open', 'notepad.exe', '', '', SW_SHOW);

Przykładowy program uruchamiający zewnętrzne aplikacje został zaprezentowany na rysunku 8.1 a jego kod źródłowy — na listingu 8.1.

Listing 8.1. Kod źródłowy programu wykorzystującego moduł ShellAPI

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, StdCtrls;

type
  TMainForm = class(TForm)
    btnRun: TButton;
    edtPath: TEdit;
    Bevel1: TBevel;
    btnNotepad: TButton;
    btnIE: TButton;
    procedure btnRunClick(Sender: TObject);
    procedure btnIEClick(Sender: TObject);
    procedure btnNotepadClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

uses ShellApi;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  edtPath.Text := ExtractFilePath(Application.ExeName);
end;

procedure TMainForm.btnNotepadClick(Sender: TObject);
begin
  ShellExecute(Handle, 'open', 'notepad.exe', '', '', SW_SHOW);
end;

procedure TMainForm.btnIEClick(Sender: TObject);
begin
  ShellExecute(Handle, 'open', 'http://4programmers.net/', '', '', SW_SHOW);
end;

procedure TMainForm.btnRunClick(Sender: TObject);
begin
  ShellExecute(Handle, 'open', PChar(edtPath.Text), '', '', SW_SHOW);
end;

end.

8.1.jpg
Rysunek 8.1. Przykładowy program wykorzystujący moduł ShellAPI

Zapewne Czytelnik zastanawia się, do czego dążę? Otóż funkcja ShellExecute znajduje się w bibliotece shell32.dll, stanowiącej standardowy element każdego systemu Windows. Moduł ShellAPI.pas, który jest włączany w tym programie, w rzeczywistości zawiera kod, który ładuje funkcje (m.in. ShellExecute) z biblioteki shell32.dll.

Podobnie jest w przypadku .NET Framework. Programy skompilowane do kodu pośredniego nie działają na komputerze, na którym nie ma platformy .NET, gdzie nie ma podstawowych podzespołów, wymaganych do prawidłowego działania aplikacji.

Podstawowym podzespołem, który jest wykorzystywany przez każdą aplikację .NET, jest podzespół mscorlib.dll. Zawiera podstawowe typy .NET, klasę bazową Exception i wiele innych ważnych informacji.

W przypadku Delphi dla .NET, podstawowym podzespołem jest Borland.Delphi.dll. Ten podzespół jest wykorzystywany przez każdą aplikację napisaną w Delphi .NET, zawiera m.in. przestrzeń nazw Borland.Delphi.System, która z kolei posiada deklarację podstawowych typów Delphi, o których zdążyliśmy już wspomnieć w niniejszej książce.

Słowo o funkcji ShellExecute

Nim przejdziemy dalej, chciałbym wspomnieć o wykorzystanej w poprzednim przykładzie funkcji ShellExecute. Jako że jest to przydatny element, postanowiłem napisać o niej parę zdań. Lista parametrów tej funkcji znajduje się w tabeli 8.1.

Tabela 8.1. Lista parametrów funkcji ShellExecute

Nazwa parametru Opis
hwnd Uchwyt do okna (obiektu z klasy rodzica).
lpOperation Operacja, który ma być wykonana. Dozwolone wartości to: open (otwarcie pliku, programu), print (drukowanie), explore (wyświetlanie zawartości katalogu).
lpFile Ścieżka do pliku lub katalogu, który ma zostać uruchomiony.
lpParameters Parametry, które zostaną przekazane do programu.
lpDirectory Katalog, w którym znajduje się program.
nShowCmd Parametr określa tryb, w jakim zostanie uruchomiony program.

Jak widać na listingu 8.1, zaprezentowałem wykorzystanie funkcji ShellExecute w różnoraki sposób. Przykładowo, zamiast ścieżki do pliku, podałem adres strony internetowej. W takim przypadku Windows uruchamia domyślną przeglądarkę i wczytuje podany adres WWW.

W drugim przypadku nie podałem pełnej ścieżki programu, wpisując jedynie jego nazwę — notepad.exe. W takiej sytuacji system wyszukuje daną aplikację w domyślnym katalogu systemowym (czyli np. C:\Windows).

Wreszcie, trzecim sposobem było podanie pełnej ścieżki do programu:

ShellExecute(Handle, 'open', PChar(edtPath.Text), '', '', SW_SHOW);

W tym przypadku ścieżka jest pobierana z komponentu edtPath.

Interesujący jest ostatni parametr, który określa tryb otwarcia pliku. W prezentowanych przykładach używałem stałej SW_SHOW, która oznaczała normalne otwarcie aplikacji. Zamiast tego można użyć kilku innych wartości:

*SW_HIDE — uruchomiony program zostanie ukryty,
*SW_MAXIMIZE — uruchomiony program zostanie zmaksymalizowany,
*SW_MINIMIZE — program zostanie zminimalizowany przy starcie.

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.

Deasembler, o którym mowa, znajduje się w katalogu C:\Program Files\Microsoft.NET\SDK\v1.1\Bin pod nazwą Ildasm.exe (rysunek 8.2).

8.2.jpg
Rysunek 8.2. 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 8.2 przedstawia zawartość podzespołu mscorlib.dll.

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

8.3.jpg
Rysunek 8.3. Podgląd kodu IL

Czytelnik pewnie chciałby teraz zapytać, w jaki sposób można zabezpieczyć się przed deasemblacją swojego 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 Delphi, 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.

Obiektowość .NET

Być może to, co za chwilę napiszę, wyda się Czytelnikowi nieco dziwne, lecz w .NET wszystko jest klasą! Platforma .NET jest całkowicie obiektowa, więc nawet jeżeli programista pisze strukturalnie (moduły, procedury i funkcje), kompilator Delphi dla .NET, przekształci wszystko na klasę.

W ten sposób np. wszystkie typy zmiennych, o których wspominałem wcześniej, są klasami. Warto w tym momencie wspomnieć chociażby o typie String, który w rzeczywistości wskazuje na klasę System.String. Tak samo jest z typem Integer (wskazuje na klasę System.Int32) itd. Istnieją oczywiście wyjątki, jak np. typ AnsiString, który jest typem stricte związanym z Delphi, nie jest kompatybilny z CTS.

W języku takim jak np. C# programowanie odbywa się w sposób obiektowy — język ten jest ściśle powiązany z programowaniem obiektowym, a każdy program musi posiadać przynajmniej jedną klasę. Wydawać by się mogło, że w Delphi można programować strukturalnie, lecz to jest tylko pozór. W rzeczywistości kod modułu jest przez kompilator przekształcany do obiektu i w takiej postaci następuje jego transformacja do kodu IL. Warto przyjrzeć się modułowi, który znajduje się na listingu 8.2.

Listing 8.2. Przykładowy kod modułu Delphi

unit Boduch.Test;

interface

  function GetName : String;

implementation

function GetName : String;
begin
  Result := 'Boduch.Test';
end;

end.

Moduł nosi nazwę Boduch.Test, zawiera jedną funkcję, niemającą specjalnego znaczenia. Po użyciu takiego modułu w tworzonej aplikacji i skompilowaniu programu kompilator przekształci kod do następującej postaci:

unit Boduch.Test;

interface

type
  &Unit = class
  public
    class function GetName : String; static; 
  end;

implementation

class function &Unit.GetName : String;
begin
  Result := 'Boduch.Test';
end;

end.

Oczywiście wszystko dzieje się w sposób niewidoczny dla programisty. Przeprowadźmy pewien test. W tym celu należy utworzyć nowy projekt aplikacji konsolowej dla .NET, dodać do programu moduł Boduch.Test.pas, a jego zawartość doprowadzić do takiej postaci, jaka została zaprezentowana na listingu 8.2.

Kolejnym krokiem jest skompilowanie budowanego programu. Jest to prosta aplikacja konsolowa, u mnie na dysku taki program zajmuje 18 kB. Można teraz uruchomić deasembler .NET i przeanalizować ten program. Deasembler wyświetli zawartość opisywanego podzespołu, z podziałem na przestrzenie nazw (rysunek 8.4).

8.4.jpg
Rysunek 8.4. Deasembler .NET

Z tego, co można zaobserwować na rysunku 8.4, w naszym podzespole znajduje się metoda GetName, której zawartość w formie kodu IL prezentuje się tak:

.method public static string  GetName() cil managed
{
  // Code size       8 (0x8)
  .maxstack  1
  .locals init ([0] string Result)
  IL_0000:  ldstr      "Boduch.Test"
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  ret
} // end of method Test::GetName

Transformacja modułu

Jak widać, kompilator Delphi przekształca kod modułu do klasy. Wspomniałem jednak tylko o tych procedurach i funkcjach modułu, które ulegają konwersji do metod. Tak samo dzieje się ze zmiennymi, znajdującymi się w module. Są one przekształcane do pól klasy i umiejscawiane w sekcji public:

unit Boduch.Test;

interface

var
  Variable : Integer;
Powyższy kod zostanie przekształcony do:
type
  &Unit = class
  public
    class Variable : Integer; 
  end;

To samo można powiedzieć o sekcji initialization, która jest opcjonalna w modułach Delphi. Podczas kompilacji programu jest tworzony konstruktor w klasie i następuje umieszczenie kodu z sekcji initialization w konstruktorze:

initialization
  Variable := 10;
Powyższy kod jest umiejscawiany w konstruktorze:
class constructor &Unit.Create; static;
begin
  Variable := 10;
end;

Odkąd moduły mogą posiadać blok begin..end, kod znajdujący się w tej sekcji jest umieszczany w metodzie o nazwie odpowiadającej nazwie modułu:

unit Boduch.Test;

interface

var
  Variable : Integer;

  function GetName : String;

implementation

function GetName : String;
begin
  Result := 'Boduch.Test';
end;

begin
  Variable := 20;
end.

Moduł jest przekształcany do takiej postaci:

unit Boduch.Test;

interface

type
  &Unit = class
  public
    class Variable : Integer; 
    class function GetName : String; static; 
    class procedure Boduch.Test;
  end;

implementation

class function &Unit.GetName : String;
begin
  Result := 'Boduch.Test';
end;

class procedure &Unit.Boduch.Test;
begin
  Variable := 10;
end;

end.

Dociekliwy Czytelnik może także spytać, co się dzieje w sytuacjach, w których zmienne globalne mają nadane wartości już w trakcie pisania programu:

var
  Variable : Integer = 10;

W takim przypadku wartość pola Variable jest nadawana w konstruktorze klasy Unit.

Komponenty .NET

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

W środowisku Win32 jest możliwe 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. Podstawowa składnia języka C# została opisana w dodatku A.

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.

Aby skompilować aplikację do postaci podzespołu z rozszerzeniem .dll, należy słowo kluczowe program w aplikacji zastąpić słowem library.

Na listingu 8.3 znajduje się przykładowy program zawierający klasę Vehicle. Program jest prosty, zawiera kilka procedur, 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 8.3. 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.

Należy zwrócić uwagę na słowo kluczowe library. Dzięki temu w efekcie kompilacji powstanie plik .dll zamiast .exe.

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

Przygotowanie komponentu C#

Teraz napiszemy aplikację w języku C#, która skorzysta z naszego podzespołu. W tym celu będzie potrzebny kompilator języka C#, który jest dołączony do pakietu .NET Framework i powinien znajdować się w katalogu C:\Windows\Microsoft.NET\v1.1.4322. Kompilator ów jest wywoływany z poziomu wiersza poleceń i nosi nazwę csc.exe.

Utwórzmy plik demo.cs, najlepiej w katalogu, w którym znajduje się skompilowany w Delphi podzespół. Zawartość tego pliku powinna wyglądać tak jak na listingu 8.4.

Listing 8.4. Aplikacja napisana w C#

using System;
using Assembly;

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

class MainClass
{
  public static void Main ( )
  {
    try
    {
    Vehicle Car = new Vehicle( );  // utworzenie zmiennej
    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( ));
    }
  }
}

Język C# nie jest tematem tej książki, więc nie będę zagłębiał się w szczegóły. Dyrektywa using Assembly; powoduje włączenie podzespołu o nazwie Assembly pod warunkiem, że poprzedni nasz program został skompilowany pod nazwą Assembly.dll. Następnie w programie jest deklarowana nowa instancja obiektu Vehicle. Następnie program wyświetla instrukcję obsługi programu oraz pobiera polecenie wpisane przez użytkownika, które z kolei zostaje przekazane do klasy z podzespołu Assembly.

Kompilacja programu

Kompilacja programu przebiega z poziomu wiersza poleceń, tak więc za pomocą poleceń znanych z MS-DOS trzeba przejść do katalogu, w którym znajduje się plik csc.exe. Następnie należy wpisać:

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.

Po skompilowaniu obydwóch podzespołów można uruchomić aplikację C# i sprawdzić jej działanie. Łatwo zobaczyć, że bezproblemowo wykorzystuje klasę Vehicle z podzespołu Delphi.

Włączenie podzespołu w Delphi

Włączenie danego podzespołu do naszej aplikacji wiąże się z ustawieniem odpowiedniej dyrektywy kompilatora. Po utworzeniu nowego projektu, czy to VCL.NET, czy WinForms, w pliku *.dpr można znaleźć podobne odwołania:

{%DelphiDotNetAssemblyCompiler '$(SystemRoot)\microsoft.net\framework\v1.1.4322\System.dll'}

Powyższa dyrektywa włącza do programu podzespół System.dll. Wspomniany podzespół zawiera m.in. podstawowe przestrzenie nazw, takie jak System.IO, czy System.Net.

Innym sposobem jest skorzystanie z menedżera projektu. Powinien zawierać gałąź References (ang. odwołania). Po kliknięciu tej gałęzi prawym przyciskiem myszy należy wybrać opcję Add References. Po jej wybraniu otworzy się okno dialogowe, w którym będzie można dokonać wyboru, który z podzespołów ma zostać dołączony do projektu.

Atrybuty podzespołu

W podzespołach jest możliwe zadeklarowanie określonych atrybutów, opisujących dany podzespół. Takie atrybuty mogą być później odczytywane przez inne programy w celu weryfikacji numeru wersji takiego programu oraz nazwy producenta. Łatwo się o tym przekonać, tworząc nowy projekt Windows Forms lub VCL.NET. Wówczas w pliku *.dpr znajdą się następujące instrukcje:

{$REGION 'Program/Assembly Information'}
//
// 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: AssemblyDescription('')]
[assembly: AssemblyConfiguration('')]
[assembly: AssemblyCompany('')]
[assembly: AssemblyProduct('')]
[assembly: AssemblyCopyright('')]
[assembly: AssemblyTrademark('')]
[assembly: AssemblyCulture('')]

// The Delphi compiler controls the AssemblyTitleAttribute via the ExeDescription.
// You can set this in the IDE via the Project Options.
// Manually setting the AssemblyTitle attribute below will override the IDE
// setting.
// [assembly: AssemblyTitle('')]


//
// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:

[assembly: AssemblyVersion('1.0.*')]

//
// In order to sign your assembly you must specify a key to use. Refer to the 
// Microsoft .NET Framework documentation for more information on assembly signing.
//
// Use the attributes below to control which key is used for signing. 
//
// Notes:
//   (*) If no key is specified, the assembly is not signed.
//   (*) KeyName refers to a key that has been installed in the Crypto Service
//       Provider (CSP) on your machine. KeyFile refers to a file which contains
//       a key.
//   (*) If the KeyFile and the KeyName values are both specified, the 
//       following processing occurs:
//       (1) If the KeyName can be found in the CSP, that key is used.
//       (2) If the KeyName does not exist and the KeyFile does exist, the key
//           in the KeyFile is installed into the CSP and used.
//   (*) In order to create a KeyFile, you can use the sn.exe (Strong Name) utility.
//       When specifying the KeyFile, the location of the KeyFile should be
//       relative to the project output directory. For example, if your KeyFile is
//       located in the project directory, you would specify the AssemblyKeyFile
//       attribute as [assembly: AssemblyKeyFile('mykey.snk')], provided your output
//       directory is the project directory (the default).
//   (*) Delay Signing is an advanced option - see the Microsoft .NET Framework
//       documentation for more information on this.
//
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile('')]
[assembly: AssemblyKeyName('')]
{$ENDREGION}

Domyślnie atrybuty podzespołu są puste — wpisany tekst zostanie skompilowany wraz z kodem IL do pliku wykonywalnego.

Aby kompilator wykrył nazwy atrybutów w kodzie źródłowym, trzeba do listy uses dodać moduł System.Reflection. W moim przypadku po poprawkach kod źródłowy programu Assembly wygląda następująco:

library Assembly;

{$APPTYPE CONSOLE}

{ Copyright (c) 2004 by Adam Boduch }

uses
  System.Reflection;

[assembly: AssemblyTitle('Delphi Assembly')]
[assembly: AssemblyDescription('Przykładowy podzespół napisany w Delphi')]
[assembly: AssemblyCopyright('Adam Boduch')]
[assembly: AssemblyVersion('1.0.0.0')]

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.

Teraz z menu Project należy wybrać pozycję Build, co spowoduje ponowne zbudowanie aplikacji wykonywalnej. Od tego momentu inne aplikacje mogą odczytywać informacje na temat naszego podzespołu. Projektant do tego celu może wykorzystać program Reflection.

Korzystanie z programu Reflection

Program Reflection jest standardowym składnikiem Delphi. Aby go otworzyć, należy skorzystać z menu Tools => Reflection. Okno robocze programu Reflection zostało przedstawione na rysunku 8.5.

8.5.jpg
Rysunek 8.5. Program Reflection

Jest to bardzo prosty program służący do wyświetlania zawartości podzespołu — m.in. jego atrybutów.

Za pomocą przycisku Open można załadować dowolny podzespół w celu wykonania jego analizy. W oknie z lewej strony zostanie wyświetlona w formie drzewa lista modułów, klas oraz metod w podzespole.

Po prawej stronie na zakładkach zostaną wyświetlone kolejno: właściwości, atrybuty, flagi oraz moduły zaznaczonej pozycji. Po zaznaczeniu głównej pozycji drzewa (Assembly), na zakładce Properties oraz Attributes można przeglądać właściwości atrybutów podzespołu, które wcześniej określono w kodzie źródłowym programu.

Warto zwrócić uwagę, że po wyświetleniu zawartości danej klasy, na liście metod oprócz metod zadeklarowanych przez projektanta, są też te, które automatycznie podlegają procesowi dziedziczenia po klasie TObject (czyli inaczej po klasie System.Object).

Mechanizm reflection

Program Reflection 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 reflection [#]_ w naszej aplikacji dzięki przestrzeni nazw System.Reflection, jaka jest udostępniana przez pakiet .NET Framework.

Pierwowzorem mechanizmu reflection w Delphi był RTTI (Run Time Type Information), który umożliwiał uzyskiwanie dostępu do informacji na temat właściwości i zdarzeń poszczególnych komponentów — wszystko podczas działania skompilowanej aplikacji. Dobrym przykładem wykorzystania RTTI w Delphi jest jego IDE — a dokładniej inspektor obiektów. Umożliwia on odczyt wartości zdarzeń i właściwości komponentów.

W .NET RTTI został zastąpiony mechanizmem reflection, który pozwala na analizę podzespołu, odczyt właściwości, metod klas oraz atrybutów.

Metadane

Mechanizm reflection 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ść modułu Borland.Delphi.System.

Pytanie brzmi: w jaki sposób mechanizm reflection umożliwia odczytywanie tych wszystkich informacji? Odpowiedź jest prosta: w podzespole są zawarte metadane, czyli dane opisujące elementy aplikacji (funkcje, klasy, moduły) skompilowane obok kodu IL w pliku wynikowym.

Funkcja GetType

Każdy obiekt .NET posiada funkcję GetType, która jest dziedziczona po klasie głównej — System.Object. Owa funkcja zwraca rezultat w postaci klasy System.Type. Dzięki niej można odczytywać takie informacje jak nazwę podzespołu, jego wersję, a także przestrzeń adresową, w jakiej się znajduje. Tym zajmiemy się nieco później — teraz pozostaniemy jedynie przy odczytaniu nazwy danego typu:

program Demo;

{$APPTYPE CONSOLE}

var
  I : Integer;
  D : Double;

begin
  Console.WriteLine('Zmienna I jest typu: ' + I.GetType.ToString);
  Console.WriteLine('Zmienna D jest typu: ' + D.GetType.ToString);
  Console.ReadLine;
end.

Typy Integer czy Double w .NET także są obiektami, zatem posiadają funkcję GetType, która zwraca informacje o obiekcie. W powyższym przykładzie użyliśmy funkcji 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, funkcja GetType zwraca informacje w postaci obiektu System.Type, z którego można wyczytać więcej informacji na temat konkretnego obiektu .NET.

W większości przypadków Delphi działa w sposób na tyle inteligentny, że używanie pełnej nazwy typu nie jest konieczne. Przykładowo, deklarując zmienną typu ciąg znakowy, wystarczy napisać String, a nie System.String. W przypadku klasy System.Type należy używać pełnej nazwy, gdyż samo słowo Type jest słowem kluczowym języka Delphi — kompilator nie jest w stanie rozróżnić, czy próbujemy zadeklarować nowy typ czy tylko skorzystać z klasy System.Type.

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 8.5.

Listing 8.5. Użycie klasy System.Type

procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  NetType : System.Type;
begin
{ pobranie typu obiektu - w tym przypadku przycisku }
  NetType := Sender.GetType;

  lbInfo.Items.Add(System.Object('Nazwa klasy: ' + NetType.FullName));
  lbInfo.Items.Add(System.Object('Przestrzeń nazw: ' + NetType.Namespace));
  lbInfo.Items.Add(System.Object('Nazwa podzespołu: ' + NetType.Assembly.GetName.Name));
  lbInfo.Items.Add(System.Object('Wersja podzespołu: ' + NetType.Assembly.GetName.Version.ToString));
  lbInfo.Items.Add(System.Object('Nazwa pliku: ' + NetType.Module.Name));
end;

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 @@NetType@@ jest przypisywany rezultat działania funkcji GetType. Od tego momentu zmienna @@NetType@@ zawiera informację na temat przycisku. Następnie informacje te są umieszczane w komponencie typu ListBox.

Interesujący w tym przykładzie może być sposób dodawania kolejnej linii do komponentu ListBox. Dzieje się to w trakcie działania programu. Za operowanie na konkretnych pozycjach komponentu odpowiada właściwość @@Items@@. Funkcja Add natomiast dodaje nową pozycję do menu. W celu dodania tekstu musimy po raz kolejny posłużyć się mechanizmem boksowania typów.

Nie trzeba dokładnie znać właściwości poszczególnych klas, w tym przypadku klasy System.Type. Zawsze można posłużyć się dokumentacją .NET SDK lub mechanizmem Code Completion.

Ładowanie podzespołu

Program przedstawiony w poprzednim przykładzie pobierał informacje o komponencie typu Button. Jest to dość proste, lecz niezbyt przydatne. Program Reflection 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):

var
  AFile : System.Reflection.Assembly;
begin
  AFile := System.Reflection.Assembly.LoadFrom('assembly.exe');
end;

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

Przykład — własny program reflection

Na rysunku 8.6 został przedstawiony przykładowy program korzystający z mechanizmu reflection do odczytu wybranego podzespołu. Program ten jest bliźniaczo podobny do oryginalnej aplikacji Reflection, dołączanej do Delphi, lecz w mojej opinii stanowi dobry przykład odczytu oraz analizy podzespołu.

8.6.jpg
Rysunek 8.6. Przykładowy program w trakcie działania

Na rysunku 8.6 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ę na komponenty TreeView (komponent nazwałem tvAssembly), Button oraz niewidoczny komponent 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.

Cały kod źródłowy został przedstawiony na listingu 8.6. Najpierw warto mu się przyjrzeć, a ja później opiszę jego budowę.

Listing 8.6. Kod źródłowy naszego programu Reflection

unit WinForm2;

interface

uses
  System.Drawing, System.Collections, System.ComponentModel,
  System.Windows.Forms, System.Data, System.Reflection;

type
  TWinForm2 = class(System.Windows.Forms.Form)
  {$REGION 'Designer Managed Code'}
  strict private
    /// <summary>
    /// Required designer variable.
    /// </summary>
    Components: System.ComponentModel.Container;
    btnLoad: System.Windows.Forms.Button;
    OpenFileDialog: System.Windows.Forms.OpenFileDialog;
    tvAssembly: System.Windows.Forms.TreeView;
    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    procedure InitializeComponent;
    procedure Button1_Click(sender: System.Object; e: System.EventArgs);
  {$ENDREGION}
  strict protected
    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    procedure Dispose(Disposing: Boolean); override;
  private
    { Private Declarations }
  public
    constructor Create;
  end;

  [assembly: RuntimeRequiredAttribute(TypeOf(TWinForm2))]

implementation

{$REGION 'Windows Form Designer generated code'}
/// <summary>
/// Required method for Designer support -- do not modify
/// the contents of this method with the code editor.
/// </summary>
procedure TWinForm2.InitializeComponent;
begin
  Self.btnLoad := System.Windows.Forms.Button.Create;
  Self.OpenFileDialog := System.Windows.Forms.OpenFileDialog.Create;
  Self.tvAssembly := System.Windows.Forms.TreeView.Create;
  Self.SuspendLayout;
  // 
  // btnLoad
  // 
  Self.btnLoad.Location := System.Drawing.Point.Create(8, 352);
  Self.btnLoad.Name := 'btnLoad';
  Self.btnLoad.Size := System.Drawing.Size.Create(238, 23);
  Self.btnLoad.TabIndex := 1;
  Self.btnLoad.Text := 'Ładuj podzespół';
  Include(Self.btnLoad.Click, Self.Button1_Click);
  // 
  // tvAssembly
  // 
  Self.tvAssembly.ImageIndex := -1;
  Self.tvAssembly.Location := System.Drawing.Point.Create(9, 16);
  Self.tvAssembly.Name := 'tvAssembly';
  Self.tvAssembly.SelectedImageIndex := -1;
  Self.tvAssembly.Size := System.Drawing.Size.Create(495, 328);
  Self.tvAssembly.TabIndex := 2;
  // 
  // TWinForm2
  // 
  Self.AutoScaleBaseSize := System.Drawing.Size.Create(5, 13);
  Self.ClientSize := System.Drawing.Size.Create(512, 381);
  Self.Controls.Add(Self.tvAssembly);
  Self.Controls.Add(Self.btnLoad);
  Self.Name := 'TWinForm2';
  Self.Text := 'Reflection';
  Self.ResumeLayout(False);
end;
{$ENDREGION}

procedure TWinForm2.Dispose(Disposing: Boolean);
begin
  if Disposing then
  begin
    if Components <> nil then
      Components.Dispose();
  end;
  inherited Dispose(Disposing);
end;

constructor TWinForm2.Create;
begin
  inherited Create;
  //
  // Required for Windows Form Designer support
  //
  InitializeComponent;
  //
  // TODO: Add any constructor code after InitializeComponent call
  //
end;

procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  NetTypes : array of System.Type;
  AFile : System.Reflection.Assembly;
  I, subI : Integer;
  ClassTree, PropTree, MethodTree, FieldTree, EventTree : TreeNode;
begin
{ usunięcie wszystkich gałęzi (jeżeli są) }
  tvAssembly.Nodes.Clear;

{ wyświetlenie okna dialogowego }
  OpenFileDialog.ShowDialog;
  try
  { pobranie nazwy wybranego podzespołu oraz załadowanie go }
    AFile := System.Reflection.Assembly.LoadFrom(OpenFileDialog.FileName);
    Text := 'Reflection [' + OpenFileDialog.FileName + ']';
  except
    raise Exception.Create('Bład! Nie można załadować podzespołu!');
  end;

 { załadowanie informacji o klasach znajdujących się
   w podzespole do tablicy NetTypes } 
  NetTypes := AFile.GetTypes;

  for I := 0 to High(NetTypes) do
  begin
  { utworzenie nowego węzła o nazwie klasy z podzespołu }
    ClassTree := tvAssembly.Nodes.Add(NetTypes[i].FullName);

  { utworzenie nowej gałęzi o nazwie 'Właściwości' }
    PropTree := ClassTree.Nodes.Add('Właściwości');
    for subI := 0 to System.Array(NetTypes[i].GetProperties).Length -1 do
    begin
      PropTree.Nodes.Add(System.Array(NetTypes[i].GetProperties).GetValue(subI).ToString);
    end;

  { utworzenie nowej gałęzi o nazwie 'Metody' }
    MethodTree := ClassTree.Nodes.Add('Metody');
    for subI := 0 to System.Array(NetTypes[i].GetMethods).Length -1 do
    begin
      MethodTree.Nodes.Add(System.Array(NetTypes[i].GetMethods).GetValue(subI).ToString);
    end;

  { utworzenie nowej gałęzi o nazwie 'Pola' }
    FieldTree := ClassTree.Nodes.Add('Pola');
    for subI := 0 to System.Array(NetTypes[i].GetFields).Length -1 do
    begin
      FieldTree.Nodes.Add(System.Array(NetTypes[i].GetFields).GetValue(subI).ToString);
    end;

  { utworzenie nowej gałęzi o nazwie 'Zdarzenia' }
    EventTree := ClassTree.Nodes.Add('Zdarzenia');
    for subI := 0 to System.Array(NetTypes[i].GetEvents).Length -1 do
    begin
      MethodTree.Nodes.Add(System.Array(NetTypes[i].GetEvents).GetValue(subI).ToString);
    end;
  end;

 { zwolnienie klasy }
  AFile.Free;

end;

end.

Po załadowaniu wybranego przez użytkownika podzespołu następuje odczytanie typów znajdujących się w tym podzespole (funkcja GetTypes) do tablicy NetTypes. 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(NetTypes[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 funkcji do tego służących):

PropTree := ClassTree.Nodes.Add('Właściwości');
for subI := 0 to System.Array(NetTypes[i].GetProperties).Length -1 do
begin
      PropTree.Nodes.Add(System.Array(NetTypes[i].GetProperties).GetValue(subI).ToString);
end;

Na samym początku jest tworzone 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.

Niezrozumiały dla Czytelnika może być zapis System.Array(NetTypes[i].GetProperties), który wygląda jak rzutowanie — i tak też jest w istocie. Funkcja GetProperties zwraca tablicę zawierającą listę właściwości, lecz nas w tym momencie interesuje liczba tych właściwości. Ową liczbę podaje właściwość @@Length@@ z klasy System.Array. Znając liczbę właściwości, wiemy, ile iteracji pętli należy wykonać. Każda iteracja pętli powoduje dodanie nowej gałęzi, która zawiera nazwę właściwości — ją również wydobywamy, stosując z pozoru dziwną aczkolwiek efektywną konstrukcję:

System.Array(NetTypes[i].GetProperties).GetValue(subI).ToString;

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 opis podzespołu, 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. Kod realizujący to zadanie znajduje się na listingu 8.7.

Listing 8.7. Odczyt atrybutów z danego podzespołu

procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  AFile : Assembly;
  Attrs : array of System.Object;
  I : Integer;
begin
{ wyświetlenie okna dialogowego }
  OpenFileDialog.ShowDialog;

  try
    AFile := Assembly.LoadFrom(OpenFileDialog.FileName);
    Text := 'Odczyt atrybutów podzespołu [' + OpenFileDialog.FileName + ']';
  except
  { wyświetlenie wyjątku w razie niepowodzenia }
    raise Exception.Create('Bład! Nie można załadować podzespołu!');
  end;

{ odczyt atrybutów i zapisanie ich do tablicy }
  Attrs := AFile.GetCustomAttributes(True);
  for I := 0 to High(Attrs) do
  begin
  { 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 then
      edtTitle.Text := (Attrs[I] as AssemblyTitleAttribute).Title;
    if Attrs[I] is AssemblyDescriptionAttribute then
      edtDescription.Text := (Attrs[I] as AssemblyDescriptionAttribute).Description;
    if Attrs[I] is AssemblyCopyrightAttribute  then
      edtCopyright.Text := (Attrs[I] as AssemblyCopyrightAttribute).Copyright;
    if Attrs[I] is ComCompatibleVersionAttribute then
    { wyświetlenie jedynie dwóch cyfr z numeru wersji }
      edtVersion.Text := Convert.ToString(
        (Attrs[I] as ComCompatibleVersionAttribute).MajorVersion) + '.' +
        Convert.ToString((Attrs[I] as ComCompatibleVersionAttribute).MinorVersion);

  end;

{ zwolnienie klasy }
  AFile.Free;
  
end;

Przed próbą kompilacji programu nie należy zapomnieć o dodaniu do listy uses modułów: System.Reflection, 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 then
  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 każda aplikacja Czytelnika będzie zawierała odpowiedni atrybut z odnośnikiem do pliku XML, to wszystkie takie programy 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:

  [TBugFixAttribute('Adam Boduch', '19-03-2004', 'Komentarz')]
  [TBugFixAttribute('Jacek Placek', '12-03-2004', 'Dodałem metodę')]
  TMyClass = class
    procedure SomeProcc;
  end;

Własny atrybut TBugFixAttribute opisuje dane dotyczące błędu — nazwisko programisty, który naprawił błąd, 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:

  TBugFixAttribute = class(TCustomAttribute)
  private
    FProgrammer : String;
    FFixDate : String;
    FComments : String;
  public
    constructor Create(Programmer : String; FixDate : String; Comments : String);
    property Programmer : String read FProgrammer write FProgrammer;
    property FixDate : String read FFixDate write FFixDate;
    property Comments : String read FComments write FComments;
  end;

Ważne jest, aby klasa dziedziczyła po klasie TCustomAttrbibute. Wówczas nasz obiekt jest rozpoznawalny jako atrybut. Jak widać, przedstawiona tutaj klasa TBugFixAttribute posiada pojedynczy konstruktor oraz trzy właściwości: Programmer (dane programisty, który poprawił kod), FixDate (data poprawki), Comments (komentarz). Na podstawie tych właściwości aplikacja będzie mogła odczytać wartości atrybutu.

Konstruktor odgrywa tutaj znaczącą rolę. Dzięki niemu deklaracja atrybutu wygląda następująco:

[TBugFixAttribute('Adam Boduch', '19-03-2004', 'Komentarz')]

Bez konstruktora każdemu parametrowi atrybutu należałoby przypisać nazwę, np.:

[TBugFixAttribute(Programmer = 'Adam Boduch', FixDate = '19-03-2004', Comments = 'Komentarz')]

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

constructor TBugFixAttribute.Create(Programmer: String; FixDate: String;
  Comments: String);
begin
  inherited Create;
  FProgrammer := Programmer;
  FFixDate := FixDate;
  FComments := Comments;
end;

Jak widać, tak czy inaczej dane i tak lądują we właściwościach naszej klasy.

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:

Attrs := TypeOf(TMyClass).GetCustomAttributes(TypeOf(TBugFixAttribute), true);

Funkcja TypeOf jest tutaj ważna, gdyż zwraca typ danych (w tym przypadku klasy) w postaci zmiennej System.Type. Po uzyskaniu typu, można wywołać metodę GetCustomAttributes pobierającą atrybuty programu. Drugi parametr funkcji, GetcustomAttributes, informuje o tym, że atrybuty mają być jedynie typu TBugFixAttributes. Mając tablicę obiektów (Attrs), odczyt atrybutów jest taki sam jak w poprzednio prezentowanym przykładzie. Listing 8.8 zawiera pełny kod programu, który korzysta z atrybutu TCustomAttributes.

Listing 8.8. Kod źródłowy programu korzystającego z własnych atrybutów

program AttributesDemo;

{$APPTYPE CONSOLE}

uses
  System.Reflection;

type
[AttributeUsage(AttributeTargets.Class or
       AttributeTargets.Constructor or
       AttributeTargets.Field or
       AttributeTargets.Method or
       AttributeTargets.Property,
       AllowMultiple = true)]

  TBugFixAttribute = class(TCustomAttribute)
  private
    FProgrammer : String;
    FFixDate : String;
    FComments : String;
  public
    constructor Create(Programmer : String; FixDate : String; Comments : String);
    property Programmer : String read FProgrammer write FProgrammer;
    property FixDate : String read FFixDate write FFixDate;
    property Comments : String read FComments write FComments;
  end;

  [TBugFixAttribute('Adam Boduch', '19-03-2004', 'Komentarz')]
  [TBugFixAttribute('Jacek Placek', '12-03-2004', 'Dodałem metodę')]
  TMyClass = class
    procedure SomeProcc;
  end;

{ TBugFixAttribute }

constructor TBugFixAttribute.Create(Programmer: String; FixDate: String;
  Comments: String);
begin
  inherited Create;
  FProgrammer := Programmer;
  FFixDate := FixDate;
  FComments := Comments;
end;

{ TMyClass }

procedure TMyClass.SomeProcc;
begin
  { kod }
end;

var
  Attrs : array of System.Object;
  I : Integer;
{ TBugFixAttribute }


begin
{ dzięki zastosowaniu dodatkowego parametru zostaną pobrane jedynie
  atrybuty typu TBugFix }
  Attrs := TypeOf(TMyClass).GetCustomAttributes(TypeOf(TBugFixAttribute), true);

  Console.WriteLine('Błędy klasy TMyClass:');

  for I := 0 to System.Array(Attrs).Length -1 do
  begin
    Console.WriteLine('----');
    Console.WriteLine('Programista: ' + (Attrs[i] as TBugFixAttribute).Programmer);
    Console.WriteLine('Data poprawki: ' + (Attrs[i] as TBugFixAttribute).FixDate);
    Console.WriteLine('Komentarz: ' + (Attrs[i] as TBugFixAttribute).Comments);
  end;

  Console.ReadLine;
end.

Po uruchomieniu programu zawartość konsoli będzie zbliżona do tej przedstawionej poniżej:

Błędy klasy TMyClass:
----
Programista: Adam Boduch
Data poprawki: 19-03-2004
Komentarz: Komentarz
----
Programista: Jacek Placek
Data poprawki: 12-03-2004
Komentarz: Dodałem metodę

Użycie atrybutu

Na listingu 8.8 można dostrzec, iż skorzystałem z jednego atrybutu, o którym wcześniej nie mówiłem — AttributeUsage. Atrybut ów jest używany przed klasą, która deklaruje własny atrybut (tak jak w naszym przypadku — TBugFixAttribute) i określa użycie przyszłego atrybutu. Budowa taka jak na listingu 8.8 wskazuje, iż atrybut będzie mógł być użyty zarówno w połączeniu z klasami, metodami, jak i konstruktorami:

[AttributeUsage(AttributeTargets.Class or
       AttributeTargets.Constructor or
       AttributeTargets.Field or
       AttributeTargets.Method or
       AttributeTargets.Property,
       AllowMultiple = true)]

Warto zwrócić uwagę na operator or, który łączy opcje użycia atrybutu.

Aplikacje .NET Framework SDK

Jak dotąd mówiłem jedynie o programie Reflection, który umożliwia analizę zawartości podzespołu. Dzięki niemu można było poznać zawartość danej aplikacji i zorientować się w kwestiach budowy klas, znajdujących się w danym programie. Warto jednak zwrócić uwagę na inne aplikacje przydatne programiście, a będące częścią pakietu .NET Framework SDK.

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

Global Assembly Cache Tool

W przypadku obiektów COM przed ich użyciem należało je zarejestrować, 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 nasza, jedna aplikacja mogła korzystać z owego podzespołu, a przy tym należało uważać na ścieżki (katalogi), w których ów program się znajduje. Przykładowo, w opisywanym przykładzie podzespół, z którego korzystamy, zawsze będzie musiał znajdować się w tym samym katalogu co program, który z niego korzysta.

.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 podzespołu wraz z jego numerem 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, natomiast 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 8.7 przedstawia podzespół zainstalowany w przestrzeni GAC.

8.7.jpg
Rysunek 8.7. Podzespół zainstalowany w przestrzeni GAC

Zalecane jest odpowiednie nazewnictwo podzespołów w postaci NazwaFirmy.NazwaPodzespołu, np. Borland.VCL itd. Łatwiej wtedy się 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 8.8).

8.8.jpg
Rysunek 8.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 właściwości, pól, zdarzeń oraz metod znajdujących się w danej klasie — 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, dodając lub usuwając wybrane podzespoły. Istnieje możliwość ustawienia wielu opcji uprawnień aplikacji, wersji, jaka ma być ładowana itp.

PEVerify — narzędzie weryfikacji

Narzędzi 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ń:

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 All Classes and Methods in C:\P6_1.exe Verified, można być pewnym, że dana aplikacja jest bezpieczna i zweryfikowana.

Test

  1. Podstawowym podzespołem .NET Framework, wykorzystywanym w każdym programie jest:
    a) mscorlib.dll,
    b) shellapi.dll,
    c) Borland.Delphi.dll.
  2. Dyrektywa włączająca podzespół do projektu Delphi to:
    a) DelphiNetAssemblyCompiler,
    b) DelphiDotNetAssemblyCompiler,
    c) DelphiIncludeAssembly.
  3. GAC to skrót od:
    a) Global Assembly Cache,
    b) Graphical Assembly Cache,
    c) Nie ma takiego pojęcia.
  4. Funkcja TypeOf zwraca dane egzemplarz typu:
    a) System.Object,
    b) System.Type,
    c) System.String.

FAQ

Dlaczego taka sama aplikacja skompilowana w C# zajmuje mniej miejsca niż w Delphi?

Spostrzegawczy Czytelnik mógł zauważyć, że aplikacja niemal identyczna (korzystająca z tych samych klas, modułów oraz działająca identycznie) skompilowana w C# zajmuje mniej miejsca niż aplikacja skompilowana w Delphi. Używając narzędzia Reflection, łatwo zauważyć, że w przypadku programów napisanych w Delphi kompilator umieszcza w pliku wynikowym dodatkowy kod (moduł Borland.Delphi.System), który jest niezależny od CLR i od .NET. Kod ów jest niezbędny do zachowania kompatybilności oraz prawidłowego działania kompilatora i nie można nakazać kompilatorowi jego niewłączania do pliku wynikowego.

Jakie są zalety dzielenia aplikacji na pojedyncze podzespoły?

Dana aplikacja może składać się z poszczególnych podzespołów, których autorami są osobni podwykonawcy. Wszystkie podzespoły w prosty sposób mogą się ze sobą komunikować. Mamy więc możliwość podziału funkcjonalności aplikacji.

Dzieląc swoją aplikację na mniejsze podzespoły, umożliwiamy użytkownikom pobieranie uaktualnień tylko tych modułów, w których znajdują się błędy, lub w których pojawiły się nowe funkcje.

Podsumowanie

Podczas lektury tego rozdziału Czytelnik zyskał kolejną porcję wiedzy na temat programowania na platformie .NET. Najważniejszą rzecz, którą należy sobie przyswoić, jest umiejętność wykorzystywania innych podzespołów w swoich aplikacjach.

Mam nadzieję, że również uświadomiłem Czytelnikowi w jaki sposób i w jakiej formie jest zapisywany kod pośredni do podzespołu. Teraz już wiadomo, iż całe programowanie w .NET opiera się na użyciu klas.

.. [#] 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.
.. [#] Nie znalazłem polskiego odpowiednika słowa Reflection, które wyrażałoby działanie tego mechanizmu. W języku angielskim słowo to oznacza odbicie, odzwierciedlenie. Ja będę posługiwał się nazwą angielską.

[[Delphi/Vademecum|Spis treści]]

[[Delphi/Vademecum/Prawa autorskie|©]] Helion 2005. Autor: Adam Boduch. Zabrania się rozpowszechniania tego tekstu bez zgody autora.

0 komentarzy