2020-06-22 15:39:13 -04:00
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
{
/// <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 ;
2022-01-06 17:11:41 -05:00
private string appName , filePath ;
2020-06-22 15:39:13 -04:00
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>
2022-01-06 17:11:41 -05:00
public FileInUseHandler ( string filePath , HWND parent = default , FileUsageType usageType = FileUsageType . Generic )
2020-06-22 15:39:13 -04:00
{
ActivationWindow = parent ;
FileUsageType = usageType ;
FilePath = filePath ;
2022-01-06 17:11:41 -05:00
appName = DefAppName ;
2020-06-22 15:39:13 -04:00
}
/// <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>
2022-01-06 17:11:41 -05:00
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 ; }
2020-06-22 15:39:13 -04:00
/// <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>
public string FilePath
{
get = > filePath ;
set
{
RevokeFromROT ( ) ;
filePath = value ? ? throw new ArgumentNullException ( nameof ( FilePath ) ) ;
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 )
{
2022-01-06 17:11:41 -05:00
ppszName = appName ;
2020-06-22 15:39:13 -04:00
return HRESULT . S_OK ;
}
HRESULT IFileIsInUse . GetCapabilities ( out OF_CAP pdwCapFlags )
{
var cancelArgs = new CancelEventArgs ( false ) ;
CloseRequested ? . Invoke ( this , cancelArgs ) ;
2022-01-06 17:11:41 -05:00
pdwCapFlags = ( ! cancelArgs . Cancel ? OF_CAP . OF_CAP_CANCLOSE : 0 ) | ( ActivationWindow . IsNull ? 0 : OF_CAP . OF_CAP_CANSWITCHTO ) ;
2020-06-22 15:39:13 -04:00
return HRESULT . S_OK ;
}
HRESULT IFileIsInUse . GetSwitchToHWND ( out HWND phwnd )
{
2022-01-06 17:11:41 -05:00
phwnd = ActivationWindow ;
2020-06-22 15:39:13 -04:00
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 ;
}
}
2022-01-06 17:11:41 -05:00
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" ;
}
}
2020-06-22 15:39:13 -04:00
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 ;
}
}
}