Skip to content

[API Proposal]: OpenFolderDialog #7689

Closed

Description

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.

  1. 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 using OpenFileDialog 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.

  2. 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 in FileDialog 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implemented

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions