using Microsoft.Win32.SafeHandles; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using Vanara.PInvoke; using static Vanara.PInvoke.Shell32; using static Vanara.PInvoke.ShlwApi; namespace Vanara.Windows.Shell; /// Represents a Shell file association defined in the Windows Registry. Wraps . public class ShellAssociation { private IQueryAssociations qassoc; /// Initializes a new instance of the class. /// The IQueryAssociations instance to use. /// The optional file extension. This should be in the ".ext" format. internal ShellAssociation(IQueryAssociations pQA, string? ext) { qassoc = pQA; Extension = ext; } /// Gets all the file associations defined for the system. /// Returns a value. public static IReadOnlyDictionary FileAssociations { get; } = new ShellAssociationDictionary(true); /// /// The icon reference of the app associated with the file type or URI scheme. This is configured by users in their default program settings. /// [DefaultValue(null)] public string? AppIconReference => GetString(ASSOCSTR.ASSOCSTR_APPICONREFERENCE); /// /// The AppUserModelID of the app associated with the file type or URI scheme. This is configured by users in their default program settings. /// [DefaultValue(null)] public string? AppId => GetString(ASSOCSTR.ASSOCSTR_APPID); /// /// The publisher of the app associated with the file type or URI scheme. This is configured by users in their default program settings. /// [DefaultValue(null)] public string? AppPublisher => GetString(ASSOCSTR.ASSOCSTR_APPPUBLISHER); /// /// Introduced in Internet Explorer 6. Describes a general type of MIME file association, such as image and bmp, so that /// applications can make general assumptions about a specific file type. /// [DefaultValue(null)] public string? ContentType => GetString(ASSOCSTR.ASSOCSTR_CONTENTTYPE); /// /// Introduced in Internet Explorer 6. Returns the path to the icon resources to use by default for this association. Positive /// numbers indicate an index into the dll's resource table, while negative numbers indicate a resource ID. An example of the syntax /// for the resource is "c:\myfolder\myfile.dll,-1". /// [DefaultValue(null)] public IconLocation? DefaultIcon => IconLocation.TryParse(GetString(ASSOCSTR.ASSOCSTR_DEFAULTICON), out var loc) ? loc : null; /// The extension string. [DefaultValue(null)] public string? Extension { get; } /// The friendly name of an executable file. [DefaultValue(null)] public string? FriendlyAppName => GetString(ASSOCSTR.ASSOCSTR_FRIENDLYAPPNAME); /// The friendly name of a document type. [DefaultValue(null)] public string? FriendlyDocName => GetString(ASSOCSTR.ASSOCSTR_FRIENDLYDOCNAME); /// Gets a list of file name extension handlers. /// The handlers for this association. public IReadOnlyList Handlers { get { if (SHAssocEnumHandlers(Extension, ASSOC_FILTER.ASSOC_FILTER_NONE, out var ieah).Failed) return new List(); using var pieah = ComReleaserFactory.Create(ieah); var e = new Collections.IEnumFromCom(ieah.Next, () => { }); return e.Select(i => new ShellAssociationHandler(i)).ToList(); } } /// /// Corresponds to the InfoTip registry value. Returns an info tip for an item, or list of properties in the form of an /// IPropertyDescriptionList from which to create an info tip, such as when hovering the cursor over a file name. The list of /// properties can be parsed with . /// [DefaultValue(null)] public string? InfoTip => GetString(ASSOCSTR.ASSOCSTR_INFOTIP); /// /// The ProgID provided by the app associated with the file type or URI scheme. This if configured by users in their default program settings. /// public ProgId ProgId => ProgId.Open(GetString(ASSOCSTR.ASSOCSTR_PROGID)!, true, true, true); /// /// Introduced in Internet Explorer 6. Corresponds to the QuickTip registry value. Same as ASSOCSTR_INFOTIP, except that it always /// returns a list of property names in the form of an IPropertyDescriptionList. The difference between this value and /// ASSOCSTR_INFOTIP is that this returns properties that are safe for any scenario that causes slow property retrieval, such as /// offline or slow networks. Some of the properties returned from ASSOCSTR_INFOTIP might not be appropriate for slow property /// retrieval scenarios. The list of properties can be parsed with PSGetPropertyDescriptionListFromString. /// [DefaultValue(null)] public string? QuickTip => GetString(ASSOCSTR.ASSOCSTR_QUICKTIP); /// /// Introduced in Internet Explorer 6. For an object that has a Shell extension associated with it, you can use this to retrieve the /// CLSID of that Shell extension object by passing a string representation of the IID of the interface you want to retrieve as the /// parameter of IQueryAssociations::GetString. For example, if you want to retrieve a handler that implements the IExtractImage /// interface, you would specify "{BB2E617C-0920-11d1-9A0B-00C04FC2D6C1}", which is the IID of IExtractImage. /// [DefaultValue(null)] public IndirectString? ShellExtension => IndirectString.TryParse(GetString(ASSOCSTR.ASSOCSTR_SHELLEXTENSION), out var s) ? s : null; /// A string value of the URI protocol schemes. For example, http:https:ftp:file: or * indicating all. [DefaultValue(null)] public string? SupportedUriProtocols => GetString(ASSOCSTR.ASSOCSTR_SUPPORTED_URI_PROTOCOLS); /// /// Introduced in Internet Explorer 6. Corresponds to the TileInfo registry value. Contains a list of properties to be displayed for /// a particular file type in a Windows Explorer window that is in tile view. This is the same as ASSOCSTR_INFOTIP, but, like /// ASSOCSTR_QUICKTIP, it also returns a list of property names in the form of an IPropertyDescriptionList. The list of properties /// can be parsed with PSGetPropertyDescriptionListFromString. /// [DefaultValue(null)] public string? TileInfo => GetString(ASSOCSTR.ASSOCSTR_TILEINFO); /// Gets the command verbs for this file association. /// Returns a value. private IReadOnlyDictionary Verbs => throw new NotImplementedException(); // TODO /// Initializes a new instance of the class based on the supplied executable name. /// The full path of the application executable. /// A instance if exists; otherwise. public static ShellAssociation CreateFromAppExeName(string appExeName) => CreateAndInit(ASSOCF.ASSOCF_INIT_BYEXENAME, appExeName); /// Initializes a new instance of the class based on the supplied CLSID. /// The CLSID. /// A instance if exists; otherwise. public static ShellAssociation CreateFromCLSID(Guid classId) => CreateAndInit(0, classId.ToString("B")); /// /// Initializes a new instance of the class based on the supplied programmatic identifier (ProgId). /// /// The ProgId. /// A instance if exists; otherwise. public static ShellAssociation CreateFromProgId(string progId) => CreateAndInit(0, progId); /// Initializes a new instance of the class based on the supplied file extension. /// The file extension. This should be in the ".ext" format. /// A instance if exists; otherwise. public static ShellAssociation FromFileExtension(string ext) { if (ext is null) throw new ArgumentNullException(nameof(ext)); if (!ext.StartsWith(".")) throw new ArgumentException("The value must be in the format \".ext\"", nameof(ext)); return CreateAndInit(ASSOCF.ASSOCF_INIT_DEFAULTTOSTAR, ext); } /// Searches for and retrieves file or protocol association-related binary data from the registry. /// The ASSOCDATA value that specifies the type of data that is to be returned. /// /// An optional string with information about the location of the data. It is normally set to a Shell verb such as open. Set this /// parameter to if it is not used. /// /// A value that, when this method returns successfully, receives the requested data value. public SafeCoTaskMemHandle? GetData(ASSOCDATA data, string? extra = null) { if (qassoc is null) return null; try { const ASSOCF flags = 0; var sz = 0U; qassoc.GetData(flags, data, extra, default, ref sz); if (sz == 0) return null; var ret = new SafeCoTaskMemHandle(sz); qassoc.GetData(flags, data, extra, ret, ref sz); return ret; } catch (COMException e) when (e.ErrorCode == HRESULT.HRESULT_FROM_WIN32(Win32Error.ERROR_NO_ASSOCIATION)) { return null; } } /// Searches for and retrieves a file or protocol association-related key from the registry. /// The ASSOCKEY value that specifies the type of key that is to be returned. /// /// An optional string with information about the location of the key. It is normally set to a Shell verb such as open. Set this /// parameter to if it is not used. /// /// A handle to the resulting registry key. public SafeRegistryHandle GetKey(ASSOCKEY key, string? extra = null) { const ASSOCF flags = 0; qassoc.GetKey(flags, key, extra, out var hkey); return new SafeRegistryHandle((IntPtr)hkey, true); } /// Searches for and retrieves a file or protocol association-related string from the registry. /// An ASSOCSTR value that specifies the type of string that is to be returned. /// /// An optional string with information about the location of the string. It is typically set to a Shell verb such as open. Set this /// parameter to if it is not used. /// /// /// A string used to return the requested string. If there are no results for this value, is returned. /// public string? GetString(ASSOCSTR astr, string? extra = null) { try { const ASSOCF flags = ASSOCF.ASSOCF_NOTRUNCATE | ASSOCF.ASSOCF_REMAPRUNDLL; var sz = 0U; qassoc.GetString(flags, astr, extra, null, ref sz); var sb = new StringBuilder((int)sz, (int)sz); qassoc.GetString(flags, astr, extra, sb, ref sz); return sb.ToString(); } catch (COMException e) when (e.ErrorCode == HRESULT.HRESULT_FROM_WIN32(Win32Error.ERROR_NO_ASSOCIATION)) { return null; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ShellAssociation CreateAndInit(ASSOCF flags, string assoc) { // if (Environment.OSVersion.Version.Major >= 6) //var elements = new[] { new ASSOCIATIONELEMENT { ac = ASSOCCLASS.ASSOCCLASS_PROGID_STR, pszClass = progId } }; //AssocCreateForClasses(elements, (uint)elements.Length, typeof(IQueryAssociations).GUID, out var iq).ThrowIfFailed(); //ret.qassoc = (IQueryAssociations)iq; var ret = new ShellAssociation(AssocCreate(), assoc); ret.qassoc.Init(flags, assoc); return ret; } /// Represents a handler (executable) for a . public class ShellAssociationHandler : ComObjWrapper { internal ShellAssociationHandler(IAssocHandler h) : base(h) { } /// Retrieves the location of the icon associated with the application. /// /// An instance that contains the path and the index of the icon within the resource file for the /// application's icon. /// public IconLocation? IconLocation => ComInterface.GetIconLocation(out var p, out var i).Succeeded ? new IconLocation(p, i) : null; /// Indicates whether the application is registered as a recommended handler for the queried file type. /// if this instance is recommended; otherwise, . /// /// /// Applications that register themselves as handlers for particular file types can specify whether they are recommended /// handlers. This has no effect on the actual behavior of the applications when launched. It is simply provided as a hint to /// the user and a value that the UI can utilize programmatically, if desired. For example, the Shell's Open With dialog /// separates entries into Recommended Programs and Other Programs. /// /// /// Note that program recommendations may change over time. One example is provided when the user chooses an application from /// the Other Programs of the Open With dialog to open a particular file type. That may cause the Shell to /// "promote" that application to recommended status for that file type. Because the recommended status may change over time, /// applications should not cache this value, but query it each time it is needed. /// /// public bool IsRecommended => ComInterface.IsRecommended() == HRESULT.S_OK; /// Retrieves the full path and file name of the executable file associated with the file type. /// A string that contains the full path of the file, including the file name. public string? Name => ComInterface.GetName(out var n).Succeeded ? n : null; /// Retrieves the display name of an application. /// A string that contains the display name of the application. public string? UIName => ComInterface.GetUIName(out var n).Succeeded ? n : null; /// Indicates whether the current object is equal to another object of the same type. /// An object to compare with this object. /// true if the current object is equal to the parameter; otherwise, false. public override bool Equals(IAssocHandler? other) => string.Equals(Name, other is not null && other.GetName(out var n).Succeeded ? n : null); /// Returns a hash code for this instance. /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. public override int GetHashCode() => Name?.GetHashCode() ?? 0; /// Directly invokes the associated handler. /// A sequence of selected items on which to invoke the handler. /// /// /// IAssocHandler objects are typically used to populate an Open With menu. When one of those menu items is selected, /// this method is called to launch the chosen application. /// /// Invoke and CreateInvoker /// /// The IDataObject used by these methods can represent either a single file or a selection of multiple files. Not all /// applications support the multiple file option. The applications that do support that scenario might impose other /// restrictions, such as the number of files that can be opened simultaneously, or the acceptable combination of file types. /// /// /// Therefore, an application often must determine whether the handler supports the selection before trying to invoke the /// handler. For example, an application might enable a menu item only if it has verified that the selection in question was /// supported by that handler. /// /// public void Invoke(params ShellItem[] items) { if (items.Length == 0) throw new ArgumentException("", nameof(items)); if (items.Length == 1) { ComInterface.Invoke(items[0].DataObject).ThrowIfFailed(); } else { ComInterface.CreateInvoker(CreateDataObj(items), out var invoker).ThrowIfFailed(); using var pInvoker = ComReleaserFactory.Create(invoker); var hr = invoker.SupportsSelection(); if (hr == HRESULT.S_FALSE) throw new ArgumentException("This handler is unable to support the selections provided.", nameof(items)); hr.ThrowIfFailed(); invoker.Invoke().ThrowIfFailed(); } } /// Sets an application as the default application for this file type. /// /// A string that contains the display name of the application. /// public void MakeDefault(string description) => ComInterface.MakeDefault(description).ThrowIfFailed(); private static System.Runtime.InteropServices.ComTypes.IDataObject CreateDataObj(IEnumerable items) { if (items is null || !items.Any()) throw new ArgumentNullException(nameof(items)); if (items is not ShellItemArray litems) litems = new ShellItemArray(items); return litems.ToDataObject()!; } } }