[WPF]為什麼使用SaveFileDialog創建文件需要刪除許可權?

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();      Window window = Application.Current.Windows.OfType<Window>().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;  }        /// <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('|', '');        // Append two nulls at the end      nullSeparatedFilter.Append('');      nullSeparatedFilter.Append('');        // Return the results as a string.      return nullSeparatedFilter.ToString();  }  

注意其中的這句:

openFileName.flags |= FOS.NOTESTFILECREATE | FOS.OVERWRITEPROMPT;  

因為我的需求就是不創建TestFile,所以我直接這麼寫而不是提供可選項。一個更好的方法是給WPF提ISSUE,我已經這麼做了:

Make SaveFileDialog support NOTESTFILECREATE.

但看來我等不到有人處理的這天,如果再有這種需求,還是將就著用我的這個自創的SaveFileDialog吧:

CustomSaveFileDialog

4. 參考

Common Item Dialog (Windows) Microsoft Docs

GetSaveFileNameA function (commdlg.h) – Win32 apps Microsoft Docs

OPENFILENAMEW (commdlg.h) – Win32 apps Microsoft Docs