【问题标题】:Allow selection of readonly files from SaveFileDialog?允许从 SaveFileDialog 中选择只读文件?
【发布时间】:2010-06-13 07:43:51
【问题描述】:

我在一个应用程序中使用Microsoft.Win32.SaveFileDialog,其中所有文件都保存为只读,但用户需要能够选择现有文件。被替换的现有文件被重命名,例如:blah.png 变为 blah.png-0.bak、blah.png-1.bak 等等。

因此,OverwritePrompt 的语言不合适 - 我们不允许允许它们覆盖文件 - 所以我设置 dlog.OverwritePrompt = false;

对话框的初始文件名是根据文档值生成的,因此对于它们来说,这很容易 - 我们提前重命名候选文件,如果用户取消或选择不同的名称,请再次重命名。

当我交付该功能时,测试人员迅速抱怨,因为他们想要重复保存名称​​与约定工作流程不同的文件(那些愚蠢、顽皮的家伙!)。

我想不出用标准对话框执行此操作的方法,该方法可以在 XP(目前)和 Windows 7 上安全运行。

我希望连接到FileOK event,但这被称为之后我收到一个警告对话框:

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

【问题讨论】:

    标签: .net wpf dialog


    【解决方案1】:

    在尝试从 Perforce 工作区中选择文件时遇到同样的问题,这些文件在签出之前都是只读的。通过调用本机 SaveFileDialog 解决了它。

    您可以在本机版本中删除 FOS_NOREADONLYRETURN,但这与 .NET 中的相同。 Windows API 只是自动添加标志。

    首先,我尝试使用 OpenFileDialog 并使用本机 SetOkButtonLabel 方法更改 OK 按钮的文本。但这会给您带来本地化问题,并且您也会丢失覆盖检查。

    我最终使用了 OnSelectionChange 事件(未在 .NET 版本中公开)来临时删除所选文件的只读标志。这个技巧非常有效,除了需要提升权限的文件夹(如 C: 的根文件夹)。但我可以忍受。

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

    您还需要这些绑定:

    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

    【讨论】:

    • 我没有时间测试这个,因为它是老客户群的一部分,所以我相信答案,但它看起来令人信服和全面,谢谢!
    • 没问题。我知道这是一个 4 年前的问题,但我想,我分享我的解决方案,因为其他人可能有同样的问题。
    【解决方案2】:

    我戳了一会儿,OPENFILENAME 结构具有控制只读行为的标志。不走运,它们只对 OpenFileDialog 启用,而不是 SaveFileDialog。只读检查是硬检查,不能绕过。

    除了让 QA 小组失望之外,我强烈建议您使用普通的 Windows 文件安全设置来保护文件内容,而不是 ReadOnly 文件属性。

    【讨论】:

      【解决方案3】:

      您可以通过使用“OpenFileDialog”而不是“SaveFileDialog”来解决此问题:

      如果您将其“CheckFileExists”属性设置为“False”,它应该像一个没有只读检查的保存对话框。

      【讨论】:

      • 虽然这确实意味着对话框有一个“打开”按钮而不是一个“保存”按钮,这可能会让最终用户感到困惑。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-10-17
      • 2023-03-22
      • 1970-01-01
      • 2023-03-03
      • 1970-01-01
      相关资源
      最近更新 更多