开发者

Allow selection of readonly files from SaveFileDialog?

I'm using Microsoft.Win32.SaveFileDialog in an app where all files are saved as readonly but the user needs to be able to choose existing files. The existing files bei开发者_开发技巧ng replaced are renamed eg: blah.png becomes blah.png-0.bak, blah.png-1.bak and so on.

Thus, the language for the OverwritePrompt is inappropriate - we are not allowing them to overwrite files - so I'm setting dlog.OverwritePrompt = false;

The initial filenames for the dialog are generated based on document values so for them, it's easy - we rename the candidate file in advance and if the user cancels or chooses a different name, rename it back again.

When I delivered the feature, testers swiftly complained because they wanted to repeatedly save files with names different from the agreed workflow (those goofy, playful guys!).

I can't figure out a way to do this with the standard dialogs, in a way that will run safely on both XP (for now) and Windows 7.

I was hoping to hook into the FileOK event but that is called after I get a warning dialog:

|-----------------------------------------|
| blah.png                                |
| This file is set to read-only.          |
| Try again with a different file name.   |
|-----------------------------------------|


Had the same problem when trying to select files from a Perforce workspace, which are read-only until checked out. Solved it by calling the native SaveFileDialog.

You can remove the FOS_NOREADONLYRETURN in the native version, but it is the same as in .NET. The Windows API simply adds the flag automatically.

First, I tried to use the OpenFileDialog and change the text of the OK button with the native SetOkButtonLabel method. But this gives you a problem with localization and you also lose the overwrite check.

I ended up using the OnSelectionChange event (which is not exposed in the .NET version) in order to temporarily remove the read-only flag of selected files. This trick works quite well, except for folders where elevated rights are needed (like the root folder of C:). But I can live with that.

using System.IO;
using System.Runtime.InteropServices;

namespace System.Windows.Forms
{
    /// <summary>
    /// Same as .NETs SaveFileDialog, except that it also allows you to select read-only files.
    /// 
    /// Based on the native Common Item Dialog, which is also used internally by SaveFileDialog. Uses
    /// the OnSelectionChange event to temporarily remove the read-only flag of selected files to
    /// trick the dialog. Unfortunately, this event is not exponsed in the .NET version.
    /// 
    /// Since the Common Item Dialog was not available until Windows Vista, call the static IsSupported()
    /// method first. If it returns false, use the regular SaveFileDialog instead. On XP, the regular 
    /// dialog works also for read-only files.
    /// 
    /// Note that this trick won't work where elevated rights are needed (e.g. in the root folder of C:).
    /// </summary>
    public class SaveFileDialogRO : IDisposable
    {
        private const int S_OK = 0;

        private FileDialogNative.IFileSaveDialog dialog;
        private string defaultExt = string.Empty;
        private FileDialogNative.FOS options;
        private string filter = string.Empty;
        private string initialDirectory = string.Empty;
        private string title = string.Empty;

        /// <summary>
        /// Returns true, if Common Item Dialog is supported, which SaveFileDialogRO uses internally.
        /// If not, just use the regular SaveFileDialog.
        /// </summary>
        public static bool IsSupported()
        {
            return Environment.OSVersion.Version.Major > 5;
        }

        public SaveFileDialogRO()
        {
             dialog = (FileDialogNative.IFileSaveDialog)new FileDialogNative.FileSaveDialogRCW();
             dialog.GetOptions(out options);
        }

        ~SaveFileDialogRO()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected void Dispose(bool disposing)
        {
            Marshal.ReleaseComObject(dialog);
        }

