Chris is the cofounder of Wave Software (http://www.wavesoftware.com/), which specializes in software development for Windows and the Internet. He can be reached at [email protected].
Visio is generally considered a generic illustration tool for creating flowcharts, organizational charts, timelines, and the like. While Visio (from Visio Corp., http://www.visio.com/), although recently acquired by Microsoft, is an excellent tool for applications such as these, it is nonetheless built around a powerful visualization engine, making it ideal for visualizing and diagramming networks, software, databases, and other such systems.
With Visio, you can use drag-and-drop to assemble diagrams from a multitude of prebuilt shapes. Each shape consists of a number of properties that control a shape's appearance and response to stimuli. Visio presents the editable properties in a ShapeSheet editor that looks somewhat like a normal spreadsheet. In addition to simple numeric values, a property (or cell) can contain a formula that derives the value through some computation.
These shapes can be transformed, connected to other shapes, and grouped. Furthermore, you can construct custom shapes from scratch or by modifying/extending those in the box. You can then use stencils to collect shapes for a particular purpose. In this article, for example, I'll use a network stencil that contains many shapes to represent the objects typically found on computer networks. In addition to stencils, Visio supports the notion of templates that are empty (or partially built) diagrams to which one or more stencils can be attached. In short, a Visio template can be used in much the same way as a Microsoft Word or Excel template.
To illustrate the use of Visio in a software development environment, I'll build the presentation layer for network resource discovery and visualization. While this article is based on Visio Professional 5.0, there are several different versions of the package, ranging from Visio Standard Edition to Visio Enterprise 5.0. In addition, there are add-ons, such as the Visio Solutions Library and the Visio Network Equipment toolkit (with various network device shapes).
There are three different ways to build a Visio-based solution:
- Write a special kind of DLL called a "Visio Solutions Library" (VSL).
- Utilize the embedded Visual Basic for Applications (VBA) development environment.
- Create a separate executable that drives Visio through its automation API.
I'll focus VSLs using C++. That doesn't mean I don't use VBA. In fact, I always start a Visio-based project by prototyping the automation usage in VBA because it is quick and easy. The embedded nature of VBA means that solutions developed require a template (or diagram) in which to store the code. Likewise, implementing a separate executable that drives Visio is similar to writing a VSL, except that Visio runs in a different process. Thus the communication between the two applications is slightly slower. I've written solutions of this form using both C++ (using the wrapper classes) and Visual Basic.
Except for VBA, your choice of development tool is restricted to those that are capable of calling through OLE automation interfaces. However, Visio makes it easier to create VSLs if you are using Microsoft Visual C++ by including a custom App Wizard. All of the aforementioned alternatives utilize the Visio Automation API to handle the interactions between your code and the Visio environment.
Visio Solutions Library
A VSL is a standard Windows DLL that contains one or more add-on objects. It is identified by the .VSL suffix and the single export:
VAORC VisioLibMain(VAOMSG wMsg, WORD wParam, LPVOID lpParam)
I've removed some of the additional preprocessor elements that control its export and calling convention. In form, it looks like a Windows message handler; indeed, this is how it operates in practice. Once the VSL has been loaded, Visio sends it messages through this function to inform it of actions and events that have occurred. As a writer of a VSL, not all the messages will be of interest, so Visio includes a default message handler (sounds a lot like DefWindowProc) called VAOUtil _DefVisMainProc.
It is possible for one VSL to contain multiple add-on objects. When Visio calls through VisioLibMain, it sets the wParam parameter to be the identifier of the add-on that it is communicating with. When the add-on first registers with Visio, it is assigned a session-wide unique identifier. If you write add-ons and VSLs from scratch, it is your responsibility to ensure you record this information and process a standard set of messages in your VisioLibMain. Alternatively, you can use C++ and leverage the wrappers and Wizard that Visio provides.
Wizards are included for Visual C++ 4 and 5 (Version 5 Wizards also work with Version 6.0). When run, Visio creates a project that resembles the standard Win32 DLL project. However, the Visio-created project contains a prebuilt VAddon derivative you use as the basis for your application. The project will compile and link successfully and, if installed, the add-on will popup a message box when invoked.
The most important VAddon override is the Run method, which is called when users invoke your add-on from the Visio UI (or programmatically). Example 1 shows the Run method implementation in my network resource discovery VSL. The VAddon source file includes a stock implementation of the VisioLibMain that routes messages to virtual methods defined on the VAddon class. By overriding these methods in your derivatives you control how your add-on reacts to messages sent from Visio.
The Wizard is only capable of generating a project with one add-on. If you want the VSL to contain multiple add-ons then they must be cranked out by hand. Study the VAddon class and, in particular, how it is registered before starting down this path.
In addition to providing a C++ wrapper for writing add-ons, Visio includes an entire library of classes that encapsulate its automation API. This library saves you from having to spend too much time worrying about the details of the interfaces (in particular AddRef and Release) and lets you concentrate on the important job of writing your solution.
All the files I've mentioned -- Wizard, C++ wrappers, and so on -- are not installed by default. You must select the custom install and make sure that the "Developing Visio Solutions" option is checked. You can tell if your installation includes these files by looking for a DVS subdirectory in the Visio directory.
You configure the Visual C++ project to debug a VSL in exactly the same way you would for any normal Win32 DLL. Under the "Debug" option for the project set the "Executable for debug session" to <VISIO>\VISIO32.EXE where <VISIO> has been substituted for the Visio installation path on your system.
To install a VSL, Visio maintains a list of directories that it searches on startup for templates, stencils, and VSLs. By default, every installation includes a Solutions subdirectory. (In the past, I've created a subdirectory under Solutions called "IntelliCorp," the company I work for, for storing our VSLs.)
If you are running Visio in Developer Mode, then each VSL that has been marked visible can be found under Tools>>Addons. If the Addons menu is not present, look under Macros or enable Developer Mode from the Advanced Options dialog. If you're building solutions, you really should be running in Developer Mode, as this enables a number of useful short-cut menus -- most notably the Show ShapeSheet item on the Shape Context menu.
You are not constrained to install your solution in the Solutions directory structure. However, if an alternative directory is used, you must ensure that the search path is updated. You can either do this through the Visio UI or programmatically by editing the Visio.ini file. Changing the path through the UI causes Visio to automatically update its cache of VLSs, stencils, and templates. Programmatically, for example, perhaps you have written an installation script for your solution, then remember to change the BuildDirectoryCache entry in the INI file to 0 as this will instruct Visio to update its cache when next started.
Pulling the Resources Together
To illustrate the ideas presented here, I've written a C++ VSL that generates diagrams to represent all the available resources on a Windows network. The VSL utilizes the Visual C++ 5.0 Wizard, C++ wrappers, a standard Visio template, and some Win32 API calls.
Figure 1 diagrams my home network. The real meat of the diagram starts with the cloud (ICBRISTOL); this is my Workgroup. The two machines it contains (DEEPTHOUGHT and DIABLO) are shown with their shares. Directory and printer shares each get their own shape.
In the interest of clarity, the code contains only a few debug ASSERTs and almost nothing in terms of error handling. If you start copying/pasting code into your solution, your first exercise would be to introduce some error handling. Also, I divided the VSL engine into a CNetworkResourceCollector and CNetworkResource class.
CNetworkResourceCollector
Listings One and Two present the CNetworkResourceCollector class. The point of entry to this object is its Run method. The actual processing is then split into the collection of resource data and its presentation. The Collect method is surprisingly short and uses recursion to walk the hierarchy of resources found in a network. In general, Windows networks are formed as follows:
Windows Network
Domain
Computer
Share (represents both directories and printers)
The core Win32 API functions used to traverse this structure are WNetOpenEnum, WNetCloseEnum, and WNetEnumResource. Before the advent of the COM task allocator, it was typical for functions that returned variable length data to require two calls. The first call determined how much memory would be required to hold the returned data. The second call actually obtained the data. The WNet functions work in this way. WNetEnumResource returns ERROR_MORE_DATA when it needs more memory to complete successfully. A common bug (at least it used to be) is illustrated in realloc call in this method; see the code comments for more details.
As each resource is found, an instance of CNetworkResource is created to encapsulate the important details of the NETRESOURCE structure. I maintain the hierarchy by passing a parent resource through each successive call to Collect. Actually, this explains the The Network Universe shape at the top of the diagram. It makes the code in Collect much simpler during its first invocation if a real CNetworkResource instance is supplied. If I didn't include this I'd have to program for a special case and this makes the code larger, more cumbersome, and can point to design deficiencies.
Once all the resources have been collected, the Display method is called, which is responsible for loading the Visio network template, calling on each of the resources to draw themselves, and finally to organize the shapes using Visio's built-in auto layout routine.
CNetworkResource
In addition to wrapping the significant elements of the NETRESOURCE structure, the CNetworkResource (available electronically; see "Resource Center," page 7) provides methods to create the Visio representation of the resource and to add new resources to it (this is how the hierarchy is maintained in the data structure). Each type of resource gets its own shape.
Generating the Diagram
I used the Basic Network template that ships with Visio 5.0 Professional as the basis for the network resource diagram. The stencils that it uses provide a good range of shapes that can be used to represent the resources of the network. In particular, I used the Desktop PC (computer), Straight Bus (network), Cloud (domain), City (root -- network universe), Printer (shared printer), and Tape Drive (shared directory). The Tape Drive shape is actually used for directory shares because there is no hard-disk shape in the stencil; odd, given that there is one for floppy drives.
To make it easier to change the shapes used to represent each resource, I created a resource to shape map (see ShapesInc.cpp; available electronically). The g_Shapes global vector holds the shape index used for each type of resource. For the most part, determining the type of resource is a simple matter of looking at the dwDisplayType attribute of the NETRESOURCE structure (or calling GetDisplayType on a CNetworkResource instance). Unfortunately, in the case of shares this always returns RESOURCEDISPLAYTYPE_SHARE. For this reason, I store the dwType element (GetType) and use it to distinguish directory and printer shares.
In my tests, only the following resource types were encountered:
RESOURCEDISPLAYTYPE_SHARE
RESOURCEDISPLAYTYPE_DOMAIN
RESOURCEDISPLAYTYPE_NETWORK
RESOURCEDISPLAYTYPE_SERVER
The production of the diagram falls neatly into these steps:
1. Create an instance of a shape for each resource.
2. Set the text of the shape.
3. Assemble the hierarchy of links between the resources.
4. Layout the diagram.
Fortunately, Visio provides a number of autolayout algorithms, so making the diagram look pretty is simply a matter of calling the right method. As I've said, Visio handles all the dirty work of laying out the shapes and links. After trying the different options, I settled on those in Table 1.
Interestingly, the layout properties are stored in the User Defined properties section on the Page object. You can see this by opening the ShapeSheet editor for the page. You configure the settings by updating the contents of each User Defined property cell.
Figure 1 took about 20 seconds to produce. Running it on the INTELLICORP domain remotely via a dial-up ISDN connection took around 15 minutes. A fair percentage of the time is spent executing the autolayout function -- reasonable considering the number of machines and shares present in the domain.
Conclusion
The underlying principle of this article is to reinforce the idea of component-based development -- the utilization of prebuilt, quality modules that can be rapidly assembled to create complete software solutions.
DDJ
Listing One
// NetworkResourceCollector.h: interface for CNetworkResourceCollector class ////////////////////////////////////////////////////////////////////// #if !defined(AFX_NETWORKRESOURCECOLLECTOR_H__C855318A_17FC_11D3_B3C5_ 00105A98B108__INCLUDED_) #define AFX_NETWORKRESOURCECOLLECTOR_H__C855318A_17FC_11D3_B3C5_ 00105A98B108__INCLUDED_ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 #include <list> class VNetInfo; class CNetworkResource; ////////////////////////////////////////////////////////////////////// // CNetworkResourceCollector class CNetworkResourceCollector { public: // C'tor/d'tor. CNetworkResourceCollector(VNetInfo* addon); ~CNetworkResourceCollector(); public: // Operations. BOOL Run(); private: // Implementation. BOOL Layout(CVisioPage& page); void DeleteNetworkResources(); CNetworkResource* CreateNetworkResource(NETRESOURCE* buffer); BOOL Display(); BOOL Collect(CNetworkResource* parent, NETRESOURCE* nr = NULL); VNetInfo* m_Addon; // Having a "special" root object makes the Collect code simpler. CNetworkResource* m_RootResource; public: #ifdef _DEBUG void Dump(); #else #define Dump() #endif // _DEBUG }; #endif // !defined(AFX_NETWORKRESOURCECOLLECTOR_H__C855318A_17FC_11D3_B3C5_ 00105A98B108__INCLUDED_)
Listing Two
// NetworkResourceCollector.cpp ////////////////////////////////////////////////////////////////////// #include "stdafx.h" #include "VNetInfo.h" #include "visiwrap.h" #include "NetInfo.h" #include "NetworkResourceCollector.h" #include "NetworkResource.h" #include "ShapesInc.h" #ifdef _DEBUG #undef THIS_FILE static char THIS_FILE[]=__FILE__; #define new DEBUG_NEW #endif CNetworkResourceCollector::CNetworkResourceCollector(VNetInfo* addon) { m_Addon = addon; m_RootResource = new CNetworkResource(); } CNetworkResourceCollector::~CNetworkResourceCollector() { DeleteNetworkResources(); } BOOL CNetworkResourceCollector::Run() { Collect(m_RootResource); Display(); return(TRUE); } BOOL CNetworkResourceCollector::Collect(CNetworkResource* parent, NETRESOURCE* nr /* NULL */) { HANDLE enumHandle = 0; DWORD rc = 0; rc = ::WNetOpenEnum( RESOURCE_GLOBALNET, RESOURCETYPE_ANY, 0, nr, &enumHandle); if(NO_ERROR == rc) { NETRESOURCE* buffer = NULL; DWORD bufferSize = 0; bufferSize = sizeof(NETRESOURCE); buffer = (NETRESOURCE*) malloc(bufferSize); ASSERT(NULL != buffer); ::ZeroMemory(buffer, bufferSize); do { DWORD count = 1; rc = ::WNetEnumResource( enumHandle, &count, buffer, &bufferSize); switch(rc) { case NO_ERROR: { // buffer now describes a single network resource. CNetworkResource* res = CreateNetworkResource(buffer); ASSERT(NULL != res); parent->AddResource(res); Collect(res, buffer); break; } case ERROR_MORE_DATA: // For clarity I am not protecting against realloc returning // NULL - should it do so then we'll lose the memory already // allocated to buffer. Be warned! buffer = (NETRESOURCE*) realloc(buffer, bufferSize); ASSERT(NULL != buffer); ZeroMemory(buffer, bufferSize); break; default: break; } } while(ERROR_NO_MORE_ITEMS != rc); free(buffer); buffer = NULL; ::WNetCloseEnum(enumHandle); } return(TRUE); } BOOL CNetworkResourceCollector::Display() { CVisioApplication app; m_Addon->GetApp(app); CVisioDocuments documents; HRESULT hr; hr = app.Documents(documents); if(SUCCEEDED(hr)) { CVisioDocument document; BSTR filename = CString("Basic Network.vst").AllocSysString(); hr = documents.Add(filename, document); FREE_BSTR(filename);; if(SUCCEEDED(hr)) { CVisioMasters masters; short documentsCount; documents.Count(&documentsCount); for(int i = 1; i <= documentsCount; i++) { CVisioDocument document; documents.Item(COleVariant((long) i), document); BSTR bstrName; CString name; document.Name(bstrName); name = bstrName; FREE_BSTR(bstrName); TRACE1("%s\n", name); // We're looking for this specific stencil. if(-1 != name.Find("basic network shapes.vss")) { document.Masters(masters); break; } } CVisioPages pages; hr = document.Pages(pages); if(SUCCEEDED(hr)) { CVisioPage page; hr = pages.Item(COleVariant((long) 1), page); if(SUCCEEDED(hr)) { m_RootResource->Display(page, masters); Layout(page); } } } } return(SUCCEEDED(hr)); } CNetworkResource* CNetworkResourceCollector:: CreateNetworkResource(NETRESOURCE* buffer) { ASSERT(NULL != buffer); CNetworkResource* nr = new CNetworkResource(buffer); ASSERT(NULL != nr); return(nr); } void CNetworkResourceCollector::DeleteNetworkResources() { delete m_RootResource; m_RootResource = NULL; } BOOL CNetworkResourceCollector::Layout(CVisioPage& page) { HRESULT hr; CVisioShape shape; hr = page.PageSheet(shape); ASSERT(SUCCEEDED(hr)); CVisioCell cell; ShapeGetCell(shape, _T("User.visControlsAsInputs"), cell); CellSetFormula(cell, _T("0")); ShapeGetCell(shape, _T("User.visPlacementStyle"), cell); CellSetFormula(cell, _T("1")); ShapeGetCell(shape, _T("User.visPlacementDepth"), cell); CellSetFormula(cell, _T("1")); ShapeGetCell(shape, _T("User.visRoutingStyle"), cell); CellSetFormula(cell, _T("7")); ShapeGetCell(shape, _T("User.visResize"), cell); CellSetFormula(cell, _T("-1")); page.Layout(); return(TRUE); }