|
//---------------------------------------------------------------------------
//
// <copyright file="FileDialog.cs" company="Microsoft">
// Copyright (C) Microsoft Corporation. All rights reserved.
// </copyright>
//
// Description:
// FileDialog is an abstract class derived from CommonDialog
// that implements shared functionality common to both File
// Open and File Save common dialogs. It provides a hook
// procedure that handles messages received while the dialog
// is visible and numerous properties to control the appearance
// and behavior of the dialog.
// The actual call to display the dialog to GetOpenFileName()
// or GetSaveFileName() (both functions defined in commdlg.dll)
// is implemented in a derived class's RunFileDialog method.
//
// When running on Vista, the COM IFileDialog interfaces are used.
// Creation of the specific IFileOpenDialog and IFileSaveDialog are
// deferred to the derived classes.
//
//---------------------------------------------------------------------------
namespace Microsoft.Win32
{
using MS.Internal;
using MS.Internal.AppModel;
using MS.Internal.Interop;
using MS.Win32;
using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Threading;
using System.Runtime.Remoting;
using System.Windows;
using CharBuffer = MS.Win32.NativeMethods.CharBuffer;
using HRESULT = MS.Internal.Interop.HRESULT;
using SecurityHelper=MS.Internal.PresentationFramework.SecurityHelper;
/// <summary>
/// Provides a common base class for wrappers around both the
/// File Open and File Save common dialog boxes. Derives from
/// CommonDialog.
///
/// This class is not intended to be derived from except by
/// the OpenFileDialog and SaveFileDialog classes.
/// </summary>
public abstract class FileDialog : CommonDialog
{
//---------------------------------------------------
//
// Constructors
//
//---------------------------------------------------
#region Constructors
/// <summary>
/// In an inherited class, initializes a new instance of
/// the System.Windows.FileDialog class.
/// </summary>
/// <SecurityNote>
/// Critical: Sets Dialog options, which are critical for set.
/// TreatAsSafe: It is okay to set the options to their defaults. The
/// ctor does not show the dialog.
/// </SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
protected FileDialog()
{
// Call Initialize to set defaults for fields
// and to set defaults for some option flags.
// Initialize() is also called from the virtual
// Reset() function to restore defaults.
Initialize();
}
#endregion Constructors
//---------------------------------------------------
//
// Public Methods
//
//---------------------------------------------------
#region Public Methods
/// <summary>
/// Resets all properties to their default values.
/// Classes derived from FileDialog are expected to
/// call Base.Reset() at the beginning of their
/// implementation of Reset() if they choose to
/// override this function.
/// </summary>
/// <Remarks>
/// Callers must have FileIOPermission(PermissionState.Unrestricted) to call this API.
/// </Remarks>
/// <SecurityNote>
/// Critical: Sets Dialog options, which are critical for set.
/// PublicOk: Demands FileIOPermission (PermissionState.Unrestricted)
/// </SecurityNote>
[SecurityCritical]
public override void Reset()
{
SecurityHelper.DemandUnrestrictedFileIOPermission();
Initialize();
}
/// <summary>
/// Returns a string representation of the file dialog with key information
/// for debugging purposes.
/// </summary>
// We overload ToString() so that we can provide a useful representation of
// this object for users' debugging purposes. It provides the full pathname for
// any files selected.
public override string ToString()
{
StringBuilder sb = new StringBuilder(base.ToString() + ": Title: " + Title + ", FileName: ");
sb.Append(FileName);
return sb.ToString();
}
#endregion Public Methods
//---------------------------------------------------
//
// Public Properties
//
//---------------------------------------------------
#region Public Properties
//
// The behavior governed by this property depends
// on whether CheckFileExists is set and whether the
// filter contains a valid extension to use. For
// details, see the ProcessFileNames function.
//
// It's worth noting that unlike most of these
// properties, AddExtension is a custom flag that
// is unique to our implementation. As such, it is
// a constant value in our class, not stored in
// NativeMethods like the other flags.
/// <summary>
/// Gets or sets a value indicating whether the
/// dialog box automatically adds an extension to a
/// file name if the user omits the extension.
/// </summary>
/// <Remarks>
/// Callers must have FileIOPermission(PermissionState.Unrestricted) to call this API.
/// </Remarks>
/// <SecurityNote>
/// Critical: Dialog options are critical for set. (Only critical for set
/// because setting options affects the behavior of the FileDialog)
/// PublicOk: Demands FileIOPermission (PermissionState.Unrestricted)
/// </SecurityNote>
public bool AddExtension
{
get
{
return GetOption(OPTION_ADDEXTENSION);
}
[SecurityCritical]
set
{
SecurityHelper.DemandUnrestrictedFileIOPermission();
SetOption(OPTION_ADDEXTENSION, value);
}
}
//
// OFN_FILEMUSTEXIST is only used for Open dialog
// boxes, according to MSDN. It implies
// OFN_PATHMUSTEXIST and "cannot be used" with a
// Save As dialog box... in practice, it seems
// to be ignored when used with Save As boxes
/// <summary>
/// Gets or sets a value indicating whether
/// the dialog box displays a warning if the
/// user specifies a file name that does not exist.
/// </summary>
/// <Remarks>
/// Callers must have FileIOPermission(PermissionState.Unrestricted) to call this API.
/// </Remarks>
/// <SecurityNote>
/// Critical: Dialog options are critical for set. (Only critical for set
/// because setting options affects the behavior of the FileDialog)
/// PublicOk: Demands FileIOPermission (PermissionState.Unrestricted)
/// </SecurityNote>
public virtual bool CheckFileExists
{
get
{
return GetOption(NativeMethods.OFN_FILEMUSTEXIST);
}
[SecurityCritical]
set
{
SecurityHelper.DemandUnrestrictedFileIOPermission();
SetOption(NativeMethods.OFN_FILEMUSTEXIST, value);
}
}
/// <summary>
/// Specifies that the user can type only valid paths and file names. If this flag is
/// used and the user types an invalid path and file name in the File Name entry field,
/// a warning is displayed in a message box.
/// </summary>
/// <Remarks>
/// Callers must have FileIOPermission(PermissionState.Unrestricted) to call this API.
/// </Remarks>
/// <SecurityNote>
/// Critical: Dialog options are critical for set. (Only critical for set
/// because setting options affects the behavior of the FileDialog)
/// PublicOk: Demands FileIOPermission (PermissionState.Unrestricted)
/// </SecurityNote>
public bool CheckPathExists
{
get
{
return GetOption(NativeMethods.OFN_PATHMUSTEXIST);
}
[SecurityCritical]
set
{
SecurityHelper.DemandUnrestrictedFileIOPermission();
SetOption(NativeMethods.OFN_PATHMUSTEXIST, value);
}
}
/// <summary>
/// The AddExtension property attempts to determine the appropriate extension
/// by using the selected filter. The DefaultExt property serves as a fallback -
/// if the extension cannot be determined from the filter, DefaultExt will
/// be used instead.
/// </summary>
public string DefaultExt
{
get
{
// For string properties, it's important to not return null, as an empty
// string tends to make more sense to beginning developers.
return _defaultExtension == null ? String.Empty : _defaultExtension;
}
set
{
if (value != null)
{
// Use Ordinal here as per FxCop CA1307
if (value.StartsWith(".", StringComparison.Ordinal)) // Allow calling code to provide
// extensions like ".ext" -
{
value = value.Substring(1); // but strip out the period to leave only "ext"
}
else if (value.Length == 0) // Normalize empty strings to null.
{
value = null;
}
}
_defaultExtension = value;
}
}
// The actual flag is OFN_NODEREFERENCELINKS (set = do not dereference, unset = deref) -
// while we have true = dereference and false=do not dereference. Because we expose
// the opposite of the Windows flag as a property to be clearer, we need to negate
// the value in both the getter and the setter here.
/// <summary>
/// Gets or sets a value indicating whether the dialog box returns the location
/// of the file referenced by the shortcut or whether it returns the location
/// of the shortcut (.lnk).
/// </summary>
/// <Remarks>
/// Callers must have FileIOPermission(PermissionState.Unrestricted) to call this API.
/// </Remarks>
/// <SecurityNote>
/// Critical: Dialog options are critical for set. (Only critical for set
/// because setting options affects the behavior of the FileDialog)
/// PublicOk: Demands FileIOPermission (PermissionState.Unrestricted)
/// </SecurityNote>
public bool DereferenceLinks
{
get
{
return !GetOption(NativeMethods.OFN_NODEREFERENCELINKS);
}
[SecurityCritical]
set
{
SecurityHelper.DemandUnrestrictedFileIOPermission();
SetOption(NativeMethods.OFN_NODEREFERENCELINKS, !value);
}
}
/// <summary>
/// Gets a string containing the filename component of the
/// file selected in the dialog box.
///
/// Example: if FileName = "c:\windows\explorer.exe" ,
/// SafeFileName = "explorer.exe"
/// </summary>
/// <SecurityNote>
/// Critical: Do not want to allow access to raw paths to Parially Trusted Applications.
/// PublicOk: Scrubs paths from the file name.
/// </SecurityNote>
public string SafeFileName
{
[SecurityCritical]
get
{
// Use the FileName property to avoid directly accessing
// the _fileNames field, then call Path.GetFileName
// to do the actual work of stripping out the file name
// from the path.
string safeFN = Path.GetFileName(CriticalFileName);
// Check to make sure Path.GetFileName does not return null.
// If it does, set safeFN to String.Empty instead to accomodate
// programmers that fail to check for null when reading strings.
if (safeFN == null)
{
safeFN = String.Empty;
}
return safeFN;
}
}
/// <summary>
/// Gets a string array containing the filename of each file selected
/// in the dialog box.
/// </summary>
/// <SecurityNote>
/// Critical: Do not want to allow access to raw paths to Parially Trusted Applications.
/// PublicOk: Scrubs paths from the file names.
/// </SecurityNote>
public string[] SafeFileNames
{
[SecurityCritical]
get
{
// Retrieve the existing filenames into an array, then make
// another array of the same length to hold the safe version.
string[] unsafeFileNames = FileNamesInternal;
string[] safeFileNames = new string[unsafeFileNames.Length];
for (int i = 0; i < unsafeFileNames.Length; i++)
{
// Call Path.GetFileName to retrieve only the filename
// component of the current full path.
safeFileNames[i] = Path.GetFileName(unsafeFileNames[i]);
// Check to make sure Path.GetFileName does not return null.
// If it does, set this filename to String.Empty instead to accomodate
// programmers that fail to check for null when reading strings.
if (safeFileNames[i] == null)
{
safeFileNames[i] = String.Empty;
}
}
return safeFileNames;
}
}
// If multiple files are selected, we only return the first filename.
/// <summary>
/// Gets or sets a string containing the full path of the file selected in
/// the file dialog box.
/// </summary>
/// <Remarks>
/// Callers must have FileIOPermission(PermissionState.Unrestricted) to call this API.
/// </Remarks>
/// <SecurityNote>
/// Critical: Do not want to allow access to raw paths to Parially Trusted Applications.
/// PublicOk: Demands FileIOPermission (PermissionState.Unrestricted)
/// </SecurityNote>
public string FileName
{
[SecurityCritical]
get
{
SecurityHelper.DemandUnrestrictedFileIOPermission();
return CriticalFileName;
}
[SecurityCritical]
set
{
SecurityHelper.DemandUnrestrictedFileIOPermission();
// Allow users to set a filename to stored in _fileNames.
// If null is passed in, we clear the entire list.
// If we get a string, we clear the entire list and make a new one-element
// array with the new string.
if (value == null)
{
_fileNames = null;
}
else
{
//
_fileNames = new string[] { value };
}
}
}
/// <summary>
/// Gets the file names of all selected files in the dialog box.
/// </summary>
/// <Remarks>
/// Callers must have FileIOPermission(PermissionState.Unrestricted) to call this API.
/// </Remarks>
/// <SecurityNote>
/// Critical: Do not want to allow access to raw paths to Parially Trusted Applications.
/// PublicOk: Demands FileIOPermission (PermissionState.Unrestricted)
/// </SecurityNote>
public string[] FileNames
{
[SecurityCritical]
get
{
SecurityHelper.DemandUnrestrictedFileIOPermission();
// FileNamesInternal is a property we use to clone
// the string array before returning it.
string[] files = FileNamesInternal;
return files;
}
}
// The filter string also controls how the AddExtension feature behaves. For
// details, see the ProcessFileNames method.
/// <summary>
/// Gets or sets the current file name filter string,
/// which determines the choices that appear in the "Save as file type" or
/// "Files of type" box at the bottom of the dialog box.
///
/// This is an example filter string:
/// Filter = "Image Files(*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF|All files (*.*)|*.*"
/// </summary>
/// <exception cref="System.ArgumentException">
/// Thrown in the setter if the new filter string does not have an even number of tokens
/// separated by the vertical bar character '|' (that is, the new filter string is invalid.)
/// </exception>
/// <remarks>
/// If DereferenceLinks is true and the filter string is null, a blank
/// filter string (equivalent to "|*.*") will be automatically substituted to work
/// around the issue documented in Knowledge Base article 831559
/// Callers must have FileIOPermission(PermissionState.Unrestricted) to call this API.
/// </remarks>
public string Filter
{
get
{
// For string properties, it's important to not return null, as an empty
// string tends to make more sense to beginning developers.
return _filter == null ? String.Empty : _filter;
}
set
{
if (String.CompareOrdinal(value,_filter) != 0) // different filter than what we have stored already
{
string updatedFilter = value;
if (!String.IsNullOrEmpty(updatedFilter))
{
// Require the number of segments of the filter string to be even -
// in other words, there must only be matched pairs of description and
// file extensions.
//
// This implicitly requires there to be at least one vertical bar in
// the filter string - or else formats.Length will be 1, resulting in an
// ArgumentException.
string[] formats = updatedFilter.Split('|');
if (formats.Length % 2 != 0)
{
throw new ArgumentException(SR.Get(SRID.FileDialogInvalidFilter));
}
}
else
{ // catch cases like null or "" where the filter string is not invalid but
// also not substantive. We set value to null so that the assignment
// below picks up null as the new value of _filter.
updatedFilter = null;
}
_filter = updatedFilter;
}
}
}
// Using 1 as the index of the first filter entry is counterintuitive for C#/C++
// developers, but is a side effect of a Win32 feature that allows you to add a template
// filter string that is filled in when the user selects a file for future uses of the dialog.
// We don't support that feature, so only values >1 are valid.
//
// For details, see MSDN docs for OPENFILENAME Structure, nFilterIndex
/// <summary>
/// Gets or sets the index of the filter currently selected in the file dialog box.
///
/// NOTE: The index of the first filter entry is 1, not 0.
/// </summary>
public int FilterIndex
{
get
{
return _filterIndex;
}
set
{
_filterIndex = value;
}
}
/// <summary>
/// Gets or sets the initial directory displayed by the file dialog box.
/// </summary>
/// <Remarks>
/// Callers must have FileIOPermission(PermissionState.Unrestricted) to call this API.
/// </Remarks>
/// <SecurityNote>
/// Critical: Don't want to allow setting of the initial directory in Partial Trust.
/// PublicOk: Demands FileIOPermission (PermissionState.Unrestricted)
/// </SecurityNote>
public string InitialDirectory
{
get
{
// Avoid returning a null string - return String.Empty instead.
return _initialDirectory.Value == null ? String.Empty : _initialDirectory.Value;
}
[SecurityCritical]
set
{
SecurityHelper.DemandUnrestrictedFileIOPermission();
_initialDirectory.Value = value;
}
}
/// <summary>
/// Restores the current directory to its original value if the user
/// changed the directory while searching for files.
///
/// This property is only valid for SaveFileDialog; it has no effect
/// when set on an OpenFileDialog.
/// </summary>
/// <Remarks>
/// Callers must have FileIOPermission(PermissionState.Unrestricted) to call this API.
/// </Remarks>
/// <SecurityNote>
/// Critical: Dialog options are critical for set.
/// PublicOk: Demands FileIOPermission (PermissionState.Unrestricted)
/// </SecurityNote>
public bool RestoreDirectory
{
get
{
return GetOption(NativeMethods.OFN_NOCHANGEDIR);
}
[SecurityCritical]
set
{
SecurityHelper.DemandUnrestrictedFileIOPermission();
SetOption(NativeMethods.OFN_NOCHANGEDIR, value);
}
}
/// <summary>
/// Gets or sets a string shown in the title bar of the file dialog.
/// If this property is null, a localized default from the operating
/// system itself will be used (typically something like "Save As" or "Open")
/// </summary>
/// <Remarks>
/// Callers must have FileIOPermission(PermissionState.Unrestricted) to call this API.
/// </Remarks>
/// <SecurityNote>
/// Critical: Do not want to allow setting the FileDialog title from a Partial Trust application.
/// PublicOk: Demands FileIOPermission (PermissionState.Unrestricted)
/// </SecurityNote>
public string Title
{
get
{
// Avoid returning a null string - return String.Empty instead.
return _title.Value == null ? String.Empty : _title.Value;
}
[SecurityCritical]
set
{
SecurityHelper.DemandUnrestrictedFileIOPermission();
_title.Value = value;
}
}
// If false, the file dialog boxes will allow invalid characters in the returned file name.
// We are actually responsible for dealing with this flag - it determines whether all of the
// processing in ProcessFileNames (which includes things such as the AddExtension feature)
// occurs.
/// <summary>
/// Gets or sets a value indicating whether the dialog box accepts only valid
/// Win32 file names.
/// </summary>
/// <Remarks>
/// Callers must have FileIOPermission(PermissionState.Unrestricted) to call this API.
/// </Remarks>
/// <SecurityNote>
/// Critical: Dialog options are critical for set. (Only critical for set
/// because setting options affects the behavior of the FileDialog)
/// PublicOk: Demands FileIOPermission (PermissionState.Unrestricted)
/// </SecurityNote>
public bool ValidateNames
{
get
{
return !GetOption(NativeMethods.OFN_NOVALIDATE);
}
[SecurityCritical]
set
{
SecurityHelper.DemandUnrestrictedFileIOPermission();
SetOption(NativeMethods.OFN_NOVALIDATE, !value);
}
}
#endregion Public Properties
//---------------------------------------------------
//
// Public Events
//
//---------------------------------------------------
#region Public Events
/// <summary>
/// Occurs when the user clicks on the Open or Save button on a file dialog
/// box.
/// </summary>
// We fire this event from DoFileOk.
public event CancelEventHandler FileOk;
#endregion Public Events
//---------------------------------------------------
//
// Protected Methods
//
//---------------------------------------------------
#region Protected Methods
/// <summary>
/// Defines the common dialog box hook procedure that is overridden to add
/// specific functionality to the file dialog box.
/// </summary>
/// <SecurityNote>
/// Critical: due to calls to GetParent and PtrToStructure in UnsafeNativeMethods
/// as well as a call to DoFileOk, which is SecurityCritical.
/// </SecurityNote>
[SecurityCritical]
protected override IntPtr HookProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam)
{
// Assume we are successful unless we encounter a problem.
IntPtr returnValue = IntPtr.Zero;
// Our File Dialogs are Explorer-style dialogs with hook procedure enabled
// (OFN_ENABLEHOOK | OFN_EXPLORER). As such, we will get the following
// messages: (as per MSDN)
//
// WM_INITDIALOG
// WM_NOTIFY (indicating actions taken by the user or other dialog box events)
// Messages for any additional controls defined by specifying a child dialog template
switch ((WindowMessage)msg)
{
case WindowMessage.WM_NOTIFY:
// Our hookproc is actually the hook procedure for a child template hosted
// inside the actual file dialog box. We want the hwnd of the actual dialog,
// so we call GetParent on the hwnd passed to the hookproc.
_hwndFileDialog = UnsafeNativeMethods.GetParent(new HandleRef(this, hwnd));
// When we receive WM_NOTIFY, lParam is a pointer to an OFNOTIFY
// structure that defines the action. OFNOTIFY is a structure
// specific to file open and save dialogs with three members:
// (defined in Commdlg.h - see MSDN for more details)
//
// struct _OFNOTIFY {
// NMHDR hdr; // this is a by-value structure;
// // the implementation in UnsafeNativeMethods breaks it into
// // hdr_hwndFrom (HWND, handle to control sending message),
// // hdr_idFrom (UINT, ID of control sending message) and
// // hdr_code (UINT, one of the CDN_??? notification constants)
//
// LPOPENFILENAME lpOFN; // pointer to the OPENFILENAME structure we created in
// // RunFileDialog when showing this dialog box.
//
// LPTSTR pszFile; // if a network sharing violation has occurred, this
// // is the name of the file affected. Only valid with
// // hdr_code = CDN_SHAREVIOLATION.
// }
//
// Convert the pointer to our OFNOTIFY stored in lparam to an object using PtrToStructure.
NativeMethods.OFNOTIFY notify = (NativeMethods.OFNOTIFY)UnsafeNativeMethods.PtrToStructure(lParam, typeof(NativeMethods.OFNOTIFY));
// WM_NOTIFY indicates that the dialog is sending us a notification message.
// notify.hdr_code is an int defining which notification is being received.
// These codes are integer constants defined originally in commdlg.h.
switch (notify.hdr_code)
{
case NativeMethods.CDN_INITDONE:
// CDN_INITDONE is sent by Explorer-style file dialogs when the
// system has finished arranging the controls in the dialog box.
//
// We use this opportunity to move the dialog box to the center
// of the appropriate monitor.
//
// As an aside, this only seems to work the first time we show
// a dialog - after that, Windows remembers the position of the
// dialog. But that's the Microsoft behavior too, so it's fine.
MoveToScreenCenter(new HandleRef(this, _hwndFileDialog));
break;
case NativeMethods.CDN_SELCHANGE:
// CDN_SELCHANGE is sent by Explorer-style file dialogs when the
// selection changes in the list box that displays the contents
// of the currently opened folder or directory.
//
// When we get this message, we check to make sure our character
// buffer is big enough to hold all of the filenames that have
// been selected. If it isn't, we create a new, bigger buffer
// and substitute it in the OPENFILENAME structure.
// Retrieve the OPENFILENAME structure from the OFNOTIFY structure
// so we can access the CharBuffer inside it.
NativeMethods.OPENFILENAME_I ofn = (NativeMethods.OPENFILENAME_I)
UnsafeNativeMethods.PtrToStructure(notify.lpOFN, typeof(NativeMethods.OPENFILENAME_I));
// Get the buffer size required to store the selected file names.
// We would like to accomplish this by sending a CDM_GETFILEPATH message
// - to which the file dialog responds with the number of unicode
// characters needed to store the file names and paths.
//
// Windows Forms used CDM_GETSPEC here, but that only retrieves the length
// of the filenames - not of the complete path. So in cases with network
// shortcuts and dereference links enabled, we end up with not enough buffer
// and an FNERR_BUFFERTOOSMALL error.
//
// Unfortunately, CDM_GETFILEPATH returns -1 when a bunch of files are
// selected, so changing to it actually makes things worse with very large
// cases. So we'll stick with CDM_GETSPEC plus extra buffer space.
//
int sizeNeeded = (int)UnsafeNativeMethods.UnsafeSendMessage(_hwndFileDialog, // hWnd of window to receive message
(WindowMessage)NativeMethods.CDM_GETSPEC, // Msg (message to send)
IntPtr.Zero, // wParam (additional info)
IntPtr.Zero); // lParam (additional info)
if (sizeNeeded > ofn.nMaxFile)
{
// A bigger buffer is required, so we'll allocate a new
// CharBuffer and substitute it for the existing one.
//try
//{
// Make the new buffer equal to the size the dialog told us we needed
// plus a reasonable growth factor.
int newBufferSize = sizeNeeded + (FILEBUFSIZE / 4);
// Allocate a new CharBuffer in the appropriate size.
CharBuffer charBufferTmp = CharBuffer.CreateBuffer(newBufferSize);
// Allocate unmanaged memory for the buffer and store the pointer.
IntPtr newBuffer = charBufferTmp.AllocCoTaskMem();
// Free the old, smaller buffer stored in ofn.lpstrFile
Marshal.FreeCoTaskMem(ofn.lpstrFile);
// Substitute buffer and update the buffer maximum size in
// the dialog.
ofn.lpstrFile = newBuffer;
ofn.nMaxFile = newBufferSize;
// Store the reference to the character buffer inside our
// class so we can free it when we're done.
this._charBuffer = charBufferTmp;
// Marshal the OPENFILENAME structure back into the
// OFNOTIFY structure, then marshal the OFNOTIFY structure
// back into lparam to update the dialog.
Marshal.StructureToPtr(ofn, notify.lpOFN, true);
Marshal.StructureToPtr(notify, lParam, true);
// }
// Windows Forms had a catch-all exception handler here
// but no justification for why it existed. If exceptions
// are thrown when we grow the buffer, re-add this catch
// and perform handling specific to the exception you are seeing.
//
// I don't see anywhere an exception would be thrown that
// we would want to simply discard in this try block, so
// we'll remove this catch and let any exceptions through.
//
// catch (Exception)
// {
// intentionally not throwing here.
// }
}
break;
case NativeMethods.CDN_SHAREVIOLATION:
// CDN_SHAREVIOLATION is sent by Explorer-style boxes when OK is clicked
// and a network sharing violation occurs for the selected file.
// Network sharing violation is a bit misleading of a term - it could
// also mean the user doesn't have permissions for the file, or it could mean
// the file is already opened by another process on the same machine.
//
// We process this message because of some odd behavior seen when a file
// is locked for writing. (for details, see VS Whidbey 95342)
//
// We get this notification followed by *two* CDN_FILEOK notifications... but only
// if the path is entered in the textbox and not selected from the folder view.
//
// If we get a CDN_SHAREVIOLATION, we'll set a flag and a counter so we can track
// which CDN_FILEOK notification we're on to avoid showing two message boxes.
this._ignoreSecondFileOkNotification = true; // We want to ignore the second CDN_FILEOK
this._fileOkNotificationCount = 0; // to avoid a second prompt by PromptFileOverwrite.
break;
case NativeMethods.CDN_FILEOK:
// CDN_FILEOK is sent when the user specifies a filename and clicks OK.
// We need to process the files selected and make sure everything's acceptable.
// If it's all OK, we don't need to do anything.
//
// To tell the dialogs to stay open after we receive a CDN_FILEOK, we must both
// return a non-zero value from this hook procedure and call SetWindowLong to
// set a nonzero value for DWL_MSGRESULT.
// --- Begin VS Whidbey 95342 Workaround ---
// See the CDN_SHAREVIOLATION case above for background info about this issue.
if (this._ignoreSecondFileOkNotification)
{
// We got a CDN_SHAREVIOLATION notification and want to ignore the second CDN_FILEOK notification.
// We'll allow the first one through and block the second.
// Recall that we initialize _fileOkNotificationCount to 0 when we get the CDN_SHAREVIOLATION.
if (this._fileOkNotificationCount == 0)
{
// This is the first CDN_FILEOK, record that we received
// it and then allow DoFileOk to be called.
this._fileOkNotificationCount = 1;
}
else
{
// This is the second CDN_FILEOK, so we want to ignore it.
this._ignoreSecondFileOkNotification = false;
// Call SetWindowLong to set the DWL_MSGRESULT value of the file dialog window
// to a non-zero number to tell the dialog to stay open.
// NativeMethods.InvalidIntPtr is defined as -1.
UnsafeNativeMethods.CriticalSetWindowLong(new HandleRef(this, hwnd), // hWnd (which window are we affecting)
NativeMethods.DWL_MSGRESULT, // nIndex (which value are we setting)
NativeMethods.InvalidIntPtr); // dwNewLong (what is the new value)
// We also need to return a non-zero value to tell the dialog to stay open.
returnValue = NativeMethods.InvalidIntPtr;
break;
}
}
// --- End VS Whidbey 95342 Workaround ---
// Call DoFileOk to check if the files that have been selected
// are acceptable. (See DoFileOk for details.)
//
// If it returns false, we must notify the dialog box that it
// needs to stay open for further input.
if (!DoFileOk(notify.lpOFN))
{
// Call SetWindowLong to set the DWL_MSGRESULT value of the file dialog window
// to a non-zero number to tell the dialog to stay open.
// NativeMethods.InvalidIntPtr is defined as -1.
UnsafeNativeMethods.CriticalSetWindowLong(new HandleRef(this, hwnd), // hWnd (which window are we affecting)
NativeMethods.DWL_MSGRESULT, // nIndex (which value are we setting)
NativeMethods.InvalidIntPtr); // dwNewLong (what is the new value)
// We also need to return a non-zero value to tell the dialog to stay open.
returnValue = NativeMethods.InvalidIntPtr;
break;
}
break;
}
break;
default:
returnValue = base.HookProc(hwnd, msg, wParam, lParam);
break;
}
// Return IntPtr.Zero to indicate success, unless we have
// adjusted the return value elsewhere in the function.
return returnValue;
}
/// <summary>
/// Raises the System.Windows.FileDialog.FileOk event.
/// </summary>
protected void OnFileOk(CancelEventArgs e)
{
if (FileOk != null)
{
FileOk(this, e);
}
}
// Because this class, FileDialog, is the parent class for both OpenFileDialog
// and SaveFileDialog, this function will perform the common setup tasks
// shared between Open and Save, and will then call RunFileDialog, which is
// overridden in both of the derived classes to show the correct dialog.
// Both derived classes know about the COM IFileDialog interfaces and can
// display those if they're available and there aren't any properties set
// that should cause us not to.
//
/// <summary>
/// Performs initialization work in preparation for calling RunFileDialog
/// to show a file open or save dialog box.
/// </summary>
/// <SecurityNote>
/// Critical: Calls Critical RunLegacyDialog and RunVistaDialog.
/// </SecurityNote>
[SecurityCritical]
protected override bool RunDialog(IntPtr hwndOwner)
{
// Verify thread access here. Generally we'd want to enforce thread affinity
// but barring that we really don't want to let multiple instances of this object
// display native dialogs.
// On XP, we have a buffer used in the hook that can get corrupted with multi-thread access.
if (UseVistaDialog)
{
return RunVistaDialog(hwndOwner);
}
return RunLegacyDialog(hwndOwner);
}
/// <summary>
/// Performs initialization work in preparation for calling RunFileDialog
/// to show a file open or save dialog box.
/// </summary>
/// <SecurityNote>
/// Critical: Calls UnsafeNativeMethods.SetWindowPos() accesses SecurityCritical data
/// _charBuffer.
/// </SecurityNote>
[SecurityCritical]
private bool RunLegacyDialog(IntPtr hwndOwner)
{
// Once we run the dialog, all of our communication with it is handled
// by processing WM_NOTIFY messages in our hook procedure, this.HookProc.
// NativeMethods.WndProc is a delegate with the appropriate signature
// needed for a Win32 window hook procedure.
NativeMethods.WndProc hookProcPtr = new NativeMethods.WndProc(this.HookProc);
// Create a new OPENFILENAME structure. OPENFILENAME is a structure defined
// in Win32's commdlg.h that contains most of the information needed to
// successfully display a file dialog box.
// NOTE: Despite the name, OPENFILENAME is the proper structure for both
// file open and file save dialogs.
NativeMethods.OPENFILENAME_I ofn = new NativeMethods.OPENFILENAME_I();
// do everything in a try block, so we always free memory in the finalizer
try
{
// Create an appropriately sized buffer to hold the filenames.
// The buffer's initial size is controlled by the FILEBUFSIZE constant,
// an arbitrary value chosen so that we will rarely have to grow the buffer.
_charBuffer = CharBuffer.CreateBuffer(FILEBUFSIZE);
// If we have a filename stored in our internal array _fileNames,
// place it in the buffer as a default filename.
if (_fileNames != null)
{
_charBuffer.PutString(_fileNames[0]);
}
// --- Set up the OPENFILENAME structure ---
// lStructSize
// Specifies the length, in bytes, of the structure.
ofn.lStructSize = Marshal.SizeOf(typeof(NativeMethods.OPENFILENAME_I));
// hwndOwner
// Handle to the window that owns the dialog box. This member can be any
// valid window handle, or it can be NULL if the dialog box has no owner.
ofn.hwndOwner = hwndOwner;
// hInstance
// This property is ignored unless OFN_ENABLETEMPLATEHANDLE or
// OFN_ENABLETEMPLATE are set. Since we do not set either,
// hInstance is ignored, so we can set it to zero.
ofn.hInstance = IntPtr.Zero;
// lpstrFilter
// Pointer to a buffer containing pairs of null-terminated filter strings.
// The last string in the buffer must be terminated by two NULL characters.
// Since our filter strings are stored terminated by vertical bar '|' chars,
// we call MakeFilterString to reformat and validate the filter string.
ofn.lpstrFilter = MakeFilterString(_filter, this.DereferenceLinks);
// nFilterIndex
// Specifies the index of the currently selected filter in the File Types
// control. Note that since 0 is reserved for a custom filter (which we
// do not support), our valid filter indexes begin at 1.
ofn.nFilterIndex = _filterIndex;
// lpstrFile
// Pointer to a buffer used to store filenames. When initializing the
// dialog, this name is used as an initial value in the File Name edit
// control. When files are selected and the function returns, the buffer
// contains the full path to every file selected.
ofn.lpstrFile = _charBuffer.AllocCoTaskMem();
// nMaxFile
// Size of the lpstrFile buffer in number of Unicode characters.
ofn.nMaxFile = _charBuffer.Length;
// lpstrInitialDir
// Pointer to a null terminated string that can specify the initial directory.
// A relatively complex algorithm is used to determine which directory is
// actually used as the initial directory - for details, see MSDN for the
// OPENFILENAME structure.
ofn.lpstrInitialDir = _initialDirectory.Value;
// lpstrTitle
// Pointer to a string to be placed in the title bar of the dialog box.
// NULL causes the title bar to display the operating system default string.
ofn.lpstrTitle = _title.Value;
// Flags
// A set of bit flags you can use to initialize the dialog box.
// Most of these will be set through public properties that then call
// GetOption or SetOption. We retrieve the flags using the Options property
// and then add three additional flags here:
//
// OFN_EXPLORER
// display an Explorer-style box (newer style)
// OFN_ENABLEHOOK
// enable the hook procedure (important for much of our functionality)
// OFN_ENABLESIZING
// allow the user to resize the dialog box
//
ofn.Flags = Options | (NativeMethods.OFN_EXPLORER |
NativeMethods.OFN_ENABLEHOOK |
NativeMethods.OFN_ENABLESIZING);
// lpfnHook
// Pointer to the hook procedure.
// Ignored unless OFN_ENABLEHOOK is set in Flags.
ofn.lpfnHook = hookProcPtr;
// FlagsEx
// Can be either zero or OFN_EX_NOPLACESBAR, depending on whether
// the Places Bar (My Computer/Favorites/etc) should be shown on the
// left side of the file dialog.
ofn.FlagsEx = NativeMethods.OFN_USESHELLITEM;
// lpstrDefExt
// Pointer to a buffer that contains the default extension; it will
// be appended to filenames if the user does not type an extension.
// Only the first three characters are appended by Windows. If this
// is NULL, no extension is appended.
if (_defaultExtension != null && AddExtension)
{
ofn.lpstrDefExt = _defaultExtension;
}
// Call into either OpenFileDialog or SaveFileDialog to show the
// actual dialog box. This call blocks until the dialog is closed;
// while dialog is open, all interaction is through HookProc.
return RunFileDialog(ofn);
}
finally
{
// Explicitly set the character buffer to null.
_charBuffer = null;
// If there is still a pointer to a memory location in
// ofn.lpstrFile, we explicitly free that memory here.
if (ofn.lpstrFile != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(ofn.lpstrFile);
}
}
}
#endregion Protected Methods
//---------------------------------------------------
//
// Internal Methods
//
//---------------------------------------------------
#region Internal Methods
/// <summary>
/// Returns the state of the given options flag.
/// </summary>
internal bool GetOption(int option)
{
return (_dialogOptions.Value & option) != 0;
}
/// <summary>
/// Sets the given option to the given boolean value.
/// </summary>
/// <SecurityNote>
/// Critical: Setting a SecurityCriticalDataForSet member (_dialogOptions).
/// </SecurityNote>
[SecurityCritical]
internal void SetOption(int option, bool value)
{
if (value)
{
// if value is true, bitwise OR the option with _dialogOptions
_dialogOptions.Value |= option;
}
else
{
// if value is false, AND the bitwise complement of the
// option with _dialogOptions
_dialogOptions.Value &= ~option;
}
}
/// <summary>
/// Prompts the user with a System.Windows.MessageBox
/// with the given parameters. It also ensures that
/// the focus is set back on the window that had
/// the focus to begin with (before we displayed
/// the MessageBox).
///
/// Returns the choice the user made in the message box
/// (true if MessageBoxResult.Yes,
/// false if OK or MessageBoxResult.No)
///
/// We have to do this instead of just calling MessageBox because
/// of an issue where keyboard navigation would fail after showing
/// a message box. See http://bugcheck/default.asp?URL=/Bugs/URT/84016.asp
/// (Microsoft ASURT 80262)
/// </summary>
/// <SecurityNote>
/// Critical: We call GetFocus() and SetFocus() in
/// UnsafeNativeMethods, which are marked SupressUnmanagedCodeSecurity.
/// </SecurityNote>
[SecurityCritical]
internal bool MessageBoxWithFocusRestore(string message,
MessageBoxButton buttons,
MessageBoxImage image)
{
bool ret = false;
// Get the window that currently has focus and temporarily cache a handle to it
IntPtr focusHandle = UnsafeNativeMethods.GetFocus();
try
{
// Show the message box and compare the return value to MessageBoxResult.Yes to get the
// actual return value.
ret = (MessageBox.Show(message, DialogCaption, buttons, image, MessageBoxResult.OK /*default button is OK*/, 0)
==
MessageBoxResult.Yes);
}
finally
{
// Return focus to the window that had focus before we showed the messagebox.
// SetFocus can handle improper hwnd values, including null.
UnsafeNativeMethods.SetFocus(new HandleRef(this, focusHandle));
}
return ret;
}
/// <summary>
/// PromptUserIfAppropriate is a virtual function that shows any prompt
/// message boxes (like "Do you want to overwrite this file") necessary after
/// the Open button is pressed in a file dialog.
///
/// Return value is false if we showed a dialog box and true if we did not.
/// (in other words, true if it's OK to continue with the open process and
/// false if we need to return the user to the dialog to make another selection.)
/// </summary>
/// <remarks>
/// SaveFileDialog overrides this method to add additional message boxes for
/// its unique properties.
///
/// For FileDialog:
/// If OFN_FILEMUSTEXIST is set, we check to be sure the path passed in on the
/// fileName parameter exists as an actual file on the hard disk. If so, we
/// call PromptFileNotFound to inform the user that they must select an actual
/// file that already exists.
/// </remarks>
/// <SecurityNote>
/// Critical: due to call to PromptFileNotFound, which displays a message box with focus restore.
/// Asserts FileIOPermission in order to determine whether the file exists.
/// </SecurityNote>
[SecurityCritical]
internal virtual bool PromptUserIfAppropriate(string fileName)
{
bool fileExists = true;
// The only option we deal with in this implementation of
// PromptUserIfAppropriate is OFN_FILEMUSTEXIST.
if (GetOption(NativeMethods.OFN_FILEMUSTEXIST))
{
try
{
// File.Exists requires a full path, so we call GetFullPath on
// the filename before checking if it exists.
(new FileIOPermission(FileIOPermissionAccess.Read | FileIOPermissionAccess.PathDiscovery, fileName)).Assert();
try
{
string tempPath = Path.GetFullPath(fileName);
fileExists = File.Exists(tempPath);
}
finally
{
CodeAccessPermission.RevertAssert();
}
}
// FileIOPermission constructor will throw on invalid paths.
catch (PathTooLongException)
{
fileExists = false;
}
if (!fileExists)
{
// file does not exist, we can't continue
// and must display an error
// Display the message box
PromptFileNotFound(fileName);
}
}
return fileExists;
}
/// <summary>
/// Implements the actual call to GetOpenFileName or GetSaveFileName.
/// </summary>
internal abstract bool RunFileDialog(NativeMethods.OPENFILENAME_I ofn);
#endregion Internal Methods
//---------------------------------------------------
//
// Internal Properties
//
//---------------------------------------------------
#region Internal Properties
/// <summary>
/// In cases where we need to return an array of strings, we return
/// a clone of the array. We also need to make sure we return a
/// string[0] instead of a null if we don't have any filenames.
/// </summary>
/// <SecurityNote>
/// Critical: Accesses _fileNames, which is SecurityCritical.
/// </SecurityNote>
internal string[] FileNamesInternal
{
[SecurityCritical]
get
{
if (_fileNames == null)
{
return new string[0];
}
else
{
return (string[])_fileNames.Clone();
}
}
}
#endregion Internal Properties
//---------------------------------------------------
//
// Internal Events
//
//---------------------------------------------------
//#region Internal Events
//#endregion Internal Events
//---------------------------------------------------
//
// Private Methods
//
//---------------------------------------------------
#region Private Methods
/// <summary>
/// Processes the CDN_FILEOK notification, which is sent by an
/// Explorer-style Open or Save As dialog box when the user specifies
/// a file name and clicks the OK button.
/// </summary>
/// <returns>
/// true if the dialog can close, or false if we need to return to
/// the dialog for additional input.
/// </returns>
/// <SecurityNote>
/// Critical due to call access to _charBuffer, _fileNames and _dialogOptions.
/// </SecurityNote>
[SecurityCritical]
private bool DoFileOk(IntPtr lpOFN)
{
NativeMethods.OPENFILENAME_I ofn = (NativeMethods.OPENFILENAME_I)UnsafeNativeMethods.PtrToStructure(lpOFN, typeof(NativeMethods.OPENFILENAME_I));
// While processing the results we get from the OPENFILENAME struct,
// we will adjust several properties of our own class to reflect the
// new data. In case we discover we need to send the user back to
// the dialog for further input, we need to be able to revert these
// changes - so we backup _dialogOptions, _filterIndex and _fileNames.
//
// We only assign brand new string arrays to _FileNames, so it's OK
// to back up by reference here.
int saveOptions = _dialogOptions.Value;
int saveFilterIndex = _filterIndex;
string[] saveFileNames = _fileNames;
// ok is a flag to determine whether we need to show the dialog
// again (false) or if we're satisfied with the results we received (true).
bool ok = false;
try
{
// Replace the ReadOnly flag in DialogOptions with the ReadOnly flag
// from the OPENFILEDIALOG structure - that is, store the user's
// choice from the Read Only checkbox so our property is up to date.
_dialogOptions.Value = _dialogOptions.Value & ~NativeMethods.OFN_READONLY |
ofn.Flags & NativeMethods.OFN_READONLY;
// Similarly, update the filterIndex to reflect the selected filter.
_filterIndex = ofn.nFilterIndex;
// Ask the character buffer to copy the memory from the location
// referenced by lpstrFile into our internal character buffer.
_charBuffer.PutCoTaskMem(ofn.lpstrFile);
if (!GetOption(NativeMethods.OFN_ALLOWMULTISELECT))
{
// Since we're selecting a single file, make a string
// array with a single element containing the entire contents
// of the character buffer.
_fileNames = new string[] { _charBuffer.GetString() };
}
else
{
// Multiselect is a bit more complex - call GetMultiselectFiles
// to handle that case.
_fileNames = GetMultiselectFiles(_charBuffer);
}
// Call ProcessFileNames() to do validation and post-processing
// tasks (see that function for details; it checks if files exist,
// prompts users with message boxes if invalid selections are made, etc.)
if (ProcessFileNames())
{
// ProcessFileNames returned true, so it's OK to fire the
// OnFileOk event.
CancelEventArgs ceevent = new CancelEventArgs();
OnFileOk(ceevent);
// We allow our calling code to do even more post-processing
// through the OnFileOk event - and therefore offer them the
// opportunity to redisplay the dialog for additional input
// using the event arguments if their validation failed.
//
// If OnFileOk is not handled, ceevent.Cancel will be false.
ok = !ceevent.Cancel;
}
}
finally
{
// No matter what happened, we need to restore dialog state
// if the result was not ok=true.
if (!ok)
{
_dialogOptions.Value = saveOptions;
_filterIndex = saveFilterIndex;
_fileNames = saveFileNames;
}
}
return ok;
}
/// <summary>
/// Extracts the filename(s) returned by the file dialog.
/// </summary>
/// Marked static for perf reasons because this function doesn't
/// actually access any instance data as per FxCop CA1822.
private static string[] GetMultiselectFiles(CharBuffer charBuffer)
{
// Iff OFN_ALLOWMULTISELECT is set for an Explorer-style dialog box
// and the user selects multiple files, lpstrFile points to a string
// containing the current directory, followed by a NULL, followed by
// two or more filenames that are NULL separated, with an extra NULL
// character after the last filename.
//
// We'll use the GetString() function of the character buffer to get
// two of these null-terminated chunks at a time, one into directory
// and one into filename.
string directory = charBuffer.GetString();
string fileName = charBuffer.GetString();
// If OFN_ALLOWMULTISELECT is enabled but the user selects only
// one file, we get the filename and path concatenated together without
// a null separator. This will cause our directory variable to
// contain the full path and fileName to be empty, so make a new
// string array with the contents of directory as its single element.
//
if (fileName.Length == 0)
{
return new string[] { directory };
}
// If the directory was provided without a directory separator
// character (typically '\' on Windows) at the end, we add it.
if (!directory.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal))
{
directory = directory + Path.DirectorySeparatorChar;
}
// Create a generic list of strings to hold the names.
List<string> names = new List<string>();
do
{
// With DereferenceLinks enabled, we can sometimes end
// up with full paths provided as filenames. We need
// to check for two cases here - the case where the
// filename begins with '\', indicating a UNC share path,
// or the case where we have a full hard disk path
// (e.g. C:\file.txt), where [1] will be the : volume
// separator and [2] will be the \ directory separator.
bool isUncPath = (fileName[0] == Path.DirectorySeparatorChar && fileName[1] == Path.DirectorySeparatorChar);
bool isFullPath = (fileName.Length > 3 &&
fileName[1] == Path.VolumeSeparatorChar &&
fileName[2] == Path.DirectorySeparatorChar );
if (!(isUncPath || isFullPath))
{
// filename is not a full path, so we need to
// add on the directory
fileName = directory + fileName;
}
names.Add(fileName);
// Get the next filename
fileName = charBuffer.GetString();
} while (!String.IsNullOrEmpty(fileName));
return names.ToArray();
}
// Provides the actual implementation of initialization tasks.
// Initialize() is called from both the constructor and the
// public Reset() function to set default values for member
// variables and for the options bitmask.
/// <SecurityNote>
/// Critical: Sets Dialog options, which are critical for set.
/// </SecurityNote>
[SecurityCritical]
private void Initialize()
{
//
// Initialize Options Flags
//
_dialogOptions.Value = 0; // _dialogOptions is an int containing a set of
// bit flags used to initialize the dialog box.
// It is placed directly into the OPENFILEDIALOG
// struct used to instantiate the file dialog box.
// Within our code, we only use GetOption and SetOption
// (change from Windows Forms, which sometimes directly
// modified _dialogOptions). As such, we initialize to 0
// here and then call SetOption to get _dialogOptions
// into the default state.
//
// Set some default options
//
// - Hide the Read Only check box.
SetOption(NativeMethods.OFN_HIDEREADONLY, true);
// - Specifies that the user can type only valid paths and file names. If this flag is
// used and the user types an invalid path and file name in the File Name entry field,
// we will display a warning in a message box.
SetOption(NativeMethods.OFN_PATHMUSTEXIST, true);
// - This is our own flag, not a standard one defined in OPENFILEDIALOG. We use this to
// indicate to ourselves that we should add the default extension automatically if the
// user does not enter it in themselves in ProcessFileNames. (See that function for
// details.)
SetOption(OPTION_ADDEXTENSION, true);
//
// Initialize additional properties
//
_title.Value = null;
_initialDirectory.Value = null;
_defaultExtension = null;
_fileNames = null;
_filter = null;
_filterIndex = 1; // The index of the first filter entry is 1, not 0.
// 0 is reserved for the custom filter functionality
// provided by Windows, which we do not expose to the user.
// Variables used for bug workaround:
// When the selected file is locked for writing, we get a sharing violation notification
// followed by *two* CDN_FILEOK notifications. These flags are used to track the multiple
// notifications so we only show one error message box to the user.
// For a more complete explanation and PS bug information, see HookProc.
_ignoreSecondFileOkNotification = false;
_fileOkNotificationCount = 0;
// Set this to an empty list so callers can simply add to it. They can also replace it wholesale.
CustomPlaces = new List<FileDialogCustomPlace>();
}
/// <summary>
/// Converts the given filter string to the format required in an OPENFILENAME_I
/// structure.
/// </summary>
private static string MakeFilterString(string s, bool dereferenceLinks)
{
if (String.IsNullOrEmpty(s))
{
// Workaround for VSWhidbey bug #95338 (carried over from Microsoft implementation)
// Apparently, when filter is null, the common dialogs in Windows XP will not dereference
// links properly. The work around is to provide a default filter; " |*.*" is used to
// avoid localization issues from description text.
//
// This behavior is now documented in MSDN on the OPENFILENAME structure, so I don't
// expect it to change anytime soon.
if (dereferenceLinks && System.Environment.OSVersion.Version.Major >= 5)
{
s = " |*.*";
}
else
{
// Even if we don't need the bug workaround, change empty
// strings into null strings.
return null;
}
}
StringBuilder nullSeparatedFilter = new StringBuilder(s);
// Replace the vertical bar with a null to conform to the Windows
// filter string format requirements
nullSeparatedFilter.Replace('|', '\0');
// Append two nulls at the end
nullSeparatedFilter.Append('\0');
nullSeparatedFilter.Append('\0');
// Return the results as a string.
return nullSeparatedFilter.ToString();
}
/// <summary>
/// Handle the AddExtension property on newly acquired filenames, then
/// call PromptUserIfAppropriate to display any necessary message boxes.
///
/// Returns false if we need to redisplay the dialog and true otherwise.
/// </summary>
/// <SecurityNote>
/// Critical: due to call to PromptUserIfAppropriate, which displays
/// message boxes with focus restore.
/// TreatAsSafe: This method does not take external input for the call for
/// PromptUserIfAppropriate.
/// </SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
private bool ProcessFileNames()
{
// Only process the filenames if OFN_NOVALIDATE is not set.
if (!GetOption(NativeMethods.OFN_NOVALIDATE))
{
// Call the FilterExtensions private property to get
// a list of valid extensions from the filter(s).
// The first extension from FilterExtensions is the
// default extension.
string[] extensions = GetFilterExtensions();
// For each filename:
// - Process AddExtension
// - Call PromptUserIfAppropriate to display necessary dialog boxes.
for (int i = 0; i < _fileNames.Length; i++)
{
string fileName = _fileNames[i];
// If AddExtension is enabled and we do not already have an extension:
if (AddExtension && !Path.HasExtension(fileName))
{
// Loop through all extensions, starting with the default extension
for (int j = 0; j < extensions.Length; j++)
{
// Assert for a valid extension
Invariant.Assert(!extensions[j].StartsWith(".", StringComparison.Ordinal),
"FileDialog.GetFilterExtensions should not return things starting with '.'");
string currentExtension = Path.GetExtension(fileName);
// Assert to make sure Path.GetExtension behaves as we think it should, returning
// "" if the string is empty and something beginnign with . otherwise.
// Use StringComparison.Ordinal as per FxCop CA1307 and CA130.
Invariant.Assert(currentExtension.Length == 0 || currentExtension.StartsWith(".", StringComparison.Ordinal),
"Path.GetExtension should return something that starts with '.'");
// Because we check Path.HasExtension above, files should
// theoretically not have extensions at this stage - but
// we'll go ahead and remove an existing extension if it
// somehow slipped through.
//
// Strip out any extension that may be remaining and place the rest
// of the filename in s.
//
// Changed to use StringBuilder for perf reasons as per FxCop CA1818
StringBuilder s = new StringBuilder(fileName.Substring(0, fileName.Length - currentExtension.Length));
// we don't want to append the extension if it contains wild cards
if (extensions[j].IndexOfAny(new char[] { '*', '?' }) == -1)
{
// No wildcards, so go ahead and append
s.Append(".");
s.Append(extensions[j]);
}
// If OFN_FILEMUSTEXIST is not set, or if it is set but the filename we generated
// does in fact exist, we update fileName and stop trying new extensions.
if (!GetOption(NativeMethods.OFN_FILEMUSTEXIST) || File.Exists(s.ToString()))
{
fileName = s.ToString();
break;
}
}
// Store this filename back in the _fileNames array.
_fileNames[i] = fileName;
}
// Call PromptUserIfAppropriate to show necessary dialog boxes.
if (!PromptUserIfAppropriate(fileName))
{
// We don't want to display a bunch of message boxes
// if one has already determined we need to return to
// the file dialog, so we will return false to short
// circuit additional processing.
return false;
}
}
}
return true;
}
/// <summary>
/// Prompts the user with a System.Windows.MessageBox
/// when a file does not exist.
/// </summary>
/// <SecurityNote>
/// Security Critical due to a call to MessageBoxWithFocusRestore.
/// </SecurityNote>
[SecurityCritical]
private void PromptFileNotFound(string fileName)
{
MessageBoxWithFocusRestore(SR.Get(SRID.FileDialogFileNotFound, fileName),
System.Windows.MessageBoxButton.OK, MessageBoxImage.Warning);
}
#endregion Private Methods
//---------------------------------------------------
//
// Private Properties
//
//---------------------------------------------------
#region Private Properties
// If multiple files are selected, we only return the first filename.
/// <summary>
/// Gets a string containing the full path of the file selected in
/// the file dialog box.
/// </summary>
/// <SecurityNote>
/// Critical: Do not want to allow access to raw paths to Parially Trusted Applications.
/// </SecurityNote>
private string CriticalFileName
{
[SecurityCritical]
get
{
if (_fileNames == null) // No filename stored internally...
{
return String.Empty; // So we return String.Empty
}
else
{
// Return the first filename in the array if it is non-empty.
if (_fileNames[0].Length > 0)
{
return _fileNames[0];
}
else
{
return String.Empty;
}
}
}
}
/// <summary>
/// Gets a string containing the title of the file dialog.
/// </summary>
/// <SecurityNote>
/// Critical: due to calls to GetWindowTextLength and GetWindowText.
/// </SecurityNote>
// When showing message boxes onscreen, we want them to have the
// same title bar as the file open or save dialog itself. We can't
// just use the Title property, because if it's null the operating
// system substitutes a standard localized title.
//
// The solution is this private property, which returns the title of the
// file dialog (using the stored handle of the dialog _hwndFileDialog to
// call GetWindowText).
//
// It is designed to only be called by MessageBoxWithFocusRestore.
private string DialogCaption
{
[SecurityCritical]
get
{
if (!UnsafeNativeMethods.IsWindow(new HandleRef(this, _hwndFileDialog)))
{
return String.Empty;
}
// Determine the length of the text we want to retrieve...
int textLen = UnsafeNativeMethods.GetWindowTextLength(new HandleRef(this, _hwndFileDialog));
// then make a StringBuilder...
StringBuilder sb = new StringBuilder(textLen + 1);
// and call GetWindowText to fill it up...
UnsafeNativeMethods.GetWindowText(new HandleRef(this, _hwndFileDialog),
sb /*target string*/,
sb.Capacity /* max # of chars to copy before truncation occurs */
);
// then return the results.
return sb.ToString();
}
}
/// <summary>
/// Extracts the file extensions specified by the current file filter into
/// an array of strings. None of the extensions contain .'s, and the
/// default extension is first.
/// </summary>
/// <exception cref="System.InvalidOperationException">
/// Thrown if the filter string stored in the dialog is invalid.
/// </exception>
private string[] GetFilterExtensions()
{
string filter = this._filter;
List<string> extensions = new List<string>();
// Always make the default extension the first in the list,
// because other functions process files in order accepting the first
// valid extension they find. It's a little strange if DefaultExt
// is not in the filters list, but I guess it's legal.
if (_defaultExtension != null)
{
extensions.Add(_defaultExtension);
}
// If we have filters, extract the extensions from the currently selected
// filter and add them to the extensions list.
if (filter != null)
{
// Filter strings are '|' delimited, so we split on them
string[] tokens = filter.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
// Calculate the index of the token containing extension(s) selected
// by the FilterIndex property. Remember FilterIndex is one based.
// Multiply by 2 because each filter consists of 2 strings.
// Now subtract one to get to the filter component.
//
// example: Text|*.txt|Pictures|*.jpg|Web Pages|*.htm
// tokens[]: 0 1 2 3 4 5
// FilterIndex = 2 selects Pictures; (2*2)-1 = 3 points to *.jpg in tokens
//
int indexOfExtension = (_filterIndex * 2) - 1;
// Check to be sure our filter index is not out of bounds (that is,
// greater than the number of filters we actually have).
// We multiply by 2 here because each filter consists of two strings,
// description and extensions, both separated by | characters.. so
// tokens.length is actually twice the number of filters we have.
if (indexOfExtension >= tokens.Length)
{
throw new InvalidOperationException(SR.Get(SRID.FileDialogInvalidFilterIndex));
}
// If our filter index is valid (0 is reserved by Windows for custom
// filter functionality we don't expose, so filters must be 1 or greater)
if (_filterIndex > 0)
{
// Find our filter in the tokens list, then split it on the
// ';' character (which is the filter extension delimiter)
string[] exts = tokens[indexOfExtension].Split(';');
foreach (string ext in exts)
{
// Filter extensions should be in the form *.txt or .txt,
// so we strip out everything before and including the '.'
// before adding the extension to our list.
// If the extension has no '.', we just ignore it as invalid.
int i = ext.LastIndexOf('.');
if (i >= 0)
{
// start the substring one beyond the location of the '.'
// (i) and continue to the end of the string
extensions.Add(ext.Substring(i + 1, ext.Length - (i + 1)));
}
}
}
}
return extensions.ToArray();
}
/// <summary>
/// Gets an integer representing the Win32 common Open File Dialog OFN_* option flags
/// used to display a dialog with the current set of property values.
/// </summary>
//
// We bitwise AND _dialogOptions with all of the options we consider valid
// before returning the resulting bitmask to avoid accidentally setting a
// flag we don't intend to. Note that this list doesn't include a few of the
// flags we set right before showing the dialog in RunDialog (like
// NativeMethods.OFN_EXPLORER), since those are only added when creating
// the OPENFILENAME structure.
//
// Also note that our private flags are not included in this list (like
// OPTION_ADDEXTENSION)
protected int Options
{
get
{
return _dialogOptions.Value & (NativeMethods.OFN_READONLY | NativeMethods.OFN_HIDEREADONLY |
NativeMethods.OFN_NOCHANGEDIR | NativeMethods.OFN_NOVALIDATE |
NativeMethods.OFN_ALLOWMULTISELECT | NativeMethods.OFN_PATHMUSTEXIST |
NativeMethods.OFN_NODEREFERENCELINKS);
}
}
#endregion Private Properties
#region Vista COM interfaces Augmentation
/// <summary>
/// Events sink for IFileDialog. MSDN says to return E_NOTIMPL for several, but not all, of these methods when we don't want to support them.
/// </summary>
/// <remarks>
/// Be sure to explictly Dispose of it, or use it in a using block. Unadvise happens as a result of Dispose.
/// </remarks>
private sealed class VistaDialogEvents : IFileDialogEvents, IDisposable
{
[SecurityCritical(SecurityCriticalScope.Everything)]
public delegate bool OnOkCallback(IFileDialog dialog);
/// <SecurityNote>
/// Critical: COM pointer that's obtained in a critical context.
/// </SecurityNote>
[SecurityCritical]
private IFileDialog _dialog;
/// <SecurityNote>
/// Critical: Callback method that's obtained in a critical context.
/// </SecurityNote>
[SecurityCritical]
private OnOkCallback _okCallback;
uint _eventCookie;
/// <SecurityNote>
/// Critical: Accesses methods on the critical interface IFileDialog.
/// </SecurityNote>
[SecurityCritical]
public VistaDialogEvents(IFileDialog dialog, OnOkCallback okCallback)
{
_dialog = dialog;
_eventCookie = dialog.Advise(this);
_okCallback = okCallback;
}
/// <SecurityNote>
/// Critical: This gets raised as a callback made in a critical context.
/// </SecurityNote>
[SecurityCritical]
HRESULT IFileDialogEvents.OnFileOk(IFileDialog pfd)
{
return _okCallback(pfd) ? HRESULT.S_OK : HRESULT.S_FALSE;
}
/// <SecurityNote>
/// Critical: This gets raised as a callback made in a critical context.
/// TreatAsSafe: This object was created in a critical context.
/// This doesn't return any critical information, just immediately returns an appropriate HRESULT.
/// </SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
HRESULT IFileDialogEvents.OnFolderChanging(IFileDialog pfd, IShellItem psiFolder)
{
return HRESULT.E_NOTIMPL;
}
/// <SecurityNote>
/// Critical: This gets raised as a callback made in a critical context.
/// TreatAsSafe: This object was created in a critical context.
/// This doesn't return any critical information, just immediately returns an appropriate HRESULT.
/// </SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
HRESULT IFileDialogEvents.OnFolderChange(IFileDialog pfd)
{
return HRESULT.S_OK;
}
/// <SecurityNote>
/// Critical: This gets raised as a callback made in a critical context.
/// TreatAsSafe: This object was created in a critical context.
/// This doesn't return any critical information, just immediately returns an appropriate HRESULT.
/// </SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
HRESULT IFileDialogEvents.OnSelectionChange(IFileDialog pfd)
{
return HRESULT.S_OK;
}
/// <SecurityNote>
/// Critical: This gets raised as a callback made in a critical context.
/// TreatAsSafe: This object was created in a critical context.
/// This doesn't return any critical information, just immediately returns an appropriate HRESULT.
/// </SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
HRESULT IFileDialogEvents.OnShareViolation(IFileDialog pfd, IShellItem psi, out FDESVR pResponse)
{
pResponse = FDESVR.DEFAULT;
return HRESULT.S_OK;
}
/// <SecurityNote>
/// Critical: This gets raised as a callback made in a critical context.
/// TreatAsSafe: This object was created in a critical context.
/// This doesn't return any critical information, just immediately returns an appropriate HRESULT.
/// </SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
HRESULT IFileDialogEvents.OnTypeChange(IFileDialog pfd)
{
return HRESULT.S_OK;
}
/// <SecurityNote>
/// Critical: This gets raised as a callback made in a critical context.
/// TreatAsSafe: This object was created in a critical context.
/// This doesn't return any critical information, just immediately returns an appropriate HRESULT.
/// </SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
HRESULT IFileDialogEvents.OnOverwrite(IFileDialog pfd, IShellItem psi, out FDEOR pResponse)
{
pResponse = FDEOR.DEFAULT;
return HRESULT.S_OK;
}
/// <SecurityNote>
/// Critical: Accesses methods on the critical interface IFileDialog.
/// </SecurityNote>
[SecurityCritical]
void IDisposable.Dispose()
{
_dialog.Unadvise(_eventCookie);
}
}
public IList<FileDialogCustomPlace> CustomPlaces { get; set; }
#region Internal and Protected Methods
// These methods are intended to be internal AND protected, but C# doesn't allow that declaration.
/// <SecurityNote>
/// Derived callers have similar attributes. See their declarations for a fuller explanation.
/// TreatAsSafe as this returns a COM interface, not a handle, which has Security attributes on it as well.
/// </SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
internal abstract IFileDialog CreateVistaDialog();
/// <SecurityNote>
/// Derived callers have similar attributes.
/// Critical: This returns a collection of file paths on the user's machine.
/// </SecurityNote>
[SecurityCritical]
internal abstract string[] ProcessVistaFiles(IFileDialog dialog);
#endregion
#region Internal Methods
/// <SecurityNote>
/// Critical: Calls methods on SecurityCritical IFileDialog interface.
/// Accesses Critical data CriticalFileName and file paths resolved from FileDialogCustomPlaces
/// that the caller doesn't necessarily have access to.
/// Asserts FileIOPermissions.
/// </SecurityNote>
[SecurityCritical]
internal virtual void PrepareVistaDialog(IFileDialog dialog)
{
dialog.SetDefaultExtension(DefaultExt);
dialog.SetFileName(CriticalFileName);
if (!string.IsNullOrEmpty(InitialDirectory))
{
IShellItem initialDirectory = ShellUtil.GetShellItemForPath(InitialDirectory);
if (initialDirectory != null)
{
// Setting both of these so the dialog doesn't display errors when a remembered folder is missing.
dialog.SetDefaultFolder(initialDirectory);
dialog.SetFolder(initialDirectory);
}
}
dialog.SetTitle(Title);
// Force no mini mode for the SaveFileDialog.
// Only accept physically backed locations.
FOS options = ((FOS)Options & c_VistaFileDialogMask) | FOS.DEFAULTNOMINIMODE | FOS.FORCEFILESYSTEM;
dialog.SetOptions(options);
COMDLG_FILTERSPEC[] filterItems = GetFilterItems(Filter);
if (filterItems.Length > 0)
{
dialog.SetFileTypes((uint)filterItems.Length, filterItems);
dialog.SetFileTypeIndex(unchecked((uint)FilterIndex));
}
IList<FileDialogCustomPlace> places = CustomPlaces;
if (places != null && places.Count != 0)
{
foreach (FileDialogCustomPlace customPlace in places)
{
IShellItem shellItem = ResolveCustomPlace(customPlace);
if (shellItem != null)
{
try
{
dialog.AddPlace(shellItem, FDAP.BOTTOM);
}
catch (ArgumentException)
{
// The dialog doesn't allow some ShellItems to be set as Places (like device ports).
// Silently ---- errors here.
}
}
}
}
}
#endregion
#region Private Methods
private bool UseVistaDialog
{
get { return Environment.OSVersion.Version.Major >= 6; }
}
/// <SecurityNote>
/// Critical: Calls Critical PrepareVistaDialog.
/// </SecurityNote>
[SecurityCritical]
private bool RunVistaDialog(IntPtr hwndOwner)
{
IFileDialog dialog = CreateVistaDialog();
PrepareVistaDialog(dialog);
using (VistaDialogEvents events = new VistaDialogEvents(dialog, HandleVistaFileOk))
{
return dialog.Show(hwndOwner).Succeeded;
}
}
/// <SecurityNote>
/// Critical: Calls critical ProcessVistaFiles. Accesses critical _dialogOptions.Value.
/// Safe: Only called in response to user input.
/// This doesn't actually return any data. It just updates internal state based
/// on user actions. Querying of any critical data doesn't happen in this path
/// and is still guarded.
/// Specifically it modifies _fileNames by way of ProcessVistaFiles.
/// This field is not exposed to PT.
/// </SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
private bool HandleVistaFileOk(IFileDialog dialog)
{
// When this callback occurs, the HWND is visible and we need to
// grab it because it is used for various things like looking up the
// DialogCaption.
UnsafeNativeMethods.IOleWindow oleWindow = (UnsafeNativeMethods.IOleWindow) dialog;
oleWindow.GetWindow(out _hwndFileDialog);
int saveOptions = _dialogOptions.Value;
int saveFilterIndex = _filterIndex;
string[] saveFileNames = _fileNames;
bool ok = false;
try
{
uint filterIndexTemp = dialog.GetFileTypeIndex();
_filterIndex = unchecked((int)filterIndexTemp);
_fileNames = ProcessVistaFiles(dialog);
if (ProcessFileNames())
{
var cancelArgs = new CancelEventArgs();
OnFileOk(cancelArgs);
ok = !cancelArgs.Cancel;
}
}
finally
{
if (!ok)
{
_fileNames = saveFileNames;
_dialogOptions.Value = saveOptions;
_filterIndex = saveFilterIndex;
}
else
{
if (0 != (Options & NativeMethods.OFN_HIDEREADONLY))
{
// When the dialog is dismissed OK, the Readonly bit can't be left on if ShowReadOnly was false
// Downlevel this happens automatically. On Vista we need to watch out for it.
_dialogOptions.Value &= ~NativeMethods.OFN_READONLY;
}
}
}
return ok;
}
private static COMDLG_FILTERSPEC[] GetFilterItems(string filter)
{
// Expecting pipe delimited filter string pairs.
// First is the label, second is semi-colon delimited list of extensions.
var extensions = new List<COMDLG_FILTERSPEC>();
if (!string.IsNullOrEmpty(filter))
{
string[] tokens = filter.Split('|');
if (0 == tokens.Length % 2)
{
for (int i = 1; i < tokens.Length; i += 2)
{
extensions.Add(
new COMDLG_FILTERSPEC
{
pszName = tokens[i-1],
pszSpec = tokens[i],
});
}
}
}
return extensions.ToArray();
}
/// <SecurityNote>
/// Critical: Resolves an opaque interface into a path on the user's machine.
/// </SecurityNote>
[SecurityCritical]
private static IShellItem ResolveCustomPlace(FileDialogCustomPlace customPlace)
{
// Use the KnownFolder Guid if it exists. Otherwise use the Path.
return ShellUtil.GetShellItemForPath(ShellUtil.GetPathForKnownFolder(customPlace.KnownFolder) ?? customPlace.Path);
}
#endregion
#endregion
//---------------------------------------------------
//
// Private Fields
//
//---------------------------------------------------
#region Private Fields
private const FOS c_VistaFileDialogMask = FOS.OVERWRITEPROMPT | FOS.NOCHANGEDIR | FOS.NOVALIDATE | FOS.ALLOWMULTISELECT | FOS.PATHMUSTEXIST | FOS.FILEMUSTEXIST | FOS.CREATEPROMPT | FOS.NODEREFERENCELINKS;
// _dialogOptions is a set of bit flags used to control the behavior
// of the Win32 dialog box.
private SecurityCriticalDataForSet<int> _dialogOptions;
// These two flags are related to a fix for an issue where Windows
// sends two FileOK notifications back to back after a sharing
// violation occurs. See CDN_SHAREVIOLATION in HookProc for details.
private bool _ignoreSecondFileOkNotification;
private int _fileOkNotificationCount;
// These private variables store data for the various public properties
// that control the appearance of the file dialog box.
private SecurityCriticalDataForSet<string> _title; // Title bar of the message box
private SecurityCriticalDataForSet<string> _initialDirectory; // Starting directory
private string _defaultExtension; // Extension appended first if AddExtension
// is enabled
private string _filter; // The file extension filters that display
// in the "Files of Type" box in the dialog
private int _filterIndex; // The index of the currently selected
// filter (a default filter index before
// the dialog is called, and the filter
// the user selected afterwards.) This
// index is 1-based, not 0-based.
// Since we have to interop with native code to show the file dialogs,
// we use the CharBuffer class to help with the marshalling of
// unmanaged memory that stores the user-selected file names.
/// <SecurityNote>
/// Critical: This is a buffer that is operated on by unmanaged
/// </SecurityNote>
[SecurityCritical]
private CharBuffer _charBuffer;
// We store the handle of the file dialog inside our class
// for a variety of purposes (like getting the title of the dialog
// box when we need to show a message box with the same title bar caption)
/// <SecurityNote>
/// Critical: The hWnd of the dialog is critical data.
/// </SecurityNote>
[SecurityCritical]
private IntPtr _hwndFileDialog;
// This is the array that stores the filename(s) the user selected in the
// dialog box. If Multiselect is not enabled, only the first element
// of this array will be used.
/// <SecurityNote>
/// Critical: The full file paths are critical data.
/// </SecurityNote>
[SecurityCritical]
private string[] _fileNames;
// Constant to control the initial size of the character buffer;
// 8192 is an arbitrary but reasonable size that should minimize the
// number of times we need to grow the buffer.
private const int FILEBUFSIZE = 8192;
// OPTION_ADDEXTENSION is our own bit flag that we use to control our
// own automatic extension appending feature.
private const int OPTION_ADDEXTENSION = unchecked(unchecked((int)0x80000000));
#endregion Private Fields
}
}
|