ĐĎॹá > ţ˙ ţ˙˙˙ ˘ Y ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ěĽÁ 9 řż Ľ bjbjýĎýĎ űF Ľ Ľ Ô ş ˙˙ ˙˙ ˙˙ l * * * * z z z 4 Ž ňĚ ňĚ ňĚ h ZÍ ü VÎ L Ž ¨ö ŽŃ 6 äŇ L 0Ó 0Ó FÓ =Ö V × d ÷× 4 Kö Mö Mö Mö Mö Mö Mö Hř hú  Mö z +Ř ßŐ ^ =Ö +Ř +Ř Mö yÝ * * 0Ó FÓ bö yÝ yÝ yÝ +Ř î * 8 0Ó z FÓ Kö yÝ +Ř Kö yÝ Â yÝ ;Ţ 2 Ŕé b z Wě FÓ ˘Ń ŰnŹŃĂŽ DĂ ňĚ Ü ×ë . Wě ô xö 0 ¨ö ě R *ű Ü ` *ű Wě yÝ Ž Ž * * * * Ů Developing Windows DNA Applications Using COMCodebook By Beth Massi Introduction This document is meant to give you a detailed look of what pieces make up a DNA application written in COMCodebook as well as guide you through the process of creating a simple browser-based application from start to finish. COMCodebook is a middle-tier framework written in Visual FoxPro 6 developed by Flash Creative Management, Inc. You can reach Flash Creative Management, Inc. on the internet at HYPERLINK http://www.flashcreative.com www.flashcreative.com. To get the most out of these lessons, you should be relatively familiar with Windows DNA theory as well as active server pages and Visual FoxPro, however, it is not necessary to have any working knowledge of COMCodebook specifically. For more information on Windows DNA, see the Microsoft internet papers HYPERLINK http://microsoft.com/dna/about/overview/intro.asp http://microsoft.com/dna/about/overview/intro.asp, HYPERLINK http://microsoft.com/dna/about/overview/devmodel.asp http://microsoft.com/dna/about/overview/devmodel.asp and HYPERLINK http://microsoft.com/dna/about/overview/facts.asp http://microsoft.com/dna/about/overview/facts.asp. You should also have a look at the article Windows DNA Development A Pattern Language by Yair Alan Griver in the Spring 2000 issue of Component Developer Magazine. For more information on Active Server Pages and Visual FoxPro, start at the Microsoft Visual Studio home page at HYPERLINK http://msdn.microsoft.com/vstudio/default.asp http://msdn.microsoft.com/vstudio/default.asp. First I will begin by briefly explaining the theory behind the COMCodebook middle-tier components. We will then move onto defining our project goals and designing the middle-tier. I will then show you how to step-by-step create a simple thin-client application with a parent-child form using active server pages and Dynamic HTML accessing the COMCodebook middle-tier components. Finally, we will scale our application up into Oracle8 and SQL-Server backends where Ill show you how simple it is to modify the COMCodebook data source components to accommodate a different database. Part 1 The Theory Behind COMCodebook COMCodebooks Component Architecture The Windows Distributed InterNet Application (DNA) model consists of three logical tiers: a user interface tier which presents information to the user and standardizes the users movements through the system, a middle or business tier which provides validation, business rule processing and standardization of the data, and the data tier which is our storage location for persisting information. You can use any database your organization requires, such as Oracle, SQL-Server, or VFP. The business tier is where the program logic, business rules and processes can run in the Microsoft Transaction Server (MTS) environment. This is what we refer to as the middle-tier. The Presentation tier merely presents and collects the information in an interface to the user. In a thin-client scenario, Internet Information Server (IIS) can be used in conjunction with Active Server Pages (ASP) to serve the Dynamic HTML (DHTML) to the browsers. The middle and data tiers can be placed on multiple servers for scalability and security reasons, leading to a theoretically unlimited number of physical servers for any given system. It is for this reason that DNA applications are sometimes known as n-tier systems. In the diagram below the highlighted parts are the pieces of a Windows DNA Application using COMCodebook. Notice that COMCodebook does not dictate the front-end interfaces or back-end database, it is only a middle-tier framework. You have the flexibility to determine which database and what kind of user interface you want to develop. Microsoft Transaction Server Considerations In the diagram above, you will notice that the middle-tier is sitting inside Microsoft Transaction Server. It is important to note a couple things. First, the COMCodebook components can run outside of Microsoft Transaction Server as well as inside. This provides you with the flexibility of deployment and scalability. Inside or outside MTS, there is no difference in your coding the middle tier. In other words, getting the components to run in MTS is transparent to you. The COMCodebook components know when you put them in MTS and take advantage of it immediately, without any re-compiling or code modifications on your part. If you decide you want to put your components inside MTS, you then have another option. You can have the components either support transactions or not support transactions. Running databases that are not XA or COM-TX compliant do not have transaction support with Microsoft Transaction Server, in which case you could still use MTS pooling, but would not use transaction support. Databases like Visual FoxPro or Access do not support MTS transactioning. Databases like Oracle, SQL-Server and DB2 support MTS transactioning. For more information on using MTS with VFP Components, read Microsoft Transaction Server for Visual FoxPro Developers by Randy Brown at HYPERLINK http://msdn.microsoft.com/library/techart/mtsvfp.htm http://msdn.microsoft.com/library/techart/mtsvfp.htm. Accessing Multiple Databases Designing a system that speaks to multiple XA and/or COM-TX compliant databases is made extremely simple when using MTS and COMCodebook. You could design your application to have multiple data sources connect to multiple databases all managed with MTS transactions. This is a beautiful thing. Defining which connection classes the data source object will use is a part of developing each individual data source object. This makes it easy to define, say, SQL-Server data sources and Oracle data sources and save data to them at the same time all inside an MTS transaction. COMCodebook Stereotypes Stereotypes are how we categorize and describe the different components that make up the COMCodebook framework. The COMCodebook framework consists of the following: PRIVATEStereotypeDescriptionResourcesResources are the containers of state for our components. They are passed from tier to tier and component to component where they are modified as necessary. They will often take the form of ADO record sets or XML data streams. A resource is a container of state, the data marshaler. It contains properties but no methods. It does not live in MTS. Resource ManagersResource Managers are the presentation tiers view into the business tier. They are business aware sets of components that provide the public interface used by the front-end developer. They are the dispensers of resources and know which data sources to use. In MTS, they "Support Transactions" Data SourcesData Sources are components that handle creating, loading and updating Resources as necessary. They communicate with the backend database directly, typically running SQL statements. They provide a conceptual translation from a physical database to a logical view of the data. In MTS, they "Require a Transaction" Resource ValidatorsResource Validators are validation objects that contain the validation for a single Resource. A resource validator does any method work on the resource. In MTS, they are set to "Support Transactions" ProcessesProcesses perform any processing that has to occur across multiple Resources, typically by calling into methods of particular resource validators. They typically only have one public method "Execute" In MTS, they are set to "Requires a New Transaction" Resource Manager ProxyA resource proxy may be needed in the presentation tier to translate the Resource objects into a format that is familiar to the front end developer. For instance, it may translate an XML stream into a VFP cursor for use in a thick VFP front-end. Basically, a proxy provides a place for a developer to put front-end specific hooks into the calls to a resource manager. It has the same methods as the Resource Manager. Lets take a look at a simple example of how a business entity will work. The following table uses these objects: rmAttorney - An Attorney resource managerdsAttorney - An Attorney data sourcersAttorney - An Attorney resourcevldAttorney An Attorney validation object TierObjectMethod CallReturns
PresrmAttorneyGetAttorneyByID(1) rmAttorneydsAttorneyGetAttorneyByID(1)rsAttorney with data for Attorney #1 which is returned to rmAttorney and then to the UIPres Modifies the data in rsAttorneyPresrmAttorneySave(rsAttorney) rmAttorneyvldAttorneyValidate(rsAttorney)RsAttorney with any changes required by the validation and a validation success messagermAttorneydsAttorneySave(rsAttorney)Success message is passed back after the rsAttorney changes are updated to the server. As you can see, the presentation developer only needs to know about the public interface of the resource managers. The resource manager acts as a facade into validation objects, process objects and data sources. The data sources are the only objects that need to know about the structure of the database and any calling conventions for that database. This is why we call it component architecture. Each layer interacts with only the layer "next" to it. So if you decide to offer SQL-Server, DB2, Sybase or any other XA-compliant database as a backend in the future, only the data sources need to be modified. Conversely, if a new browser comes out that supports the next generation of HTML, only the presentation services need to be reprogrammed. The business logic will always be intact. If there is a bug in the business services, only the application server needs to be upgraded. Say bye-bye to workstation installs and upgrades. The Business Entity The Business Entity is what we refer to as the set of components that define the behavior and rules of a resource. Remember that a resource is a container of state or data, like an ADO recordset. There raises a question of where the hierarchical relationship between tables that form a resource should be managed. It is possible to create various business entities that provide resources with different levels of hierarchy, and have the front-end application call whichever resource manager that is appropriate. It is also possible to have the resources be a single layer, with the application front-end describing the relationships necessary at the time, allowing the middle-tier to handle the formalities of retrieving and updating things in the proper order. The COMCodebook framework which has selected the latter approach, believing that it is more flexible over time. In other words, the COMCodebook Business Entity is what we refer to as the set of components that define the behavior and rules of a single layer of data. For instance, in the above example, the "Attorney" Business entity is used. It consists of the Attorney Resource Manager, Attorney Data source, and Attorney Validator objects. These objects only know how to get/save/add/delete data to the Attorney table in the database. To get the data, we define interfaces to the business entities based on the methods of resource retrieval. For example, we will have GetAttorneyByID(), GetAttorneyByName(), and GetAttorneyBySSN() among others defined as public functions in the resource managers that will return resources (ADO Recordsets). The resource managers will then call the public GetAttorneyBy...() interfaces in the data sources which will execute the SQL Statements on the database and will return the resource to the resource manager. The Validator objects are called before a save() of the resource manager and run through the data field by field (or row by row) in the resource to make sure it is valid. This is where the field and row level business rules are located. The Resource Manager Controller (RMC) You are probably asking yourself, How do I connect the resources together in a hierarchical way? Where does the hierarchical relationship between the entities exist? The example above only shows how a single business entity works. Many relations between your data may exist. Because the business entity only describes one single layer of data, many relationships between them will also exist. This means that many of your business entities have one-to-many or one-to-one relationships to each other. How do we manage these relationships and finally present them to the user? The answer is the Resource Manager Controller (RMC). An RMC "controls" the update/delete/insert of many resources all at once by making method calls to all the resource managers. That way our presentation tier can act upon many resources at the same time. When the presentation tier calls the save() of the RMC, it makes sure all the resources are valid by calling the validate() method of each resource manager and then executing their save() methods. This creates a complex transaction because each of the individual save() methods of the resource manager's are wrapped in an MTS transaction as well. PRIVATE "TYPE=PICT;ALT=RMCtoData.gif (17009 bytes)" The Resource Manager Proxy Now that we understand how to display our data in a hierarchical way using the resource manager controllers, let's take a deeper look into the way the resources travel from middle-tier to a thick client front-end. First we must understand how resources are translated on the thick client. Since the resources are actually ADO Recordsets, in a VFP front-end there must be a way to translate the resources into a usable form, namely a VFP cursor. We do this with a Resource Manager Proxy (RMProxy). A RMProxy will intercept the calls to/from the business tier to execute processing. This is where we create the cursors to use on the forms or create ADO recordsets from our modified cursors to send back to the resource managers. Additionally, there may be many Resource Manager Proxies on a form. They are contained in the Resource Manager Proxy Controller. It is this object that instantiates the remote RM Controller then sends environment information via XML to it, which sets up its environment of resource managers on the middle-tier. Thus it is possible to present complex data entity relationships to the user for modification all at once. (Of course, the users don't know that they are complex.) The above diagram shows an example of how VFP thick-client translates resources. In a thin-client, a resource manager proxy is probably not necessary. This is because an active server pages application already knows how to display an ADO recordset resource. Ill show you how to translate the ADO recordsets into HTML and back again in our simple application. COMCodebook Code Layers Before we develop our first application, it is necessary to understand that the COMCodebook framework code is structured into three layers, the a-layer, the i-layer and the c-layer. The c-layer is the Codebook layer. It is the layer where all framework code is written and from which all objects ultimately inherit from. This is the layer that Flash Creative Management provides for you. You should never have to modify this layer. The a-layer is the application layer. This is the layer where you spend your time. It is where all your application specific code resides. The i-layer is an intermediate layer that lies between the "c" and "a" layers. It is where company specific code is written, if necessary. It is a place where you, the Codebook developer, can safely modify the source to meet your needs and continue to take advantage of periodic "c" layer updates from Flash. Part 2 - Our First COMCodebook Web-based Application Now that we have discussed some of the theory behind COMCodebook, lets dive head first into our first web-based application! To make things simple (and easy to download), the lessons will be using a Visual FoxPro database and will not run inside Microsoft Transaction Server since VFP does not support MTS transactions. However, lets remember that getting the components to run in MTS is transparent to you. (See: Microsoft Transaction Server Considerations in Part 1) If you are interested in more robust solutions, the final part of this document discusses scaling our application up into Oracle and SQL-Server databases and what minimal amount of code we need to change in the middle-tier to do so. Those sections assume you are familiar with the particular databases being discussed and focus only on the middle-tier modification and MTS configuration. Project Specifications The purpose of this application is to present users with a flexible way of searching for attorneys as well as a way to search for attorney notes, allowing them to add/edit/delete them. The user will be given a search form where they will type in their search criteria and will then be presented a list of attorneys and their notes that meet the search criteria. From there the user may edit an attorney record or add a new one. They will need to modify the Name, Firm, License Number and Address information of the attorney. Since there can be multiple notes per attorney, the user will also need to somehow browse a list of notes, preferably on the same page, and then be able to add/update/delete them as well. Setting Up Our Development Environment Before you begin, you will need to download the latest version of the COMCodebook framework from the COMCodebook Web Site (thats where you got this document, so hopefully you can find it (). Please read the instructions on how to set up the sample application, TimeTrac, that ships with COMCodebook as an example and make sure it works for you. The software necessary to make the TimeTrac sample app work includes the following: The NT 4 Option Pack which includes Microsoft Transaction Server and Internet Information Server / Personal Web Server. Or Windows 2000 Professional or Server with IIS installed. Visual FoxPro 6 Service Pack 4 or higher ( HYPERLINK "http://msdn.microsoft.com/vstudio/sp/vs6sp4/default.asp" http://msdn.microsoft.com/vstudio/sp/vs6sp4/default.asp) ActiveX Data Objects (ADO) 2.5 or higher installed ( HYPERLINK http://www.microsoft.com/data/download.htm http://www.microsoft.com/data/download.htm). Microsoft Internet Explorer 5.0 or Higher ( HYPERLINK http://www.microsoft.com/windows/IE/ http://www.microsoft.com/windows/IE/) In our thin-client sample application, I will also use Visual InterDev 6 to develop the active server pages so you will want to make sure that is available as well. It is also necessary for developing this application to have Internet Information Server (NT Server) or Personal Web Server (NT Workstation) on your development machine which you can install from the NT 4 Option Pack if you are running NT 4.0 Server or Workstation. When you extract the COMCodebook framework, extract it to the root of your drive with sub-directories. This will put everything under a directory called FLASH. If you have extracted your framework to your D:\ then the directory structure should look similar to this: The AttorneyApp sample file shells are provided to you in the zip file AttorneyApp.zip. This sets up the application for development automatically for you. It contains all the data and project files necessary, but does not contain all of the code yet. We will write the code together in the lessons that follow. If however, you get lost or your code just doesnt seem to be working, there is a complete set of zipped up code in the \Finished subfolder. Be sure to place your AttorneyApp folder under the Codebook folder by extracting this zip file to the same root drive that you extracted COMCodebook with subdirectories. Under the AttorneyApp folder you will see subfolders that will contain the data source, resource manager, validator and processes projects: \DataServices, \ResourceMgrs, \Validation and \Processes. Also, we will have \Include and \Libs folders. \Include is where the application include files will be placed and the \Libs directory is where all your a-layer programs will reside. We will also have an \Interface directory that will contain all the active server pages as well as a \jscripts and \images folder for JavaScript code we write and images we may use on the forms. \Interface will be our web directory. Finally, you will see a \Data folder which will contain the VFP database. So the structure of your AttorneyApp sub-folders should look like this: Notice that this is similar to the \WatsonDevelopment folder that is provided by Flash Creative Management as a sample application. That application is a thick-client, so it has additional folders we will not need. Reviewing the Database The simple sample VFP database is provided for you. It is located in the AttorneyApp\Data folder. Notice that the tables in the database contain more fields than we will need to display to the user on the forms. This is okay. The data sources do not have to be a mirror image of your tables. In fact, when we set up our flexible searching, we just might need to pull data from additional tables as well. This is what makes the data sources flexible on data retrieval. Attorney TableAttorney Note TableATTORNEY_ID ATTNOTE_IDLICENSE_NUMBER ATTORNEY_IDFIRM_NAME NOTE_TIME LASTNAME NOTE_LOCKFIRSTNAME NOTE_NAME MI USER_NAME TITLE_ID NOTE_TEXTTITLE_DESC PRO_SUFFIX_ID PRO_SUFFIX_ABBV ADDRESS CITY STATE ZIP PHONE FAX TYPE TAX_1099 TAX_IDThere will be a parent-child relationship between attorney and attorney notes table on Attorney_ID. Designing the Business Entities Since we have two tables in this application, we will have to define two business entities; Attorney and AttorneyNote. This translates to two Resource Managers (Attorney and AttorneyNote), two data sources (AttorneyDataSource and AttornetNoteDataSource) and two validators (AttorneyValidator and AttorneyNoteValidator). Data Sources and Resource Managers To design the data sources and resource managers we need to ask ourselves, How will our presentation services need to retrieve the Attorney and Attorney Note data? When we answer this question we will be able to create the public Get() interfaces (methods) into our data sources. As mentioned earlier, to get the data, we define interfaces to the business entities based on the methods of resource retrieval. How many Get() interfaces you create is up to you, however, you can almost always be sure you will need to create at least a Get..ByID() method that accepts the primary ID of the record you want and returns that particular record. In our situation, we will have a GetAttorneyByID( ) and a GetAttorneyNoteByID(). This means that we will be able to retrieve, by ID, a specific attorney or attorney note record. In our application we are going to want to retrieve attorney notes related to an attorney somehow. We dont want the user to have to pick through all the notes when they already have picked an attorney. So we are going to want to automatically retrieve all the notes related to an attorney when the user picks an attorney record. That means we will need a Get() method called GetAttorneyNoteByAttorneyID(). This method will accept the attorney_id and return a resource of all the attorney notes for that attorney. Here are some more examples of Get() interfaces we may need: AttorneyAttorneyNoteGetAttorneyByID()GetAttorneyNoteByID()GetAttorneyByName()GetAttorneyNoteByAttorneyID()GetAttorneyBySearchCriteria()GetAttorneyNoteBySearchCriteria() The last two interfaces are the most interesting. They are going to allow the client to pass in a resource like an ADO recordset or an XML data stream that contains search criteria parameters. This is how we will facilitate flexible searching in our interface. This is because we will want to present the user with many search criteria parameters. To create a Get() method for every possibility would be painful. This way we have one generic method to retrieve data from user searches. Ill discuss the search criteria resources when we start coding the data sources. Validators The validators will be where we check to make sure each piece of data in a resource is valid when a save is performed. If the data is not valid, then a validation messanger object is created and passed back to the client. The messanger object can take the form of an XML stream which we can use for displaying validation messages on the HTML pages. Ill show you how we can do this later. The kind of validation we need in this application is simple. In the Attorney resource we will check to make sure the LastName is not empty. We will also check to make sure that the Attorney Note Note_Time field is a non-empty date. Process Objects Process objects or processes are components that work on multiple resources at the same time to execute processing. For instance, the Resource Manager Controller is a built-in process object. Its job is to validate and save multiple resources. In our simple attorney application we will not need any other process objects besides the Resource Manager Controller. Part 3 - Coding the Data Sources Now that we have reviewed the database and designed our business entities, we are ready to get to work! The first thing we are going to do is code our data sources. The data sources are Lets open Visual FoxPro and in the command window type: CD D:\FLASH\Codebook\AttorneyApp\Interface\DO StartCB This will open all four of the middle-tier projects; AttData, AttValid, AttResMgr and AttProcess as well as set up your working directories. It is important that you type these two lines every time you work with or recompile your projects. If you dont, VFP will not be able to find all the correct include files before building. The Data Source Project We are going to want to concentrate on the Data Sources first. The program files you see in the AttData project are what we need to create data sources. Most of them come from the COMCodebook Framework so you dont even need to worry about them. The three programs that start with the letter a are the programs I have provided for you. The two important ones are adatasources.prg and aparameterobjects.prg. This is where we are going to start entering some code. As you can see, there are not a lot of programs to go through. All the classes in the COMCodebook Framework are contained in .prg files. This not only makes it easier to work with source control software; it also allows you, as the programmer, to easily get the overall picture of the framework (without having to sift through methods of class libraries in the designer). As you work with the framework more and more, you will find that programming the middle-tier components is easy and quick. This allows you to concentrate harder on the processes and user interface. Lets open up the most important program file in our project, adatasources.prg. There you will see that I have already created the shell of the AttorneyDataSource class and have provided the full code for the AttorneyNoteDataSource class. As we work through the lesson I will be instructing you on where to enter the code in the AttorneyDataSource. I have already programmed the AttorneyNoteDataSource to save time, but you will find that its structure is exactly the same as the AttorneyDataSource. The Data Sources inherit from iAbstractADODataSource (located in idatasources.prg) which inherits from cAbstractADODataSource (located in cdatasources.prg). Lets start with the AttorneyDataSource. Youll notice that there are six properties that you will need to pay special attention to: Name cTableNamecTableID cSearchSyntaxClasscADOComponentsParameterClass cConfigurationObjectClass The AttorneyDataSources name is AttorneyDataSource, simple enough. The cTableName and cTableID properties tell us which table this data source corresponds to on Save. The cTableID is used to automatically fill the primary key on an insert of a new record. We will talk about the cSearchSyntaxClass property a little later in the lesson. So in our AttorneyDataSource we will fill the first three properties as follows: Name = "AttorneyDataSource"cTableName = "ATTORNEY"cTableID = "ATTORNEY_ID" The last two properties are a bit more interesting. The ADO Components Parameters The cADOComponentsParameterClass property holds the name of the concrete parameter class you define in the aparameterobjects.prg. Lets take a look at this file now. You will see that I have defined the ADO Component parameters for Attorney and AttorneyNotes: *********************************************************************DEFINE CLASS prm_AttorneyADOComponents AS iADOComponentsParameter********************************************************************* Name = "prm_AttorneyADOComponents" lEnableServerAssignedKeys = .T. nlocktype = 4 ncursortype = 1 ENDDEFINE *********************************************************************DEFINE CLASS prm_AttorneyNoteADOComponents AS iADOComponentsParameter********************************************************************* Name = "prm_AttorneyNoteADOComponents" lEnableServerAssignedKeys = .T. nlocktype = 4 ncursortype = 1 ENDDEFINE The two interesting properties of the ADO resource are nLockType and nCursorType. Having nLockType set to 4 here translates to having the ADO recordsets LockType property set to optimistic batch updates. In the same way, having nCursorType set to 1 here means that the ADO recordset use a keyset cursor. Take a look at CursorTypeEnum and LockTypeEnum in the MSDN library for more information. Now that we have set up these two ADO Component Parameter classes, we will put their names in the AttorneyDataSource and AttorneyNoteDataSources cADOComponentsParameterClass property. Going back to our AttorneyDataSource in the adatasources.prg we will set: cADOComponentsParameterClass = "prm_AttorneyADOComponents" The Connection Parameter Class The last property that is important to us is the cConfigurationObjectClass. This property holds the name of the concrete class that is used to connect to the database itself. Take a look at the aparameterobjects.prg again and scroll to the end of the file. There youll see that I have set up three connection parameter classes, prm_AttorneyApp_vfp, prm_AttorneyApp_SQL, and prm_AttorneyApp_Oracle. These three parameters are to be used to connect to VFP, SQL-Server and Oracle respectively. Although initially our example will only use the VFP connection, we will see how it is easy to set up your data sources to connect to different databases later. All you need to do is specify the corresponding connection classes in the data sources cConfigurationObjectClass property and write compliant SQL statements for that database. Its that simple! So back in our AttorneyDataSource we will set the cConfigurationObjectClass property like so: cConfigurationObjectClass = "prm_AttorneyApp_vfp" Setting Up the Connection Configuration File Now we will need to set up the connection configuration file for the connection classes. This tells them how to connect to the database. To understand exactly what goes into a configuration file, lets take a look up the hierarchy of the connection classes. Open up the cparameterobjects.prg and right-click to expose the shortcut menu. Select Procedure/Function List and select CAbstractADOConfigurationParameter.Init. This will bring your cursor to the abstract class that all the connection classes inherit from. Scroll up a little to see the property list for the class. Do you see the cConfigFile property? That property contains the name of the Connection Configuration file, in this case CONNCFG.DBF. If you wanted to change the name, then you would change it in your I-layer, not here. Okay, so lets modify our CONNCFG.DBF so that our data sources know where to get the data! The CONNCFG.DBF file is located in the same directory as the data source DLL (AttData.DLL). In our case it is in AttorneyApp\DataServices. Open up the CONNCFG.DBF in Visual FoxPro by clicking on it from explorer. You should see the following in a browse window: I have entered three connection parameters, prm_AttorneyApp_vfp, prm_attorneyapp_sql and prm_attorneyapp_oracle. Since we will be using VFP databases, enter the location of the attorneyapp data in the Cdatasrc field (i.e. D:\FLASH\CODEBOOK\ATTORNEYAPP\DATA\ATTORNEYAPP.DBC) If you keep scrolling to the left, you will see some additional fields. They are used with different types of databases. If you enter data in the fields that are not relevant to the particular database you are connecting to, the COMCodebook connection classes will ignore them. Here are the meanings of the fields relative to each database well discuss: FieldVFPSQLORACLECindexThe name of the Configuration ParameterThe name of the Configuration ParameterThe name of the Configuration ParameterCconnecttoThe Connection TimeoutThe Connection TimeoutCdatasrcThe Location of the DBCThe SQL-Server Server nameThe Oracle Service NameCinitcatThe SQL-Server Database nameCpasswordA valid database passwordA valid database passwordCsecinfoSecurity InformationCproviderThe OLE DB Provider Name (MSDASQL.1)The OLE DB Provider Name (SQLOLEDB.1) The OLE DB Provider Name (MSDAORA.1)CuseridA valid database user idA valid database user idMspecial This information is used by the COMCodebook connection classes to set up the valid connection strings to the particular databases. For more information on connection strings See article Q193332 in the Microsoft Knowledge Base. The last field in the table is a memo field called Mspecial. This field is used to extend the connection strings if necessary. Since we cannot anticipate every database connection string syntax out there, this field is used for special cases. For Oracle, VFP, and SQL-Server this field is not necessary. For information on creating special connection strings, refer to the documentation in the CspecialConnectionString class in cparameterobjects.prg. The Get() Interfaces Are we having fun yet?! Now it is time to write our public Get() interfaces! Back in our adatasources.prg you will see that I have started the shell of all the AttorneyDataSource.Get() methods we want to code for our application. If you recall we designed three Get() interfaces for each data source: AttorneyAttorneyNoteGetAttorneyByID()GetAttorneyNoteByID()GetAttorneyByName()GetAttorneyNoteByAttorneyID()GetAttorneyBySearchCriteria()GetAttorneyNoteBySearchCriteria() I have put comments in the areas where we will need to code the SQL statements to retrieve the data. You should also happen to notice that there is a GetProxy() method in the classes. *------------------FUNCTION GetProxy()*------------------ LOCAL loADO loADO = THIS.GetADOAggregateParameter() loADO.oCommand.ActiveConnection = loADO.oConnection *-- INSERT SQL HERE loADO.oCommand.CommandText = [ ] THIS.ExecuteSQLQuery( loADO ) RETURN loADO.oRecordSet ENDFUNC What is a GetProxy()? GetProxy() is your default multi-select interface, usually returning simply a Name and ID from the table. For instance, if the client needs to fill a pick-list or combobox, it could easily call the GetProxy() method to retrieve this information. What shall we return from our AttorneyDataSource.GetProxy()? How about the Attorney LastName, FirstName and the Attorney_ID? So our CommandText would look like so: loADO.oCommand.CommandText = [SELECT RTRIM(LastName)+ ", " +FirstName AS cDesc, Attorney_id AS ] +; [ upID FROM Attorney ORDER BY Lastname, FirstName ] Our next interface is the most important, GetAttorneyByID(). Here, the client will pass in the primary ID of the Attorney record and we will return the record they asked for in an ADO recordset resource. It is ultimately up to us as the data source programmers to return all the fields or only some of them. We could even rename them to more meaningful names. This goes right along with the theory that the presentation programmer is completely insulated from the actual database tables and fields themselves. They can only access the data how we want them to. Whether you want to expose the names of your fields or not is up to you. It also depends on how many long SQL statements you are willing to write! Take a look at the AttorneyDataSource.GetAttorneyByID(): *---------------------------------------FUNCTION GetAttorneyByID( tuAttorneyID )*--------------------------------------- LOCAL loADO, lopAttorneyID loADO = THIS.GetADOAggregateParameter()loADO.oCommand.ActiveConnection = loADO.oConnection *-- INSERT SQL HEREloADO.oCommand.CommandText = [] *-- CONSTRUCT YOUR PARAMETER(S)lopAttorneyID = loADO.oCommand.CreateParameter( )loADO.oCommand.Parameters.Append( lopAttorneyID ) THIS.ExecuteSQLQuery( loADO )RETURN loADO.oRecordSet ENDFUNC In our simple application, since we will be writing the presentation as well, it is no secret to us what the names of our fields and tables are. In this case I am going to return all the fields with their real names exposed: loADO.oCommand.CommandText = [SELECT Attorney.* FROM Attorney WHERE Attorney_ID = ? ] lopAttorneyID = ; loADO.oCommand.CreateParameter("Attorney_ID", adVarChar, adParamInput, 9, tuAttorneyID ) Heres an interesting line of code we havent seen before. This is the way you construct ADO parameter objects to be able to have parameters in the SQL strings. We are creating an ADO Parameter object setting it up with the value of tuAttorneyID to append to the command object. If you are not familiar with ADO command and parameter objects, see Command object (ADO) in the MSDN Library. The next interface is GetAttorneyByName(). This method is very similar to the GetAttorneyByID(), but this time we have two search parameters instead of just one (LastName and FirstName) and the resource we return may have more than one record in it. Take a look at the code we need to write highlighted in bold: *---------------------------------FUNCTION GetAttorneyByName( tcLastName, tcFirstName )*--------------------------------- LOCAL loADO, lopLastName, lopFirstName loADO = THIS.GetADOAggregateParameter() loADO.oCommand.ActiveConnection = loADO.oConnection *-- INSERT SQL HERE loADO.oCommand.CommandText = [SELECT LastName, FirstName, Phone FROM Attorney WHERE LastName LIKE ? AND FirstName LIKE ? ORDER BY LastName, FirstName] *-- CONSTRUCT YOUR PARAMETER(S) lopLastName = loADO.oCommand.CreateParameter( "LastName", adVarChar, adParamInput, 20, ALLTRIM( tcLastName ) + "%" ) lopFirstName = loADO.oCommand.CreateParameter( "FirstName", adVarChar, adParamInput, 15, ALLTRIM( tcFirstName ) + "%" ) loADO.oCommand.Parameters.Append( lopLastName ) loADO.oCommand.Parameters.Append( lopFirstName ) THIS.ExecuteSQLQuery( loADO ) RETURN loADO.oRecordSet ENDFUNC Handling Search Criteria Resources Now that we have finished our standard get methods, you should notice that even though we may have search parameters, they are hard coded to search specific fields in the tables. (i.e. Attorney_ID = ?) Also, the order that the records are retrieved is also fixed. How will we easily allow our presentation services to search on other fields and order the records returned? We need to think about how we can create flexible WHERE and ORDER BY clauses on our SELECT statements. The answer is the Search Criteria Manager (SCM). Built into the COMCodebook framework is a standard method of retrieving search criteria parameters based on the data sources underlying tables fields. If you recall, earlier we went through the important properties of the a-layer data sources. We skipped the cSearchSyntaxClass property. The cSearchSyntaxClass property of the data source holds the Search Syntax Class name that is used by the SCM. Since there are many diferent databases, there can be diferent syntax used by those databases to construct the clauses of the SELECT statements. We must tell the SCM which Syntax Object to use when constructing our clauses. The SCM provides the flexible searching we will need for our presentation services. There are two SCMs provided for you in COMCodebook, CSearchCriteriaManagerXML and CSearchCriteriaManagerRS which are subclasses of CAbstractSearchCriteriaManager. Like all parts of COMCodebook, you can extend the SCM objects and write your own search manager objects. For more information on complex searching and a complete tour through the SCM object model, please see the Creating Flexible Searches in COMCodebook Datasources.doc found on the COMCodebook Download page. The Search Criteria Manager does two things. First, it provides the client with a search criteria resource in the form of an ADO recordset or an XML stream by way of two public interfaces of the data source class; GetXMLSearchCriteriaResource() and GetRSSearchCriteriaResource(). The client can then fill in the appropriate field values and then send the resource back into the Get BySearchCriteria() interface. The data source then calls upon the proper SCM again to do its second thing; construct the clauses of the data sources SELECT Statements by using the Syntax Object. This is done by calling the method CreateFilterFromResource() in the Get..BySearchCriteria() method. Take a look at your GetAttorneyBySearchCriteria() interface in the adatasources.prg and enter the SELECT statement shown below: FUNCTION GetAttorneyBySearchCriteria( tuFilter ) *------------------ LOCAL loADO, lcSelect loADO = THIS.GetADOAggregateParameter() loADO.oCommand.ActiveConnection = loADO.oConnection *-- INSERT DEFAULT SQL HERE lcSelect = [SELECT Attorney.* FROM Attorney ] If THIS.CreateFilterFromResource( tuFilter ) = FILE_OK With this.oSearchClauses loADO.oCommand.CommandText = lcSelect + .cWhere + .cOrder EndWith Else loADO.oCommand.CommandText = lcSelect + [ ORDER BY Lastname, Firstname] Endif THIS.oSearchClauses = Null THIS.ExecuteSQLQuery( loADO ) RETURN loADO.oRecordSet ENDFUNC The clauses that the Syntax Object constructs are placed into an object that you reference via the oSearchClauses property of the data source. We can then reference the WHERE and ORDER BY clauses of our select statement in the properties cWhere and cOrder. I know this might sound like a lot of work, but the beauty is that COMCodebook has practically done all of it for you. Besides programming the Get..SearchCriteria() method above, all we need to do is determine if the default set of fields is enough to provide to our presentation services. By default, if the client calls the Attorney.GetRSSearchCriteriaResource() then all the fields from the attorney table (and only attorney) will show up in our search criteria resource. An ADO recordset search criteria resource has the following character fields: Table The name of the base tableName Field NameType ADO DataTypeEnum represented as a characterValue Field ValueEndValue Field End Value (for retrieving ranges of data)Order The order Operator The operator keyword; LIKE, CONTAINS, BETWEENOptions Order options like DESCENDING By default, when the client calls up a search criteria resource, the fields Table, Name, Type and Operator are filled in. It is up to the presentation services to fill in the rest of the criteria (Value, EndValue, Operator, Order, Options) appropriately and pass it back into the Get BySearchCriteria() interface. The way the particular Search Criteria Managers decompose the search criteria resource to create valid clauses depends on the Syntax Object being used. COMCodebook provides a standard CAbstractSearchSyntax class in csearchcriteria.prg and also sub-classes CVFPSearchSyntax, CSQLSearchSyntax and COracleSearchSyntax so you can immediately use the flexible searching with these databases. I should also mention that the search criteria resource structure is also extendible, like most things in COMCodebook. If you need to provide additional search criteria properties to your presentation services, you can take a look at the comments in the IAbstractSearchCriteriaManager class in isearchcriteria.prg that I have supplied. In our application, both the attorney and attorney note data sources use a Visual FoxPro Database, so we will need to specify the SearchSyntax_VFP as the cSearchSyntaxClass: cSearchSyntaxClass = "SearchSyntax_VFP" Our AttorneyDataSource will not need to provide any additional search fields to the presentation services, however, take a look at the AttorneyNoteDataSource.AddSearchCriteriaInfo_Post method near the bottom of the adatasources.prg. There I have specified to add Attorney.LastName to the search so that we could look up notes on an attorneys last name. *------------------ PROTECTED FUNCTION AddSearchCriteriaInfo_Post( roRS )*------------------ With This.oSearchReferences.oSearchCriteriaManager .AddSearchElements( @roRS, "Attorney", "LastName", adChar ) Endwith Return FILE_OK Endfunc The way we add records to our search criteria resource is by way of the Search Criteria Manager. A reference to this object is contained in the Search References object. The Search References object is a special parameter object defined in cparameterobjects.prg. What we are doing above is adding properties and their values of a single row or node of data to the SCM. We are sending the Search Criteria Resource, the table name, the field name, and the type of data to the SCM to add to our resource. The last thing to do is to add the attorneys last name to the SELECT statement in the GetAttorneyNoteBySearchCriteria(). lcSelect = [ SELECT Attnote.*, RTRIM(Attorney.LastName) + ', ' + Attorney.FirstName ] + ; [ AS FullName FROM AttNote, Attorney ] If This.CreateFilterFromResource( tuFilter ) = FILE_OK With this.oSearchClauses .cWhere = iif(empty(.cWhere), [ WHERE ], .cWhere + [ AND ]) .cWhere = .cWhere + [ Attnote.Attorney_Id = Attorney.Attorney_ID ] loADO.oCommand.CommandText = lcSelect + .cWhere + .cOrder Endwith . . . Now we can allow the presentation services to search and order on Attorney.LastName for attorney notes as well as any of the fields in the Attorney Note table! When we program the user interface, Ill show you how we will work with the search resources themselves. Getting GetPrimaryKey() to Work For You GetPrimaryKey() is defined in the CAbstractADODataSource class in cdatasources.prg. Here is where the data sources retrieve primary keys for new records in their resources before they save them. The default is to call a stored procedure called sp_NewID to retrieve a primary key from the database. It executes the stored procedure through the ADO Command Object which returns the primary key value into an ADO Parameter Object. If the name of your primary key generator is different than sp_NewID and/or it accepts/returns different parameters, you can overwrite this method in your i or a-layers. If you will be developing applications against existing databases, you may need to enhance the GetPrimaryKey() function. For instance, if you needed to generate character primary keys, we could define a new GetPrimaryKey() method. For our purposes here, since we are working with a VFP database that is exactly what we are going to do. We will define a simpler GetPrimaryKey() method using a handy VFP SYS() function. Take a look at the bottom of the adatasources.prg. You will see a class definition for aAbstractADODataSource. There is the GetPrimaryKey() method we are going to use. ************************************************************************DEFINE CLASS aAbstractADODataSource AS IAbstractADODataSource************************************************************************ PROTECTED FUNCTION GetPrimaryKey( rcPrimaryKey, toADO ) LOCAL lnRetVal, lcPrimaryKey, lnStartIndex lnRetVal = FILE_OK lcPrimaryKey = RIGHT(SYS(2015), 9) IF .NOT. ISNULL( lcPrimaryKey ) rcPrimaryKey = lcPrimaryKey ELSE lnRetVal = FILE_ERROR THIS.RecordError( 999999 , ; "GetPrimaryKey()" , ; 12 , ; "Primary Key could not be generated." ) ENDIF RETURN lnRetVal ENDFUNC ENDDEFINE This method uses the VFP SYS(2015) function to generate a unique ID. The name that SYS(2015) returns is created from the system date and system time. Calling SYS(2015) more than once during the same millisecond interval will return a unique character string. Granted, this is not the most robust way of generating unique ids, but if you were running these components all from the same server, you could be almost certain you would get a unique primary key every time. Using Character Primary Keys By default, COMCodebook assumes you are using integer primary keys in your database when deleting records, and in most cases you probably will. However, there may be times when you are using character fields as the primary keys of your tables as we are in our VFP database. Because of this assumption, you will need to override the default behavior of the data source method DefineDeleteSQL() which is responsible for generating the delete SQL statement syntax for the Delete() method. By default in the c-layer the method looks like this: PROTECTED FUNCTION DefineDeleteSQL( tcID, rcDeleteSQL ) LOCAL lnRetVal IF .NOT. EMPTY( THIS.cTableName ) AND .NOT. EMPTY( THIs.cTableID ) rcDeleteSQL = "DELETE FROM " + THIS.cTableName + " WHERE " + THIS.cTableID + " = " + tcID lnRetVal = FILE_OK ELSE rcDeleteSQL = "" lnRetVal = FILE_ERROR ENDIF RETURN lnRetVal ENDFUNC To make the delete work for character IDs the select statement must read: rcDeleteSQL = "DELETE FROM " + THIS.cTableName + " WHERE " + ; THIS.cTableID + " = '" + tcID + "'" Open your adatasources.prg. Notice right below the GetPrimaryKey() method, I have defined a DefineDeleteSQL() method with this code that includes the single quotes necessary to construct a valid SQL statement. Using this function will allow our deletes to work with character primary keys. This is another great example of how we can enhance the framework easily to fit our needs. In the last part of this document, we will discuss how to upsize our database into SQL Server and Oracle. We will use integer primary keys in those examples to demonstrate how to use the COMCodebook default behavior for generating primary IDs. Done With Data Sources Okay, I know that seemed like a lot of work, but it was your first run. And the data sources are the components with the most amount of code (besides the presentation layer). Among other things, we learned how to program our Get() interfaces as well as extend our search criteria resource. Next we will look at how easy it is to code the validators and the resource managers. They practically code themselves! Part 4 - The Resource Managers, Validators and Processes Coding the Resource Managers Resource Managers are the presentation tiers view into the business tier. They are business aware sets of components that provide the public interface used by the front-end developer. They know what data sources and validators to use. The resource manager project for our sample application is AttResMgr.PJX. Open up the project in Visual FoxPro: As you can see, there are a lot less files in here than the data source project. This is because the data sources are our work-horses. They are responsible for talking to the databases directly. The resource managers on the other hand are simply a façade into our business model. They provide a standard interface into any business resource that we need to access, and as such do not require much custom coding. Data sources require that we understand both the data structures and the resources that we need to access. Resource managers simply require that we know the name of a resource. Think of the data source as building a car from a kit, while the resource managers are equivalent to buying that car from a dealer. In the first case, you need to understand all the parts and how they should go together. The second case allows you to simply ask for the name of the car. The Get() Interfaces Programming the Get() interfaces of the resource managers is simple once the data sources are programmed. This is because the resource managers have all the same interfaces as the data sources. Lets take a look at the GetProxy() method of the Attorney resource manager by opening the aresourcemanagers.prg: *------------------FUNCTION GetProxy()*------------------ LOCAL loDataSource, loADORecordSet loDataSource = .NULL. && The Data Source loADORecordSet = .NULL. && The Resource THIS.GetDataSourceObject( @loDataSource ) loADORecordSet = loDataSource.GetProxy() RETURN loADORecordSet ENDFUNC As you can see all that the resource manager is really doing is calling the AttorneyDataSource.GetProxy(). Here is a little bit more complicated interface because we need to pass two parameters to the data source: *---------------------------------FUNCTION GetAttorneyByName( tcLastName, tcFirstName )*--------------------------------- LOCAL loDataSource, loADORecordSet loDataSource = .NULL. && The Data Source loADORecordSet = .NULL. && The Resource THIS.GetDataSourceObject( @loDataSource ) loADORecordSet = loDataSource.GetAttorneyByName( tcLastName, tcFirstName ) RETURN loADORecordSet ENDFUNC The resource manager Get() methods always accept the same number of parameters as the data source Get() methods and call the data source passing in those parameters. Sounds simple, right? Well it is. However, you might be asking yourself How does the resource manager know what data source to call? To answer that we must look at how COMCodebook manages the resource managers collaborating objects. Defining Participating Entities All the objects used in a resource managers lifetime are what we call participating entities or collaborating objects. They are all the objects that work together to get things done. For instance, the resource manager needs to know what data source and which validatior objects to use when getting and saving resources. Lets look to the Attorney.DefineParticipatingEntities() method in adatasources.prg: *-------------------------------------PROTECTED FUNCTION DefineParticipatingEntities()*------------------------------------- DIMENSION THIS.aEntityReferences[3,4] THIS.aEntityReferences[1,1] = "AttData.AttorneyDataSource"THIS.aEntityReferences[1,4] = "AttorneyDataSource" THIS.aEntityReferences[2,1] = "AttValid.AttorneyValidator"THIS.aEntityReferences[2,4] = "AttorneyValidator" ENDFUNC What we are looking at here is the vital information the resource manager needs to call its data source and validator. The aEntityReferences array property holds the names of the data source and validator components along with other information that is filled in by the COMCodebook framework and used on save. The two columns you must fill in on the a-layer is column 1; the name of the class definition to use (i.e. AttData.AttorneyDataSource) and column 4; the alias used to reference this object (i.e. AttorneyDataSource). Methods in the resource manager like GetDataSourceObject() use this information to instantiate the necessary objects. You should notice that we are once again placing a classs name into a property. Remember we did this in our data sources cSearchSyntaxClass, ADOComponentsParameterClass and cConfigurationObjectClass properties.This is handy for two reasons. For one, it keeps collaborating object names together in one section of code. Second and more importantly, it allows us to easily change which object to use without having to search for the line of CreateObject() code where we instantiate the object (which could possibly be far up the framework hierarchy). Consider this piece of code: Obj = CreateObject("AttData.AttorneyDataSource") As opposed to this: THIS.cClassName = "AttData.AttorneyDataSource"Obj = CreateObject(THIS.cClassName) The two pieces of code do the exact same thing; they create a reference to an AttorneyDataSource object in the AttData.DLL. The benefit of the latter is that we can now separate our class name from the actual instantiation of it. Youll see a big benefit for doing this when we talk about debugging in the next section. Debugging VFP components is a lot easier when they are contained in the same project. We could easily change our THIS.cClassName in the example above to simply AttorneyDataSource which would tell VFP to instantiate a local VFP class instead. This way we could compile our data sources and resource managers together and debug them easier than if they were in separate DLLs. More on how to do this later. Coding the Validators The validators are responsible for field by field or row by row validation of a single resource. In our design of the validators we defined two rules. In the Attorney resource we will check to make sure the LastName is not empty. In the Attorney Note resource the Note_Time field must be non-empty date. Lets open up our AttValid project if its not already open. There is only one .prg file in this project that you have to worry about, avalidators.prg. Here is where we define the validation rules for the fields. Scroll down a little to the class definition for the AttorneyValidator. Type in your validation code: *------------------------------------------------------DEFINE CLASS AttorneyValidator AS aValidator OLEPUBLIC*------------------------------------------------------ Name = "AttorneyValidator" *------------------------------------------ FUNCTION vldLastName( tcValue, tlPreStatus, roMessage) *------------------------------------------ LOCAL llRetVal *-- Define our business rule here llRetVal = .NOT. EMPTY( tcValue ) *-- If NOT llRetVal If Vartype(roMessage) = "O" roMessage.AddMessage("AttorneyEmptyLastName", THIS.Name ) Endif Endif RETURN llRetVal ENDFUNC ENDDEFINE Since our simple application has simple validation rules, our class definitions are also simple. You see how the name of the function corresponds to the name of the resources field with the prefix vld? This is how the resource manager calls the corresponding field-by-field validation. Remember, this field name is not necessarily the same name as the field in the underlying table. It is the name your data source assigns this field via your select statements. In our case they happen to be the same. If the validation fails, (llRetVal = .F.) a message is added to the validation messanger object. This message is defined in the AttorneyApp\Validation\MSGSVC.DBF file. This VFP table defines the validation keys and messages for our application. It only contains two records because we only have two business rules. If we had more, we would define them here. For each record we need to fill in at least two pieces of information: the ckey field and the coriginal field. The ckey field is the name of the key that is being passed to the AddMessage() method. The coriginal field is the validation text that will be displayed when a validation fails. There is a field called cguivisual that is optional and is the name of a picture you may want to display. If you are interested in reading about the validators in more detail, please read the whitepaper HYPERLINK "http://www.comcodebook.com/WhitePapers/validating_data_in_comcodebook.htm" Validating Data In COMCodebook by Michael Emmons. If you scroll down a little, youll notice I already wrote the AttorneyNoteValidator class definition for you. The important validation line is: llRetVal = .NOT. EMPTY( tdValue ) AND .NOT. ISNULL(tdValue) Simple stuff! Even though the validators are simple to program, do not underestimate their role in the middle-tier. They save your database from rejecting records that dont follow the rules. Performing these rules before the resources are even lined up to be saved saves you a trip to the database. That means better performance and scalability. The Processes The last project I want to take a look at is the Process project, Processes\AttProcess.pjx. This project contains the resource manager controller (RMC) process object. We talked a little bit about the RMC in the COMCodebook Theory section. Just remember that the RMC is a built-in process object that handles multiple-resource/multiple-record saves. Well get into how to use the RMC when we build our front end. For now, just be aware that we have a project for all our process objects. If we were to define more processes in our application, we could put them here. This simple attorney application is only going to use the built-in process object, the Resource Manager Controller. Take a look at the AttProcess.pjx file: The aprocess.prg contains our concrete class definition for the Resource Manager Controller, RMController. We do not need to do any additional coding in this project since we do not have any additional processes in our application. Done With Resource Managers, Validators and Processes Do you know what you just did? You just coded all the pieces that make up the middle-tier of our Attorney application. Congratulations! We learned how easy it is to create the resource managers once the data sources are written. We also saw how the collaborating objects names are stored in properties of the resource manager to make it easy to modify and debug. Next step is compiling and registering the components. Part 5 Compiling, Testing and Debugging Compiling the Components Now that we have put all our pieces together it is finally time to build the projects. First make sure your paths are set up correctly. In the VFP Command Window type: CD D:\FLASH\Codebook\AttorneyApp\Interface\DO StartCB This will set up your development environment and open up all four projects, AttData, AttResMgr, AttValid, and AttProcess. Lets build the data source project AttData first by clicking on the build button on the project manager. Under the Build Options, select Multi-threaded COM server (dll) then click OK. Go ahead and leave the default \DataServices directory to place the .DLL file into. Visual FoxPro will whiz through compiling all the prgs into a DLL and register the COM component in your registry. Now build the AttResMgr, AttValid and then the AttProcess the same way you built AttData accepting the default directories to place the DLLs (\ResourceMgrs, \Validators and \Processes respectively). To show you that VFP registered your components, lets take a look at the registry: As you can see, VFP has compiled all your code and registered all the components. Easy stuff! Breaking Your Interface Id like to take a moment to mention the problems you will run into when compiling components where you have broken your interface. Visual Basic, Visual C++ and Java are early-binding or vtable-binding languages. This is unlike Visual FoxPro, or scripting languages like VBScript which are late-binding. Early-binding languages can strongly-type their variables. This means that the calls to functions on the components and their signatures are resolved and compiled into the application. This allows for faster access to the components in these languages because the calls are resolved at compile time. In a late-binding language like VFP or VBScript, the component function calls must be resolved at runtime. When you remove a public method in a class definition in your component you will break the early-binding VB or VC++ client code. This can also happen when you change a public methods calling syntax in your component, like adding or removing parameters. So if you have a VFP component being accessed from an early-binding language, you should not change the calling syntax for accessing any public method or property because of the risk of the dependencies the early-binding components have built on the original class definition. If you do happen to break an interface during development, and you probably will, you will have to modify and re-build all the components that rely on that interface to use the new calling syntax. This is up to you to remember. Visual FoxPro will happily re-compile your components without warning. You will receive obscure error messages from the calling code if the interface is broken. Visual Basic usually gives you a Type Mismatch error. If all else fails, re-compile all your components. For more information on vtable-binding see Building, Versioning, and Maintaining Visual Basic Components by Ivo Salmre in the Online MSDN library at HYPERLINK http://msdn.microsoft.com/isapi/msdnlib2.idc?theURL=/library/techart/msdn_bldvbcom.htm http://msdn.microsoft.com/isapi/msdnlib2.idc?theURL=/library/techart/msdn_bldvbcom.htm. Testing Your Components Calling the Get() Interfaces Whew! We are finally ready to test our components. Lets run a simple test through the Visual FoxPro command window. Type: oAttorney = CreateObject("AttResMgr.Attorney") You just instantiated the Attorney resource manager. Now lets request some data. Type: rs = oAttorney.GetProxy() The rs variable is your resource. In our case an ADO recordset was returned. To browse through the fields, type these lines of code: ? rs.Fields(0).Value? rs.Fields(1).Valuers.MoveNext You can also refer to the field object by name. For instance rs.Fields(0).Value is the same as rs.Fields(cDesc).Value. Remember in our AttorneyDataSource.GetProxy() method we defined the fields returned as cDesc and upID. Play around with the other interfaces we defined like GetAttorneyByName(), and create an AttorneyNote resource and test it as well. When you are finished with your resource manager you should set the reference to NULL like so: oAttorney = Null This will release the Attorney resource manager object. Calling the Get..BySearchCriteria() Interface Lets try something a little harder by testing our GetAttorneyNoteBySearchCriteria() interface. To test it, we need to send it a search criteria resource. How do we do that? We must first decide what kind of resource we want, an ADO recordset or an XML stream. For testing, lets get an XML stream. (Ill show you how to manipulate an ADO search criteria recordset in ASP when we build our web-interface.) First we must instantiate the AttorneyNote resource manager. In the command window type: oAttNote = CreateObject("AttResMgr.AttorneyNote") Then we will call the GetXMLSearchCriteriaResource() interface: XML = oAttNote.GetXMLSearchCriteriaResource() This will return an XML stream with all the search criteria elements. To make manipulating the XML stream by hand easier, lets save the XML string variable to a file by typing the following: STRTOFILE(XML, "D:\attnote.xml") VFP will save the contents of the XML stream to a file on your D:\ drive called attnote.xml. You can open the .xml file by clicking on it in Explorer. If you have Internet Explorer 5.0 or higher this should open the XML file in the browser. You can collapse and expand the FIELD nodes on the document by clicking the minus (-) and plus (+) signs on the nodes. Notice that all the fields from the ATTNOTE table are there plus the ATTORNEY.LastName that we added in our AttorneyNoteDataSources AddXMLSearchCriteriaChildNodes_Post() method in the adatasources.prg. Well this is great, but how do we enter some search criteria? Internet Explorer only displays the XML, it does not allow you to manually edit it. To edit it, open the file D:\AttNote.xml in notepad. There we can edit the XML stream by hand. Remember, we are only doing it by hand to test. If we were writing a user interface, it would have the code in it to parse the XML stream automatically filling in user search criteria. The Microsoft XML parser is an excellent tool for manipulating XML files. But for right now, lets type in some search criteria by hand. Scroll down to the last FIELD node, LastName. Lets put an F in the Value element like so: