Property Handlers

With the design decisions made as to which properties to support and which properties to display in the Microsoft Windows Explorer user interface (UI), you can now build a property handler that opens the file format to read and write properties.

Property handlers are a crucial component of the overall property system. They are invoked out-of-process by the indexer to read and index property values and are also invoked by Windows Explorer in-process to read and write property values directly in the files. These handlers need to be written with utmost attention and tested thoroughly to prevent degraded performance or the loss of data in the affected files. The property system provides techniques and tools to make handlers as robust as possible, and as a property handler author you should try to use as much of that functionality as possible.

In this topic, we draw from a sample XML-based file format that describes a recipe. However, the file has a file extension of .recipe rather than .xml. The .recipe file extension will be registered as its own distinct file format rather than relying on the more generic .xml file format, whose handler uses a secondary stream to store properties and as such is not appropriate for our use. It is best to register unique file extensions for your file types.

  • Initializing Property Handlers
  • In-Memory Property Store
  • Dealing with PROPVARIANT-Based Values
  • Supporting Open Metadata
  • Full-Text Contents
  • Providing Values for Properties
  • Writing Back Values
  • Implementing IPropertyStoreCapabilities
  • Registering Property Handlers
  • Property Handler Performance

Initializing Property Handlers

Before a property is used by the system, it is initialized by calling an implementation of IInitializeWithStream. The property handler should be initialized by having the system assign it a stream rather than leaving that assignment to the handler implementation. This method of initialization ensures several things.

  • The property handler can run in a restricted process—an important security feature—without access rights to directly read or write files, rather accessing their content through the stream.
  • The system can be trusted to handle the file oplocks World Wide Web link correctly—an important reliability measure.
  • The property system provides an automatic safe saving service without any extra functionality required from the property handler implementation. Read more about this benefit of streams in the Writing Back Values section.
  • Use of IInitializeWithStream abstracts your implementation from file system details. This allows the handler to support initialization through alternative storages such as an File Transfer Protocol (FTP) folder or a .zip file.

There are cases where initialization with streams is not possible. In those situations, there are two further interfaces that property handlers can implement: IInitializeWithFile and IInitializeWithItem. Note that if a property handler does not implement IInitializeWithStream it must opt out of running in the isolated process into which the system indexer would place it by default in the case of a change to the stream. To opt out of this feature, the following registry value is set.

HKEY_CLASSES_ROOT

CLSID

  • {66742402-F9B9-11D1-A202-0000F81FEDEE}

However, it is far better to implement IInitializeWithStream and do a stream-based initialization. Your property handler will be safer and more reliable as a result. Disabling process isolation is generally intended only for legacy property handlers and should be strenuously avoided by any new code.

We now examine the implementation of a property handler in detail. In this first code example, we show the implementation of IInitializeWithStream::Initialize. The handler is initialized by loading an XML-based recipe document through a pointer to that document's associated IStream instance. The _spDocEle variable used near the end of the code example is defined earlier in the sample as an MSXML2::IXMLDOMElementPtr.

Note  The following and all subsequent code examples are taken from the recipe handler sample included in the Windows Software Development Kit (SDK).

HRESULT CRecipePropertyStore::Initialize(IStream *pStream, DWORD grfMode)
{
    HRESULT hr = E_FAIL;
    
    try
    {
        if (!_spStream)
        {
            hr = _spDomDoc.CreateInstance(__uuidof(MSXML2::DOMDocument60));
            
            if (SUCCEEDED(hr))
            {
                if (VARIANT_TRUE == _spDomDoc->load(static_cast<IUnknown *>(pStream)))
                {
                    _spDocEle = _spDomDoc->documentElement;
                }

Once the document itself is loaded, we load the properties to display in Windows Explorer by calling the protected _LoadProperties method, which we examine in detail in the next section.


                if (_spDocEle)
                {
                    hr = _LoadProperties();
    
                    if (SUCCEEDED(hr))
                    {
                        _spStream = pStream;
                    }
                }
                else
                {
                    hr = E_FAIL;  // parse error
                }
            }
        }
        else
        {
            hr = E_UNEXPECTED;
        }
    }
    
    catch (_com_error &e)
    {
        hr = e.Error();
    }
    
    return hr;
}

If the stream is read-only but the grfMode parameter contains the STG_READWRITE flag, the initialization should fail with the STG_E_ACCESSDENIED return code. Without this check, Windows Explorer shows the property values as writable even though they are not, leading to a confusing end user experience.

