Click here to Skip to main content
15,884,298 members
Articles / Programming Languages / Shell

The Windows Property System

Rate me:
Please Sign up or sign in to vote.
4.99/5 (17 votes)
5 Jan 2017CPOL17 min read 28.3K   2K   23   10
Accessing the Windows Property System in the Shell

Properties

Introduction

The Windows Property System[^] provides common interfaces for accessing metadata referencing various areas of the Windows operating system. These areas may be devices, windows, the file system, and more. This article targets the properties available in the Windows Shell, but the information here could quite easily be reused for any other part of the system. The most recognisable part of the system is probably when you select Properties in the context menu for a file - the metadata that is displayed in those pages are all part of the Windows Property System. The beauty of this system is it allows you to edit metadata in a multitude of file formats without having to know that format. For example, there are properties that read/write from the ID3 tags in audio and video files, or EXIF data in photographs, or authoring data in documents, be they open XML or proprietary formats.

The code in this class library encapsulates the internal interfaces and functions, rather than exposing them directly. I did it this way for two reasons; firstly the declaration of the interfaces and functions are not very friendly for a .NET application, and the second reason is that during the course of researching and writing this, I did find a number of bugs in the system, which I have managed to code around.

Background

I am currently working on another class library that involves saving files, and I wanted to be able to gather and set appropriate metadata about the file, and write it as the file was being saved. The Properties window is only available after the file has been written, and I felt that was like putting major data collection process into the clean-up. I felt it much better for the code to be able to prompt the user either before or during the save process, and perhaps even set some of that metadata itself, without the need for user interaction.

Behind the Scenes

There are two basic objects used for the identification and valuing of a property, and then three main objects that provide access to the system to retrieve and/or set the properties. The names of these mirror the interfaces and structures exposed by the Windows Property System, but are tidied up for ease of use in the .NET environment. I will go through these objects one at a time.

One thing I have done in this library is to take on board a good deal of the marshalling of structures and interfaces. Due to the complexity of the marshalling (and perhaps bugs in the system - I don't know), I found that the automatic marshalling process didn't quite get it right all of the time, and was inconsistent in the way it expected the marshalling to occur. For instance, when passing a reference to a Guid, some methods insisted on:

C#
[MarshalAs(UnmanagedType.Struct)] ref Guid riid

while others liked:

C#
[MarshalAs(UnmanagedType.LPStruct)] Guid riid

and I found often if you used the wrong marshaller, then you would either get a critical Memory Access Exception, or some really strange result (one of the calls was even changing the value of the Guid). So, I created my own GUID object, marshalling the value into unmanaged memory myself - and it seems to have sorted the issues. It is a similar story with the PROPERTYKEY and PROPVARIANT structures, the latter of which is very similar to a normal VARIANT, but is dissimilar enough that if it gets marshalled to an object, an exception can be thrown. As a result, most of the time when you look at a function or method declaration in the code, where a PROPVARIANT, REFIID, PROPERTYKEY or Interface object is required, either [In] or [Out], you will see an IntPtr being passed instead. I allocate the memory for the IntPtr when required using Marshal.CoTaskMemAlloc, and tend to free it either immediately if it's not likely to be reused very often, or during the object's Dispose method in the case of the PropVariant and PropertyKey objects that are passed around like a fifth of Jack at a frat party. This also makes things a lot easier for those times when you need to pass a NULL reference to a method or function.

Where I encapsulate the COM interface pointers, I ensure that the pointers are maintained wholly within the enclosing class, I get the pointers to these interfaces from the relevant API calls as an IntPtr pointing to an IUnknown interface. I then use the .NET Marshal class to convert that pointer to a unique object referencing the interface I require. Finally, I release the original IUnknown in the IntPtr. Using this pattern allows me to safely call Marshal.FinalReleaseComObject on the interface reference during the class's Dispose method, ensuring I release all references to the COM object.

One interesting thing I came across - when I do the release on the pUnk pointer, checking the return always has a value. I am hoping that it's just the interface pointer adding references to it for each interface it implements - otherwise, I suspect there may be some sort of leakage, With any luck, the FinalRealeaseComObject call gets the lot of them.

1You may notice that one interface I did not include in this library is the IPropertySystem. That is because it is broken. Calling any of the methods defined by this interface invariably generates a critical Memory Access Exception. Even creating the COM CoClass, and calling the methods on that, generates the exception. I think that somehow the Vtable for the COM class has got its knickers in a knot, and the API calls that indirectly access this object are only working because they can see the full native class and associated Vtable. Anyway, there are enough of these API functions available to allow the class library to operate unimpeded.

If any memory is allocated by the Property System, the CoTaskMemory allocation routines are used (as opposed to the Global Heap). This class library manages all of that, freeing this memory using Marshal.FreeCoTaskMem as required. A user of this library does not have to worry about this, but if anyone ever needs to modify these classes in any way, this bit of information should be kept in mind. In order to keep things consistent, if the library has had to allocate unmanaged memory, it uses the same allocation method.

Using the Code

To view and/or modify Windows Properties for a particular file type, there are two steps to perform. First is to identify the properties that are applicable to that particular file type, and the second is to retrieve and/or set those properties for the individual file. The first part is handled with a PropertyDescriptionList, which, as the name implies, is a list of the PropertyDescriptions appropriate for the circumstances. If you already know the properties you are looking at for the file, you can create your own PropertyDescriptionList by passing a string made up of these properties through to the constructor. The format of this string is "prop:<property1>;<property2>;....;<propertyN>" where <property1> to <propertyN> are the canonical names of the properties (such as System.Title).

If, however, you need to discover what properties are available for a particular file type, you are able to construct a PropertyDescriptionList either from an existing file of the correct type or from just the file extension. When using the constructor PropertyDescriptionList(string path, PropertyKey propListKey), if you use an existing file, it will generate the PropertyDescriptionList from that file's handler. If the file does not exist, behind the scenes, a "Fake" file will be created with the supplied extension. It will then use the handler for that fake item to load the PropertyDescriptionList. See my previous article on When a File is not a File[^] for details about creating a fake file. The second parameter for this constructor is the PropertyKey for one of the System.PropList[^] properties. This key indicates the eventual usage of the property list that will be returned. The most common of these would be the System.PropList.FullDetails list, which gets the properties shown in the Details tab of the Windows Properties Dialog. However, if you want to view/edit the properties that will be used for Windows Search, you might select the System.PropList.ContentViewModeForSearch, or the System.PropList.InfoTip key if you are playing around with the file's InfoTip.

Now is probably a good time to mention that there is a static IReadOnlyDictionary property included in the PropertyKey class, called Keys. This dictionary is loaded with every PropertyKey registered on the system (on my Windows 10 desktop, there are 1531 of them), indexed by its canonical name. So instead of creating a new PropertyKey each time you want a specific one, you can just reference it from this dictionary. For example, PropertyKey.Keys["System.PropList.FullDetails"] returns the PropertyKey object for the canonical name System.PropList.FullDetails. I'm actually in the habit of entering a using static WinProps.PropertyKey; statement at the top of the module, and then simply referring to these items as Keys[name].

Once you have a PropertyDescriptionList with the properties you require, it's time to determine which of these properties you want to read and/or set. You do this by creating a PropertyStore object, passing the file you want the store to belong to. You may also include the keys of specific properties you are interested in. If you do not include the keys, the system will attempt to retrieve all of the properties associated with that file that match the criteria specified in the PropertyStore.GetFlags. Note that if the PropertyStore is unable to retrieve all the properties that match that flag, then the constructor will fail and the PropertyStore will not be created.

Ok - a quick example of putting all this together. We will take the case of a complaints department at a local store. When a complaint is received, it is investigated, and a document is generated informing the complainant of the results of that investigation. When performing an investigation, it would be handy to know if that item has had complaints against it previously, and what were the results of the investigation. To assist with this, we will set a number of properties that can be displayed in the columns of window Explorer. We will also add a number of System.Keyword properties allowing Windows Search to quickly locate relevant documents.

C#
class Complaint {
   string Item { get; set; }
   string Employee { get; set; }
   string Customer { get; set; }
   string Resolution { get; set; }
      //...
   string GenerateDocument() {
      //...
      Save();
      return fileName;
   }
   void SetProperties(string fileName) {
      //...
   }

Consider the above class will be doing the work. The employee fills in the report and clicks Process. The letter is generated and saved, and then SetProperties is called to attach the metadata to the document. First, we get a PropertyDescriptionList for the properties we are interested in.

C#
private static PropertyKey[] _complaintProperties = {
   PropertyKey.Keys["System.Author"],
   PropertyKey.Keys["System.Title"],
   PropertyKey.Keys["System.Subject"],
   PropertyKey.Keys["System.Comments"],
   PropertyKey.Keys["System.Keywords"]
}
void SetProperties(string fileName) {
   PropertyDescriptionList properties = new PropertyDescriptionList(_complaintProperties);

The PropertyDescriptions in this PropertyDescriptionList define the type and structure of the PropertyValues that will be assigned to the file. For this particular scenario, this step is possibly not required. However, it is recommended to do to allow for possible expansion.

Next is to get the PropertyStore for the newly created file. As we know which properties are of interest, we will only ask the handler for those particular properties. Also, as we are going to be updating, we need to pass the .PropertyStore.GetFlags.ReadWrite flag to the constructor.

C#
PropertyStore store = new PropertyStore
(fileName, _complaintProperties, PropertyStore.GetFlags.ReadWrite);

Now, for each value we are passing through, we need to create a PropVariant to store that value. Note that if a property is defined as multi-value (a field in the PropertyDescription), then the value needs to be a vector, even if there is only one value being inserted. If the PropVariant is not of the correct type, the system will try and coerce the value, but relying on that is really unwise. The authors seem to have got it right for the few items they tested, but not for everything. Your best chance is to set the PropVariant up correctly in the first place.

C#
PropVariant vAuthor = properties[_complaintProperties[0]].TypeFlags.IsMultiValued ?
PropVariant.FromStringAsVector(Employee) : new PropVariant(Employee);
PropVariant vTitle = new PropVariant(string.Format("{0} Complaint", Item));
PropVariant vSubject = new PropVariant(string.Format
("Resolved complaint from {0} re {1}", Customer, Item));
PropVariant vComments = new PropVariant(Resolution);
PropVariant vKeyWords = PropVariant.FromStringAsVector
(string.Format({0};Complaint;Resolved;{1};{2}", Item, Customer, Resolution));

Finally, we save these properties to the file. One important point is that the IsEditable flag is checked before setting each value. This is important, as each property handler decides which properties can or cannot be updated. Third party applications may install property handlers for file types that differ from the ones provided by the operating system, so don't assume you know that a property will be editable. If you attempt to write to a property that the handler thinks it should be setting internally, then you will get an Access Denied exception.

C#
if (store.IsEditable(_complaintProperties[0])
   store.SetValue(_complaintProperties[0], vAuthor);
if (store.IsEditable(_complaintProperties[1])
   store.SetValue(_complaintProperties[1], vTitle);
if (store.IsEditable(_complaintProperties[2])
   store.SetValue(_complaintProperties[2], vSubject);
if (store.IsEditable(_complaintProperties[3])
   store.SetValue(_complaintProperties[3], vComments);
if (store.IsEditable(_complaintProperties[4])
   store.SetValue(_complaintProperties[4], vKeywords);
store.Commit();

There would be a bit more work to do with this little snippet, such as ensuring that none of the values actually include a semi-colon, but this article is not about complete programming practices - it is only about the Windows Properties System.

The sample included with this article is a bit more complex than the one described above, and allows the setting of properties on various formats of images.

Reference

A reference help file is attached to this article documenting all of the objects exposed by this library. Below is a quick summary, as well as a bit of background and extra useful information to assist when dealing with these objects.

The library exposes seven main classes:

  • PropertyKey - used to identify an individual property
  • PropVariant - a variant-like object that contains a property's value
  • PropertyEnumeration - defines enumeration values that a property may contain
  • PropertyDescription - defines how a property is displayed and the type of information it contains
  • PropertyDescriptionList - a collection of related PropertyDescriptions
  • PropertyStore - The actual properties as applied to an item (normally a file)
  • ShellItem - Represents an object in the Shell. Encapsulates the IShellItem and IShellItem2 interfaces.

Properties are identified by a PropertyKey. Internally, a PropertyKeycontains a Guid and a numeric Pid. I think originally the Guid was meant to identify a related group of properties, and the Pid would identify a single property within that group. However, looking at some of the values in the system, this ideal appears to have gone by the wayside, and so in reality nothing can be assumed by the actual contents of the PropertyKey. However, each PropertyKey also has a canonical name, and these names do have some sort of structure. They are normally made up of two or three identifiers, delimited by a period (.). The first identifier is always System, and the last identifies the actual property. If the property belongs directly to the System group, then those two identifiers make up the full canonical name. If the property belongs to another grouping, then that name comes before the individual identity. Some examples are System.Title and System.Author, belonging to the System group, with System.Image.BitDepth and System.Image.Dimensions belonging to the Image group.

The PropVariant is a bit of a strange beast. Like a Variant, it can contain just about any value type you could think of, and its internal structure is, in fact, extremely similar to that of the VARIANT structure used throughout the COM and Windows API systems. However, it is dissimilar enough so that any attempt to use the internal .NET marshalling from Variant to object with a PropVariant will fail catastrophically. Its main difference is in the way it handles arrays - rather than a pointer to a SafeArray, which is the COM norm, it has an internal structure referred to as Counted Array, or CA. The CA_xxxx members of the PROPVARIANT (named for the type of array) contain a counter of the number of elements, and a pointer to the elements themselves. Any PropVariant with its VarType member including the VT_VECTOR flag will have one of these structures as its data.

Another strange thing about the PropVariant is that a number of (but not all) the functions that deal with them are able to see a scalar value as a single-element vector. As there seems to be no rhyme or reason as to which functions operate in this fashion, this library has fully separated scalar operations and vector operations, so that attempting to obtain element 0 from a scalar PropVariant will always generate an out of bounds exception.

The enumeration type used by the property system is considerably more complex than a normal enumerated type variable, insofar as each enumerated name can have a range of values. A good example of this is the System.Rating[^] property, that has an actual value between 1 and 99. However, when displayed, this property is described with a number of stars. The way this works is that each enumeration value contains a minValue and a setValue field, If the value of a rating property is greater than or equal to the minValue, but less than the minValue of the next enumeration member, then it is displayed as that enumeration member's text. The final enumeration member's setValue field contains the maximum value that can be contained by that property. If a user selects a rating from one of the enumeration members, then the property is assigned the value in the setValue field of that enumeration member. If all this seems a little complicated, then take a look at the tables at the bottom of the linked System.Rating page above, and it will become clearer.

PropertyDescriptions and PropertyDescriptionLists are mostly established and saved in the registry by a file type when it is registered. These types will generally have a Property Handler defined, that manages the saving and retrieval of the properties associated with the particular file type. This article does not explore the writing of a property handler, as it is a complex topic in itself, and really should not be done in managed code at all. However, the fact that these lists are pre-established means that you can get them without having a file to load them from. This is where the Fake File[^] that I wrote about recently comes into play. By creating a fake file, and asking that item for a PropertyDescriptionList, it will look up all of the relevant registry entries and provide the list for you. This method does not work so well with a PropertyStore however, as the PropertyStore actually needs to open the file to read and/or write the property values. As such, creating a PropertyStore will generally result in a File Not Found exception, unless you pass the flag GetFlags.BestEffort. Even so, a PropertyStore created in this way will only give you properties that are handled by the system, and only ever as read-only. Anything that needs the actual file will not be present in that store.

The ShellItem class represents an item, normally a file or folder, in the Windows Shell. Although this is more related to Shell programming than the Windows Property System, this object really forms the link between the two API areas. This class encapsulates both the IShellItem and IShellItem 2 interfaces, and allows direct (read-only) access to many of the properties associated with the item.

1Errata

I finally figured out why the IPropertySystem interface would not work properly, and it is my own fault. I was correct in my assumption that the VTable was getting screwed up, but that was because these shell interfaces do not implement IDispatch. As such, when declaring the interface as a ComImport, it is necessary to set the InterfaceTypeAttribute to ComInterfaceType.InterfaceIsIUnknown. If you leave this out, the default becomes ComInterfaceType.InterfaceIsDual, the IDispatch methods are assumed, and the VTable is adjusted accordingly. However, because this interface does not actually exist in the VTable, everything gets thrown out of whack.

I'm not changing the library to incorporate this at the moment, as the global API functions are working - I may do down the track.

History

November 29, 2016 - Initial publication

December 8, 2016 - Added the ShellItem class. The IShellItem and IShellItem2 interfaces are so widely used in reference to Windows Property System, I decided it was relevant to add this class to the library.

January 6, 2017 - Added the Errata section describing what I had done wrong to make the IPropertyStore interface not work.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Australia Australia
Been programming for 40 years now, starting when I was 13 on DEC PDP 11 (back in the day of paper tape storage, and hex switch boot procedures). Got right into micro-computers from an early age, with machines like the Dick Smith Sorcerer and the CompuColor II. Started CP/M and MS-DOS programming in the mid 1980's. By the end of the '80's, I was just starting to get a good grip on OOP (Had Zortech C++ V1.0).

Got into ATL and COM programming early 2002. As a result, my gutter vocabulary has expanded, but it certainly keeps me off the streets.

Recently, I have had to stop working full time as a programmer due to permanent brain damage as a result of a tumour (I just can't keep up the pace required to meet KPI's). I still like to keep my hand in it, though, and will probably post more articles here as I discover various tricky things.

Comments and Discussions

 
QuestionAmazing Pin
u1923u12389h17-Feb-24 5:01
u1923u12389h17-Feb-24 5:01 
QuestionHow to do this from win32 api Pin
davercadman19-Sep-19 13:51
davercadman19-Sep-19 13:51 
QuestionTrying to write property to pdf file Pin
rderancourt25-Jun-19 10:17
rderancourt25-Jun-19 10:17 
QuestionOwn metadata Pin
beda_t3-Jul-17 19:55
beda_t3-Jul-17 19:55 
AnswerRe: Own metadata Pin
Midi_Mick4-Jul-17 14:32
professionalMidi_Mick4-Jul-17 14:32 
Praisevery nice Pin
BillW3311-Jan-17 2:44
professionalBillW3311-Jan-17 2:44 
GeneralRe: very nice Pin
Midi_Mick11-Jan-17 2:51
professionalMidi_Mick11-Jan-17 2:51 
Questioninteresting, thx Pin
Tom Henn8-Dec-16 2:23
Tom Henn8-Dec-16 2:23 
AnswerRe: interesting, thx Pin
Midi_Mick8-Dec-16 2:46
professionalMidi_Mick8-Dec-16 2:46 
Praisethis is a greate work Pin
Member 53615729-Nov-16 21:24
Member 53615729-Nov-16 21:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.