Vanara/Windows.Shell.Common/FileInUseHandler.cs

250 lines
9.3 KiB
C#

using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices.ComTypes;
using Vanara.PInvoke;
using static Vanara.PInvoke.Ole32;
using static Vanara.PInvoke.Shell32;
namespace Vanara.Windows.Shell;
/// <summary>Constants used to indicate how a file in use is being used.</summary>
/// <remarks>
/// The interpretation of "playing" or "editing" is left to the application's implementation. Generally, "playing" would refer to a
/// media file while "editing" can refer to any file being altered in an application. However, the application itself best knows how to
/// map these terms to its actions.
/// </remarks>
public enum FileUsageType
{
/// <summary>The file is being played by the process that has it open.</summary>
Playing = FILE_USAGE_TYPE.FUT_PLAYING,
/// <summary>The file is being edited by the process that has it open.</summary>
Editing = FILE_USAGE_TYPE.FUT_EDITING,
/// <summary>
/// The file is open in the process for an unspecified action or an action that does not readily fit into the other two categories.
/// </summary>
Generic = FILE_USAGE_TYPE.FUT_GENERIC,
}
/// <summary>
/// A handler for applications that open file types that can be opened by other applications. An application's use of this object
/// enables Windows Explorer to discover the source of sharing errors, which enables users to address and retry operations that fail due
/// to those errors. This object handles registering the file with the Running Object Table (see <see cref="Ole32.IRunningObjectTable" />). It will revoke
/// that registration on disposal or when the <see cref="Registered" /> property is set to <see langword="false"/>.
/// </summary>
/// <remarks>This object should be created for the duration of use of any file that can be opened by other applications. It's scope should follow
/// that of the file handle or file stream.</remarks>
/// <example>
/// <code title="Example use of FileInUseHandler"><![CDATA[// The following should be taken as example methods within the application that handle opening and closing files.
///private Dictionary<string, (FileStream fileStream, FileInUseHandler inUseHandler)> openFiles =
/// new Dictionary<string, (FileStream, FileInUseHandler)>();
///
///private void OpenFile(string filePath)
///{
/// // Create the in-use handler tied to the 'mainForm' of this application.
/// var inUseHandler = new FileInUseHandler(filePath, mainForm, FileUsageType.Editing);
/// // Assign event handler to close this file if requested and allowed.
/// inUseHandler.CloseFile += (s, e) => CloseFile(((FileInUseHandler)s).FilePath);
/// // Assign event handler to prompt the user if another app requests control of this file.
/// inUseHandler.CloseRequested += (s, e) =>
/// e.Cancel = MessageBox.Show($"Another application has requested that {Path.GetFileName(((FileInUseHandler)s).FilePath)} be closed to allow it to edit. Allow?",
/// "Close file request", MessageBoxButtons.YesNo) == DialogResult.No;
/// // Capture file name, open file stream and in-use handler.
/// openFiles.Add(filePath, (File.OpenWrite(filePath), inUseHandler));
///}
///
///private void CloseFile(string filePath)
///{
/// // If the file is open, close the stream via disposal, and revoke in-use registration via disposal
/// if (openFiles.TryGetValue(filePath, out var info))
/// {
/// info.fileStream.Dispose();
/// info.inUseHandler.Dispose();
/// openFiles.Remove(filePath);
/// }
///}]]></code>
/// </example>
/// <seealso cref="IFileIsInUse" />
/// <seealso cref="IDisposable" />
public class FileInUseHandler : IFileIsInUse, IDisposable
{
private bool disposedValue;
private string appName;
private string filePath;
private IMoniker? moniker;
private uint regId;
/// <summary>Initializes a new instance of the <see cref="FileInUseHandler"/> class.</summary>
/// <param name="filePath">The file path.</param>
/// <param name="parent">The parent.</param>
/// <param name="usageType">Type of the usage.</param>
public FileInUseHandler(string filePath, HWND parent = default, FileUsageType usageType = FileUsageType.Generic)
{
ActivationWindow = parent;
FileUsageType = usageType;
FilePath = filePath;
appName = DefAppName;
}
/// <summary>Finalizes an instance of the <see cref="FileInUseHandler"/> class.</summary>
~FileInUseHandler()
{
Dispose(disposing: false);
}
/// <summary>Occurs after permission has been given to close the file and the file must now be closed.</summary>
public event EventHandler? CloseFile;
/// <summary>
/// Occurs when another application is requesting that the file be closed. If this event is not registered or if in response to this
/// event, you set the <see cref="CancelEventArgs.Cancel"/> property to <see langword="true"/>, then the requesting application will
/// be informed that it cannot take control.
/// </summary>
public event CancelEventHandler? CloseRequested;
/// <summary>
/// Gets or sets the top-level window of the application that is using the file that should be activated when requested. If this
/// value is <see langword="null"/>, then the calling application will be told that it cannot activate the file's owning application.
/// </summary>
/// <value>The activation window.</value>
public HWND ActivationWindow { get; set; }
/// <summary>Gets or sets the name of the application using the file.</summary>
/// <value>
/// The name of the application that can be passed to the user in a dialog box so that the user knows the source of the conflict and
/// can act accordingly. For instance "File.txt is in use by Litware.".
/// </value>
public string AppName { get => appName; set => appName = value; }
/// <summary>
/// Gets or sets the full path to the file that is in use by this application. Setting this value will revoke any prior file's
/// registration in the ROT and register this file there.
/// </summary>
/// <value>The full path to the file that is in use by this application. This value cannot be <see langword="null"/>.</value>
/// <exception cref="ArgumentNullException">FilePath</exception>
[MemberNotNull(nameof(filePath))]
public string FilePath
{
get => filePath ?? throw new InvalidOperationException();
set
{
if (!System.IO.File.Exists(filePath)) throw new System.IO.FileNotFoundException(null, filePath);
RevokeFromROT();
filePath = value;
RegisterInROT();
}
}
/// <summary>Gets or sets a value that indicates how the file in use is being used.</summary>
/// <value>The type of the file use.</value>
public FileUsageType FileUsageType { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the file specified in <see cref="FilePath"/> is registered in the Running Object Table.
/// </summary>
/// <value><see langword="true"/> if registered; otherwise, <see langword="false"/>.</value>
public bool Registered
{
get => regId != 0;
set
{
if (!value)
RevokeFromROT();
else if (regId == 0)
RegisterInROT();
}
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
HRESULT IFileIsInUse.CloseFile()
{
if (CloseFile is null) return HRESULT.E_NOTIMPL;
CloseFile.Invoke(this, EventArgs.Empty);
RevokeFromROT();
return HRESULT.S_OK;
}
HRESULT IFileIsInUse.GetAppName(out string ppszName)
{
ppszName = appName;
return HRESULT.S_OK;
}
HRESULT IFileIsInUse.GetCapabilities(out OF_CAP pdwCapFlags)
{
var cancelArgs = new CancelEventArgs(false);
CloseRequested?.Invoke(this, cancelArgs);
pdwCapFlags = (!cancelArgs.Cancel ? OF_CAP.OF_CAP_CANCLOSE : 0) | (ActivationWindow.IsNull ? 0 : OF_CAP.OF_CAP_CANSWITCHTO);
return HRESULT.S_OK;
}
HRESULT IFileIsInUse.GetSwitchToHWND(out HWND phwnd)
{
phwnd = ActivationWindow;
return HRESULT.S_OK;
}
HRESULT IFileIsInUse.GetUsage(out FILE_USAGE_TYPE pfut)
{
pfut = (FILE_USAGE_TYPE)FileUsageType;
return HRESULT.S_OK;
}
/// <summary>Releases unmanaged and - optionally - managed resources.</summary>
/// <param name="disposing">
/// <see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
RevokeFromROT();
}
disposedValue = true;
}
}
private static string DefAppName
{
get
{
var asm = System.Reflection.Assembly.GetEntryAssembly();
if (asm is not null)
{
object[] attrs = asm.GetCustomAttributes(typeof(System.Reflection.AssemblyProductAttribute), false);
if (attrs != null && attrs.Length > 0)
return ((System.Reflection.AssemblyProductAttribute)attrs[0]).Product;
return System.IO.Path.GetFileNameWithoutExtension(asm.Location);
}
return "Unknown";
}
}
private void RegisterInROT()
{
GetRunningObjectTable(0, out var rot).ThrowIfFailed();
using var prot = ComReleaserFactory.Create(rot);
CreateFileMoniker(FilePath, out moniker).ThrowIfFailed();
regId = rot.Register(ROTFLAGS.ROTFLAGS_REGISTRATIONKEEPSALIVE | ROTFLAGS.ROTFLAGS_ALLOWANYCLIENT, this, moniker!);
}
private void RevokeFromROT()
{
if (regId == 0) return;
GetRunningObjectTable(0, out var rot).ThrowIfFailed();
using var prot = ComReleaserFactory.Create(rot);
try { rot.Revoke(regId); } catch { }
regId = 0;
moniker = null;
}
}