• [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

  • 相关阅读:
    如何将数据库中已有表导入到powerDesigner生成pdm文件
    Delphi TcxTreelist 表格左边总是缩进去 ,好像有偏移 解决方法
    HTML、CSS、JS对unicode字符的不同处理
    老生常谈ajax
    浅谈javascript面向对象
    HTML5原生拖放实例分析
    一个小动画,颠覆你的CSS世界观
    布局神器display:table-cell
    javascript 日常总结
    G2 2.0 更灵活、更强大、更完备的可视化引擎!
  • 原文地址:https://www.cnblogs.com/dino623/p/why_save_file_dialog_needs_delete_permission.html
Copyright © 2020-2023  润新知