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('|', '
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('|', '\0');
// append two nulls at the end
nullseparatedfilter.append('\0');
nullseparatedfilter.append('\0');
// return the results as a string.
return nullseparatedfilter.tostring();
}
'); // append two nulls at the end nullseparatedfilter.append('
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('|', '\0');
// append two nulls at the end
nullseparatedfilter.append('\0');
nullseparatedfilter.append('\0');
// return the results as a string.
return nullseparatedfilter.tostring();
}
'); nullseparatedfilter.append('
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('|', '\0');
// append two nulls at the end
nullseparatedfilter.append('\0');
nullseparatedfilter.append('\0');
// return the results as a string.
return nullseparatedfilter.tostring();
}
'); // 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