Browse Source

Update headless implementations, managed and samples

Max Katz 3 years ago
parent
commit
e717cce7e8

+ 1 - 1
samples/ControlCatalog/ControlCatalog.csproj

@@ -1,6 +1,6 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <Nullable>enable</Nullable>    
   </PropertyGroup>

+ 3 - 0
samples/ControlCatalog/MainView.xaml

@@ -69,6 +69,9 @@
       <TabItem Header="CalendarDatePicker">
         <pages:CalendarDatePickerPage />
       </TabItem>
+      <TabItem Header="Dialogs">
+        <pages:DialogsPage />
+      </TabItem>
       <TabItem Header="Drag+Drop">
         <pages:DragAndDropPage />
       </TabItem>

+ 0 - 5
samples/ControlCatalog/MainView.xaml.cs

@@ -24,11 +24,6 @@ namespace ControlCatalog
             {
                 IList tabItems = ((IList)sideBar.Items);
                 tabItems.Add(new TabItem()
-                {
-                    Header = "Dialogs",
-                    Content = new DialogsPage()
-                });
-                tabItems.Add(new TabItem()
                 {
                     Header = "Screens",
                     Content = new ScreenPage()

+ 52 - 24
samples/ControlCatalog/Pages/DialogsPage.xaml

@@ -1,29 +1,57 @@
-<UserControl xmlns="https://github.com/avaloniaui"
-             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             x:Class="ControlCatalog.Pages.DialogsPage">
-  <StackPanel Orientation="Vertical" Spacing="4" Margin="4">
-      <CheckBox Name="UseFilters">Use filters</CheckBox>
-      <Button Name="OpenFile">_Open File</Button>
-      <Button Name="OpenMultipleFiles">Open _Multiple File</Button>
-      <Button Name="SaveFile">_Save File</Button>
-      <Button Name="SelectFolder">Select Fo_lder</Button>
-      <Button Name="OpenBoth">Select _Both</Button>
+<UserControl x:Class="ControlCatalog.Pages.DialogsPage"
+             xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+  <StackPanel Margin="4"
+              Orientation="Vertical"
+              Spacing="4">
 
-      <TextBlock x:Name="PickerLastResultsVisible"
-                 Classes="h2"
-                 IsVisible="False"
-                 Text="Last picker results:" />
-      <ItemsPresenter x:Name="PickerLastResults" />
+    <TextBlock Text="Windows:" />
 
-      <TextBlock Margin="0, 8, 0, 0"
-                 Classes="h1"
-                 Text="Window dialogs" />
-      <Button Name="DecoratedWindow">Decorated _window</Button>
-      <Button Name="DecoratedWindowDialog">Decorated w_indow (dialog)</Button>
-      <Button Name="Dialog" ToolTip.Tip="Shows a dialog">_Dialog</Button>
-      <Button Name="DialogNoTaskbar">Dialog (_No taskbar icon)</Button>
-      <Button Name="OwnedWindow">Own_ed window</Button>
-      <Button Name="OwnedWindowNoTaskbar">Owned window (No tas_kbar icon)</Button>
+    <Expander Header="Window dialogs">
+      <StackPanel Spacing="4">
+        <Button Name="DecoratedWindow">Decorated _window</Button>
+        <Button Name="DecoratedWindowDialog">Decorated w_indow (dialog)</Button>
+        <Button Name="Dialog" ToolTip.Tip="Shows a dialog">_Dialog</Button>
+        <Button Name="DialogNoTaskbar">Dialog (_No taskbar icon)</Button>
+        <Button Name="OwnedWindow">Own_ed window</Button>
+        <Button Name="OwnedWindowNoTaskbar">Owned window (No tas_kbar icon)</Button>
+      </StackPanel>
+    </Expander>
+
+    <TextBlock Margin="0,20,0,0" Text="Pickers:" />
+
+    <CheckBox Name="UseFilters">Use filters</CheckBox>
+    <Expander Header="FilePicker API">
+      <StackPanel Spacing="4">
+        <CheckBox Name="ForceManaged">Force managed dialog</CheckBox>
+        <CheckBox Name="OpenMultiple">Open multiple</CheckBox>
+        <Button Name="OpenFolderPicker">Select Fo_lder</Button>
+        <Button Name="OpenFilePicker">_Open File</Button>
+        <Button Name="SaveFilePicker">_Save File</Button>
+        <Button Name="OpenFileFromBookmark">Open File Bookmark</Button>
+        <Button Name="OpenFolderFromBookmark">Open Folder Bookmark</Button>
+      </StackPanel>
+    </Expander>
+    <Expander Header="Legacy OpenFileDialog">
+      <StackPanel Spacing="4">
+        <Button Name="OpenFile">_Open File</Button>
+        <Button Name="OpenMultipleFiles">Open _Multiple File</Button>
+        <Button Name="SaveFile">_Save File</Button>
+        <Button Name="SelectFolder">Select Fo_lder</Button>
+        <Button Name="OpenBoth">Select _Both</Button>
+      </StackPanel>
+    </Expander>
+
+    <TextBlock x:Name="PickerLastResultsVisible"
+               Classes="h2"
+               IsVisible="False"
+               Text="Last picker results:" />
+    <ItemsPresenter x:Name="PickerLastResults" />
+
+    <TextBox Name="BookmarkContainer" Watermark="Bookmark" />
+    <TextBox Name="OpenedFileContent"
+             MaxLines="10"
+             Watermark="Picked file content" />
 
   </StackPanel>
 </UserControl>

+ 211 - 23
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@@ -1,13 +1,21 @@
 using System;
+using System.Buffers;
 using System.Collections.Generic;
 using System.Linq;
 using System.Reflection;
+using System.Threading.Tasks;
+using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
 using Avalonia.Dialogs;
 using Avalonia.Layout;
 using Avalonia.Markup.Xaml;
-#pragma warning disable 4014
+using Avalonia.Platform.Storage;
+using Avalonia.Platform.Storage.FileIO;
+
+#pragma warning disable CS0618 // Type or member is obsolete
+#nullable enable
+
 namespace ControlCatalog.Pages
 {
     public class DialogsPage : UserControl
@@ -18,13 +26,16 @@ namespace ControlCatalog.Pages
 
             var results = this.Get<ItemsPresenter>("PickerLastResults");
             var resultsVisible = this.Get<TextBlock>("PickerLastResultsVisible");
+            var bookmarkContainer = this.Get<TextBox>("BookmarkContainer");
+            var openedFileContent = this.Get<TextBox>("OpenedFileContent");
+            var openMultiple = this.Get<CheckBox>("OpenMultiple");
 
-            string? lastSelectedDirectory = null;
+            IStorageFolder? lastSelectedDirectory = null;
 
-            List<FileDialogFilter>? GetFilters()
+            List<FileDialogFilter> GetFilters()
             {
                 if (this.Get<CheckBox>("UseFilters").IsChecked != true)
-                    return null;
+                    return new List<FileDialogFilter>();
                 return  new List<FileDialogFilter>
                 {
                     new FileDialogFilter
@@ -39,12 +50,23 @@ namespace ControlCatalog.Pages
                 };
             }
 
+            List<FilePickerFileType>? GetFileTypes()
+            {
+                if (this.Get<CheckBox>("UseFilters").IsChecked != true)
+                    return null;
+                return new List<FilePickerFileType>
+                {
+                    FilePickerFileTypes.All,
+                    FilePickerFileTypes.TextPlain
+                };
+            }
+
             this.Get<Button>("OpenFile").Click += async delegate
             {
                 // Almost guaranteed to exist
-                var fullPath = Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName;
-                var initialFileName = fullPath == null ? null : System.IO.Path.GetFileName(fullPath);
-                var initialDirectory = fullPath == null ? null : System.IO.Path.GetDirectoryName(fullPath);
+                var uri = Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName;
+                var initialFileName = uri == null ? null : System.IO.Path.GetFileName(uri);
+                var initialDirectory = uri == null ? null : System.IO.Path.GetDirectoryName(uri);
 
                 var result = await new OpenFileDialog()
                 {
@@ -62,7 +84,7 @@ namespace ControlCatalog.Pages
                 {
                     Title = "Open multiple files",
                     Filters = GetFilters(),
-                    Directory = lastSelectedDirectory,
+                    Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
                     AllowMultiple = true
                 }.ShowAsync(GetWindow());
                 results.Items = result;
@@ -70,11 +92,13 @@ namespace ControlCatalog.Pages
             };
             this.Get<Button>("SaveFile").Click += async delegate
             {
+                var filters = GetFilters();
                 var result = await new SaveFileDialog()
                 {
                     Title = "Save file",
-                    Filters = GetFilters(),
-                    Directory = lastSelectedDirectory,
+                    Filters = filters,
+                    Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
+                    DefaultExtension = filters?.Any() == true ? "txt" : null,
                     InitialFileName = "test.txt"
                 }.ShowAsync(GetWindow());
                 results.Items = new[] { result };
@@ -85,14 +109,9 @@ namespace ControlCatalog.Pages
                 var result = await new OpenFolderDialog()
                 {
                     Title = "Select folder",
-                    Directory = lastSelectedDirectory,
+                    Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null
                 }.ShowAsync(GetWindow());
-
-                if (!string.IsNullOrEmpty(result))
-                {
-                    lastSelectedDirectory = result;
-                }
-
+                lastSelectedDirectory = new BclStorageFolder(new System.IO.DirectoryInfo(result));
                 results.Items = new [] { result };
                 resultsVisible.IsVisible = result != null;
             };
@@ -101,7 +120,7 @@ namespace ControlCatalog.Pages
                 var result = await new OpenFileDialog()
                 {
                     Title = "Select both",
-                    Directory = lastSelectedDirectory,
+                    Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
                     AllowMultiple = true
                 }.ShowManagedAsync(GetWindow(), new ManagedFileDialogOptions
                 {
@@ -116,20 +135,20 @@ namespace ControlCatalog.Pages
             };
             this.Get<Button>("DecoratedWindowDialog").Click += delegate
             {
-                new DecoratedWindow().ShowDialog(GetWindow());
+                _ = new DecoratedWindow().ShowDialog(GetWindow());
             };
             this.Get<Button>("Dialog").Click += delegate
             {
                 var window = CreateSampleWindow();
                 window.Height = 200;
-                window.ShowDialog(GetWindow());
+                _ = window.ShowDialog(GetWindow());
             };
             this.Get<Button>("DialogNoTaskbar").Click += delegate
             {
                 var window = CreateSampleWindow();
                 window.Height = 200;
                 window.ShowInTaskbar = false;
-                window.ShowDialog(GetWindow());
+                _ = window.ShowDialog(GetWindow());
             };
             this.Get<Button>("OwnedWindow").Click += delegate
             {
@@ -146,13 +165,166 @@ namespace ControlCatalog.Pages
 
                 window.Show(GetWindow());
             };
+
+            this.Get<Button>("OpenFilePicker").Click += async delegate
+            {
+                var result = await GetStorageProvider().OpenFilePickerAsync(new FilePickerOpenOptions()
+                {
+                    Title = "Open file",
+                    FileTypeFilter = GetFileTypes(),
+                    SuggestedStartLocation = lastSelectedDirectory,
+                    AllowMultiple = openMultiple.IsChecked == true
+                });
+
+                await SetPickerResult(result);
+            };
+            this.Get<Button>("SaveFilePicker").Click += async delegate
+            {
+                var fileTypes = GetFileTypes();
+                var file = await GetStorageProvider().SaveFilePickerAsync(new FilePickerSaveOptions()
+                {
+                    Title = "Save file",
+                    FileTypeChoices = fileTypes,
+                    SuggestedStartLocation = lastSelectedDirectory,
+                    SuggestedFileName = "FileName",
+                    DefaultExtension = fileTypes?.Any() == true ? "txt" : null,
+                    ShowOverwritePrompt = false
+                });
+
+                if (file is not null && file.CanOpenWrite)
+                {
+                    // Sync disposal of StreamWriter is not supported on WASM
+#if NET6_0_OR_GREATER
+                    await using var stream = await file.OpenWrite();
+                    await using var reader = new System.IO.StreamWriter(stream);
+#else
+                    using var stream = await file.OpenWrite();
+                    using var reader = new System.IO.StreamWriter(stream);
+#endif
+                    await reader.WriteLineAsync(openedFileContent.Text);
+
+                    lastSelectedDirectory = await file.GetParentAsync();
+                }
+
+                await SetPickerResult(file is null ? null : new [] {file});
+            };
+            this.Get<Button>("OpenFolderPicker").Click += async delegate
+            {
+                var folders = await GetStorageProvider().OpenFolderPickerAsync(new FolderPickerOpenOptions()
+                {
+                    Title = "Folder file",
+                    SuggestedStartLocation = lastSelectedDirectory,
+                    AllowMultiple = openMultiple.IsChecked == true
+                });
+
+                await SetPickerResult(folders);
+
+                lastSelectedDirectory = folders.FirstOrDefault();
+            };
+            this.Get<Button>("OpenFileFromBookmark").Click += async delegate
+            {
+                var file = bookmarkContainer.Text is not null
+                    ? await GetStorageProvider().OpenFileBookmarkAsync(bookmarkContainer.Text)
+                    : null;
+
+                await SetPickerResult(file is null ? null : new[] { file });
+            };
+            this.Get<Button>("OpenFolderFromBookmark").Click += async delegate
+            {
+                var folder = bookmarkContainer.Text is not null
+                    ? await GetStorageProvider().OpenFolderBookmarkAsync(bookmarkContainer.Text)
+                    : null;
+
+                await SetPickerResult(folder is null ? null : new[] { folder });
+                
+                lastSelectedDirectory = folder;
+            };
+
+            async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items)
+            {
+                items ??= Array.Empty<IStorageItem>();
+                var mappedResults = items.Select(FullPathOrName).ToList();
+                bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmark() : "Can't bookmark";
+
+                if (items.FirstOrDefault() is IStorageItem item)
+                {
+                    var resultText = item is IStorageFile ? "File:" : "Folder:";
+                    resultText += Environment.NewLine;
+
+                    var props = await item.GetBasicPropertiesAsync();
+                    resultText += @$"Size: {props.Size}
+DateCreated: {props.DateCreated}
+DateModified: {props.DateModified}
+CanBookmark: {item.CanBookmark}
+";
+                    if (item is IStorageFile file)
+                    {
+                        resultText += @$"
+CanOpenRead: {file.CanOpenRead}
+CanOpenWrite: {file.CanOpenWrite}
+Content:
+";
+                        if (file.CanOpenRead)
+                        {
+#if NET6_0_OR_GREATER
+                            await using var stream = await file.OpenRead();
+#else
+                            using var stream = await file.OpenRead();
+#endif
+                            using var reader = new System.IO.StreamReader(stream);
+
+                            // 4GB file test, shouldn't load more than 10000 chars into a memory.
+                            const int length = 10000;
+                            var buffer = ArrayPool<char>.Shared.Rent(length);
+                            try
+                            {
+                                var charsRead = await reader.ReadAsync(buffer, 0, length);
+                                resultText += new string(buffer, 0, charsRead);
+                            }
+                            finally
+                            {
+                                ArrayPool<char>.Shared.Return(buffer);
+                            }
+                        }
+                    }
+
+                    openedFileContent.Text = resultText;
+
+                    lastSelectedDirectory = await item.GetParentAsync();
+                    if (lastSelectedDirectory is not null)
+                    {
+                        mappedResults.Insert(0,  "Parent: " + FullPathOrName(lastSelectedDirectory));
+                    }
+                }
+
+                results.Items = mappedResults;
+                resultsVisible.IsVisible = mappedResults.Any();
+            }
+        }
+
+        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToVisualTree(e);
+
+            var openedFileContent = this.Get<TextBox>("OpenedFileContent");
+            try
+            {
+                var storageProvider = GetStorageProvider();
+                openedFileContent.Text = $@"CanOpen: {storageProvider.CanOpen}
+CanSave: {storageProvider.CanSave}
+CanPickFolder: {storageProvider.CanPickFolder}";
+            }
+            catch (Exception ex)
+            {
+                openedFileContent.Text = "Storage provider is not available: " + ex.Message;
+            }
         }
 
         private Window CreateSampleWindow()
         {
             Button button;
             Button dialogButton;
-            
+
             var window = new Window
             {
                 Height = 200,
@@ -191,7 +363,22 @@ namespace ControlCatalog.Pages
             return window;
         }
 
-        Window GetWindow() => this.VisualRoot as Window  ?? throw new NullReferenceException("Invalid Owner");
+        private IStorageProvider GetStorageProvider()
+        {
+            var forceManaged = this.Get<CheckBox>("ForceManaged").IsChecked ?? false;
+            return forceManaged
+                ? new ManagedStorageProvider<Window>(GetWindow(), null)
+                : GetTopLevel().StorageProvider;
+        }
+
+        private static string FullPathOrName(IStorageItem? item)
+        {
+            if (item is null) return "(null)";
+            return item.TryGetUri(out var uri) ? uri.ToString() : item.Name;
+        }
+
+        Window GetWindow() => this.VisualRoot as Window ?? throw new NullReferenceException("Invalid Owner");
+        TopLevel GetTopLevel() => this.VisualRoot as TopLevel ?? throw new NullReferenceException("Invalid Owner");
 
         private void InitializeComponent()
         {
@@ -199,3 +386,4 @@ namespace ControlCatalog.Pages
         }
     }
 }
+#pragma warning restore CS0618 // Type or member is obsolete

+ 3 - 3
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@@ -76,21 +76,21 @@
       <StackPanel Orientation="Vertical" Margin="10">
         <Label Target="upDown" FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of decimal NumericUpDown:</Label>
         <NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5"
-                       CultureInfo="en-US" VerticalAlignment="Center" Value="{Binding DecimalValue}"
+                       VerticalAlignment="Center" Value="{Binding DecimalValue}"
                        Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
       </StackPanel>
 
       <StackPanel Orientation="Vertical" Margin="10">
         <Label Target="DoubleUpDown" FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of double NumericUpDown:</Label>
         <NumericUpDown Name="DoubleUpDown" Minimum="0" Maximum="10" Increment="0.5"
-                       CultureInfo="en-US" VerticalAlignment="Center" Value="{Binding DoubleValue}"
+                       VerticalAlignment="Center" Value="{Binding DoubleValue}"
                        Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
       </StackPanel>
       
       <StackPanel Orientation="Vertical" Margin="10">
         <Label Target="ValidationUpDown" FontSize="14" FontWeight="Bold" VerticalAlignment="Center">NumericUpDown with Validation Errors:</Label>
         <NumericUpDown x:Name="ValidationUpDown" Minimum="0" Maximum="10" Increment="0.5"
-                       CultureInfo="en-US" VerticalAlignment="Center"
+                       VerticalAlignment="Center"
                        Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}">
           <DataValidationErrors.Error>
             <sys:Exception /> 

+ 4 - 9
samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs

@@ -84,15 +84,10 @@ namespace ControlCatalog.Pages
             }
         }
 
