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; } } }