Browse Source

Merge pull request #10603 from AvaloniaUI/update-storage-provider-one-last-time

Change StorageFolder.GetItemsAsync to return IAsyncEnumerable
Max Katz 2 years ago
parent
commit
123a2ed8fe

+ 2 - 2
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@@ -324,9 +324,9 @@ namespace ControlCatalog.Pages
                         mappedResults.Add("+> " + FullPathOrName(selectedItem));
                         if (selectedItem is IStorageFolder folder)
                         {
-                            foreach (var innerItems in await folder.GetItemsAsync())
+                            await foreach (var innerItem in folder.GetItemsAsync())
                             {
-                                mappedResults.Add("++> " + FullPathOrName(innerItems));
+                                mappedResults.Add("++> " + FullPathOrName(innerItem));
                             }
                         }
                     }

+ 6 - 2
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@@ -104,8 +104,12 @@ namespace ControlCatalog.Pages
                         }
                         else if (item is IStorageFolder folder)
                         {
-                            var items = await folder.GetItemsAsync();
-                            contentStr += $"Folder {item.Name}: items {items.Count}{Environment.NewLine}{Environment.NewLine}";
+                            var childrenCount = 0;
+                            await foreach (var _ in folder.GetItemsAsync())
+                            {
+                                childrenCount++;
+                            }
+                            contentStr += $"Folder {item.Name}: items {childrenCount}{Environment.NewLine}{Environment.NewLine}";
                         }
                     }
 

+ 6 - 10
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs

@@ -131,19 +131,17 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
         return Task.FromResult(new StorageItemProperties());
     }
 
-    public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
+    public async IAsyncEnumerable<IStorageItem> GetItemsAsync()
     {
         if (!await EnsureExternalFilesPermission(false))
         {
-            return Array.Empty<IStorageItem>();
+            yield break;
         }
-
-        List<IStorageItem> files = new List<IStorageItem>();
-
+        
         var contentResolver = Activity.ContentResolver;
         if (contentResolver == null)
         {
-            return files;
+            yield break;
         }
 
         var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(Uri!, DocumentsContract.GetTreeDocumentId(Uri));
@@ -168,12 +166,10 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
                         continue;
                     }
 
-                    files.Add(mime == DocumentsContract.Document.MimeTypeDir ? new AndroidStorageFolder(Activity, uri, false) :
-                        new AndroidStorageFile(Activity, uri));
+                    yield return mime == DocumentsContract.Document.MimeTypeDir ? new AndroidStorageFolder(Activity, uri, false) :
+                        new AndroidStorageFile(Activity, uri);
                 }
         }
-
-        return files;
     }       
 }
 

+ 7 - 5
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs

@@ -57,14 +57,16 @@ internal class BclStorageFolder : IStorageBookmarkFolder
         return Task.FromResult<IStorageFolder?>(null);
     }
 
-    public Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
+    public async IAsyncEnumerable<IStorageItem> GetItemsAsync()
     {
-         var items = DirectoryInfo.GetDirectories()
+        var items = DirectoryInfo.EnumerateDirectories()
             .Select(d => (IStorageItem)new BclStorageFolder(d))
-            .Concat(DirectoryInfo.GetFiles().Select(f => new BclStorageFile(f)))
-            .ToArray();
+            .Concat(DirectoryInfo.EnumerateFiles().Select(f => new BclStorageFile(f)));
 
-         return Task.FromResult<IReadOnlyList<IStorageItem>>(items);
+        foreach (var item in items)
+        {
+            yield return item;
+        }
     }
 
     public virtual Task<string?> SaveBookmarkAsync()

+ 1 - 1
src/Avalonia.Base/Platform/Storage/IStorageFolder.cs

@@ -16,5 +16,5 @@ public interface IStorageFolder : IStorageItem
     /// <returns>
     /// When this method completes successfully, it returns a list of the files and folders in the current folder. Each item in the list is represented by an <see cref="IStorageItem"/> implementation object.
     /// </returns>
-    Task<IReadOnlyList<IStorageItem>> GetItemsAsync();
+    IAsyncEnumerable<IStorageItem> GetItemsAsync();
 }

+ 12 - 4
src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs

@@ -1,4 +1,5 @@
 using System.Runtime.InteropServices.JavaScript;
+using System.Threading.Tasks;
 
 namespace Avalonia.Browser.Interop;
 
@@ -8,15 +9,22 @@ internal static partial class GeneralHelpers
     public static partial JSObject[] ItemsArrayAt(JSObject jsObject, string key);
     public static JSObject[] GetPropertyAsJSObjectArray(this JSObject jsObject, string key) => ItemsArrayAt(jsObject, key);
     
+    [JSImport("GeneralHelpers.itemAt", AvaloniaModule.MainModuleName)]
+    public static partial JSObject ItemAtInt(JSObject jsObject, int key);
+    public static JSObject GetArrayItem(this JSObject jsObject, int key) => ItemAtInt(jsObject, key);
+    
     [JSImport("GeneralHelpers.itemsArrayAt", AvaloniaModule.MainModuleName)]
     public static partial string[] ItemsArrayAtAsStrings(JSObject jsObject, string key);
     public static string[] GetPropertyAsStringArray(this JSObject jsObject, string key) => ItemsArrayAtAsStrings(jsObject, key);
     
     [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)]
-    public static partial string IntCallMethodString(JSObject jsObject, string name);
+    public static partial string IntCallMethodStr(JSObject jsObject, string name);
+    [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)]
+    public static partial string IntCallMethodStrStr(JSObject jsObject, string name, string arg1);
     [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)]
