BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











Wsparcie trybu projektowania (ang. design-time) na platformie .NET – Teoria

17-01-2004 00:00 | User 79209
Czyli trochę teorii przyszłego klikania.

Szybkość wytwarzania oprogramowania ma coraz większe znaczenie i o ile nad konsekwencjami tego stanu rzeczy można dyskutować, to nad nieuchronnością tego procesu już nie. Pochodną tego faktu jest rozwój wszelakich technologii i związanych z nimi narzędzi, które starają się wyręczyć programistę tam, gdzie tylko jest to możliwe. Jednym z takich rozwiązań jest wizualne programowanie (ang. visual programming), w którym dużą część aplikacji można stworzyć za pomocą najzwyklejszej myszki. I właśnie wsparcie platformy .NET dla tego trybu będzie głównym tematem artykułu.

Całość powyżej przedstawionego zagadnienia określa się w języku angielskim mianem design-time, design lub design mode. Wszystkie te wyrażenia są najczęściej tłumaczone na język polski jako tryb projektowania lub czas projektowania. W artykule będę używał odpowiedników angielskich.

Podstawowe składniki design-time platformy .NET

Można wyróżnić pięć głównych elementów design-time zawartych w platformie .NET.

Atrybuty (ang. attributes)

Atrybuty są to tak zwane metadane, które dostarczają dodatkowych informacji na temat kodu, którego dotyczą. Przykładowo BrowsableAttribute określa, czy dana właściwość lub zdarzenie udekorowane tym atrybutem, ma być widoczne w okienku Properties. Poniższy kod przedstawia użycie atrybutu, które spowoduje, że właściwość ZeroProperty nie będzie widoczna w okienku Properties. Użyty atrybut ma zastosowanie tylko w design-time, aczkolwiek istnieją atrybuty mające zastosowanie także w run-time. Przykładem takiego atrybutu może być DllImportAttribute, który oznacza, że udekorowana nim metoda pochodzi z kodu niezarządzanego.

[Browsable(flase)]
public int ZeroProperty
{
get
{
           return 0;
        }
}

Designery (ang. designers):

Designer jest obiektem określającym zachowanie i wygląd komponentów w design-time i tylko w design-time. Może dodawać, usuwać komponenty, może zmieniać ich właściwości, może także obsługiwać zdarzenia pochodzące z interakcji z użytkownikiem. W design-time zdarzenia typu MouseEnter, MouseHover, Click i wiele innych są obsługiwane tylko przez designera. Warto przy tym wspomnieć, że na przykład zdarzenie Resize jest obsługiwane tylko przez komponent, a zdarzenie Paint przez oba elementy. Ale jest to raczej wyjątek od reguły. Oprócz zdarzeń, designer może korzystać z wielu usług (ang. services), których interfejsy są częścią frameworka. Napisałem interfejsy, ponieważ sama ich implementacja należy już do środowiska udostępniającego design-mode. Takim środowiskiem jest na przykład Visual Studio .NET. Ogólnie rzecz ujmując designer może być postrzegany jako pośrednik łączący komponent(y) ze środowiskiem implementującym funkcjonalność design-mode. Dla wygody, aczkolwiek nie do końca poprawnie, środowisko to będę dalej określał mianem IDE (ang. Integrated Development Environment).

Konwertery typów (ang. type converters):

Konwerter typu jest odpowiedzialny za konwersję danej wartości z jednego do drugiego typu.

Większość typów podstawowych jak String, Int32, wyliczenia i inne mają zdefiniowane standardowe konwertery, które zapewniają konwersję text-to-value łącznie ze sprawdzeniem poprawności takiej operacji. Przykładowo, w okienku Properties użytkownik wpisuje łańcuch znakowy, który potem jest zmieniany przy pomocy konwertera na odpowiedni obiekt lub wartość. Jeżeli konwersja się nie powiedzie, to wyskakuje małe okienko z ogólnym opisem błędu oraz przyciskiem Details, po naciśnięciu którego ukaże nam się dokładniejsza informacja o błędzie. Przykład takiego okienka zamieszczony jest na rysunku 1. Konwerter typu we współpracy z designer hostem (będzie o tym dalej) potrafi na przykład wygenerować kod konfigurujący określoną właściwość. Konfiguracja ta może być zapisana w zasobach (ang. resources, pliki *.resx, *.resources) lub w kodzie, w metodzie InitializeComponent().

Wizualne edytory typu (ang. UI type editors):

Wizualny edytor typu dostarcza interfejsu, dzięki któremu użytkownik może edytować wartość danego typu. .NET framework dostarcza takie edytory dla wielu typów, między innymi dla Color, Font i innych. Aby je zobaczyć wystarczy w oknie Properties kliknąć na właściwość mającą jeden z wymienionych typów. Edytor jest związany z daną właściwością poprzez zastosowanie atrybutu EditorAttribute. Edytor typu może przybrać dwie formy, pierwszą z nich jest formatka typu System.Windows.Forms.Form - np. dla Font, a drugą kontrolka typu drop-down – np. dla Color. Przykłady takich edytorów dla dwóch wspomnianych typów danych pokazałem na rysunku 1.

 