-        public IList<CultureInfo> Cultures { get; } = new List<CultureInfo>()
-        {
-            new CultureInfo("en-US"),
-            new CultureInfo("en-GB"),
-            new CultureInfo("fr-FR"),
-            new CultureInfo("ar-DZ"),
-            new CultureInfo("zh-CN"),
-            new CultureInfo("cs-CZ")
-        };
+        // Trimmed-mode friendly where we might not have cultures
+        public IList<CultureInfo?> Cultures { get; } = CultureInfo.GetCultures(CultureTypes.SpecificCultures)
+            .Where(c => new[] { "en-US", "en-GB", "fr-FR", "ar-DZ", "zh-CH", "cs-CZ" }.Contains(c.Name))
+            .ToArray();
 
         public FormatObject SelectedFormat
         {

+ 5 - 1
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@@ -1,15 +1,17 @@
 using System;
 using Avalonia.Controls;
+using Avalonia.Controls.Platform;
 using Avalonia.Controls.Remote.Server;
 using Avalonia.Input;
 using Avalonia.Platform;
+using Avalonia.Platform.Storage;
 using Avalonia.Remote.Protocol;
 using Avalonia.Remote.Protocol.Viewport;
 using Avalonia.Threading;
 
 namespace Avalonia.DesignerSupport.Remote
 {
-    class PreviewerWindowImpl : RemoteServerTopLevelImpl, IWindowImpl
+    class PreviewerWindowImpl : RemoteServerTopLevelImpl, IWindowImpl, ITopLevelImplWithStorageProvider
     {
         private readonly IAvaloniaRemoteTransportConnection _transport;
 
@@ -90,6 +92,8 @@ namespace Avalonia.DesignerSupport.Remote
 
         public bool NeedsManagedDecorations => false;
 
+        public IStorageProvider StorageProvider => new NoopStorageProvider();
+
         public void Activate()
         {
         }

+ 0 - 1
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@@ -55,7 +55,6 @@ namespace Avalonia.DesignerSupport.Remote
                 .Bind<IPlatformThreadingInterface>().ToConstant(threading)
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
                 .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
-                .Bind<ISystemDialogImpl>().ToSingleton<SystemDialogsStub>()
                 .Bind<IWindowingPlatform>().ToConstant(instance)
                 .Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
                 .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();

+ 23 - 9
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@@ -11,6 +11,8 @@ using Avalonia.Input;
 using Avalonia.Input.Platform;
 using Avalonia.Input.Raw;
 using Avalonia.Platform;
+using Avalonia.Platform.Storage;
+using Avalonia.Platform.Storage.FileIO;
 using Avalonia.Rendering;
 
 namespace Avalonia.DesignerSupport.Remote
@@ -222,15 +224,6 @@ namespace Avalonia.DesignerSupport.Remote
         public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) => new IconStub();
     }
 
-    class SystemDialogsStub : ISystemDialogImpl
-    {
-        public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent) =>
-            Task.FromResult((string[])null);
-
-        public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) =>
-            Task.FromResult((string)null);
-    }
-
     class ScreenStub : IScreenImpl
     {
         public int ScreenCount => 1;
@@ -253,4 +246,25 @@ namespace Avalonia.DesignerSupport.Remote
             return ScreenHelper.ScreenFromWindow(window, AllScreens);
         }
     }
+
+    internal class NoopStorageProvider : BclStorageProvider
+    {
+        public override bool CanOpen => false;
+        public override Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
+        {
+            return Task.FromResult<IReadOnlyList<IStorageFile>>(Array.Empty<IStorageFile>());
+        }
+
+        public override bool CanSave => false;
+        public override Task<IStorageFile> SaveFilePickerAsync(FilePickerSaveOptions options)
+        {
+            return Task.FromResult<IStorageFile>(null);
+        }
+
+        public override bool CanPickFolder => false;
+        public override Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
+        {
+            return Task.FromResult<IReadOnlyList<IStorageFolder>>(Array.Empty<IStorageFolder>());
+        }
+    }
 }

+ 0 - 4
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@@ -14,10 +14,6 @@
     <ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
   </ItemGroup>
 
-  <ItemGroup Label="InternalsVisibleTo">
-    <InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
-  </ItemGroup>
-
   <Import Project="..\..\build\ApiDiff.props" />
   <Import Project="..\..\build\DevAnalyzers.props" />
 </Project>

+ 9 - 26
src/Avalonia.Dialogs/ManagedFileChooserFilterViewModel.cs

@@ -1,48 +1,31 @@
-using System;
-using System.Collections.Generic;
 using System.Linq;
-using Avalonia.Controls;
+using System.Text.RegularExpressions;
+using Avalonia.Platform.Storage;
 
 namespace Avalonia.Dialogs
 {
     internal class ManagedFileChooserFilterViewModel : InternalViewModelBase
     {
-        private readonly string[] _extensions;
+        private readonly Regex[] _patterns;
         public string Name { get; }
 
-        public ManagedFileChooserFilterViewModel(FileDialogFilter filter)
+        public ManagedFileChooserFilterViewModel(FilePickerFileType filter)
         {
             Name = filter.Name;
 
-            if (filter.Extensions.Contains("*"))
+            if (filter.Patterns?.Contains("*.*") == true)
             {
                 return;
             }
 
-            _extensions = filter.Extensions?.Select(e => "." + e.ToLowerInvariant()).ToArray();
-        }
-
-        public ManagedFileChooserFilterViewModel()
-        {
-            Name = "All files";
+            _patterns = filter.Patterns?
+                .Select(e => new Regex(Regex.Escape(e).Replace(@"\*", ".*").Replace(@"\?", "."), RegexOptions.Singleline | RegexOptions.IgnoreCase))
+                .ToArray();
         }
 
         public bool Match(string filename)
         {
-            if (_extensions == null)
-            {
-                return true;
-            }
-
-            foreach (var ext in _extensions)
-            {
-                if (filename.EndsWith(ext, StringComparison.InvariantCultureIgnoreCase))
-                {
-                    return true;
-                }
-            }
-
-            return false;
+            return _patterns == null || _patterns.Any(ext => ext.IsMatch(filename));
         }
 
         public override string ToString() => Name;

+ 64 - 33
src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs

@@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
 using Avalonia.Collections;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
+using Avalonia.Platform.Storage;
 using Avalonia.Threading;
 
 namespace Avalonia.Dialogs
@@ -106,7 +107,7 @@ namespace Avalonia.Dialogs
             QuickLinks.AddRange(quickSources.GetAllItems().Select(i => new ManagedFileChooserItemViewModel(i)));
         }
 
-        public ManagedFileChooserViewModel(FileSystemDialog dialog, ManagedFileDialogOptions options)
+        public ManagedFileChooserViewModel(ManagedFileDialogOptions options)
         {
             _options = options;
             _disposables = new CompositeDisposable();
@@ -131,50 +132,63 @@ namespace Avalonia.Dialogs
             CancelRequested += delegate { _disposables?.Dispose(); };
 
             RefreshQuickLinks(quickSources);
+            
+            SelectedItems.CollectionChanged += OnSelectionChangedAsync;
+        }
 
-            Title = dialog.Title ?? (
-                        dialog is OpenFileDialog ? "Open file"
-                        : dialog is SaveFileDialog ? "Save file"
-                        : dialog is OpenFolderDialog ? "Select directory"
-                        : throw new ArgumentException(nameof(dialog)));
-
-            var directory = dialog.Directory;
-
-            if (directory == null || !Directory.Exists(directory))
+        public ManagedFileChooserViewModel(FilePickerOpenOptions filePickerOpen,  ManagedFileDialogOptions options)
+            : this(options)
+        {
+            Title = filePickerOpen.Title ?? "Open file";
+            
+            if (filePickerOpen.FileTypeFilter?.Count > 0)
             {
-                directory = Directory.GetCurrentDirectory();
+                Filters.AddRange(filePickerOpen.FileTypeFilter.Select(f => new ManagedFileChooserFilterViewModel(f)));
+                _selectedFilter = Filters[0];
+                ShowFilters = true;
             }
 
-            if (dialog is FileDialog fd)
+            if (filePickerOpen.AllowMultiple)
             {
-                if (fd.Filters?.Count > 0)
-                {
-                    Filters.AddRange(fd.Filters.Select(f => new ManagedFileChooserFilterViewModel(f)));
-                    _selectedFilter = Filters[0];
-                    ShowFilters = true;
-                }
+                SelectionMode = SelectionMode.Multiple;
+            }
 
-                if (dialog is OpenFileDialog ofd)
-                {
-                    if (ofd.AllowMultiple)
-                    {
-                        SelectionMode = SelectionMode.Multiple;
-                    }
-                }
+            Navigate(filePickerOpen.SuggestedStartLocation);
+        }
+        
+        public ManagedFileChooserViewModel(FilePickerSaveOptions filePickerSave,  ManagedFileDialogOptions options)
+            : this(options)
+        {
+            Title = filePickerSave.Title ?? "Save file";
+            
+            if (filePickerSave.FileTypeChoices?.Count > 0)
+            {
+                Filters.AddRange(filePickerSave.FileTypeChoices.Select(f => new ManagedFileChooserFilterViewModel(f)));
+                _selectedFilter = Filters[0];
+                ShowFilters = true;
             }
+            
+            _savingFile = true;
+            _defaultExtension = filePickerSave.DefaultExtension;
+            _overwritePrompt = filePickerSave.ShowOverwritePrompt ?? true;
+            FileName = filePickerSave.SuggestedFileName;
+            
+            Navigate(filePickerSave.SuggestedStartLocation, FileName);
+        }
+        
+        public ManagedFileChooserViewModel(FolderPickerOpenOptions folderPickerOpen,  ManagedFileDialogOptions options)
+            : this(options)
+        {
+            Title = folderPickerOpen.Title ?? "Select directory";
 
-            _selectingDirectory = dialog is OpenFolderDialog;
+            _selectingDirectory = true;
 
-            if (dialog is SaveFileDialog sfd)
+            if (folderPickerOpen.AllowMultiple)
             {
-                _savingFile = true;
-                _defaultExtension = sfd.DefaultExtension;
-                _overwritePrompt = sfd.ShowOverwritePrompt ?? true;
-                FileName = sfd.InitialFileName;
+                SelectionMode = SelectionMode.Multiple;
             }
 
-            Navigate(directory, (dialog as FileDialog)?.InitialFileName);
-            SelectedItems.CollectionChanged += OnSelectionChangedAsync;
+            Navigate(folderPickerOpen.SuggestedStartLocation);
         }
 
         public void EnterPressed()
@@ -247,6 +261,23 @@ namespace Avalonia.Dialogs
 
         public void Refresh() => Navigate(Location);
 
+        public void Navigate(IStorageFolder path, string initialSelectionName = null)
+        {
+            string fullDirectoryPath;
+
+            if (path?.TryGetUri(out var fullDirectoryUri) == true
+                && fullDirectoryUri.IsAbsoluteUri)
+            {
+                fullDirectoryPath = fullDirectoryUri.LocalPath;
+            }
+            else
+            {
+                fullDirectoryPath = Directory.GetCurrentDirectory();
+            }
+            
+            Navigate(fullDirectoryPath, initialSelectionName);
+        }
+        
         public void Navigate(string path, string initialSelectionName = null)
         {
             if (!Directory.Exists(path))

+ 26 - 116
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@@ -1,125 +1,26 @@
-using System.IO;
+#nullable enable
+
+using System;
 using System.Linq;
 using System.Threading.Tasks;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
+using Avalonia.Platform.Storage;
 
 namespace Avalonia.Dialogs
 {
     public static class ManagedFileDialogExtensions
     {
-        internal class ManagedSystemDialogImpl<T> : ISystemDialogImpl where T : Window, new()
+        internal class ManagedStorageProviderFactory<T> : IStorageProviderFactory where T : Window, new()
         {
-            async Task<string[]> Show(SystemDialog d, Window parent, ManagedFileDialogOptions options = null)
+            public IStorageProvider CreateProvider(TopLevel topLevel)
             {
-                var model = new ManagedFileChooserViewModel((FileSystemDialog)d,
-                    options ?? new ManagedFileDialogOptions());
-
-                var dialog = new T
-                {
-                    Content = new ManagedFileChooser(),
-                    Title = d.Title,
-                    DataContext = model
-                };
-
-                dialog.Closed += delegate { model.Cancel(); };
-
-                string[] result = null;
-                
-                model.CompleteRequested += items =>
-                {
-                    result = items;
-                    dialog.Close();
-                };
-
-                model.OverwritePrompt += async (filename) =>
+                if (topLevel is Window window)
                 {
-                    Window overwritePromptDialog = new Window()
-                    {
-                        Title = "Confirm Save As",
-                        SizeToContent = SizeToContent.WidthAndHeight,
-                        WindowStartupLocation = WindowStartupLocation.CenterOwner,
-                        Padding = new Thickness(10),
-                        MinWidth = 270
-                    };
-
-                    string name = Path.GetFileName(filename);
-
-                    var panel = new DockPanel()
-                    {
-                        HorizontalAlignment = Layout.HorizontalAlignment.Stretch
-                    };
-
-                    var label = new Label()
-                    {
-                        Content = $"{name} already exists.\nDo you want to replace it?"
-                    };
-
-                    panel.Children.Add(label);
-                    DockPanel.SetDock(label, Dock.Top);
-
-                    var buttonPanel = new StackPanel()
-                    {
-                        HorizontalAlignment = Layout.HorizontalAlignment.Right,
-                        Orientation = Layout.Orientation.Horizontal,
-                        Spacing = 10
-                    };
-
-                    var button = new Button()
-                    {
-                        Content = "Yes",
-                        HorizontalAlignment = Layout.HorizontalAlignment.Right
-                    };
-
-                    button.Click += (sender, args) =>
-                    {
-                        result = new string[1] { filename };
-                        overwritePromptDialog.Close();
-                        dialog.Close();
-                    };
-
-                    buttonPanel.Children.Add(button);
-
-                    button = new Button()
-                    {
-                        Content = "No",
-                        HorizontalAlignment = Layout.HorizontalAlignment.Right
-                    };
-
-                    button.Click += (sender, args) =>
-                    {
-                        overwritePromptDialog.Close();
-                    };
-
-                    buttonPanel.Children.Add(button);
-
-                    panel.Children.Add(buttonPanel);
-                    DockPanel.SetDock(buttonPanel, Dock.Bottom);
-
-                    overwritePromptDialog.Content = panel;
-
-                    await overwritePromptDialog.ShowDialog(dialog);
-                };
-
-                model.CancelRequested += dialog.Close;
-
-                await dialog.ShowDialog<object>(parent);
-                return result;
-            }
-
-            public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
-            {
-                return await Show(dialog, parent);
-            }
-
-            public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
-            {
-                return (await Show(dialog, parent))?.FirstOrDefault();
-            }
-            
-            public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent, ManagedFileDialogOptions options)
-            {
-                return await Show(dialog, parent, options);
+                    var options = AvaloniaLocator.Current.GetService<ManagedFileDialogOptions>();
+                    return new ManagedStorageProvider<T>(window, options);
+                }
+                throw new InvalidOperationException("Current platform doesn't support managed picker dialogs");
             }
         }
 
@@ -127,7 +28,7 @@ namespace Avalonia.Dialogs
             where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
         {
             builder.AfterSetup(_ =>
-                AvaloniaLocator.CurrentMutable.Bind<ISystemDialogImpl>().ToSingleton<ManagedSystemDialogImpl<Window>>());
+                AvaloniaLocator.CurrentMutable.Bind<IStorageProviderFactory>().ToSingleton<ManagedStorageProviderFactory<Window>>());
             return builder;
         }
 
@@ -135,17 +36,26 @@ namespace Avalonia.Dialogs
             where TAppBuilder : AppBuilderBase<TAppBuilder>, new() where TWindow : Window, new()
         {
             builder.AfterSetup(_ =>
-                AvaloniaLocator.CurrentMutable.Bind<ISystemDialogImpl>().ToSingleton<ManagedSystemDialogImpl<TWindow>>());
+                AvaloniaLocator.CurrentMutable.Bind<IStorageProviderFactory>().ToSingleton<ManagedStorageProviderFactory<TWindow>>());
             return builder;
         }
 
