![]() |
![]() |
![]() |
A major advantage of TAO and OpenDDS, open-source implementations of the Object Management Group's CORBA and the Data Distribution Service, is the wide variety of platforms to which they have been ported. While retaining platform neutrality is a worthy goal, the dominance of Microsoft Windows in the PC marketplace, over 90% [1] market share as of the writing of this article, encourages the use of Windows-specific features when developing for that platform.
Since the release of Visual Studio.NET in 2002, Microsoft's direction for development has been that of the .NET Framework [2]. In a manner similar to Java bytecode, high-level languages are compiled into an assembly-like intermediate language, standardized as the Common Language Infrastructure by Ecma International in ECMA-335 [3], which is then ultimately compiled and run on the target machine.
Although special languages such as C# have been created for .NET development, C++ has the ability to use the .NET Framework as well. How well-integrated C++ code is with the .NET Framework, though, is dependent upon how many code changes are made to conform to the new C++/CLI syntax. As TAO and OpenDDS are written in standard C++, this article series will show how they can be adapted to be used in a .NET application.
For several years, OCI has been engaged with a customer in the maintenance of a legacy data-acquisition application. Data is collected by remote sensing devices and stored in a database, and the sensing devices are managed, and the data viewed, by an application written for Microsoft Windows.
Although originally a single-user application referencing a local database, over time the application has evolved into one where multiple users can simultaneously connect to a single centralized database. If one user makes a change to the database, all other connected users must be made aware of the change so their local states can be updated.
A solution to this problem is to create a single process to manage access to the database, and to provide database change notifications to interested client applications. Having a single process interact with the database on behalf of clients, instead of allowing each client direct access to the database, ensures that no database change can be made without the system, as a whole, becoming aware of it. Additionally, abstracting the details of the database access from the clients allows the database process be moved to other hosts, or even be implemented on a different platform, without requiring more than just a reference to the new location of the database process to be changed in the configuration of the clients — no client code changes would be necessary.
TAO and OpenDDS were selected as the middleware to accomplish this task for two main reasons. The first is that it is easy to be up and running quickly — the prototype that was developed to illustrate the architecture was completed in under three days. The second is that, as both TAO and OpenDDS are open source, there are no license fees or other costs involved. The resulting application can be deployed widely without incurring a per-seat, or per-CPU, or even a development/SDK charge.
The existing legacy application interacts with a database via ADO.NET [4], a series of classes provided by the .NET Framework, which provide a uniform means of accessing various data source types. The code that uses ADO.NET is in a library, written in C#. The main application that uses this library is written in C++.
In order to solve the problem outlined above, the following architecture was designed. In this diagram, components written in C# shown with box hatching and components written in C++ shown with angled hatching.
The separate application to manage the database, called the DataServer, was written. This application, in C++, uses the same C# library as before. It now, however, acts as a CORBA server, processing requests from clients to perform database operations. As operations are performed, the DataServer publishes a DDS data sample to clients to notify them of the results of the operation. DDS is used instead of the CORBA Notification Service for notifications as DDS samples are strongly typed (rather than being the CORBA Any type), plus DDS provides quality-of-service policies that the Notification Service does not. This article describes the core elements of the DataServer, as well as a client application, written in C#, that makes use of it. The client performs CORBA operation invocations, and subscribes to the DDS data samples. This article will also illustrate the use of MPC for project maintenance.
SIDEBAR
The code in this article was developed with Microsoft Visual Studio 2005.
It was compiled against
TAO version 1.6a,
OpenDDS version 1.3,
and MPC version 3.7.2.
Inline assembly was disabled
to prevent the .NET-related compiler warning C4793, as the use of __asm forces native code
generation. Wide character support was enabled, as .NET uses Unicode for
string representation. The build settings for these features are as follows:
// add to %ACE_ROOT%\ace\config.h #define ACE_LACKS_INLINE_ASSEMBLY 1 #define ACE_USES_WCHAR 1 // add to %ACE_ROOT%\bin\MakeProjectCreator\config\default.features uses_wchar=1
The core functionality of the Data Server is provided by DataLib, a library written in C# which interacts with the database. For this example, we'll use System.Data.SQLite, a public domain SQLite ADO.NET provider.
DataLib consists of a single file, DataLib.cs, containing a single class
named Database, and referencing several .NET libraries as well
as System.Data.SQLite. The structure of the Database class
is below. Methods are provided to open and close the database, where, for
simplicity, a single database connection is used. Additional methods are
provided to create, read, update and delete records in an item table, where
an item has both an autogenerated numeric ID and a description. For
the implementation of these methods, please see the
code archive that
accompanies this article.
namespace DataLib
{
using System;
using System.Data;
using System.Data.SQLite;
public class Database
{
// open and close the database connection
public bool Open()
public void Close()
// create a new item, and return the autogenerated ID
public bool CreateItem(string description, out Int64 id)
// read the description from a specific item, given the item ID
public bool ReadItem(Int64 id, out string description)
// update the description of an item given its ID
public bool UpdateItem(Int64 id, string description)
// delete an item, given its ID
public bool DeleteItem(Int64 id)
}
}
Although the DataLib project can be created from within Visual Studio, using MPC (the Makefile, Project, and Workspace Creator) provides a number of benefits:
.mpc) files can inherit from base project
(.mpb) files, allowing settings such as output directory specification or
the setting of a warning level to be made in one place and applied across all
projects. If these settings were changed within Visual Studio, changes would
have to be made over and over again, once for each project in the solution.Documentation for MPC can be found here, though we will describe features of MPC that are useful for this application.
As this system will consist of several projects, we create
a base project to allow settings that all projects should inherit. We
also set an environment variable, DATASERVER_ROOT, to represent
the top-level directory of the project. This allows us to move the entire source tree
while correctly maintaining any full paths used in the project files.
The base of all projects in the workspace is named DataServerBase.mpb,
and has the following contents:
// DataServerBase.mpb
project {
specific {
Release::install = $(DATASERVER_ROOT)/Output/Release
Debug::install = $(DATASERVER_ROOT)/Output/Debug
warning_level = 4
}
}
When generating projects for Visual Studio, it is important to remember that
the variables set in the MPC or MPB file reflect ones set in the Visual
Studio IDE. Generally, settings which correspond to strings are set
directly in the MPC or MPB file, but those that represent dropdowns
are set by the numeric index of the choice of interest in the dropdown. In
this case, output directories are specified by name, but the warning level
is set numerically to 4, which represents the choice of /W4 in the IDE.
As this project will contain different types of projects, written in
different languages, it is useful to define base projects which
apply to subsets of projects in the workspace. The contents of the file
CSBase follows, the base for all C# projects.
// CSBase.mpb
project : DataServerBase {
specific {
// to avoid "Load of property 'ReferencePath' failed. Cannot
// add '.' as a reference path as it is relative. Please specify
// an absolute path." on C# project load into the IDE
libpaths -= .
}
}
By default, MPC adds the current directory to the list of directories
where libraries are found, though Visual Studio will generate a warning if the
directory is not an absolute path. The entry above removes the current directory
from the library paths. This project inherits from DataServerBase, so settings
that are made in DataServerBase are applied in addition to what is in this
file.
Finally, an MPC file is needed for the project itself. DataLib.mpc
is as follows:
// DataLib.mpc
project : CSBase {
// To remove the warning "Load of property 'ReferencePath' failed.
// Cannot add '..\lib' as a reference path as it is relative. Please
// specify an absolute path."
expand(DATASERVER_ROOT) {
$DATASERVER_ROOT
}
lit_libs += System System.Data System.Xml
lit_libs += System.Data.SQLite
libpaths += $(DATASERVER_ROOT)\lib
}
The expand option causes the environment variable to be treated
as an absolute path — by default, MPC converts environment variables to relative
paths. As with the previous issue with libpaths, this also
prevents a warning in Visual Studio from being generated when the
lib subdirectory is added.
As this is a .NET application, references to various .NET assemblies must be
provided, in addition to the System.Data.SQLite assembly, which provides the
database connectivity. For this example, System.Data.SQLite.dll is located
in the lib subdirectory off of the main project directory, and the
libpaths entry adds that directory to the library path.
This project inherits from CSBase, so has all of the settings supplied by
CSBase.mpb and DataServerBase.mpb.
It is interesting to note what does not need to be specified. As this is an
MPC file for a C# project, you do not need to provide specific names of
.cs files — all .cs files in the same directory as the MPC
file are automatically included. Also, you do not need to specify a file
name for the output — in this instance, MPC will use the base name of the MPC
file, which is what we want.
With the project file created, the last step is to create a workspace (.mwc) file,
which corresponds to the contents of the Visual Studio solution (.sln)
file. DataServer.mwc, located in the project root, looks like this:
// DataServer.mwc
workspace {
specific {
cmdline += -language csharp
DataLib
}
}
For this project, DataLib.cs and DataLib.mpc are in a subdirectory named DataLib,
off of the root. The workspace file specifies that the DataLib subdirectory is to be
searched for MPC files, and that any MPC files found there should be treated as
describing C# projects.
Running MPC on the MWC file generates the solution file. The solution file can then be opened in Visual Studio, the DataLib project compiled, and the DataLib assembly built. As this code was developed using Visual Studio 2005 (VC8), we can generate the solution file by executing:
%ACE_ROOT%\bin\mwc.pl -type vc8 DataServer.mwc
from a console prompt set to the project's root directory.
Now that the library for data access has been developed, we can write
a CORBA server which uses that library. We wish to expose the functionality
of the library as a CORBA object, so an interface, described in IDL, must
be created. In a subdirectory named IDL off of the root,
we create a file, Database.idl, which contains that interface.
// Database.idl
interface Database
{
boolean CreateItem(in wstring description, out long long id);
boolean ReadItem(in long long id, out wstring description);
boolean UpdateItem(in long long id, in wstring description);
boolean DeleteItem(in long long id);
};
The operations of the interface correspond to the client-accessible methods
of DataLib::Database, the class defined in DataLib. The IDL types
that are used in the
interface correspond to the types used in C#. In particular, strings in .NET are
in Unicode, so wstring is used to pass them, and as database
IDs are 64-bit, long long is needed.
In the IDL subdirectory, create the file IDL.mpc to allow the
file to be compiled by the TAO IDL compiler.
// IDL.mpc
project : taoidldefaults {
IDL_Files {
Database.idl
}
custom_only = 1
}
Inheriting from the taoidldefaults base project, a base project included
in the TAO distribution, provides the needed infrastructure. We only need
to list the IDL file in the IDL_Files section, and MPC generates
the tao_idl compilation commands. We do need to indicate that the project
has no executable output via the custom_only flag, however.
For this project to be added to the solution file, the workspace file,
DataServer.mwc, must be modified to include the IDL directory. After
the addition, DataServer.mwc looks like this:
// DataServer.mwc
workspace {
specific {
cmdline += -language csharp
DataLib
}
IDL
}
The IDL project is not in C#, so the IDL directory is listed outside of the
specific section. The compilation of this project produces
the client stub and server skeleton files, DatabaseC.[cpp,h,inl]
and DatabaseS.[cpp,h,inl], respectively.
With the database interface defined, we can create a C++ class that which
implements the interface. We create a subdirectory off of the root named
DataServer, and two files in that subdirectory, Database_i.h
and Database_i.cpp. The files as presented here were based off
of generated implementation files via the -GI option to tao_idl
and modified accordingly. Amendments to the generated code are noted here — please
see the
code archive
associated with this article for the full file listings.
These files define class Database_i, an implementation of the
Database CORBA interface. An instance of this implementation is called a servant.
As we would like the instance of the DataLib::Database class
to be maintained by the server itself, we must be able to pass a reference to it to the
servant. As DataLib::Database is a .NET class and Database_i
is not (as it is a standard, unmanaged C++ class), a reference to the DataLib::Database
object must be stored using gcroot<>, a templated helper class
provided by the vcclr.h header file. We must add #include <vcclr.h>
to the top of Database_i.h, and add a class member variable to class
Database_i to store the .NET reference (indicated by the caret) to DataLib::Database.
// Database_i.h
class Database_i
: public virtual POA_Database
{
gcroot<DataLib::Database^> database_;
...
This variable is initialized by the Database_i constructor.
// Database_i.h
Database_i(gcroot<DataLib::Database^> database);
// Database_i.cpp
Database_i::Database_i(gcroot<DataLib::Database^> database) :
database_(database)
{
}
In the methods of Database_i, we use the database_ member
variable to reference the DataLib::Database object, such as in the implementation of
CreateItem().
// Database_i.cpp
::CORBA::Boolean Database_i::CreateItem(
const ::CORBA::WChar * description,
::CORBA::LongLong_out id)
{
System::String^ netDescription = gcnew System::String(description);
::CORBA::Boolean result = database_->CreateItem(netDescription, id);
delete netDescription;
return result;
}
Implementation of the CORBA Database interface is essentially
a translation between CORBA and .NET. In this method, a string provided by
CORBA is converted to a .NET String before being passed to
DataLib::Database::CreateItem(). The code above also illustrates
a benefit of C++/CLI. The variable netDescription is
allocated on the garbage-collected heap via gcnew. It can still be determininstically
freed, however, by a call to delete, as if it was an allocation made
by new on the unmanaged heap. However, if an exception
is thrown by the invocation of CreateItem() and delete is not
called, netDescription will still be freed by the garbage collector
when it executes at some point in the future.
The implementation of ReadItem() also involves string
translation, but this time from .NET to CORBA.
// Database_i.cpp
::CORBA::Boolean Database_i::ReadItem (
::CORBA::LongLong id,
::CORBA::WString_out description)
{
System::String^ netDescription;
::CORBA::Boolean result = database_->ReadItem(id, netDescription);
if (result) {
pin_ptr<const wchar_t> s = PtrToStringChars(netDescription);
description = s;
}
return result;
}
The pin_ptr<> template and PtrToStringChars()
function are two more Visual Studio-provided helpers to assist in dealing with
.NET types in standard C++. PtrToStringChars() provides a means to
directly address .NET String contents, and, as .NET strings are in Unicode, the
contents are represented as an array of wchar_t. As the .NET String
is on the garbage-collected heap, pin_ptr<> is used to keep
the string contents from being relocated until access to it is complete. It must
remain accessible until the skeleton marshals it into the GIOP reply.
The implementation of UpdateItem() and DeleteItem()
are analagous to the above.
With the completion of the servant, we can now implement the DataServer itself.
In the file DataServer.cpp, the main() function of
DataServer begins as most simple CORBA servers do.
// DataServer.cpp
int ACE_TMAIN(int argc, ACE_TCHAR *argv[]) {
try {
// initialize the ORB
CORBA::ORB_var orb = CORBA::ORB_init(argc, argv);
// get a reference to the RootPOA
CORBA::Object_var obj =
orb->resolve_initial_references("RootPOA");
PortableServer::POA_var poa =
PortableServer::POA::_narrow(obj.in());
// activate the POAManager
PortableServer::POAManager_var mgr = poa->the_POAManager();
mgr->activate();
To create the servant, we first create an object of type DataLib::Database,
open the database connection, and pass a reference to the object to
the servant's constructor. In this instance, the percent sign in .NET acts somewhat like an ampersand
does in standard C++ — it provides a reference to an object.
// open the database
DataLib::Database database;
if (!database.Open())
throw std::exception("Cannot open the database");
// create the Database servant
Database_i servant(%database);
PortableServer::ObjectId_var oid =
poa->activate_object(&servant);
CORBA::Object_var database_obj = poa->id_to_reference(oid.in());
There are a number of ways to provide the IOR of an object to callers, such as via a file or via the Naming Service. For this application, we use the IORTable, a TAO-specific feature which allows a client to find a server via a corbaloc URL.
CORBA::String_var ior_str =
orb->object_to_string(database_obj.in());
CORBA::Object_var tobj =
orb->resolve_initial_references("IORTable");
IORTable::Table_var table = IORTable::Table::_narrow(tobj.in());
table->bind("DataServer", ior_str.in());
std::cout << "DataServer bound to IORTable" << std::endl;
main() ends by calling run() on the ORB instance,
and by providing cleanup and error reporting.
// accept requests from clients
orb->run();
orb->destroy();
}
catch (CORBA::Exception& ex) {
std::cerr << "CORBA exception: " << ex << std::endl;
}
catch (std::exception& ex) {
std::cerr << "Exception: " << ex.what() << std::endl;
}
return 0;
}
We next create an MPC file for the DataServer. It is slightly more complicated than previous MPC files.
// DataServer.mpc
project : taoserver, CPPBase, iortable {
after += IDL
after += DataLib
includes += ../IDL
Source_Files {
Database_i.cpp
DataServer.cpp
../IDL/DatabaseC.cpp
../IDL/DatabaseS.cpp
}
managed = 1
}
In the same way that the IDL project inherits from the taoidldefaults
base project to derive behavior,
TAO provides other base projects which allow features of TAO to be easily
referenced by an application — these base projects set include paths, library
linkages, preprocessor symbols and other configuration options so the user
of TAO does not have to. As DataServer is an application that uses server components
of TAO, it inherits from taoserver. It uses the IORTable,
so it inherits from iortable as well — if it had used Naming
Service functionality, it would have inherited from naming. We
desire it to have the same attributes as other C++ applications in the solution,
so it also inherits from CPPBase.
The after statements ensure that the IDL and DataLib projects
are built prior to this one. As source code files are not all located in
the same directory as the MPC file, we must specify them explicitly via the
includes statement and the Source_Files section.
Finally, the /clr compiler
option must be set to allow .NET functionality to be directly used in C++ code, so
managed = 1 is specified.
This project must also be added to the workspace, leading to a DataServer.mwc
that looks like this:
// DataServer.mwc
workspace {
specific {
cmdline += -language csharp
DataLib
}
IDL
DataServer
}
We then regenerate the solution file using MPC, rebuild, and now have a working server.
SIDEBAR
As discussed in [5] and [6],
incomplete types will generate linker warning LNK4248 when compiled with /clr, and is seen
with many types in TAO. For example:
warning LNK4248: unresolved typeref token (01000016) for
'TAO_ORB_Core'; image may not run
The actual type that will be used is defined in
TAO itself, which is not compiled with /clr. In practice, this warning is harmless, though defining the symbol with an empty body in the module
that generates the warning will suppress the message. Please see the file LNK4248.h in the
code archive
for an approach to this issue.
With the server side complete, we can begin development of the client.
The DataServerConnectorLib library acts as a client of the DataServer.
More specifically, the class
DataServerConnector in this library makes the client-side CORBA
calls to invoke methods on the server. Although this class is implemented in C++
to make use of TAO, this class is a fully-fledged .NET type, so it can be used
by the Client application which is written in C#. For future convenience, the method
Run() of this class is executed in a .NET thread to allow an ORB
to continue execution independent of the code that uses DataServerConnector,
and the Start() and Shutdown() methods manage this thread.
The other public methods of this class mirror the CORBA Database interface,
in appropriate .NET syntax.
We start by creating the file DataServerConnectorLib.h in the
DataServerConnectorLib subdirectory off of the root, and add the
following class definitions:
using namespace System;
using namespace System::Threading;
class DataServerConnectorState {
CORBA::ORB_var orb_;
Database_var database_;
public:
DataServerConnectorState(CORBA::ORB_ptr orb, Database_ptr database);
Database_ptr DatabasePtr() { return database_; }
CORBA::ORB_ptr OrbPtr() { return orb_; }
};
public ref class DataServerConnector {
DataServerConnectorState *state_;
Thread^ thread_;
AutoResetEvent startupEvent_;
void Run();
static void ThreadStart(Object^ param);
public:
DataServerConnector();
~DataServerConnector();
void Start();
void Shutdown();
bool CreateItem(String ^description, Int64 %id);
bool ReadItem(Int64 id, String^% description);
bool UpdateItem(Int64 id, String^ description);
bool DeleteItem(Int64 id);
};
The using statements allow us to use types from various .NET assemblies
without needing to specify the fully qualified names, such as Thread
instead of System::Threading::Thread. The DataServerConnector class is declared as
a public ref class. The public keyword indicates that the class is
visible outside of the assembly; __declspec(dllexport) is not used with .NET types,
as it would be with standard Windows dynamic link libraries to export symbols. The ref keyword
indicates that the class is a garbage-collected .NET type, and not an unmanaged,
standard C++ class.
DataServerConnectorState is, however, an unmanaged, standard
C++ class. Unmanaged types (such as CORBA::ORB_var) cannot be
member variables of a .NET class, but pointers to unmanaged types can be.
DataServerConnectorState acts as a container for the unmanaged
state of DataServerConnector.
The implementation of DataServerConnectorState is straightforward — it
stores ORB and servant pointers for later use. It resides, with the implementation
of DataServerConnector, in the file DataServerConnectorLib.cpp.
// DataServerConnectorLib.cpp
DataServerConnectorState::DataServerConnectorState(CORBA::ORB_ptr orb,
Database_ptr database) {
orb_ = CORBA::ORB::_duplicate(orb);
database_ = Database::_duplicate(database);
}
The Run() method contains the CORBA client implementation. Because
ORB_init() requires C-style argc and argv,
they must be constructed from the .NET command-line argument array, so we begin
by performing that conversion, and then initialize the ORB.
// DataServerConnectorLib.cpp
void DataServerConnector::Run() {
int argc = 0;
wchar_t **argv = NULL;
try {
// convert .NET arguments to standard argc/argv
array<String^>^ arguments = Environment::GetCommandLineArgs();
argc = arguments->Length;
argv = new wchar_t *[argc];
for (int i=0; i<argc; i++) {
pin_ptr<const wchar_t> arg = PtrToStringChars(arguments[i]);
argv[i] = _wcsdup(arg);
}
CORBA::ORB_var orb = CORBA::ORB_init(argc, argv);
We now obtain an object reference to the Database object. Because the server registered the object in the IORTable, the client can locate it by passing
-ORBInitRef DataServer=
corbaloc:iiop:server_hostname:server_port/DataServer
on its command line, and calling resolve_initial_references().
// obtain the reference
CORBA::Object_var database_obj =
orb->resolve_initial_references("DataServer");
if (CORBA::is_nil(database_obj.in()))
throw std::exception("Could not get the Database IOR");
// narrow the IOR to a Database object reference.
Database_var database = Database::_narrow(database_obj.in());
if (CORBA::is_nil(database.in()))
throw
std::exception("IOR was not a Database object reference");
We now store references to the ORB and Database object for later use,
run the ORB to process any requests, and perform cleanup on error.
DataConnectorException, a subclass of the .NET Exception
class, is defined to wrap and re-throw any exceptions that are generated. This
allows native exceptions, such as CORBA::Exception to be propagated
to the .NET world.
// save the references via a pointer to an unmanaged class
state_ = new DataServerConnectorState(orb, database);
// good to go - tell the outside world
startupEvent_.Set();
// run the ORB
orb->run();
orb->destroy();
}
catch (CORBA::Exception& ex) {
std::stringstream ss;
ss << "Exception: " << ex;
throw
gcnew DataConnectorException(gcnew String(ss.str().c_str()));
}
catch (std::exception& ex) {
std::stringstream ss;
ss << "Exception: " << ex.what();
throw
gcnew DataConnectorException(gcnew String(ss.str().c_str()));
}
}
We must also implement methods that wrap the CORBA method invocations.
As translation was performed in the DataServer, we do the same, but in
reverse — from .NET to CORBA. For example, CreateItem() is
defined below. The .NET string is converted to a CORBA::WString
via the PtrToStringChars()/pin_ptr<> combination we have
used before. The CORBA::LongLong used to store the out
parameter from the CreateItem() CORBA interface method is converted
to a .NET Int64 to be returned to the caller. Note that the percent
sign in the argument list, in this usage, acts as an out parameter.
As with Run(), exceptions are propagated as the DataConnectorException
type. The other wrapper methods are analagous.
// DataServerConnectorLib.cpp
bool DataServerConnector::CreateItem(String ^description, Int64 %id) {
try {
pin_ptr<const wchar_t> cppDescription =
PtrToStringChars(description);
CORBA::WString_var desc = CORBA::wstring_dup(cppDescription);
CORBA::LongLong cid;
CORBA::Boolean result =
state_->DatabasePtr()->CreateItem(desc, cid);
id = cid;
return result;
} catch (CORBA::Exception& ex) {
std::stringstream ss;
ss << "Exception: " << ex;
throw
gcnew DataConnectorException(gcnew String(ss.str().c_str()));
}
}
After the .NET thread management methods are added, development of the
DataServerConnectorLib is complete. We now create an MPC file for it.
The options specified are similar to those used in DataServer.mpc.
// DataServerConnectorLib.mpc
project : taoexe, CPPBase {
after += IDL
includes += ../IDL
Source_Files {
DataServerConnectorLib.cpp
../IDL/DatabaseC.cpp
}
managed = 1
}
We also add it to DataServer.mwc.
// DataServer.mwc
workspace {
specific {
cmdline += -language csharp
DataLib
}
IDL
DataServer
DataServerConnectorLib
}
Regenerating the solution with MPC and recompiling yields a working
DataServerConnectorLib.
The last module we will create is a GUI in C# to demonstrate the system. The GUI consists of a ListView to display messages, and a series of Buttons and TextBoxes to exercise the database methods. Please refer to the code archive for details of the GUI itself.
The implementation of the button click methods invoke the corresponding
database functions — the methods exposed by the DataServerConnector
class. User input into the TextBoxes associated with each button is used,
as appropriate. For instance, the click handler for the Create button is
as follows:
private void bCreate_Click(object sender, EventArgs e)
{
try
{
// if input is blank, do nothing, else create the item
if (String.IsNullOrEmpty(tCreateDesc.Text))
return;
// invoke the method
long id = 0;
if (dataConnector_.CreateItem(tCreateDesc.Text, ref id))
Log("Item '" + tCreateDesc.Text + "' created with id " + id);
else
Log("Item '" + tCreateDesc.Text + "' could not be created");
}
catch (DataConnectorException ex)
{
Log(ex.Message);
}
// after completion (or failure) clear the input
tCreateDesc.Text = "";
}
In this method, tCreateDesc is the TextBox associated
with the Create button. If the user has entered text, it will be used
as the item description of the item to be created. The call to DataServerConnector::CreateItem()
invokes the CORBA method, the ID of the created item is returned (the ref
in C# corresponds to the % in C++ in the argument list of DataServerConnector::CreateItem()), and displayed to
the user in the ListView via the call to Log(). The other methods are
implemented similarly.
With the code complete, we create an MPC file for the Client project, as follows:
// Client.mpc
project : CSBase {
exename = Client
after += DataServerConnectorLib
specific {
winapp = true
}
Source_Files {
*.cs
*.Designer.cs
}
Source_Files {
subtype = Form
Client.cs
}
Resx_Files {
generates_source = 1
subtype = Designer
Properties/Resources.resx
}
lit_libs += System System.Data System.Xml
lit_libs += System.Drawing System.Windows.Forms
}
This MPC file is more complex than we have seen so far, due to the nature
of a graphical .NET application. In this project, the file Program.cs
contains the C# Main() function, so unless otherwise specified, the
output will be named Program.exe. We use the
exename keyword to change the name of the output to Client.exe.
We must specify winapp = true as, by default, MPC will create
a console-based C# application, and Client is a GUI-based one. Two Source_Files
sections are necessary, as the file Client.cs contains a subclass
of System.Windows.Forms.Form to act as the main window of the application.
Form code requires additional infrastructure (e.g., support for one or
more associated resource files) that normal code files do not. The
resource file Resources.resx is similar in that it has an associated
autogenerated C# file that provides access to the resources it contains.
With the MPC file complete, we now add it to the workspace, yielding:
// DataServer.mwc
workspace {
specific {
cmdline += -language csharp
DataLib
Client
}
IDL
DataServer
DataServerConnectorLib
}
The following screen shots demonstrate the system. We start two Clients,
as well as the DataServer (not shown). For this run, the server was run on
the machine oci1373 and started with the following command (on a
single line):
DataServer -ORBDottedDecimalAddresses 0
-ORBListenEndpoints iiop://:12346
Each of the Client instances were started with this command (on a single line):
Client -ORBDottedDecimalAddresses 0
-ORBInitRef DataServer=corbaloc:iiop:oci1373:12346/DataServer
We enter "My First Item" into the TextBox associated with the Create button on the first Client.
Pressing the Create button creates the database item, and the generated ID of 1 is reflected in the ListView.
On the second Client, we enter the ID of 1 into the TextBox associated with the Read button.
Pressing the Read button displays "My First Item" as the item description, demonstrating that the second Client has referenced the same database as the first Client.
This article has described how to use TAO in a .NET application to implement both a CORBA client and server. The next article in this series will show how to incorporate OpenDDS to provide database notifications.
[1] Top Operating System Share Trend
http://marketshare.hitslink.com/os-market-share.aspx?qprid=9
[2] .NET Framework Overview
http://www.microsoft.com/net/overview.aspx
[3] Standard ECMA-335 Common Language Infrastructure (CLI)
http://www.ecma-international.org/publications/standards/Ecma-335.htm
[4] ADO.NET
http://msdn.microsoft.com/en-us/library/aa286484.aspx
[5] warning LNK4248: unresolved typeref token (01000017) for '_TREEITEM'; image may not run
http://social.msdn.microsoft.com/Forums/en-US/vclanguage/thread/0730e965-7299-44ca-8a95-59e2eb23d153
[6] Linker Tools Warning LNK4248
http://msdn.microsoft.com/en-us/library/h8027ys9%28VS.80%29.aspx
Object Computing, Inc. (OCI) is the leading provider of object-oriented technology training in the Midwest. Thousands of students participate in our training program every year. Targeted toward software engineers and the development community, our extensive program of over 50 hands-on workshops is delivered to corporations and individuals throughout the U.S. and internationally. OCI's Education Services include Private Training, Public Training, and Lab Rentals. Visit www.ociweb.com/training or contact us at training@ociweb.com.
OCI offers downloads and commercial support for a variety of middleware technologies.
Copyright
©2009
Object Computing, Inc. All rights reserved.
OMG, CORBA, IIOP, and all OMG marks and logs are trademarks or registered
trademarks of Object Management Group, Inc. in the United States and/or
other countries.
Java and all
Java-based marks are trademarks or registered trademarks of Sun
Microsystems, Inc. in the United States and/or other countries.
.NET, C#, and .NET-based marks are trademarks or registered trademarks of Microsoft
Corporation in the United States and/or other countries.