diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs
index c049f9e763..98ef6d2530 100644
--- a/src/Avalonia.Base/Logging/LogArea.cs
+++ b/src/Avalonia.Base/Logging/LogArea.cs
@@ -44,5 +44,15 @@ namespace Avalonia.Logging
/// The log event comes from X11Platform.
///
public const string X11Platform = nameof(X11Platform);
+
+ ///
+ /// The log event comes from AndroidPlatform.
+ ///
+ public const string AndroidPlatform = nameof(AndroidPlatform);
+
+ ///
+ /// The log event comes from IOSPlatform.
+ ///
+ public const string IOSPlatform = nameof(IOSPlatform);
}
}
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
new file mode 100644
index 0000000000..5af02219ce
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Security;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage.FileIO;
+
+[Unstable]
+public class BclStorageFile : IStorageBookmarkFile
+{
+ private readonly FileInfo _fileInfo;
+
+ public BclStorageFile(FileInfo fileInfo)
+ {
+ _fileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo));
+ }
+
+ public bool CanOpenRead => true;
+
+ public bool CanOpenWrite => true;
+
+ public string Name => _fileInfo.Name;
+
+ public virtual bool CanBookmark => true;
+
+ public Task GetBasicPropertiesAsync()
+ {
+ var props = new StorageItemProperties();
+ if (_fileInfo.Exists)
+ {
+ props = new StorageItemProperties(
+ (ulong)_fileInfo.Length,
+ _fileInfo.CreationTimeUtc,
+ _fileInfo.LastAccessTimeUtc);
+ }
+ return Task.FromResult(props);
+ }
+
+ public Task GetParentAsync()
+ {
+ if (_fileInfo.Directory is { } directory)
+ {
+ return Task.FromResult(new BclStorageFolder(directory));
+ }
+ return Task.FromResult(null);
+ }
+
+ public Task OpenRead()
+ {
+ return Task.FromResult(_fileInfo.OpenRead());
+ }
+
+ public Task OpenWrite()
+ {
+ return Task.FromResult(_fileInfo.OpenWrite());
+ }
+
+ public virtual Task SaveBookmark()
+ {
+ return Task.FromResult(_fileInfo.FullName);
+ }
+
+ public Task ReleaseBookmark()
+ {
+ // No-op
+ return Task.CompletedTask;
+ }
+
+ public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
+ {
+ try
+ {
+ if (_fileInfo.Directory is not null)
+ {
+ uri = Path.IsPathRooted(_fileInfo.FullName) ?
+ new Uri(new Uri("file://"), _fileInfo.FullName) :
+ new Uri(_fileInfo.FullName, UriKind.Relative);
+ return true;
+ }
+
+ uri = null;
+ return false;
+ }
+ catch (SecurityException)
+ {
+ uri = null;
+ return false;
+ }
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ }
+
+ ~BclStorageFile()
+ {
+ Dispose(disposing: false);
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
new file mode 100644
index 0000000000..7267017eaf
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Security;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage.FileIO;
+
+[Unstable]
+public class BclStorageFolder : IStorageBookmarkFolder
+{
+ private readonly DirectoryInfo _directoryInfo;
+
+ public BclStorageFolder(DirectoryInfo directoryInfo)
+ {
+ _directoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo));
+ if (!_directoryInfo.Exists)
+ {
+ throw new ArgumentException("Directory must exist", nameof(directoryInfo));
+ }
+ }
+
+ public string Name => _directoryInfo.Name;
+
+ public bool CanBookmark => true;
+
+ public Task GetBasicPropertiesAsync()
+ {
+ var props = new StorageItemProperties(
+ null,
+ _directoryInfo.CreationTimeUtc,
+ _directoryInfo.LastAccessTimeUtc);
+ return Task.FromResult(props);
+ }
+
+ public Task GetParentAsync()
+ {
+ if (_directoryInfo.Parent is { } directory)
+ {
+ return Task.FromResult(new BclStorageFolder(directory));
+ }
+ return Task.FromResult(null);
+ }
+
+ public virtual Task SaveBookmark()
+ {
+ return Task.FromResult(_directoryInfo.FullName);
+ }
+
+ public Task ReleaseBookmark()
+ {
+ // No-op
+ return Task.CompletedTask;
+ }
+
+ public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
+ {
+ try
+ {
+ uri = Path.IsPathRooted(_directoryInfo.FullName) ?
+ new Uri(new Uri("file://"), _directoryInfo.FullName) :
+ new Uri(_directoryInfo.FullName, UriKind.Relative);
+
+ return true;
+ }
+ catch (SecurityException)
+ {
+ uri = null;
+ return false;
+ }
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ }
+
+ ~BclStorageFolder()
+ {
+ Dispose(disposing: false);
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
new file mode 100644
index 0000000000..469388021e
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage.FileIO;
+
+[Unstable]
+public abstract class BclStorageProvider : IStorageProvider
+{
+ public abstract bool CanOpen { get; }
+ public abstract Task> OpenFilePickerAsync(FilePickerOpenOptions options);
+
+ public abstract bool CanSave { get; }
+ public abstract Task SaveFilePickerAsync(FilePickerSaveOptions options);
+
+ public abstract bool CanPickFolder { get; }
+ public abstract Task> OpenFolderPickerAsync(FolderPickerOpenOptions options);
+
+ public virtual Task OpenFileBookmarkAsync(string bookmark)
+ {
+ var file = new FileInfo(bookmark);
+ return file.Exists
+ ? Task.FromResult(new BclStorageFile(file))
+ : Task.FromResult(null);
+ }
+
+ public virtual Task OpenFolderBookmarkAsync(string bookmark)
+ {
+ var folder = new DirectoryInfo(bookmark);
+ return folder.Exists
+ ? Task.FromResult(new BclStorageFolder(folder))
+ : Task.FromResult(null);
+ }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
new file mode 100644
index 0000000000..f90d0a5a2f
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
@@ -0,0 +1,40 @@
+using System;
+using System.IO;
+using System.Linq;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage.FileIO;
+
+[Unstable]
+public static class StorageProviderHelpers
+{
+ public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter)
+ {
+ var name = Path.GetFileName(path);
+ if (name != null && !Path.HasExtension(name))
+ {
+ if (filter?.Patterns?.Count > 0)
+ {
+ if (defaultExtension != null
+ && filter.Patterns.Contains(defaultExtension))
+ {
+ return Path.ChangeExtension(path, defaultExtension.TrimStart('.'));
+ }
+
+ var ext = filter.Patterns.FirstOrDefault(x => x != "*.*");
+ ext = ext?.Split(new[] { "*." }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
+ if (ext != null)
+ {
+ return Path.ChangeExtension(path, ext);
+ }
+ }
+
+ if (defaultExtension != null)
+ {
+ return Path.ChangeExtension(path, defaultExtension);
+ }
+ }
+
+ return path;
+ }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs
new file mode 100644
index 0000000000..98848ac9f7
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Platform.Storage;
+
+///
+/// Represents a name mapped to the associated file types (extensions).
+///
+public class FilePickerFileType
+{
+ public FilePickerFileType(string name)
+ {
+ Name = name;
+ }
+
+ ///
+ /// File type name.
+ ///
+ public string Name { get; }
+
+ ///
+ /// List of extensions in GLOB format. I.e. "*.png" or "*.*".
+ ///
+ ///
+ /// Used on Windows and Linux systems.
+ ///
+ public IReadOnlyList? Patterns { get; set; }
+
+ ///
+ /// List of extensions in MIME format.
+ ///
+ ///
+ /// Used on Android, Browser and Linux systems.
+ ///
+ public IReadOnlyList? MimeTypes { get; set; }
+
+ ///
+ /// List of extensions in Apple uniform format.
+ ///
+ ///
+ /// Used only on Apple devices.
+ /// See https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers.
+ ///
+ public IReadOnlyList? AppleUniformTypeIdentifiers { get; set; }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs b/src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs
new file mode 100644
index 0000000000..5da037999a
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs
@@ -0,0 +1,48 @@
+namespace Avalonia.Platform.Storage;
+
+///
+/// Dictionary of well known file types.
+///
+public static class FilePickerFileTypes
+{
+ public static FilePickerFileType All { get; } = new("All")
+ {
+ Patterns = new[] { "*.*" },
+ MimeTypes = new[] { "*/*" }
+ };
+
+ public static FilePickerFileType TextPlain { get; } = new("Plain Text")
+ {
+ Patterns = new[] { "*.txt" },
+ AppleUniformTypeIdentifiers = new[] { "public.plain-text" },
+ MimeTypes = new[] { "text/plain" }
+ };
+
+ public static FilePickerFileType ImageAll { get; } = new("All Images")
+ {
+ Patterns = new[] { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp" },
+ AppleUniformTypeIdentifiers = new[] { "public.image" },
+ MimeTypes = new[] { "image/*" }
+ };
+
+ public static FilePickerFileType ImageJpg { get; } = new("JPEG image")
+ {
+ Patterns = new[] { "*.jpg", "*.jpeg" },
+ AppleUniformTypeIdentifiers = new[] { "public.jpeg" },
+ MimeTypes = new[] { "image/jpeg" }
+ };
+
+ public static FilePickerFileType ImagePng { get; } = new("PNG image")
+ {
+ Patterns = new[] { "*.png" },
+ AppleUniformTypeIdentifiers = new[] { "public.png" },
+ MimeTypes = new[] { "image/png" }
+ };
+
+ public static FilePickerFileType Pdf { get; } = new("PDF document")
+ {
+ Patterns = new[] { "*.pdf" },
+ AppleUniformTypeIdentifiers = new[] { "com.adobe.pdf" },
+ MimeTypes = new[] { "application/pdf" }
+ };
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs b/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs
new file mode 100644
index 0000000000..1f9202b0e7
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Platform.Storage;
+
+///
+/// Options class for method.
+///
+public class FilePickerOpenOptions
+{
+ ///
+ /// Gets or sets the text that appears in the title bar of a file dialog.
+ ///
+ public string? Title { get; set; }
+
+ ///
+ /// Gets or sets the initial location where the file open picker looks for files to present to the user.
+ ///
+ public IStorageFolder? SuggestedStartLocation { get; set; }
+
+ ///
+ /// Gets or sets an option indicating whether open picker allows users to select multiple files.
+ ///
+ public bool AllowMultiple { get; set; }
+
+ ///
+ /// Gets or sets the collection of file types that the file open picker displays.
+ ///
+ public IReadOnlyList? FileTypeFilter { get; set; }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs b/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs
new file mode 100644
index 0000000000..0f4d690f7a
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Platform.Storage;
+
+///
+/// Options class for method.
+///
+public class FilePickerSaveOptions
+{
+ ///
+ /// Gets or sets the text that appears in the title bar of a file dialog.
+ ///
+ public string? Title { get; set; }
+
+ ///
+ /// Gets or sets the file name that the file save picker suggests to the user.
+ ///
+ public string? SuggestedFileName { get; set; }
+
+ ///
+ /// Gets or sets the default extension to be used to save the file.
+ ///
+ public string? DefaultExtension { get; set; }
+
+ ///
+ /// Gets or sets the initial location where the file open picker looks for files to present to the user.
+ ///
+ public IStorageFolder? SuggestedStartLocation { get; set; }
+
+ ///
+ /// Gets or sets the collection of valid file types that the user can choose to assign to a file.
+ ///
+ public IReadOnlyList? FileTypeChoices { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether file open picker displays a warning if the user specifies the name of a file that already exists.
+ ///
+ public bool? ShowOverwritePrompt { get; set; }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs b/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs
new file mode 100644
index 0000000000..de90da30b2
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs
@@ -0,0 +1,22 @@
+namespace Avalonia.Platform.Storage;
+
+///
+/// Options class for method.
+///
+public class FolderPickerOpenOptions
+{
+ ///
+ /// Gets or sets the text that appears in the title bar of a folder dialog.
+ ///
+ public string? Title { get; set; }
+
+ ///
+ /// Gets or sets an option indicating whether open picker allows users to select multiple folders.
+ ///
+ public bool AllowMultiple { get; set; }
+
+ ///
+ /// Gets or sets the initial location where the file open picker looks for files to present to the user.
+ ///
+ public IStorageFolder? SuggestedStartLocation { get; set; }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs
new file mode 100644
index 0000000000..65811b7fbd
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs
@@ -0,0 +1,21 @@
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage;
+
+[NotClientImplementable]
+public interface IStorageBookmarkItem : IStorageItem
+{
+ Task ReleaseBookmark();
+}
+
+[NotClientImplementable]
+public interface IStorageBookmarkFile : IStorageFile, IStorageBookmarkItem
+{
+}
+
+[NotClientImplementable]
+public interface IStorageBookmarkFolder : IStorageFolder, IStorageBookmarkItem
+{
+
+}
diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs
new file mode 100644
index 0000000000..2f12514e50
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs
@@ -0,0 +1,32 @@
+using System.IO;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage;
+
+///
+/// Represents a file. Provides information about the file and its contents, and ways to manipulate them.
+///
+[NotClientImplementable]
+public interface IStorageFile : IStorageItem
+{
+ ///
+ /// Returns true, if file is readable.
+ ///
+ bool CanOpenRead { get; }
+
+ ///
+ /// Opens a stream for read access.
+ ///
+ Task OpenRead();
+
+ ///
+ /// Returns true, if file is writeable.
+ ///
+ bool CanOpenWrite { get; }
+
+ ///
+ /// Opens stream for writing to the file.
+ ///
+ Task OpenWrite();
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
new file mode 100644
index 0000000000..83b316bc3b
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
@@ -0,0 +1,12 @@
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage;
+
+///
+/// Manipulates folders and their contents, and provides information about them.
+///
+[NotClientImplementable]
+public interface IStorageFolder : IStorageItem
+{
+
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs
new file mode 100644
index 0000000000..078311a286
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage;
+
+///
+/// Manipulates storage items (files and folders) and their contents, and provides information about them
+///
+///
+/// This interface inherits . It's recommended to dispose when it's not used anymore.
+///
+[NotClientImplementable]
+public interface IStorageItem : IDisposable
+{
+ ///
+ /// Gets the name of the item including the file name extension if there is one.
+ ///
+ string Name { get; }
+
+ ///
+ /// Gets the full file-system path of the item, if the item has a path.
+ ///
+ ///
+ /// Android backend might return file path with "content:" scheme.
+ /// Browser and iOS backends might return relative uris.
+ ///
+ bool TryGetUri([NotNullWhen(true)] out Uri? uri);
+
+ ///
+ /// Gets the basic properties of the current item.
+ ///
+ Task GetBasicPropertiesAsync();
+
+ ///
+ /// Returns true is item can be bookmarked and reused later.
+ ///
+ bool CanBookmark { get; }
+
+ ///
+ /// Saves items to a bookmark.
+ ///
+ ///
+ /// Returns identifier of a bookmark. Can be null if OS denied request.
+ ///
+ Task SaveBookmark();
+
+ ///
+ /// Gets the parent folder of the current storage item.
+ ///
+ Task GetParentAsync();
+}
diff --git a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
new file mode 100644
index 0000000000..32fb148790
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform.Storage;
+
+[NotClientImplementable]
+public interface IStorageProvider
+{
+ ///
+ /// Returns true if it's possible to open file picker on the current platform.
+ ///
+ bool CanOpen { get; }
+
+ ///
+ /// Opens file picker dialog.
+ ///
+ /// Array of selected or empty collection if user canceled the dialog.
+ Task> OpenFilePickerAsync(FilePickerOpenOptions options);
+
+ ///
+ /// Returns true if it's possible to open save file picker on the current platform.
+ ///
+ bool CanSave { get; }
+
+ ///
+ /// Opens save file picker dialog.
+ ///
+ /// Saved or null if user canceled the dialog.
+ Task SaveFilePickerAsync(FilePickerSaveOptions options);
+
+ ///
+ /// Returns true if it's possible to open folder picker on the current platform.
+ ///
+ bool CanPickFolder { get; }
+
+ ///
+ /// Opens folder picker dialog.
+ ///
+ /// Array of selected or empty collection if user canceled the dialog.
+ Task> OpenFolderPickerAsync(FolderPickerOpenOptions options);
+
+ ///
+ /// Open from the bookmark ID.
+ ///
+ /// Bookmark ID.
+ /// Bookmarked file or null if OS denied request.
+ Task OpenFileBookmarkAsync(string bookmark);
+
+ ///
+ /// Open from the bookmark ID.
+ ///
+ /// Bookmark ID.
+ /// Bookmarked folder or null if OS denied request.
+ Task OpenFolderBookmarkAsync(string bookmark);
+}
diff --git a/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs b/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs
new file mode 100644
index 0000000000..41b9bfa941
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs
@@ -0,0 +1,43 @@
+using System;
+
+namespace Avalonia.Platform.Storage;
+
+///
+/// Provides access to the content-related properties of an item (like a file or folder).
+///
+public class StorageItemProperties
+{
+ public StorageItemProperties(
+ ulong? size = null,
+ DateTimeOffset? dateCreated = null,
+ DateTimeOffset? dateModified = null)
+ {
+ Size = size;
+ DateCreated = dateCreated;
+ DateModified = dateModified;
+ }
+
+ ///
+ /// Gets the size of the file in bytes.
+ ///
+ ///
+ /// Can be null if property is not available.
+ ///
+ public ulong? Size { get; }
+
+ ///
+ /// Gets the date and time that the current folder was created.
+ ///
+ ///
+ /// Can be null if property is not available.
+ ///
+ public DateTimeOffset? DateCreated { get; }
+
+ ///
+ /// Gets the date and time of the last time the file was modified.
+ ///
+ ///
+ /// Can be null if property is not available.
+ ///
+ public DateTimeOffset? DateModified { get; }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs b/src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs
new file mode 100644
index 0000000000..3eee8e848e
--- /dev/null
+++ b/src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs
@@ -0,0 +1,12 @@
+#nullable enable
+using Avalonia.Platform.Storage;
+
+namespace Avalonia.Controls.Platform;
+
+///
+/// Factory allows to register custom storage provider instead of native implementation.
+///
+public interface IStorageProviderFactory
+{
+ IStorageProvider CreateProvider(TopLevel topLevel);
+}
diff --git a/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs b/src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs
similarity index 88%
rename from src/Avalonia.Controls/Platform/ISystemDialogImpl.cs
rename to src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs
index 715eda5cfa..996fff6775 100644
--- a/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs
+++ b/src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs
@@ -1,3 +1,4 @@
+using System;
using System.Threading.Tasks;
using Avalonia.Metadata;
@@ -6,6 +7,7 @@ namespace Avalonia.Controls.Platform
///
/// Defines a platform-specific system dialog implementation.
///
+ [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
[Unstable]
public interface ISystemDialogImpl
{
diff --git a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs
new file mode 100644
index 0000000000..2775c53803
--- /dev/null
+++ b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+#nullable enable
+
+namespace Avalonia.Controls.Platform
+{
+ ///
+ /// Defines a platform-specific system dialog implementation.
+ ///
+ [Obsolete]
+ internal class SystemDialogImpl : ISystemDialogImpl
+ {
+ public async Task ShowFileDialogAsync(FileDialog dialog, Window parent)
+ {
+ if (dialog is OpenFileDialog openDialog)
+ {
+ var filePicker = parent.StorageProvider;
+ if (!filePicker.CanOpen)
+ {
+ return null;
+ }
+
+ var options = openDialog.ToFilePickerOpenOptions();
+
+ var files = await filePicker.OpenFilePickerAsync(options);
+ return files
+ .Select(file => file.TryGetUri(out var fullPath)
+ ? fullPath.LocalPath
+ : file.Name)
+ .ToArray();
+ }
+ else if (dialog is SaveFileDialog saveDialog)
+ {
+ var filePicker = parent.StorageProvider;
+ if (!filePicker.CanSave)
+ {
+ return null;
+ }
+
+ var options = saveDialog.ToFilePickerSaveOptions();
+
+ var file = await filePicker.SaveFilePickerAsync(options);
+ if (file is null)
+ {
+ return null;
+ }
+
+ var filePath = file.TryGetUri(out var fullPath)
+ ? fullPath.LocalPath
+ : file.Name;
+ return new[] { filePath };
+ }
+ return null;
+ }
+
+ public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
+ {
+ var filePicker = parent.StorageProvider;
+ if (!filePicker.CanPickFolder)
+ {
+ return null;
+ }
+
+ var options = dialog.ToFolderPickerOpenOptions();
+
+ var folders = await filePicker.OpenFolderPickerAsync(options);
+ return folders
+ .Select(f => f.TryGetUri(out var uri) ? uri.LocalPath : null)
+ .FirstOrDefault(u => u is not null);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs b/src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs
new file mode 100644
index 0000000000..b42040f3c3
--- /dev/null
+++ b/src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs
@@ -0,0 +1,11 @@
+using Avalonia.Metadata;
+using Avalonia.Platform;
+using Avalonia.Platform.Storage;
+
+namespace Avalonia.Controls.Platform;
+
+[Unstable]
+public interface ITopLevelImplWithStorageProvider : ITopLevelImpl
+{
+ public IStorageProvider StorageProvider { get; }
+}
diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs
index 093f10be51..f3fb4d9a6d 100644
--- a/src/Avalonia.Controls/SystemDialog.cs
+++ b/src/Avalonia.Controls/SystemDialog.cs
@@ -3,12 +3,15 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
+using Avalonia.Platform.Storage;
+using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Controls
{
///
/// Base class for system file dialogs.
///
+ [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public abstract class FileDialog : FileSystemDialog
{
///
@@ -26,6 +29,7 @@ namespace Avalonia.Controls
///
/// Base class for system file and directory dialogs.
///
+ [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public abstract class FileSystemDialog : SystemDialog
{
[Obsolete("Use Directory")]
@@ -45,6 +49,7 @@ namespace Avalonia.Controls
///
/// Represents a system dialog that prompts the user to select a location for saving a file.
///
+ [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public class SaveFileDialog : FileDialog
{
///
@@ -73,11 +78,27 @@ namespace Avalonia.Controls
return (await service.ShowFileDialogAsync(this, parent) ??
Array.Empty()).FirstOrDefault();
}
+
+ public FilePickerSaveOptions ToFilePickerSaveOptions()
+ {
+ return new FilePickerSaveOptions
+ {
+ SuggestedFileName = InitialFileName,
+ DefaultExtension = DefaultExtension,
+ FileTypeChoices = Filters?.Select(f => new FilePickerFileType(f.Name!) { Patterns = f.Extensions.Select(e => $"*.{e}").ToArray() }).ToArray(),
+ Title = Title,
+ SuggestedStartLocation = InitialDirectory is { } directory
+ ? new BclStorageFolder(new System.IO.DirectoryInfo(directory))
+ : null,
+ ShowOverwritePrompt = ShowOverwritePrompt
+ };
+ }
}
///
/// Represents a system dialog that allows the user to select one or more files to open.
///
+ [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public class OpenFileDialog : FileDialog
{
///
@@ -100,11 +121,25 @@ namespace Avalonia.Controls
var service = AvaloniaLocator.Current.GetRequiredService();
return service.ShowFileDialogAsync(this, parent);
}
+
+ public FilePickerOpenOptions ToFilePickerOpenOptions()
+ {
+ return new FilePickerOpenOptions
+ {
+ AllowMultiple = AllowMultiple,
+ FileTypeFilter = Filters?.Select(f => new FilePickerFileType(f.Name!) { Patterns = f.Extensions.Select(e => $"*.{e}").ToArray() }).ToArray(),
+ Title = Title,
+ SuggestedStartLocation = InitialDirectory is { } directory
+ ? new BclStorageFolder(new System.IO.DirectoryInfo(directory))
+ : null
+ };
+ }
}
///
/// Represents a system dialog that allows the user to select a directory.
///
+ [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public class OpenFolderDialog : FileSystemDialog
{
[Obsolete("Use Directory")]
@@ -129,14 +164,35 @@ namespace Avalonia.Controls
var service = AvaloniaLocator.Current.GetRequiredService();
return service.ShowFolderDialogAsync(this, parent);
}
+
+ public FolderPickerOpenOptions ToFolderPickerOpenOptions()
+ {
+ return new FolderPickerOpenOptions
+ {
+ Title = Title,
+ SuggestedStartLocation = InitialDirectory is { } directory
+ ? new BclStorageFolder(new System.IO.DirectoryInfo(directory))
+ : null
+ };
+ }
}
///
/// Base class for system dialogs.
///
+ [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public abstract class SystemDialog
{
+ static SystemDialog()
+ {
+ if (AvaloniaLocator.Current.GetService() is null)
+ {
+ // Register default implementation.
+ AvaloniaLocator.CurrentMutable.Bind().ToSingleton();
+ }
+ }
+
///
/// Gets or sets the dialog title.
///
@@ -146,6 +202,7 @@ namespace Avalonia.Controls
///
/// Represents a filter in an or an .
///
+ [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public class FileDialogFilter
{
///
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index 57fb82485c..d09b824958 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -11,6 +11,7 @@ using Avalonia.Logging;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Platform;
+using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.Utilities;
@@ -93,7 +94,8 @@ namespace Avalonia.Controls
private ILayoutManager? _layoutManager;
private Border? _transparencyFallbackBorder;
private TargetWeakEventSubscriber? _resourcesChangesSubscriber;
-
+ private IStorageProvider? _storageProvider;
+
///
/// Initializes static members of the class.
///
@@ -319,6 +321,11 @@ namespace Avalonia.Controls
double IRenderRoot.RenderScaling => PlatformImpl?.RenderScaling ?? 1;
IStyleHost IStyleHost.StylingParent => _globalStyles!;
+
+ public IStorageProvider StorageProvider => _storageProvider
+ ??= AvaloniaLocator.Current.GetService()?.CreateProvider(this)
+ ?? (PlatformImpl as ITopLevelImplWithStorageProvider)?.StorageProvider
+ ?? throw new InvalidOperationException("StorageProvider platform implementation is not available.");
IRenderTarget IRenderRoot.CreateRenderTarget() => CreateRenderTarget();