6 32 Tworzenie serwerów automatyzacji ActiveX z użyciem MFC (2)


Rozdział 32.
Tworzenie serwerów automatyzacji ActiveX z użyciem MFC


W tym rozdziale:

Korzystanie z ClassWizarda w celu zredukowania czasu wymaganego do zaimplementowania serwera
Generowanie i wychwytywanie wyjątków OLE
Wybór i zarządzanie interfejsami serwera automatyzacji
Różnice przy generowaniu wyjątków
Tworzenie serwerów automatyzacji
Wybór pomiędzy serwerami o pojedynczych lub o wspólnych egzemplarzach


Jak wiesz z rozdziału 31., MFC i Visual C++ zawieraj ą proste i łatwe w użyciu mechanizmy do tworzenia kontrolek ActiveX.Visual C++ zawiera podobne mechanizmy do tworzenia serwerów automatyzacji. W rzeczywistości, przy opracowywaniu AppWizarda i ClassWizarda miano na myśli właśnie takie wsparcie. Tworzenie i manipulowanie serwerami automatyzacji jest jednym z głównych zastosowań Visual C++.
W tym rozdziale, wykorzystując MFC, stworzysz prosty serwer automatyzacji typu in-process, rejestrujący łańcuchy tekstowe do pliku. Do testowania serwera będziesz używał aplikacji takiej jak Visual Basic. Visual Basic doskonale nadaje się do testowania serwerów automatyzacji, gdyż nie wymaga poświęcania temu zadaniu dużej ilości pracy i kodu. W trakcie czytania tego rozdziału będziesz rozbudowywał swoją implementację, wykorzystując pewne bardziej zaawansowane zagadnienia związane z tworzeniem serwerów.
Tworzenie podstawowego projektu
Podczas tworzenia serwera automatyzacji pierwszym krokiem jest stworzenie podstawowego projektu, na którym będzie oparta cała aplikacja. MFC zawiera AppWizarda znacznie ułatwiającego to zadanie. Ten AppWizard składa się z serii okien dialogowych zawierających odpowiednie opcje, pozwalających na szybkie i łatwe stworzenie podstawowego zestawu plików projektu.
1. Aby stworzyć podstawowy projekt, uruchomVisual C++ i w menu File wybierz polecenie New. Pojawi się okno dialogowe New.
2. W oknie dialogowym New kliknij zakładkę Projects. Następnie jako typ projektu wybierz MFC AppWizard (dli).
3. Jako nazwę projektu (Project name) wpisz MFCServer, zaś jako kartotekę projektu wskaż \Helion\Rozdz32\MFCServer. Kliknij przycisk OK, przechodząc dalej.
Rysunek 32.1 przedstawia okno dialogowe New przygotowane do stworzenia programu MFCServer.
4. W pierwszym kroku kreatora definiowany jest rodzaj aplikacji, jaka ma zostać utworzona. W przypadku naszego serwera automatyzacji jako typ biblioteki DLL wybierz opcję Regular DLL with MFC statically linked, co spowoduje co prawda stworzenie nieco większej aplikacji, ale która będzie ładować się nieco szybciej, gdyż nie będzie konieczne ładowanie bibliotek DLL MFC przy każdym uruchomieniu serwera.
5. Włącz opcję Automation (automatyzacja) - gdyż właśnie taki jest podstawowy powód tworzenia tej aplikacji. Następnie kliknij przycisk Rnish przechodząc dalej. Visual C++ wyświetli informacyjne okno dialogowe New Project Information.
6. W tym oknie możesz przejrzeć wybrane ustawienia przed rzeczywistym stworzeniem projektu. Kliknij przycisk OK, zatwierdzając projekt. AppWizard automatycznie stworzy wszystkie podstawowe pliki konieczne dla stworzenia serwera automatyzacji w postaci biblioteki DLL.
Tabela 32. l zawiera listę wszystkich plików stworzonych przez AppWizarda wraz z krótkim opisem ich przeznaczenia.
Tabela 32.1. Podstawowe pliki źródłowe stworzone przez AppWizarda
Nazwa pliku
Opis
MFCServer.clw
Plik projektu VisualC++.
MFCServer.cpp
Główny plik źródłowy aplikacji i punkt wejścia do biblioteki DLL.
MFCServer.de
fStandardowy plik DEF aplikacji. Ten plik zawiera deklaracje eksportowanych funkcji potrzebne dla wszystkich serwerów in-process.

MFCServer dsp
Plik projektu Cisual C++.
MFCServer.dsw
Plik projektu Visual C++.
MFCServer h
Główny plik nagłówkowy aplikacji. MFCServer.ncbPlik projektu Visual C++.
MFCServer.odl
Standardowy plik ODL (Object Definition Language, język definicji obiektu).

MFCServer.r c
Standardowy plik zasobów.
ReadMe.txt
Plik tekstowy opisujący przeznaczenie plików projektu.
Resource.h
Plik nagłówkowy zasobów.
StdAfo. cpp
Standardowy plik dla prekompilowanych plików nagłówkowych.
StdAfa.h MFC.
Standardowy plik nagłówkowy dla prekompilowanych plików nagłówkowych. Dołączane są w nim wszystkie specyficzne pliki nagłówkowe

Res \MFCServer. rc2
Standardowy drugi plik zasobów. Ten plik zawiera wszystkie informacje o zasobach, których nie da się edytować bezpośrednio za pomocą Visual C++.


Po stworzeniu domyślnego projektu możesz go skompilować, ale nie będziesz mógł z nim zbyt wiele zrobić, gdyż serwer jeszcze nie zawiera interfejsów, metod ani właściwości.
Dodawanie do aplikacji interfejsu automatyzacji
Aby stać się serwerem automatyzacji, aplikacja musi zawierać przynajmniej jeden interfejs oparty na IDispatch. W MFC do zaimplementowania interfejsu musisz użyć klasy ccmdTarget (o klasie ccmdTarget mówiliśmy w rozdziale 19.). Przy dodawaniu interfejsów automatyzacji do aplikacji możesz skorzystać z usług ClassWizarda. Wykonaj poniższe kroki, aby dodać pierwszy interfejs:
1. Przywołaj ClassWizarda (wciskając kombinację Ctrl+W). Pojawi się okno dialogowe kreatora.
2. Kliknij przycisk Add Class (dodaj klasę), po czym z menu wybierz polecenie New. Pojawi się okno dialogowe New Class (nowa klasa).
3. W polu Name (nazwa) wpisz CTracker, zaś jako jej klasę bazową wybierz z rozwijanej listy Base class klasę CCmdTarget.
4. W sekcji Automation włącz opcję Automation. Opcja Creatable by type ID wraz z polem tekstowym jest używana do definiowania identyfikatora Progio, używanego przez Twoją aplikację oraz aplikacje wywołujące do tworzenia i uruchamiania serwera automatyzacji. Czytelny dla człowieka identyfikator ProgiD jest stosowany w miejsce identyfikatora CLSID, gdyż jest dużo łatwiejszy do zapisania i zapamiętania. Pamiętaj, by przy tworzeniu identyfikatorów ProgiD nie powtarzać się. W przypadku naszej aplikacji w polu ProgiD pozostaw domyślną wartość. Rysunek 32.2 przedstawia okno dialogowe New Class po wprowadzeniu informacji potrzebnych do stworzenia klasy.
5. Kliknij przycisk OK tworząc nową klasę i dodając ją do aplikacji.
6. Kliknij przycisk OK w oknie dialogowym ClassWizarda, aby powrócić do projektu.
Przy tworzeniu nowej klasy ccmdTarget MFC nie tylko tworzy plik nagłówkowy oraz plik źródłowy (w tym przypadku pliki Tracker.h oraz Tracker.cpp), ale także - ponieważ włączyliśmy obsługę automatyzacji dla tej klasy - aktualizuje plik ODL, tworząc w nim nowe pozycje Dispinterface oraz CoClass, takie jak w pliku MFCServer.odl na listingu 32.1.
Dispinterface jest naszym podstawowym interfejsem opartym na IDispatch i właśnie do niego ClassWizard będzie dodawał nowe metody i właściwości. Interfejs CoClass jest interfejsem naszej fabryki klas (ang. class factory). Fabryka klas jest częścią aplikacji odpowiedzialną za tworzenie serwera automatyzacji w chwili pojawienia się takiej potrzeby. Więcej informacji na temat fabryk klas i ich roli w OLE znajdziesz w dokumentacji OLE i MFC.
Listing 32.1. Plik MFCServer.odlpo automatycznym uzupełnieniu go o interfejsy Dispinterface _________/ CoClass naszego serwera automatyzacji_
_________________ ____
// MFCServer.odl : type library source for MFCServer.dll