+        [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
         public static Task<string[]> ShowManagedAsync(this OpenFileDialog dialog, Window parent,
-            ManagedFileDialogOptions options = null) => ShowManagedAsync<Window>(dialog, parent, options);
+            ManagedFileDialogOptions? options = null) => ShowManagedAsync<Window>(dialog, parent, options);
 
-        public static Task<string[]> ShowManagedAsync<TWindow>(this OpenFileDialog dialog, Window parent,
-            ManagedFileDialogOptions options = null) where TWindow : Window, new()
+        [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
+        public static async Task<string[]> ShowManagedAsync<TWindow>(this OpenFileDialog dialog, Window parent,
+            ManagedFileDialogOptions? options = null) where TWindow : Window, new()
         {
-            return new ManagedSystemDialogImpl<TWindow>().ShowFileDialogAsync(dialog, parent, options);
+            var impl = new ManagedStorageProvider<TWindow>(parent, options);
+
+            var files = await impl.OpenFilePickerAsync(dialog.ToFilePickerOpenOptions());
+            return files
+                .Select(file => file.TryGetUri(out var fullPath)
+                    ? fullPath.LocalPath
+                    : file.Name)
+                .ToArray();
         }
     }
 }

+ 147 - 0
src/Avalonia.Dialogs/ManagedStorageProvider.cs

