C++Builder 5 Developers Guide

   

To demonstrate how to fire an event from a server to a client, we'll create an example program that models a restaurant. In this restaurant customers enter, wait to be seated, order their food, wait for their food, eat, pay their bill, and eventually leave. Customers, in our example, will be represented as client applications that will connect to the restaurant using COM interfaces, and will receive service using COM outgoing interfaces. We'll explore building a customer application in a few moments, but first let's look at what it takes to create our Restaurant Automation Server.

For starters, we create a standard GUI application. This will be an outproc server. Next , we create a COM Automation Object through the File, New, Other, ActiveX selection, which are the steps we performed earlier. Figure 17.16 illustrates the New Automation Object dialog for the object we'll create.

Figure 17.16. The New Automation Object dialog for the CustomerAction object.

Notice, in this example we are using the Free threading model, and we have checked the Generate Event Code check box. When OK is selected, the Type Library Editor will appear with an interface, a disp interface, and CoClass. We're going to add a few records and enumerate to this type library, as shown in Figure 17.17.

Figure 17.17. The Type Library Editor for the CustomerAction object and its related interfaces, enumerations, and records.

Again, when we click Refresh, the TLB code will be generated for our server. Also added to this example is a lot of functionality exposed through the GUI by declaring properties and methods within the public section of our form class. These are accessible by the ConnectionActionImpl code.

You're encouraged to look at the complete Form code within the example source provided on the book CD-ROM, which can be found under the COM_Restaurant folder for this chapter. We'll look at just a couple of methods provided by the form class that illustrate the two-way Automation. First, however, we'll concentrate on the code for the ConnectionActionImpl.cpp file, as shown in Listing 17.7.

Listing 17.7 ConnectionActionImpl.cpp Source Code

// CUSTOMERACTIONIMPL : Implementation of TCustomerActionImpl // (CoClass: CustomerAction, Interface: ICustomerAction) // written by Paul Gustavson, 2002. #include <vcl.h> #pragma hdrstop #include "CUSTOMERACTIONIMPL.H" #include "RestaurantForm.h" ///////////////////////////////////////////////////////////////////////////// // TCustomerActionImpl static TCOMCriticalSection CS; STDMETHODIMP TCustomerActionImpl::EnterRestaurant(long* customerID) { long result = Form1->AddCustomer(this); *customerID = result; return S_OK; } STDMETHODIMP TCustomerActionImpl::RequestSeating(long customerID) { Form1->RequestSeating(customerID); return S_OK; } STDMETHODIMP TCustomerActionImpl::PlaceOrder(long customerID, TOrder Order) { Form1->PlaceOrder(customerID,Order); return S_OK; } STDMETHODIMP TCustomerActionImpl::RequestCheck(long customerID) { Form1->RequestCheck(customerID); return S_OK; } STDMETHODIMP TCustomerActionImpl::PayBill(long customerID, PaymentType Type, double Amount) { Form1->PayBill(customerID,Type, Amount); return S_OK; } STDMETHODIMP TCustomerActionImpl::LeaveTip(long customerID, double Amount) { Form1->LeaveTip(customerID,Amount); return S_OK; } STDMETHODIMP TCustomerActionImpl::ExitRestaurant(long customerID) { Form1->RemoveCustomer(customerID); return S_OK; }

As you can see, there's not a lot of code that we've had to add within this file. For each method, we simply call methods contained in the main application, and immediately return an S_OK response to the client. What's important to understand is that a majority of these Form1 methods are asynchronous; that is, any call we make to Form1 returns immediately. Form1 simply puts items such as a customer waiting for a seat, waiting for food, or waiting for a check into a Timer queue to model the simulation. Let's take a look at some of the important elements of the Form1 code.

Listing 17.8 reflects the code used to identify when a customer has entered the restaurant.

Listing 17.8 The AddCustomer() Method