Rysunek 1.  Od lewej: błąd w wyniku konwersji, edytor typu drop-down, edytor typu formatka

Usługi trybu design-time (ang. design-time services):

Platforma .NET dostarcza wielu usług design-time. Dokładnie rzecz ujmując dostarcza samych interfejsów, co zostało wyjaśnione w przy omawianiu designerów. Przykładem takiej usługi może być ISelectionService, która udostępnia dwa zdarzenia SelectionChanged i SelectionChanging, dzięki którym designer może być na bieżąco powiadamiany o tym, który komponent został wybrany przez użytkownika. Dostępne usługi dają ogromne możliwości, a ich lista jest całkiem spora. W tabeli 1 zebrałem tylko niewielką ich część. Oczywiście, nie każde środowisko programistyczne musi implementować wszystkie usługi, aczkolwiek nie wiele jest takich, które można całkowicie pominąć.

 

Typ usługi

Opis usługi

IDesignerHost

Interfejs do tak zwanego designer hosta. Dostarcza metod do dodawania, tworzenia, usuwania, osadzania (ISite) komponentów oraz do wykonywania wielu operacji design-time naraz.

IComponentChangeService

Interfejs dostarczający zdarzeń, które występują, gdy komponent jest dodawany, usuwany, modyfikowany lub zmieniana jest jego nazwa.

ISelectionService

Interfejs pozwala na sprawdzenie, które komponenty są aktualnie wybrane w oknie IDE. Umożliwia także zmianę tego stanu, czyli wybranie innych komponentów.

IToolboxService

Interfejs pozwala sprawdzić i zmodyfikować stan elementów znajdujących się na Toolboxie.

IUndoService

Interfejs obsługuje mechanizm undo/redo.

IMenuCommandService

Interfejs obsługuje menu okna IDE. Zalicza się do tego między innymi menu główne, menu kontekstowe oraz linki na dole okna Properties.

IReferenceService

Interfejs zapewnia mapowanie nazwy komponentu na jego referencje. Przykładowo dla ciągu ”myControl” otrzymamy referencje do zmiennej MyControl myControl

IServiceContainer

Interfejs pozwala dodawać i usuwać usługi, które będą mogły być używane przez inne designery.

Tabela 1. Usługi design-time

 

Teoria designerów

Przy okazji bardziej szczegółowego opisu designerów warto zatrzymać się na chwilę i wyjaśnić różnicę pomiędzy komponentem, który implementuje interfejs System.ComponentModel.IComponent, a kontrolką, która dziedziczy po klasie  System.Windows.Forms.Control. Warto wspomnieć, że klasa Control dziedziczy po klasie System.ComponentModel.Component, która z kolei implementuje interfejs IComponent. Dzięki temu można postrzegać kontrolki jako rozszerzone komponenty, czego przykład znajduje się poniżej. Komponent jest klasą, której obiekty wspierają design-time, ale w trybie tym, nie posiadają własnej graficznej reprezentacji, np. ToolTip. Natomiast kontrolka jest to komponent, który taką reprezentację posiada, np. Button. Oczywiście, w run-time zarówno ToolTip jak i Button posiadają graficzną reprezentację. W Visual Studio .NET obiekty, których klasy nie dziedziczą po Control, są umieszczane w polu na dole okna (ang component tray). Ilustracja tego jest na rysunku 2..

Rysunek 2. Widok kontrolki i komponentu w Visual Studio .NET

W design-mode można manipulować tylko obiektami, które implementują interfejs IComponent, ponieważ dzięki zawartemu w nim (właściwość Site) interfejsowi ISite możliwa jest dwukierunkowa komunikacja pomiędzy komponentem, jego kontenerem i różnego rodzaju designerami, które opisałam w następnych akapitach.

Każdy obiekt, który implementuje interfejs IComponent może posiadać dodatkowy obiekt, który będzie z nim związany tylko w design-time. Ta ważna cecha pozwala kod klasy tego obiektu umieścić w osobnym pakiecie(ang. assembly) i nie dystrybuować go z finalną wersją kodu. Obiekt taki nosi nazwę designera i zawiera kod, który umożliwia manipulację komponentem. Czynności te mogą być tak proste jak chowanie czy zmienianie właściwości, które komponent udostępnia w oknie Properties lub tak skomplikowane, jak współpraca z innymi designerami w celu zapewnienia wizualnej edycji grupie komponentów. Designery można ogólnie podzielić na dwie kategorie, przy czym niezależnie od podziału, każdy designer musi implementować interfejs IDesigner.

