1. 程式人生 > >[WPF]為什麼使用SaveFileDialog建立檔案需要刪除許可權?

[WPF]為什麼使用SaveFileDialog建立檔案需要刪除許可權?

## 1. 問題 好像很少人會遇到這種需求。假設有一個資料夾,使用者有幾乎所有許可權,但沒有刪除的許可權,如下圖所示: ![](https://img2020.cnblogs.com/blog/38937/202004/38937-20200402225600791-1595840703.png) 這時候使用SaveFileDialog在這個資料夾裡建立檔案居然會報如下錯誤: ![](https://img2020.cnblogs.com/blog/38937/202004/38937-20200402225607165-1042291237.png) 這哪裡是網路位置了,我又哪裡去找個管理員?更奇怪的是,雖然報錯了,但檔案還是會創建出來,不過這是個空檔案。不僅WPF,普通的記事本也會有這個問題,SaveFileDialog會建立一個空檔案,記事本則沒有被儲存。具體可以看以下GIF: ![](https://img2020.cnblogs.com/blog/38937/202004/38937-20200402225625230-1906222736.gif) ## 2. 問題原因 其實當SaveFileDialog關閉前,對話方塊會建立一個測試檔案,用於檢查檔名、檔案許可權等,然後又刪除它。所以如果有檔案的建立許可權,而沒有檔案的刪除許可權,在建立測試檔案後就沒辦法刪除這個測試檔案,這時候就會報錯,而測試檔案留了下來。 有沒有發現`SaveFileDialog`中有一個屬性Options? ``` CS // // 摘要: // 獲取 Win32 通用檔案對話方塊標誌,檔案對話方塊使用這些標誌來進行初始化。 // // 返回結果: // 一個包含 Win32 通用檔案對話方塊標誌的 System.Int32,檔案對話方塊使用這些標誌來進行初始化。 protected int Options { get; } ``` 本來應該可以設定一個`NOTESTFILECREATE`的標誌位,但WPF中這個屬性是隻讀的,所以WPF的SaveFileDialog肯定會建立測試檔案。 ## 3. 解決方案 SaveFileDialog本身只是Win32 API的封裝,我們可以參考SaveFileDialog的原始碼,偽裝一個呼叫方法差不多的MySaveFileDialog,然後自己封裝`GetSaveFileName`這個API。程式碼大致如下: ``` CS 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(); Window window = Application.Current.Windows.OfType().Where(w => w.IsActive).FirstOrDefault(); if (window != null) { var wih = new WindowInteropHelper(window); 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; } /// /// Converts the given filter string to the format required in an OPENFILENAME_I /// structure. ///
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(); } ``` 注意其中的這句: ``` CS openFileName.flags |= FOS.NOTESTFILECREATE | FOS.OVERWRITEPROMPT; ``` 因為我的需求就是不建立TestFile,所以我直接這麼寫而不是提供可選項。一個更好的方法是給WPF提ISSUE,我已經這麼做了: [Make SaveFileDialog support NOTESTFILECREATE.](https://github.com/dotnet/wpf/issues/870) 但看來我等不到有人處理的這天,如果再有這種需求,還是將就著用我的這個自創的SaveFileDialog吧: [CustomSaveFileDialog](https://github.com/DinoChan/CustomSaveFileDialog) ## 4. 參考 [Common Item Dialog (Windows) Microsoft Docs](https://docs.microsoft.com/zh-cn/previous-versions/windows/desktop/legacy/bb776913(v=vs.85)?redirectedfrom=MSDN) [GetSaveFileNameA function (commdlg.h) - Win32 apps Microsoft Docs](https://docs.microsoft.com/zh-cn/windows/win32/api/commdlg/nf-commdlg-getsavefilenamea) [OPENFILENAMEW (commdlg.h) - Win32 apps Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-openfilenamew)