@@ -0,0 +1,147 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Platform.Storage;
+using Avalonia.Platform.Storage.FileIO;
+
+namespace Avalonia.Dialogs;
+
+public class ManagedStorageProvider<T> : BclStorageProvider where T : Window, new()
+{
+    private readonly Window _parent;
+    private readonly ManagedFileDialogOptions _managedOptions;
+
+    public ManagedStorageProvider(Window parent, ManagedFileDialogOptions? managedOptions)
+    {
+        _parent = parent;
+        _managedOptions = managedOptions ?? new ManagedFileDialogOptions();
+    }
+
+    public override bool CanSave => true;
+    public override bool CanOpen => true;
+    public override bool CanPickFolder => true;
+            
+    public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
+    {
+        var model = new ManagedFileChooserViewModel(options, _managedOptions);
+        var results = await Show(model, _parent);
+
+        return results.Select(f => new BclStorageFile(new FileInfo(f))).ToArray();
+    }
+
+    public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
+    {
+        var model = new ManagedFileChooserViewModel(options, _managedOptions);
+        var results = await Show(model, _parent);
+
+        return results.FirstOrDefault() is { } result
+            ? new BclStorageFile(new FileInfo(result))
+            : null;
+    }
+
+    public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
+    {
+        var model = new ManagedFileChooserViewModel(options, _managedOptions);
+        var results = await Show(model, _parent);
+
+        return results.Select(f => new BclStorageFolder(new DirectoryInfo(f))).ToArray();
+    }
+            
+    private async Task<string[]> Show(ManagedFileChooserViewModel model, Window parent)
+    {
+        var dialog = new T
+        {
+            Content = new ManagedFileChooser(),
+            Title = model.Title,
+            DataContext = model
+        };
+
+        dialog.Closed += delegate { model.Cancel(); };
+
+        string[]? result = null;
+                
+        model.CompleteRequested += items =>
+        {
+            result = items;
+            dialog.Close();
+        };
+
+        model.OverwritePrompt += async (filename) =>
+        {
+            var overwritePromptDialog = new Window()
+            {
+                Title = "Confirm Save As",
+                SizeToContent = SizeToContent.WidthAndHeight,
+                WindowStartupLocation = WindowStartupLocation.CenterOwner,
+                Padding = new Thickness(10),
+                MinWidth = 270
+            };
+
+            string name = Path.GetFileName(filename);
+
+            var panel = new DockPanel()
+            {
+                HorizontalAlignment = Layout.HorizontalAlignment.Stretch
+            };
+
+            var label = new Label()
+            {
+                Content = $"{name} already exists.\nDo you want to replace it?"
+            };
+
+            panel.Children.Add(label);
+            DockPanel.SetDock(label, Dock.Top);
+
+            var buttonPanel = new StackPanel()
+            {
+                HorizontalAlignment = Layout.HorizontalAlignment.Right,
+                Orientation = Layout.Orientation.Horizontal,
+                Spacing = 10
+            };
+
+            var button = new Button()
+            {
+                Content = "Yes",
+                HorizontalAlignment = Layout.HorizontalAlignment.Right
+            };
+
+            button.Click += (sender, args) =>
+            {
+                result = new string[1] { filename };
+                overwritePromptDialog.Close();
+                dialog.Close();
+            };
+
+            buttonPanel.Children.Add(button);
+
+            button = new Button()
+            {
+                Content = "No",
+                HorizontalAlignment = Layout.HorizontalAlignment.Right
+            };
+
+            button.Click += (sender, args) =>
+            {
+                overwritePromptDialog.Close();
+            };
+
+            buttonPanel.Children.Add(button);
+
+            panel.Children.Add(buttonPanel);
+            DockPanel.SetDock(buttonPanel, Dock.Bottom);
+
+            overwritePromptDialog.Content = panel;
+
+            await overwritePromptDialog.ShowDialog(dialog);
+        };
+
+        model.CancelRequested += dialog.Close;
+
+        await dialog.ShowDialog<object>(parent);
+        return result ?? Array.Empty<string>();
+    }
+}