        /// <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>
        public bool CheckFileExists 
        {
            get
            {
                return (options & FileDialogNative.FOS.FOS_FILEMUSTEXIST) != 0;
            }
            set
            {
                if (value)
                    options |= FileDialogNative.FOS.FOS_FILEMUSTEXIST;
                else
                    options &= ~FileDialogNative.FOS.FOS_FILEMUSTEXIST;
                dialog.SetOptions(options);
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the dialog box displays a warning if the user specifies a 
        /// path that does not exist.
        /// </summary>
        public bool CheckPathExists
        {
            get
            {
                return (options & FileDialogNative.FOS.FOS_PATHMUSTEXIST) != 0;
            }
            set
            {
                if (value)
                    options |= FileDialogNative.FOS.FOS_PATHMUSTEXIST;
                else
                    options &= ~FileDialogNative.FOS.FOS_PATHMUSTEXIST;
                dialog.SetOptions(options);
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the dialog box prompts the user for permission to create a 
        /// file if the user specifies a file that does not exist.
        /// </summary>
        public bool CreatePrompt 
        {
            get
            {
                return (options & FileDialogNative.FOS.FOS_CREATEPROMPT) != 0;
            }
            set
            {
                if (value)
                    options |= FileDialogNative.FOS.FOS_CREATEPROMPT;
                else
                    options &= ~FileDialogNative.FOS.FOS_CREATEPROMPT;
                dialog.SetOptions(options);
            }
        }

        /// <summary>
        /// Gets or sets the default file name extension.
        /// </summary>
        public string DefaultExt
        {
            get
            {
                return defaultExt;
            }
            set
            {
                dialog.SetDefaultExtension(value);
                defaultExt = value;
            }
        }

        /// <summary>
        /// Gets or sets the default file name extension.
        /// </summary>
        public bool DereferenceLinks
        {
            get
            {
                return (options & FileDialogNative.FOS.FOS_NODEREFERENCELINKS) == 0;
            }
            set
            {
                if (!value)
                    options |= FileDialogNative.FOS.FOS_NODEREFERENCELINKS;
                else
                    options &= ~FileDialogNative.FOS.FOS_NODEREFERENCELINKS;
                dialog.SetOptions(options);
            }
        }

        /// <summary>
        /// Gets or sets a string containing the file name selected in the file dialog box.
        /// </summary>
        public string FileName 
        {
            get
            {
                // Get the selected file name (fails if the dialog has been cancelled or not yet been shown)
                string fileName;
                try
                {
                    FileDialogNative.IShellItem item;
                    dialog.GetResult(out item);

                    item.GetDisplayName(FileDialogNative.SIGDN.SIGDN_FILESYSPATH, out fileName);
                }
                catch (Exception)
                {
                    // Return the name that was set via SetFileName (fails if none has been set)
                    try
                    {
                        dialog.GetFileName(out fileName);
                    }
                    catch (Exception)
                    {
                        fileName = string.Empty;
                    }
                }
                return fileName;
            }
            set
            {
                dialog.SetFileName(value);
            }
        }

        /// <summary>
        /// Gets the file names of all selected files in the dialog box.
        /// For the SaveFileDialog, this will always be at most a single file.
        /// </summary>
        public string[] FileNames
        {
            get
            {
                // Get the selected file name (fails if the dialog has been cancelled or not yet been shown)
                try
                {
                    string fileName;
                    FileDialogNative.IShellItem item;
                    dialog.GetResult(out item);

                    item.GetDisplayName(FileDialogNative.SIGDN.SIGDN_FILESYSPATH, out fileName);
                    return new string[] { fileName };
                }
                catch (Exception)
                {
                    return new string[0];
                }
            }
        }

        /// <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 in the dialog box.
        /// </summary>
        /// <remarks>
        /// For each filtering option, the filter string contains a description of the filter, followed 
        /// by the vertical bar (|) and the filter pattern. The strings for different filtering options are 
        /// separated by the vertical bar.</br>
        /// The following is an example of a filter string:</br>
        /// Text files (*.txt)|*.txt|All files (*.*)|*.*
        /// </remarks>
        public string Filter 
        {
            get
            {
                return filter;
            }
            set
            {
                // Split at vertical bars
                string[] types = value.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
                if (types.Length == 0 || types.Length % 2 != 0)
                    throw new ArgumentException("Invalid filter: " + value);

                // Convert to COMDLG_FILTERSPEC array
                int numTypes = types.Length / 2;
                FileDialogNative.COMDLG_FILTERSPEC[] specs = new FileDialogNative.COMDLG_FILTERSPEC[numTypes];
                for (int i = 0; i < numTypes; ++i)
                {
                    specs[i] = new FileDialogNative.COMDLG_FILTERSPEC
                    {
                        pszName = types[i * 2 + 0],
                        pszSpec = types[i * 2 + 1],
                    };
                }

                // Set new filter
                dialog.SetFileTypes((uint)numTypes, specs);
                filter = value;
            }
        }

        /// <summary>
        /// Gets or sets the index of the filter currently selected in the file dialog box.
        /// Note: The index value of the first filter entry is 1!
        /// </summary>
        public int FilterIndex 
        {
            get
            {
                uint index;
                dialog.GetFileTypeIndex(out index);
                return (int)index;
            }
            set
            {
                dialog.SetFileTypeIndex((uint)value);
            }
        }

        /// <summary>
        /// Gets or sets the initial directory displayed by the file dialog box.
        /// </summary>
        public string InitialDirectory 
        {
            get
            {
                return initialDirectory;
            }
            set
            {
                FileDialogNative.IShellItem item;
                IntPtr idl;
                uint atts = 0;
                if (SHILCreateFromPath(value, out idl, ref atts) == S_OK)
                {
                    if (SHCreateShellItem(IntPtr.Zero, IntPtr.Zero, idl, out item) == S_OK)
                    {
                        dialog.SetFolder(item);
                        initialDirectory = value;
                    }

                    CoTaskMemFree(idl);
                }
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the Save As dialog box displays a warning if the user 
        /// specifies a file name that already exists.
        /// </summary>
        public bool OverwritePrompt
        {
            get
            {
                return (options & FileDialogNative.FOS.FOS_OVERWRITEPROMPT) != 0;
            }
            set
            {
                if (value)
                    options |= FileDialogNative.FOS.FOS_OVERWRITEPROMPT;
                else
                    options &= ~FileDialogNative.FOS.FOS_OVERWRITEPROMPT;
                dialog.SetOptions(options);
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the dialog box restores the current directory before closing.
        /// </summary>
        public bool RestoreDirectory 
        {
            get
            {
                return (options & FileDialogNative.FOS.FOS_NOCHANGEDIR) != 0;
            }
            set
            {
                if (value)
                    options |= FileDialogNative.FOS.FOS_NOCHANGEDIR;
                else
                    options &= ~FileDialogNative.FOS.FOS_NOCHANGEDIR;
                dialog.SetOptions(options);
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the Help button is displayed in the file dialog box.
        /// </summary>
        public bool ShowHelp
        {
            get
            {
                return true;
            }
            set
            {
                // seems to be always true in case of the Common Item Dialog
            }
        }

        /// <summary>
        /// Gets or sets whether the dialog box supports displaying and saving files that have multiple file name extensions.
        /// </summary>
        public bool SupportMultiDottedExtensions
        {
            get
            {
                return true;
            }
            set
            {
                // seems to be always true in case of the Common Item Dialog
            }
        }

        /// <summary>
        /// Gets or sets the file dialog box title.
        /// </summary>
        public string Title 
        {
            get
            {
                return title;
            }
            set
            {
                dialog.SetTitle(value);
                title = value;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the dialog box accepts only valid Win32 file names.
        /// </summary>
        public bool ValidateNames
        {
            get
            {
                return true;
            }
            set
            {
                // seems to be always true in case of the Common Item Dialog
            }
        }

        /// <summary>
        /// Runs the dialog box with a default owner.
        /// </summary>
        public DialogResult ShowDialog()
        {
            return ShowDialog(null);
        }

        /// <summary>
        /// Runs the dialog box with the specified owner.
        /// </summary>
        public DialogResult ShowDialog(IWin32Window owner)
        {
            // Set event handler
            SaveFileDialogROEvents events = new SaveFileDialogROEvents();
            uint cookie;
            dialog.Advise(events, out cookie);

            // Show dialog
            int hr = dialog.Show(owner != null ? owner.Handle : IntPtr.Zero);

            // Remove event handler
            dialog.Unadvise(cookie);
            events.RestoreAttribute();      // needed in case of cancel

            // Convert return value to DialogResult
            return hr == S_OK ? DialogResult.OK : DialogResult.Cancel;
        }

        /// <summary>
        /// Event handler, which temporarily removes the read-only flag of selected files.
        /// </summary>
        class SaveFileDialogROEvents : FileDialogNative.IFileDialogEvents
        {
            FileInfo lastReadOnlyFile = null;

            public int OnFileOk(FileDialogNative.IFileDialog pfd)
            {
                // This method is not called in case of cancel
                RestoreAttribute();
                return S_OK;
            }

            public int OnFolderChanging(FileDialogNative.IFileDialog pfd, FileDialogNative.IShellItem psiFolder)
            {
                return S_OK;
            }

            public void OnFolderChange(FileDialogNative.IFileDialog pfd)
            {
                RestoreAttribute();
            }

            public void OnSelectionChange(FileDialogNative.IFileDialog pfd)
            {
                // Get selected file
                string name;
                try
                {
                    FileDialogNative.IShellItem item;
                    pfd.GetCurrentSelection(out item);
                    item.GetDisplayName(FileDialogNative.SIGDN.SIGDN_FILESYSPATH, out name);
                }
                catch (Exception)
                {
                    // No file selected yet
                    return;
                }

                // Has it changed?
                if (lastReadOnlyFile != null && lastReadOnlyFile.FullName == name)
                    return;

                // Restore read-only attribute of the previous file, if necessary
                RestoreAttribute();

                // Remove read-only attribute of the selected file, if necessary
                FileInfo f = new FileInfo(name);
                if (f.Exists && (f.Attributes & FileAttributes.ReadOnly) != 0)
                {
                    try
                    {
                        f.Attributes &= ~FileAttributes.ReadOnly;
                        lastReadOnlyFile = f;
                    }
                    catch (Exception)
                    {
                        // Not enough rights, nothing we can do
                        return;
                    }
                }
            }

            public void OnShareViolation(FileDialogNative.IFileDialog pfd, FileDialogNative.IShellItem psi, out FileDialogNative.FDE_SHAREVIOLATION_RESPONSE pResponse)
            {
                pResponse = FileDialogNative.FDE_SHAREVIOLATION_RESPONSE.FDESVR_DEFAULT;
            }

            public void OnTypeChange(FileDialogNative.IFileDialog pfd)
            {
            }

            public void OnOverwrite(FileDialogNative.IFileDialog pfd, FileDialogNative.IShellItem psi, out FileDialogNative.FDE_OVERWRITE_RESPONSE pResponse)
            {
                // Removing the read-only attribute in here, unfortunately does not work
                pResponse = FileDialogNative.FDE_OVERWRITE_RESPONSE.FDEOR_DEFAULT;
            }

            /// <summary>
            /// Restores the read-only attribute of the previously selected file.
            /// </summary>
            public void RestoreAttribute()
            {
                if (lastReadOnlyFile != null)
                {
                    lastReadOnlyFile.Attributes |= FileAttributes.ReadOnly;
                    lastReadOnlyFile = null;
                }
            }
        }

        [DllImport("shell32.dll")]
        private static extern int SHILCreateFromPath([MarshalAs(UnmanagedType.LPWStr)] string pszPath, out IntPtr ppIdl, ref uint rgflnOut);

        [DllImport("shell32.dll")]
        private static extern int SHCreateShellItem(IntPtr pidlParent, IntPtr psfParent, IntPtr pidl, out FileDialogNative.IShellItem ppsi);

        [DllImport("ole32.dll")]
        public static extern void CoTaskMemFree(IntPtr ptr);
    }
}

You also need these bindings:

http://www.dotnetframework.org/default.aspx/DotNET/DotNET/8@0/untmp/whidbey/REDBITS/ndp/fx/src/WinForms/Managed/System/WinForms/FileDialog_Vista_Interop@cs/1/FileDialog_Vista_Interop@cs


I poked at this for a while, the OPENFILENAME structure has flags to control read-only behavior. No luck, they are only enabled for OpenFileDialog, not SaveFileDialog. The read-only check is a hard one, you cannot bypass it.

Other than disappointing the QA group, I'd strongly recommend you protect file content with normal Windows file security settings, not the ReadOnly file attribute.


You can get around this by using 'OpenFileDialog' instead of 'SaveFileDialog':

If you set its 'CheckFileExists' property to 'False', it should act like a save-dialog with no read-only checking.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