Description
openedon Mar 30, 2023
Background and motivation
I was asked to create an API Proposal issue for the OpenFolderDialog, PR #7244
The purpose of the API is to enable folder selection using the common folder dialogs in WPF. #438 is the top voted issue in the repository.
This proposal involves splitting a hierarchy of classes and removing support for Windows XP dialogs (they fallback to Vista dialogs).
API Proposal
The proposed API is a compromise on perceived compatibility requirements.
Note that users cannot inherit from FileDialog
due to internal abstract members, so protected
and abstract/virtual
changes are non-breaking.
Class diagram in PR. Public members diff:
namespace Microsoft.Win32;
public abstract partial class CommonDialog
{
protected CommonDialog();
public object Tag { get; set; }
protected virtual void CheckPermissionsToShowDialog();
protected virtual IntPtr HookProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam);
public abstract void Reset();
protected abstract bool RunDialog(IntPtr hwndOwner);
public virtual bool? ShowDialog();
public bool? ShowDialog(Window owner);
}
+ public abstract partial class CommonItemDialog : CommonDialog
+ {
+ private protected CommonItemDialog();
+
+ // lifted up from FileDialog
+ public IList<FileDialogCustomPlace> CustomPlaces { get; set; }
+ public bool DereferenceLinks { get; set; }
+ public string InitialDirectory { get; set; }
+ public string FileName { get; set; }
+ public string[] FileNames { get; }
+ public bool RestoreDirectory { get; set; }
+ public string SafeFileName { get; }
+ public string[] SafeFileNames { get; }
+ public string Title { get; set; }
+ public bool ValidateNames { get; set; }
+ public event CancelEventHandler FileOk { add; remove; }
+ protected void OnFileOk(CancelEventArgs e);
+ protected override bool RunDialog(IntPtr hwndOwner};
+ public override void Reset();
+ public override string ToString();
+
+ // new members
+ public bool AddToRecent { get; set; } // = true; (FOS_DONTADDTORECENT)
+ public bool AllowReadOnlyItems { get; set; } // = false; (FOS_NOREADONLYRETURN)
+ public Guid? ClientGuid { get; set; } // (IFileDialog::SetClientGuid)
+ public string DefaultDirectory { get; set; } // (IFileDialog::SetDefaultFolder)
+ public bool ShowHiddenFiles { get; set; } // = false; (FOS_FORCESHOWHIDDEN )
+ public string RootDirectory { get; set; } // (IFileDialog2::SetNavigationRoot)
+ }
- // unless noted otherwise, all removed members lifted up to CommonItemDialog
-
- public abstract partial class FileDialog : CommonDialog
+ public abstract partial class FileDialog : CommonItemDialog
{
protected FileDialog();
public bool AddExtension { get; set; }
public virtual bool CheckFileExists { get; set; }
public bool CheckPathExists { get; set; } // WAS VIRTUAL
- public IList<FileDialogCustomPlace> CustomPlaces { get; set; }
public string DefaultExt { get; set; }
- public bool DereferenceLinks { get; set; }
- public string InitialDirectory { get; set; }
- public string FileName { get; set; }
- public string[] FileNames { get; }
public string Filter { get; set; }
public int FilterIndex { get; set; }
- protected int Options { get; } // REMOVED
- public bool RestoreDirectory { get; set; }
- public string SafeFileName { get; }
- public string[] SafeFileNames { get; }
- public string Title { get; set; }
- public bool ValidateNames { get; set; }
- public event CancelEventHandler FileOk { add; remove; }
- protected override IntPtr HookProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam); // OVERRIDE REMOVED
- protected void OnFileOk(CancelEventArgs e);
public override void Reset();
protected override bool RunDialog(IntPtr hwndOwner);
public override string ToString();
}
public sealed partial class OpenFileDialog : FileDialog
{
public OpenFileDialog();
+ public bool ForcePreviewPane { get; set; } // = false; (FOS_FORCEPREVIEWPANEON)
public bool Multiselect { get; set; }
public bool ReadOnlyChecked { get; set; }
public bool ShowReadOnly { get; set; }
protected override void CheckPermissionsToShowDialog();
public System.IO.Stream OpenFile();
public System.IO.Stream[] OpenFiles();
public override void Reset();
}
+ public sealed partial class OpenFolderDialog : CommonItemDialog
+ {
+ public OpenFolderDialog();
+ public bool Multiselect { get; set; }
+ public override void Reset();
+ }
public sealed partial class SaveFileDialog : FileDialog
{
public SaveFileDialog();
+ public bool CreateTestFile { get; set; } // = true; (FOS_NOTESTFILECREATE)
public bool CreatePrompt { get; set; }
public bool OverwritePrompt { get; set; }
public System.IO.Stream OpenFile();
public override void Reset();
}
API Usage
OpenFolderDialog dialog = new OpenFolderDialog();
dialog.Multiselect = true;
dialog.ShowDialog();
string[] folders = dialog.FileNames;
Alternative Designs
There are several ways how the dialog could be introduced.
-
Pile on the technical debt and just add another flag property on
OpenFileDialog
. This is PickFolders option for OpenFileDialog (minimal) #6374.
Pros: Minimal risk, no breaking changes, reflects native API design.
Cons: When usingOpenFileDialog
for folders, some of its properties would become meaningless, some would start throwing. Any future enhancements to the API would become more and more difficult. -
Inherit directly from
CommonDialog
, or do not inherit form anything.
Pros: No backward compatibility requirements, can have clean design.
Cons: Does not reflect the fact the underlying API is a file dialog. User code that prepares the dialog would have to be duplicated across different types. Vast majority of the code that is currently inFileDialog
would have to be duplicated (and any future enhancements to it).
Moving the members around and/or splitting the inheritance chain seems to be the only way how to achieve a reasonably clean design and utilize existing code.
Ideally, the situation would be like this:
OpenFolderDialog : CommonDialog
OpenFileDialog : FileDialog : CommonDialog
where CommonDialog
would specifically mean common file dialog (dropping the current design where CommonDialog
was expected to be used for other system dialogs like color, font, etc.), and all the members that are shared between file and folder dialogs would be lifted up to CommonDialog
. Unfortunately touching CommonDialog
is a compatibility concern, because unlike FileDialog
, people can inherit directly from CommonDialog
and even if it mostly wouldn't be a breaking change, we would pollute it with file-related members even for unrelated inheritors.
Having to keep CommonDialog
what it is, we need to introduce another class in the inheritance chain for the shared members for both files and folders.
However, introducing a class in-between, e.g.
OpenFolderDialog : FileDialog : CommonDialog
OpenFileDialog : CommonFileDialog : FileDialog : CommonDialog
would mean some of the members would have to be moved down from FileDialog
to a derived class. That is a breaking change for when a variable is declared as FileDialog
, it would be missing the moved members.
The remaining options is to introduce a new base class (i.e. this proposal):
OpenFolderDialog : CommonItemDialog : CommonDialog
OpenFileDialog : FileDialog : CommonItemDialog : CommonDialog
and lift the members up from FileDialog
to a parent class. This will keep variables declared as FileDialog
working like before, the existing members would become inherited.
(There is also always the possibility to throw away the current implementation and create a clean design in a different namespace.)
Risks
No public API removed, a new base class introduced. Current applications using the types will keep working without recompilation. Code using reflection even on public types might break if it is not flattening them.
Applications with Windows XP compatibility flag will still work, however, the dialogs will use the IFileDialog
API rather than the legacy dialogs.