The property handler is initialized only once in its lifetime. If a second initialization is requested, the handler should return HRESULT_FROM_WIN32(ERROR_ALREADY_INITIALIZED).

In-Memory Property Store

Before we look at the implementation of _LoadProperties, we should understand the PropertyMap array used in the sample to map properties in the XML document to existing properties in the property system through their PKEY values.

Notice that we do not expose every element and attribute in the XML file as a property. Instead, we select only those that we believe will be useful to end users in the organization of their documents (in this case, recipes). This is an important concept to keep in mind as you develop your property handlers: the difference between information that is truly useful for organizational scenarios and information that belongs to the details of your file and can be seen by opening the file itself. Properties are not intended to be a full duplication of an XML file.

PropertyMap c_rgPropertyMap[] =
{
{ L"Recipe/Title", PKEY_Title, 
                   VT_LPWSTR, 
                   NULL, 
                   PKEY_Null },
{ L"Recipe/Comments", PKEY_Comment, 
                      VT_LPWSTR, 
                      NULL, 
                      PKEY_Null },
{ L"Recipe/Background", PKEY_Author, 
                        VT_VECTOR | VT_LPWSTR, 
                        L"Author", 
                        PKEY_Null },
{ L"Recipe/RecipeKeywords", PKEY_Keywords, 
                            VT_VECTOR | VT_LPWSTR, 
                            L"Keyword", 
                            PKEY_KeywordCount },
};

Here is the full implementation of the _LoadProperties method called by IInitializeWithStream::Initialize.

HRESULT CRecipePropertyStore::_LoadProperties()
{
    HRESULT hr = E_FAIL;    
    
    if (_spCache)
    {
        hr = S_OK;
    }
    else
    {
        // Create the in-memory property store.
        hr = PSCreateMemoryPropertyStore(IID_PPV_ARGS(&_spCache));
    
        if (SUCCEEDED(hr))
        {
            // Cycle through each mapped property.
            for (UINT i = 0; i < ARRAYSIZE(c_rgPropertyMap); ++i)
            {
                _LoadProperty(c_rgPropertyMap[i]);
            }
    
            _LoadExtendedProperties();
            _LoadSearchContent();
        }
    }
    return hr;
}

The _LoadProperties method calls the Shell helper function PSCreateMemoryPropertyStore to create an in-memory property store (cache) for the handled properties. By using a cache, changes are tracked for you. This frees you from tracking whether a property value has been changed in the cache but not yet saved to persisted storage. It also frees you from needlessly persisting property values that have not changed.

The _LoadProperties method also calls _LoadProperty, whose implementation is shown here, once for each mapped property. _LoadProperty gets the value of the property as specified in the PropertyMap element in the XML stream and assigns it to the in-memory cache through a call to IPropertyStoreCache::SetValueAndState. The PSC_NORMAL flag in the call to IPropertyStoreCache::SetValueAndState indicates that the property value has not been altered since the time it entered the cache.

HRESULT CRecipePropertyStore::_LoadProperty(PropertyMap &map)
{
    HRESULT hr = S_FALSE;
    
    MSXML2::IXMLDOMNodePtr spXmlNode(_spDomDoc->selectSingleNode(map.pszXPath));
    if (spXmlNode)
    {
        PROPVARIANT propvar = { 0 };
        propvar.vt = map.vt;
        
        if (map.vt == (VT_VECTOR | VT_LPWSTR))
        {
            hr = _LoadVectorProperty(spXmlNode, &propvar, map);
        }
        else
        {
            // If there is no value, set to VT_EMPTY to indicate
            // that it is not there. Do not return failure.
            if (spXmlNode->text.length() == 0)
            {
                propvar.vt = VT_EMPTY;
                hr = S_OK;
            }
            else
            {
                // SimplePropVariantFromString is a helper function
                // particular to the sample. It is found in Util.cpp.
                hr = SimplePropVariantFromString(spXmlNode->text, &propvar);
            }
        }
    
        if (S_OK == hr)
        {
            hr = _spCache->SetValueAndState(map.key, &propvar, PSC_NORMAL);
            PropVariantClear(&propvar);
        }
    }
    return hr;
}

Dealing with PROPVARIANT-Based Values

