Helper functions to make shell bind contexts slightly more manageable


Last time, we learned about the wonderful world of shell bind context strings, and I promised some helper functions to make this slightly more manageable.

Here are some helper functions which supplement the Create­Bind­Ctx­With­Opts function we created some time ago.

#include <propsys.h>

HRESULT EnsureBindCtxPropertyBag(
    IBindCtx *pbc, REFIID riid, void **ppv)
{
 *ppv = nullptr;
 CComPtr<IUnknown> spunk;
 HRESULT hr = pbc->GetObjectParam(STR_PROPERTYBAG_PARAM, &spunk);
 if (FAILED(hr)) {
  hr = PSCreateMemoryPropertyStore(IID_PPV_ARGS(&spunk));
  if (SUCCEEDED(hr)) {
   hr = pbc->RegisterObjectParam(STR_PROPERTYBAG_PARAM, spunk);
  }
 }
 if (SUCCEEDED(hr)) {
  hr = spunk->QueryInterface(riid, ppv);
 }
  return hr;
}

HRESULT AddBindCtxDWORD(
    IBindCtx *pbc, LPCWSTR pszName, DWORD dwValue)
{
 CComPtr<IPropertyBag> sppb;
 HRESULT hr = EnsureBindCtxPropertyBag(pbc, IID_PPV_ARGS(&sppb));
 if (SUCCEEDED(hr)) {
  hr = PSPropertyBag_WriteDWORD(sppb, pszName, dwValue);
 }
 return hr;
}

HRESULT AddBindCtxString(
    IBindCtx *pbc, LPCWSTR pszName, LPCWSTR pszValue)
{
 CComPtr<IPropertyBag> sppb;
 HRESULT hr = EnsureBindCtxPropertyBag(pbc, IID_PPV_ARGS(&sppb));
 if (SUCCEEDED(hr)) {
  hr = PSPropertyBag_WriteStr(sppb, pszName, pszValue);
 }
 return hr;
}

HRESULT CreateDwordBindCtx(
    LPCWSTR pszName, DWORD dwValue, IBindCtx **ppbc)
{
 CComPtr<IBindCtx> spbc;
 HRESULT hr = CreateBindCtx(0, &spbc);
 if (SUCCEEDED(hr)) {
  hr = AddBindCtxDWORD(spbc, pszName, dwValue);
 }
 *ppbc = SUCCEEDED(hr) ? spbc.Detach() : nullptr;
 return hr;
}

HRESULT CreateStringBindCtx(
    LPCWSTR pszName, LPCWSTR pszValue, IBindCtx **ppbc)
{
 CComPtr<IBindCtx> spbc;
 HRESULT hr = CreateBindCtx(0, &spbc);
 if (SUCCEEDED(hr)) {
  hr = AddBindCtxString(spbc, pszName, pszValue);
 }
 *ppbc = SUCCEEDED(hr) ? spbc.Detach() : nullptr;
 return hr;
}

The Ensure­Bind­Ctx­Property­Bag function puts a property bag in the bind context if there isn't one already.

The Add­Bind­Ctx­DWORD function adds a DWORD to that associated property bag. If you wanted to add multiple DWORDs to a bind context, you would call this function multiple times. You can also use the Add­Bind­Ctx­String if the thing you want to add is a string.

The Create­Dword­Bind­Ctx function handles the simple case where you want to create a bind context that contains a single DWORD. Similarly, Create­String­Bind­Ctx.

But now things are starting to get kind of unwieldy. What if you want a bind context with a string and a DWORD? Let's go for something a bit more fluent.

But first, some scaffolding.

class CStaticUnknown : public IUnknown
{
public:
 // *** IUnknown ***
 IFACEMETHODIMP QueryInterface(
  _In_ REFIID riid, _Outptr_ void **ppv)
 {
  *ppv = nullptr;
  HRESULT hr = E_NOINTERFACE;
  if (riid == IID_IUnknown) {
   *ppv = static_cast<IUnknown *>(this);
   AddRef();
   hr = S_OK;
  }
  return hr;
 }

