Extracting metadata from Windows Property System – Part II

In the first part of the article we have defined all the necessary interfaces and related API methods for extracting metadata from Windows Property System (WPS). In this part, we will put those definitions to work by extracting data in a simple WPF application.
Before we dive into more code and the code gets complicated with added layers (it almost always does), let me provide you with a simple overview of the process. We will first get a hold of the IShellItem2 interface of a shell item (possibly a file), then we will pass a property key to the interface to extract associated metadata. Simple, really!

Retrieving the IShellItem2 Interface

I am going to create a ‘Wrapper’ class that is responsible for retrieving and releasing the IShellItem2 interface. I will name my class ‘ShellItemWrapper’:

class ShellItemWrapper
{
public IShellItem2 ShellItem;
public ShellItemWrapper(string parsingName)
{
Guid riid = typeof(IShellItem2).GUID;
try
{
this.ShellItem =
NativeMethods.SHCreateItemFromParsingName(parsingName, IntPtr.Zero, riid);
}
catch (Exception e)
{
Console.WriteLine(e.Message +
"\nFailed to obtain shell interface for " + parsingName);
}
}
~ShellItemWrapper()
{
if (this.ShellItem != null)
Marshal.FinalReleaseComObject(this.ShellItem);
}
}
Copied to clipboard!

The Wrapper class will be responsible for obtaining and releasing the IShellItem2 interface for us. All we have to do is pass the parsing name of the shell item to the constructor. The parsing names generally correspond with file and folder paths (e.g. ‘c:\NewFolder\readme.txt’), but you can also pass CSIDLs (e.g. ‘::{5399E694-6CE5-4D6C-8FCE-1D8870FDCBA0}’ for Control Panel folder).

I am also going to add a Window to my WPF project and in the code behind file create a method named ‘GetShellDetails’ that takes parsing name as its only argument.

In my method first thing I need to do is retreive the IShellItem2 interface by creating an instance of my Wrapper class:

private void GetShellItemDetails(string parsingName)
{
ShellItemWrapper wrapper = new ShellItemWrapper(parsingName);
if (wrapper.ShellItem != null)
{

}
}

Metadata extraction

At this point you can simply pass a PROPERTYKEY struct to one of the ‘Get..’ methods of the IShellItem2 interface to extract metadata.

PROPERTYKEY struct consists of two fields. First is the format identifier (fmtid) which is a GUID that uniquely identifies a group of related properties. For example, FMTID_Storage which has the GUID of B725F130-47EF-101A-A5F1-02608C9EEBAC groups together a bunch of storage related metadata.

The second field is the property id (pid) which points to specific property within the format identifier group. For example PKEY_Size which has the value of 12 refers to the Size (in bytes) property of the shell item. As you might have noticed, Microsoft uses FMTID_ and PKEY_ prefixes for naming format and property identifiers (respectively). These values are listed in the propkey.h header file. We will follow these naming conventions here as well.

As mentioned earlier, we can simply construct a new PROPERTYKEY struct with proper FMTID and PID values to extract metadata as shown below.

PROPERTYKEY pKey = new PROPERTYKEY("B725F130-47EF-101A-A5F1-02608C9EEBAC", 12);
ulong fileSize = wrapper.ShellItem.GetUInt64(ref pKey);

However, as you start exploring the WPS, you will discover that not all shell items support all format identifiers. Hence, if the format identifier or the property id is not supported by a particular shell item, you will encounter a COM Exception 0x80070490 Element not found error. So we need to trap these errors in our code.

We will do that by extending our Wrapper class to avoid creating numerous Try-Catch blocks for every call to the interface. We will simply wrap the methods of IShellItems2 interface in our Wrapper class and hide the ShellItem member. Here is the slightly modified Wrapper class with an additional method for retrieving strings:

class ShellItemWrapper
{
private IShellItem2 ShellItem;
public bool HasShellItem = false;
public ShellItemWrapper(string parsingName)
{
Guid riid = typeof(IShellItem2).GUID;
try
{
this.ShellItem =
NativeMethods.SHCreateItemFromParsingName(parsingName, IntPtr.Zero, riid);
HasShellItem = true;
}
catch (Exception e)
{
Console.WriteLine(e.Message +
"\nFailed to obtain shell interface for " + parsingName);
}
}
~ShellItemWrapper()
{
if (this.ShellItem != null)
{
Marshal.FinalReleaseComObject(this.ShellItem);
#if DEBUG
Console.WriteLine("Disposing IShellItem2");
#endif
}
}

public ulong GetUInt64(PROPERTYKEY pKey)
{
ulong ret = ulong.MaxValue;
try
{
ret = this.ShellItem.GetUInt64(ref pKey);
}
catch (Exception e)
{
Console.WriteLine(e.Message +
"\nGetUInt64 failed for " + pKey.ToString());
}
return ret;
}
public string GetString(PROPERTYKEY pKey)
{
string ret = null;
try
{
ret = this.ShellItem.GetString(ref pKey);
}
catch (Exception e)
{
Console.WriteLine(e.Message +
"\nGetString failed for " + pKey.ToString());
}
return ret;
}
}


And this is the final version of our main window’s GetShellItemDetailsMethod:
private void GetShellItemDetails(string parsingName)

{
ShellItemWrapper wrapper = new ShellItemWrapper(parsingName);
if (wrapper.HasShellItem)
{
Guid FMTID_Storage = new Guid("B725F130-47EF-101A-A5F1-02608C9EEBAC");
// PKEY_Size
// gets file size
ulong fileSize = wrapper.GetUInt64(new PROPERTYKEY(FMTID_Storage, 12));
// PKEY_ItemTypeText
// gets item type text
string itemType = wrapper.GetString(new PROPERTYKEY(FMTID_Storage, 4));
}
}





Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: