Introduction to the Catalog Object Model

Today's post will provide a basic introduction to the Windows Embedded CE Platform Builder Catalog. If you don't know what that is, consider yourself lucky. If you do have to deal with the catalog data, today's post might be helpful. We'll write a quick C# utility that demonstrates the catalog object model and also provides some useful diagnostic information that might be helpful to catalog file authors. The tool will display any errors or warnings that PB encounters while loading the catalog. It will also list all of the catalog files that it was able to load.

I don't really know how it happened, but somehow about 5 years ago, through no fault of my own, I ended up owning the PB "catalog". For those who don't know, the catalog is a repository for information about the CE operating system, and PB uses this information to help developers customize their CE builds. For example, if you have the environment variable "SYSGEN_AUDIO" set when you build a CE operating system, your operating system will include sound system support. The catalog has a list of these "features", along with a friendly title, description, and some metadata that helps PB ensure that the environment variable is properly exposed to the developer.

There were some significant issues with the catalog's design, and maintaining the catalog data was not a fun experience. CEC files were not  Maintaining the code that worked with the catalog was also problematic. For PB 6.0, I finally had the opportunity to start from scratch with a new catalog format. While a certain amount of backwards compatibility was required (the new catalog needed to essentially provide the same data as the old one, and there would need to be an upgrade path). So I designed a new catalog system, written in C# and based on XML files. While it isn't perfect, I think it is an improvement.

From a development perspective, one thing that is nice about the new catalog is that it has a pretty clean object model (an interface for developers who want to use the catalog). While we've never published the interface for the object model, we don't support it for external use, and everything I say here comes without warrantee or guarrantee of future compatibility, it isn't too hard to get started with basic tasks using the catalog object model. The reflection and metadata included in every managed assembly make this possible.

Before I go too far, I want to be clear that the design for the catalog object model emphasized the most common use of the catalog as a read-only repository. Loading and reading data from the catalog is (hopefully) reasonably simple and efficient. Using the object model to create or edit catalog files is a bit more of a challenge and not quite as efficient.