Pierwszy rodzaj designerów (ang. designer), to designery, które są w relacji jeden do jednego z komponentem. Inaczej rzecz ujmując, jeden obiekt klasy komponentowej ma przypisany w design-time jeden obiekt designera. Obiekt taki może pochodzić przykładowo z klasy ComponentDesigner, ControlDesigner lub pochodnej, która jest przypisana do klasy komponentu za pomocą atrybutu DesignerAtrribute. Jeżeli klasą bazową będzie ComponentDesigner to designer może między innymi dodać do menu kontekstowego nowy element oraz obsłużyć jego wywołanie – właściwość Verbs. Może zmieniać, dodawać i usuwać właściwości pokazywane w oknie Properties – metody PreXXXX i PostXXXX. Ma także dostęp do usług design-time oferowanych przez IDE – metoda GetService, a także ma możliwość powiadamiania IDE o wprowadzonych w komponencie zmianach – metody RaiseXXXX. Dziedziczenie po ControlDesigner rozszerza funkcjonalność designera głównie o obsługę kilkunastu zdarzeń myszy. Ponadto designer może coś dorysować do kontrolki – metoda OnPaintAdornments, zdecydować czy kontrolka designera może być zagnieżdżona w danej kontrolce – metoda CanBeParentedTo oraz powiadomić użytkownika o zaistniałym błędzie – metoda DisplayError. Bardzo przydatnym jest też mechanizm, dzięki któremu designer określa, czy kliknięcie w danym miejscu o współrzędnych (x,y) ma być przesłane do kontrolki – metoda GetHitTest.

Drugi rodzaj designerów (ang. root designer) jest bardzo podobny do pierwszego, z tą różnicą, że w danym oknie IDE może być tylko jeden obiekt takiej klasy i musi ona dodatkowo implementować interfejs IRootDesigner. Wspomniane okno można utożsamiać z oknem otwartym w Visual Studio.NET po wykonaniu double-click na pliku zawartym w oknie Solution Explorer, który reprezentuje klasę z Windows Forms, Web Forms lub UserControl. Root designer dostarcza płaszczyznę, na której odbywa się cały proces projektowania. Płaszczyzna ta może być obiektem Windows Forms (najczęściej Form), Web Forms (najczęściej Page) lub obiektem implementującym wszystkie specyficzne interfejsy danego IDE. Visual Studio .NET wspiera oba wymienione rodzaje. Obiekt reprezentujący płaszczyznę dostępny jest jako wynik wywołania metody IRootDesigner .GetView(). Jeżeli obiekt ten pochodzi z klasy dziedziczącej po Control,  to otrzymuje w czasie design-time pełny zestaw zdarzeń. Oznacza to, że zdarzenia typu MouseMove, Click itd. są przez ten obiekt odbierane w taki sposób, jakby znajdował się on w run-time. Dzięki temu możliwości interakcji z użytkownikiem są nieporównywalnie większe od tych oferowanych przez designera z poprzedniego akapitu. Tak szeroki wachlarz możliwości nie jest dostępny w momencie, gdy komponent jest zagnieżdżony w innym komponencie. Wtedy dostępny jest tylko designer pierwszego rodzaju. Komponent związany z root designerem nazywany jest root componetem i NIE jest tym samym co obiekt reprezentujący płaszczyznę projektową.  Na płaszczyźnie dostarczonej przez root designera mogą znaleźć się inne komponenty, które posiadają designery pierwszego rodzaju. Tak wiec w jednym oknie IDE może być wiele designerów. Do danej klasy komponentowej mogą być przypisane za pomocą DesignerAtrribute dwie klasy designerów. Jedna pierwszego i druga drugiego rodzaju. Obiekt drugiej będzie stworzony wtedy, gdy komponent będzie root componentem, a obiekt pierwszej w pozostałych przypadkach.

Z dwoma wyżej wymienionymi designerami nieodłącznie związany jest tak zwany designer host, który implementuje interfejs IDesignerHost i odpowiedzialny jest za zarządzanie wszystkimi instancjami komponentów, designerów oraz za zapisywanie i pobieranie danych. Tak jak w przypadku root designera, każde okno IDE posiada jednego designer hosta. Dzięki niemu możliwe jest między innymi tworzenie nowych i usuwanie niepotrzebnych komponentów bezpośrednio z kodu designera. Jest on także odpowiedzialny za trwałość zmian wprowadzonych przez designery. Zmiany te są zapisywane albo bezpośrednio do kodu, albo do pliku zasobów. Klasy, z których tego typu obiekty pochodzą, są implementowane przez wytwórców danego IDE.

Podsumowanie

Mam nadzieję, że zaprezentowana teoria dała Wam choćby minimalne rozeznanie co do możliwości design-time na platformie.NET. Tych, których ta tematyka zainteresowała, zapraszam do lektury innych artykułów traktujących o design-time. Łącznie z tym artykułem powstał drugi o tytule: Wsparcie dla design-time na platformie .NET – Designery, w którym opisuję praktyczne wykorzystanie przedstawionej tutaj teorii. Zachęcam też do pisania własnych prac na ten - według mnie bardzo interesujący – temat.

Jeżeli chodzi o dodatkowe informacje to polecam:

  1. http://www.msdn.microsoft.com i tam artykuły Pana, który nazywa się Shawn Burke

  2. http://divil.co.uk/net/articles/

  3. http://www.gotdotnet.com/Community/UserSamples/Details.aspx?SampleGuid=c3248df8-be4f-4be3-9ae4-ef73311f5e7f

Paweł Pabich

 

Podobne artykuły

Komentarze 1

kamiljaroszuk1582
kamiljaroszuk1582
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
ewidentnie sciagniete
pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.