using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using Vanara.InteropServices;
using Vanara.PInvoke;
using static Vanara.PInvoke.Ole32;
using static Vanara.PInvoke.Shell32;
namespace Vanara.Windows.Shell
{
/// Constants used to indicate how a file in use is being used.
///
/// 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.
///
public enum FileUsageType
{
/// The file is being played by the process that has it open.
Playing = FILE_USAGE_TYPE.FUT_PLAYING,
/// The file is being edited by the process that has it open.
Editing = FILE_USAGE_TYPE.FUT_EDITING,
///
/// The file is open in the process for an unspecified action or an action that does not readily fit into the other two categories.
///
Generic = FILE_USAGE_TYPE.FUT_GENERIC,
}
///
/// 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 ). It will revoke
/// that registration on disposal or when the property is set to .
///
/// 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.
///
/// openFiles =
/// new Dictionary();
///
///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);
/// }
///}]]>
///
///
///
public class FileInUseHandler : IFileIsInUse, IDisposable
{
private bool disposedValue;
private string appName, filePath;
private IMoniker moniker;
private uint regId;
/// Initializes a new instance of the class.
/// The file path.
/// The parent.
/// Type of the usage.
public FileInUseHandler(string filePath, HWND parent = default, FileUsageType usageType = FileUsageType.Generic)
{
ActivationWindow = parent;
FileUsageType = usageType;
FilePath = filePath;
appName = DefAppName;
}
/// Finalizes an instance of the class.
~FileInUseHandler()
{
Dispose(disposing: false);
}
/// Occurs after permission has been given to close the file and the file must now be closed.
public event EventHandler CloseFile;
///
/// 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 property to , then the requesting application will
/// be informed that it cannot take control.
///
public event CancelEventHandler CloseRequested;
///
/// 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 , then the calling application will be told that it cannot activate the file's owning application.
///
/// The activation window.
public HWND ActivationWindow { get; set; }
/// Gets or sets the name of the application using the file.
///
/// 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.".
///
public string AppName { get => appName; set => appName = value; }
///
/// 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.
///
/// The full path to the file that is in use by this application. This value cannot be .
/// FilePath
public string FilePath
{
get => filePath;
set
{
RevokeFromROT();
filePath = value ?? throw new ArgumentNullException(nameof(FilePath));
RegisterInROT();
}
}
/// Gets or sets a value that indicates how the file in use is being used.
/// The type of the file use.
public FileUsageType FileUsageType { get; set; }
///
/// Gets or sets a value indicating whether the file specified in is registered in the Running Object Table.
///
/// if registered; otherwise, .
public bool Registered
{
get => regId != 0;
set
{
if (!value)
RevokeFromROT();
else if (regId == 0)
RegisterInROT();
}
}
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
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;
}
/// Releases unmanaged and - optionally - managed resources.
///
/// to release both managed and unmanaged resources; to release only unmanaged resources.
///
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
RevokeFromROT();
}
disposedValue = true;
}
}
private 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, (object)(IFileIsInUse)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;
}
}
}