Data Path Properties

The impetus for much of the work in this article finds its origins in the concept of progressive rendering (progressive disclosure) for something like a picture or a picture control. In such cases, the picture should be able to display an initial low-detail picture, followed by mid-detail and high-detail renderings.

This article is, however, concerned with the more general problem of retrieving any significant amounts of data in a cooperative fashion from possibly many distributed locations after the control has already been instantiated and is possibly interacting with the container and the user in other ways. This capability has been called "Progressive Rendering," "Progressive Property Disclosure," and "Progressive Downloading." The term "Progressive Data Retrieval" will be used to refer to all of these more special cases at once, because it makes no assumptions about the exact type of data that is being retrieved—for example, it might be properties, might be images, might be anything.

In all the following sections, it is very important to keep in mind that the control is ultimately responsible for specifying the exact format and structure of any linked data and deciding exactly how it will be pulled from the named source. The container has nothing to do with this, other than being able to control the relative priority of the retrieval and being notified when the loading state of a control changes. So, for example, a container doesn't have to care about how a picture control chooses to progressively render low-detail, mid-detail, and high-detail images (from a GIF, for instance). The picture control would internally know where and how each image is stored in its data, and retrieve those in turn. The container would then be notified through IAdviseSink::OnViewChange when the control has enough data to do something meaningful in IViewObject::Draw (which is a moot point for an in-place active control whose window is visible, because the control would just update itself, but this will be important later on for windowless controls).

The point is that the container chooses where to tell controls to store and retrieve their data; the controls choose how to store and retrieve that data in a cooperative, asynchronous, and progressive manner. The asynchronous aspect can apply to none, some, or all of a control's data.

This section is, thus, about "Data Path Properties," and describes the architecture for having a control save and load pieces of its data (BLOBs especially) in locations other than where the control's properties are kept. That is, for any number of reasons, it might be inappropriate or impossible for a control to store absolutely all of its data in the same location as its properties. Here are some examples:

  • The control is being embedded and its BLOB data is too large to store in an embedded manner, such as in an HTML document.
  • The control wants to retrieve BLOBs in an asynchronous manner regardless of where the control's properties are stored. A video control is a good example where this would be important.
  • Because the control might be sharing BLOBs with other controls, it is inappropriate, wasteful, or both to duplicate the BLOB in each control's persistent state (this is similar to the original linking concept in OLE Documents). Sharing a company logo across many Web pages is a good example, and excellent performance can be achieved in this case when a URL cache is in operation—the BLOB itself might have already been downloaded from another page and can simply be pulled from the cache immediately.
  • The control might want to allow BLOBs to be modified so that whenever the control retrieves the data, it can retrieve the most recent information. A weather-map control that always shows the latest satellite image is a good example.
  • The control might simply want to separate properties from BLOBs for its own reasons.

To address these particular concerns, a control can use one or more "Data Path Properties" to reference external sources of data. As shown in the following illustration, each path, as they're called for short, is a value of some kind that is stored like any other properties in the control's persistent data. When the control needs to retrieve data from a source, it has the container create a moniker from the string representation of the property value and calls IMoniker::BindToStorage to retrieve some sort of storage pointer (usually IStream, but not limited to that). In binding a URL moniker, the control can choose whether the binding itself will be synchronous or asynchronous, and whether the data transfer itself will be synchronous or asynchronous.

In short, each path property is a control-managed link to an outside data source. As described in an earlier section, this capability has always been available in OLE Documents, except that such links were always internal to the control. Such hidden use of external resources is not acceptable in the Internet environment where authoring tools need to manage the interconnections between many Internet sites.

Data Path Properties address the following issues:

  • How to expose the existence of path properties from a control.
  • How to specify exactly what type of data a particular path should refer to so that an authoring tool can provide appropriate browsing UI or automatically assign a path property referring to the correct format of data.
  • How to assign values to path properties, which can occur through either the authoring tool (automatically or manually) or the control itself (manually through its own property pages).
  • How to save each path property in the control's persistent data.
  • How to cooperatively retrieve data referred to with a path property, which might include container participation.
  • How to communicate a control's readiness to a container at run time so that the container can manage dependencies on the control's internal state.

Linking capabilities using path properties are equally usable in both the Persistent Embedding and Persistent Linking cases as described in an earlier section. For clarity, the combinations of Persistent Embedding and Persistent Linking with Data Path Properties are shown in the following illustrations.

With data paths, a control can allow its persistent data to be distributed around a file system or the Internet. For example, a sophisticated motion-picture control can separately reference its AVI data, its audio data, and a lengthy text transcript of the dialog box. The control could then have MoviePath, AudioPath, and TranscriptPath properties. In a similar manner, a picture control might store three separate detail images and use the properties LowDetailImagePath, MediumDetailImagePath, and HighDetailImagePath (and maybe the motion picture control uses these three as well to provide progressive rendering of a static image—anything is possible). This document places no restrictions on the exact data formats stored in locations given to path properties—the control ultimately determines the formats it can employ.

The following sections provide information about data path properties, starting with how a control exposes its path properties and the data formats associated with those paths. This is followed with a discussion of how path values are assigned, how a control saves a path property, and how a control uses a property to retrieve data asynchronously. A later section describes how the control communicates its "readiness" to a container and how the container handles different readiness states.

Exposing Data Path Properties and Corresponding Data Format

The following characteristics apply to publicly exposed path properties and controls that provide them:

  • A path property can be a property of any type, most commonly a BSTR (allowing easy manipulation through Microsoft Visual Basic code such as MyObj.TranscriptPath), but other types are allowed, such as IUnknown or IDispatch. Choice of the exact property type is the decision of the control developer.
  • All path properties regardless of type are tagged with a special custom attribute GUID that identifies the property as being a path. This simplifies the process for an authoring tool to find path properties with a quick check through the control's type information. This standard GUID is defined as GUID_PathProperty with the value of {0002DE80-0000-0000-C000-000000000046}.
  • The custom attribute described in in the second description above can express one or more MIME type values to describe the actual data type that this path should reference. The syntax is the same as that for the Accept: fields of HTTP headers (see below, and see the HTTP specification for complete information).
  • A path property can be assigned any dispID that the control developer sees fit.
  • A control marks each path property with the [bindable] attribute. This is because a container might allow path assignment through its own user interface and might be displaying path properties in its own property browser. Thus, [bindable] is necessary to keep the property browser synchronized with the control's state. With this attribute, the control must call IPropertyNotifySink methods if the container has connected an implementation of that sink interface through the control's connection points. Controls can choose to mark path properties as [requestedit] and [displaybind], if desired, but these are not requirements.
  • The control's coclass entry in its type information is tagged with another special custom attribute GUID that specifies "this control has one or more path properties." This standard GUID is defined as GUID_HasPathProperties with the value {0002DE81-0000-0000-C000-000000000046}. The value of this attribute is the number of path properties that exist in the control. This attribute thus allows authoring tools to quickly and efficiently determine if a control has path properties at all, and quickly describes how many path properties exist.

The HTTP Accept: syntax is summarized as follows:

<field>  =    Accept: <entry> *[ , <entry>
    <entry>  =    <content type> *[ ; <param> ]
    <param>  =    <attr> = <float>
    <attr>   =    q / mxs / mxb
    <float>  =    <ANSI-C floating point text representation>

as in:

Accept: text/plain, text/html, image/*
Accept: text/x-dvi; q=.8; mxb=100000; mxt=5.0, text/x-c

A "custom attribute" as now supported in type information is defined using the keyword custom with a (GUID, value) pair as arguments for the attribute. This is shown in the following example:

#define GUID_PathProperty 0002DE80-0000-0000-C000-000000000046

[id(1), bindable, displaybind, propget, custom(GUID_PathProperty,
    "text/plain;
    q=0.5, text/html, text/*")]
    BSTR TranscriptPath(void);

This defines the BSTR property TranscriptPath as a path property with the types of plain text, HTML text, and other text (using HTTP Accept: syntax). This information is retrieved from the type information through the new type library interface ITypeInfo2 as described in the next section.

As an example of defining path properties in this way, consider a control that has an AVI MoviePath property, a RIFF AudioPath property, and an ANSI text TranscriptPath property that would include them in its type library this way (in ODL syntax; exact MIME types have been omitted because the code below is demonstrating the definition of multiple properties).

#define GUID_PathProperty      0002DE80-0000-0000-C000-000000000046
#define GUID_HasPathProperties 0002DE81-0000-0000-C000-000000000046

[<attributes>]
library
    {
    importlib <path to header that includes GUID_PropertyPath and
    GUID_HasPathProperties>

    [<attributes>]
    interface IMyObjectProperties
        {
        // Other stuff here...

        [id(1), bindable, displaybind, propget, custom(GUID_PathProperty, "...")]
            BSTR MoviePath(void);

        [id(1), bindable, displaybind, propput, custom(GUID_PathProperty, "...")]
            void MoviePath([in] BSTR pathVideo);

        [id(2), bindable, propget, custom(GUID_PathProperty, "...")]
            BSTR AudioPath(void);

        [id(2), bindable, propput, custom(GUID_PathProperty, "...")]
            void AudioPath([in] BSTR pathAudio);

        [id(3), bindable, propget, custom(GUID_PathProperty, "...")]
            BSTR TranscriptPath(void);

        [id(3), bindable, propput, custom(GUID_PathProperty, "...")]
            void TranscriptPath([in] BSTR pathText);

        // Other stuff here...
        }


    [<other attributes>, custom(GUID_HasPathProperties, "3")]
    coclass MyObject
        {
        interface IMyObjectProperties;
        ...
        }

    }

Author-Time Discovery of Path Properties

Authoring tools and other containers will want to know exactly which control properties are, in fact, data path properties as well as the type of data that should be referenced through each property. This allows the authoring tool to effectively enumerate data paths and retrieve their current values, which is useful when the authoring tool needs to update any such properties to perform link management.

First, to determine if any particular control has any path properties, the authoring tool should perform the following actions:

  1. Retrieve the control's type information for its coclass either through IProvideClassInfo::GetClassInfo or from the type library directly. These methods result in an ITypeInfo interface for that part of the type library.
  2. Call ITypeInfo::QueryInterface(IID_ITypeInfo2, &pITypeInfo2) to retrieve the interface necessary to read custom attributes. If this interface does not exist, the container has to assume that no path properties exist.
  3. Call pITypeInfo2->GetCustData(GUID_HasPathProperties, &va) to retrieve the value of the custom attribute into the VARIANT in va. If this function fails, or if the value in va is empty or zero, the control has no path properties.

Having this knowledge in hand, the authoring tool can then enumerate the properties in any of the control's incoming interfaces and check if those properties are data paths by using the following procedure. These steps assume that pTI2 is the ITypeInfo2 pointer to one of the control's interface or dispinterface entries (available through the ITypeInfo of the control's coclass).

  1. Get the count of the functions in the interface using pTI2->GetTypeAttr, which returns a TYPEATTR whose cFuncs field has the number of functions. Call pTI2->ReleaseTypeAttr when finished.
  2. Iterate in a for loop where the index counter (IHTMLOptionElement::index) begins at zero and counts to cFuncs-1 (cFuncs is from step 1) through the functions.
  3. For each function, call pTI2->GetFuncCustData(index, GUID_PathProperty, &va) to attempt to read the custom path property attribute's value into the VARIANT va.
  4. If the ITypeInfo2::GetFuncCustData call fails, or if the contents of va are empty, this property is not a data path. On success, the property is a data path and the value in va will be the description of the MIME type.
  5. To determine the dispID of the property, call pTI2->GetFuncDesc and look in the FUNCDESC structure for the memid field, which is the dispID. Be sure to call pTI2->ReleaseFuncDesc when done with the FUNCDESC structure.
  6. For optimization purposes, one can count the number of path properties found so far and exit the loop early if that number matches the value that was read from the GUID_HasPathProperties in the steps used to determine if the control had any paths.
  7. When the loop is complete, the authoring tool will have a list of dispIDs (which might have some duplicates) for all the control's data path properties. The authoring tool can use pTI2->GetNames with these dispIDs to retrieve text names for those properties.

What then does a tool do with such knowledge of paths and their data types? There are three general scenarios:

  • If the authoring tool is interactive, it might be able to display the data browsing UI based on the data format associated with any given path property. That is, if the tool knows that a path property uses the MIME type given in the value of the GUID_PathProperty attribute and that tool has a UI that can browse for that type, the tool can enable a "..." button in its property browser UI that calls a File/Open type of dialog box when clicked. The user then selects a file, and the name of that data file is assigned to the path property.
  • If the authoring tool is automatic, it uses other available information (for example, a data repository) to determine the source of any particular piece of data. Knowing the format that the control needs for a data path allows such a tool to find some appropriate data, create a path name for it, and assign that name to the control.
  • When the authoring tool publishes a document to an Internet site, it will want to check for any dependencies on outside data—that is, it will want to check for any control properties that reference data outside the document. The authoring tool can then check whether that path will be valid at run time and if not, it can choose to relocate the data to an accessible location and assign a new path to the control before saving that control. This is necessary, for example, when a control references a file on the local file system when the document is being placed on an Internet site.

Of course, the sophistication of the authoring tool is almost limitless. With the tagging and MIME-typing of data path properties, this document guarantees that any such tool can always determine the data format associated with a path. It's then a matter of whether the tool recognizes that GUID.

Assigning Path Properties

The values of path properties—the strings that identify storage locations—are assumed to be assigned only at author time. In this case there are two possibilities:

  • The authoring tool obtains the name from the user or creates it automatically, assigning it to the control through a "set property" operation (through a dispinterface or dual interface depending on the nature of the control). Note that if a transfer is already under way for this property, the control must abort the transfer as described below.
  • The control obtains the name itself from its own property page UI; in that case it makes the change unilaterally. Any path properties marked with [requestedit] means the control is required to call any connected IPropertyNotifySink interface's IPropertyNotifySink::OnRequestEdit function to decide whether the change is actually allowed. If IPropertyNotifySink::OnRequestEdit returns S_FALSE, the control must not change its path in this way (the authoring tool is essentially saying that the document or Internet site state is read-only as far as document/page contents are concerned).

In both cases, a change in the value of any path property will generate a call to any connected IPropertyNotifySink interface's IPropertyNotifySink::OnChanged member, because path properties are marked with [bindable]. In both cases, an authoring tool that displays its own property browsing UI would update itself to reflect the current state of the control. This also gives the authoring tool the chance to modify the path when the user changes it through the control's property pages directly. When the user makes the change, the control will notify the authoring tool through IPropertyNotifySink::OnChanged. The authoring tool then reads the new value of the path property, modifies it as necessary, and sends the modified value back to the control. Therefore, in both the first and second cases above, the authoring tool has the final say in the exact value assigned to any path property.

The most preferable case is where the value is simply a container-relative name. Relative file names and URLs are identical in form: "../pictures/tree.bmp" serves equally well in both the local file system and Internet environments. Relative item names, such as "!Picture 6", identify the path as pointing to some other part of the same container document. In URL cases, the name can be absolute, which indicates that there's no relative path between the container document and the location described in the data path.

In any case, a control must always obtain some moniker from a data path name to bind to the storage so referenced. Because the name can only be relative, the container is always given the chance to create the actual moniker itself. The details of this are covered in the next section.

Use of relative names allows an authoring tool to move the root document—and possibly an entire site—around, without invalidating any external links stored in data paths. For example, consider the author-time case where the document being created is stored in "c:\pages\mypage.doc" and in that document is a bitmap control with an ImagePath property. The authoring tool assigns the name "frog.bmp" to this property. When the control wants to access the bitmap named in the ImagePath property, it passes "frog.bmp" to the authoring tool and asks for a moniker in return. The authoring tool sees two file names and can combine them into one "c:\pages\frog.bmp" name. The authoring tool then returns this name in a moniker so that the control can bind that moniker and retrieve the data.

Now when the author saves the document to an Internet site at publish time, the location of the document changes to something like "http://www.bogons.com/bogazoid/mypage.htm". In addition, the authoring tool copies "frog.bmp" to that same site, where it becomes "http://www.bogons.com/bogazoid/frog.bmp". At run time, the control reloads the path property name "frog.bmp" and asks the container to make it a moniker. The container simply combines the document URL with the relative name "frog.bmp" to generate the exact URL for the bitmap.

In some cases, the control might have obtained an absolute file name (or URL) from its own property pages at author time. So, for instance, the user types "c:\pages\frog.bmp" instead of just "frog.bmp." Initially, the control will internally save the absolute string, but because it sends an IPropertyNotifySink::OnChanged notification (data path is [bindable]), the authoring tool retrieves this new value and computes the relative path to that same file from the document's current location (using the file moniker's RelativePathTo member, for example). The relative path in this case will end up as just "frog.bmp", which the authoring tool simply assigns back to the control.

In the case where the user gives a control an absolute URL, there's nothing the authoring tool can do at author time when the document is stored on a local file system. At publish time, however, the authoring tool first determines the new URL of the document. It can then cycle through all known path properties of each control before it saves that control. If the authoring tool finds an absolute URL in a property, that tool can attempt to compute the relative path between the document's URL and the path's URL. When it has computed this relative path, it can assign the relative name to the path property and proceed with saving the control.

When the authoring tool supports relative path names that are not either file names or URLs, but are instead item names that identify a piece of storage in the document itself, such as "#Picture 6", the authoring tool must specify, in its own documentation, what prefix character marks the name as an item of some kind. This is a note to the container's moniker creation mechanism in IBindHost::CreateMoniker as described in the next section.

Storing and Comparing Path Properties

Controls should always ensure that they can produce a string for any path property. This is because all interfaces that deal with path properties (either IDispatch for the property itself or IBindHost, in a section below, for converting the string into a moniker) deal with paths as strings.

However, controls should not compare two path properties as strings. Instead, the comparison should be carried out using two monikers created from those strings through IBindHost::CreateMoniker (see below). With monikers, the comparison between pmk1 and pmk2 is the call pmk1->IsEqual(pmk2) or pmk2->IsEqual(pmk1).

Integrated Author Tool Browsing and Control Property Pages

A sophisticated container or authoring tool will likely provide its own browsing UI for various types of data, typically the most common types of data that would be assigned to path properties (images, sound, and video). To maintain as much of a consistent user interface as possible, a container should make such a browsing UI available to a control's property pages so that the property page can invoke that UI instead of showing its own, which would likely be inconsistent.

This integration is achieved through a container-side interface called IOleBuilderManager. An exact specification for this interface is not, however, available at this time and will be provided at a later date. The basic idea, however, is that through this interface a control's property page can test for the availability of container-side browsing UI and invoke that UI if it is available. This allows the page to enable a Browse... button for a particular path property, invoking the container's UI when that button is clicked.

Container Supply of Monikers Through IBindHost

Working with data paths requires, of course, the ability of the control to access data referenced by its path properties. The key part of such data access is providing some means by which the control turns a data path name into a moniker that it can bind. Because the container/authoring tool is the one that generally knows the context that modifies any relative path name, the container, in this relationship, is responsible for generating an appropriate moniker when the control asks for one.

This step happens through a container's (or authoring tool's) implementation of the IBindHost interface, which is implemented as a service available through a container site's implementation of the IServiceProvider interface, and is the topic of the next section. The control accesses the bind host implementation by calling QueryInterface(IID_IServiceProvider...) on whatever site pointer it already has and calling IServiceProvider::QueryService(SID_SBindHost). Use of the service provider means that the bind host is generally a separate object from the site, although as an implementation convenience it could be implemented on the site directly.

Of course, for a control to have a site through which it can access these services, the control must provide some means through which the container can pass its site pointer to the control in the first place. Many controls implement IOleObject, whose IOleObject::SetClientSite method accomplishes exactly this. However, not all controls need to implement IOleObject, so a section below describes a simple siting mechanism using IObjectWithSite to achieve the same ends.

This document requires that a control using data paths supports a siting mechanism. If the control doesn't support IOleObject, it must support IObjectWithSite. A container that supports data paths must provide a site through one of these mechanisms and must generally implement IBindHost and provide access to it through the site's IServiceProvider interface. If a control that uses paths finds itself without a container site, it can still degenerate gracefully by calling MkParseDisplayNameEx itself, as necessary.

Overview of IBindHost

The creation of a moniker from a potentially relative path name is what is considered a container-side "service" that a control accesses through IBindHost::CreateMoniker. In addition, a control asks its container to download data through IBindHost::MonikerBindToStorage as described in a later section.

The IBindHost interface is defined as follows:

interface IBindHost : IUnknown
    {
    HRESULT CreateMoniker([in] LPCOLESTR pszName, [in] IBindCtx *pBC
        , [out] IMoniker** ppmk, [in] DWORD dwReserved);

    HRESULT MonikerBindToStorage([in] IMoniker *pMk, [in] IBindCtx *pBC
        , [in] IBindStatusCallback *pBSC, [in] REFIID riid
        , [out, iid_is(riid)] void **ppvObj);

    HRESULT MonikerBindToObject([in] IMoniker *pMk, [in] IBindCtx *pBC
        , [in] IBindStatusCallback *pBSC, [in] REFIID riid
        , [out, iid_is(riid)] void **ppvObj);
    }

The IBindHost::CreateMoniker method takes a text string of some path—which can be absolute or relative—and returns a moniker that references the actual absolute location. That is, the container combines its known document location with the path supplied in pszName to create the resulting moniker. How the container does this depends on the document path and the type of name found in pszName, which is either something that the URL moniker can handle (including file names) or an item name referring to some portion of the container document itself.

In the item name case, a container can specify a prefix character, such as "#" or ">", that it will use to differentiate an item name from some other kind of name. This fact should be known to users of the container at author time so that those users can enter the correct names in whatever UI is used to assign path values. When the container detects its prefix character, it knows that it has to parse the name according to its own rules.

In any other case, the container creates a moniker from the text string directly using MkParseDisplayNameEx. This handles the cases where URLs are given as well as display names of file item monikers and other similar composites. If for some reason IBindHost::CreateMoniker fails, the calling control can degenerate to calling MkParseDisplayNameEx itself, but this should only be used as a last resort.

Either way, the container now has a moniker for the data path as well as a moniker naming its own document. To return a full moniker from MkParseDisplayNameEx, it calls pmkDocument->ComposeWith(pmkPath,fOnlyIfNotGeneric,pmkReturn), where pmkReturn receives a pointer to the moniker to return the caller.

This rule assumes that the document moniker class implements IMoniker::ComposeWith in an intelligent manner. If the document moniker is a file moniker, for example, and the path moniker is a file moniker, IMoniker::ComposeWith will return one file moniker with the combination. The same applies for any case where the path moniker is a URL and the document is either a file or a URL. It is even possible to mix a URL document moniker with a file moniker for the path, provided that the file moniker is not absolute (where the combination would make no sense).

When the path moniker is some item or composite, the file or URL moniker IMoniker::ComposeWith implementations simply create another composite.

The bottom line is that IBindHost::CreateMoniker will create a precise moniker that is the combination of the document's own moniker (obtainable through IOleClientSite::GetMoniker(OLEWHICHMK_CONTAINER)) and a moniker created from the path name. The control can use this moniker to bind, but this is not the moniker that the control should save. See below for more information on persistence.

Generic Siting with IObjectWithSite

A control that does not need IOleObject, but still requires access to its container site specifically for the purposes of accessing container-side services, must instead implement IObjectWithSite.

interface IObjectWithSite : IUnknown
    {
    HRESULT SetSite([in] IUnknown *pUnkSite);
    HRESULT GetSite([in] REFIID riid, [out, iid_is(riid)] void **ppvSite);
    }

In the absence of IOleObject a container can attempt to provide the IUnknown pointer to its site through IObjectWithSite::SetSite. IObjectWithSite::GetSite is included as a hooking mechanism, which allows a third party to intercept calls from the control to the site.

When a control needs to create a moniker from a data path, it obtains the bind host through the site's IServiceProvider interface and proceeds from there.