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()!;
}
}
}