 IFACEMETHODIMP_(ULONG) AddRef()
 {
  return 2;
 }

 IFACEMETHODIMP_(ULONG) Release()
 {
  return 1;
 }

};

CStaticUnknown s_unkStatic;

This static implementation of IUnknown is one we'll use for the bind context strings whose mere presence indicates that a flag is set.

class CBindCtxBuilder
{
public:
 CBindCtxBuilder()
 {
  m_hrCumulative = CreateBindCtx(0, &m_spbc);
 }

 CBindCtxBuilder& SetMode(DWORD grfMode);
 CBindCtxBuilder& SetFindData(const WIN32_FIND_DATA *pfd);
 CBindCtxBuilder& SetFlag(PCWSTR pszName);
 CBindCtxBuilder& SetVariantDword(PCWSTR pszName, DWORD dwValue);
 CBindCtxBuilder& SetVariantString(PCWSTR pszName, PCWSTR pszValue);

 HRESULT Result() const { return m_hrCumulative; }

 IBindCtx *GetBindCtx() const
 { return SUCCEEDED(m_hrCumulative) ? m_spbc : nullptr; }
private:
 HRESULT EnsurePropertyBag();

private:
 CComPtr<IBindCtx> m_spbc;
 CComPtr<IPropertyBag> m_sppb;
 HRESULT m_hrCumulative;
};

The bind context builder class is a helper class that creates a bind context, and then fills it with stuff. For now, we let you set the following:

After you build up the bind context, you can check the Result() to see if it was built successfully, and use Get­Bind­Ctx to extract the result.

Here's the implementation. It's really not that exciting. We accumulate any error in m_hrCumulative, and once an error occurs, all future methods do nothing aside from preserving the error. To make the object fluent, the methods return a reference to themselves.

There is a special bind context method for setting the mode:

CBindCtxBuilder&
CBindCtxBuilder::SetMode(DWORD grfMode)
{
 if (SUCCEEDED(m_hrCumulative)) {
  BIND_OPTS bo = { sizeof(bo), 0, grfMode, 0 };
  m_hrCumulative = m_spbc->SetBindOptions(&bo);
 }
 return *this;
}

Find data is set as a direct object on the bind context, as we saw some time ago:

CBindCtxBuilder&
CBindCtxBuilder::SetFindData(const WIN32_FIND_DATA *pfd)
{
 if (SUCCEEDED(m_hrCumulative)) {
  m_hrCumulative = AddFileSysBindCtx(m_spbc, pfd);
 }
 return *this;
}

Flags are set by there mere presence, so we associate them with a dummy IUnknown that does nothing:

CBindCtxBuilder&
CBindCtxBuilder::SetFlag(PCWSTR pszName)
{
 if (SUCCEEDED(m_hrCumulative)) {
  m_hrCumulative = m_spbc->RegisterObjectParam(
    const_cast<PWSTR>(pszName), &s_unkStatic);
 }
 return *this;
}

If a property is set in the property bag, we need to proceed in two steps. First, we create the property bag if we don't have one already. Second, we put the value into the property bag:

CBindCtxBuilder&
CBindCtxBuilder::SetVariantDword(
    PCWSTR pszName, DWORD dwValue)
{
 if (SUCCEEDED(m_hrCumulative)) {
  m_hrCumulative = EnsurePropertyBag();
 }
 if (SUCCEEDED(m_hrCumulative)) {
  m_hrCumulative =  PSPropertyBag_WriteDWORD(
    m_sppb, pszName, dwValue);
 }
 return *this;
}

CBindCtxBuilder&
CBindCtxBuilder::SetVariantString(
    PCWSTR pszName, PCWSTR pszValue)
{
 if (SUCCEEDED(m_hrCumulative)) {
  m_hrCumulative = EnsurePropertyBag();
 }
 if (SUCCEEDED(m_hrCumulative)) {
  m_hrCumulative =  PSPropertyBag_WriteStr(
    m_sppb, pszName, pszValue);
 }
 return *this;
}

