About Cooperative and Asynchronous Data Retrieval

When a control has a moniker for a data path property, it will eventually want to call that moniker's IMoniker::BindToStorage method to retrieve an interface pointer through which the control can retrieve data (or write it as described in the following section).

To deliver optimal quality of service to browser users, a control must read data from an external source in a cooperative manner as much as possible. That is, the control should support reading linked data in an asynchronous manner, paying attention to container prioritization and allowing the container to participate in the transfer as it wants. To provide the container with authority over the binding operation, the control should use the IBindHost::MonikerBindToStorage and IBindHost::MonikerBindToObject methods to bind to any moniker.

A control will first, of course, check whether the moniker it obtained from the container is, in fact, an asynchronous moniker by querying it for IMonikerAsync (see About URL Monikers), which specified this interface as an alias for IUnknown. If this interface is not present, the moniker is synchronous and the control does not need to create an IBindStatusCallback callback interface to receive asynchronous notifications from the moniker bind operation.

If the moniker is asynchronous, the control operates as any other client of such a moniker does, by creating an IBindStatusCallback callback interface for asynchronous download, as described in About URL Monikers. The specific requirement for a control is that instead of binding to its moniker directly, it should bind to the moniker through its container, through IBindHost::MonikerBindToStorage or IBindHost::MonikerBindToObject. This call gives the container a chance to register any callbacks or other bits in the bind operation as desired. For example, a container that wants to watch the progress of the transfer through IBindStatusCallback::OnProgress will register its own callback for the bind operation, paying attention to IBindStatusCallback::OnProgress callbacks and delegating all other callbacks to the control that initiated the bind operation.

If the container does not implement IBindHost, the control can always bind to its monikers directly through IMoniker::BindToStorage and IMoniker::BindToObject. These should only be used in degenerate cases when there is no IBindHost present.

A control passes its IBindStatusCallback to its container directly through IBindHost::BindToXXX. However, if the control wants to participate in format negotiation (for example, HTTP), it must create a bind context and register its FORMATETC enumerator on this bind context before calling IBindHost::BindToXXX. When a control initiates an asynchronous transfer for a data path, usually within IPersist*::Load, it should always obtain the data through IBindStatusCallback::OnDataAvailable exclusively. This is so the control can immediately release the moniker and the bind context and return immediately from IPersist*::Load so that the container can continue processing. Such code inside IPersistStreamInit::Load where another asynchronous stream is obtained, for example, would appear as follows:

HRESULT CImpIPersistStream::Load(IStream *pstm)
   {
    IMoniker *pmkPath;
    IStream  *pstmAsync;
    IBindCtx *pbc;

    /* 
     * Load properties with pstm->Read, which is either blocking 
     * or synchronous.
     * Assume that one property is a path moniker, which is  
     * re-created with OleLoadFromStream into pmkPath.
     * Also, assume that m_pbsc is the IBindStatusCallback  
     * implementation, and m_pEnumFE is the FORMATETC enumerator.
     */

    // pIBindHost is a pointer to the container's IBindHost.
    CreateAsyncBindCtx(0, NULL, m_pEnumFE, &pbc);
    pIBindHost->MonikerBindToStorage(pmkPath, pbc, m_pbsc,
        IID_IStream, &pstmAsync);


    // Do this only for async monikers.
    if (NULL!=pstm)
        pstmAsync->Release();

    pbc->Release();
    pmkPath->Release();

    // Data shows up in IBindStatusCallback::OnDataAvailable.

    return S_OK;
    }

When retrieving data, the path moniker that names the external storage location is either synchronous or asynchronous. If the moniker used in binding is a synchronous moniker, read calls from the control will effectively block the execution thread as is expected—this is how the OLE programming model already works with regard to moniker binding.

Of course, an object that does not want to block and can return E_PENDING as necessary from some of its interface members (see below) might elect to perform the otherwise blocking read operations in another thread, thereby allowing the original thread to continue. This still allows IPersist*::Load to return right away without waiting for the data to arrive.

