Listening to WPD Events

The WPD API allows applications to listen for events from the driver or the device. These events are primarily used to indicate changes in device state such as a new object being added, an object being deleted, etc. A list of possible WPD events is available in the MSDN documentation.

The events are delivered via a callback mechanism and are packed in an IPortableDeviceValues collection. The collection usually contains enough information about the affected object so that the client doesn't have to look up additional information. In fact, it is discouraged to look up additional information during event callbacks since that will affect performance. The rule of thumb is - if the event didn't contain the information you needed, you didn't need the information anyway.

You can imagine a scenario where your client is transferring multiple objects to the device. For each object transferred, the driver generates an WPD_EVENT_OBJECT_ADDED event. Now if your event callback tried to get more information from the driver, it would have to contend with the main thread that is busy transferring content and you end up throttling your overall transfer perf. 

Class definition

 // Our callback class must implement IPortableDeviceEventCallback
class CPortableDeviceEventsCallback : public IPortableDeviceEventCallback
{
public:
    CPortableDeviceEventsCallback():
            m_cRef(1), m_pwszEventCookie(NULL)
    {
    }

    ~CPortableDeviceEventsCallback()
    {
    }

    // Standard QI implementation for IID_IPortableDeviceEventCallback
    HRESULT __stdcall QueryInterface(
        REFIID  riid,
        LPVOID* ppvObj);
    
    // Standard AddRef implementation
    ULONG __stdcall AddRef();
    
    // Standard Release implementation
    ULONG __stdcall Release();  

    // Main OnEvent handler called for events
    HRESULT __stdcall OnEvent(IPortableDeviceValues* pEventParameters);    

    // Register for device events. 
    HRESULT __stdcall Register(IPortableDevice* pDevice);   

    // Unregister from device event notification
    HRESULT __stdcall UnRegister(IPortableDevice* pDevice);    

private:
    // Ref-counter
    ULONG   m_cRef;

    // Cookie to use while unadvising
    LPWSTR  m_pwszEventCookie;
};

COM Gunk

 // Standard QI implementation for IID_IPortableDeviceEventCallback
HRESULT __stdcall CPortableDeviceEventsCallback::QueryInterface(
    REFIID  riid,
    LPVOID* ppvObj)
{
    HRESULT hr = S_OK;
    if (ppvObj == NULL)
    {
        hr = E_INVALIDARG;
        return hr;
    }

    if ((riid == IID_IUnknown) ||
        (riid == IID_IPortableDeviceEventCallback))
    {
        AddRef();
        *ppvObj = this;
    }
    else
    {
        hr = E_NOINTERFACE;
    }
    
    return hr;
}

// Standard AddRef implementation
ULONG __stdcall CPortableDeviceEventsCallback::AddRef()
{
    InterlockedIncrement((long*) &m_cRef);
    return m_cRef;
}

// Standard Release implementation
ULONG __stdcall CPortableDeviceEventsCallback::Release()
{
    ULONG ulRefCount = m_cRef - 1;

    if (InterlockedDecrement((long*) &m_cRef) == 0)
    {
        delete this;
        return 0;
    }
    return ulRefCount;
}

Registering and unregistering for events

 // Register for device events. 
HRESULT __stdcall CPortableDeviceEventsCallback::Register(IPortableDevice* pDevice)
{
    //
    // Check if we are already registered. If so
    // return S_FALSE
    //
    if (m_pwszEventCookie != NULL)
    {
        return S_FALSE;
    }

    HRESULT hr = S_OK;

    CComPtr<IPortableDeviceValues> spEventParameters;
    if (hr == S_OK)
    {
        hr = CoCreateInstance(CLSID_PortableDeviceValues,
                              NULL,
                              CLSCTX_INPROC_SERVER,
                              IID_IPortableDeviceValues,
                              (VOID**) &spEventParameters);
    }

    // IPortableDevice::Advise is used to register for event notifications
    // This returns a cookie (string) that is needed while unregistering
    if (hr == S_OK)
    {
        hr = pDevice->Advise(0, this, spEventParameters, &m_pwszEventCookie);
    }

    return hr;
}

// Unregister from device event notification
HRESULT __stdcall CPortableDeviceEventsCallback::UnRegister(IPortableDevice* pDevice)
{
    //
    // Return S_OK if we are not registered for any thing
    //
    if (m_pwszEventCookie == NULL)
    {
        return S_OK;
    }

    // IPortableDevice::Unadvise is used to stop event notification
    // We use the cookie (string) that we received while registering
    HRESULT hr = pDevice->Unadvise(m_pwszEventCookie);

    // Free string allocated earlier by Advise call
    CoTaskMemFree(m_pwszEventCookie);
    m_pwszEventCookie = NULL;

    return hr;
}

