Splitting the UI and the Engine

This section offers a pragmatic approach to application design to ensure that applications' user interfaces are cleanly decoupled from their underlying functionality, thereby ensuring that the non-UI parts of the application are almost entirely portable . It explains why you should view the UI and engine as separate components of your application, and it provides details for an example implementation.

The User Interface renders an interpretation of the application's data and allows the user to interact with it. The UI often gives the user the ability to enter, edit or delete the data. The UI, therefore, actually comprises two interfaces ”one to handle user input, and another to transfer user requests into the engine.

The engine of an application comprises the data, the algorithms that manage and process that data and routines that persist (store and retrieve) it. Consider the game of chess as an example. If this were to be written as an application, the model would comprise the internal state of the board, the rules of movement for each piece, and algorithms for calculating the game state at each turn (check, checkmate and so on). The UI layer might be complex, but it would have nothing to do with the chess game itself. It would be merely a mechanism for representing the game state and providing a way for users to input their next move.

This application's engine would also encapsulate an AI component so that the computer could calculate its next move. Once the move was made, the UI would be notified so that it could display the game's new state.

All applications can be cleanly divided along these functional lines, and developers should make a clear separation on both the class and component level. From the component perspective, the engine should be written as a stand-alone DLL. Alternatively, it could be written as an .exe server if the engine had to be able to service several different applications simultaneously , or if asynchronous handling was required for data-sharing purposes. The application's .mmp file would then simply include the library file for the engine.

There are many reasons for doing this. A basic principle of good design is to avoid creating unnecessary dependencies between components. Also, maximizing code reuse so that common functional requirements can be satisfied from shared libraries is very desirable. The engine for your application should be a functional unit that stands apart from whatever GUI layer represents the data to the user and allows them to interact with it. For the purposes of flexibility, it is best to allow the engine to reside in its own DLL and to offer its own API to any UI layer that might want to use it.

This sounds like extra work, and yes, it does require slightly more effort ”plus it adds some complexity ”but it also affords numerous benefits. Another reason for separating the engine is so that it can be tested without the need to write a UI layer first, and that the engine can be thoroughly tested via test harnesses before the UI enters into the equation. From a project standpoint, this means that the UI layers and engines can be developed in parallel, thereby significantly reducing the minimum required development time.

Software must be maintained . Often this must be performed by developers who were not present during the initial design and development phases. The maintainability of code is directly related to its structure ”unstructured code will prove difficult and expensive to maintain. Separation of GUI and engine is an opportunity to cleanly define the boundaries of independent components. This seems like stating the obvious, but applications are most often crafted to tight deadlines ”the "get-it-out-the-door" mentality sometimes pays little attention to these longer- term design considerations.

Separation of GUI and engine is certainly not a goal that is specific to mobile computing devices. But it does bear even greater practical relevance for these. You will probably want to ensure that your applications are available on as many devices as possible. In the world of mobile computing, there are a host of Symbian OS devices, but they will differ in the version of Symbian OS, or the UI platform they deploy. However, most of the non-UI services, and the APIs that use these services, are identical. As a result of this transplatform consistency beneath the UI layer, it is possible to write engines that are directly reusable, or that will require only minor tweaks when porting.

The ShapeDrawer example application gives practical guidance in the following areas:

  • Writing a separate engine with its own API.

  • Writing a GUI that invokes this API and alters the state of the engine.

  • Demonstrating the built-in class member functions, called by the framework, to help coordinate the UI with the changing state of the engine.

As explained in the chapter introduction, this application allows the user to draw shapes on the screen using one view, and to list the drawn shapes (with their coordinates) in another. The model's job, therefore, is to process user requests to draw squares or circles, and to remember the placement of each shape. In order to achieve this, the model must support:

  • A shape class which holds position, size and shape.

  • Methods for adding these new shapes.

  • Methods that read and write shape data, to and from streams.

  • Methods to actually draw the shapes.

This last requirement may seem contrary to the general rationale for separating UI functionality from the engine. However, it is deemed possible to perform this drawing in a generic way, since the actual rendering of the shapes only requires function calls to a CWindowGc object, which can be passed into the Draw() function by reference. The CWindowGc is definitely a UI component, but remember that the UI API classes reside in different tiers: this class resides in a lower-level component that will be consistent across various platforms. Alternatively, the engine could be designed to simply deliver all of the relevant shape data back to the UI, which then renders it. However, the fact that methods called on CWindowGc are cross-platform is used to simplify the code.