+ 0 - 1
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@@ -62,7 +62,6 @@ namespace Avalonia.Headless
                 .Bind<IClipboard>().ToSingleton<HeadlessClipboardStub>()
                 .Bind<ICursorFactory>().ToSingleton<HeadlessCursorFactoryStub>()
                 .Bind<IPlatformSettings>().ToConstant(new HeadlessPlatformSettingsStub())
-                .Bind<ISystemDialogImpl>().ToSingleton<HeadlessSystemDialogsStub>()
                 .Bind<IPlatformIconLoader>().ToSingleton<HeadlessIconLoaderStub>()
                 .Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())

+ 23 - 13
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@@ -12,6 +12,8 @@ using Avalonia.Media;
 using Avalonia.Media.Fonts;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Platform;
+using Avalonia.Platform.Storage;
+using Avalonia.Platform.Storage.FileIO;
 using Avalonia.Utilities;
 
 namespace Avalonia.Headless
@@ -73,19 +75,6 @@ namespace Avalonia.Headless
         public TimeSpan TouchDoubleClickTime => DoubleClickTime;
     }
 
-    class HeadlessSystemDialogsStub : ISystemDialogImpl
-    {
-        public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
-        {
-            return Task.Run(() => (string[])null);
-        }
-
-        public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
-        {
-            return Task.Run(() => (string)null);
-        }
-    }
-
     class HeadlessGlyphTypefaceImpl : IGlyphTypefaceImpl
     {
         public short DesignEmHeight => 10;
@@ -219,4 +208,25 @@ namespace Avalonia.Headless
             return ScreenHelper.ScreenFromWindow(window, AllScreens);
         }
     }