The tricky part in handling asynchronous data is communicating the control's readiness state, including E_PENDING return codes. These topics are covered later in this document.

Aborting a Data Transfer

Any control that is currently reading or writing data in an asynchronous manner must pay attention to IBindStatusCallback::OnStopBinding. If the control has all its data already, this notification merely says that the transfer is complete. If the control has not received all its data, the transfer was aborted for some other reason—perhaps the container scrolled the object out of view and stopped the transfers itself.

The control itself can call IBinding::Abort if it needs to stop the transfer for some reason. The control receives the IBinding pointer through IBindStatusCallback::OnStartBinding and must save it for later use. One such use is for handling the case where a container destroys a control while a transfer is under way—the control has to stop the transfer as part of its destruction procedure.

Also, if a container or authoring tool assigns a new value to a path property while the control is currently retrieving data asynchronously for that path, the control must stop the transfer through IBinding::Abort as soon as is reasonable and save the new path value.

Because a control cannot predict exactly how and when one or more data transfers will be aborted, it must be careful where re-entrance is concerned, especially inside the implementation of IBindStatusCallback::OnStopBinding, but in all other members as well. That is, the control should maintain an internal state machine to protect itself from operations that occur during or after an aborted transfer.

Transfer Priority

As the container owns the document as a whole, the container establishes which control will be given the right to transfer data first, second, and so on. When multiple transfers are going on at the same time, a container can control the relative priority of each transfer through its own implementations of IBindStatusCallback, which are used to control each bind operation, as managed through IBindHost::MonikerBindToXXX. A container can establish its desired relative priority in IBindStatusCallback::GetPriority. When the transfer begins and the callbacks receive IBindStatusCallback::OnStartBinding, the container will have the IBinding interface through which it can set the priority there as well. Controls should not interfere with this process.

Controls That Use WinInet or Sockets Directly

There is a certain class of controls that might want to bypass the mechanisms of the URL moniker to retrieve bits for some data path directly from the network through a transport layer such as Microsoft Win32 Internet (WinInet) or Sockets. In such cases, the control is not willing to call IBindHost::MonikerBindToStorage, but instead wants to use the absolute URL directly.

To accomplish this, the control still asks the container for the full moniker in the first place because this step is necessary to resolve any relative path names. When the control has the moniker, it can retrieve the full URL simply by calling IMoniker::GetDisplayName.

Object Persistence and Path Properties

When a control is asked to save its persistent data through some IPersist*::Save call, it is being asked to save its properties, including its current path properties and the BLOBs in the locations specified with those path properties. That is, when the control saves its properties into the storage appropriate for the IPersist* interface in use, it also synchronously saves its BLOBs by obtaining the moniker for the path property from the container as described in a later section, calling IMoniker::BindToStorage with the container's bind context, and writing data to the storage obtained earlier. When IPersist*::Save returns, the object has completely written any data it needed to save.

A control should save its path property values as strings, not as monikers. In short, a control should simply save whatever values are assigned to its data path properties (as opposed to serializing any monikers that might have been created using these strings).

Container Flexibility in Moniker Choice

The discussion in the previous section made no stipulations as to what class of moniker must be assigned to any path property. This is intentional: a container can instruct a control to store its persistent data in any location of the container's choosing, as long as the container can create a path that, when turned into a moniker, references the exact piece of storage that the control needs to load. In the case of creating a complex Web site, most of these monikers will be URL monikers. But there are many other moniker classes that can be used effectively, such as file monikers, generic composite monikers, or even custom monikers.