At application startup, the RestoreL() method of the document will be called automatically by the framework. This initiates the reinstatement of the document's model ( CShapeListManager ) from streams stored in the application's file. In addition to kicking off this process automatically, the framework also provides an automatic update to the AppUI, to notify it that the state of the model has changed (by way of initializing it from a file store). HandleModelChangeL() is called in the AppUi as soon as the model has restored itself (completed its RestoreL() method). The AppUi then renders the model's data by calling methods on its API. The following code shows the engine's primary API class that the AppUi layer uses to render the data:

 class CShapeListManager : public CBase    { public:    IMPORT_C static CShapeListManager* NewLC();    IMPORT_C static CShapeListManager* NewL();    IMPORT_C ~CShapeListManager();    IMPORT_C void Clear();    IMPORT_C void AddShapeL(NShapes::TShape* aShape); //takes ownership    IMPORT_C void RemoveShapeL(NShapes::TShape* aShape);    IMPORT_C NShapes::TShape* GetNextShape(); // no ownership xfer    IMPORT_C TStreamId StoreL(CStreamStore& aStore) const;    IMPORT_C void ExternalizeL(RWriteStream& aStream) const;    IMPORT_C void RestoreL(const CStreamStore& aStore, const TStreamId& aStreamId);    IMPORT_C void InternalizeL(RReadStream& aStream); protected:    IMPORT_C CShapeListManager();    IMPORT_C void ConstructL(); private:    TInt iNextShapeIndex;    RPointerArray<NShapes::TShape> iShapeList;    }; 

The following code shows how GetNextShape() is used by the UI layer in order to, in an iterative manner, grab a pointer to each TShape object stored in the model, and then to call TShape::Draw() in order to represent it:

[View full width]
 
[View full width]
void CShapeDrawerGraphicViewContainer::Draw(const TRect& /*aRect*/) const { CWindowGc& graphicsContext = SystemGc(); // Clear the application view graphicsContext.Clear(); // Draw the 'cursor' crosshair. // Size is KCrosshairWidth by KCrosshairHeight pixels graphicsContext.SetPenSize(TSize(1, 1)); graphicsContext.SetPenColor(KRgbBlack); graphicsContext.DrawLine(TPoint(iPosition.iX - KCrosshairWidth, iPosition.iY), TPoint(iPosition.iX + KCrosshairWidth, iPosition.iY)); graphicsContext.DrawLine(TPoint(iPosition.iX, iPosition.iY - KCrosshairHeight), TPoint(iPosition.iX, iPosition.iY + KCrosshairHeight)); // Draw all the current shapes TShape* shape = iDocument->Model()->GetNextShape(); while (shape) { shape->Draw(graphicsContext); shape = iDocument->Model()->GetNextShape(); } }

Here you see the function iterating through each shape contained in the model. As discussed previously, the shape objects perform their own rendering. In a similar fashion, the application's list view also represents the model's data, but it does so textually. Although it calls TShape::Coordinates() instead of Draw() in order to build a text string that represents each item in the model, its interaction with the model is very similar. The code below shows how the UI can add information to the model (note that the full method is not shown here):

[View full width]
 
[View full width]
TKeyResponse CShapeDrawerGraphicViewContainer::OfferKeyEventL(const TKeyEvent& aKeyEvent, TEventCode aType) { if (aType != EEventKey) { return EKeyWasNotConsumed; } // Move left if (aKeyEvent.iScanCode == EStdKeyLeftArrow) { if (iPosition.iX > (Rect().iTl.iX + KCrosshairWidth)) { iPosition.iX; DrawNow(); } return EKeyWasConsumed; } // Move right ... // Move up ... // Move down ... // Place a shape else if (aKeyEvent.iScanCode == EStdKeyDevice3) { TShape* newShape = NULL; // Update the coordinates in the model to the // position at which the event occurred. switch (iBrushShapeType) { case ECircle : newShape = new (ELeave) TCircle(iPosition, KBrushRadius); iDocument->Model()->AddShapeL(newShape); // Takes ownership break; case ERectangle : newShape = new (ELeave) TRectangle(iPosition, KBrushHeight, KBrushWidth); iDocument->Model()->AddShapeL(newShape); // Takes ownership break; default : User::Panic(KShapeDrawerPanicName, EShapeDrawerInvalidBrushType); } DrawNow(); return EKeyWasConsumed; } return EKeyWasNotConsumed; }

Here you see that the view holds a data member that holds the type of shape to draw ( iBrushShapeType ), which the user can select from a menu. The TCircle and trectangle classes are both part of the engine's API. Adding a shape to the model is then very simple. Note that the model then takes ownership of the new shapes. It is clear now how the model's API can be used by the UI layer in order to both give input to, and receive necessary output from, the model.



Developing Series 60 Applications. A Guide for Symbian OS C++ Developers
Developing Series 60 Applications: A Guide for Symbian OS C++ Developers: A Guide for Symbian OS C++ Developers
ISBN: 0321227220
EAN: 2147483647
Year: 2003
Pages: 139

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net