+
+    internal class NoopStorageProvider : BclStorageProvider
+    {
+        public override bool CanOpen => false;
+        public override Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
+        {
+            return Task.FromResult<IReadOnlyList<IStorageFile>>(Array.Empty<IStorageFile>());
+        }
+
+        public override bool CanSave => false;
+        public override Task<IStorageFile> SaveFilePickerAsync(FilePickerSaveOptions options)
+        {
+            return Task.FromResult<IStorageFile>(null);
+        }
+
+        public override bool CanPickFolder => false;
+        public override Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
+        {
+            return Task.FromResult<IReadOnlyList<IStorageFolder>>(Array.Empty<IStorageFolder>());
+        }
+    }
 }

+ 5 - 1
src/Avalonia.Headless/HeadlessWindowImpl.cs

@@ -3,19 +3,21 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using Avalonia.Automation.Peers;
 using Avalonia.Controls;
+using Avalonia.Controls.Platform;
 using Avalonia.Controls.Platform.Surfaces;
 using Avalonia.Controls.Primitives.PopupPositioning;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.Media.Imaging;
 using Avalonia.Platform;
+using Avalonia.Platform.Storage;
 using Avalonia.Rendering;
 using Avalonia.Threading;
 using Avalonia.Utilities;
 
 namespace Avalonia.Headless
 {
-    class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow
+    class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow, ITopLevelImplWithStorageProvider
     {
         private IKeyboardDevice _keyboard;
         private Stopwatch _st = Stopwatch.StartNew();
@@ -245,6 +247,8 @@ namespace Avalonia.Headless
 
         public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
 
+        public IStorageProvider StorageProvider => new NoopStorageProvider();
+
         void IHeadlessWindow.KeyPress(Key key, RawInputModifiers modifiers)
         {
             Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyDown, key, modifiers));