MicroStation Development: User Interface Options

When we design an application for MicroStation CONNECT we have a choice of programming languages …

Often, an interactive application needs a user interface (UI). The UI lets a human examine and change application variables through a dialog box or other mechanism. VBA provides User Forms, .NET has a rich variety of Windows Forms and Windows Presentation Foundation (WPF) widgets.

This article is for C++ developers. We'll ignore VBA User Forms; there's plenty of information about those elsewhere. The Microsoft UI technologies for C++ are useful. They can be very productive, in terms of developer design time. However, none of the Microsoft technologies integrates perfectly with MicroStation.

User Interface Options and the MicroStationAPI

MicroStation's own UI originated with MDL (see the History of MicroStation for more). It is designed to provide a programmer's route into a UI that caters for interactive use and provides a simple way to handle program variables.

MicroStation UI: Global Variables and Access Strings

MicroStation dialogs contain dialog items. Each dialog item fulfills a clear purpose for data entry or data modification. For example, a text dialog item lets you, the programmer, show the value of a variable to the user. The user can edit the text item and the change is reflected automatically in the state of your variable.

Dialogs and dialog items are under the supervision of the MicroStation Dialog Manager. A dialog item may have an access string. An access string is typically one of your program variables.

C++ Objects and Global Variables
DItem_TextRsc TEXTID_ExampleText =
{
  NOCMD, LCMD, NOSYNONYM, NOHELP, MHELP, NOHOOK,
  NOARG, 10, "%d", "%d", "", "", NOMASK,
  TEXT_NOCONCAT, "", "g_example.intVar_"
};

In the above example of a text item resource specification, "g_example.intVar_" is the item's access string. Data exchange between global variable g_example.intVar_ and that access string is handled automatically by MicroStation's Dialog Manager.

Dialog Manager

The MicroStation Dialog Manager is a UI engine. As far as the user is concerned, it might not exist. As far as the programmer is concerned, it provides an elegant mechanism to handle the interaction between program variables and dialog items. The Dialog Manager silently transfers data from your code to the dialog item by matching your program variable to the item's access string.

When you change your variable, you can tell the dialog to synchronise its items. The UI changes to show the current state of your application. When a user edits a dialog item, the item's new state is reflected automatically in your application variable.

Published Global Variables

For the above to work, you must inform the Dialog Manager about your program variables. The process is termed publishing: you publish your variables to inform Dialog Manager that it should manage them. There is an API specially for that purpose: the mdlDialog_publishApi.

Variable Type Compiler

The mdlDialog_publishApi must understand the structure of your variables. For primitive types that is not an issue, but if you use a struct to define a set of variables then you must invoke the resource type compiler.

C++ and Global Variables

Global variables, which are required by MicroStation's Dialog Manager, don't sit well with the idioms of object-oriented programming. Advocates of encapsulation, for example, are likely to react strongly against a proposal to introduce global variables into their code. Unfortunately, if we want to benefit from the Dialog Manager, we must encourage them to do exactly that.

C++ Objects and Global Variables

Combining Encapsulation with Global Variables

Now you have reached the kernel of this article. We wanted a mechanism that would enable the use of C++ objects with MicroStation's global variables, required for interaction with the Dialog Manager. The characteristics of that class would be …

Boost::Variant

Boost provide some useful libraries, including boost::variant. boost::variant is the enabling technology that underpins this article. See the Boost::variant documentation for details.

You'll notice a couple of other Boost components used here. Here's a summary of the protagonist and accompanying players in this tale …

boost::variant
A C++ template class that forms the core of boost::variant
boost::static_visitor
Part of the infrastructure of boost::variant
boost::lexical_cast
An invaluable tool for converting between different data types

std::variant

Note that recent versions of C++ include std::variant. However, until C++ 17 arrives, it lacks features that boost::variant provides. Here, we use boost::variant in our own class, starting with this type alias …

using	PropertyValue	= boost::variant<Int32, double, Bentley::WString>;

An instance of PropertyValue stores a data type compatible with those commonly used with the MicroStation Dialog Manager, which are int, double and wide-character strings.

Next, we use another boost::variant to store a type-safe pointer to the external global variable …

using	BoundVariable	= boost::variant<Int32*, double*, WCharP>;

Both are encapsulated in our ItemData class. We have a PropertyValue that holds data, and a BoundVariable that points to external data …

struct ItemData
{
    PropertyValue               propValue_;
    BoundVariable               boundVariable_;
};

The pointer in the BoundVariable is the key to exchanging data between the external variable and our data member variable. We initialise the stored data type in the constructor. The ItemData class has three constructors — one for each of the data types we want to be able to store …

struct ItemData
{
    PropertyValue               propValue_;
    BoundVariable               boundVariable_;
    //	Construct a String variant
    explicit ItemData (WCharCP value, WCharP  boundVariable)
        :  propValue_ (value), boundVariable_ (boundVariable) {};
    //	Construct an Integer variant
    explicit ItemData (Int32   value, Int32*  boundVariable)
        :  propValue_ (value), boundVariable_ (boundVariable) {}
    //	Construct a Double variant
    explicit ItemData (double  value, double* boundVariable)
        :  propValue_ (value), boundVariable_ (boundVariable) {};
};

Getting the stored value is simple enough, if you're willing to accept a variant …

struct ItemData
{
    PropertyValue               propValue_;
    BoundVariable               boundVariable_;
    PropertyValueCR             Value () const { return propValue_; }
    //	Construction
    explicit ItemData (WCharCP value, WCharP  boundVariable) : propValue_ (value), boundVariable_ (boundVariable) {};
    explicit ItemData (Int32   value, Int32*  boundVariable) : propValue_ (value), boundVariable_ (boundVariable) {}
    explicit ItemData (double  value, double* boundVariable) : propValue_ (value), boundVariable_ (boundVariable) {};
};

You can alternatively get the stored value as a string …

struct ItemData
{
    PropertyValue               propValue_;
    BoundVariable               boundVariable_;
    PropertyValueCR             Value () const { return propValue_; }
    Bentley::WString            ToString () const;
    //	Construction
    explicit ItemData (WCharCP value, WCharP  boundVariable) : propValue_ (value), boundVariable_ (boundVariable) {};
    explicit ItemData (Int32   value, Int32*  boundVariable) : propValue_ (value), boundVariable_ (boundVariable) {}
    explicit ItemData (double  value, double* boundVariable) : propValue_ (value), boundVariable_ (boundVariable) {};
};

We have to do some work to convert the stored value (int, double or std::wstring) to a string. The following implementations are interesting for their use of boost::static_visitor. That is a base class design that eases access to a boost::variant. We use a boost::static_visitor, as necessary, to convert the internal representation to its external type …

struct CastToStringVisitor :  boost::static_visitor
{
    std::wstring operator() (Int32   val)   const { return boost::lexical_cast<std::wstring>(val);  }
    std::wstring operator() (double  val)   const { return boost::lexical_cast<std::wstring>(val);  }
    std::wstring operator() (WString val)   const { return std::wstring (val.c_str ()); }
};

WString    ItemData::ToString    () const
{
    return WString (boost::apply_visitor (CastToStringVisitor (), propValue_).c_str ());
}


When a user has edited a UI dialog item, she may have changed the value stored in one of our global variables. We set this ItemData's property value by copying from its bound external variable using the ItemData::SetValue method …

struct SetValueVisitor :  boost::static_visitor void
{
	PropertyValueP					propValue_;
	SetValueVisitor (PropertyValueP	propValue) : propValue_ (propValue) {}
	void operator() (Int32*  val) {  *propValue_ = *val; }
	void operator() (double* val) {  *propValue_ = *val; }
	void operator() (WCharP  val) {  *propValue_ = val;  }
};
bool		ItemData::SetValue		()
{
	SetValueVisitor	visitor (&propValue_);
	boost::apply_visitor (visitor, boundVariable_);
	return true;
}

Copy this ItemData property value to its bound external variable …

struct PropagateVisitor :  boost::static_visitorvoid
{
    PropertyValueCP   propValue_;
    PropagateVisitor  (PropertyValueCP propValue) : propValue_ (propValue) {}
    void operator()   (Int32*  val) const { *val =  boost::get <Int32> (*propValue_);}
    void operator()   (double* val) const { *val =  boost::get <double> (*propValue_); }
    void operator()   (WCharP  val) const { wcscpy (val,  boost::get <WString> (*propValue_).c_str ()); }
};
void     ItemData::Propagate  ()  const
{
    PropagateVisitor  visitor (&propValue_);
    boost::apply_visitor (visitor, boundVariable_);
}

Usage

Here's an example of how we use this ItemData class. First, suppose our MicroStation application has a global struct defined using a C-style declaration to make it compatible with the Dialog Manager …

typedef struct example_c_struct_
{
    int      intVar_;
    WChar    unicodeVar_;

} EXAMPLE_STRUCT;

We declare a variable of that type at global scope …

EXAMPLE_STRUCT  g_example;

We publish that variable, probably in our MdlMain(). When a variable is published, the Dialog Manager knows about that variable and monitors interaction between the variable and dialog items that use it …

void MdlMain (int argc, WCharCP argv[])
[
    ...
    SymbolSet* symbolSet = mdlCExpression_initializeSet(VISIBILITY_DIALOG_BOX, 0, 0);
    mdlDialog_publishComplexVariable (symbolSet, "example_c_struct_", "g_example", &g_example);
    ...
}

You will see the global variable appear in dialog item definition. The dialog item's access string is your variable. Here's an example text item that appears in a dialog resource definition (.r) file …

DItem_TextRsc TEXTID_ExampleText =
{
  NOCMD, LCMD, NOSYNONYM, NOHELP, MHELP, NOHOOK,
  NOARG, 10, "%d", "%d", "", "", NOMASK,
  TEXT_NOCONCAT, "", "g_example.intVar_"
};

Instantiate an ItemData class with the address of your variable …

//  Instantiation
ItemData data (42, &g_example.intVar_);
//  Get value as string
std::wstring stringVal = data.ToString ();
//  Assign a new value
data.SetValue (99);
//  Propagate the value to the global variable
data.Propagate ();
//  Inform the Dialog Manager that something changed …
mdlDialog_itemsSynch (mdlDialog_find (DIALOGID_Example, 0));

Standard Containers

As a first-class C++ object, ItemData is compatible with the standard containers. That is, you can add an ItemData to a std::vector, then sort that vector, copy it and delete it with no concerns about memory allocation or destruction. Look, here's a vector of ItemData objects …

using ItemDataCollection = std::vector<ItemData>;
//  Add ItemData to a collection
ItemDataCollection items;
items.push_back (ItemData (42, &g_example.intVar_));
items.push_back (ItemData (L"example", g_example.unicodeVar_));
//	Get ItemData from a collection
WStringCR  data1 = items[0].ToString ();
WStringCR  data2 = items[1].ToString ();