In the implementation of _LoadProperty, a property values is provided in the form of a PROPVARIANT. A set of APIs in the software development kit (SDK) is provided to convert from primitive types such as PWSTR or int to or from PROPVARIANT types. These APIs are found in Propvarutil.h.

For example, to convert a PROPVARIANT to a string, you can use PropVariantToString as shown here.

PropVariantToString(REFPROPVARIANT propvar, PWSTR psz, UINT cch);

To initialize a PROPVARIANT from a string, you can use InitPropVariantFromString.

InitPropVariantFromString(PCWSTR psz, PROPVARIANT *ppropvar);

If you examine one of the recipe files included in the sample, you will notice that there can be more than one keyword in each file. To account for this, the property system supports multi-valued strings represented as a vector of strings (for instance "VT_VECTOR | VT_LPWSTR"). The _LoadVectorProperty method in our sample deals with the vector-based values.

HRESULT CRecipePropertyStore::_LoadVectorProperty(MSXML2::IXMLDOMNode *pNodeParent,
                                              PROPVARIANT *ppropvar,
                                              struct PropertyMap &map)
{
    HRESULT hr = S_FALSE;
    MSXML2::IXMLDOMNodeListPtr spList = pNodeParent->selectNodes(map.pszSubNodeName);
    
    if (spList)
    {
        UINT cElems = spList->length;
        ppropvar->calpwstr.cElems = cElems;
        ppropvar->calpwstr.pElems = (PWSTR*)CoTaskMemAlloc(sizeof(PWSTR)*cElems);
    
        if (ppropvar->calpwstr.pElems)
        {
            for (UINT i = 0; (SUCCEEDED(hr) && i < cElems); ++i)
            {
                hr = SHStrDup(spList->item[i]->text, 
                              &(ppropvar->calpwstr.pElems[i]));
            }
    
            if (SUCCEEDED(hr))
            {
                if (!IsEqualPropertyKey(map.keyCount, PKEY_Null))
                {
                    PROPVARIANT propvarCount = { VT_UI4 };
                    propvarCount.uintVal = cElems;
                    
                    _spCache->SetValueAndState(map.keyCount,
                                               &propvarCount, 
                                               PSC_NORMAL);
                }
            }
            else
            {
                PropVariantClear(ppropvar);
            }
        }
        else
        {
            hr = E_OUTOFMEMORY;
        }
    }
    
    return hr;
}

If a value does not exist in the file, do not return an error. Instead, set the value to VT_EMPTY and return S_OK. VT_EMPTY indicates that the property value does not exist.

Supporting Open Metadata

In our example, we use an XML-based file format. We can extend its schema to support properties undreamt of during development, a system known as open metadata. The strategy we use to accomplish this in this example is to create a node under the Recipe element called ExtendedProperties. Below is an example of the ExtendedProperties element in XML.

<ExtendedProperties>
    <Property 
        Name="{65A98875-3C80-40AB-ABBC-EFDAF77DBEE2}, 100"
        EncodedValue="HJKHJDHKJHK"/>
</ExtendedProperties>

To load persisted extended properties during initialization, we implement the _LoadExtendedProperties method.

