How to set extended file properties?
Add following NuGet packages to your project:
Microsoft.WindowsAPICodePack-Shell
by MicrosoftMicrosoft.WindowsAPICodePack-Core
by Microsoft
Read and Write Properties
using Microsoft.WindowsAPICodePack.Shell;
using Microsoft.WindowsAPICodePack.Shell.PropertySystem;
string filePath = @"C:\temp\example.docx";
var file = ShellFile.FromFilePath(filePath);
// Read and Write:
string[] oldAuthors = file.Properties.System.Author.Value;
string oldTitle = file.Properties.System.Title.Value;
file.Properties.System.Author.Value = new string[] { "Author #1", "Author #2" };
file.Properties.System.Title.Value = "Example Title";
// Alternate way to Write:
ShellPropertyWriter propertyWriter = file.Properties.GetPropertyWriter();
propertyWriter.WriteProperty(SystemProperties.System.Author, new string[] { "Author" });
propertyWriter.Close();
Important:
The file must be a valid one, created by the specific assigned software. Every file type has specific extended file properties and not all of them are writable.
If you right-click a file on desktop and cannot edit a property, you wont be able to edit it in code too.
Example:
- Create txt file on desktop, rename its extension to docx. You can't
edit itsAuthor
orTitle
property. - Open it with Word, edit and save
it. Now you can.
So just make sure to use some try
catch
Further Topic:
MS Docs: Implementing Property Handlers
.NET: How to set extended file attributes in a cross-platform way?
There is no API (yet). Here's my proposal to add it: https://github.com/dotnet/runtime/issues/49604
Read/Write 'Extended' file properties (C#)
For those of not crazy about VB, here it is in c#:
Note, you have to add a reference to Microsoft Shell Controls and Automation from the COM tab of the References dialog.
public static void Main(string[] args)
{
List<string> arrHeaders = new List<string>();
Shell32.Shell shell = new Shell32.Shell();
Shell32.Folder objFolder;
objFolder = shell.NameSpace(@"C:\temp\testprop");
for( int i = 0; i < short.MaxValue; i++ )
{
string header = objFolder.GetDetailsOf(null, i);
if (String.IsNullOrEmpty(header))
break;
arrHeaders.Add(header);
}
foreach(Shell32.FolderItem2 item in objFolder.Items())
{
for (int i = 0; i < arrHeaders.Count; i++)
{
Console.WriteLine(
$"{i}\t{arrHeaders[i]}: {objFolder.GetDetailsOf(item, i)}");
}
}
}
adding/editing new extended properties to the details tab in existing files
The details tab in the properties window is populated with metadata property handlers. The metadata property system is something Microsoft introduced with Windows Vista and it was made open and extensible, enabling independent developers (like Solidworks) to implement and support their own file properties. Very roughly, the flow of execution is something like this:
User clicks file properties
Look up property handler for the file format
If found property handler:
Query property handler for properties
Populate file details with queried properties
Else:
Populate file details with generic file info
Property handlers are COM objects. COM (Component Object Model) is Microsoft's attempt at a language-independent object oriented framework whose origins go all the way back to the nineties, but for the purposes of this explanation suffice it to say that a COM object is a C++ class that implements the IUnknown
interface. A property handler has to implement the IPropertyStore
interface on top of that:
struct IPropertyStore : public IUnknown
{
public:
virtual HRESULT GetCount(
DWORD *cProps) = 0;
virtual HRESULT GetAt(
DWORD iProp,
PROPERTYKEY *pkey) = 0;
virtual HRESULT GetValue(
REFPROPERTYKEY key,
PROPVARIANT *pv) = 0;
virtual HRESULT SetValue(
REFPROPERTYKEY key,
REFPROPVARIANT propvar) = 0;
virtual HRESULT Commit( void) = 0;
};
A convenience implementation of this interface is CLSID_InMemoryPropertyStore
provided to developers to ease their own implementation of IPropertyStore
. The interesting methods here are GetValue
and SetValue
. Properties are assigned a unique GUID, which the PROPERTYKEY
structure passed into these functions contains to identify the property. The implementations details for GetValue
and SetValue
are left to the developer, so it is up to the developer how and where to store the value for each property -- these values could be stored in another file, in an alternate file stream, or in the registry to name a few options -- but for transportability reasons it is recommended to store the values in the file itself. This way, if the file is zipped up and emailed, for example, the properties go with it.
The property handler COM object is compiled into a DLL and registered with the system with regsvr32
. This allows Windows to know where to go look for properties for that specific file format. Once registered, the property handler can be obtained in a number of ways, one of which is the convenience function SHGetPropertyStoreFromParsingName
:
HRESULT GetPropertyStore(PCWSTR pszFilename, GETPROPERTYSTOREFLAGS gpsFlags, IPropertyStore** ppps)
{
WCHAR szExpanded[MAX_PATH];
HRESULT hr = ExpandEnvironmentStrings(pszFilename, szExpanded, ARRAYSIZE(szExpanded)) ? S_OK : HRESULT_FROM_WIN32(GetLastError());
if (SUCCEEDED(hr))
{
WCHAR szAbsPath[MAX_PATH];
hr = _wfullpath(szAbsPath, szExpanded, ARRAYSIZE(szAbsPath)) ? S_OK : E_FAIL;
if (SUCCEEDED(hr))
{
hr = SHGetPropertyStoreFromParsingName(szAbsPath, NULL, gpsFlags, IID_PPV_ARGS(ppps));
}
}
return hr;
}
Once obtained, GetValue
and SetValue
can be invoked on the IPropertyStore
object to either obtain, change or set new value for a property. If using SetValue
though, make sure to invoke Commit
as well.
Microsoft provides a utility, called PropertyEdit
, to get and set metadata properties on a file as part of their Windows Classic Samples. It's a shame they don't mention it anywhere in their help pages. Since you already have Solidworks installed, the property handler for the file formats you're interested in should already be registered on the system and it should be a matter of compiling PropertyEdit
and using it to get and set the metadata properties the handler supports. It's a simple command line utility.
If you need, or want to support custom metadata for your own file format, there is a full-blown sample property handler as well: RecipePropertyHandler
.
For reference, to set a property by its canonical name:
HRESULT GetPropertyStore(PCWSTR pszFilename, GETPROPERTYSTOREFLAGS gpsFlags, IPropertyStore** ppps)
{
WCHAR szExpanded[MAX_PATH];
HRESULT hr = ExpandEnvironmentStrings(pszFilename, szExpanded, ARRAYSIZE(szExpanded)) ? S_OK : HRESULT_FROM_WIN32(GetLastError());
if (SUCCEEDED(hr))
{
WCHAR szAbsPath[MAX_PATH];
hr = _wfullpath(szAbsPath, szExpanded, ARRAYSIZE(szAbsPath)) ? S_OK : E_FAIL;
if (SUCCEEDED(hr))
{
hr = SHGetPropertyStoreFromParsingName(szAbsPath, NULL, gpsFlags, IID_PPV_ARGS(ppps));
}
}
return hr;
}
HRESULT SetPropertyValue(PCWSTR pszFilename, PCWSTR pszCanonicalName, PCWSTR pszValue)
{
// Convert the Canonical name of the property to PROPERTYKEY
PROPERTYKEY key;
HRESULT hr = PSGetPropertyKeyFromName(pszCanonicalName, &key);
if (SUCCEEDED(hr))
{
IPropertyStore* pps = NULL;
// Call the helper to get the property store for the
// initialized item
hr = GetPropertyStore(pszFilename, GPS_READWRITE, &pps);
if (SUCCEEDED(hr))
{
PROPVARIANT propvarValue = {0};
hr = InitPropVariantFromString(pszValue, &propvarValue);
if (SUCCEEDED(hr))
{
hr = PSCoerceToCanonicalValue(key, &propvarValue);
if (SUCCEEDED(hr))
{
// Set the value to the property store of the item.
hr = pps->SetValue(key, propvarValue);
if (SUCCEEDED(hr))
{
// Commit does the actual writing back to the file stream.
hr = pps->Commit();
if (SUCCEEDED(hr))
{
wprintf(L"Property %s value %s written successfully \n", pszCanonicalName, pszValue);
}
else
{
wprintf(L"Error %x: Commit to the propertystore failed.\n", hr);
}
}
else
{
wprintf(L"Error %x: Set value to the propertystore failed.\n", hr);
}
}
PropVariantClear(&propvarValue);
}
pps->Release();
}
else
{
wprintf(L"Error %x: getting the propertystore for the item.\n", hr);
}
}
else
{
wprintf(L"Invalid property specified: %s\n", pszCanonicalName);
}
return hr;
}
Reading extended file properties with .NET Core
I don't think these are file attributes. I guess, it is MP3
metadata stored in ID3
tags.
You are using .NET Framework NuGet package WindowsAPICodePack-Shell
that can read such metadata.
Option #1
I couldn't find a .NET Core version of the original package.
But I found an unofficial .NET Core fork of the library: Microsoft-WindowsAPICodePack-Shell
(it's not authored by Microsoft).
Option #2
For .NET Core you can install the TagLibSharp
NuGet package.
And then you just read metadata like this:
var file = new FileInfo("track.mp3");
var tagLibFile = TagLib.File.Create(file.Name);
var title = tagLibFile.Tag.Title;
var album = tagLibFile.Tag.Album;
var albumArtist = tagLibFile.Tag.AlbumArtists;
var genres = tagLibFile.Tag.JoinedGenres;
var length = tagLibFile.Properties.Duration;
Editing extended file properties using powershell or VBA?
You cannot easily do that in VBA or Outlook Object Model: these extra properties must be set on the OLE storage level used by the MSG file.
If using Redemption (I am its author) is an option, it exposes olMsgWithSummary
format (similar to olMsg
and olMsgUnicode
in OOM) that will do what you need. The script below saves the currently selected Outlook message:
set Session = CreateObject("Redemption.RDOSession")
Session.MAPIOBJECT = Application.Session.MAPIOBJECT
set oMsg = Application.ActiveExplorer.Selection(1)
set rMsg = Session.GetRDOObjectFromOutlookObject(oMsg)
rMsg.SaveAs "c:\temp\ExtraProps.msg", 1035 '1035 is olMsgWithSummary
Your script above would like like the following (off the top of my head):
Public Sub SaveMessageAsMsg()
Dim xMail As Outlook.MailItem
Dim xObjItem As Object
Dim xPath As String
Dim xDtDate As Date
Dim rSession As Object
Dim rSession As Object
Dim xName, xFileName As String
On Error Resume Next
Set xShell = CreateObject("Shell.Application")
Set xFolder = xShell.BrowseForFolder(0, "Select a folder:", 0, "C:\Users\" & Environ("UserName") & "ANON VARIABLE")
If Not TypeName(xFolder) = "Nothing" Then
Set xFolderItem = xFolder.self
xFileName = xFolderItem.Path & "\"
Else
xFileName = ""
Exit Sub
End If
set rSession = CreateObject("Redemption.RDOSession")
rSession.MAPIOBJECT = Outlook.Session.MAPIOBJECT
For Each xObjItem In Outlook.ActiveExplorer.Selection
If xObjItem.Class = olMail Then
Set xMail = xObjItem
SenderName = xMail.SenderName
xName = xMail.Subject
xDtDate = xMail.ReceivedTime
xName = Replace(Format(xDtDate, "yyyy-mm-dd ", vbUseSystemDayOfWeek, _
vbUseSystem) & " @ " & Format(xDtDate, "hh:mm:ss", _
vbUseSystemDayOfWeek, vbUseSystem) & " - " & SenderName & " - " & xName & ".msg", ":", ".")
Dim RegEx As Object
Set RegEx = CreateObject("VBScript.RegExp")
With RegEx
.Pattern = "[\\/\*\?""<>\|]"
.Global = True
ValidName = .Replace(xName, "")
End With
xPath = xFileName + ValidName
set rMsg = rSession.GetRDOObjectFromOutlookObject(xMail)
rMsg.SaveAs xPath, 1035
End If
Next
End Sub
Delphi: How to SET(write) extended file properties?
Partial answer: The set property Delphi code can be found here.
or if you have the latest JCL library - use TJclFilePropertySet at jclNtfs.pas
Warning: Notice that this code works for xls files but it does not seem to work for txt/cvs and jpg files in Windows 7 Pro/Enterprise or 2008 (64 bits).
It seems that M$ has changed the way properties work in these OS: "You can't add or change the file properties of some types of files. For example, you can't add any properties to TXT or RTF file". Sadly for me, going back to XP mode is not an option.
Related Topics
C# SQL Server - Passing a List to a Stored Procedure
Determine the Number of Lines Within a Text File
What Is the Use of "Ref" for Reference-Type Variables in C#
C# Convert String from Utf-8 to Iso-8859-1 (Latin1) H
Accessing UI (Main) Thread Safely in Wpf
Reflection to Identify Extension Methods
Serializing and Deserializing Expression Trees in C#
Is Int[] a Reference Type or a Value Type
Read Xml Attribute Using Xmldocument
What Is the Best Data Type to Use for Money in C#
Shorter Syntax for Casting from a List<X> to a List<Y>
Using Filesystemwatcher to Monitor a Directory
Why Should I Prefer Single 'Await Task.Whenall' Over Multiple Awaits