1. 问题
好像很少人会遇到这种需求. 假设有一个文件夹, 用户有几乎所有权限, 但没有删除的权限, 如下图所示:
这时候使用 SaveFileDialog 在这个文件夹里创建文件居然会报如下错误:
这哪里是网络位置了, 我又哪里去找个管理员? 更奇怪的是, 虽然报错了, 但文件还是会创建出来, 不过这是个空文件. 不仅 WPF, 普通的记事本也会有这个问题, SaveFileDialog 会创建一个空文件, 记事本则没有被保存. 具体可以看以下 GIF:
2. 问题原因
其实当 SaveFileDialog 关闭前, 对话框会创建一个测试文件, 用于检查文件名, 文件权限等, 然后又删除它. 所以如果有文件的创建权限, 而没有文件的删除权限, 在创建测试文件后就没办法删除这个测试文件, 这时候就会报错, 而测试文件留了下来.
有没有发现 SaveFileDialog 中有一个属性 Options?
- //
- // 摘要:
- // 获取 Win32 通用文件对话框标志, 文件对话框使用这些标志来进行初始化.
- //
- // 返回结果:
- // 一个包含 Win32 通用文件对话框标志的 System.Int32, 文件对话框使用这些标志来进行初始化.
- protected int Options {
- get;
- }
本来应该可以设置一个 NOTESTFILECREATE 的标志位, 但 WPF 中这个属性是只读的, 所以 WPF 的 SaveFileDialog 肯定会创建测试文件.
3. 解决方案
SaveFileDialog 本身只是 Win32 API 的封装, 我们可以参考 SaveFileDialog 的源码, 伪装一个调用方法差不多的 MySaveFileDialog, 然后自己封装 GetSaveFileName 这个 API. 代码大致如下:
- internal class FOS
- {
- public const int OVERWRITEPROMPT = 0x00000002;
- public const int STRICTFILETYPES = 0x00000004;
- public const int NOCHANGEDIR = 0x00000008;
- public const int PICKFOLDERS = 0x00000020;
- public const int FORCEFILESYSTEM = 0x00000040;
- public const int ALLNONSTORAGEITEMS = 0x00000080;
- public const int NOVALIDATE = 0x00000100;
- public const int ALLOWMULTISELECT = 0x00000200;
- public const int PATHMUSTEXIST = 0x00000800;
- public const int FILEMUSTEXIST = 0x00001000;
- public const int CREATEPROMPT = 0x00002000;
- public const int SHAREAWARE = 0x00004000;
- public const int NOREADONLYRETURN = 0x00008000;
- public const int NOTESTFILECREATE = 0x00010000;
- public const int HIDEMRUPLACES = 0x00020000;
- public const int HIDEPINNEDPLACES = 0x00040000;
- public const int NODEREFERENCELINKS = 0x00100000;
- public const int DONTADDTORECENT = 0x02000000;
- public const int FORCESHOWHIDDEN = 0x10000000;
- public const int DEFAULTNOMINIMODE = 0x20000000;
- public const int FORCEPREVIEWPANEON = 0x40000000;
- }
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
- public class OpenFileName
- {
- internal int structSize = 0;
- internal IntPtr hwndOwner = IntPtr.Zero;
- internal IntPtr hInstance = IntPtr.Zero;
- internal string filter = null;
- internal string custFilter = null;
- internal int custFilterMax = 0;
- internal int filterIndex = 0;
- internal string file = null;
- internal int maxFile = 0;
- internal string fileTitle = null;
- internal int maxFileTitle = 0;
- internal string initialDir = null;
- internal string title = null;
- internal int flags = 0;
- internal short fileOffset = 0;
- internal short fileExtMax = 0;
- internal string defExt = null;
- internal int custData = 0;
- internal IntPtr pHook = IntPtr.Zero;
- internal string template = null;
- }
- public class LibWrap
- {
- // Declare a managed prototype for the unmanaged function.
- [DllImport("Comdlg32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
- public static extern bool GetSaveFileName([In, Out] OpenFileName ofn);
- }
- public bool? ShowDialog()
- {
- var openFileName = new OpenFileName();
- Windows Windows = Application.Current.Windows.OfType<Windows>().Where(w => w.IsActive).FirstOrDefault();
- if (Windows != null)
- {
- var wih = new WindowInteropHelper(Windows);
- IntPtr hWnd = wih.Handle;
- openFileName.hwndOwner = hWnd;
- }
- openFileName.structSize = Marshal.SizeOf(openFileName);
- openFileName.filter = MakeFilterString(Filter);
- openFileName.filterIndex = FilterIndex;
- openFileName.fileTitle = new string(new char[64]);
- openFileName.maxFileTitle = openFileName.fileTitle.Length;
- openFileName.initialDir = InitialDirectory;
- openFileName.title = Title;
- openFileName.defExt = DefaultExt;
- openFileName.structSize = Marshal.SizeOf(openFileName);
- openFileName.flags |= FOS.NOTESTFILECREATE | FOS.OVERWRITEPROMPT;
- if (RestoreDirectory)
- openFileName.flags |= FOS.NOCHANGEDIR;
- // 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.
- char[] chars = new char[FILEBUFSIZE];
- for (int i = 0; i <FileName.Length; i++)
- {
- chars[i] = FileName[i];
- }
- openFileName.file = new string(chars);
- // nMaxFile
- // Size of the lpstrFile buffer in number of Unicode characters.
- openFileName.maxFile = FILEBUFSIZE;
- if (LibWrap.GetSaveFileName(openFileName))
- {
- FileName = openFileName.file;
- return true;
- }
- return false;
- }
- /// <summary>
- /// Converts the given filter string to the format required in an OPENFILENAME_I
- /// structure.
- /// </summary>
- private static string MakeFilterString(string s, bool dereferenceLinks = true)
- {
- 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();
- }
注意其中的这句:
openFileName.flags |= FOS.NOTESTFILECREATE | FOS.OVERWRITEPROMPT;
因为我的需求就是不创建 TestFile, 所以我直接这么写而不是提供可选项. 一个更好的方法是给 WPF 提 ISSUE, 我已经这么做了:
Make SaveFileDialog support NOTESTFILECREATE. https://github.com/dotnet/wpf/issues/870
但看来我等不到有人处理的这天, 如果再有这种需求, 还是将就着用我的这个自创的 SaveFileDialog 吧:
https://github.com/DinoChan/CustomSaveFileDialog
4. 参考
- Common Item Dialog (Windows) Microsoft Docs
- GetSaveFileNameA function (commdlg.h) - Win32 apps Microsoft Docs
- OPENFILENAMEW (commdlg.h) - Win32 apps Microsoft Docs
来源: https://www.cnblogs.com/dino623/p/why_save_file_dialog_needs_delete_permission.html