// This file will be processed by the MIDL compiler to // produce the type library (MFCServer.tlb).
[ uuid(56B738C4-OC67-llD4-9D9F-0020AF9FDDD6) , version(1.0) library MFCServer
{
importlib("stdole32.tlb"); importlib("stdole2.tlb");

// Primary dispatch interface for CTracker

[ uuid(56B738Dl-OC67-HD4-9D9F-0020AF9FDDD6) ]
dispinterface ITracker
{
properties:
// NOTĘ - ClassWizard will maintain property
// Information here. Use extreme caution when
// editing this section.
//{{AFX_ODL_PROP(CTracker)
//}}AFX_ODL_PROP

methods:
// NOTĘ - ClassWizard will maintain method
// information here. Use extreme caution when
// editing this section.
//{{AFX_ODL_METHOD(CTracker)
//}}AFX_ODL_METHOD
};
// Class information for CTracker

[ uuid(56B738D3-OC67-llD4-9D9F-0020AF9FDDD6)
coclass Tracker
{
[default] dispinterface ITracker;
},
//{{AFX_APPEND_ODL} //}}AFX APPEND ODL}
},
Zwróć uwagę w tym kodzie, że mimo iż ClassWizard dodał nowy interfejs do serwera, jednak nie udostępnił go na zewnątrz. Tak więc na razie stworzyliśmy po prostu serwer automatyzacji, którego nie może stworzyć żadna aplikacja - czyli który jak dotąd jest bezużyteczny.
Wszystkie komponenty ActiveX są tworzone poprzez obiekt zwany fabryką klas (ang. class factory). W celu wsparcia dla fabryki klas MFC definiuje klasę coleObjectFactory. Nie możesz jednak dodać tej klasy bezpośrednio do implementacji swojego serwera. Zamiast tego MFC wymusza użycie dwóch zdefiniowanych przez siebie makr,
DECLARE_OLECREATE Oraz DECLARE_OLECREATE.
Aby zbudować swoją fabrykę klas:
1. W oknie widoku projektu kliknij zakładkę ClassView.
2. Rozwiń listę klas, po czym dwukrotnie kliknij klasę CTracker w celu otwarcia pliku Tracker.h.
3. Do definicji klasy dopisz makro DECLAREJDLECREATE. Makro wymaga pojedynczego parametru, nazwy klasy, którą w naszym przypadku jest CTracker. Poniższy kod implementuje plik nagłówkowy Tracker.h:
//...
// NOTĘ - the ClassWizard will add and remove member
// functions here. //}}AFX_DISPATCH DECLARE_DISPATCH_MAP( ) DECLARE_INTERFACE_MAP() DECLARE_OLECREATE(CTracker)
};
4. Następnie, ponownie w oknie widoku projektu, kliknij zakładkę FileYiew, rozwiń listę Source files (plików źródłowych) i dwukrotnie kliknij plik Tracker.cpp w celu otwarcia go w edytorze.
5. Dopisz do pliku źródłowego makro IMPLEMENT_OLECREATE. To makro wymaga podania trzech parametrów: nazwy klasy, identyfikatora ProgiD używanego przez aplikację do tworzenia serwera oraz identyfikatora CLSID interfejsu Cociass, takiego jak zdefiniowany w pliku ODL.
6. Gdy AppWizard tworzył plik ODL, przy okazji wygenerował identyfikator CLSID dla biblioteki typów. Gdy ClassWizard dodawał klasę CTracker, stworzył nowe identyfikatory CLSID: jeden dla interfejsu Dispinterface, a drugi dla CoClass. Powinieneś wprowadzić poniższe zmiany (zwróć uwagę, że Twój CLSID będzie inny niż identyfikator użyty w książce, chyba że skopiowałeś pliki z płytki CD-ROM):

// ...
// {56B738D1-OC67-11D4-9D9F-0020AF9FDDD6} static const IID IID_ITracker =
{ Ox56b738dl, Oxc67, Oxlld4, { Ox9d, Ox9f, 0x0, 0x20, Oxaf, Ox9f, Oxdd, Oxd6 } } ;

BEGIN_INTERFACE_MAP( CTracker, CCmdTarget)
INTERFACE_PART (CTracker, IID_ITracker, Dispatch)
END_INTERFACE_MAP ( )
IMPLEMENT_OLECREATE( CTracker, _T ( "MFCServer .Tracker" ) , Ox56b738dl, Oxc67, Oxlld4, Ox9d, Ox9f, 0x0, 0x20, Oxaf , Ox9f , Oxdd, Oxd6) ;
// ...
W tym momencie uzupełniłeś implementację serwera o fabrykę klas, dzięki której inne aplikacje będą mogły tworzyć nasz serwer. Zanim jednak inne aplikacje będą mogły to robić, OLE musi wiedzieć, gdzie znaleźć ten serwer, do czego wykorzystują Rejestr systemowy. Wszystkie komponenty ActiveX dostępne publicznie innym aplikacjom muszą obsługiwać rejestrację i muszą tworzyć właściwe pozycje Rejestru. Tematem rejestracji serwera zajmiemy się więc w następnej sekcji.
Rejestrowanie serwera
Komponenty ActiveX mają po jednej lub po kilka pozycji Rejestru używanych do opisu różnych aspektów aplikacji oraz sposobów ich użycia. Rejestr jest krytycznym elementem przy uruchamianiu i używaniu komponentów ActiveX. Lokalne serwery przy rejestracji opierają się na parametrach linii poleceń. Do zadań twórcy lokalnego serwera należy sprawdzenie opcji linii poleceń i podjęcie odpowiedniej akcji. Tabela 32.2 zawiera opcje linii poleceń dla rejestracji serwera lokalnego.
Tabela 32.2. Opcje linii poleceń dla rejestracji serwera lokalnego Opcja linii poleceń
Opis
R
Rejestruje wszystkie komponenty.
U
Wyrejestrowywuje wszystkie komponenty.
S
Przeprowadza rejestrację "po cichu", bez wyświetlania okna dialogowego potwierdzającego powodzenie operacji. Wciąż jednak wyświetlane są ewentualne komunikaty błędów przy rejestracji. Można łączyć tę opcję z opcjami R i U.

Wszystkie komponenty ActiveX in-process udostępniają w celu rejestracji dwie eksportowane przez siebie funkcje: DllRegisterServer () oraz DllUnregisterServer ().
AppWizard automatycznie dodaje funkcję DllRegisterServer () do głównego pliku tworzonej aplikacji. Wewnątrz tej funkcji powinieneś zarejestrować wszystkie komponenty zawarte w aplikacji. Także każdy komponent ActiveX musi być odpowiedzialny za obsługę własnej rejestracji.
Klasa coleobjectFactory automatycznie obsługuje rejestrację. Nawet mimo iż możesz nie zdawać sobie z tego sprawy, klasa coleobjectFactory zawiera pojedynczą połączoną listę używaną do śledzenia wszystkich klas coleobjectFactory zaimplementowanych w pojedynczej aplikacji. Ta połączona lista jest statyczną składową klasy, co oznacza, że wszystkie egzemplarze klasy korzystają z tej samej listy. Klasa coleobjectFactory zawiera także statyczną funkcję składową, updateRegistryAll (), przechodzącą przez listę klas coleobjectFactory i instruującą każdą z nich by się samodzielnie zarejestrowała.
Wyrejestrowanie serwera jest nieco bardziej złożone. Podczas tworzenia projektu AppWizard'a nie dodaje do niego eksportowanej funkcji DllunregisterServer (). To niedopatrzenie może wynikać z pewnych ograniczeń w MFC. Grupa twórców MFC prawdopodobnie uważała, że nie ma potrzeby dodawać obsługi wyrejestrowywania do podstawowej klasy coleobjectFactory w MFC. Ta decyzja jest o tyleż dziwna, gdyż wszystkie wymagania co do logo Microsoftu nakazują, by wszystkie instalowane i rejestrowane aplikacje muszą także być w stanie odinstalować się i wyrejestrować.
W celu obsłużenia wyrejestrowania serwera musisz dodać do klasy eksportowaną funkcję DLLUnregisterServer () i, w celu wyrejestrowania serwera, wywołać funkcję ColeobjectFactory: :UpdateRegistryAll o, przekazując jej wartość FALSE. Sam kod wy rej estrowy wuj ący wymaga więcej pracy. W tej książce nie zawarliśmy kodu wyreje-strowywującego, lecz nie jest on skomplikowany. Pierwszym krokiem jest stworzenie
nowej klasy wyprowadzonej z klasy coieObjectFactory oraz przesłonięcie w niej wirtualnej funkcji updateRegistry ( ) . Wewnątrz nowej funkcji kod powinien sprawdzać parametr przekazany przez funkcję wywołującą i na podstawie jego wartości powinny zostać podjęte odpowiednie operacje rejestrowania lub wyrejestrowywania. MFC zawiera prostą funkcją pomocniczą dla rejestrowania, AfxOieRegisterServerciass (), lecz nie zawiera podobnej funkcji do wyrejestrowywania. Po przeszukaniu plików źródłowych MFC odkrywamy pełny zestaw funkcji pomocniczych dla Rejestru, jednak niestety, są one dostępne tylko z zaimplementowanych w MFC kontrolek ActiveX. Ponieważ nie mamy żadnego wsparcia ze strony MFC, musimy sami zaimplementować kod aktualizujący Rejestr (używając wywołań Windows API). Pamiętaj, że podczas procesu wyrejestrowywania konieczne jest usunięcie wszystkich pozycji Rejestru stworzonych podczas rejestracji serwera, włącznie z identyfikatorami Progio, CLSID oraz biblioteką typów.
Tworzenie roboczego kodu serwera
Ponieważ nasz serwer ma być używany do zapisu danych do pliku, przed dodaniem do niego metod i właściwości musimy stworzyć nieco kodu wykonującego nałożone zadania. Pierwszym krokiem jest dokonanie zmian w pliku nagłówkowym Tracker.h. Musisz dopisać do niego zestaw zmiennych składowych dla przechowania uchwytu pliku oraz informacji timera, które będą używane w dalszej implementacji serwera. Zmiany w pliku Tracker.h będą następujące:

// ...
DECLARE_OLECREATE (CTracker) protected:
FILE * m_fileLog; long m_lTimeBegin; long m_lHiResTime; long m ILastHiResTime;
};
Następnym krokiem jest aktualizacja pliku źródłowego klasy. W celu wykorzystania funkcji timera multimedialnego do pliku Tracker.cpp musimy dołączyć plik nagłówkowy mmsystem.h. Musimy także stworzyć konstruktor oraz destruktor serwera. Zmiany w pliku Tracker.cpp zostały przedstawione na listingu 32.2.
Listing 32.2. Kod w pliku Tracker.cpp po zaimplementowaniu konstruktora i destruktora
// ...
#include "Tracker.h"
// potrzebna dla korzystania z timera mul1|imedialnego
#include

ttifdef _DEBUG tfdefine new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = _FILE_;
#endif
////////////////////////////////////////////////////
// CTracker

IMPLEMENT_DYNCREATE(CTracker, CCmdTarget)

CTracker::CTracker () {
EnableAutomation();
// upewniamy się, że aplikacja nie zostanie wyładowana przed // wyzerowaniem licznika odwołań : :Afx01eLockApp() ;

// ustawiamy rozdzielczość timera
m_lTimeBegin = timeBeginPeriod(l) ;
m_lHiResTime = m_lLastHiResTime = timeGetTime();

// pobieramy bieżąca, datę i czas
CTime oTimeStamp = CTime::GetCurrentTime();
CString cstrFileName;

// tworzymy nazwę pliku na podstawie daty cstrFileName.Format(_T("%s.tracklog"),
(LPCTSTR) oTimeStamp.Format("%Y%m%d"));

// otwieramy plik
m_fiieLog = fopen(cstrFileName, _T("a"));