HRESULT CRecipePropertyStore::_LoadExtendedProperties()
{
    HRESULT hr = S_FALSE;
    MSXML2::IXMLDOMNodeListPtr spList = 
                  _spDomDoc->selectNodes(L"Recipe/ExtendedProperties/Property");
    
    if (spList)
    {
        UINT cElems = spList->length;
        
        for (UINT i = 0; i < cElems; ++i)
        {
            MSXML2::IXMLDOMElementPtr spElement;
            
            if (SUCCEEDED(spList->item[i]->QueryInterface(IID_PPV_ARGS(&spElement))))
            {
                PROPERTYKEY key;
                _bstr_t bstrPropName = spElement->getAttribute(L"Name").bstrVal;
    
                if (!!bstrPropName &&
                    (SUCCEEDED(PropertyKeyFromString(bstrPropName, &key))))
                {
                    PROPVARIANT propvar = { 0 };
                    _bstr_t bstrEncodedValue = 
                               spElement->getAttribute(L"EncodedValue").bstrVal;
                   
                    if (!!bstrEncodedValue)
                    {
                        // DeserializePropVariantFromString is a helper function
                        // particular to the sample. It is found in Util.cpp.
                        hr = DeserializePropVariantFromString(bstrEncodedValue, 
                                                              &propvar);
                    }

We use serialization APIs declared in Propsys.h to serialize and deserialize PROPVARIANT types into blobs of data and then use Base64 encoding to serialize those blobs into strings that can be saved in the XML. This is stored in the EncodedValue attribute of the ExtendedProperties element. The following utility method implemented in the sample's Util.cpp file performs the serialization. It begins with a call to the StgSerializePropVariant function to perform the binary serialization.

HRESULT SerializePropVariantAsString(const PROPVARIANT *ppropvar, PWSTR *pszOut)
{
    SERIALIZEDPROPERTYVALUE *pBlob;
    ULONG cbBlob;
    HRESULT hr = StgSerializePropVariant(ppropvar, &pBlob, &cbBlob);

Next, the CryptBinaryToString API declared in Wincrypt.h performs the Base64 conversion.

    if (SUCCEEDED(hr))
    {
        hr = E_FAIL;
        DWORD cchString;
        
        if (CryptBinaryToString((BYTE *)pBlob, 
                                cbBlob,
                                CRYPT_STRING_BASE64, 
                                NULL, 
                                &cchString))
        {
            *pszOut = (PWSTR)CoTaskMemAlloc(sizeof(WCHAR) *cchString);
    
            if (*pszOut)
            {
                if (CryptBinaryToString((BYTE *)pBlob, 
                                         cbBlob,
                                         CRYPT_STRING_BASE64,
                                         *pszOut, 
                                         &cchString))
                {
                    hr = S_OK;
                }
                else
                {
                    CoTaskMemFree(*pszOut);
                }
            }
            else
            {
                hr = E_OUTOFMEMORY;
            }
        }
    }
    
    return S_OK;
}

The DeserializePropVariantFromString function, also found in Util.cpp, reverses the operation, deserializing values from the XML file.

Full-Text Contents

Property handlers can also facilitate a full-text search of the file contents, and is an easy way to provide that functionality if the file format is not overly complicated. There is an alternative, more powerful way to provide the full text of the file through IFilter. The table below summarizes the benefits of each method for use in determining whether you should add the additional support for IFilter to provide full-text content.

IFilter IPropertyStore
Allows write back to files? No Yes
Provides mix of content and properties? Yes Yes
Multilingual? Yes No
MIME/Embedded? Yes No
Text boundaries? Sentence, paragraph, chapter None
Implementation supported for SPS/SQL Server? Yes No
Implementation Complex Simple

In the recipe handler sample, the recipe file format does not have any complex requirements, so we implement only IPropertyStore for full-text support. We enable full-text search for the XML nodes named in this array.

const PWSTR c_rgszContentXPath[] = {
    L"Recipe/Ingredients/Item",
    L"Recipe/Directions/Step",
    L"Recipe/RecipeInfo/Yield",
    L"Recipe/RecipeKeywords/Keyword",
};

The property system contains the System.Search.Contents (PKEY_Search_Contents) property created for the purpose of providing full-text content to the indexer. This property's value is never displayed directly in the UI, so we simply concatenate the text from all of the XML nodes named in the array above into a single string. That string is then provided to the indexer as the full-text content of the recipe file through a call to IPropertyStoreCache::SetValueAndState.

HRESULT CRecipePropertyStore::_LoadSearchContent()
{
    HRESULT hr = S_FALSE;
    _bstr_t bstrContent;
    
    for (UINT i = 0; i < ARRAYSIZE(c_rgszContentXPath); ++i)
    {
        MSXML2::IXMLDOMNodeListPtr spList = 
                                  _spDomDoc->selectNodes(c_rgszContentXPath[i]);
    
        if (spList)
        {
            UINT cElems = spList->length;
            
            for (UINT elt = 0; elt < cElems; ++elt)
            {
                bstrContent += L" ";
                bstrContent += spList->item[elt]->text;
            }
        }
    }
    
    if (bstrContent.length() > 0)
    {
        PROPVARIANT propvar = { VT_LPWSTR };
        hr = SHStrDup(bstrContent, &(propvar.pwszVal));
    
        if (SUCCEEDED(hr))
        {
            hr = _spCache->SetValueAndState(PKEY_Search_Contents, 
                                            &propvar, 
                                            PSC_NORMAL);
            PropVariantClear(&propvar);
        }
    }
    
    return hr;
}

Providing Values for Properties

When used to read values, property handlers generally are invoked in two different ways.

  • To enumerate all property values.
  • To get the value of a specific property.

In the case of enumeration, a property handler is asked to enumerate its properties either during indexing or when the properties dialog asks for properties to display in the Other group. Indexing goes on constantly as a background operation. Whenever a file changes, the indexer is notified and it reindexes the file by asking the property handler to enumerate its properties. Therefore, it is critical that property handlers are implemented efficiently and return property values as quickly as possible. Enumerate all the properties for which you have values, just as you would for any collection, but do not enumerate properties that involve memory-intensive calculations or network requests that could make them slow to retrieve.

When writing a property handler, you usually need to consider two sets of properties.

  • Primary properties: Properties that your file type supports natively. For example, a photo property handler for Exchangeable Image File (EXIF) metadata natively supports System.Photo.FNumber.
  • Extended properties: Properties that your file type supports as part of open metadata.

Because the sample uses in-memory cache, implementing IPropertyStore methods is just a matter of delegating to that cache as shown here.

IFACEMETHODIMP GetCount(__out DWORD *pcProps)
{ return _spCache->GetCount(pcProps); }

IFACEMETHODIMP GetAt(DWORD iProp, __out PROPERTYKEY *pkey)
{ return _spCache->GetAt(iProp, pkey); }

IFACEMETHODIMP GetValue(REFPROPERTYKEY key, __out PROPVARIANT *pPropVar)
{ return _spCache->GetValue(key, pPropVar); }

If you should choose not to delegate to the in-memory cache, you must implement your methods with the following expected behavior in mind.

  • IPropertyStore::GetCount: If there are no properties, this method returns S_OK.
  • IPropertyStore::GetAt: If iProp is greater than or equal to cProps, this method returns E_INVALIDARG and the structure pointed to by the pkey parameter is filled with zeroes.
  • IPropertyStore::GetCount and IPropertyStore::GetAt reflect the current state of the property handler. If a PROPERTYKEY is added to or removed from the file through IPropertyStore::SetValue, these two methods must reflect that change the next time they are called.
  • IPropertyStore::GetValue: If this method is asked for a value that does not exist, it returns S_OK with the value reported as VT_EMPTY.

Writing Back Values

When the property handler writes the value of a property using IPropertyStore::SetValue, it does not write the value to the file until IPropertyStore::Commit is called. The in-memory cache can be useful in implementing this scheme. In our sample code the IPropertyStore::SetValue implementation simply sets the new value in the in-memory cache and sets the state of that property to PSC_DIRTY.

HRESULT CRecipePropertyStore::SetValue(REFPROPERTYKEY key, const PROPVARIANT *pPropVar)
{
    HRESULT hr = E_FAIL;
    
    if (IsEqualPropertyKey(key, PKEY_Search_Contents))
    {
        // This property is read-only
        hr = HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED);  
    }
    else
    {
        hr = _spCache->SetValueAndState(key, pPropVar, PSC_DIRTY);
    }
    
    return hr;
}

In any IPropertyStore implementation, the following behavior is expected from IPropertyStore::SetValue.

  • If the property already exists, the value of the property is set.
  • If the property does not exist, the new property is added and its value set.
  • If the property value cannot be persisted in the same fidelity as given—for instance, truncation due to size limitations in the file format—the value is set as far as possible and INPLACE_S_TRUNCATED is returned.
  • If the property is not supported by the property handler, HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED) is returned.
  • If there is another reason that the property value cannot be set such as the file being locked or lack of rights to edit through access control lists (ACLs), then STG_E_ACCESSDENIED is returned.

One major advantage of using streams, as we do in the sample, comes in the area of reliability. Property handlers must always consider that they cannot leave a file in an inconsistent state in the case of a catastrophic failure. Corrupting a user's files obviously should be avoided, and the best way to do this is with a "copy-on-write" mechanism. If your property handler uses a stream to access a file, then you get this behavior automatically; the system writes any changes to the stream, atomically replacing the file with the new copy only during the commit operation.

To override this behavior and control the file saving process manually, you can opt out of the safe save behavior by setting the ManualSafeSave value in your handler's registry entry as shown here.

HKEY_CLASSES_ROOT

CLSID

  • {66742402-F9B9-11D1-A202-0000F81FEDEE}

When a handler is assigned the ManualSafeSave value, the stream with which it is initialized is not a copy-on-write stream. The handler itself must implement the copy-on-write behavior to ensure that the file cannot be corrupted. To do this, the handler queries the stream as to whether it supports IDestinationStreamFactory. If so, the handler calls IDestinationStreamFactory::GetDestinationStream to get a temporary stream to which to write any alterations to the file. When you are ready to write back to the original file, the temporary stream is closed to release any locks and then IPropertyStore::Commit is called on the original stream during initialization. This atomically replaces the contents of the original stream with the contents of the temporary stream. The original stream then safely writes all changes back to the source file.

ManualSafeSave is also the default situation if you do not initialize your handler with a stream. Without an original stream to receive the contents of the temporary stream, you must use ReplaceFile to perform an atomic replacement of the source file.

In cases where the file is potentially very large, such as a video file larger than 1 gigabyte (GB), copy-on-write can be inefficient because it requires the rewriting of the entire file for each property change. If the file has enough space in its header to write metadata and if the writing of that metadata does not cause the file to grow or shrink, then it may be safe to write in-place. This is equivalent to the handler asking for ManualSafeSave and calling IStream::Commit in the implementation of IPropertyStore::Commit, and has much better performance than copy-on-write. If the file size changes due to property value changes, writing in-place should not be done because of the corrupt file it would leave in the case of an abnormal termination.

As shown below in the sample implementation of IPropertyStore::Commit, we register our handler for ManualSafeSave to illustrate the manual safe save option. Normally, however, it is acceptable to rely on the automatic copy-on-write behavior which requires less code in the property handler and results in a potentially more reliable implementation.

The _SaveProperties method writes the property values stored in the in-memory cache to the XMLdocument object. It is discussed in more detail later in this topic.

HRESULT CRecipePropertyStore::Commit()
{
    HRESULT hr = E_FAIL;
    
    try
    {
        hr = _SaveProperties();
    
        if (SUCCEEDED(hr))
        {
            hr = _spStream->Seek(c_li0, STREAM_SEEK_SET, NULL);

Next we ask whether the stream we were given supports IDestinationStreamFactory.

            if (SUCCEEDED(hr))
            {
                IDestinationStreamFactoryPtr spSafeCommit;
                
                hr = _spStream.QueryInterface(IID_PPV_ARGS(&spSafeCommit));

If IDestinationStreamFactory is supported we ask for the temporary stream.

               if (SUCCEEDED(hr))
                {
                    IStreamPtr spStreamCommit;
                    
                    hr = spSafeCommit->GetDestinationStream(&spStreamCommit);
                    if (SUCCEEDED(hr))
                    {
                        _variant_t varStream(static_cast<IUnknown *>(spStreamCommit));

We save the XML to the temporary stream through the XMLDocument object then commit that stream.

                        _spDomDoc->save(&varStream);

                        hr = spStreamCommit->Commit(STGC_DEFAULT);

Now we commit the original stream, which writes the data back to the original file in a safe manner.

                        if (SUCCEEDED(hr))
                        }
                            _spStream->Commit(STGC_DEFAULT);
                        }
                    }
                }
            }
        }
    }
    
    catch (_com_error &e)
    {
        hr = e.Error();
    }
    return hr;
}

Next we will examine the _SaveProperties implementation.

HRESULT CRecipePropertyStore::_SaveProperties()
{
    HRESULT hr = S_OK;
    
    DWORD cProps;

First, we get the number of properties stored in the in-memory cache.

    _spCache->GetCount(&cProps);

Next we iterate through the properties to determine whether the value of a property has been changed since it was loaded into memory.

    for (UINT i = 0; i < cProps; ++i)
    {
        PROPERTYKEY key;
        
        _spCache->GetAt(i, &key);

The IPropertyStoreCache::GetValueAndState method tells us the state of the property in the cache. The PSC_DIRTY flag, which was set in the IPropertyStore::SetValue implementation, marks a property as changed.

        PSC_STATE psc;
    
        hr = _spCache->GetValueAndState(key, NULL, &psc);
        if (SUCCEEDED(hr) && psc == PSC_DIRTY)
        {
            PROPVARIANT propvar = { 0 };

            hr = _spCache->GetValue(key, &propvar);
            if (SUCCEEDED(hr))
            {
                try
                {
                    hr = E_FAIL;

We map the property to the XML node as specified in the c_rgPropertyMap array.

                    for (UINT i = 0; i < ARRAYSIZE(c_rgPropertyMap); ++i)
                    {
                        if (IsEqualPropertyKey(key, c_rgPropertyMap[i].key))
                        {
                            hr = _SaveProperty(&propvar, c_rgPropertyMap[i]);
                        }
                    }

If a property is not in our map, then it is a new property that was set by the Windows Explorer. We support open metadata and so save the new property in the ExtendedProperties section of the XML.

                    if (FAILED(hr))                    
                    {
                        hr = _SaveExtendedProperty(key, &propvar);
                    }
                }
                catch (...)
                {
                    PropVariantClear(&propvar);
                    throw;
                }
            }
        }
    }
    
    return hr;
}

Implementing IPropertyStoreCapabilities

IPropertyStoreCapabilities informs the Shell UI whether a given property can be edited in the Shell UI. It is important to note that this strictly relates to the ability to edit the property in the UI, not whether you can successfully call IPropertyStore::SetValue on the property. A property that provokes a return value of S_FALSE from IPropertyStoreCapabilities::IsPropertyWritable might still be possible to set through an application.

interface IPropertyStoreCapabilities : IUnknown
{
    HRESULT IsPropertyWritable([in] REFPROPERTYKEY key);
}

IsPropertyWritable returns S_OK to indicate that end users should be allowed to directly edit the property; S_FALSE indicates that they should not. S_FALSE can mean that applications are responsible for writing the property, not users. The Shell disables editing controls as appropriate based on the results of calls to this method. A handler that does not implement IPropertyStoreCapabilities is assumed to support open metadata through support for the writing of any property.

If you are building a handler that handles strictly read-only properties, then you should implement your Initialize method (IInitializeWithStream, IInitializeWithItem, or IInitializeWithFile) such that it returns STG_E_ACCESSDENIED when called with the STG_READWRITE flag.

Some properties have their isInnate attribute set to true. Innate properties have the following characteristics.

  • The property is usually calculated in some way. For instance, System.Image.BitDepth is calculated from the image itself.
  • Changing the property would not make sense without changing the file. For instance, changing System.Image.Dimensions would not resize the image, so it does not make sense to allow the user to change it.
  • In some cases, these properties are provided automatically by the system. Examples include System.DateModified, which is provided by the file system, and System.SharedWith, which is based on who the user is sharing the file with.

Due to these characteristics, properties marked as IsInnate are provided to the user in the Shell UI only as read-only properties.

If a property is marked as IsInnate, the property system does not store that property in the property handler. Therefore, property handlers do not need special code to account for these properties in their implementations.

If the value of the IsInnate attribute is not explicitly stated for a given property, the default value is false.

Registering Property Handlers

With the property handler implemented, it must be registered and its file extension associated with the handler. The following example using the .recipe extension shows the registry keys and values required to do this.

HKEY_CLASSES_ROOT

CLSID

{50d9450f-2a80-4f08-93b9-2eb526477d1a}

(Default) = Recipe Property Handler
ManualSafeSave [REG_DWORD] = 00000001

  • InProcServer32

HKEY_LOCAL_MACHINE

SOFTWARE

Microsoft

Windows

CurrentVersion

PropertySystem

PropertyHandlers

  • .recipe

Shell Extensions

  • Approved

Property Handler Performance

Property handlers are invoked for each file on a particular machine. They are invoked mainly under two circumstances.

  • During indexing of the file. This is done out-of-process, in an isolated process with restricted rights.
  • When files are accessed in the Windows Explorer for the purpose of reading and writing property values. This is done in-process.

It is possible that an end user could have literally tens of thousands of files of any specific type—including yours— on their machines. A user could attempt to set a keyword on all those files at once. They could copy those files from another location, during which those files would be indexed. Therefore, certain considerations should be kept in mind.

  • Property handlers should have a very fast response time in enumerating their properties. Memory-intensive calculations of property values, network lookups, or the search for resources other than the file itself should be avoided to ensure fast response times.

  • If possible, when dealing with large files the file format should be arranged in such a way that reading or writing property values does not require reading the whole file from disk. Even if the file needs to be sought, it should not be read in its entirety into memory since that bloats the working set of the indexer or Windows Explorer as they try to index or access these files.

    One useful technique to accomplish this is to pad the header of the files with extra space so that the next time a property value needs to be written, the value can be written in place rather than fully replacing of the file. This requires the ManualSafeSave functionality and needs to be tested extensively to ensure that files are not corrupted in the case of a failure during a save operation.