Initializing Property Handlers

This topic explains how to create and register property handlers to work with the Windows property system.

This topic is organized as follows:

Property Handlers

Property handlers are a crucial part of the property system. They are invoked in-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 carefully written and tested to prevent degraded performance or the loss of data in the affected files. For more information on indexer-specific considerations that affect property handler implementation, see Developing Property Handlers for Windows Search.

This topic discusses a sample XML-based file format that describes a recipe with a .recipe file name extension. The .recipe file name extension is 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. We recommend that you register unique file name extensions for your file types.

Before You Begin

Property handlers are COM objects that create the IPropertyStore abstraction for a specific file format. They read (parse) and write this file format in a manner that conforms to its specification. Some property handlers do their work based on APIs that abstract access to a specific file format. Before you develop a property handler for your file format, you need to understand how your file format stores properties, and how those properties (names and values) are mapped into the property store abstraction.

When planning your implementation, remember that property handlers are low-level components that are loaded in the context of processes like Windows Explorer, the Windows Search indexer, and third-party applications that use the Shell item programming model. As a result, property handlers cannot be implemented in managed code, and should be implemented in C++. If your handler uses any APIs or services to do its work, you must ensure that those services can function properly in the environment(s) in which your property handler is loaded.

Note

Property handlers are always associated with specific file types; thus, if your file format contains properties that require a custom property handler, you should always register a unique file name extension for each file format.

 

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 the following things:

  • The property handler can run in a restricted process (an important security feature) without having 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 correctly, which is an important reliability measure.
  • The property system provides an automatic safe saving service without any extra functionality required from the property handler implementation. See the Writing Back Values section for more information about streams.
  • Use of the IInitializeWithStream abstracts your implementation from file system details. This enables the handler to support initialization through alternative storages such as a File Transfer Protocol (FTP) folder or a compressed file with a .zip file name extension.

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. If a property handler does not implement the IInitializeWithStream, it must opt out of running in the isolated process into which the system indexer would place it by default if there were a change to the stream. To opt out of this feature, set the following registry value.

HKEY_CLASSES_ROOT
   CLSID
      {66742402-F9B9-11D1-A202-0000F81FEDEE}
         DisableProcessIsolation = 1

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.

To examine the implementation of a property handler in detail, look at the following code example, which is an 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;
                }

 

After the document itself is loaded, the properties to be displayed in Windows Explorer are loaded by calling the protected _LoadProperties method, as illustrated in the following code example. This process is examined 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 STGM_READWRITE flag, the initialization should fail and return STG_E_ACCESSDENIED. 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 looking at the implementation of _LoadProperties, you 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.

You should not expose every element and attribute in the XML file as a property. Instead, select only those that you 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 = <mark type="const">S_OK</mark>;
    }
    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 illustrated in the following code) 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 = <mark type="const">S_OK</mark>;
            }
            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 value 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 illustrated here.

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

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

InitPropVariantFromString(PCWSTR psz, PROPVARIANT *ppropvar);

As you can see in any of the recipe files included in the sample, 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 the example uses 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

This example uses an XML-based file format. Its schema can be extended to support properties that were not thought of during developmet, for example. This system is known as open metadata. This example extends the property system by creating a node under the Recipe element called ExtendedProperties, as illustrated in the following code example.

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

To load persisted extended properties during initialization, implement the _LoadExtendedProperties method, as illustrated in the following code example.

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);
                    }

Serialization APIs declared in Propsys.h are used to serialize and deserialize PROPVARIANT types into blobs of data, and then Base64 encoding is used to serialize those blobs into strings that can be saved in the XML. These strings are 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, as illustrated in the following code example.

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

Next, the CryptBinaryToString function, 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 = <mark type="const">S_OK</mark>;
                }
                else
                {
                    CoTaskMemFree(*pszOut);
                }
            }
            else
            {
                hr = E_OUTOFMEMORY;
            }
        }
    }
    
    return <mark type="const">S_OK</mark>;}

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

For information about support for open metadata, see "File Types that Support Open Metadata" in File Types.

Full-Text Contents

Property handlers can also facilitate a full-text search of the file contents, and they are 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 the IFilter interface implementation.

The following table summarizes the benefits of each approach using either IFilter or IPropertyStore.

Capability 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 only IPropertyStore has been implemented for full-text support. Full-text search is implemented for the XML nodes named in the following 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, which was created to provide full-text content to the indexer. This property's value is never displayed directly in the UI; the text from all of the XML nodes named in the array above are concatenated 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 as illustrated in the following code example.

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 are usually invoked for one of the following reasons:

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

For enumeration, a property handler is asked to enumerate its properties either during indexing or when the properties dialog box 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 re-indexes 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 the following 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 illustrated in the following code example.

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 choose not to delegate to the in-memory cache, you must implement your methods to provide> the following expected behavior:

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 the 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 at the same accuracy 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 the sample, is 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, you get this behavior automatically; the system writes any changes to the stream, 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 illustrated here.

HKEY_CLASSES_ROOT
   CLSID
      {66742402-F9B9-11D1-A202-0000F81FEDEE}
         ManualSafeSave = 1

When a handler specifies the ManualSafeSave value, the stream with which it is initialized is not a transacted stream (STGM_TRANSACTED). The handler itself must implement the safe save function to ensure that the file is not corrupted if the save operation is interrupted. If the handler implements in-place writing, it will write to the stream that it is given. If the handler does not support this feature, then it must retrieve a stream with which to write the updated copy of the file using IDestinationStreamFactory::GetDestinationStream. After the handler is done writing, it should call IPropertyStore::Commit on the original stream to complete the operation, and replace the contents of the original stream with the new copy of the 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.