This design, therefore, allows the container full flexibility in choosing storage locations, depending on the authoring state of the document. At author time, the container (generally at the author's request) can choose to keep the persistent data of all controls embedded in the document. In this case, the container merely passes each control a name identifying some location in the document or on the local file system, such as a single item moniker or a file moniker. The container then provides the necessary binding support for whatever moniker it supplies through IBindHost::CreateMoniker. In the case where the authoring tool has the data stored in the document itself, supplying a file item moniker to the control means that the control's call to IBindHost::MonikerBindToStorage returns very quickly.

At publish time, the container can then choose to break whatever portions of control data it desires out of the document, saving that data in other locations and reassigning paths. Because the container will have some idea of the size of each control's persistent data, it can choose to leave some of that data embedded in the document anyway, leaving the controls concerned with simple file item monikers, and such. If the container moves some data to other Web sites, it would create URL monikers for those sites and assign them the path properties of the appropriate controls. As far as the control is concerned, the data has moved, but the data is still named with some moniker. The control continues to use the same IBindHost::MonikerBindToStorage code that it did before.

What is important to understand here is that the control never has to distinguish between author-time and publish/run-time differences as far as its data path storage is concerned. The control always has some moniker naming its persistent data stores, and always calls IBindHost::MonikerBindToStorage in all circumstances. This maintains a single programming model across all document modes, while still giving the container complete flexibility in choosing storage locations and how the data in the document is distributed.

Exposing Path Properties from Nested Controls

Some controls, regardless of whether they use any path properties, might contain other nested controls that do use such paths. Nevertheless, an authoring tool will want to construct a list of all paths used by all controls, regardless of the level of nesting involved. This facilitates site management—the authoring tool can examine all external data references from the top-level document and correct the paths involved if the site is being relocated.

Any control that itself uses one or more path properties need only expose those properties as described in earlier sections. Those controls that themselves contain other controls must expose those nested controls so that an authoring tool can check the path properties in those controls as well.

For this reason, a containing control must implement IOleContainer to allow easier navigation to a particular control given the control's name, with the behavior shown in the following table. Note that the containing control might also choose to implement IOleItemContainer, which is derived from IOleContainer, as well.

Member function Behavior
IUnknown members Implemented as usual.
IParseDisplayName::ParseDisplayName Returns E_NOTIMPL.
IOleContainer::LockContainer Returns E_NOTIMPL.
IOleContainer::EnumObjects Returns an IEnumUnknown enumerator through which the caller can retrieve the IUnknown pointers for each nested control.

 

Given this behavior, the top-level container or authoring tool that wants to examine all path properties in all controls, regardless of the nesting level, uses IProvideClassInfo::GetClassInfo to retrieve the control's type information, and then uses the same procedures described earlier to build a list of the control's dispIDs that each correspond to some data path property. The caller can retrieve this property value, examine it, and modify it as necessary. This procedure is summarized in the following code example.

// In top-level container.
construct IEnumUnknown for all top-level controls in pEnum
FindAllPaths(pEnum)
// Done.

function FindAllPaths(IEnumUnknown *pEnum)
    }
    IUnknown *pObj;

    while (SUCCEEDED(pEnum->Next(&pObj)))
        {
        call IProvideClassInfo::GetClassInfo or obtain coclass type
            information via type lib

        if (SUCCEEDED(ITypeInfo2::GetCustData) and value is greater
            than zero)
            {
            iterate over properties
                {
                for each path property
                    {
                    examine the path
                    update/modify the path as necessary
                    }
                }

            ITypeInfo2::Release();
            }

        if (SUCCEEDED(pObj->QueryInterface(IID_IOleContainer,
            &pCont)))
            {
            pCont->EnumObjects(&pEnumNested)

            if (NULL!=pEnumNested)
                FindAllPaths(pEnumNested)

            pCont->Release();
            }

        pObj->Release();
        }
    }

Through this process, the top-level container can examine all path properties in use in the document. It is unlikely that this will be a seriously expensive operation in all but the most complex documents. Typically, there will only be a handful of controls on a page with any path properties at all, and even fewer containing controls with a significant number of nested controls. Second, third, fourth, and deeper levels of nesting will be exceptionally rare, so implementing this scheme with a recursive code structure will not be expensive in probably ninety percent of all situations.

What is described here for a containing control does not apply to controls that internally use some kind of helper object as a supporting component. In such cases, the helper object is not a "nested control"—it is merely a provider of a useful service. The control that is using the helper should not consider itself a "containing control" because it does not implement the necessary container-side services that would be necessary to nest an actual control.