// jeśli mamy uchwyt pliku
if(m_fileLog)
{
// wypisujemy informacje początkowe
fprintf(m_fileLog, _T ("************************\n"});
fprintf (m_f ileLog, __T ( "Start %s\n"),
(LPCTSTR) oTimeStamp.Format("%B %#d, %Y, %I:%M %p"));
fprintf(m_fileLog, T("\n"));
}
}
CTracker::-CTracker()
{
// jeśli mamy uchwyt pliku if(m_fileLog)
{
// wypisujemy informacje końcowe
CTime oTimeStamp = CTime::GetCurrentTime();
fprintf(m_fileLog, _T("\n"));
fprintf(m_fileLog, _T("Koniec %s\n"),
oTimeStamp.Format("%B %#d, %Y, %I:%M %p"; fprintf(m_fileLog, _T("********************:\N"));

// zamykamy plik fclose(m_fileLog);
}
// jeśli możemy korzystać z usług timera if(m_lTimeBegin == TIMERR_NOERROR)
// przywracamy oryginalny stan timera
timeEndPeriod(1;

// umożliwiamy wyładowanie aplikacji ::Afx01eUnlockApp();
}
// ...
Jak widać, musisz także zaktualizować konstruktor i destruktor serwera. Pierwsze wywołanie funkcji EnableAutomation() jest wynikiem włączenia opcji Automation w oknie New Class ClassWizarda. Następnie, kod musi wywołać metodę AfxOleLockApp (), zapewniającą, że aplikacja nie zostanie wyładowana z pamięci, zanim licznik odwołań (tzn. ilość aktualnie otwartych obiektów tej klasy) nie spadnie do zera. Gdy używasz OLE w aplikacjach MFC, wywołując wspomnianą funkcję, musisz zawsze blokować aplikację w pamięci. Ten krok jest krytyczny i musisz zamieścić wywołanie tej funkcji we wszystkich serwerach opartych na MFC.
Następnie kod w konstruktorze tworzy timer multimedialny o wysokiej rozdzielczości i zachowuje jego bieżącą wartość w zmiennych składowych. Ten timer dobrze nadaje się do wyznaczania ilości milisekund, jakie upłynęły od czasu ostatniego wywołania metody. Wynik działania timera doskonale nadaje się do śledzenia wydajności funkcji lub zestawu funkcji.
Następnie pobieramy bieżącą datę i tworzymy nazwę pliku w formacie YYYYMMDD. trackiog. Po zakończonym powodzeniem otwarciu pliku wypisujemy pewne dane początkowe i wychodzimy z konstruktora.
Destruktor wykonuje operacje odwrotne niż konstruktor. Jeśli mamy poprawny uchwyt pliku, zapisujemy pewne informacje końcowe i zamykamy plik. Następnie kończymy działanie timera. Pamiętaj, aby wywołać funkcję Afx01eUnlockApp() w celu umożliwienia systemowi operacyjnemu ewentualne wyładowanie aplikacji z pamięci.
Na koniec, musisz jeszcze zaktualizować ustawienia samego projektu. Ponieważ w naszym przykładzie korzystamy z funkcji timera zadeklarowanych w pliku mmsystem.h, musimy dołączyć do projektu odpowiednią bibliotekę zawierającą implementacje tych funkcji. Aby dołączyć bibliotekę, w menu Project wybierz polecenie Settings. Pojawi się okno dialogowe Project Settings. Z rozwijanej listy Settings For wybierz pozycję Ali Configurations. Kliknij zakładkę Link i dopisz plik winmm.lib w polu edycji Object/library modules. Kliknij przycisk OK zamykając okno dialogowe.
W ten sposób dodaliśmy podstawowy kod obsługujący naszą implementację. Serwer będzie otwierał plik w konstruktorze, pozostawiając go otwartym przez cały czas swojego istnienia. Podczas niszczenia serwera zostanie wywołany jego destruktor, w którym nastąpi zamknięcie pliku. Następnym krokiem jest uczynienie naszego przykładu bardziej użytecznym przez dodanie metod i właściwości, których użyjemy do wypisywania danych do otwartego pliku.
Wartości logiczne w Visual C++ i w Visual Basicu
Ważne jest, by zwrócić uwagę na zasadniczą różnicę pomiędzy Visual C++ a Visual Basicem w przechowywaniu wartości logicznych typu boolean. W C++ wartości logiczne są zdefiniowane jako typ całkowity, tj. są wartościami 32-bitowymi. Jednak w przypadku Visual Basica liczba całkowita jest wartością 16-bitową. W przypadku prostych serwerów automatyzacji stworzonych w MFC różnica w rozmiarach zmiennych nie jest problemem, gdyż MFC ukrywa szczegóły związane z konwersją wartości 32-bitowych na 16-bitowe i odwrotnie.
Jednak w przypadku aplikacji z podwójnymi interfejsami różnica w rozmiarze staje się znaczącym problemem. Przy odwołaniu do niestandardowego interfejsu serwera z podwójnym interfejsem funkcje są wywoływane w ten sam sposób jak każde inne funkcje aplikacji. System operacyjny umieszcza po prostu argumenty na stosie, a następnie wywołuje funkcję. Podczas wykonywania funkcji argumenty są zdejmowane ze stosu. Gdy Visual Basic wywołuje funkcję Visual C++, stos zostaje uszkodzony z powodu różnych rozmiarów zmiennych logicznych w obu językach. Aby się przed tym zabezpieczyć, aplikacje Yisual C++ powinny definiować wszelkie dane logiczne jako dane typu VARIANT_BOOL, zdefiniowane w OLE jako wartości 16-bitowe, z zagwarantowanym niezmiennym rozmiarem bez względu na stosowane narzędzie lub język.
Także same wartości zmiennych logicznych są inne w Visual C++ i inne w Visual Basicu. Programiści Visual Basica używają O dla wartości logicznej FALSE, zaś dla wartości logicznej TRUE używają wartości -l. Wynika to z faktu, że w zapisie binarnym te wartości wynoszą odpowiednio 00000000 i 11111111. Z drugiej strony, programiści Visual C++ przywykli do stosowania jako wartości logicznej FALSE również wartości 0, lecz dla wartości logicznej TRUE stosują wartość 1.
Te różnice w wartościach zmiennych logicznych mogą stwarzać znaczne problemy podczas integracji aplikacji napisanych w Visual C++ z aplikacjami Visual Basica. Oprócz tego, Visual Basic 4 zawiera nieco inną interpretację języka, w zależności od testowanych wartości. Niektóre funkcje Visual Basica nie sprawdzają wartości zero lub nie zero, lecz sprawdzają absolutne wartości 0 i -1, lub odwrotnie, w zależności od typu danych i funkcji. Używając danych o typach logicznych, przy definiowaniu wartości rozsądnie jest korzystać ze stałych VARIANT_
FALSE Oraz VARIANT TRUE.
Dodawanie metod
Metoda automatyzacji może być bezparametrowa lub może posiadać kilka parametrów, a także może, lecz nie musi, zwracać wartość. Określenie metoda jest synonimem funkcji lub podprogramu w zależności od języka, którego używasz. Ponieważ nasz serwer jest oparty na interfejsie IDispatch, jesteśmy ograniczeni co do rodzajów typów danych,
jakie możemy przekazać w wywołaniach metod. Jak wiesz z ostatniego rozdziału, możesz przekazywać lub odbierać tylko te typy danych, które można przekazać poprzez ogólny typ VARIANT.
Jak już wspominaliśmy, nasz serwer automatyzacji będzie używany do zapisu napisów do pliku. Serwer będzie posiadał metodę OutputLines (), poprzez którą klient serwera będzie mógł zapisać napis do pliku. Metoda będzie akceptować tablicę łańcuchów oraz opcjonalny parametr określający głębokość wcięcia linii i będzie zapisywać otrzymane łańcuchy do pliku. Parametr wcięcia będzie używany do przesunięcia łańcucha o n znaków tabulacji w celu dostarczenia prostego lecz efektywnego mechanizmu formatowania po-zycj i w pliku.
Aby dodać metodę, w menu View wybierz polecenie ClassWizard. Kliknij zakładkę Auto-mation (automatyzacja), po czym kliknij przycisk Add Method (dodaj metodę). W oknie dialogowym Add Method jako nazwę zewnętrzną (External name) wpisz OutputLines, zaś jako zwracany typ (Return Type) wybierz BOOL.
Metoda OutputLines () będzie miała dwa parametry: varOutputArray jako typ VARIANT przekazywany przez referencję, zawierający tablicę danych przeznaczonych do zapisania w pliku, oraz varindent jako typ VARIANT przekazywany poprzez wartość, stanowiący argument opcjonalny, określający głębokość wcięcia napisu (w tabulatorach) w momencie zapisywania go do pliku. Aby dodać parametry metody, dwukrotnie kliknij linię na liście Parameter list, tuż pod kolumną Name, po czym wpisz varOutputArray. Kliknij tuż pod kolumną Type w celu uaktywnienia rozwijanej listy typów. Wybierz z listy pozycję VARIANT *. Powtórz te sane czynności dla drugiego argumentu, varindent, lecz tym razem jako typ danych wybierz VARIANT. Rysunek 32.3 przedstawia okno dialogowe Add Method już po dopisaniu obu parametrów.
ak wiesz z rozdziału 31., ze względu na ograniczenia przekazywanych typów danych nakładane przez automatyzację, nie mamy możliwości przekazywania tablic jako parametrów metod, chyba że stworzymy własny kod szeregujący. Możemy jednak przekazać dane typu VARIANT mogące zawierać tablice. Tak więc, w naszym prostym serwerze zdefiniujemy varOutputArray jako typ VARIANT. Oprócz tego, musimy zdefiniować zmienną varOutputArray jako przekazywaną przez referencję, tak aby tablica przechowywana w typie VARIANT nie była kopiowana, tak jak miałoby to miejsce przy przekazywaniu danych poprzez wartość.
Wiemy także, że opcjonalne parametry muszą występować na końcu listy parametrów i muszą być typu VARIANT. Parametr varindent jest parametrem opcjonalnym służącym do wcięcia tekstu w celu stworzenia podstawowych możliwości formatowania.
Pamiętaj także, że ClassWizard oprócz zmodyfikowania plików, nagłówkowego i źródłowego, dodał także pozycję do pliku ODL. To właśnie do pliku ODL należy zadeklarowanie parametru metody jako parametru opcjonalnego. Aby parametr był opcjonalny, musi być zadeklarowany z użyciem słowa kluczowego optional, tak jak opisywaliśmy to w rozdziale 31. Pamiętaj, że to słowo kluczowe musisz dopisać samodzielnie, gdyż ClassWizard nie zrobi tego za Ciebie. Po dopisaniu słowa kluczowego odpowiedni fragment pliku ODL powinien wyglądać następująco:

// NOTĘ - ClassWizard will maintain method Information here.
// Use extreme caution when editing this section.
//{{AFX_ODL_METHOD(CTracker)
[id(2)] boolean OutputLines(VARIANT*varOutputArray,
[optional] VARIANT varlndent); //}}AFX_ODL_METHOD

Zanim dodamy implementację metody OutputLines (), musimy najpierw dodać zmienną składową do definicji klasy. Serwer będzie używał nowej składowej, m_lindent, do przechowywania pomiędzy wywołaniami metody OutputLines () bieżącego poziomu wcięcia. Nową zmienną składową powinieneś dopisać do pliku nagłówkowego Tracker.h:

protected:
FILE * m_fileLog; long m_lTimeBegin; long m_lHiResTime; long m_lLastHiResTime; long m_llndent;
};
Musisz także zaktualizować konstruktor w celu zainicjowania tej zmiennej poprawną wartością początkową (taką jak 0). Po wykonaniu koniecznych kroków w celu dodania nowej zmiennej składowej możesz przystąpić do pisania metody OutputLines (). Stworzona przez nas metoda OutputLines () została przedstawiona na listingu 32.3.
Listing 32.3. Implementacja metody OutputLines()

////////////////////////////////////////////////////////////////////////
// CTracker message handlers

BOOL CTracker::OutputLines(VARIANT FAR* varOutputArray, const VARIANT FAR& varIndent)
{BOOL bResult =VARIANT_TRUE;

// jeśli mamy plik i jeśli wariant zawiera tablicę łańcucha if(m_fileLog && varOutputArray->vt ==
(VT_ARRAY | VT_BSTR))
{
// blokujemy tablicę, aby móc jej użyć
if(::SafeArrayLock(varOutputArray->parray) == S_OK)
{
LONG1LBound;
// pobieramy dolny indeks tablicy
if(::SafeArrayGetLBound(varOutputArray->parray, l, &1LBound) == S_OK)
{
LONG1UBound;

// pobieramy ilość elementów w tablicy if(::SafeArrayGetUBound(varOutputArray->parray, l, &lUBound) == S_OK)
{
CString cstrlndent; CTime oTimeStamp; BSTR bstrTemp;

// jeśli mamy parametr wcięcia if(varlndent.vt != VT_14)

// pobieramy wariant, który możemy użyć // w celu konwersji VARIANT varConvertedValue;

// inicjalizujemy wariant
::VariantInit(&varConvertedValue);

// sprawdzamy, czy możemy zamienić dane // na coś użytecznego; możemy użyć także // VariantChangeTypeEx() if(S_OK == ::VariantChangeType(&varConvertedValue,
(VARIANT *) &varIndent, O, VT_I4)) // przypisujemy wartość naszej // zmiennej składowej m_lIndent = varConvertedValue.1Val;
}
else
// przypisujemy wartość naszej // zmiennej składowej m_llndent = varIndent.1Val;

// jeśli musimy wciąć tekst for(long lIndentCount = 0;
lIndentCount < m_lIndent;
lIndentCount++)
// dodajemy tabulator do łańcucha
cstrlndent += _T("\t");

// dla każdego z elementów w tablicy for(long lArrayCount = 1LBound;
1ArrayCount < (lUBound +1LBound);
1ArrayCount+)
{
// aktualizujemy czas
oTimeStamp = CTime::GetCurrentTime();
m IHiResTime = timeGetTime();
// pobieramy dane z tablicy
if(::SafeArrayGetElement(
varOutputArray->parray, &!ArrayCount, SbstrTemp) == S_OK)
{
// wypisujemy dane fprintf(m_fileLog,
_T ("%s(%101d)-%s%ls\n") , (LPCTSTR)
oTimeStamp.Format("%H:%M:%S"), m_lHiResTime - m_lLastHiResTime, (LPCTSTR) cstrlndent, bstrTemp);

// zachowujemy ostatnią // wartość timera m_lLastHiResTime = m_lHiResTime;

// zwalniamy bstr
::SysFreeString(bstrTemp);
}
}
}
else
bResult = VARIANT FALSE;
}
else
bResult = VARIANT_FALSE;

// odblokowujemy tablicę, gdyż jej już
// nie potrzebujemy
::SafeArrayUnlock(varOutputArray->parray);
}
else
bResult = VARIANTFALSE;
}
else
bResult = VARIANT_FALSE;

// zwracamy wynik return bResult;
}
Najpierw kod wewnątrz metody sprawdza, czy posiadamy poprawny uchwyt pliku oraz tablicę łańcuchów danych. Następnym krokiem jest zablokowanie tablicy w celu umożliwienia wykonywania na niej operacji. Ten krok jest konieczny we wszystkich funkcjach operujących na bezpiecznych tablicach. Następne wywołanie funkcji, SafeArrayGet-Bound (), wyznacza początkowy punkt tablicy, którym może być albo O, albo l. Ta procedura jest bardzo ważna, gdyż pewne języki programowania, na przykład C++, definiuj ą tablice jako rozpoczynające się od zera, zaś inne języki, takie jak na przykład Yisual Basic, definiuj ą tablice jako rozpoczynające się od 1. Następnie pobieramy ilość rozmiarów tablicy. Zwróć uwagę, że ta wartość reprezentuje ilość wymiarów, a nie ostatni wymiar względem pierwszego elementu tablicy.
Po wyznaczeniu granic tablicy kod musi sprawdzić, czy funkcja otrzymała także wartość wcięcia. Chcemy otrzymać długą wartość całkowitą, VT_4, ale jeśli tak się nie stanie, spróbujemy dokonać konwersji otrzymanych danych na coś użytecznego. Jeśli nie będziemy mogli dokonać konwersji danej, po prostu użyjemy wartości, która już jest w naszej zmiennej składowej. W celu wcięcia tekstu funkcja dołącza od jednego do n znaków ta-bulacji na początku każdego z łańcuchów.
Dla każdego elementu w tablicy łańcuchów funkcja pobiera aktualny czas i dane powiązane z każdym z elementów, a następnie wypisuje łańcuch wraz z tabulatorami do otwartego pliku. Po zapisie łańcucha jest on zwalniany. Na koniec, kod metody odblokowuje tablicę i kończy działanie, zwracając odpowiednią wartość.
Warto poświęcić chwilę na przejrzenie dokumentacji Visual C++ dotyczącej ODL, automatyzacji oraz typów VARIANT, aby sprawdzić, na ile elastycznie można definiować metody i ich parametry. Teraz, gdyż już dodaliśmy metodę, możemy zaimplementować jej przeciwieństwo, czyli właściwość.
Dodawanie właściwości do serwera
Właściwość może być uważana za udostępnioną zmienną, zdefiniowaną w serwerze automatyzacji. Właściwości są przydatne przy ustawianiu i odczytywaniu informacji dotyczących stanu serwera. Właściwości serwera są implementowane w postaci pary metod -jednej do pobierania wartości, zaś drugiej do jej ustawiania. Zmienna składowa m_iindent dodana wcześniej do definicji klasy będzie doskonałym kandydatem do udostępnienia jako właściwość serwera.
Tak jak w przypadku metod, dodawania właściwości możesz skorzystać z pomocy ClassWizarda. Aby dodać właściwość, uruchom ClassWizarda i na zakładce Automation kliknij przycisk Add Property (dodaj właściwość). W oknie dialogowym Add Property jako zewnętrzną nazwę (External Name) właściwości wpisz indent, zaś jako jej typ (Type) wybierz long. Kliknij opcję Get/Set methods w celu wybrania sposobu implementacji, po czym kliknij przycisk OK, zatwierdzając utworzenie właściwości. W oknie dialogowym ClassWizarda kliknij przycisk Edit Code w celu przejścia bezpośrednio do edycji pliku źródłowego. Okno dialogowe New Property przygotowane do utworzenia nowej właściwości zostało przedstawione na rysunku 32.4.
Sama implementacja naszej właściwości indent jest już prosta. Tak jak w przypadku kontrolki tworzonej w rozdziale 31., funkcja Getindento zwraca wartość przechowywaną w zmiennej składowej m_lindent, zaś funkcja Set indent () zachowuje nową wartość w tej zmiennej składowej, w pewnym stopniu sprawdzając dodatkowo poprawność przekazanych danych. Funkcje odczytujące i zapisujące właściwość możesz zaimple-mentować następująco:

long CTracker::Getlndent()
{
// zwracamy zmienną składową return m_lIndent;
}
void CTracker::Setlndent(long nNewYalue)
{
// jeśli nową wartością jest przynajmniej 0 if(nNewValue >= 0)
// przypisujemy wartość naszej zmiennej składowej
m_ llndent = nNewValue;
}
Właściwości, podobnie jak metody, mogą być implementowane na wiele sposobów, włącznie ze stosowaniem parametrów i wartości wyliczeniowych. Te bardziej zaawansowane rozwiązania omawialiśmy w rozdziale 31.
Jak dotąd, dodaliśmy do serwera metodę i właściwość, lecz nie zajęliśmy się jeszcze zagadnieniem obsługi błędów. W pewnych sytuacjach proste zwrócenie sukcesu lub porażki nie pozwala programiście na wystarczające zrozumienie przyczyny błędu. Tak więc w następnej sekcji zajmiemy się wyjątkami OLE, dzięki którym możesz przekazać więcej informacji o błędach.
Generowanie wyjątków OLE
Podczas wykonywania metody czasem konieczne jest zakończenie tego procesu z powodu błędu krytycznego, który nastąpił lub który za chwilę ma nastąpić. Na przykład, program sterujący wywołuje metodę serwera zapisującą dane do pliku, lecz serwer nie może otworzyć pliku lub nie może do niego zapisać danych z powodu braku miejsca na dysku. W takiej sytuacji trzeba zawiesić dalsze działanie aż do usunięcia przyczyny błędu. Błędy tego rodzaju są nazywane wyjątkami (ang. exceptiori). Jako wyjątek można potraktować każdy rodzaj błędu, zaś sposób obsługi wyjątku zależy od zadań aplikacji oraz od tego, co chcesz osiągnąć.
Podczas tworzenia komponentów ActiveX możesz natknąć się na dwie formy wyjątków. Pierwszą z nich są wyjątki C++ Wyjątek C++ to opisywany już wcześniej mechanizm języka używany do tworzenia błędów krytycznych, specyficznych dla aplikacji, w której te błędy zostały zdefiniowane. Drugą formą są wyjątki OLE. Wyjątki OLE są używane do zgłaszania błędów komponentów na zewnątrz, do aplikacji korzystających z tych komponentów. Różnica polega na tym, że wyjątki C++ są całkowicie wewnętrzne dla
implementacji aplikacji, zaś wyjątki OLE można stosować zarówno wewnętrznie, jak i zewnętrznie, choć w większości przypadków wykorzystuje się je głównie do zgłaszania błędów innym aplikacjom.
Interfejs IDispatch zawiera w swoich funkcjach specyficzne parametry, przeznaczone do obsługi wyjątków i przekazywania ich aplikacji sterującej. Szczegóły związane z generowaniem wyjątków OLE obsługuje implementacja klasy CGmdTarget w MFC. Domyślna implementacja klasy wychwytuje zgłoszone wyjątki C++ i zamienia je na odpowiednie informacje o błędach, zgodne z interfejsem IDispatch. Jedyne co musisz zrobić to za pomocą instrukcji throw zgłosić wyjątek typu coieDispatchException - resztą zajmie się MFC. Jednak gdy tworzysz serwery o podwójnym interfejsie, musisz obsłużyć wyjątki w nieco inny sposób. Zajmiemy się tym w dalszej części rozdziału.
Pierwszym krokiem w przygotowaniu do obsługi wyjątków OLE jest dodanie do pliku ODL wyliczenia typów błędów, jakie serwer może wygenerować. Dodanie tego wyliczenia do pliku ODL odpowiada udostępnieniu programistom aplikacji korzystających z Twojego serwera listy kodów błędów serwera. Stałe dodaje się w postaci dołączanego pliku nagłówkowego, więc z tych samych stałych można korzystać również w kodach źródłowych C++. Oprócz tego, do definicji typedef dodaje się identyfikator UUID generowany za pomocą programu GUIDGEN.EXE, dzięki czemu można j ą zidentyfikować w bibliotece typów. W naszym przykładowym serwerze wyliczenie kodów błędów można zaimple-mentować następująco:

[ uuid(llC82947-4EDD-llDO-BED8-00400538977D) ]
coclass TRACKER
{
[default] interface ITracker;
};
typedef [uuid (11C82948-4EDD-11DO-BED8-00400538977D) ,
helpstring ( "Stałe błędów serwera Tracker")] #include "trackererror .h"

//{ {AFX APPEND ODL} }
};
Z kolei plik TrackerError.h zawiera standardowy typ wyliczeniowy C++ z listą kodów błędów zgłaszanych przez serwer. Początkowa wartość kodów błędów należy do przedziału wartości definiowanych przez użytkownika. Bądź uważny podczas przypisywania kodów błędom, gdyż większość narzędzi najpierw porównuje kod z kodami błędów zdefiniowanych w systemie, a dopiero potem z kodami zdefiniowanymi w pliku nagłówkowym wyjątków. Plik TrackerError.h został zdefiniowany następująco:

// Wyliczenie błędów enum tagTrackerError {
MFCSERVER_E_NO_UBOUND = 46080,
MFCSERVER_E_NO_LBOUND = 46081,
MFCSERVER_E_NO_ARRAYLOCK = 46082,
MFCSERVER_E_NO_FILE = 46083,
MFCSERVER_E_BAD_ARRAY_PARAMETER = 46084,
MFCSERVER_E_INVALID_VALUE = 46085 }TRACKERERROR;
Po zdefiniowaniu kodów wyjątków i przygotowaniu pliku ODL do wskazania wartości błędów w bibliotece typów następnym krokiem jest dopisanie we wszystkich odpowiednich miejscach plików źródłowych serwera kodu generującego wyjątki. W przypadku naszego programu, zmodyfikowany kod programu został przedstawiony na listingu 32.4 (zawarto na nim tylko te funkcje, które uległy zmianie).
Listing 32.4. Funkcje serwera po dodaniu do nich kodu zgłaszającego wyjątki__
________
////////////////////////////////////////////////// // CTracker message handlers