Large file formats that will be used in a way that produces files greater than 1 MB should implement support for in-place property writing; otherwise, the performance behavior does not meet the expectations of clients of the property system. In this scenario, the time required to write properties should not be affected by the file size.

For very large files, for example a video file of 1 GB or more, a different solution is required. If there is not enough space in the file to perform in-place writing, the handler may fail the property update if the amount of space reserved for in-place property writing has been exhausted. This failure occurs to avoid poor performance arising from 2 GB of IO (1 to read, 1 to write). Because of this potential failure, these file formats should reserve enough space for in-place property writing.

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, it might be safe to write in-place. We recommend 64 KB as a starting point. Writing in-place 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 attempted because of the potential for a corrupted file in the event of an abnormal termination.

Note

For performance reasons we recommend that the ManualSafeSave option be used with property handlers working with files that are 100 KB or larger.

 

As illustrated in the following sample implementation of IPropertyStore::Commit, the handler for ManualSafeSave is registered to illustrate the manual safe save option. The _SaveCacheToDom method writes the property values stored in the in-memory cache to the XMLdocument object.

HRESULT CRecipePropertyStore::Commit()
{
    HRESULT hr = E_UNEXPECTED;
    if (_pCache)
    {
        // Check grfMode to ensure writes are allowed.
        hr = STG_E_ACCESSDENIED;
        if (_grfMode & STGM_READWRITE)
        {
            // Save the internal value cache to XML DOM object.
            hr = _SaveCacheToDom();
            if (SUCCEEDED(hr))
            {
                // Reset the output stream.
                LARGE_INTEGER liZero = {};
                hr = _pStream->Seek(liZero, STREAM_SEEK_SET, NULL);

Next, ask whether the pecified supports IDestinationStreamFactory.

                        if (SUCCEEDED(hr))
                        {
                            // Write the XML out to the temprorary stream and commit it.
                            VARIANT varStream = {};
                            varStream.vt = VT_UNKNOWN;
                            varStream.punkVal = pStreamCommit;
                            hr = _pDomDoc->save(varStream);
                            if (SUCCEEDED(hr))
                            {
                                hr = pStreamCommit->Commit(STGC_DEFAULT);_

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

                        if (SUCCEEDED(hr))
                                {
                                    // Commit the real output stream.
                                    _pStream->Commit(STGC_DEFAULT);
                                }
                            }

                            pStreamCommit->Release();
                        }

                        pSafeCommit->Release();
                    }
                }
            }
        }
    }

Then examine the _SaveCacheToDom implementation.

// Saves the values in the internal cache back to the internal DOM object.
HRESULT CRecipePropertyStore::_SaveCacheToDom()
{
    // Iterate over each property in the internal value cache.
    DWORD cProps;  

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

HRESULT hr = _pCache->GetCount(&cProps);          
            

Now 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; SUCCEEDED(hr) && (i < cProps); ++i)
    {
        PROPERTYKEY key;
        hr = _pCache->GetAt(i, &key); 

The IPropertyStoreCache::GetState method gets 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.

        if (SUCCEEDED(hr))
        {
            // check the cache state; only save dirty properties
            PSC_STATE psc;
            hr = _pCache->GetState(key, &psc);
            if (SUCCEEDED(hr) && psc == PSC_DIRTY)
            {
                // get the cached value
                PROPVARIANT propvar = {};
                hr = _pCache->GetValue(key, &propvar); 

Map the property to the XML node as specified in the eg_rgPropertyMap array.

if (SUCCEEDED(hr))
                {
                    // save as a native property if the key is in the property map
                    BOOL fIsNativeProperty = FALSE;
                    for (UINT i = 0; i < ARRAYSIZE(g_rgPROPERTYMAP); ++i)
                    {
                        if (IsEqualPropertyKey(key, *g_rgPROPERTYMAP[i].pkey))
                        {
                            fIsNativeProperty = TRUE;
                            hr = _SaveProperty(propvar, g_rgPROPERTYMAP[i]);
                            break;
                        }     

If a property is not in the map, it is a new property that was set by Windows Explorer. Because open metadata is supported, save the new property in the ExtendedProperties section of the XML.

                    
                    // Otherwise, save as an extended property.
                    if (!fIsNativeProperty)
                    {
                        hr = _SaveExtendedProperty(key, propvar);
                    }

                    PropVariantClear(&propvar);
                }
            }
        }
    }

    return hr;    

Implementing IPropertyStoreCapabilities

IPropertyStoreCapabilities informs the Shell UI whether a particular property can be edited in the Shell UI. It is important to note that this relates only 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 capable of being 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 edit the property directly; 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 only read-only properties, then you should implement your Initialize method (IInitializeWithStream, IInitializeWithItem, or IInitializeWithFile) so that it returns STG_E_ACCESSDENIED when called with the STGM_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 particular property, the default value is false.

Registering and Distributing Property Handlers

With the property handler implemented, it must be registered and its file name extension associated with the handler. For more information, see Registering and Distributing Property Handlers.

Understanding Property Handlers

Using Kind Names

Using Property Lists

Registering and Distributing Property Handlers

Property Handler Best Practices and FAQ