OnEvent callback implementation

 // Main OnEvent handler called for events
HRESULT __stdcall CPortableDeviceEventsCallback::OnEvent(
    IPortableDeviceValues* pEventParameters)
{
    HRESULT hr = S_OK;

    if (pEventParameters == NULL)
    {
        hr = E_POINTER;
    }

    // The pEventParameters collection contains information about the event that was
    // fired. We'll at least need the EVENT_ID to figure out which event was fired
    // and based on that retrieve additional values from the collection

    // Display the event that was fired
    GUID EventId;
    if (hr == S_OK)
    {
      hr = pEventParameters->GetGuidValue(WPD_EVENT_PARAMETER_EVENT_ID, &EventId);       
    }

    LPWSTR pwszEventId = NULL;
    if (hr == S_OK)
    {
        hr = StringFromCLSID(EventId, &pwszEventId);
    }

    if (hr == S_OK)
    {
        printf("******** Event ********\n");
        printf("Event: %ws\n", pwszEventId);
    }

    if (pwszEventId != NULL)
    {
        CoTaskMemFree(pwszEventId);
    }

    // Display the ID of the object that was affected
    // We can also obtain WPD_OBJECT_NAME, WPD_OBJECT_PERSISTENT_UNIQUE_ID,
    // WPD_OBJECT_PARENT_ID, etc.
    LPWSTR pwszObjectId = NULL;
    if (hr == S_OK)
    {
        hr = pEventParameters->GetStringValue(WPD_OBJECT_ID, &pwszObjectId);
    }

    if (hr == S_OK)
    {
        printf("Object ID: %ws\n", pwszObjectId);
    }

    if (pwszObjectId != NULL)
    {
        CoTaskMemFree(pwszObjectId);
    }

    // Note that we intentionally do not call Release on pEventParameters since we 
    // do not own it

    return hr;
}

Registering for events requires providing the IPortableDevice::Advise API with an IPortableDeviceEventsCallback interface. This required us to create a class inheriting from that interface. Also, the Advise call expects it to be a regular COM interface (i.e. implementing IUnknown) and that's why we are required to implement QueryInterface, AddRef and Release. The OnEvent method is the one that will be called whenever a new event arrives. For illustration, we simply display the event ID and object ID (if any). We do not own the supplied pEventParameters parameter, so we make sure we don't call Release on it.

main() - Setting up the event notification

 //=============================================================================
// Main entry point 
//-----------------------------------------------------------------------------
int _cdecl _tmain(int argc, __in_ecount(argc) TCHAR* argv[])
{
    HRESULT hr = S_OK;
    
    // Initialize COM multi-threaded since event notifications arrive in a different 
    // thread. If we initialize COM single-threaded, we run the risk of a hang while
    // unregistering for the event notifications.
    hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);

    if (hr == S_OK)
    {
        // Place code to connect to the device here
        // IPortableDevice::Open(...)

        // Create an instance of our callback using the new operator
        CPortableDeviceEventsCallback* pCallback = new CPortableDeviceEventsCallback;

        if (pCallback == NULL)
        {
            hr = E_OUTOFMEMORY;
        }

        // Setup our callback class to register for events
        if (hr == S_OK)
        {
            hr = pCallback->Register(spDevice);
        }

        // Block so that our event callback will get a chance to execute
        if (hr == S_OK)
        {
            printf("Press any key to stop listening to events\n");
            _getch();
        }

        // Unregister for event notifications
        if (hr == S_OK)
        {
            hr = pCallback->UnRegister(spDevice);
        }

        // Call Release on our callback object, and /not/ delete
        if (pCallback != NULL)
        {
            pCallback->Release();
        }    

        // Close device connection
        if (bDeviceOpened)
        {
            hr = spDevice->Close();
        }   
    }

    CoUninitialize();

    return 0;
}

Registering for events turns out to be pretty easy once we use the CPortableDeviceEventsCallback class. We need to be sure to initialize COM multi-threaded to start with. The callback is allocated using new. We need to do this to avoid a race condition where the API may still be holding onto the callback after we unregister. This is also the reason why we call Release on the callback instead of simply freeing it using delete. This way the reference held by the API will not point to garbage. Once the API calls Release, the Release implementation will free the object.