BOOL CTracker::OutputLines(VARIANT FAR* varOutputArray,
const VARIANT FAR& varIndent) {
BOOL bResult = VARIANT_TRUE;
// jeśli mamy plik i jeśli wariant zawiera tablicę łańcucha if(m_fileLog && varOutputArray->vt == (VT_ARRAY l VT_BSTR))
{
// blokujemy tablicę, aby móc jej użyć
if(::SafeArrayLock(varOutputArray->parray) == S_OK)
{
LONG ILBound;

// pobieramy dolny indeks tablicy if(::SafeArrayGetLBound(varOutputArray->parray, 1, &1LBound) == S_OK)
{
LONG1UBound;

// pobieramy ilość elementów w tablicy if(::SafeArrayGetUBound(varOutputArray->parray, 1, &1UBound) == S_OK)
{
CString cstrlndent; CTime oTimeStamp; BSTR bstrTemp;

// jeśli mamy parametr wcięcia if(varlndent,vt != VT_I4)
{
// pobieramy wariant, który możemy użyć // w celu konwersji VARIANT varConvertedValue;

// inicjalizujemy wariant
::VariantInit(&varConvertedValue);
// sprawdzamy, czy możemy zamienić dane // na coś użytecznego; możemy użyć także // VariantChangeTypeEx() if(S_OK ==
::YariantChangeType(&varConvertedValue, (VARIANT *) &varlndent, O, VT_I4)) // przypisujemy wartość naszej // zmiennej składowej m llndent = varConvertedValue.lVal;
}
else
// przypisujemy wartość naszej // zmiennej składowej m_lIndent = varIndent.lVal;

// jeśli musimy wciąć tekst for(long HndentCount = 0;
HndentCountHndentCount++)
// dodajemy tabulator do łańcucha
cstrlndent += _T("\t");

// dla każdego z elementów w tablicy for(long lArrayCount = 1LBound;
1ArrayCount < (lUBound+1LBound);
1ArrayCount++)
{
// aktualizujemy czas
oTimeStamp = CTime::GetCurrentTime();
m_lHiResTime = timeGetTime();
// pobieramy daną z tablicy if(::SafeArrayGetElement(
varOutputArray->parray,
&1ArrayCount,
sbstrTemp) == S_OK)
{
// wypisujemy dane
fprintf(m_fileLog,
_T("%s(%101d)-%s%ls\n"), (LPCTSTR) oTimeStamp.Format("%H:%M:%S' m_lHiResTime - m_lLastHiResTime, (LPCTSTR) cstrlndent, bstrTemp);
// zachowujemy ostatnią // wartość timera m_lLastHiResTime = m_lHiResTime;
// zwalniamy bstr
::SysFreeString(bstrTemp);
}
}
}
else {
bResult = VARIANT_FALSE;

// nie możemy pobrać rekordu na podstawie // instrukcji SQL, więc zgłaszamy wyjątek C01eDispatchException * p01eDispExcep = new C01eDispatchException(_T(""), NULL, 0)
// formatujemy kod błędu p01eDispExcep->m_scError =
MAKE_SCODE(SEVERITY_ERROR,
FACILITY_ITF, MFCSERVER_E_NO_UBOUND) ; // ustawiamy plik źródłowy p01eDispExcep->m_strSource = __FILE__;
// formatujemy opis błędu p01eDispExcep->m_strDescription =
_T("Nie powiodło się pobranie górnego' " indeksu tablicy.");
// zgłaszamy wyjątek OLE throw(p01eDispExcep);
}
}
else{
bResult = VARIANT_FALSE;
// nie możemy pobrać rekordu na podstawie // instrukcji SQL, więc zgłaszamy wyjątek COleDispatchException * p01eDispExcep =
new C01eDispatchException(_T(""), NULL, 0}
// formatujemy kod błędu pOleDispExcep->m_scError =
MAKE_SCODE(SEVERITY_ERROR,
FACILITY_ITF, MFCSERVER_E_NO_LBOUND); // ustawiamy plik źródłowy p01eDispExcep->m_strSource = _FILE_; // formatujemy opis błędu pOleDispExcep->m_strDescription =
_T("Nie powiodło się pobranie dolnego" " indeksu tablicy.");
// zgłaszamy wyjątek OLE throw(p01eDispExcep);
}
// odblokowujemy tablicę, gdyż jej
// już nie potrzebujemy
::SafeArrayUnlock(varOutputArray->parray)
}
else
{
bResult = VARIANT_FALSE;
// nie możemy pobrać rekordu na podstawie // instrukcji SQL, więc zgłaszamy wyjątek C01eDispatchException * p01eDispExcep =
new C01eDispatchException(_T(""), NULL, 0) ;
// formatujemy kod błędu p01eDispExcep->m_scError =
MAKE_SCODE(SEVERITY_ERROR,
FACILITY_ITF, MFCSERVER_E_NO_ARRAYLOCK); // ustawiamy plik źródłowy p01eDispExcep->m_strSource = _FILE_; // formatujemy opis błędu p01eDispExcep->m_strDescription =
_T("Nie powiodło się zablokowanie pamięci' " tablicy.");
// zgłaszamy wyjątek OLE throw(p01eDispExcep);
}
}
else {
bResult = VARIANT_FALSE;
// nie możemy pobrać rekordu na podstawie instrukcji SQL, // więc zgłaszamy wyjątek C01eDispatchException * p01eDispExcep =
new C01eDispatchException(_T("") , NULL, 0);
// jeśli nie mamy uchwytu pliki if(!m_fileLog)
// formatujemy kod błędu p01eDispExcep->m_scError =
MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF, MFCSERVER_E_NO_FILE) ; else
// formatujemy kod błędu p01eDispExcep->m_scError = MAKE_SCODE(SEVERITY_ERROR, FACILITY_ITF,
MFCSERVER_E_BAD_ARRAY_PARAMETER); // ustawiamy plik źródłowy p01eDispExcep->m_strSource = _FILE_; // jeśli nie mamy uchwytu pliki if(!m_fileLog)
// formatujemy opis błędu
p01eDispExcep->m_strDescription = _T("Niewłaściwy" " uchwyt pliku. Nie powiodło się" " otwarcie pliku do zapisu."}; else
// formatujemy opis błędu
p01eDispExcep->m_strDescription = _T("Pierwszym " "parametrem musi być tablica znaków " "przekazana przez referencję.");
// zgłaszamy wyjątek OLE throw(p01eDispExcep);
}
// zwracamy wynik return bResult;
}
void CTracker::Setlndent(long nNewValue) {
// jeśli nową wartością jest przynajmniej O if(nNewValue >= 0)
// przypisujemy wartość naszej zmiennej składowej m llndent = nNewValue;
else
{
// nie możemy pobrać rekodu na podstawie instrukcji SQL, // więc zgłaszamy wyjątek
C01eDispatchException * p01eDispExcep = new C01eDispatchException(_T(""), NULL, 0);
// formatujemy kod błędu
p01eDispExcep->m_scError = MAKE_SCODE(SEVERITY_ERROR,
FACILITY_ITF,
MFCSERVER_E_INVALID_VALUE); // ustawiamy plik źródłowy p01eDispExcep->m_strSource = _FILE_; // formatujemy opis błędu
p01eDispExcep->m_strDescription = _T("Błędna wartość." " Musi być większa lub równa 0.");
// zgłaszamy wyjątek OLE throw(p01eDispExcep);
}
}
Jak widać, zamiast zwracać po prostu wartość VARIANT_FALSE (lub, co gorsza, zupełnie ignorować błąd), w tym momencie generujemy zrozumiałe kody i opisy błędów, informujące programistę o przyczynie niepowodzenia. Kod generujący wyjątki jest całkiem prosty. Najpierw tworzymy wskaźnik do obiektu coieDispatchException i wypełniamy jego zmienne składowe odpowiednimi informacjami dotyczącymi błędu (informacje o innych rodzajach wyjątków znajdziesz w dokumentacji Visual C++). W naszej implementacji ustawiamy po prostu kod błędu, nazwę pliku generującego wyjątek oraz opis błędu. Możesz także dostarczyć nazwę pliku pomocy odraz identyfikator tematu w tym pliku w celu pełniejszego wyjaśnienia przyczyny błędu. Zwróć uwagę na użycie makra MAKE_SCODE w celu wygenerowania poprawnego numeru błędu SCODE dla wyjątku.
Wyjątki są przydatne dla zgłaszania błędów i problemów aplikacjom sterującym i programistom korzystającym z komponentów ActiveX. Jeśli tylko możesz, zawsze powinieneś z nich korzystać.
Serwery o podwójnym interfejsie
Serwer z podwójnym interfejsem posiada dwa interfejsy, poprzez które można się z nim komunikować. Jeden z nich, interfejs IDispatch, jest tym, z którym pracowaliśmy do tej pory. Drugi, niestandardowy interfejs, jest interfejsem, o którym jeszcze nic nie mówiliśmy. Podwójność interfejsu odnosi się do faktu, że bez względu na to, który interfejs wybierzesz, zawsze komunikujesz się z tym samym serwerem i zawsze otrzymasz tę samą odpowiedź.
Interfejs oparty na interfejsie IDispatch korzysta ze standardowego mechanizmu wywoływania metod i właściwości serwera. Gdy wywołujesz metodę serwera, przekazujesz identyfikator tej metody w celu pobrania struktury opisującej jej parametry i zwracany
typ. Te dane są pakowane i przekazywane do serwera, który je rozpakowuje i na podstawie dostarczonego identyfikatora wywołuje odpowiednią metodę. Z drugiej strony, niestandardowy interfejs jest zupełnie inny. Używając takiego interfejsu, odwołujesz się bezpośrednio do funkcji serwera, niezależnie od standardowego mechanizmu wywoływania metod i właściwości. Pakowanie parametrów i zwracanych wartości spoczywa na barkach kompilatora, który zbudował aplikację.
Ponieważ interfejsy są pisane w celu umożliwienia dostępu do tego samego zestawu funkcji, niestandardowy interfejs serwera musi spełniać wymagania co do typów danych, narzucane przez automatyzację. W ten sposób nie musisz tworzyć własnego kodu do przekazywania danych pomiędzy dwoma aplikacjami, sterownikiem i serwerem. Standardowym szeregowaniem parametrów zajmie się OLE.
Główną zaletą podwójnego interfejsu jest wydajność. Ilość operacji związanych z wywołaniem metody poprzez niestandardowy interfejs jest dużo mniejsza niż w przypadku wywoływania metod poprzez interfejs IDispatch. Główną wadą obsługi podwójnego interfejsu jest fakt, że serwery tego typu stworzone w MFC nie posiadają wsparcia ze strony ClassWizarda i wymagaj ą ręcznych modyfikacji kodu. Jednak już po stworzeniu niestandardowego interfejsu kod związany z jego obsługą nie jest zbyt skomplikowany.
Zaleta wynikająca z korzystania z niestandardowego interfejsu w podwójnym interfejsie staje się dużo mniej znacząca w momencie, gdy działanie programu przekracza granice pomiędzy procesami. Rzeczywisty wzrost wydajności, od 25 do 50 procent, występuje tylko wtedy, gdy serwer jest komponentem in-process w stosunku do aplikacji wywołującej. Wzrost wydajności zależy od ilości i typu parametrów przekazywanych pomiędzy aplikacjami. Jeśli rzeczywiście chcesz sprawdzić liczby określające przyrost wydajności, zajrzyj do archiwów Microsoft Systems Journal (na stronie WWW Microsoftu, http:/ /www.microsoft.com), zawierających kilka artykułów poświęconych konkretnie różnicom w wydajności pomiędzy interfejsami IDispatch a interfejsami niestandardowymi.
Pierwszym krokiem podczas zamiany opartego na MFC serwera ActiveX na serwer z podwójnym interfejsem jest modyfikacja pliku ODL. Nie ma potrzeby generowania nowego identyfikatora UUID, gdyż funkcjonalność serwera nie uległa zmianie. Jednak do klasy interfejsu musisz dodać atrybuty oleautomation i dual. Musisz także dodać atrybut hidden, tak aby drugi interfejs nie był widoczny w Visual Basicu. To działa, gdyż Yisual Basic wyświetla interfejs CoClass oraz pokazuje wszystkie metody i właściwości serwera. Jako ogólną zasadę powinieneś przyjąć, że będziesz ukrywał interfejsy i pozostawiał widoczne klasy CoClass używane przez sterownik do ich tworzenia. Jest to konieczne, gdyż aplikacje takie jak Visual Basic wyświetlają oba interfejsy, choć w rzeczywistości w Visual Basicu poprawna jest tylko nazwa CoClass. Innymi słowy, jeśli spróbujesz odwołać się do obiektu poprzez nazwę jego interfejsu, otrzymasz błąd. Konieczne zmiany w pliku ODL w celu uaktywnienia obsługi podwójnego interfejsu przedstawiono na listingu 32.5.
Listing 32.5. Zmiany, jakich trzeba dokonać w pliku ODL w celu uaktywnienia obsługi _________podwójnego interfejsu_______________________________
[ uuid(llC82943-4EDD-llDO-BED8-00400538977D) , version(1.0) ]
library MFCServer
{
importlib("stdole32.tlb") ;
[ uuid(llC82946-4EDD-llDO-BED8-00400538977D), oleautomation,
dual, hidden ]
interface ITracker: IDispatch {
[id(l), propget] HRESULT Indent([out,
retval] long * Value);
[id(l), propput] HRESULT Indent([in] long Yalue); [id(2)] HRESULT OutputLines(
[in] VARIANT * varOutputArray, [in, optional] VARIANT varlndent, [out, retval] VARIANT BOOL * RetVal);
};
// CoClass for CTracker

[ uuid(HC82947-4EDD-llDO-BED8-00400538977D)
coclass TRACKER
{
[default] interface ITracker;
};
typedef [uuid(11C82948-4EDD-11DO-BED8-00400538977D) , helpstring("Stałe błędów serwera Tracker")] #include "trackererror.h"

//{{AFX APPEND ODL}}
};
W celu obsługi podwójnego interfejsu nie trzeba zmieniać pozostałych pozycji pliku ODL. Jednak ponieważ serwer obsługuje podwójny interfejs, musimy zmienić deklarację interfejsu tak, aby był dziedziczony z interfejsu IDispatch, a nie zadeklarowany jako typ dispinterface, tak jak w oryginalnej implementacji. Deklaracje metod i właściwości przy podwójnym interfejsie różnią się od deklaracji w dispinterface, który jest bardziej podobnym do standardowego C++. Zwróć uwagę, że w deklaracji interfejsu nie stosujemy już stów kluczowych takich jak method czy properties. Te słowa kluczowe odnoszą się do słowa kluczowego dispinterface. Jak już wspominaliśmy, właściwości są dostępne poprzez parę metod korzystających z tego samego identyfikatora dispid. Czynnikami rozróżniającymi są atrybuty metod propget i propput, wyznaczające kierunek przepływu danych. Oprócz tego, musisz zmienić także CoClass tak, aby odnosiła się do interface, a nie do dispinterface.
Wszystkie metody podwójnego interfejsu muszą zwracać wartość typu HRESULT. Jeśli metoda wymaga zwrócenia wartości, musi ona być określona jako ostatni parametr metody i musi posiadać atrybuty out oraz retval. Wszystkie parametry muszą posiadać atrybuty określające kierunek przepływu danych. Tabela 32.3 zawiera pełny opis dostępnych atrybutów i ich kombinacji.
Zmiany dokonane w pliku ODL uniemożliwiają ClassWizardowi automatyczną aktualizację tego pliku w momencie dodawania do serwera nowych metod i właściwości. Od tego momentu musisz samodzielnie aktualizować pozycje w tym pliku.
Tabela 32.3. Atrybuty kierunku przekazywania danych w parametrach
Atrybut
Opis
in
Parametr jest przekazywany od aplikacji wywołującej do serwera. out
Parametr jest przekazywany od serwera do aplikacji wywołującej.
in, out
Parametr jest przekazywany od aplikacji wywołującej do serwera, zaś serwer zwraca wartość w parametrze.
out, retval
Parametr jest wartością zwracaną przez metodę i jest przekazywany od serwera do aplikacji wywołującej.

Kompilator ODL ma możliwość generowania plików nagłówkowych C++ opisujących wszystkie interfejsy i wyliczenia zawarte w bibliotece typów stworzonej dla serwera. Plik nagłówkowy wygenerowany przez kompilator ODL jest przydatny do tworzenia prototypów funkcji, wymaganych w implementacji serwera. Dodajesz pozycję do pliku ODL i kompilujesz ją do biblioteki typów. Następnie kopiujesz nową metodę z pliku nagłówkowego do definicji klasy, wprowadzasz pewne niewielkie zmiany i gotowe. Dodatkowo, w ten sposób posiadasz także plik interfejsu, który może być używany przez inne aplikacje w celu dostępu do niestandardowego interfejsu w Twoim serwerze, a także wyliczenia używane podczas dostępu do jego metod i właściwości.
Aby wygenerować plik nagłówkowy, musisz zaktualizować ustawienia projektu. W tym celu w menu Project wybierz polecenie Settings. Pojawi się okno dialogowe Project Settings. Z rozwijanej listy Settings For wybierz pozycję Ali Configurations. Rozwiń węzeł projektu MFCServer oraz węzeł Source Files, po czym zaznacz plik MFCServer.odl. Kliknij zakładkę MIDL i w polu edycji Output header file wpisz Trackerinterface .h. Za każdym razem, gdy będzie kompilowana biblioteka typów, kompilator odtworzy plik Trackerinterface.h, odzwierciedlając zmiany w implementacji. Rysunek 32.5 przedstawia okno dialogowe Project Settings po dokonaniu odpowiednich zmian.
Po dokonaniu zmian w projekcie musisz dołączyć nowy plik nagłówkowy do pliku źródłowego Tracker.cpp. Oprócz tego musisz usunąć dyrektywę włączającą plik TrackerError.h, gdyż stworzone wcześniej wyliczenie typów błędów także jest już zawarte w pliku nagłówkowym wygenerowanym przez kompilator ODL. Plik Trackerlnterface.h musi być włączony przed plikiem Tracker.h, gdyż klasa crracker jest zależna od informacji zawartych w pliku Trackerlnterface.h. Innymi słowy, dyrektywy włączające powinny w pliku Tracker.cpp wystąpić w następującej kolejności:

#include "MFCServer. h"
// plik wygenerowany przez kompilator ODL
#include "trackerinterface .h"
#include "Tracker.h"
// potrzebne dla korzystania z timera multimedialnego
#include

FC definiuje zestaw makr dla opisu interfejsów w kontekście implementacji komponentu MFC. Makro interfejsu definiuje interfejs, jego nazwę oraz zawarte w nim metody. W celu opisania niestandardowej części interfejsu serwera musisz dodać do definicji klasy deklaracje interfejsu MFC. Możesz je dodać w dowolnym miejscu po makrze DECLARE_INTERFACE_MAP. Deklaracja interfejsu w pliku Tracker.h została przedstawiona na listingu 32.6.
Listing 32.6. Deklaracja interfejsu w pliku Tracker.h

// ..._________________________
// potrzebne przy obsłudze podwójnego interfejsu BEGIN_INTERFACE_PART (SubDispatch, ITracker)
STDMETHOD(GetTypelnfoCount) (THIS_ UINT FAR* pctinfo) ; STDMETHOD(GetTypelnfo) (THIS_ UINT itinfo, LCID Icid,
ITypelnfo FAR* FAR* pptinfo) ; STDMETHOD(GetlDsOfNames) (THIS_ REFIID riid,
OLECHAR FAR* FAR* rgszNames, UINT cNames, LCID Icid, DISPID FAR* rgdispid) ;
STDMETHOD(Invoke) (THIS_ DISPID dispidMember, REFIID riid, LCID Icid, WORD wFlags, DISPPARAMS FAR* pdispparams, VARIANT FAR* pvarResult, EXCEPINFO FAR* pexcepinfo, UINT FAR* puArgErr) ; virtual /* [propget] [id] */ HRESULT STDMETHODCALLTYPE
get_Indent (/* [retval] [out] */ long _ RPC_FAR *Value) virtual /* [propput] [id] */ HRESULT STDMETHODCALLTYPE
put_Indent (/* [in] */ long Value) ; virtual /* [id] */ HRESULT STDMETHODCALLTYPE OutputLines (
/* [in] */ YARIANT _ RPC_FAR *varOutputArray, /* [optional] [in] */ VARIANT varlndent, /* [retval] [out] */ VARIANT_BOOL _ RPC_FAR *RetVal); END_INTERFACE_PART (SubDispatch)

// ...
Pierwszy parametr makra BEGIN_INTERFACE_PART, SubDispatch, jest nazwą używaną do tworzenia zagnieżdżonej klasy wewnątrz definicji klasy serwera. Drugim parametrem jest nazwa interfejsu, z którego zagnieżdżona klasa jest dziedziczona. Interfejs iTracker jest zadeklarowany w pliku nagłówkowym Trackerlnterface.h, stworzonym na podstawie pliku ODL. Deklaracja ITracker w pliku nagłówkowym zawiera zestaw czystych funkcji wirtualnych, które muszą być skopiowane do deklaracji interfejsu. Podczas kopiowania funkcji pamiętaj, by usunąć = O z końców deklaracji, gdyż właśnie tam będziesz je implementował.
Plik nagłówkowy zawiera deklarację interfejsu, zaś plik źródłowy zawiera jego implementację. MFC AppWizard stworzył domyślną implementację dla tworzonego przez siebie serwera. Oryginalna definicja kierowała wywołania do domyślnego interfejsu IDispatch zdefiniowanego w MFC. Ponieważ implementujemy interfejs dziedziczony z interfejsu IDispatch, konieczne jest skierowanie wszystkich wywołań IDispatch do nowego interfejsu. Mapa interfejsu znajdująca się w pliku źródłowym serwera musi zostać zmieniona w celu odzwierciedlenia zawartości serwera, jaką zadeklarowaliśmy. Zmienimy interfejs z IDispatch na nazwę interfejsu zadeklarowanego w pliku nagłówkowym, w tym przypadku SubDispatch. Zmiana nazwy interfejsu powoduje w efekcie skierowanie wszystkich wywołań interfejsu IDispatch najpierw do naszej implementacji interfejsu, które możemy obsłużyć sami lub przekazać domyślnej implementacji. Interfejs ITracker w pliku Tracker.cpp został zaimplementowany następująco:

BEGIN_INTERFACE_MAP(CTracker, CCmdTarget)
INTERFACE_PART(CTracker, IID_IDispatch, SubDispatch) INTERFACE_PART(CTracker, IID_ITracker, SubDispatch)
END_INTERFACE_MAP()

Niestety, MFC nie pozwala na prawdziwe, takie jak w C++, dziedziczenie posiadanych przez siebie interfejsów COM, konieczne jest więc skierowanie komunikatów dla konkretnego interfejsu COM do odpowiednich funkcji obsługi w serwerze. Kierowanie komunikatów odbywa się za pomocą makra BEGIN_MESSAGE_MAP. Ponieważ nasza implementacja obsługuje zarówno interfejs IDispatch, jak i interfejs niestandardowy, musimy dodać dwie pozycje do mapy komunikatów (po jednej dla każdego z interfejsów). W naszym przypadku skierujemy wszystkie komunikaty IDispatch do funkcji interfejsu niestandardowego, podobnie jak wszystkie komunikaty interfejsu niestandardowego.
Ostatnim krokiem w dodawaniu do serwera obsługi podwójnego interfejsu jest dodanie implementacji funkcji zadeklarowanych w tym interfejsie. Pierwszy zestaw funkcji do zaimplementowania to podstawowe funkcje IDispatch, jakie serwer odziedziczył od klasy IDispatch. We wszystkich przypadkach skorzystamy z implementacji metody w klasie bazowej. Możemy zaimplementować te funkcje samodzielnie lub wykorzystać w tym celu MFC. Ważne jest jednak, by pamiętać, że metoda musi wywoływać tylko podstawowe funkcje IDispatch. Ponieważ serwer wychwytuje wszystkie komunikaty IDispatch i kieruje je do niestandardowej implementacji, do pobierania wskaźnika do interfejsu IDispatch serwera nie możesz skorzystać z funkcji GetiDispatch () (gdyż w ten sposób spowodowałbyś rekursywne wywołania, ponieważ funkcje IDispatch są kierowane do naszej implementacji serwera). Zamiast tego implementacja musi wywoływać funkcje IDispatch bezpośrednio (w postaci ( (IDispatch*) &pThis->m_xDispatch) ->), pomijając funkcje kierowania komunikatów i unikając problemu rekursji. Nasze funkcje możemy zaimplementować tak jak na listingu 32.7.
Listing 32.7. Funkcje obsługi dla interfejsu IDispatch
//////////////////////////////////////////////////////////////////
// CTracker Standard IDispatch Dual Interface Handlers ULONG FAR EXPORT CTracker::XSubDispatch::AddRef()
{
METHOD_PROLOGUE(CTracker, SubDispatch) return pThis->ExternalAddRef();
}
ULONG FAR EXPORT CTracker::XSubDispatch::Release()
{
METHOD_PROLOGUE(CTracker, SubDispatch) return pThis->ExternalRelease();
}
HRESULT FAR EXPORT CTracker::KSubDispatch::
Cjuerylnterface (REFIID riid, LPYOID FAR* ppvObj )
{
METHOD_PROLOGUE(CTracker, SubDispatch) return (HRESULT) pThis->
ExternalQueryInterface(&riid, ppvObj);
}
HRESULT FAR EKPORT CTracker::KSubDispatch:: GetTypelnfoCount(UINT FAR* pctinfo)
{
METHOD_PROLOGUE(CTracker, SubDispatch) return ((IDispatch*)&pThis->m_xDispatch)-> GetTypelnfoCount(pctinfo);
}
HRESULT FAR EXPORT CTracker::XSubDispatch:: GetTypeInfo(UINT itinfo, LCID Icid, ITypelnfo FAR* FAR* pptinfo)
{
METHOD_PROLOGUE(CTracker, SubDispatch) return ((IDispatch*)&pThis->m_xDispatch)-> GetTypelnfo(itinfo, Icid, pptinfo);
}
HRESULT FAR EXPORT CTracker::KSubDispatch::
GetIDsOfNames(REFIID riid, OLECHAR FAR* FAR* rgszNames, UINT cNames, LCID Icid, DISPID FAR* rgdispid)
{
METHOD_PROLOGUE(CTracker, SubDispatch) return ((IDispatch*)&pThis->m_xDispatch)->
GetIDsOfNames(riid, rgszNames, cNames, Icid, rgdispid);
}
HRESULT FAR EXPORT CTracker::XSubDispatch::
Invoke(DISPID dispidMember, REFIID riid, LCID Icid, WORD wFlags, DISPPARAMS FAR* pdispparams, YARIANT FAR* pvarResult, EXCEPINFO FAR* pexcepinfo, UINT FAR* puArgErr)
{
METHOD_PROLOGUE(CTracker, SubDispatch) return ((IDispatch*)&pThis->m_xDispatch)->
Invoke(dispidMember, riid, Icid, wFlags, pdispparams, pvarResult, pexcepinfo, puArgErr);
}
Powinieneś zwrócić uwagę, że zagnieżdżoną klasę SubDispatch musieliśmy zadeklarować w implementacji jako KSubDispatch. x pochodzi od makr BEGIN_INTERFACE_PART i nie ma jakiegoś szczególnego znaczenia. To samo odnosi się do zmiennej składowej
m_xDispatch.
Zwróć uwagę na użycie makra METHOD_PROLOGUE, wymaganego w implementacji opartej na MFC w celu zapewnienia, że MFC będzie we właściwym stanie w momencie wykonywania funkcji.
Ostatnim krokiem w budowaniu serwera z podwójnym interfejsem jest zaimplemento-wanie funkcji specyficznych dla serwera. Możesz uprościć implementację, wywołując oryginalne funkcje MFC z implementacji podwójnego interfejsu, zamiast tworzyć je całkowicie od nowa. W ten sposób rozwiążesz kilka problemów. Po pierwsze, będziesz mógł oprzeć się na funkcjach odwzorowania komunikatów MFC przy wywoływaniu metod podczas wywoływania ich poprzez interfejs IDispatch, więc nie musisz tworzyć własnego kodu IDispatch. Obsługa oryginalnej implementacji MFC zapobiega także konieczności ewentualnej zmiany wewnętrznego kodu, który może być zależny od już istniejących funkcji. Jak przekonasz się w dalszej części rozdziału, dodanie obsługi błędów do kodu serwera z podwójnym interfejsem jest dużo prostsze, gdy zastosujesz tę metodę. Funkcje interfejsu iTracker w pliku Tracker.cpp zaimplementowaliśmy tak jak na listingu 32.8.
Listing 32.8. Implementacja funkcji interfejsu ITracker w pliku Tracker.cpp
////////////////////////////////////////////////////////////
// CTracker interface handlers
HRESULT CTracker :: KSubDispatch : :get_Indent (LONG * Indent) {
METHOD_PROLOGUE (CTracker, SubDispatch)
HRESULT hResult = S_OK;
*Indent = pThis->GetIndent ( ) ;
return hResult;
}
HRESULT CTracker: : KSubDispatch : :put_Indent (LONG Indent) {
METHOD_PROLOGUE (CTracker, SubDispatch)
HRESULT hResult = S_OK;
pThis->Set!ndent (Indent) ;
return hResult;
}
HRESULT FAR EKPORT CTracker :: KSubDispatch :: OutputLines (VARIANT FAR* varOutputArray,
VARIANT varlndent, VARIANT BOOL FAR* RetVai;
{
METHOD_PROLOGUE(CTracker, SubDispatch)
HRESULT hResult = S_OK;
*RetVal = pThis->OutputLines(varOutputArray, varlndent);
return hResult;
}
Po skompilowaniu serwera i zarejestrowaniu go jesteśmy gotowi do przetestowania funkcjonowania nowego podwójnego interfejsu. W C++ możesz tworzyć serwer tak jak poprzednio, lecz tym razem możesz odwołać się do niestandardowego interfejsu, używając funkcji Queryinterface o, podając właściwy identyfikator IID interfejsu. W Visual Basicu musisz jedynie dodać bibliotekę typów serwera do listy odwołań oraz zmienić nazwę zmiennej na nazwę występującą w oknie dialogowym Object References. Zamiast metody CreateObject () możesz w VIisual Basicu użyć także operatora New. Innymi słowy, teraz poniższy kod programu Visual Basica zostanie wykonany poprawnie:

Dim MyTracker as TRACKER Set MyTracker = new TRACKER

W C++ wywołujesz funkcję Oueryinterface (), przekazując jej identyfikator interfejsu HD_ITracker. Ostatnim krokiem w konwersji na serwer o podwójnym interfejsie jest poprawna obsługa błędów. Należy przyjąć zasadę, że serwer nie może zgłaszać wyjątków C++ z wnętrza implementacji interfejsu niestandardowego w podwójnym interfejsie. Konkretnie, w przypadku podwójnego interfejsu nie może zgłaszać wyjątków z żadnego z interfejsów. MFC bierze na siebie wychwytywanie wyjątków C++ i konwertowanie ich na wyjątki OLE zrozumiałe dla OLE. Ponieważ w naszym serwerze obsługujemy podwójny interfejs, musimy uczynić to samo - tj. dokonać konwersji wyjątków C++ na wyjątki OLE, które serwer może przekazać programowi wywołującemu poprzez dowolny z interfejsów.
Generowanie wyjątków OLE podwójnego interfejsu
Reguły obsługi błędów przy podwójnym interfejsie w przypadku obsługi błędów interfejsu niestandardowego różnią się nieco od reguł poznanych wcześniej. Jak dowiedziałeś się we wcześniejszej części rozdziału, musisz zmodyfikować wszystkie metody interfejsu niestandardowego tak, aby zwracały wartość typu HRESULT w miejsce normalnie zwracanych wartości. Typ HRESULT jest używany do wskazania, czy w kontekście wywoływanej metody wystąpił błąd lub wyjątek. Gdy sterownik automatyzacji wywołuje metodę w interfejsie niestandardowym, powinien sprawdzać, czy zwrócona wartość jest równa s_OK. Jeśli nie, sterownik może sprawdzić, czy serwer obsługuje rozszerzone informacje o błędzie poprzez interfejs iSupportErrorinfo. Gdy serwer obsługujący interfejs iSupportErrorinfo zgłasza błąd, robi to poprzez stworzenie obiektu lErrorinfo zawierającego informacje o błędzie.
MFC nie obsługuje interfejsu ISupportErrorinfo domyślnie, musisz więc dodać go samodzielnie. Dodając ten interfejs do swojego serwera, możesz skorzystać z zestawu pomocniczych makr. Te makra są oparte na makrach zdefiniowanych w przykładzie ACDUAL dostarczanym wraz z MFC i możesz ich użyć w celu uproszczenia sobie zadania implementacji interfejsu ISupportErrorinfo. W naszym przykładzie zaimplemento-waliśmy te makra w pliku ErrorlnfoMacros.h, przedstawionym na listingu 32.9.
Listing 32.9. Makra w pliku ErrorlnfoMacros.h
// ten kod jest oparty po części na przykładzie
// ACDUAL MFC\OLE dostarczanym wraz z Visuał C++ 5.0
////////////////////////////////////////////
// DECLARE_DUAL_ERRORINFO jest rozwijane w celu zadeklarowania
// klasy ISupportErrorlnfo . Działa razem z makrami
// DUAL_ERRORINFO_PART oraz DECLARE_DUAL_ERRORINFO
// zdefiniowanymi poniżej.
ttdefine DECLARE_DUAL_ERRORINFO ( ) \
BEGIN_INTERFACE_PART (SupportErrorlnfo, ISupportErrorlnfo) \ STDMETHOD(InterfaceSupportsErrorlnfo) ( \
THIS_ REFIID riid); \ END_INTERFACE_PART (SupportErrorlnfo) \
///////
// DUAL_ERRORINFO_PART dodaje odpowiednią pozycję do mapy
// interfejsu dla ISupportErrorlnfo, jeśli użyte zostało makro
// DECLARE_DUAL_ERRORINFO.
#define DUAL_ERRORINFO_PART (objectClass) \
INTERFACE_PART (objectClass, IID_ISupportErrorInf o, \ SupportErrorlnfo) \
////////////////////////////////
// IMPLEMENT_DUAL_ERRORINFO jest rozwijane do implementacji // ISupportErrorlnfo odpowiadającej deklaracji w makrze // DECLARE_DUAL_ERRORINFO.
#define IMPLEMENT_DUAL_ERRORINFO (objectClass, riidSource) \ STDMETHODIMP_(ULONG) \
objectClass: :XSupportErrorInfo: :AddRef () \
{\
METHOD_PROLOGUE (objectClass, SupportErrorlnfo) \
return pThis->ExternalAddRef()\
}\
STDMETHODIMP_(ULONG) objectClass: : KSupportErrorlnfo : :
ReleaseO \
{\
METHOD_PROLOGUE (objectClass, SupportErrorlnfo) \ return pThis->ExternalRelease ( ) ; \
}\
STDMETHODIMP obj ectClass : : KSupportErrorlnf o : : \ Ouerylnterf ace ( \ REFIID iid, LPVOID* ppvObj ) \
{ \
METHOD_PROLOGUE(objectClass, SupportErrorlnfo) \ return pThis->Externa!Query!nterface(&iid, ppvObj); \
} \
STDMETHODIMP objectClass::KSupportErrorlnfo:: \ InterfaceSupportsErrorlnfo( \ REFIID iid) \
{ \
METHOD_PROLOGUE(objectClass, SupportErrorlnfo) \ return (iid == riidSource) ? S OK : S FALSE; \
}
Po stworzeniu pliku ErrorlnfoMacros.h dołącz go do pliku Tracker.cpp. Pamiętaj, by dołączyć ten plik prze d plikiem nagłówkowym Trackerlnterface.h.
Oprócz tego, musisz dodać makro oraz funkcję pomocniczą do deklaracji klasy serwera. Makro deklaruje interfejs iSupportErrorinfo, zaś funkcja pomocnicza służy do translacji wyjątku do obiektu lErrorinfo. Zmiany w pliku Tracker.h możesz zaimplementować następująco:
// ...
DECLARE_OLECREATE (CTracker)
// dodajemy deklarację implementacji ISupportErrorinfo // aby wskazać, że obsługujemy obiekt błędu automatyzacji OLE DECLARE_DUAL_ERRORINFO ( )
HRESULT CreateErrorlnfo (CException * pAnyException,
REFIID riidSource) ;
// potrzebne przy obsłudze podwójnego interfejsu BEGIN_INTERFACE_PART (SubDispatch, ITracker)
// ...
Gdy stworzyliśmy deklarację interfejsu w pliku nagłówkowym, powinniśmy dodać interfejs do mapy interfejsów. Oprócz tego musimy dodać do interfejsu makro implementacji oraz implementację funkcji pomocniczej. Zmiany w pliku Tracker.cpp możesz zaimplementować tak jak na listingu 32.10.
Listing 32.10. Zmiany w pliku Tracker.cpp
BEGIN_INTERFACE_MAP(CTracker, CCmdTarget) INTERFACE_PART(CTracker, IID_ITracker, DUAL_ERRORINFO_PART(CTracker)
END INTERFACE MAP()
SubDispatch)
IMPLEMENT_OLECREATE(CTracker, _T("MFCServer.Tracker"),
OxllC82947, Ox4edd, OxlldO, Oxbe, Oxd8, 0x0, 0x40, 0x5, 0x38, 0x97, Ox7d)
// Implementujemy ISupportErrorinfo w celu wskazania, że // posiadamy funkcję obsługi błędów automatyzacji OLE IMPLEMENT_DUAL_ERRORINFO(CTracker, IID_ITracker)
// ten kod jest oparty na przykładowej aplikacji ACDUAL MFC/OLE
// dostarczanej wraz z Visual C++.
HRESULT CTracker::CreateErrorlnfo(CException * pAnyException,
REFIID riidSource) {
ASSERT_VALID(pAnyException);
// tworzymy obiekt informacji o błędzie
ICreateErrorlnfo * pcerrinfo;
HRESULT hr = ::CreateErrorlnfo(&pcerrinfo);
//, gdy się powiodło if(SUCCEEDED(hr)) {
// rozprowadzamy wykonanie? if(pAnyException->
IsKindOf(RUNTIME_CLASS(C01eDispatchException))) {
// wyjątek specyficzny dla IDispatch C01eDispatchException * e =
(C01eDispatchException *) pAnyException;
// zwracanej wartości przypisujemy wartość błędu hr = e->m_scError;
// Ustawiamy obiekt Errlnfo pcerrinfo->SetGUID(riidsource) ; pcerrinfo->SetDescription(e->
m_strDescription.AllocSysString()); pcerrinfo->SetHelpContext(e->m_dwHelpContext); pcerrinfo->SetHelpFile(e->
m_strHelpFile.AllocSysString()); pcerrinfo->SetSource(e->
m_strSource.AllocSysString()); } else if (pAnyException->
IsKindOf(RUNTIME_CLASS(CMemoryException))) {
// nie powiodła się alokacja pamięci hr = EJDUTOFMEMORY;
// Ustawiamy obiekt Errlnfo pcerrinfo->SetGUID(riidSource); CString cstrFileName(AfxGetAppName() ) ; pcerrinfo->SetSource(cstrFileName.AllocSysString());
}
else {
// inny nieznany błąd hr = E_UNEXPECTED;
// Ustawiamy obiekt Errlnfo pcerrinfo->SetGUID(riidSource); CString cstrFileName(AfxGetAppName()); pcerrinfo->SetSource(cstrFileName.AllocSysString());
}
// QI dla interfejsu lErrorlnfo lErrorlnfo * perrinfo; if(SUCCEEDED(pcerrinfo->
Ouerylnterface(IID_IErrorInfo,
(LPYOID *) &perrinfo)))
{
// ustawiamy obiekt informacji o błędzie ::SetErrorlnfo(O, perrinfo);
// zwalniamy odwołanie perrinfo->Release();
}
// zwalniamy odwołanie pcerrinfo->Release() ;
}
// usuwamy wyjątek pAnyException->Delete() ;