-    public static partial string IntCallMethodStringString(JSObject jsObject, string name, string arg1);
+    public static partial Task<JSObject?> IntCallMethodPromiseObj(JSObject jsObject, string name);
 
-    public static string CallMethodString(this JSObject jsObject, string name) => IntCallMethodString(jsObject, name);
-    public static string CallMethodString(this JSObject jsObject, string name, string arg1) => IntCallMethodStringString(jsObject, name, arg1);
+    public static string CallMethodString(this JSObject jsObject, string name) => IntCallMethodStr(jsObject, name);
+    public static string CallMethodString(this JSObject jsObject, string name, string arg1) => IntCallMethodStrStr(jsObject, name, arg1);
+    public static Task<JSObject?> CallMethodObjectAsync(this JSObject jsObject, string name) => IntCallMethodPromiseObj(jsObject, name);
 }

+ 3 - 3
src/Browser/Avalonia.Browser/Interop/StorageHelper.cs

@@ -40,9 +40,9 @@ internal static partial class StorageHelper
     [JSImport("StorageItem.openRead", AvaloniaModule.StorageModuleName)]
     public static partial Task<JSObject> OpenRead(JSObject item);
 
-    [JSImport("StorageItem.getItems", AvaloniaModule.StorageModuleName)]
-    [return: JSMarshalAs<JSType.Promise<JSType.Object>>]
-    public static partial Task<JSObject?> GetItems(JSObject item);
+    [JSImport("StorageItem.getItemsIterator", AvaloniaModule.StorageModuleName)]
+    [return: JSMarshalAs<JSType.Object>]
+    public static partial JSObject? GetItemsIterator(JSObject item);
 
     [JSImport("StorageItems.itemsArray", AvaloniaModule.StorageModuleName)]
     public static partial JSObject[] ItemsArray(JSObject item);

+ 34 - 13
src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs

@@ -258,24 +258,45 @@ internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder
     {
     }
 
-    public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
+    public async IAsyncEnumerable<IStorageItem> GetItemsAsync()
     {
-        using var items = await StorageHelper.GetItems(FileHandle);
-        if (items is null)
+        using var itemsIterator = StorageHelper.GetItemsIterator(FileHandle);
+        if (itemsIterator is null)
         {
-            return Array.Empty<IStorageItem>();
+            yield break;
         }
 
-        var itemsArray = StorageHelper.ItemsArray(items);
+        while (true)
+        {
+            var nextResult = await itemsIterator.CallMethodObjectAsync("next");
+            if (nextResult is null)
+            {
+                yield break;
+            }
+
+            var isDone = nextResult.GetPropertyAsBoolean("done");
+            if (isDone)
+            {
+                yield break;
+            }
 
-        return itemsArray
-            .Select(reference => reference.GetPropertyAsString("kind") switch
+            var valArray = nextResult.GetPropertyAsJSObject("value");
+            var storageItem = valArray?.GetArrayItem(1); // 0 - item name, 1 - item instance
+            if (storageItem is null)
             {
-                "directory" => (IStorageItem)new JSStorageFolder(reference),
-                "file" => new JSStorageFile(reference),
-                _ => null
-            })
-            .Where(i => i is not null)
-            .ToArray()!;
+                yield break;
+            }
+
+            var kind = storageItem.GetPropertyAsString("kind");
+            switch (kind)
+            {
+                case "directory":
+                    yield return new JSStorageFolder(storageItem);
+                    break;
+                case "file":
+                    yield return new JSStorageFile(storageItem);
+                    break;
+            }
+        }
     }
 }

+ 6 - 1
src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts

@@ -1,5 +1,5 @@
 export class GeneralHelpers {
-    public static itemsArrayAt(instance: any, key: string): any[] {
+    public static itemsArrayAt(instance: any, key: any): any[] {
         const items = instance[key];
         if (!items) {
             return [];
@@ -12,6 +12,11 @@ export class GeneralHelpers {
         return retItems;
     }
 
+    public static itemAt(instance: any, key: any): any {
+        const item = instance[key];
+        return item;
+    }
+
     public static callMethod(instance: any, name: string /*, args */): any {
         const args = Array.prototype.slice.call(arguments, 2);
         return instance[name].apply(instance, args);

+ 3 - 7
src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts

@@ -89,16 +89,12 @@ export class StorageItem {
         }
     }
 
-    public static async getItems(item: StorageItem): Promise<StorageItems> {
+    public static getItemsIterator(item: StorageItem): any | null {
         if (item.kind !== "directory" || !item.handle) {
-            return new StorageItems([]);
+            return null;
         }
 
-        const items: StorageItem[] = [];
-        for await (const [, value] of (item.handle as any).entries()) {
-            items.push(new StorageItem(value));
-        }
-        return new StorageItems(items);
+        return (item.handle as any).entries();
     }
 
     private async verityPermissions(mode: "read" | "readwrite"): Promise<void | never> {

+ 7 - 2
src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs

@@ -114,8 +114,9 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder
     {
     }
 
-    public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
+    public async IAsyncEnumerable<IStorageItem> GetItemsAsync()
     {
+        // TODO: find out if it can be lazily enumerated.
         var tcs = new TaskCompletionSource<IReadOnlyList<IStorageItem>>();
 
         new NSFileCoordinator().CoordinateRead(Url,
@@ -142,6 +143,10 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder
             throw new NSErrorException(error);
         }
 
-        return await tcs.Task;
+        var items = await tcs.Task;
+        foreach (var item in items)
+        {
+            yield return item;
+        }
     }
 }