long __fastcall TForm1::AddCustomer(TCustomerActionImpl* ca) { // MessageBeep(MB_OK); long customerID = nextcustomerid; nextcustomerid++; numcustomers++; customer[customerID-1] = TCustomer(lasttimesec); // initialize; customer[customerID-1].status = Entered; customer[customerID-1].customerID = customerID; customer[customerID-1].customeraction = ca; LabelNumCustomers->Caption = AnsiString(numcustomers); StringGridCustomers->Cells[0][customerID] = AnsiString(customerID); StringGridCustomers->Cells[1][customerID] = "Entered"; StringGridCustomers->Cells[4][customerID] = SimulationTime(customer[customerID-1].timein); // if (numseats > 0) // StringGridCustomers->Cells[1][customerID] = "Seated"; return customerID; }

What's important about this code is that a pointer to TCustomerActinImpl has been passed in as a parameter. This parameter identifies the interface to the event sink object for the customer. We'll use this later so that the server can fire events back to the customer when seating is ready, when an order is ready, or when the bill is ready, and so on.

Listing 17.9 reflects the code used to capture an order by the customer.

Listing 17.9 The PlaceOrder() Method

void __fastcall TForm1::PlaceOrder(long customerID, TOrder order) { if (customerID >= 1) { StringGridCustomers->Cells[1][customerID] = "Waiting For Meal"; customer[customerID-1].order = order; customer[customerID-1].status = WaitingForFood; if (!customer[customerID-1].timer) { customer[customerID-1].timer = new TTimer(Form1); customer[customerID-1].timer->OnTimer = customer[customerID-1].timerevent; } customer[customerID-1].timer->Interval = (time_ToServeMeal * 60000) / multiplier; customer[customerID-1].timer->Enabled = true; } // click on StringGrid for this customer - show the order StringGridCustomers->Row = customerID; StringGridCustomersSelectCell(Application,0,customerID,NULL); }

The most important thing to understand in this code is that when a customer order comes through, a unique customer-oriented timer is established, which will be triggered when the simulation time for cooking and serving a meal is reached.

Let's take a look at the timer event handling code in Listing 17.10.

Listing 17.10 The TCustomer::timerevent() Event Handler

void __fastcall TCustomer::timerevent(TObject *Sender) { // if time up timer->Enabled = false; if (status == WaitingToBeSeated) { Form1->StringGridCustomers->Cells[1][customerID] = "Seated with menu"; status = PreparingToOrder; customeraction->Fire_OnSeatingReady(customerID); // set up timer for waiter to check to see if customer is ready to order timer->Interval = (Form1->time_ReadyToOrder * 60000) / Form1->multiplier; timer->Enabled = true; } else if (status == PreparingToOrder) { Form1->StringGridCustomers->Cells[1][customerID] = "Ready To Order Query"; customeraction->Fire_OnReadyToOrderQuery(); } else if (status == WaitingForFood) { Form1->StringGridCustomers->Cells[1][customerID] = "Meal Has Arrived"; status = Eating; customeraction->Fire_OnOrderReceived(); } else if (status == Eating) { } else if (status == WaitingForBill) { Form1->StringGridCustomers->Cells[1][customerID] = "Bill On Table"; Form1->StringGridCustomers->Cells[2][customerID] = FloatToStrF(bill, ffCurrency, 0, 2); customeraction->Fire_OnCheckReceived(bill); } else if (status == WaitingForChange_Card) { Form1->StringGridCustomers->Cells[1][customerID] = "Change / Charge Card Returned"; status = AfterDinnerChat; customeraction->Fire_OnChangeReturned(change); } else if (status == AfterDinnerChat) { } else if (status == Departed) { } }

This code is used to handle time-oriented simulation events. When a simulation event of interest occurs, the appropriate event sink method is fired back to the customer client application. This enables the client to not be held up in a synchronous wait for processing to complete. It also frees the client from polling the server for status updates and demonstrates an effective way for a server to notify external applications without resorting to PostMessage() calls.

Implementing Event Sinks within a Client

Now we need to create a client that not only calls the methods of an interface which is handled by our Restaurant server, but can set up the event sink method handlers for the Restaurant server to provide notifications of completed events.

To set up the triggering of events, we need to create a class that implements our event sinks. C++Builder provides a template class TEventDispatcher found in utilcls.h to help us write IDispatch -based event sinks. It implements the IDispatch interface for servers to call when firing events. Its InvokeEvent() method is used to forward the server calls to their corresponding event handlers. InvokeEvent() is defined as follows :

// To be overriden in derived class to dispatch events virtual HRESULT InvokeEvent(DISPID id, TVariant* params = 0) = 0;

TEventDispatcher also exposes some methods for connecting the sink to, and disconnecting it from, a server: ConnectEvents() and DisconnectEvents() , respectively.

We will use the TEventDispatcher template for creating a class that will delegate the processing of the COM event to a C++Builder VCL event handler. Our implementation of the event sinks class to support the ICustomerActionEvents dispinterface is provided in Listing 17.11.

Listing 17.11 RestaurantSink.h File

/*--------------------------------------------------------------------------- RestaurantSink.h Chapter 17 - COM Automation Server Example w/ Event Firing created by Paul Gustavson, 2002 ---------------------------------------------------------------------------*/ #if !defined(RESTAURANTSINK_H__) #define RESTAURANTSINK_H__ #include <atlvcl.h> #include <atlbase.h> #include <atlcom.h> #include <ComObj.HPP> #include <utilcls.h> #include "RestaurantWaiter_TLB.h" typedef void __fastcall (__closure * TOrderReceivedEvent)(void); typedef void __fastcall (__closure * TCheckReceivedEvent)(double Amount); typedef void __fastcall (__closure * TChangeReturnedEvent)(double Amount); typedef void __fastcall (__closure * TSeatingReadyEvent)(long customerID); typedef void __fastcall (__closure * TReadyToOrderQuery)(void); //--------------------------------------------------------------------------- // Create a class that handles ICustomerActionEvents class TRestaurantSink : public TEventDispatcher<TRestaurantSink, &DIID_ICustomerActionEvents> { protected: // Event field TOrderReceivedEvent FOnOrderReceived; TCheckReceivedEvent FOnCheckReceived; TChangeReturnedEvent FOnChangeReturned; TSeatingReadyEvent FOnSeatingReady; TReadyToOrderQuery FOnReadyToOrderQuery; // Event dispatcher HRESULT InvokeEvent(DISPID id, TVariant* params) { if ((id == 1) && (FOnOrderReceived != NULL)) // OnOrderReceived FOnOrderReceived(); else if ((id == 2) && (FOnCheckReceived != NULL)) // OnCheckReceived FOnCheckReceived(params[0]); else if ((id == 3) && (FOnChangeReturned != NULL)) // OnChangeReturned FOnChangeReturned(params[0]); else if ((id == 4) && (FOnSeatingReady != NULL)) // OnReadyToOrderQuery FOnSeatingReady(params[0]); else if ((id == 5) && (FOnReadyToOrderQuery != NULL)) // OnReadyToOrderQuery FOnReadyToOrderQuery(); return S_OK; } // Reference to the event sender CComPtr<IUnknown> m_pSender; public: __property TOrderReceivedEvent OnOrderReceived = { read = FOnOrderReceived, write = FOnOrderReceived }; __property TCheckReceivedEvent OnCheckReceived = { read = FOnCheckReceived, write = FOnCheckReceived }; __property TChangeReturnedEvent OnChangeReturned = { read = FOnChangeReturned, write = FOnChangeReturned }; __property TSeatingReadyEvent OnSeatingReady = { read = FOnSeatingReady, write = FOnSeatingReady }; __property TReadyToOrderQuery OnReadyToOrderQuery = { read = FOnReadyToOrderQuery, write = FOnReadyToOrderQuery }; public: TRestaurantSink() : m_pSender(NULL), FOnOrderReceived(NULL), FOnCheckReceived(NULL), FOnSeatingReady(NULL), FOnReadyToOrderQuery(NULL) { } virtual ~TRestaurantSink() { Disconnect(); } // Connect to Server void Connect(IUnknown* pSender) { if (pSender != m_pSender) m_pSender = pSender; if (NULL != m_pSender) ConnectEvents(m_pSender); } // Disconnect from Server void Disconnect() { if (NULL != m_pSender) { DisconnectEvents(m_pSender); m_pSender = NULL; } } }; #endif //RESTAURANTSINK_H__

This code exhibits the class needed to handle ICustomerActionEvents . Five methods are associated to this event sink object: OnOrderReceivedEvent() , OnCheckReceivedEvent() , OnChangeReturnedEvent() , OnSeatingReadyEvent() , and OnReadyToOrderQuery() . We also implemented an OnInvokeEvent() method, which is our dispatcher event method. Within this method, which is invoked by the server, we examine the id being fired and check to see if an event method has been assigned to the appropriate dispatch event. Upon a match, the client's event method that was associated to the dispatch event within the class will be triggered. This header file needs to be included within the header file for our client application.

#include "RestaurantSink.h"

Within the private section of our form class we need to identify not only the TCOMICustomerAction interface we desire , but the event sink object created in RestaurantSink.h .

private: // User declarations long myid; TCOMICustomerAction Restaurant; TRestaurantSink FRestaurantSink; double bill;

Within the protected section of our form class for the client, we need to declare the event methods.

protected: void __fastcall OnOrderReceived(); void __fastcall OnCheckReceived(double amount); void __fastcall OnChangeReturned(double amount); void __fastcall OnSeatingReady(long customerID); void __fastcall OnReadyToOrderQuery();

Next, we need to connect these dispatch events within our client, which is depicted in Listing 17.12.

Listing 17.12 FormCreate() method

void __fastcall TForm2::FormCreate(TObject *Sender) { Restaurant = Restaurantwaiter_tlb::CoCustomerAction::Create(); // Connect dispatch events: FRestaurantSink.OnOrderReceived = TForm2::OnOrderReceived; FRestaurantSink.OnCheckReceived = TForm2::OnCheckReceived; FRestaurantSink.OnChangeReturned = TForm2::OnChangeReturned; FRestaurantSink.OnSeatingReady = TForm2::OnSeatingReady; FRestaurantSink.OnReadyToOrderQuery = TForm2::OnReadyToOrderQuery; FRestaurantSink.Connect(Restaurant); Restaurant->EnterRestaurant(&myid); Caption = "Customer #" + AnsiString(myid); }

In this method, we first establish an instance to the interface. Then, we assign event methods defined by our client application to the event sink dispatch methods. Also important is to establish a connection point between the event sink object and the instance to the interface using the event sink's Connect() method, which we defined in the RestaurantSink.h file.

Listing 17.13 shows the code for both a call to a COM object method and an event method handler.

Listing 17.13 ButtonPlaceorderClick() and OnOrderReceived()

void __fastcall TForm2::ButtonPlaceOrderClick(TObject *Sender) { TOrder order; order.Drink = FormMenu->RadioGroupDrink->ItemIndex; order.Appetizer = FormMenu->RadioGroupAppetizer->ItemIndex; order.Dessert = FormMenu->RadioGroupDessert->ItemIndex; order.Entree = FormMenu->RadioGroupEntree->ItemIndex; order.SideDish = FormMenu->RadioGroupSideDish->ItemIndex; Restaurant->PlaceOrder(myid,order); LabelStatus->Caption = "Placed Order. Waiting for meal..."; FormMenu->Close(); // close menu ButtonPlaceOrder->Enabled = false; ButtonMenu->Enabled = false; } //--------------------------------------------------------------------------- void __fastcall TForm2::OnOrderReceived() { LabelStatus->Caption = "Meal has arrived. You are now eating."; ButtonRequestBill->Enabled = true; }

The first method represents the code used when the Customer is ready to order his meal. The actual call made to the object method contained within the server is Restaurant->PlaceOrder() , which returns immediately ”it's asynchronous. When the simulation event is complete, the Restaurant server triggers the client's OnOrderReceived() event method.

Figure 17.18 illustrates an active Restaurant serving multiple Customers simultaneously .

Figure 17.18. An execution run of the Restaurant Server Application with multiple Customer Clients.

You're encouraged to look at the code for the Restaurant and the Customer client to further examine the intricacies of how they both work. Figure 17.19 shows the composition for the projects associated to these two applications.

Figure 17.19. Project group for the Restaurant Server Application and Customer Client Application.

NOTE

Be sure to add the C++Builder ATL folder to your include path , as shown in Figure 17.20. Otherwise, you'll receive a batch of errors upon compilation indicating the ATL header files cannot be found. The ATL folder and its related files are key elements in C++Builder for supporting COM development.

Figure 17.20. Adding the ATL path to the Project Options.


   
Top

Категории