// zwracamy wartość błędu return hr;
}
Funkcja CreateErrorinfoO tłumaczy wyjątki na obiekty lErrorinfo. Ta funkcja jest oparta na kodzie stanowiącym część przykładu ACDUAL dostarczanego wraz z MFC. Jej podstawowym zadaniem jest zamiana wyjątków coieDispatchException na obiekty lErrorinfo. Może jednak obsłużyć także inne przekazywane wyjątki, jednak w takich sytuacjach ilość informacji o błędzie jest dużo mniejsza. Przetłumaczony wyjątek jest ustawiany jako bieżący obiekt informacji o błędzie dla aktualnego wątku.
Ostatnim krokiem jest aktualizacja metod niestandardowego interfejsu, tak aby kierowały wszystkie wyjątki do dodanej przed chwilą pomocniczej funkcji. Dodatkowy wymagany kod jest prosty i łatwy w dodaniu. Dla każdej z metod powinieneś ująć jej wywołanie w blok try. . .catch tłumaczący każdy wyjątek na obiekt lErrorinfo i zwracający kod wyjątku aplikacji wywołującej. Dodatkowy kod w pliku Tracker.cpp został przedstawiony na listingu 32.11.
Listing 32.11. Tłumaczenie wyjątków na obiekty lErrorinfo_______________ __
//////////////////////////II// /l////////II//////////////////// // CTracker interface handlers
HRESULT CTracker::XSubDispatch::get_Indent(LONG * Indent)
{
METHOD_PROLOGUE(CTracker, SubDispatch)
HRESULT hResult = S_OK;
try
{
*Indent = pThis->GetIndent() ;
} catch(CException * pException)
{
hResult = pThis->
CreateErrorlnfo(pException, IID_ITracker);
return hResult;
}
HRESULT CTracker::KSubDispatch::put_Indent(LONG Indent)
{
METHOD_PROLOGUE(CTracker, SubDispatch)
HRESULT hResult = S_OK;
try
{
pThis->SetIndent(Indent) ;
}
catch(CException * pException) {
hResult = pThis->
CreateErrorlnfo(pException, IID_ITracker); } return hResult;
}
HRESULT FAR EKPORT CTracker::KSubDispatch::
OutputLines(VARIANT FAR* varOutputArray, VARIANT varlndent, VARIANT_BOOL FAR* RetVal)
{
METHOD_PROLOGUE(CTracker, SubDispatch)
HRESULT hResult = S_OK;
try
{
*RetVal = pThis->OutputLines(varOutputArray, varIndent);
} catch(CException * pException)
{
hResult = pThis->
CreateErrorlnfo(pException, IID_ITracker);
}
return hResult;
}
Jak dotąd, omówiliśmy wszystkie podstawowe zagadnienia związane z tworzeniem serwera ActiveX i użyciem go przez aplikacje inne niż twoja własna. Jednak mogą zdarzyć się sytuacje, w których będziesz musiał stworzyć serwer i użyć go wewnątrz aplikacji, w której jest zdefiniowany, lub Twoja aplikacja może zawierać implementację więcej niż jednego serwera, w której udostępniony jest tylko jeden serwer jako możliwy do stworzenia obiekt, zaś pozostałe serwery są tworzone tylko w odpowiedzi na wywołanie określonych metod udostępnionego serwera. W takich sytuacjach mówimy o obiektach zagnieżdżonych. Przy korzystaniu z zagnieżdżonych serwerów możesz stosować wiele technik. W pozostałej części rozdziału zajmiemy się trzema specyficznymi przypadkami związanymi z serwerami automatyzacji: tworzeniem serwera w programie C++, tworzenie współdostępnego serwera oraz tworzenia serwera jednorazowego użytku.
Tworzenie obiektu serwera w programie C++
Choć większą część tego rozdziału poświęciliśmy nauce tworzenia i korzystania z serwerów automatyzacji za pomocą MFC i OLE, jednak nie jest to jedyna metoda tworzenia i używania tych serwerów. Serwery automatyzacji można tworzyć, także korzystając ze składni języka C++.
Czasem zdarza się, że konieczne jest stworzenie i wykorzystanie serwera we wnętrzu programu, który go definiuje. Na przykład, aplikacja może zawierać trzy serwery, z których
tylko jeden jest bezpośrednio dostępny innym aplikacjom poprzez OLE. Pozostałe dwa serwery mogą być tworzone przez udostępniony serwer używający C++ i poprzez wywołanie metody zwracane, innej aplikacji, która następnie korzysta z nich tak, jakby były utworzone poprzez OLE.
Jak już wiesz, aby aplikacja mogła być utworzona przez inną aplikację poprzez OLE, serwer musi zawierać makra MFC DECLAREJDLECREATE oraz IMPLEMENT_OLECREATE. Przez usunięcie lub niedołączenie tych makr możesz określić, że inne aplikacje nie mogą tworzyć serwera z użyciem standardowych mechanizmów tworzenia serwerów OLE. Nie oznacza.to jednak niemożności utworzenia serwera z użyciem C++ i MFC. Zwróć uwagę, że w.ten sposób można tworzyć każdy serwer OLE, a nie tylko taki, którego nie da się utworzyć poprzez mechanizmy OLE. MFC wspiera tworzenie serwerów OLE poprzez użycie pomocniczej klasy CRuntimeClass.
Obiektu CRuntimeClass możesz użyć do tworzenia serwerów, których możesz użyć wewnętrznie w aplikacji, w której zdefiniowałeś te serwery, oraz zewnętrznie, jako parametr lub wartość zwracaną innej aplikacji. W celu obsłużenia tworzenia obiektów poprzez klasę CRuntimeClass klasa musi zawierać w implementacji makro IMPLEMENT_DYNAMIC, IMPLEMENT_DYNCREATE lub IMPLEMENT_SERIAL. Ten warunek jest spełniony we wszystkich klasach MFC pochodzących od klasy cobject, a nie tylko w klasach korzystających z OLE.
Poniższy kod z powodu swojej prostoty nie został zawarty w przykładowej aplikacji. W tym kodzie jako przykładowy serwer wykorzystujemy serwer CTracker. Właśnie takiego kodu powinieneś użyć, aby utworzyć serwer z użyciem klasy CRuntimeClass:

// tworzymy obiekt czasu wykonania (runtime) CTracker CRuntimeClass *pRuntimeClass = RUNTIME_CLASS(CTracker);

// Tworzymy obiekt OLE CTracker
CTracker *opTracker = (CTracker*) pRuntimeClass->CreateObject();

// Używamy obiektu zgodnie z potrzebami aplikacji
// Po zakończeniu używania obiektu niszczymy go delete opTracker;
Po utworzeniu obiektu można przekazać go także innej aplikacji. MFC posiada dwie funkcje do pobierania działającego serwera, GetiDispatchO oraz Getinterface (). Opis funkcji GetiDispatch () znajduje się w pliku pomocy Visual C++, lecz o funkcji Getinterface () wspomniano jedynie w opisach przykładów. Funkcja Getinterface () przyjmuje pojedynczy parametr - identyfikator IID żądanego interfejsu. Ta funkcja nie zwiększa licznika odwołań do zwracanego obiektu. Funkcja GetiDispatch () umożliwia określenie, czy ma zostać zwiększony licznik odwołań do obiektu.
Częste problemy
związane z tworzeniem serwerów OLE poprzez C++
Podczas tworzenia komponentów OLE z użyciem C++ zwykle natrafia się na dwa problemy: zliczanie odwołań oraz działanie serwera w przestrzeni adresowej procesu oraz poza tą przestrzenią. W przypadku zliczania odwołań problem polega na zbyt małej lub zbyt dużej liczbie zarejestrowanych odwołań do danego procesu. Problemy w zliczaniu odwołań mogą wystąpić bez względu na sposób tworzenia serwera, zarówno przy użyciu C++ jak i OLE. Zwykle problem wiąże się z użyciem serwera w relacji jeden do wielu. Wszystkie aplikacje korzystające z tego samego serwera muszą poprawnie zwiększać i zmniejszać licznik odwołań. Problemy z ilością odwołań manifestują się poprzez przedwczesne zamknięcie serwera lub poprzez pozostawianie działającego serwera, mimo że żadna aplikacja z niego nie korzysta. Pamiętaj, by wszystkie liczniki odwołań były poprawnie zwiększane i zmniejszane, zwłaszcza w przypadku korzystania z jednego serwera przez kilka aplikacji lub przekazywania tworzonego serwera innym aplikacjom.
Następny problem związany z serwerem jest dużo bardziej subtelny i łatwiej można go przeoczyć, lecz efekt tego przeoczenia może być bardzo duży. Wynika on z faktu, że nie ma gwarancji, że serwer będzie działał w przestrzeni adresowej aplikacji, która z niego korzysta, a jedynie że będzie działał w przestrzeni adresowej aplikacji, która go stworzyła.
Na przykład, aplikacja A tworzy i wykorzystuje serwer in-process o nazwie serwer l. W tym samym czasie aplikacja A tworzy działającą poza jej przestrzenią adresową aplikację B. Aplikacja A przekazuje serwer Serwer l aplikacji B. Serwer l działa w aplikacji B jako serwer poza przestrzenią adresową, gdyż obiekt serwer l został utworzony w przestrzeni adresowej aplikacji A. Ważne jest, by o tym pamiętać podczas tworzenia zagnieżdżonych obiektów lub obiektów dzielonych, gdyż różnica w wydajności działania serwerów działających w przestrzeni adresowej i poza przestrzenią adresową aplikacji jest dramatyczna.
W tym momencie wiesz już, jak tworzyć pojedyncze egzemplarze obiektów. Przejdziemy więc do dzielenia obiektów - czyli udostępnianiu pojedynczego działającego egzemplarza obiektu kilku aplikacjom kontenerów.
Tworzenie dzielonych serwerów
Do dzielenia obiektu pomiędzy aplikacje OLE wykorzystuje mechanizm zwany tablicą działających obiektów (Running Object Table). Chodzi o to, że nadający się do podziału obiekt umieszcza swój identyfikator CLSID i odwołanie do swojego interfejsu lunknown tablicy działających obiektów. Każda aplikacja, która tego potrzebuje, może poprosić o wskaźnik do działającego egzemplarza obiektu zamiast o tworzenie nowego egzemplarza. Użycie takiego podziału obiektu jest przydatne w sytuacjach, w których aplikacja powinna działać jako pojedynczy serwer w systemie, a nie jako osobne kopie tego samego
serwera. Doskonałym kandydatem na taki serwer jest nasz serwer Tracker, gdyż może z niego korzystać wiele aplikacji, dokonując wpisów do tego samego pliku dziennika.
Pierwszym krokiem we włączaniu wsparcia dla dzielenia obiektu jest dodanie zmiennej składowej przechowującej identyfikator, który identyfikuje obiekt w tablicy działających obiektów. Ten identyfikator musi być przechowywany, gdyż jest używany także później, przy usuwaniu obiektu z tablicy podczas niszczenia go. W naszej przykładowej implementacji nową zmienną dodamy jako składową publiczną klasy. Oto fragment pliku Tracker. h z dodaną nową zmienną:
// ...
END_INTERFACE_PART (SubDispatch)

//public :
DWORD m_dwRegister;

protected:
FILE * m_fileLog;
// ...
Podczas rejestrowania serwera w tablicy działających obiektów do identyfikacji obiektu w tablicy musimy użyć identyfikatora CLSID obiektu CoClass. Nasza implementacja wymaga zadeklarowania identyfikatora CLSID obiektu CoClass w pliku źródłowym. Identyfikator można skopiować z pliku Trackerlnterface.h. Identyfikator CLSID należy skopiować poniżej linii włączającej plik nagłówkowy initguid.h. Plik initguid.h jest potrzebny do poprawnego zadeklarowania identyfikatora CLSID i przedstawienia go kompilatorowi. Oto deklaracja identyfikatora CLSID w pliku źródłowym Tracker. cpp:
// ...
static const IID IID_ITracker = { Ox11c82946, Ox4edd, Ox11dO,Oxbe, Oxd8 , 0x0, 0x40, 0x5, 0x38, 0x97, Ox7d } } ;
#include
DEFINE_GUID(CLSID_TRACKER, Ox11C82947L, Ox4EDD, 0x11DO, OxBE, 0xD8,0x00,0x40,0x05, 0x38,0x97,0x7D) ;
BEGIN_INTERFACE_MAP(CTracker, CCmdTarget)
// ...
Następnie musimy dopisać kod rejestrujący działający serwer w tablicy działających obiektów. Specyfika naszego serwera dokładnie określa miejsce, w którym należy dokonać rejestracji. W naszej implementacji odpowiednim miejscem będzie konstruktor. Inne implementacje mogą być zależne od osiągnięcia przez serwer określonego stanu przed przystąpieniem do rejestracji. Decyzja zależy wyłącznie od Ciebie i od zastosowanej implementacji. Konstruktor klasy CTracker rejestrujący serwer w tablicy działających obiektów został przedstawiony na listingu 32.12.
Listing 32.12. Implementacja obsługi dzielenia serwera CTracker
// ...
EnableAutomation ( ) ;
// upewniamy się, że aplikacja nie zostanie wyładowana przed // wyzerowaniem licznika odwołań : :Afx01eLockApp( ) ;
// zerujemy składową m_dwRegister = NULL;
// QI dla lUnknown = pamiętajmy, że bez AddRef LPUNKNOWN plUnknown = this->Get!nterface ( &IID_IUnknown) ;
// jeśli mamy lUnknown
if (plUnknown)
{
// rejestrujemy clsid jako aktywny obiekt, dzięki czemu // inne aplikacje otrzymają ten sam obiekt if (: :RegisterActiveObject (plUnknown, CLSID_TRACKER, ACTIVEOBJECT_STRONG, &m_dwRegister ) != S_OK) // zerujemy odwołanie m_dwRegister = NULL;
}
// ustawiamy rozdzielczość timera
m_lTimeBegin = timeBeginPeriod (1) ;
m IHiResTime = m ILastHiResTime = timeGetTime
// ...
W naszej implementacji pierwszym krokiem jest wyzerowanie zmiennej składowej. Implementacja jest zależna od tej zmiennej przy sprawdzaniu, czy serwer został poprawnie zarejestrowany jako działający. Następnie musimy pobrać interfejs lUnknown obiektu i, jeśli to się powiedzie, przekazać go wraz z adresem zmiennej składowej do funkcji RegisterActiveObject (). Wymagamy "mocnej" rejestracji, powodującej dodatkowe zwiększenie liczby odwołań do serwera w celu utrzymania go w pamięci. Jeśli nie powiodło się zarejestrowanie serwera, dla pewności i tak zerujemy zmienną składową.
Ostatnim krokiem jest wywołanie funkcji RevokeActiveObject () w celu usunięcia serwera z tablicy działających obiektów. Ten krok jest najbardziej krytycznym aspektem obsługi dzielonych obiektów. Nie należy jednak dodawać tego kodu do destruktora serwera, gdyż nigdy nie zostanie wywołany. Destruktor jest wywoływany w odpowiedzi na niszczenie serwera, następujące w momencie, gdy licznik odwołań do serwera osiągnie zero. Jednak ponieważ w wywołaniu RegisterActiveObject () zażądaliśmy dodatkowego zwiększenia licznika odwołań, licznik nigdy nie osiągnie tego stanu. W celu zapewnienia poprawnego usunięcia serwera z tabeli najlepiej jest zaimplementować ponownie funkcję ReleaseO interfejsu lUnknown serwera, dzięki której będzie można monitorować licznik odwołań do serwera. Implementacja funkcji Release () usuwającej obiekt z tablicy działających obiektów została przedstawiona na listingu 32.13.
Listing 32.13. Implementacja funkcji Release()
// ...
ULONG FAR EXPORT CTracker::KSubDispatch::Release(} {
METHOD_PROLOGUE(CTracker, SubDispatch)
// wywołanie funkcji i sprawdzenie licznika odwołań long IRefCount = pThis->ExternalRelease();
// gdy jesteśmy zarejestrowani jako działający serwer
// i jest to jedyne odwołanie
if(pThis->m_dwRegister && IRefCount == 1)
{
// zwiększamy licznik odwołań, abyśmy się nie zniszczyli
// przed zakończeniem funkcji
pThis->ExternalAddRef();
// pobieramy identyfikator rejestracji DWORD tdwRegister = pThis->m_dwRegister;
// zerujemy zmienną składową, aby zabezpieczyć się przed // ponownym wywołaniem tej metody pThis->m_dwRegister = 0;
// usuwamy interfejs z tablicy działających obiektów ::RevokeActiveObject(tdwRegister, NULL);
// To wywołanie powinno zniszczyć nasz serwer, więc // musimy zmniejszyć licznik odwołań, return pThis->ExternalRelease();
}
// wyjście return IRefCount;
}
// ...
Za każdym razem gdy program wywołuje funkcję Release (), zaczyna ona działanie od zmniejszenia licznika odwołań do obiektu i zachowania zwróconej wartości. Następnie funkcja sprawdza, czy serwer został zarejestrowany jako działający (o zarejestrowaniu serwera świadczy niezerowa wartość zmiennej składowej). Funkcja sprawdza także, czy licznik odwołań ma wartość 1. Jeśli tak, oznacza to, że serwer jest gotów do usunięcia, gdyż jedyną aplikacją, która się do niego odwołuje, jest tablica działających obiektów. Jednak przed wywołaniem funkcji RevokeActiveObject () ważne jest, by zwiększyć licznik odwołań i wyzerować zmienną składową. Funkcja RevokeActiveObject () re-kursywnie wywołuje funkcję Release (), a nie chcemy, by serwer został natychmiast zniszczony (gdyż spowodowałoby to naruszenie pamięci i załamanie programu). Po powrocie z funkcji RevokeActiveObject () wywołujemy ostatni raz funkcję Release (), aby rzeczywiście zniszczyć serwer i usunąć go z pamięci.
W czasie istnienia serwera możemy otrzymywać ten sam egzemplarz serwera i używać go w wielu różnych aplikacjach. W Visual Basicu do pobierania działającego egzemplarza serwera służy funkcja Getobject (), zaś w Yisual C++ funkcja GetActiveObject (). Gdy kontener otrzyma wskaźnik do serwera, może z niego korzystać tak, jakby utworzył go poprzez standardowe mechanizmy OLE.
Ta metoda dzielenia obiektów jest poprawna, lecz wymaga, by aplikacja korzystająca z serwera brała czynną rolę w decydowaniu, czy powinna korzystać ze wspólnego obiektu czy też stworzyć własny obiekt. Można przyjąć inne podejście: możesz dostarczyć egzemplarz działającego serwera aplikacji wywołującej funkcję CreateObject (), zamiast polegać na wywołaniu przez aplikację funkcji Getobject o . Taki serwer jest nazywany serwerem w pojedynczym egzemplarzu, gdyż programy kontenerów nie mogą tworzyć więcej niż pojedynczy egzemplarz obiektu.
Serwer w pojedynczym egzemplarzu
Aby stworzyć serwer w pojedynczym egzemplarzu, konieczne jest wykonanie wszystkich kroków opisanych w poprzedniej sekcji, "Tworzenie dzielonych serwerów", lecz nie z wnętrza samej implementacji serwera, ale poprzez fabrykę klas dla serwera. Implementując obiekt dzielenia serwera wewnątrz fabryki klas, jesteśmy w stanie kontrolować ilość egzemplarzy obiektu bez konieczności opierania się na dobrej woli aplikacji korzystających z serwera.
Niestety, MFC nie umożliwia w prosty sposób dostępu do klasy coieObjectFactory, odpowiedzialnej za tworzenie serwerów OLE i umożliwiającej ten rodzaj implementacji. Jednak dzięki dziedziczeniu klas C++ możemy stworzyć nową, specjalną wersję klasy coieObjectFactory obsługującą dzielenie egzemplarzy serwera.
W celu umożliwienia obsługi dzielenia serwera musimy dodać dwa makra do deklaracji i implementacji klasy serwera. W pliku źródłowym serwera makro DECLARE_OLECREATE_SHARED musi zastąpić makro DECLARE_OLECREATE, zaś makro IMPLEMENT_OLECREATE musi być zastąpione makrem IMPLEMENT_OLECREATE_SHARED. Jedyna różnica w nowych makrach polega na tym, że do serwera zamiast klasy coieObjectFactory jest dodawana klasa coieObjectFactoryShared. Tworzenie kodu obsługującego serwer w pojedynczym egzemplarzu jest podobne do tworzenia kodu obsługującego normalny dzielony serwer.
Kod implementujący serwer w pojedynczym egzemplarzu jest zawarty w plikach sharedobject.cpp oraz sharedobject.h umieszczonych na dołączonej do książki płytce CD-ROM, w kartotece Rozdz32\MFCServer.
Podsumowanie
W tym rozdziale nauczyłeś się tworzyć podstawową implementację serwera automatyzacji MFC. Nauczyłeś się również rozbudowywać podstawowy szkielet dostarczony przez MFC o nowe, interesujące elementy implementacji.
Do innych obszarów implementacji serwera, które można rozbudować, należy dodawanie do serwera interfejsu użytkownika, także w formie okien dialogowych oraz interfejsów zdarzeń. Użycie standardowych okien dialogowych MFC sprawia, że implementacja interfejsu użytkownika jest łatwą i przyjemną częścią implementacji. Jednak obecnie żadna z aplikacji kontenera nie potrafi skorzystać z żadnego z takich elementów serwera. Jeśli aplikacja ma takie wymagania, musisz sam określić, jak je zaimplementować. Możliwość tworzenia usług i zdalnych serwerów także sprawia, że tworzenie serwerów automatyzacji może być bardzo interesujące.
Każdego dnia coraz więcej programistów korzysta z automatyzacji i dołącza ją do podstawowych elementów swoich aplikacji. O ile dawniej przy wymianie danych można było korzystać tylko z mechanizmu DDE lub, co gorsza, z wymiany danych poprzez pliki, obecnie w każdej sytuacji wymagającej przekazania danych pomiędzy aplikacjami można korzystać z automatyzacji OLE.
Serwery automatyzacji stanowią łatwy i elastyczny sposób tworzenia niewielkich komponentów ActiveX przeznaczonych do użycia w aplikacjach. Obsługa zarówno interfejsu IDispatch, jak i interfejsu niestandardowego (w serwerach z podwójnym interfejsem) także daje użytkownikowi serwera dużo większą elastyczność w wyborze stylu i metod implementacji.

Wyszukiwarka

Podobne podstrony:
26 Tworzenie kontrolek active x
Windows 8 Tworzenie aplikacji z uzyciem C i XAML w8twap
Tworzenie skrĂłtĂłw na biurku z pomoca automatora
AutoCAD Civil 3D 2007 Automatyczne tworzenie modelu terenu 3D z płaskiej mapy cyfrowej
Ćwiczenia Active Directory jednostki organizacyjne tworzenie
32 Hierarchia klas MFC w Wisual c 6 0
Active Directory tworzenie własnej struktury organizacyjnej na potrzeby szkoły
32 Wyznaczanie modułu piezoelektrycznego d metodą statyczną
Brand Equity czyli rynkowe efekty tworzenia marki
DP Miscallenous wnt5 x86 32
tworzenie marki
tworzenie aplikacji w jezyku java na platforme android
Automatyka okrętowa – praca kontrolna 2
automatyka i sterowanie wyklad

więcej podobnych podstron