And finally, the helper function that creates a property bag if we don't have one already.

HRESULT CBindCtxBuilder::EnsurePropertyBag()
{
 HRESULT hr = S_OK;
 if (!m_sppb) {
  hr = PSCreateMemoryPropertyStore(
    IID_PPV_ARGS(&m_sppb));
  if (SUCCEEDED(hr)) {
   hr = m_spbc->RegisterObjectParam(
    STR_PROPERTYBAG_PARAM, m_sppb);
  }
 }
 return hr;
}

The idea here is that the class is used like this:

CBindCtxBuilder builder;
builder.SetMode(STGM_CREATE)
       .SetFindData(&wfd)
       .SetFlag(STR_FILE_SYS_BIND_DATA_WIN7_FORMAT)
       .SetFlag(STR_BIND_FOLDERS_READ_ONLY);
hr = builder.Result();
if (SUCCEEDED(hr)) {
 hr = psf->ParseDisplayName(hwnd, builder.GetBindCtx(),
   pszName, &cchEaten, &pidl, &dwAttributes);
}

You create the bind context builder, then use the various Set­Xxx methods to fill the bind context with goodies, and then you check if it all worked okay. If so, then you use Get­Bind­Ctx to get the resulting bind context and proceed on your way.

Comments (11)
  1. Joshua says:

    Well, I never thought I'd see AddRef like that!

  2. Rick C says:

    It's not the first time Raymond's done that (I don't think) and I believe I've seen it elsewhere once or maybe twice.  You can use that when you can be sure you control the lifetime of the object, IIRC.

    [It's been done many times. -Raymond]
  3. foo says:

    > Well, I never thought I'd see AddRef like that!

    I reckon it's called confidence.

  4. skSdnW says:

    I believe the static IUnknown trick is only safe in exe files and dlls without a DllCanUnloadNow export. The object/function you pass your interface to is allowed to hang on to it as long as they want which means your dll might be unloaded by the time they call Release. I see no reason why IShellFolder would cache the IBindCtx but that does not make it right. The same goes for CBindCtxBuilder on the stack.

    [The CBindCtxBuilder is not passed to ParseDisplayName. The bind context created by CreateBindCtx is passed, and that has its own reference count. as for the static object: It's no less safe than having a reference-counted dummy object, because the code for the dummy object... is in your DLL! -Raymond]
  5. skSdnW says:

    @Raymond. A reference-counted dummy object would Increment/Decrement the counter used by DllCanUnloadNow in its constructor/destructor so the your dll would not be unloaded if somebody decided to cache your dummy object...

    [Good point. The static object (if in an unloadable DLL) would still need a reference count. -Raymond]
  6. skSdnW says:

    I see you are using STR_FILE_SYS_BIND_DATA_WIN7_FORMAT so let me dare to ask, what is the difference between a XP, 7 and 8.1 STR_FILE_SYS_FIND_DATA? Is it only a difference in the internal pidl format?

    [I don't know. I just picked that because it illustrated one of the scenarios. -Raymond]
  7. Neil says:

    The static object could increment/decrement the counter used by DllCanUnloadNow in its Addref/Release methods.

  8. instead of the dummy unknown, for these plain 0-1 option strings I found that passing the bind context ITSELF in RegisterObjectParam does the trick. No need for separate dodgy objects, you can do dodgy IUnknown with IBindCtx itself :)

  9. PS. not such a good idea after all, that would create circular references to our IBindCtx. It is possible to "undo" these by calling RevokeObjectParam after use but the simplicity of the idea is gone...

  10. Anti pattern deluxe says:

    Is COM ugly or what? Be gone with it!

    [You seem to be confusing my showing how to do something with an endorsement of it. This happens a lot when I share a tip on batch file programming. (And of course I have yet to see anybody propose an alternative that is language-independent.) -Raymond]
  11. Anonymous Coward says:

    @Anti pattern deluxe

    Don't confuse "COM is ugly" with "Some COM objects are ugly".

Comments are closed.

Skip to main content