Creating a Complex Control

A complex control creates two properties: one of type DataSource and one of type DataMember in the control’s .odl (or .idl) file, which is compiled into its type library. Containers that support data binding recognize these properties and provide the necessary user interface for setting DataSource and DataMember. You can also set these properties in code. Containers that support data binding are Visual Basic, Visual C++ (MFC), and Internet Explorer 4.0. Microsoft Access will support data binding in a subsequent release.

Once DataSource is set, the control calls IDataSource::getDataMember, passing in the value of its DataMember property and retrieving an IRowPosition interface on return. To retrieve the rowset interface, the control calls IRowPosition::GetRowset, after which the process of data consumption can begin.

Complex controls also implement IDataSourceListener to be notified when data becomes available, becomes unavailable, or has been completely refreshed for a particular DataMember. A control calls IDataSource::addDataSourceListener when it wants to be notified, and IDataSource::removeDataSourceListener to remove a previously added listener. For more granular changes to the rowset, IRowsetNotify should be implemented and added to a rowset’s notification list using its IConnectionPointContainer interface. However, rowsets are not required to implement the latter two interfaces. Therefore, their usage may not be supported in some cases.

Creating Properties of Type DataSource and DataMember

The first step in creating a complex bound control is to create the DataSource and DataMember properties by modifying the control's .odl file. Respectively, DataSource and DataMember are properties of type DataSource and DataMember. These types are defined in Msdatsrc.tlb, which should be imported into the control’s library definition using the importlib ODL directive. Controls exposing these properties are recognized by containers as complex data-bound controls.

At design time, the container supplies a list of data sources for the DataSource property. Once a data source is selected for a control, the container supplies a list of data members for the DataMember property. This list is based on the selected data source.

Note   Visual Basic 5.0 does not recognize DataSource type properties, and therefore does not supply a list of data sources. Users must set the DataSource and DataMember properties in code.

Modifying the .odl File

To get definitions for DataSource and DataMember types, add the following code within the declaration of your control class library (you must do this before defining any other types):

importlib("STDOLE32.TLB");      // These two lines should
importlib(STDTYPE_TLB);      // already be in the ODL file.
importlib("msdatsrc.tlb");   // This line should be added.

Create your property definition as follows:

[id(DISPID_DATASOURCE), propget] 
HRESULT DataSource([out, retval] DataSource** ppdp);
[id(DISPID_DATASOURCE), propputref] 
HRESULT DataSource([in] DataSource* pdp);

// The DataMember property
[id(DISPID_DATAMEMBER), propget, bindable] 
HRESULT DataMember([out, retval] DataMember* pbstrDataMember);
[id(DISPID_DATAMEMBER), propput, bindable] 
HRESULT DataMember([in] DataMember bstrDataMember);

DataSource is defined as type IDataSource, and DataMember is defined as type BSTR in Msdatsrc.h. DISPID_DATASOURCE and DISPID_DATAMEMBER must be explicitly defined for the control and can be any unreserved value.

Note   The container can only supply a list of data members at design time if the DataMember property has the bindable attribute flag.

Retrieving the Rowset

Once a control's DataSource property is set, it should cache the IDataSource interface pointer, and perform only the necessary amount of work on the new data source. At design time, this may just involve releasing interfaces and deallocating memory pertaining to the previous data source. At run time, the process becomes more complex because a control may want to repaint its content using the new data. This may require a series of expensive operations. Control writers must decide beforehand what operations their controls must perform with a data source for design time and run time to best optimize the controls' response time. This distinction is illustrated later with the sample complex bound list control.

Ultimately, the control must retrieve the rowset presented by the data source in order to do what it was written for—to consume data. This is done by calling IDataSource::getDataMember, which takes as its first argument the data member name from the DataMember property. This can be Null or an empty string to designate the default data member. The second argument is the interface ID that the control can consume. For most cases, this is IID_IRowPosition. Finally, the third argument is a pointer to a buffer in which the provider returns a pointer to the interface requested.

The following code sample demonstrates the use of IDataSource::getDataMember to request a pointer to an IRowPosition interface:

// This method is called to initialize the member
// variable m_pRowPosition.
HRESULT CMyControl::InitRowPosition(void)
{
   // Initialize if you need to.
   if (m_pRowPosition) 
      return S_OK;
   
   // m_pDataSource is a member variable that caches
   // an IDataSource pointer.
   // m_dataMember is a member variable that stores
   // the current data member name. The default can be 
   // NULL or an empty string (""). 
   //
   // If getDataMember succeeds and the requested
   // interface pointer returned is NULL, then
   // the consumer must assume that no
   // data is available.
   //
   return m_pDataSource->getDataMember(m_state.dmDataMember, IID_IRowPosition, (IUKNOWN**)&m_pRowPosition);
}

The next step is to retrieve an IRowset interface by using IRowPosition::GetRowset. This method takes as its first argument the interface ID of any rowset interface that may be supported by the provider, and its second argument a buffer in which to return the requested pointer. The following code illustrates this:

// This method is called to initialize 
// the class member variable m_pRowset.
//
HRESULT CMyControl::InitRowset(void)
{
   // Initialize if you need to.
   if (m_pRowset)
      return S_OK;

   // Ensures that you have m_pRowPosition.
   HRESULT hr = InitRowPosition();
   RETURN_ON_FAILURE(hr);

   // m_pRowPosition may be NULL if data
   // is not yet avaiable.
   if (NULL == m_pRowPosition)
      return S_FALSE;

   // This line retrieves the rowset and 
   // stores it in a class member variable.
   hr = m_pRowPosition->GetRowset(IID_IRowset, (IUnknown **)&m_pRowset);
   return hr;
}

Interfaces for Data Binding

The following is a description of the interfaces that an OLE DB provider must implement to be consumed by OLE DB consumers. For more detailed information about implementing these interfaces, see the OLE DB Programmer’s Reference.

IDataSource

A consumer gains access to a data access interface by calling IDataSource::getDataMember. A consumer can request a number of data access interfaces. Here, the data access interface of interest is IID_IRowPosition.

IRowPosition

Consumers use IRowPosition to track and manage row currency. A consumer can gain access to the rowset object using IRowPosition::GetRowset.

Notifications

Notifications are an important aspect of data binding. They allow a control to track changes in the data source and respond appropriately to them. Refer to the section “Handling Notifications” in Chapter 1 for a review of this subject. Following is a discussion of how a control uses notifications as part of the data-consumption process.

IDataSourceListener

Once a control's DataSource property is set, the control should add its implementation of IDataSourceListener to the data source's notification list before it attempts to call IDataSource methods. This is crucial in the case of IDataSource::getDataMember, because this method may return Null with success when there is no data. When this occurs, a control must be able to handle this case and wait patiently until its IDataSourceListener::dataMemberChanged is called. This event signifies that data is available for the given data member. A control checks the passed-in data member parameter against its own. If the two match, then the control may again call IDataSource::getDataMember.

IDataSourceListener::dataMemberAdded allows consumers to update information that depends on concurrency with the data source's data member list.

IDataSourceListener::dataMemberRemoved must be handled by consumers to prevent them from continuing to access a rowset that is associated with a deleted data member. A consumer should check the data member passed in by the event against the one that the consumer is using and perform any necessary cleanup.

IRowPositionChange

This interface is implemented by consumers to track the current row in the data source. Controls that track the current row must process IRowPositionChange::OnRowPositionChange to stay current.

IRowsetNotify

IRowsetNotify notifications allow multiple consumers attached to the same rowset object to synchronize changes made to the rowset. This is the most comprehensive of the three notification interfaces, but is optional and may not be supported by all providers. For a detailed discussion of IRowsetNotify and notifications, refer to the OLE DB Programmer's Reference.

IConnectionPointContainer and IConnectionPoint

A consumer adds itself to the notification chain of IRowPositionChange and IRowsetNotify by using the IConnectionPointContainer interface. Because providers are not required to support IRowsetNotify, neither are they required to support IConnectionPoint interfaces. When these interfaces are supported, consumers use them to add and remove themselves from a provider’s notification chain.

For more information about the IConnectionPoint and IConnectionPointContainer interfaces, see the COM Programmer’s Reference and Automation Programmer’s Reference.