Loading a catalog means parsing about 2 megabytes of XML and loading the data into memory. While this happens in less than 1/10 of a second, we don't want to do this too often. In addition, we don't usually need (or want) multiple copies of the catalog in memory. To help with this, the catalog object model provides a cache of read-only loaded catalogs. When loading a catalog via the object model, you can choose to get your own copy of the catalog or you can ask for a read-only copy, which may come from the cache. (The cache is based on the .NET garbage collector via weak references. As long as anybody is using a catalog, it won't be evicted from the cache. As soon as nobody is using it, it becomes eligible for garbage collection. If it re-enters use before it is collected, it is no longer eligible for collection.)

The first step in using the catalog from your own code is to reference the catalog library. This code is in the Microsoft.PlatformBuilder.Catalog assembly, which (assuming you've installed Platform Builder 6 into C:\Program Files) can be found in C:\Program Files\Microsoft Platform Builder\6.00\cepb\idevs\Microsoft.PlatformBuilder.Catalog.dll. In Visual Studio, add a reference to this file. For convenience, you can add a "using" declaration to the top of your code to import the Catalog's namespace:

using Microsoft.PlatformBuilder.Catalog;

Next, to load a catalog, you use the CatalogManager class. The CatalogManager is a collection of catalog files. It handles searching through a winceroot for catalog files to load, loading all of the catalog files into memory, and maintaining the organization of the data in the catalog. It "flattens" the contents of the files, merging the data from all of the catalog files into a single collection of catalog elements. It also groups the data by type (so I can get a list of BSPs without going through all elements in the whole catalog), and generates indexes (so I can quickly look up a BSP by name).

When creating a CatalogManager, you can either create your own (writable) copy or you can load a read-only cached copy that might be shared by other components in the same process. To create a writable copy, just use the CatalogManager's constructor:

CatalogManager catalog = new CatalogManager();

The default constructor for the CatalogManager will load only the "Global" catalog files. These are the catalog files in C:\Program Files\Microsoft Platform Builder\6.00\Catalog, and need to be available to PB whether or not it is working with a CE OS tree. To load the catalog files from an OS tree as well as the global catalog files, just add the winceroot path to the constructor call:

CatalogManager catalog = new CatalogManager(@"C:\wince600");

If you don't need to make changes to the catalog manager's data and don't need precise control over the load sequence, you can use the CatalogManager factory method. This will return a cached catalog if one is available, so it is usually more efficient for repeated use than creating and loading a new catalog each time.

CatalogManager catalog = CatalogManager.GetCatalogManager(@"C:\wince600");

Next, we want to know if the catalog manager had any trouble while loading the catalog. For normal operation, developers generally don't want a defective catalog file to prevent PB from working correctly, so the catalog manager will silently catch catalog load errors. However, when working on catalog files, it is helpful to know what PB really thinks about your catalog. The catalog manager keeps track of warnings and errors generated during catalog load. You can access the errors via the catalog manager's Messages property, which is a read-only collection of CatalogMessage objects.

The catalog manager also provides a utility method to format the error or warning messages. It will then write the messages to any TextWriter (i.e. a StreamWriter or StringWriter). For example, to print all error or warning messages to stdout, you could do this:

CatalogManager.WriteCatalogMessages(catalog.Messages, Console.Out);

Finally, we want to access some of the data in the catalog. Let's print out a list of all of the catalog files that were successfully loaded:

foreach (CatalogFile file in catalog.Files)
{
    Console.WriteLine("CatalogFile: {0} - \"{0}\"",
file.FileInformation.Title, file.FullName);
}

Similar collections exist for the BSPs, CPUs, and "Items" in the catalog.

Here is a simple command-line program that pulls this all together. You can build this via a Visual Studio C# command-line app project, or you can compile this at the command line with the csc.exe C# compiler. The only trick is to add a reference to the Microsoft.PlatformBuilder.Catalog.dll assembly (from C:\Program Files\Microsoft Platform Builder\6.00\cepb\idevs).

Have fun!

using System;
using System.IO;
using Microsoft.Win32;
using Microsoft.PlatformBuilder.Catalog;
namespace CatalogErrors
{
public class Program
{
public static void Main(string[] args)
{
try
{
string winceroot = null;
if (args.Length > 0)
{
string arg0 = args[0].ToLowerInvariant();
if (arg0 != "/?" &&
arg0 != "-?" &&
arg0 != "/h" &&
arg0 != "-h" &&
arg0 != "--help")
{
winceroot = args[0];
}
}
else
{
winceroot = GetDefaultWinceroot();
}

if (winceroot != null &&
!Directory.Exists(winceroot))
{
Console.Error.WriteLine(
"ERROR: Winceroot \"{0}\" not found.",
winceroot);
winceroot = null;
}

if (winceroot == null)
{
Console.Error.WriteLine(
"Usage: CatalogErrors [winceroot]");
Console.Error.WriteLine(
"Displays any CE catalog load warnings and errors.");
Console.Error.WriteLine(
"If winceroot is not specified, uses default from registry.");
}
else
{
Run(winceroot);
}
}
catch (Exception ex)
{
Console.Error.WriteLine(
"ERROR: {0}: {1}{2}",
ex.GetType().FullName,
ex.Message,
ex.StackTrace);
ex = ex.InnerException;
while (ex != null)
{
Console.Error.WriteLine(
" : {0}: {1}{2}",
ex.GetType().FullName,
ex.Message,
ex.StackTrace);
}
}
}

private static string GetDefaultWinceroot()
{
string winceroot = null;
using (RegistryKey localDir = Registry.CurrentUser.OpenSubKey(
@"Software\Microsoft\Platform Builder\6.00\Directories"))
{
if (localDir != null)
{
winceroot = localDir.GetValue("OS Install Dir") as string;
}
}

if (winceroot == null)
{
using (RegistryKey globalDir = Registry.CurrentUser.OpenSubKey(
@"Software\Microsoft\Platform Builder\6.00\Directories"))
{
if (globalDir != null)
{
winceroot = globalDir.GetValue("OS Install Dir") as string;
}
}
}

return winceroot;
}

private static void Run(string winceroot)
{
CatalogManager catalog =
CatalogManager.GetCatalogManager(@"C:\wince600");
CatalogManager.WriteCatalogMessages(catalog.Messages, Console.Out);

foreach (CatalogFile file in catalog.Files)
{
Console.WriteLine("CatalogFile: {0} - \"{0}\"",
file.FileInformation.Title, file.FullName);
}
}
}
}