diff --git a/Windows.Shell/ShellProperties/ReadOnlyPropertyStore.cs b/Windows.Shell/ShellProperties/ReadOnlyPropertyStore.cs
new file mode 100644
index 00000000..fb0430cb
--- /dev/null
+++ b/Windows.Shell/ShellProperties/ReadOnlyPropertyStore.cs
@@ -0,0 +1,264 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Vanara.PInvoke;
+using static Vanara.PInvoke.Ole32;
+using static Vanara.PInvoke.PropSys;
+
+namespace Vanara.Windows.Shell
+{
+ /// Encapsulates the IPropertyStore object.
+ ///
+ ///
+ public abstract class ReadOnlyPropertyStore : IReadOnlyDictionary, IDisposable
+ {
+ /// The IPropertyStore instance.
+ protected IPropertyStore iPropertyStore;
+
+ private PropertyDescriptionDictionary descriptions;
+
+ /// Initializes a new instance of the class.
+ protected ReadOnlyPropertyStore() { }
+
+ /// Gets the number of properties in the current property store.
+ public int Count => Run(ps => (int)(ps?.GetCount() ?? 0));
+
+ /// Value that allows matching this property store's keys to their property descriptions.
+ /// The property descriptions.
+ public virtual IReadOnlyDictionary Descriptions => descriptions ??= new PropertyDescriptionDictionary(this);
+
+ /// Gets a value indicating whether the is read-only.
+ public virtual bool IsReadOnly => true;
+
+ /// Gets an containing the keys of the .
+ public IEnumerable Keys => Run(ps => GetKeyEnum(ps).ToList());
+
+ /// Gets an containing the values in the .
+ public IEnumerable Values => Run(ps => GetKeyEnum(ps).Select(k => TryGetValue(ps, k, out object v) ? v : null).ToList());
+
+ /// Gets or sets the value of the property with the specified known key.
+ /// The value.
+ /// The known key of the property (e.g. "System.Title"}.
+ /// The value of the property.
+ public virtual object this[string knownKey] => this[GetPropertyKeyFromName(knownKey)];
+
+ /// Gets or sets the value of the property with the specified PROPERTYKEY.
+ /// The value.
+ /// The PROPERTYKEY of the property.
+ /// The value of the property.
+ public virtual object this[PROPERTYKEY key] => TryGetValue(key, out var r) ? r : null;
+
+ /// Gets the property key for a canonical property name.
+ /// A property name.
+ /// The requested property key.
+ public static PROPERTYKEY GetPropertyKeyFromName(string name)
+ {
+ if (name is null) throw new ArgumentNullException(nameof(name));
+ var hr = PSGetPropertyKeyFromName(name, out var pk);
+ if (hr == HRESULT.TYPE_E_ELEMENTNOTFOUND) throw new ArgumentOutOfRangeException(nameof(name));
+ hr.ThrowIfFailed();
+ return pk;
+ }
+
+ /// Determines whether the contains an element with the specified key.
+ /// The key to locate in the .
+ /// true if the contains an element with the key; otherwise, false.
+ public bool ContainsKey(PROPERTYKEY key) => Keys.Contains(key);
+
+ ///
+ /// Copies the elements of the to an , starting at a particular index.
+ ///
+ ///
+ /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing.
+ ///
+ /// The zero-based index in at which copying begins.
+ /// arrayIndex - The number of items exceeds the length of the supplied array.
+ /// array
+ public void CopyTo(KeyValuePair[] array, int arrayIndex)
+ {
+ if (array.Length < (arrayIndex + Count))
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex), "The number of items exceeds the length of the supplied array.");
+ if (array is null)
+ throw new ArgumentNullException(nameof(array));
+ var i = arrayIndex;
+ foreach (var kv in this)
+ array[i++] = kv;
+ }
+
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ public virtual void Dispose()
+ {
+ if (iPropertyStore != null)
+ {
+ Marshal.ReleaseComObject(iPropertyStore);
+ iPropertyStore = null;
+ }
+ }
+
+ /// Gets the property.
+ /// The type of the value.
+ /// The key.
+ /// The cast value of the property.
+ /// key
+ public TVal GetProperty(PROPERTYKEY key) => TryGetValue(key, out var ret) ? ret : throw new ArgumentOutOfRangeException(nameof(key));
+
+ /// Gets the property description related to a property key.
+ /// The key.
+ /// The related property description, if one exists; otherwise .
+ public PropertyDescription GetPropertyDescription(PROPERTYKEY key) => PropertyDescription.Create(key);
+
+ /// Gets the string value of the property.
+ /// The key.
+ /// The formatting flags.
+ /// The string value of the property.
+ /// key
+ public string GetPropertyString(PROPERTYKEY key, PROPDESC_FORMAT_FLAGS flags = PROPDESC_FORMAT_FLAGS.PDFF_DEFAULT)
+ {
+ using var pv = GetPropVariant(key);
+ return PropertyDescription.Create(key)?.FormatForDisplay(pv, flags);
+ }
+
+ /// Gets the PROPVARIANT value for a key.
+ /// The key.
+ /// The PROPVARIANT value.
+ /// key
+ public PROPVARIANT GetPropVariant(PROPERTYKEY key)
+ {
+ return Run(ps =>
+ {
+ var pv = new PROPVARIANT();
+ ps.GetValue(key, pv);
+ return pv;
+ });
+ }
+
+ /// Gets the value associated with the specified key.
+ /// The key whose value to get.
+ ///
+ /// When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the
+ /// type of the parameter. This parameter is passed uninitialized.
+ ///
+ ///
+ /// if the object that implements contains an element with the
+ /// specified key; otherwise, .
+ ///
+ public bool TryGetValue(PROPERTYKEY key, out object value) => TryGetValue(key, out value);
+
+ /// Gets the value associated with the specified key.
+ /// The type of the returned value.
+ /// The key whose value to get.
+ ///
+ /// When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the
+ /// type of the parameter. This parameter is passed uninitialized.
+ ///
+ ///
+ /// if the object that implements contains an element with the
+ /// specified key; otherwise, .
+ ///
+ public virtual bool TryGetValue(PROPERTYKEY key, out TVal value)
+ {
+ var result = Run(ps =>
+ {
+ var ret = TryGetValue(ps, key, out var val);
+ return (ret, ret ? val : default);
+ });
+ value = result.Item2;
+ return result.ret;
+ }
+
+ /// Returns an enumerator that iterates through the collection.
+ /// A that can be used to iterate through the collection.
+ IEnumerator> IEnumerable>.GetEnumerator() => (Run(ps => GetKeyEnum(ps).Select(k => new KeyValuePair(k, TryGetValue(ps, k, out object pv) ? pv : null))) ?? new KeyValuePair[0]).GetEnumerator();
+
+ /// Returns an enumerator that iterates through a collection.
+ /// An object that can be used to iterate through the collection.
+ IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>)this).GetEnumerator();
+
+ /// Gets the value associated with the specified key.
+ /// The IPropertyStore instance.
+ /// The key whose value to get.
+ ///
+ /// When this method returns, the value associated with the specified key, if the key is found; otherwise, default(T) .
+ ///
+ /// if the property store contains an element with the specified key; otherwise, .
+ protected static bool TryGetValue(IPropertyStore ps, PROPERTYKEY key, out T value)
+ {
+ try
+ {
+ value = (T)ps.GetValue(key);
+ return true;
+ }
+ catch { }
+ value = default;
+ return false;
+ }
+
+ /// The IPropertyStore instance. This can be null.
+ protected virtual IPropertyStore GetIPropertyStore() => iPropertyStore;
+
+ /// Gets an enumeration of the keys in the property store.
+ /// Keys in the property store.
+ protected virtual IEnumerable GetKeyEnum(IPropertyStore ps)
+ {
+ if (ps is null) yield break;
+ for (uint i = 0; i < Count; i++)
+ yield return ps.GetAt(i);
+ }
+
+ /// Runs the specified action with a retrived IPropertyStore instance.
+ /// The action to run.
+ protected void Run(Action action)
+ {
+ iPropertyStore ??= GetIPropertyStore();
+ if (iPropertyStore is null) return;
+ action(iPropertyStore);
+ }
+
+ /// Runs the specified action with a retrived IPropertyStore instance.
+ /// The return type of the action and method.
+ /// The action to run.
+ /// The return value from .
+ protected T Run(Func action)
+ {
+ iPropertyStore ??= GetIPropertyStore();
+ return iPropertyStore is null ? default : action.Invoke(iPropertyStore);
+ }
+
+ private class PropertyDescriptionDictionary : IReadOnlyDictionary
+ {
+ private ReadOnlyPropertyStore store;
+
+ public PropertyDescriptionDictionary(ReadOnlyPropertyStore ps) => store = ps;
+
+ public IEnumerable Values => store.Keys.Select(k => store.GetPropertyDescription(k)).ToList();
+
+ public int Count => store.Count;
+
+ public IEnumerable Keys => store.Keys;
+
+ public PropertyDescription this[PROPERTYKEY key] => store.GetPropertyDescription(key);
+
+ public bool ContainsKey(PROPERTYKEY key) => store.ContainsKey(key);
+
+ public IEnumerator> GetEnumerator() =>
+ store.Keys.Select(k => new KeyValuePair(k, store.GetPropertyDescription(k))).ToList().GetEnumerator();
+
+ public bool TryGetValue(PROPERTYKEY key, out PropertyDescription value)
+ {
+ if (store.ContainsKey(key))
+ {
+ value = this[key];
+ return true;
+ }
+ value = null;
+ return false;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+ }
+}
\ No newline at end of file