From 623922c5ae438c050102c7d8e37f8c343e97ef6a Mon Sep 17 00:00:00 2001
From: yair100 <39923744+yair100@users.noreply.github.com>
Date: Mon, 5 Oct 2026 10:07:56 -0400
Subject: [PATCH 1/5] Code Quality: Improve search performance (#18992)
---
.../Utils/Storage/Search/FolderSearch.cs | 544 +++++++++---------
src/Files.App/ViewModels/ShellViewModel.cs | 98 +++-
2 files changed, 372 insertions(+), 270 deletions(-)
diff --git a/src/Files.App/Utils/Storage/Search/FolderSearch.cs b/src/Files.App/Utils/Storage/Search/FolderSearch.cs
index b136464d8bdc..d128143c4454 100644
--- a/src/Files.App/Utils/Storage/Search/FolderSearch.cs
+++ b/src/Files.App/Utils/Storage/Search/FolderSearch.cs
@@ -3,7 +3,9 @@
using Files.Shared.Helpers;
using Microsoft.Extensions.Logging;
+using Microsoft.UI.Dispatching;
using System.IO;
+using System.IO.Enumeration;
using System.Text.RegularExpressions;
using Windows.Storage;
using Windows.Storage.FileProperties;
@@ -35,7 +37,18 @@ public sealed class FolderSearch
private uint UsedMaxItemCount => MaxItemCount > 0 ? MaxItemCount : uint.MaxValue;
- public EventHandler? SearchTick;
+ public DispatcherQueue DispatcherQueue { get; set; } = MainWindow.Instance.DispatcherQueue;
+
+ ///
+ /// Raised on a throttle with the results found since the previous tick, on a background thread during Win32 walks.
+ ///
+ public event EventHandler>? SearchTick;
+
+ private readonly IntervalSampler tickSampler = new(500);
+ private readonly HashSet indexedResultPaths = new(StringComparer.OrdinalIgnoreCase);
+ private readonly List shortcutResults = [];
+ private List pendingResults = [];
+ private bool hasRaisedTick;
private bool IsAQSQuery => Query is not null && (Query.StartsWith('$') || Query.Contains(':', StringComparison.Ordinal));
@@ -100,6 +113,21 @@ public async Task SearchAsync(IList results, CancellationToken token
{
App.Logger.LogWarning(e, "Search failure");
}
+
+ try
+ {
+ if (MaxItemCount > 0)
+ await LoadSuggestionIconsAsync(results, token);
+ else
+ await ResolveShortcutTargetsAsync(token);
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (Exception e)
+ {
+ App.Logger.LogWarning(e, "Failed to finalize search results");
+ }
}
private async Task AddItemsForHomeAsync(IList results, CancellationToken token)
@@ -144,11 +172,41 @@ public async Task> SearchAsync()
return results;
}
+ private void AddResult(IList results, ListedItem item, CancellationToken token)
+ {
+ if (token.IsCancellationRequested)
+ return;
+
+ results.Add(item);
+ pendingResults.Add(item);
+ if (item is ShortcutItem shortcutItem)
+ shortcutResults.Add(shortcutItem);
+
+ RaiseSearchTickIfDue(token);
+ }
+
+ private void RaiseSearchTickIfDue(CancellationToken token)
+ {
+ if (pendingResults.Count == 0 || token.IsCancellationRequested || (hasRaisedTick && !tickSampler.CheckNow()))
+ return;
+
+ var batch = pendingResults;
+ pendingResults = [];
+ hasRaisedTick = true;
+
+ SearchTick?.Invoke(this, batch);
+ }
+
+ private uint GetRemainingItemCount(IList results)
+ => results.Count >= UsedMaxItemCount ? 0 : UsedMaxItemCount - (uint)results.Count;
+
private async Task SearchAsync(BaseStorageFolder folder, IList results, CancellationToken token)
{
- //var sampler = new IntervalSampler(500);
uint index = 0;
- var stepSize = Math.Min(defaultStepSize, UsedMaxItemCount);
+ var stepSize = Math.Min(defaultStepSize, GetRemainingItemCount(results));
+ if (stepSize == 0)
+ return;
+
var options = ToQueryOptions();
var queryResult = folder.CreateItemQueryWithOptions(options);
@@ -166,25 +224,79 @@ private async Task SearchAsync(BaseStorageFolder folder, IList resul
try
{
if (!item.Name.StartsWith('.') || UserSettingsService.FoldersSettingsService.ShowDotFiles)
- results.Add(await GetListedItemAsync(item));
+ {
+ var listedItem = await GetListedItemAsync(item);
+ AddResult(results, listedItem, token);
+ if (listedItem.ItemPath is not null)
+ indexedResultPaths.Add(listedItem.ItemPath);
+ }
}
catch (Exception ex)
{
App.Logger.LogWarning(ex, "Error creating ListedItem from StorageItem");
}
-
- if (results.Count == 32 || results.Count % 300 == 0 /*|| sampler.CheckNow()*/)
- {
- SearchTick?.Invoke(this, EventArgs.Empty);
- }
}
index += (uint)items.Count;
- stepSize = Math.Min(defaultStepSize, UsedMaxItemCount - (uint)results.Count);
+ stepSize = Math.Min(defaultStepSize, GetRemainingItemCount(results));
+ if (stepSize == 0)
+ return;
+
items = await queryResult.GetItemsAsync(index, stepSize).AsTask(token);
}
}
+ // Awaited so the final sort treats folder shortcuts as folders
+ private async Task ResolveShortcutTargetsAsync(CancellationToken token)
+ {
+ if (shortcutResults.Count == 0)
+ return;
+
+ var links = new ShellLinkItem?[shortcutResults.Count];
+ await Parallel.ForEachAsync(
+ Enumerable.Range(0, links.Length),
+ new ParallelOptions { CancellationToken = token, MaxDegreeOfParallelism = 4 },
+ async (i, _) => links[i] = await FileOperationsHelpers.ParseLinkAsync(shortcutResults[i].GetRequiredPath(), resolveTarget: false));
+
+ await DispatcherQueue.EnqueueOrInvokeAsync(() =>
+ {
+ for (var i = 0; i < links.Length; i++)
+ {
+ if (links[i] is not { } link)
+ continue;
+
+ var shortcutItem = shortcutResults[i];
+ shortcutItem.TargetPath = link.TargetPath;
+ shortcutItem.Arguments = link.Arguments;
+ shortcutItem.WorkingDirectory = link.WorkingDirectory;
+ shortcutItem.RunAsAdmin = link.RunAsAdmin;
+ shortcutItem.ShowWindowCommand = link.ShowWindowCommand;
+ shortcutItem.PrimaryItemAttribute = link.IsFolder ? StorageItemTypes.Folder : StorageItemTypes.File;
+ }
+ });
+ }
+
+ private Task LoadSuggestionIconsAsync(IList results, CancellationToken token)
+ {
+ return Task.WhenAll(results.Where(x => x.FileImage is null).Select(async item =>
+ {
+ var iconResult = await FileThumbnailHelper.GetIconAsync(
+ item.GetRequiredPath(),
+ Constants.ShellIconSizes.Small,
+ item.PrimaryItemAttribute == StorageItemTypes.Folder,
+ IconOptions.ReturnIconOnly);
+
+ if (iconResult is null || token.IsCancellationRequested)
+ return;
+
+ await DispatcherQueue.EnqueueOrInvokeAsync(async () =>
+ {
+ if (await iconResult.ToBitmapAsync() is { } bitmapImage)
+ item.FileImage = bitmapImage;
+ });
+ }));
+ }
+
private async Task AddItemsForLibraryAsync(LibraryLocationItem library, IList results, CancellationToken token)
{
foreach (var folder in library.Folders)
@@ -297,7 +409,6 @@ private bool MatchesTagExpression(IEnumerable? fileTags, TagQueryExpress
private async Task SearchTagsAsync(string folder, IList results, CancellationToken token)
{
- //var sampler = new IntervalSampler(500);
var expression = ParseTagQuery(AQSQuery);
if (expression.OrGroups.Count == 0)
@@ -350,8 +461,8 @@ private async Task SearchTagsAsync(string folder, IList results, Can
if (shouldBeListed)
{
var item = GetListedItemAsync(match.FilePath, findData);
- if (item is not null && !token.IsCancellationRequested)
- results.Add(item);
+ if (item is not null)
+ AddResult(results, item, token);
}
}
}
@@ -366,9 +477,7 @@ private async Task SearchTagsAsync(string folder, IList results, Can
?? throw new InvalidOperationException($"The search item '{match.FilePath}' could not be opened.");
if (!item.Name.StartsWith('.') || UserSettingsService.FoldersSettingsService.ShowDotFiles)
{
- var listedItem = await GetListedItemAsync(item);
- if (!token.IsCancellationRequested)
- results.Add(listedItem);
+ AddResult(results, await GetListedItemAsync(item), token);
}
}
catch (Exception ex)
@@ -376,14 +485,6 @@ private async Task SearchTagsAsync(string folder, IList results, Can
App.Logger.LogWarning(ex, "Error creating ListedItem from StorageItem");
}
}
-
- if (token.IsCancellationRequested)
- return;
-
- if (results.Count == 32 || results.Count % 300 == 0 /*|| sampler.CheckNow()*/)
- {
- SearchTick?.Invoke(this, EventArgs.Empty);
- }
}
}
@@ -396,235 +497,211 @@ private async Task AddItemsAsync(string folder, IList results, Cance
else
{
var workingFolder = await GetStorageFolderAsync(folder);
+ var storageFolder = workingFolder ? workingFolder.Result : null;
- var hiddenOnlyFromWin32 = false;
- if (workingFolder)
+ if (IsAQSQuery)
{
- var storageFolder = workingFolder.Result
- ?? throw new InvalidOperationException($"The search folder '{folder}' could not be opened.");
- await SearchAsync(storageFolder, results, token);
- hiddenOnlyFromWin32 = (results.Count != 0);
+ if (storageFolder is not null)
+ await SearchAsync(storageFolder, results, token);
+
+ return;
}
- if (!IsAQSQuery)
+ if (storageFolder is SystemStorageFolder && !DriveHelpers.IsMtpPath(folder) && !await IsFullyIndexedAsync(storageFolder))
{
- await SearchWithWin32Async(folder, hiddenOnlyFromWin32, UsedMaxItemCount - (uint)results.Count, results, token);
+ await SearchWithWin32Async(folder, false, results, token);
+ return;
}
+
+ var hiddenOnlyFromWin32 = false;
+ if (storageFolder is not null)
+ {
+ var countBefore = results.Count;
+ await SearchAsync(storageFolder, results, token);
+ hiddenOnlyFromWin32 = results.Count != countBefore;
+ }
+
+ await SearchWithWin32Async(folder, hiddenOnlyFromWin32, results, token);
}
}
- private async Task SearchWithWin32Async(string folder, bool hiddenOnly, uint maxItemCount, IList results, CancellationToken token)
+ private static async Task IsFullyIndexedAsync(BaseStorageFolder folder)
{
- //var sampler = new IntervalSampler(500);
- if (token.IsCancellationRequested)
- return;
-
- (FindCloseSafeHandle? hFile, WIN32_FIND_DATAW findData) = await Task.Run(() =>
+ try
{
- WIN32_FIND_DATAW findDataTsk = default;
- FindCloseSafeHandle hFileTsk;
- unsafe
- {
- hFileTsk = PInvoke.FindFirstFileEx($"{folder}\\*{QueryWithWildcard}", FINDEX_INFO_LEVELS.FindExInfoBasic,
- &findDataTsk, FINDEX_SEARCH_OPS.FindExSearchNameMatch, FIND_FIRST_EX_FLAGS.FIND_FIRST_EX_LARGE_FETCH);
- }
- return (hFileTsk, findDataTsk);
- }).WithTimeoutAsync(TimeSpan.FromSeconds(5));
- if (token.IsCancellationRequested)
+ return await folder.GetIndexedStateAsync() == IndexedState.FullyIndexed;
+ }
+ catch (Exception)
{
- hFile?.Dispose();
- return;
+ return false;
}
+ }
- var pendingShortcuts = new List<(string Path, WIN32_FIND_DATAW FindData)>();
+ private Task SearchWithWin32Async(string folder, bool hiddenOnly, IList results, CancellationToken token)
+ {
+ return Task.Factory.StartNew(
+ () => SearchWithWin32(folder, hiddenOnly, results, token),
+ token,
+ TaskCreationOptions.LongRunning,
+ TaskScheduler.Default)
+ .WaitAsync(token);
+ }
- if (hFile is { IsInvalid: false } findHandle)
- {
- // Always enter the delegate so the find handle is disposed; cancellation is checked before mutations.
- await Task.Run(() =>
- {
- using (findHandle)
- {
- var hasNextFile = false;
- do
- {
- if (token.IsCancellationRequested)
- break;
+ private void SearchWithWin32(string root, bool hiddenOnly, IList results, CancellationToken token)
+ {
+ var expression = FileSystemName.TranslateWin32Expression($"*{QueryWithWildcard}");
+ var showHiddenItems = UserSettingsService.FoldersSettingsService.ShowHiddenItems;
+ var showProtectedSystemFiles = UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles;
+ var showDotFiles = UserSettingsService.FoldersSettingsService.ShowDotFiles;
+
+ var pendingFolders = new Queue<(string Path, bool UseFindTimeout)>();
+ pendingFolders.Enqueue((root, RequiresFindTimeout(root)));
- if (results.Count >= maxItemCount)
- break;
+ while (pendingFolders.TryDequeue(out var pendingFolder))
+ {
+ var (folder, useFindTimeout) = pendingFolder;
+ if (token.IsCancellationRequested || results.Count >= UsedMaxItemCount)
+ return;
- string fileName = findData.cFileName.ToString();
- var itemPath = Path.Combine(folder, fileName);
- var isSystem = ((FileAttributes)findData.dwFileAttributes & FileAttributes.System) == FileAttributes.System;
- var isHidden = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden;
- var startWithDot = fileName.StartsWith('.');
- var isShortcut = FileExtensionHelpers.IsShortcutOrUrlFile(fileName);
+ using (var findHandle = FindFirstFile(folder, useFindTimeout, out var findData, token))
+ {
+ if (findHandle is null || findHandle.IsInvalid)
+ continue;
- bool shouldBeListed = (hiddenOnly ?
- (!isHidden && isShortcut) || (isHidden && UserSettingsService.FoldersSettingsService.ShowHiddenItems && (!isSystem || UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles)) :
- !isHidden || (UserSettingsService.FoldersSettingsService.ShowHiddenItems && (!isSystem || UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles))) &&
- (!startWithDot || UserSettingsService.FoldersSettingsService.ShowDotFiles);
+ do
+ {
+ var fileName = findData.cFileName.ToString();
+ if (fileName is "." or "..")
+ continue;
- if (shouldBeListed)
- {
- if (isShortcut)
- {
- pendingShortcuts.Add((itemPath, findData));
- }
- else
- {
- var item = GetListedItemAsync(itemPath, findData);
- if (item is not null && !token.IsCancellationRequested)
- results.Add(item);
- }
- }
+ var attributes = (FileAttributes)findData.dwFileAttributes;
+ var isDirectory = attributes.HasFlag(FileAttributes.Directory);
+ var reparseTag = attributes.HasFlag(FileAttributes.ReparsePoint) ? findData.dwReserved0 : 0;
- if (!token.IsCancellationRequested && (results.Count == 32 || results.Count % 300 == 0 /*|| sampler.CheckNow()*/))
- SearchTick?.Invoke(this, EventArgs.Empty);
- hasNextFile = PInvoke.FindNextFile(findHandle, out findData);
- } while (hasNextFile);
- }
- });
- }
- else
- {
- hFile?.Dispose();
- }
+ if (isDirectory && !IsNameSurrogateReparseTag(reparseTag))
+ pendingFolders.Enqueue((Path.Combine(folder, fileName), useFindTimeout || IsCloudFilesReparseTag(reparseTag)));
- foreach (var (itemPath, itemFindData) in pendingShortcuts)
- {
- if (results.Count >= maxItemCount || token.IsCancellationRequested)
- break;
+ if (!FileSystemName.MatchesWin32Expression(expression, fileName))
+ continue;
- string shortcutFileName = itemFindData.cFileName.ToString();
- var isUrl = FileExtensionHelpers.IsWebLinkFile(shortcutFileName);
- var shortcutFindData = itemFindData;
- var isHidden = ((FileAttributes)shortcutFindData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden;
- PInvoke.FileTimeToSystemTime(shortcutFindData.ftLastWriteTime, out SYSTEMTIME modifiedTime);
- PInvoke.FileTimeToSystemTime(shortcutFindData.ftCreationTime, out SYSTEMTIME createdTime);
- var fileSize = Win32FindDataExtensions.GetSize(shortcutFindData);
- var itemFileExtension = shortcutFileName.Contains('.', StringComparison.Ordinal) ? Path.GetExtension(itemPath)! : string.Empty;
+ var isSystem = attributes.HasFlag(FileAttributes.System);
+ var isHidden = attributes.HasFlag(FileAttributes.Hidden);
+ var startWithDot = fileName.StartsWith('.');
+ var isShortcut = !isDirectory && FileExtensionHelpers.IsShortcutOrUrlFile(fileName);
- var shortcutItem = new ShortcutItem(null)
- {
- PrimaryItemAttribute = StorageItemTypes.File,
- FileExtension = itemFileExtension,
- IsHiddenItem = isHidden,
- Opacity = isHidden ? Constants.UI.DimItemOpacity : 1,
- FileImage = null,
- LoadFileIcon = false,
- ItemNameRaw = shortcutFileName,
- ItemDateModifiedReal = modifiedTime.ToDateTime(),
- ItemDateCreatedReal = createdTime.ToDateTime(),
- ItemType = isUrl ? Strings.ShortcutWebLinkFileType.GetLocalizedResource() : Strings.Shortcut.GetLocalizedResource(),
- ItemPath = itemPath,
- FileSize = fileSize.ToSizeString(),
- FileSizeBytes = fileSize,
- IsUrl = isUrl,
- };
+ bool shouldBeListed = (hiddenOnly ?
+ (!isHidden && isShortcut) || (isHidden && showHiddenItems && (!isSystem || showProtectedSystemFiles)) :
+ !isHidden || (showHiddenItems && (!isSystem || showProtectedSystemFiles))) &&
+ (!startWithDot || showDotFiles);
- if (results.Any(r => string.Equals(r.ItemPath, itemPath, StringComparison.OrdinalIgnoreCase)))
- continue;
+ var itemPath = Path.Combine(folder, fileName);
+ if (!shouldBeListed || (hiddenOnly && indexedResultPaths.Contains(itemPath)))
+ continue;
- if (MaxItemCount == 0)
- {
- _ = FileOperationsHelpers.ParseLinkAsync(itemPath).ContinueWith((t) =>
- {
- if (t.IsCompletedSuccessfully && t.Result is not null)
+ try
{
- _ = FilesystemTasks.Wrap(() => MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() =>
- {
- shortcutItem.TargetPath = t.Result.TargetPath;
- shortcutItem.Arguments = t.Result.Arguments;
- shortcutItem.WorkingDirectory = t.Result.WorkingDirectory;
- shortcutItem.RunAsAdmin = t.Result.RunAsAdmin;
- shortcutItem.ShowWindowCommand = t.Result.ShowWindowCommand;
- shortcutItem.PrimaryItemAttribute = t.Result.IsFolder ? StorageItemTypes.Folder : StorageItemTypes.File;
- }));
+ if ((isShortcut ? GetShortcutItem(itemPath, findData) : GetListedItemAsync(itemPath, findData)) is { } item)
+ AddResult(results, item, token);
}
- });
- }
- else
- {
- var iconResult = await FileThumbnailHelper.GetIconAsync(
- itemPath,
- Constants.ShellIconSizes.Small,
- false,
- IconOptions.ReturnIconOnly);
- if (iconResult is not null)
- shortcutItem.FileImage = await iconResult.ToBitmapAsync();
+ catch (Exception ex)
+ {
+ App.Logger.LogWarning(ex, "Error creating ListedItem from Win32 find data");
+ }
+ }
+ while (!token.IsCancellationRequested && results.Count < UsedMaxItemCount && PInvoke.FindNextFile(findHandle, out findData));
}
- if (token.IsCancellationRequested)
- break;
-
- results.Add(shortcutItem);
-
- if (!token.IsCancellationRequested && (results.Count == 32 || results.Count % 300 == 0))
- {
- SearchTick?.Invoke(this, EventArgs.Empty);
- }
+ RaiseSearchTickIfDue(token);
}
- if (token.IsCancellationRequested)
- return;
+ }
+
+ private static FindCloseSafeHandle? FindFirstFile(string folder, bool useTimeout, out WIN32_FIND_DATAW findData, CancellationToken token)
+ {
+ if (!useTimeout)
+ return FindFirstFile(folder, out findData);
- (FindCloseSafeHandle? hSubDir, WIN32_FIND_DATAW subDirData) = await Task.Run(() =>
+ var findTask = Task.Run(() => (Handle: FindFirstFile(folder, out var data), Data: data));
+ try
{
- WIN32_FIND_DATAW subDirDataTsk = default;
- FindCloseSafeHandle hSubDirTsk;
- unsafe
+ if (findTask.Wait(TimeSpan.FromSeconds(5), token))
{
- hSubDirTsk = PInvoke.FindFirstFileEx($"{folder}\\*", FINDEX_INFO_LEVELS.FindExInfoBasic,
- &subDirDataTsk, FINDEX_SEARCH_OPS.FindExSearchNameMatch, FIND_FIRST_EX_FLAGS.FIND_FIRST_EX_LARGE_FETCH);
+ (var handle, findData) = findTask.Result;
+ return handle;
}
- return (hSubDirTsk, subDirDataTsk);
- }).WithTimeoutAsync(TimeSpan.FromSeconds(5));
- if (token.IsCancellationRequested)
- {
- hSubDir?.Dispose();
- return;
}
-
- if (hSubDir is { IsInvalid: false } subDirectoryHandle)
+ catch (OperationCanceledException)
{
- var subDirectories = new List();
-
- // Always enter the delegate so the find handle is disposed; cancellation is checked before mutations.
- await Task.Run(() =>
- {
- using (subDirectoryHandle)
- {
- var hasNextDir = false;
- do
- {
- if (token.IsCancellationRequested)
- break;
+ }
- string subDirName = subDirData.cFileName.ToString();
- var isDirectory = ((FileAttributes)subDirData.dwFileAttributes & FileAttributes.Directory) == FileAttributes.Directory;
- if (isDirectory && subDirName != "." && subDirName != "..")
- subDirectories.Add(Path.Combine(folder, subDirName));
+ _ = findTask.ContinueWith(t => t.Result.Handle.Dispose(), TaskContinuationOptions.OnlyOnRanToCompletion);
+ findData = default;
+ return null;
+ }
- hasNextDir = PInvoke.FindNextFile(subDirectoryHandle, out subDirData);
- } while (hasNextDir);
- }
- });
+ private static unsafe FindCloseSafeHandle FindFirstFile(string folder, out WIN32_FIND_DATAW findData)
+ {
+ WIN32_FIND_DATAW data = default;
+ var handle = PInvoke.FindFirstFileEx(Path.Join(folder, "*"), FINDEX_INFO_LEVELS.FindExInfoBasic,
+ &data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, FIND_FIRST_EX_FLAGS.FIND_FIRST_EX_LARGE_FETCH);
+ findData = data;
+ return handle;
+ }
- foreach (var subDir in subDirectories)
- {
- if (results.Count >= maxItemCount || token.IsCancellationRequested)
- break;
+ private static bool RequiresFindTimeout(string root)
+ {
+ if (DriveHelpers.IsNetworkPath(root))
+ return true;
- await SearchWithWin32Async(subDir, hiddenOnly, maxItemCount - (uint)results.Count, results, token);
- }
+ try
+ {
+ // Virtual cloud drives such as Google Drive report Fixed but aren't NTFS
+ var drive = new DriveInfo(root);
+ return drive.DriveType is not System.IO.DriveType.Fixed
+ || drive.DriveFormat is not ("NTFS" or "ReFS")
+ || File.GetAttributes(root).HasFlag(FileAttributes.ReparsePoint);
}
- else
+ catch (Exception)
{
- hSubDir?.Dispose();
+ return true;
}
}
+ // Junctions, symlinks and mount points
+ private static bool IsNameSurrogateReparseTag(uint reparseTag)
+ => (reparseTag & 0x20000000) != 0;
+
+ // IO_REPARSE_TAG_CLOUD and IO_REPARSE_TAG_CLOUD_1 to _F
+ private static bool IsCloudFilesReparseTag(uint reparseTag)
+ => (reparseTag & 0xFFFF0FFF) == 0x9000001A;
+
+ private static ShortcutItem GetShortcutItem(string itemPath, WIN32_FIND_DATAW findData)
+ {
+ string fileName = findData.cFileName.ToString();
+ var isUrl = FileExtensionHelpers.IsWebLinkFile(fileName);
+ var isHidden = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden;
+ PInvoke.FileTimeToSystemTime(findData.ftLastWriteTime, out SYSTEMTIME modifiedTime);
+ PInvoke.FileTimeToSystemTime(findData.ftCreationTime, out SYSTEMTIME createdTime);
+ var fileSize = Win32FindDataExtensions.GetSize(findData);
+
+ return new ShortcutItem(null)
+ {
+ PrimaryItemAttribute = StorageItemTypes.File,
+ FileExtension = fileName.Contains('.', StringComparison.Ordinal) ? Path.GetExtension(itemPath)! : string.Empty,
+ IsHiddenItem = isHidden,
+ Opacity = isHidden ? Constants.UI.DimItemOpacity : 1,
+ FileImage = null,
+ LoadFileIcon = false,
+ ItemNameRaw = fileName,
+ ItemDateModifiedReal = modifiedTime.ToDateTime(),
+ ItemDateCreatedReal = createdTime.ToDateTime(),
+ ItemType = isUrl ? Strings.ShortcutWebLinkFileType.GetLocalizedResource() : Strings.Shortcut.GetLocalizedResource(),
+ ItemPath = itemPath,
+ FileSize = fileSize.ToSizeString(),
+ FileSizeBytes = fileSize,
+ IsUrl = isUrl,
+ };
+ }
+
private ListedItem? GetListedItemAsync(string itemPath, WIN32_FIND_DATAW findData)
{
string fileName = findData.cFileName.ToString();
@@ -680,27 +757,6 @@ await Task.Run(() =>
}
}
- if (listedItem is not null && MaxItemCount > 0) // Only load icon for searchbox suggestions
- {
- _ = FileThumbnailHelper.GetIconAsync(
- listedItem.ItemPath,
- Constants.ShellIconSizes.Small,
- isFolder,
- IconOptions.ReturnIconOnly)
- .ContinueWith((t) =>
- {
- if (t.IsCompletedSuccessfully && t.Result is not null)
- {
- _ = FilesystemTasks.Wrap(() => MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () =>
- {
- var bitmapImage = await t.Result.ToBitmapAsync();
- if (bitmapImage is not null)
- listedItem.FileImage = bitmapImage;
- }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low));
- }
- });
- }
-
return listedItem;
}
@@ -799,24 +855,6 @@ private async Task GetListedItemAsync(IStorageItem item)
FileSizeBytes = (long)props.Size,
IsUrl = isUrl,
};
- if (MaxItemCount == 0)
- {
- _ = FileOperationsHelpers.ParseLinkAsync(file.Path).ContinueWith((t) =>
- {
- if (t.IsCompletedSuccessfully && t.Result is not null)
- {
- _ = FilesystemTasks.Wrap(() => MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() =>
- {
- shortcutItem.TargetPath = t.Result.TargetPath;
- shortcutItem.Arguments = t.Result.Arguments;
- shortcutItem.WorkingDirectory = t.Result.WorkingDirectory;
- shortcutItem.RunAsAdmin = t.Result.RunAsAdmin;
- shortcutItem.ShowWindowCommand = t.Result.ShowWindowCommand;
- shortcutItem.PrimaryItemAttribute = t.Result.IsFolder ? StorageItemTypes.Folder : StorageItemTypes.File;
- }));
- }
- });
- }
listedItem = shortcutItem;
}
else
@@ -837,17 +875,6 @@ private async Task GetListedItemAsync(IStorageItem item)
};
}
}
- if (listedItem is not null && MaxItemCount > 0) // Only load icon for searchbox suggestions
- {
- var iconResult = await FileThumbnailHelper.GetIconAsync(
- item.Path,
- Constants.ShellIconSizes.Small,
- item.IsOfType(StorageItemTypes.Folder),
- IconOptions.ReturnIconOnly);
-
- if (iconResult is not null)
- listedItem.FileImage = await iconResult.ToBitmapAsync();
- }
return listedItem
?? throw new InvalidOperationException($"The search result '{item.Path}' is neither a file nor a folder.");
}
@@ -866,7 +893,6 @@ private QueryOptions ToQueryOptions()
query.SortOrder.Add(new SortEntry { PropertyName = "System.Search.Rank", AscendingOrder = false });
query.SetPropertyPrefetch(PropertyPrefetchOptions.BasicProperties, null);
- query.SetThumbnailPrefetch(ThumbnailMode.ListView, 24, ThumbnailOptions.UseCurrentScale);
return query;
}
diff --git a/src/Files.App/ViewModels/ShellViewModel.cs b/src/Files.App/ViewModels/ShellViewModel.cs
index aa6b8edfd695..d3a44093007f 100644
--- a/src/Files.App/ViewModels/ShellViewModel.cs
+++ b/src/Files.App/ViewModels/ShellViewModel.cs
@@ -32,6 +32,8 @@ namespace Files.App.ViewModels
///
public sealed partial class ShellViewModel : ObservableObject, IDisposable
{
+ private const int MaxSearchResultsAppendedPerTick = 500;
+
private readonly SemaphoreSlim enumFolderSemaphore;
private readonly SemaphoreSlim getFileOrFolderSemaphore;
private readonly SemaphoreSlim bulkOperationSemaphore;
@@ -52,6 +54,10 @@ public sealed partial class ShellViewModel : ObservableObject, IDisposable
// Files and folders list for manipulating
private ConcurrentCollection filesAndFolders;
+
+ // Search results waiting to be displayed, only accessed on the UI thread
+ private readonly Queue pendingSearchResults = new();
+
private readonly IWindowsIniService WindowsIniService = Ioc.Default.GetRequiredService();
private readonly IWindowsJumpListService jumpListService = Ioc.Default.GetRequiredService();
private readonly IDialogService dialogService = Ioc.Default.GetRequiredService();
@@ -1102,6 +1108,9 @@ private void FilesAndFolderFilterUpdated()
}
+ private static bool MatchesFilter(ListedItem item, string? filter)
+ => string.IsNullOrEmpty(filter) || item.Name?.Contains(filter, StringComparison.OrdinalIgnoreCase) == true;
+
// Apply changes immediately after manipulating on filesAndFolders completed
public async Task ApplyFilesAndFoldersChangesAsync()
{
@@ -1125,8 +1134,7 @@ public async Task ApplyFilesAndFoldersChangesAsync()
var displayedFilesAndFolders = string.IsNullOrEmpty(filter)
? filesAndFoldersLocal
- : await Task.Run(() => filesAndFoldersLocal.Where(
- x => x.Name?.Contains(filter, StringComparison.OrdinalIgnoreCase) == true).ToList(), addFilesCTS.Token);
+ : await Task.Run(() => filesAndFoldersLocal.Where(x => MatchesFilter(x, filter)).ToList(), addFilesCTS.Token);
await dispatcherQueue.EnqueueOrInvokeAsync(() =>
{
@@ -1135,6 +1143,8 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() =>
if (addFilesCTS.IsCancellationRequested || FilesAndFoldersFilter != filter)
return;
+ pendingSearchResults.Clear();
+
FilesAndFolders.BeginBulkOperation();
try
{
@@ -3436,7 +3446,8 @@ public async Task SearchAsync(FolderSearch search)
ItemLoadStatusChanged?.Invoke(this, new ItemLoadStatusChangedEventArgs() { Status = ItemLoadStatusChangedEventArgs.ItemLoadStatus.Starting });
CancelSearch();
- searchCTS = new CancellationTokenSource();
+ var currentSearchCTS = searchCTS = new CancellationTokenSource();
+ var token = currentSearchCTS.Token;
filesAndFolders.Clear();
IsLoadingItems = true;
IsSearchResults = true;
@@ -3453,23 +3464,88 @@ public async Task SearchAsync(FolderSearch search)
ItemLoadStatusChanged?.Invoke(this, new ItemLoadStatusChangedEventArgs() { Status = ItemLoadStatusChangedEventArgs.ItemLoadStatus.InProgress });
+ // Stops late ticks from appending after the final apply
+ using var tickCTS = CancellationTokenSource.CreateLinkedTokenSource(token);
+ var tickToken = tickCTS.Token;
+ void Search_SearchTick(object? sender, IReadOnlyList newItems) => _ = AppendSearchResultsAsync(newItems, tickToken);
+
var results = new List();
- search.SearchTick += async (s, e) =>
+ search.DispatcherQueue = dispatcherQueue;
+ search.SearchTick += Search_SearchTick;
+ await search.SearchAsync(results, token);
+ search.SearchTick -= Search_SearchTick;
+ tickCTS.Cancel();
+
+ if (!token.IsCancellationRequested)
{
+ await PreloadIconsAsync(results);
filesAndFolders = new ConcurrentCollection(results);
+
await OrderFilesAndFoldersAsync();
await ApplyFilesAndFoldersChangesAsync();
- };
+ }
- await search.SearchAsync(results, searchCTS.Token);
+ // A newer search owns the loading state
+ if (ReferenceEquals(searchCTS, currentSearchCTS))
+ {
+ ItemLoadStatusChanged?.Invoke(this, new ItemLoadStatusChangedEventArgs() { Status = ItemLoadStatusChangedEventArgs.ItemLoadStatus.Complete });
+ IsLoadingItems = false;
+ }
+ }
- filesAndFolders = new ConcurrentCollection(results);
+ private async Task AppendSearchResultsAsync(IReadOnlyList newItems, CancellationToken token)
+ {
+ try
+ {
+ await bulkOperationSemaphore.WaitAsync(token);
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
- await OrderFilesAndFoldersAsync();
- await ApplyFilesAndFoldersChangesAsync();
+ try
+ {
+ await PreloadIconsAsync(newItems);
+ await dispatcherQueue.EnqueueOrInvokeAsync(() =>
+ {
+ if (token.IsCancellationRequested)
+ return;
- ItemLoadStatusChanged?.Invoke(this, new ItemLoadStatusChangedEventArgs() { Status = ItemLoadStatusChangedEventArgs.ItemLoadStatus.Complete });
- IsLoadingItems = false;
+ filesAndFolders.AddRange(newItems);
+ foreach (var item in newItems)
+ pendingSearchResults.Enqueue(item);
+
+ var filter = FilesAndFoldersFilter;
+ var appendedCount = 0;
+ while (appendedCount < MaxSearchResultsAppendedPerTick && pendingSearchResults.TryDequeue(out var item))
+ {
+ if (!MatchesFilter(item, filter))
+ continue;
+
+ FilesAndFolders.Add(item);
+ appendedCount++;
+ }
+
+ UpdateEmptyTextType();
+ DirectoryInfoUpdated?.Invoke(this, EventArgs.Empty);
+ });
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogWarning(ex, ex.Message);
+ }
+ finally
+ {
+ bulkOperationSemaphore.Release();
+ }
+ }
+
+ private async Task PreloadIconsAsync(IEnumerable items)
+ {
+ var iconSize = GetPreloadIconSize();
+ foreach (var item in items)
+ item.PreloadedIconData ??= await iconCacheService.GetIconAsync(item.ItemPath, item.IsFolder ? null : item.FileExtension, item.IsFolder, iconSize);
}
public void CancelSearch()
From 923879312ba519687850fecfb9ba617047404973 Mon Sep 17 00:00:00 2001
From: Steve
Date: Tue, 6 Oct 2026 00:09:11 +0900
Subject: [PATCH 2/5] Code Quality: Improved startup performance (#19002)
---
src/Files.App/App.xaml.cs | 86 +++++++++----------
.../Data/Items/WidgetFolderCardItem.cs | 30 +++++--
src/Files.App/Extensions/StringExtensions.cs | 40 ++++++---
.../Helpers/Application/AppLanguageHelper.cs | 3 +
.../Helpers/Application/AppLifecycleHelper.cs | 8 +-
src/Files.App/Helpers/UI/AppSystemBackdrop.cs | 6 ++
src/Files.App/Helpers/UI/UIHelpers.cs | 32 +++++++
src/Files.App/MainWindow.xaml.cs | 67 +++++++++++++--
.../Services/App/AppThemeModeService.cs | 2 +-
.../Settings/GeneralSettingsService.cs | 12 ++-
.../UserControls/NavigationToolbar.xaml | 7 +-
.../UserControls/NavigationToolbar.xaml.cs | 3 +
.../UserControls/TabBar/TabBar.xaml.cs | 8 +-
.../CachingJsonSettingsDatabase.cs | 4 +-
.../Widgets/QuickAccessWidgetViewModel.cs | 49 ++++++++---
src/Files.App/Views/MainPage.xaml.cs | 13 ++-
src/Files.App/Views/ShellPanesPage.xaml.cs | 1 +
17 files changed, 275 insertions(+), 96 deletions(-)
diff --git a/src/Files.App/App.xaml.cs b/src/Files.App/App.xaml.cs
index 0e4c9657dd82..a9113d1741e2 100644
--- a/src/Files.App/App.xaml.cs
+++ b/src/Files.App/App.xaml.cs
@@ -83,6 +83,7 @@ async Task ActivateAsync()
{
// Build the DI container off-thread while the window initializes
var appModel = AppModel;
+ Task commandsWarmupTask = Task.CompletedTask;
var servicesTask = Task.Run(() =>
{
try
@@ -92,31 +93,36 @@ async Task ActivateAsync()
// Configure Ioc here so Ioc.Default-dependent constructions warm off-thread too
Ioc.Default.ConfigureServices(provider);
- // Warm the settings file reads off the UI thread
+ // Load the appearance settings needed for the first frame off the UI thread.
_ = provider.GetRequiredService().LeaveAppRunning;
- _ = provider.GetRequiredService().AppThemeBackdropMaterial;
-
- // Read through these statics by the action/context ctors warmed below
- QuickAccessManager = provider.GetRequiredService();
- HistoryWrapper = provider.GetRequiredService();
- FileTagsManager = provider.GetRequiredService();
- LibraryManager = provider.GetRequiredService();
-
- // Warm every command and hotkey off-thread, below normal so window creation wins the cores
- var previousPriority = Thread.CurrentThread.Priority;
- Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;
- try
- {
- _ = provider.GetRequiredService();
- }
- catch (Exception)
- {
- // A command ctor that needs the UI thread aborts the warm-up; it runs on first use instead
- }
- finally
+ var appearanceSettings = provider.GetRequiredService();
+ _ = appearanceSettings.AppThemeMode;
+ _ = appearanceSettings.AppThemeBackdropMaterial;
+
+ // Commands are needed for the main page, not for the initial window and backdrop.
+ commandsWarmupTask = Task.Run(() =>
{
- Thread.CurrentThread.Priority = previousPriority;
- }
+ var previousPriority = Thread.CurrentThread.Priority;
+ Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;
+ try
+ {
+ // Action and context constructors access these statics.
+ QuickAccessManager = provider.GetRequiredService();
+ HistoryWrapper = provider.GetRequiredService();
+ FileTagsManager = provider.GetRequiredService();
+ LibraryManager = provider.GetRequiredService();
+
+ _ = provider.GetRequiredService();
+ }
+ catch (Exception)
+ {
+ // UI-thread-only constructors run on first use instead.
+ }
+ finally
+ {
+ Thread.CurrentThread.Priority = previousPriority;
+ }
+ });
return provider;
}
@@ -136,17 +142,8 @@ async Task ActivateAsync()
if (!isStartupTask)
{
- // Initialize and activate MainWindow
- MainWindow.Instance.Activate();
-
- if (showSplashScreen)
- {
- // Wait for the Window to initialize
- await Task.Delay(10);
-
- SplashScreenLoadingTCS = new TaskCompletionSource();
- MainWindow.Instance.ShowSplashScreen();
- }
+ // Construct the window while appearance settings are loaded.
+ _ = MainWindow.Instance;
}
// Configure the DI (dependency injection) container
@@ -157,28 +154,27 @@ async Task ActivateAsync()
Ioc.Default.ConfigureServices(serviceProvider);
}
- // Configure Sentry
- if (AppLifecycleHelper.AppEnvironment is not AppEnvironment.Dev)
- AppLifecycleHelper.ConfigureSentry();
-
var userSettingsService = Ioc.Default.GetRequiredService();
var isLeaveAppRunning = userSettingsService.GeneralSettingsService.LeaveAppRunning;
- if (isStartupTask && !isLeaveAppRunning)
+ if (!isStartupTask || !isLeaveAppRunning)
{
- // Initialize and activate MainWindow
- MainWindow.Instance.Activate();
-
if (showSplashScreen)
{
- // Wait for the Window to initialize
- await Task.Delay(10);
-
SplashScreenLoadingTCS = new TaskCompletionSource();
MainWindow.Instance.ShowSplashScreen();
}
+
+ if (!await MainWindow.Instance.ActivateWithBackdropAsync())
+ return;
}
+ await commandsWarmupTask;
+
+ // Configure Sentry
+ if (AppLifecycleHelper.AppEnvironment is not AppEnvironment.Dev)
+ AppLifecycleHelper.ConfigureSentry();
+
// TODO: Replace with DI
QuickAccessManager = Ioc.Default.GetRequiredService();
HistoryWrapper = Ioc.Default.GetRequiredService();
diff --git a/src/Files.App/Data/Items/WidgetFolderCardItem.cs b/src/Files.App/Data/Items/WidgetFolderCardItem.cs
index c3ea40506d40..3b56f4eae2ce 100644
--- a/src/Files.App/Data/Items/WidgetFolderCardItem.cs
+++ b/src/Files.App/Data/Items/WidgetFolderCardItem.cs
@@ -3,6 +3,8 @@
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Win32;
+using Windows.Win32.System.Com;
+using Windows.Win32.System.WinRT;
using Windows.Win32.UI.Shell;
namespace Files.App.Data.Items
@@ -23,16 +25,17 @@ public sealed partial class WidgetFolderCardItem : WidgetCardItem, IWidgetCardIt
private BitmapImage? _Thumbnail;
public BitmapImage? Thumbnail { get => _Thumbnail; set => SetProperty(ref _Thumbnail, value); }
+ private bool _isDisposed;
// Constructor
- public WidgetFolderCardItem(IWindowsStorable item, string text, bool isPinned, string tooltip)
+ public WidgetFolderCardItem(IWindowsStorable item, string text, string path, bool isPinned, string tooltip)
{
AutomationProperties = text;
Item = item;
Text = text;
IsPinned = isPinned;
- Path = item.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING);
+ Path = path;
Tooltip = tooltip;
}
@@ -40,21 +43,36 @@ public WidgetFolderCardItem(IWindowsStorable item, string text, bool isPinned, s
public async Task LoadCardThumbnailAsync()
{
- if (string.IsNullOrEmpty(Path))
+ if (_isDisposed || string.IsNullOrEmpty(Path))
return;
var thumbnailSize = (int)(Constants.ShellIconSizes.Large * App.AppModel.AppWindowDPI);
// Ensure thumbnail size is at least 1 to prevent layout errors
thumbnailSize = Math.Max(1, thumbnailSize);
- Item.TryGetThumbnail(thumbnailSize, SIIGBF.SIIGBF_ICONONLY, out var rawThumbnailData);
- if (rawThumbnailData is null)
+ var hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, typeof(IShellItem).GUID, Item.ThisPtr, out IAgileReference shellItemReference);
+ if (hr.ThrowIfFailedOnDebug().Failed)
return;
- Thumbnail = await rawThumbnailData.ToBitmapAsync();
+ var rawThumbnailData = await STATask.RunPooled(() =>
+ {
+ if (shellItemReference.Resolve(out IShellItem shellItem).ThrowIfFailedOnDebug().Failed)
+ return null;
+
+ using var folder = new WindowsFolder(shellItem);
+ folder.TryGetThumbnail(thumbnailSize, SIIGBF.SIIGBF_ICONONLY, out var data);
+ return data;
+ }, App.Logger);
+ if (_isDisposed || rawThumbnailData is null)
+ return;
+
+ var thumbnail = await rawThumbnailData.ToBitmapAsync();
+ if (!_isDisposed)
+ Thumbnail = thumbnail;
}
public void Dispose()
{
+ _isDisposed = true;
Item.Dispose();
}
}
diff --git a/src/Files.App/Extensions/StringExtensions.cs b/src/Files.App/Extensions/StringExtensions.cs
index c1409a2446f5..09525c655076 100644
--- a/src/Files.App/Extensions/StringExtensions.cs
+++ b/src/Files.App/Extensions/StringExtensions.cs
@@ -55,26 +55,29 @@ public static string WithEnding(this string str, string ending)
return result;
}
- private static readonly ResourceMap resourcesTree = new ResourceManager().MainResourceMap.TryGetSubtree("Resources");
+ private static readonly ResourceManager resourceManager = new();
+ private static readonly ResourceMap resourcesTree = resourceManager.MainResourceMap.TryGetSubtree("Resources");
+ private static readonly Lock resourceLookupLock = new();
+ private static ResourceContext? resourceContext;
private static readonly ConcurrentDictionary cachedResources = new();
private static readonly Dictionary abbreviations = new()
{
- { ByteSize.KiloByteSymbol, Strings.KiloByteSymbol.GetLocalizedResource() },
- { ByteSize.MegaByteSymbol, Strings.MegaByteSymbol.GetLocalizedResource() },
- { ByteSize.GigaByteSymbol, Strings.GigaByteSymbol.GetLocalizedResource() },
- { ByteSize.TeraByteSymbol, Strings.TeraByteSymbol.GetLocalizedResource() },
- { ByteSize.PetaByteSymbol, Strings.PetaByteSymbol.GetLocalizedResource() },
- { ByteSize.BitSymbol, Strings.ByteSymbol.GetLocalizedResource() },
- { ByteSize.ByteSymbol, Strings.ByteSymbol.GetLocalizedResource() }
+ { ByteSize.KiloByteSymbol, Strings.KiloByteSymbol },
+ { ByteSize.MegaByteSymbol, Strings.MegaByteSymbol },
+ { ByteSize.GigaByteSymbol, Strings.GigaByteSymbol },
+ { ByteSize.TeraByteSymbol, Strings.TeraByteSymbol },
+ { ByteSize.PetaByteSymbol, Strings.PetaByteSymbol },
+ { ByteSize.BitSymbol, Strings.ByteSymbol },
+ { ByteSize.ByteSymbol, Strings.ByteSymbol }
};
public static string ConvertSizeAbbreviation(this string value)
{
foreach (var item in abbreviations)
{
- value = value.Replace(item.Key, item.Value, StringComparison.Ordinal);
+ value = value.Replace(item.Key, item.Value.GetLocalizedResource(), StringComparison.Ordinal);
}
return value;
@@ -102,9 +105,24 @@ public static string GetLocalizedResource(this string resourceKey)
return value;
}
- value = resourcesTree?.TryGetValue(resourceKey)?.ValueAsString;
+ lock (resourceLookupLock)
+ {
+ if (cachedResources.TryGetValue(resourceKey, out value))
+ return value;
+
+ resourceContext ??= resourceManager.CreateResourceContext();
+ value = resourcesTree?.TryGetValue(resourceKey, resourceContext)?.ValueAsString;
+ return cachedResources[resourceKey] = value ?? string.Empty;
+ }
+ }
- return cachedResources[resourceKey] = value ?? string.Empty;
+ internal static void ResetLocalizedResources()
+ {
+ lock (resourceLookupLock)
+ {
+ resourceContext = null;
+ cachedResources.Clear();
+ }
}
}
}
diff --git a/src/Files.App/Helpers/Application/AppLanguageHelper.cs b/src/Files.App/Helpers/Application/AppLanguageHelper.cs
index 673e69673c5d..fa7eb6ec5700 100644
--- a/src/Files.App/Helpers/Application/AppLanguageHelper.cs
+++ b/src/Files.App/Helpers/Application/AppLanguageHelper.cs
@@ -3,6 +3,7 @@
using System.Globalization;
using Windows.Globalization;
+using StringExtensions = Files.App.Extensions.StringExtensions;
namespace Files.App.Helpers
{
@@ -87,6 +88,7 @@ public static bool TryChange(int index)
// Update the primary language override
ApplicationLanguages.PrimaryLanguageOverride = index == 0 ? _defaultCode : PreferredLanguage.Code;
+ StringExtensions.ResetLocalizedResources();
return true;
}
@@ -117,6 +119,7 @@ public static bool TryChange(string code)
// Update the primary language override
ApplicationLanguages.PrimaryLanguageOverride = index == 0 ? _defaultCode : PreferredLanguage.Code;
+ StringExtensions.ResetLocalizedResources();
return true;
}
}
diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs
index c51d27d0267b..630158d15873 100644
--- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs
+++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs
@@ -86,13 +86,15 @@ static AppLifecycleHelper()
///
/// Gets application icon path.
///
- public static string AppIconPath { get; } =
- SystemIO.Path.Combine(Package.Current.InstalledLocation.Path, AppEnvironment switch
+ public static string AppIconPath => _appIconPath.Value;
+
+ private static readonly Lazy _appIconPath = new(() =>
+ SystemIO.Path.Combine(Package.Current.InstalledPath, AppEnvironment switch
{
AppEnvironment.Dev => Constants.AssetPaths.DevLogo,
AppEnvironment.SideloadPreview or AppEnvironment.StorePreview => Constants.AssetPaths.PreviewLogo,
_ => Constants.AssetPaths.StableLogo
- });
+ }));
///
/// Initializes the app components.
diff --git a/src/Files.App/Helpers/UI/AppSystemBackdrop.cs b/src/Files.App/Helpers/UI/AppSystemBackdrop.cs
index 334ce02d2773..033c9c030386 100644
--- a/src/Files.App/Helpers/UI/AppSystemBackdrop.cs
+++ b/src/Files.App/Helpers/UI/AppSystemBackdrop.cs
@@ -16,6 +16,7 @@ internal sealed partial class AppSystemBackdrop : SystemBackdrop
private XamlRoot? root;
private SystemBackdropConfiguration? configuration;
private SystemBackdropTheme? prevTheme = null;
+ private bool? isInputActive;
public AppSystemBackdrop(bool isSecondaryWindow = false)
{
@@ -33,6 +34,8 @@ protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop con
this.target = connectedTarget;
this.root = xamlRoot;
configuration = GetDefaultSystemBackdropConfiguration(connectedTarget, xamlRoot);
+ if (isInputActive is bool active)
+ configuration.IsInputActive = active;
controller = GetSystemBackdropController(userSettingsService.AppearanceSettingsService.AppThemeBackdropMaterial, configuration.Theme);
controller?.SetSystemBackdropConfiguration(configuration);
controller?.AddSystemBackdropTarget(connectedTarget);
@@ -82,6 +85,8 @@ private void OnSettingChanged(object? sender, SettingChangedEventArgs e)
controller?.RemoveAllSystemBackdropTargets();
controller?.Dispose();
configuration = GetDefaultSystemBackdropConfiguration(target, root);
+ if (isInputActive is bool active)
+ configuration.IsInputActive = active;
var newController = GetSystemBackdropController(userSettingsService.AppearanceSettingsService.AppThemeBackdropMaterial, configuration.Theme);
newController?.SetSystemBackdropConfiguration(configuration);
newController?.AddSystemBackdropTarget(target);
@@ -93,6 +98,7 @@ private void OnSettingChanged(object? sender, SettingChangedEventArgs e)
// Driven from the window's Activated event since the default configuration doesn't reliably flip IsInputActive on pointerless activation (e.g. taskbar).
public void SetInputActive(bool isInputActive)
{
+ this.isInputActive = isInputActive;
if (configuration is not null)
configuration.IsInputActive = isInputActive;
}
diff --git a/src/Files.App/Helpers/UI/UIHelpers.cs b/src/Files.App/Helpers/UI/UIHelpers.cs
index 3d470d670e47..1b0a248dabe6 100644
--- a/src/Files.App/Helpers/UI/UIHelpers.cs
+++ b/src/Files.App/Helpers/UI/UIHelpers.cs
@@ -19,6 +19,38 @@ public static class UIHelpers
{
public static event PropertyChangedEventHandler? PropertyChanged;
+ public static void RunAfterNextRender(this FrameworkElement element, Action action)
+ {
+ if (!element.IsLoaded)
+ return;
+
+ var canceled = false;
+ element.Unloaded += OnUnloaded;
+ CompositionTarget.Rendered += OnRendered;
+
+ void OnUnloaded(object sender, RoutedEventArgs e)
+ {
+ canceled = true;
+ element.Unloaded -= OnUnloaded;
+ CompositionTarget.Rendered -= OnRendered;
+ }
+
+ void OnRendered(object? sender, RenderedEventArgs e)
+ {
+ CompositionTarget.Rendered -= OnRendered;
+ // Keep the deferred work outside the rendering callback itself.
+ if (!element.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
+ {
+ element.Unloaded -= OnUnloaded;
+ if (!canceled && element.IsLoaded)
+ action();
+ }))
+ {
+ element.Unloaded -= OnUnloaded;
+ }
+ }
+ }
+
///
/// True if a user-editable text input currently owns keyboard focus within the given XamlRoot.
/// Used to gate code that programmatically reassigns focus on a background-completion
diff --git a/src/Files.App/MainWindow.xaml.cs b/src/Files.App/MainWindow.xaml.cs
index 3402f616f9bc..f369ddbe536c 100644
--- a/src/Files.App/MainWindow.xaml.cs
+++ b/src/Files.App/MainWindow.xaml.cs
@@ -4,7 +4,9 @@
using Microsoft.Extensions.Logging;
using Microsoft.UI;
using Microsoft.UI.Windowing;
+using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using System.IO;
using System.Runtime.InteropServices;
@@ -24,6 +26,7 @@ public sealed partial class MainWindow : WindowEx
private bool CanWindowToFront { get; set; } = true;
private readonly Lock _canWindowToFrontLock = new();
+ private bool _isWindowIconInitialized;
protected override bool PersistPlacement => true;
@@ -37,10 +40,49 @@ public MainWindow() : base(416, 316)
AppWindow.TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
AppWindow.TitleBar.ButtonPressedBackgroundColor = Colors.Transparent;
AppWindow.TitleBar.ButtonHoverBackgroundColor = Colors.Transparent;
+ }
+
+ private void InitializeWindowIcon()
+ {
+ if (_isWindowIconInitialized)
+ return;
+
+ AppWindow.SetIcon(AppLifecycleHelper.AppIconPath);
+ _isWindowIconInitialized = true;
+ }
+
+ internal async Task ActivateWithBackdropAsync()
+ {
+ var rootFrame = EnsureWindowIsInitialized();
+ if (rootFrame is null)
+ return false;
+
+ // Submit the lightweight backdrop before showing the window with its normal animation.
+ await CompositionTarget.GetCompositorForCurrentThread().RequestCommitAsync();
+
+ var firstFrame = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ CompositionTarget.Rendered += OnRendered;
+ Closed += OnClosed;
+ try
+ {
+ Activate();
+ // Let the backdrop paint before main-page construction occupies the UI thread.
+ return await firstFrame.Task;
+ }
+ finally
+ {
+ CompositionTarget.Rendered -= OnRendered;
+ Closed -= OnClosed;
+ }
+
+ void OnRendered(object? sender, RenderedEventArgs args)
+ {
+ if (rootFrame.IsLoaded)
+ firstFrame.TrySetResult(true);
+ }
- // Deferred: reads the .ico from disk
- DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
- AppWindow.SetIcon(AppLifecycleHelper.AppIconPath));
+ void OnClosed(object sender, WindowEventArgs args)
+ => firstFrame.TrySetResult(false);
}
public void ShowSplashScreen()
@@ -58,16 +100,13 @@ public async Task InitializeApplicationAsync(object? activatedEventArgs)
if (rootFrame is null)
return;
- // Reuse the existing backdrop on resume; rebuilding the Mica controller leaves the window on the fallback color for ~1s
- SystemBackdrop ??= new AppSystemBackdrop();
-
switch (activatedEventArgs)
{
case ILaunchActivatedEventArgs launchArgs:
if (launchArgs.Arguments is not null &&
(CommandLineParser.SplitArguments(launchArgs.Arguments, true)[0].EndsWith($"files-dev.exe", StringComparison.OrdinalIgnoreCase)
|| CommandLineParser.SplitArguments(launchArgs.Arguments, true)[0].EndsWith($"files-dev", StringComparison.OrdinalIgnoreCase)
- || CommandLineParser.SplitArguments(launchArgs.Arguments, true)[0].Equals(Path.Join(Package.Current.InstalledLocation.Path, "Files.exe"), StringComparison.OrdinalIgnoreCase)))
+ || CommandLineParser.SplitArguments(launchArgs.Arguments, true)[0].Equals(Path.Join(Package.Current.InstalledPath, "Files.exe"), StringComparison.OrdinalIgnoreCase)))
{
// WINUI3: When launching from commandline the argument is not ICommandLineActivatedEventArgs (#10370)
var ppm = CommandLineParser.ParseUntrustedCommands(launchArgs.Arguments);
@@ -250,6 +289,11 @@ private async Task EnsureContentHasKeyboardFocusAsync()
{
// Create a Frame to act as the navigation context and navigate to the first page
rootFrame = new() { CacheSize = 1 };
+ rootFrame.Loaded += (_, _) =>
+ {
+ if (!_isWindowIconInitialized)
+ rootFrame.RunAfterNextRender(InitializeWindowIcon);
+ };
rootFrame.NavigationFailed += (s, e) =>
{
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
@@ -257,6 +301,15 @@ private async Task EnsureContentHasKeyboardFocusAsync()
// Place the frame in the current Window
Instance.Content = rootFrame;
+ Ioc.Default.GetRequiredService().SetAppThemeMode(this, callThemeModeChangedEvent: false);
+ }
+
+ if (SystemBackdrop is null)
+ {
+ // Initialize the active material directly instead of transitioning from its inactive fallback.
+ var backdrop = new AppSystemBackdrop();
+ backdrop.SetInputActive(true);
+ SystemBackdrop = backdrop;
}
return rootFrame;
diff --git a/src/Files.App/Services/App/AppThemeModeService.cs b/src/Files.App/Services/App/AppThemeModeService.cs
index d79825e47746..dbfa00ba2bd0 100644
--- a/src/Files.App/Services/App/AppThemeModeService.cs
+++ b/src/Files.App/Services/App/AppThemeModeService.cs
@@ -83,7 +83,7 @@ public void SetAppThemeMode(Window? window = null, AppWindowTitleBar? titleBar =
try
{
window ??= MainWindow.Instance;
- titleBar ??= MainWindow.Instance.AppWindow?.TitleBar;
+ titleBar ??= window.AppWindow?.TitleBar;
rootTheme ??= AppThemeMode;
if (window.Content is FrameworkElement rootElement)
diff --git a/src/Files.App/Services/Settings/GeneralSettingsService.cs b/src/Files.App/Services/Settings/GeneralSettingsService.cs
index e754b3c7705e..7801fd3143b8 100644
--- a/src/Files.App/Services/Settings/GeneralSettingsService.cs
+++ b/src/Files.App/Services/Settings/GeneralSettingsService.cs
@@ -401,7 +401,17 @@ public Dictionary? ShowHashesDictionary
public string UserId
{
- get => Get(Guid.NewGuid().ToString());
+ get
+ {
+ var userId = Get(null);
+ if (userId is null)
+ {
+ userId = Guid.NewGuid().ToString();
+ Set(userId);
+ }
+
+ return userId;
+ }
set => Set(value);
}
diff --git a/src/Files.App/UserControls/NavigationToolbar.xaml b/src/Files.App/UserControls/NavigationToolbar.xaml
index c3cab3a58c54..667099c68de3 100644
--- a/src/Files.App/UserControls/NavigationToolbar.xaml
+++ b/src/Files.App/UserControls/NavigationToolbar.xaml
@@ -21,6 +21,7 @@
xmlns:ucs="using:Files.App.UserControls.StatusCenter"
xmlns:vm="using:Files.App.ViewModels.UserControls"
x:Name="NavToolbar"
+ Loaded="NavToolbar_Loaded"
Loading="NavToolbar_Loading"
mc:Ignorable="d">
@@ -73,7 +74,11 @@
-
+
diff --git a/src/Files.App/UserControls/NavigationToolbar.xaml.cs b/src/Files.App/UserControls/NavigationToolbar.xaml.cs
index c47b9484809c..3e3769d5866b 100644
--- a/src/Files.App/UserControls/NavigationToolbar.xaml.cs
+++ b/src/Files.App/UserControls/NavigationToolbar.xaml.cs
@@ -51,6 +51,9 @@ public NavigationToolbar()
// Methods
+ private void NavToolbar_Loaded(object sender, RoutedEventArgs e)
+ => this.RunAfterNextRender(() => FindName(nameof(ListaryIntegration)));
+
private void NavToolbar_Loading(FrameworkElement _, object e)
{
Loading -= NavToolbar_Loading;
diff --git a/src/Files.App/UserControls/TabBar/TabBar.xaml.cs b/src/Files.App/UserControls/TabBar/TabBar.xaml.cs
index 58e6e793ae22..dd67e005e3c8 100644
--- a/src/Files.App/UserControls/TabBar/TabBar.xaml.cs
+++ b/src/Files.App/UserControls/TabBar/TabBar.xaml.cs
@@ -426,11 +426,11 @@ private void TabViewItem_Loaded(object sender, RoutedEventArgs e)
}
}
- private async void DragAreaRectangle_Loaded(object sender, RoutedEventArgs e)
- {
- if (HorizontalTabView.ActualWidth <= 0 && TabBarAddNewTabButton.Width <= 0)
- await Task.Delay(100);
+ private void DragAreaRectangle_Loaded(object sender, RoutedEventArgs e)
+ => this.RunAfterNextRender(UpdateTitleBarInsets);
+ private void UpdateTitleBarInsets()
+ {
var titleBarInset = ((AppLanguageHelper.IsPreferredLanguageRtl
? MainWindow.Instance.AppWindow.TitleBar.LeftInset
: MainWindow.Instance.AppWindow.TitleBar.RightInset) / DragAreaRectangle.XamlRoot.RasterizationScale) + 40;
diff --git a/src/Files.App/Utils/Serialization/Implementation/CachingJsonSettingsDatabase.cs b/src/Files.App/Utils/Serialization/Implementation/CachingJsonSettingsDatabase.cs
index d7df6480c46c..e6f6b268fff7 100644
--- a/src/Files.App/Utils/Serialization/Implementation/CachingJsonSettingsDatabase.cs
+++ b/src/Files.App/Utils/Serialization/Implementation/CachingJsonSettingsDatabase.cs
@@ -28,8 +28,8 @@ public CachingJsonSettingsDatabase(
else
{
var defaultElement = GetElementFromValue(defaultValue);
- if (_settingsCache.TryAdd(key, defaultElement) && !SaveSettings(_settingsCache))
- _settingsCache.TryRemove(key, out _);
+ // Persist cached defaults with the next settings change, not during reads.
+ _settingsCache.TryAdd(key, defaultElement);
return defaultValue;
}
diff --git a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs
index 8f365c3b4938..d0f75b8aa275 100644
--- a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs
+++ b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs
@@ -36,6 +36,9 @@ public sealed partial class QuickAccessWidgetViewModel : BaseWidgetViewModel, IW
// TODO: Replace with IMutableFolder.GetWatcherAsync() once it gets implemented in IWindowsStorable
private readonly SystemIO.FileSystemWatcher? _quickAccessFolderWatcher;
private bool isDisposed;
+ private int _refreshVersion;
+
+ private sealed record FolderSnapshot(IAgileReference ShellItem, string Text, string Path, bool IsPinned, string Tooltip);
// Constructor
@@ -75,23 +78,47 @@ public Task RefreshWidgetAsync()
{
return MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () =>
{
+ if (isDisposed)
+ return;
+
+ var refreshVersion = ++_refreshVersion;
+ var homeFolder = HomePageContext.HomeFolder;
+ var folders = await STATask.RunPooled(() =>
+ {
+ List result = [];
+ // The Shell enumerator and its metadata reads stay on this persistent STA.
+ foreach (IWindowsStorable folder in homeFolder.GetQuickAccessFolderAsync().ToBlockingEnumerable())
+ {
+ using (folder)
+ {
+ folder.GetPropertyValue("System.Home.IsPinned", out var isPinned);
+ folder.TryGetShellTooltip(out var tooltip);
+ var text = folder.GetDisplayName(SIGDN.SIGDN_PARENTRELATIVEFORUI);
+ var path = folder.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING);
+ var hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, typeof(IShellItem).GUID, folder.ThisPtr, out IAgileReference shellItem);
+ if (hr.ThrowIfFailedOnDebug().Failed)
+ continue;
+
+ result.Add(new(shellItem, text, path, isPinned, tooltip ?? string.Empty));
+ }
+ }
+ return result;
+ }, App.Logger);
+
+ if (isDisposed || refreshVersion != _refreshVersion || folders is null)
+ return;
+
foreach (var item in Items)
item.Dispose();
Items.Clear();
- await foreach (IWindowsStorable folder in HomePageContext.HomeFolder.GetQuickAccessFolderAsync(default))
+ foreach (var folder in folders)
{
- folder.GetPropertyValue("System.Home.IsPinned", out var isPinned);
- var tooltip = await STATask.RunPooled(() => folder.TryGetShellTooltip(out var t).Succeeded ? t : null, App.Logger);
-
- Items.Insert(
- Items.Count,
- new WidgetFolderCardItem(
- folder,
- folder.GetDisplayName(SIGDN.SIGDN_PARENTRELATIVEFORUI),
- isPinned,
- tooltip ?? string.Empty));
+ if (folder.ShellItem.Resolve(out IShellItem shellItem).ThrowIfFailedOnDebug().Failed)
+ continue;
+
+ Items.Add(new WidgetFolderCardItem(new WindowsFolder(shellItem), folder.Text, folder.Path, folder.IsPinned, folder.Tooltip));
}
});
}
diff --git a/src/Files.App/Views/MainPage.xaml.cs b/src/Files.App/Views/MainPage.xaml.cs
index c480f651b73f..3577f29d915b 100644
--- a/src/Files.App/Views/MainPage.xaml.cs
+++ b/src/Files.App/Views/MainPage.xaml.cs
@@ -70,6 +70,11 @@ public MainPage()
App.AppModel.PropertyChanged += AppModel_PropertyChanged;
ApplySidebarWidthState();
+
+ // Realize the navigation controls before the first measure pass.
+ FindName(nameof(InnerNavigationToolbar));
+ FindName(nameof(TabControl));
+ FindName(nameof(NavToolbar));
}
private void NumberedTabKeyboardAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs e)
@@ -131,6 +136,9 @@ private void HorizontalMultitaskingControl_Loaded(object sender, RoutedEventArgs
private int SetTitleBarDragRegion(InputNonClientPointerSource source, SizeInt32 size, double scaleFactor, Func getScaledRect)
{
+ if (TabControl is null)
+ return -1;
+
var height = (int)TabControl.ActualHeight;
source.SetRegionRects(NonClientRegionKind.Passthrough, [getScaledRect(this, new RectInt32(0, 0, (int)(TabControl.ActualWidth + TabControl.Margin.Left - TabControl.DragArea.ActualWidth), height))]);
AttachTitleBarMessageMonitor();
@@ -328,10 +336,7 @@ private void Page_Loaded(object sender, RoutedEventArgs e)
MainWindow.Instance.AppWindow.Changed += (_, _) => MainWindow.Instance.RaiseSetTitleBarDragRegion(SetTitleBarDragRegion);
- // Defers loading until after the page has loaded to improve startup perf
- FindName(nameof(InnerNavigationToolbar));
- FindName(nameof(TabControl));
- FindName(nameof(NavToolbar));
+ MainWindow.Instance.RaiseSetTitleBarDragRegion(SetTitleBarDragRegion);
// Notify user that drag and drop is disabled
// Prompt is disabled in the dev environment to prevent issues with the automation testing
diff --git a/src/Files.App/Views/ShellPanesPage.xaml.cs b/src/Files.App/Views/ShellPanesPage.xaml.cs
index 7b6c35798fb6..0900abda6c73 100644
--- a/src/Files.App/Views/ShellPanesPage.xaml.cs
+++ b/src/Files.App/Views/ShellPanesPage.xaml.cs
@@ -962,6 +962,7 @@ private void Pane_ContentChanged(object? sender, TabBarItemParameter e)
[DynamicWindowsRuntimeCast(typeof(UIElement))]
[DynamicWindowsRuntimeCast(typeof(ButtonBase))]
+ [DynamicWindowsRuntimeCast(typeof(DependencyObject))]
private void Pane_PointerPressed(object sender, PointerRoutedEventArgs e)
{
// A button cancels its press once it loses focus, so leave focus alone while one is being pressed
From 6593fe4654ee03bc068dcd64225eb72614d63dea Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 5 Oct 2026 11:29:51 -0400
Subject: [PATCH 3/5] Bump Microsoft.Windows.CsWin32 from 0.3.335 to 0.3.346
(#19005)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Directory.Packages.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 53de05b339d5..add5c0e9ef15 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -42,7 +42,7 @@
-
+
From d691c8c84cbae914388fd0549b98931e77d21321 Mon Sep 17 00:00:00 2001
From: yair100 <39923744+yair100@users.noreply.github.com>
Date: Mon, 5 Oct 2026 11:30:41 -0400
Subject: [PATCH 4/5] Build: v4.2.38
---
src/Files.App/Package.appxmanifest | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Files.App/Package.appxmanifest b/src/Files.App/Package.appxmanifest
index 276e73dbb0f4..4dbde45b21a5 100644
--- a/src/Files.App/Package.appxmanifest
+++ b/src/Files.App/Package.appxmanifest
@@ -17,7 +17,7 @@
+ Version="4.2.38.0" />
Files - Dev
From 574d9ae15512e3e5e8290e996795a71c8c938a80 Mon Sep 17 00:00:00 2001
From: yair100 <39923744+yair100@users.noreply.github.com>
Date: Mon, 5 Oct 2026 11:53:40 -0400
Subject: [PATCH 5/5] Fix: Fixed several crashes and log noise reported by
Sentry (#18985)
---
.github/workflows/ci.yml | 11 +++++++++
.../Actions/Navigation/NextTabAction.cs | 6 ++---
.../Actions/Navigation/PreviousTabAction.cs | 6 ++---
.../UserControls/TabBar/TabBar.xaml.cs | 5 ++--
.../Utils/Cloud/CloudDrivesManager.cs | 11 +++++++++
src/Files.App/ViewModels/ShellViewModel.cs | 4 +++-
.../Tests/ArchiveTests.cs | 24 +++++++++++++++++--
7 files changed, 56 insertions(+), 11 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index acb16082775a..29ddd90234ca 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -354,6 +354,17 @@ jobs:
with:
name: '*'
+ # Uploaded after the cleanup step above so a failed run keeps its diagnostics
+ - if: failure()
+ name: Upload the test diagnostics to the Artifacts
+ uses: actions/upload-artifact@v7
+ with:
+ name: 'Test Diagnostics (${{ env.CONFIGURATION }}, ${{ env.AUTOMATED_TESTS_ARCHITECTURE }})'
+ if-no-files-found: warn
+ path: |
+ ${{ env.AUTOMATED_TESTS_ASSEMBLY_DIR }}\appium.log
+ ${{ env.AUTOMATED_TESTS_ASSEMBLY_DIR }}\testResults.trx
+
# - name: Generate markdown from the tests result
# shell: pwsh
# run: |
diff --git a/src/Files.App/Actions/Navigation/NextTabAction.cs b/src/Files.App/Actions/Navigation/NextTabAction.cs
index 980bc29dc557..1442aaf4f5d8 100644
--- a/src/Files.App/Actions/Navigation/NextTabAction.cs
+++ b/src/Files.App/Actions/Navigation/NextTabAction.cs
@@ -41,9 +41,9 @@ public async Task ExecuteAsync(object? parameter = null)
// Small delay for the UI to load
await Task.Delay(500);
- // Focus the content of the selected tab item (needed for keyboard navigation)
- var paneHolder = contentPageContext.ShellPage.GetRequiredPaneHolder();
- paneHolder.FocusActivePane();
+ // Focus the content of the selected tab item (needed for keyboard navigation).
+ // The tab can be gone by the time the delay elapses, and focusing it is best effort
+ contentPageContext.ShellPage?.PaneHolder?.FocusActivePane();
}
private void MultitaskingContext_PropertyChanged(object? sender, PropertyChangedEventArgs e)
diff --git a/src/Files.App/Actions/Navigation/PreviousTabAction.cs b/src/Files.App/Actions/Navigation/PreviousTabAction.cs
index 19ecd9f30ad6..07616182aaac 100644
--- a/src/Files.App/Actions/Navigation/PreviousTabAction.cs
+++ b/src/Files.App/Actions/Navigation/PreviousTabAction.cs
@@ -44,9 +44,9 @@ public async Task ExecuteAsync(object? parameter = null)
// Small delay for the UI to load
await Task.Delay(500);
- // Focus the content of the selected tab item (needed for keyboard navigation)
- var paneHolder = contentPageContext.ShellPage.GetRequiredPaneHolder();
- paneHolder.FocusActivePane();
+ // Focus the content of the selected tab item (needed for keyboard navigation).
+ // The tab can be gone by the time the delay elapses, and focusing it is best effort
+ contentPageContext.ShellPage?.PaneHolder?.FocusActivePane();
}
private void MultitaskingContext_PropertyChanged(object? sender, PropertyChangedEventArgs e)
diff --git a/src/Files.App/UserControls/TabBar/TabBar.xaml.cs b/src/Files.App/UserControls/TabBar/TabBar.xaml.cs
index dd67e005e3c8..155b685a1093 100644
--- a/src/Files.App/UserControls/TabBar/TabBar.xaml.cs
+++ b/src/Files.App/UserControls/TabBar/TabBar.xaml.cs
@@ -313,8 +313,9 @@ private async void TabView_TabDroppedOutside(TabView sender, TabViewTabDroppedOu
Items.Remove(tabItem);
if (!await NavigationHelpers.OpenTabInNewWindowAsync(tabViewItemArgs.Serialize(), droppedPoint.X, droppedPoint.Y))
{
- Items.Insert(indexOfTabViewItem, tabItem);
- sender.SelectedIndex = selectedTabViewItemIndex;
+ // Other tabs can close while the new window opens, so the remembered positions may no longer exist
+ Items.Insert(Math.Clamp(indexOfTabViewItem, 0, Items.Count), tabItem);
+ sender.SelectedIndex = Math.Clamp(selectedTabViewItemIndex, 0, Items.Count - 1);
}
else
// Dispose tab arguments
diff --git a/src/Files.App/Utils/Cloud/CloudDrivesManager.cs b/src/Files.App/Utils/Cloud/CloudDrivesManager.cs
index 5aaf12b1925f..e4a7d5bea72d 100644
--- a/src/Files.App/Utils/Cloud/CloudDrivesManager.cs
+++ b/src/Files.App/Utils/Cloud/CloudDrivesManager.cs
@@ -4,12 +4,18 @@
using Microsoft.Extensions.Logging;
using System.Collections.Specialized;
using System.IO;
+using System.Runtime.InteropServices;
using Windows.Storage;
namespace Files.App.Utils.Cloud
{
public static class CloudDrivesManager
{
+ // HRESULT_FROM_WIN32 of ERROR_FILE_NOT_FOUND, ERROR_PATH_NOT_FOUND and ERROR_INVALID_DRIVE
+ private const int FileNotFoundHResult = unchecked((int)0x80070002);
+ private const int PathNotFoundHResult = unchecked((int)0x80070003);
+ private const int InvalidDriveHResult = unchecked((int)0x8007000F);
+
private static readonly ILogger _logger = Ioc.Default.GetRequiredService>();
private static readonly ICloudDetector _detector = Ioc.Default.GetRequiredService();
public static EventHandler? DataChanged;
@@ -79,6 +85,11 @@ private static async Task SetRootAndUpdatePropertiesAsync(DriveItem cloudProvide
{
_logger?.LogInformation(ex, "Failed to find the cloud folder");
}
+ catch (COMException ex) when (ex.HResult is FileNotFoundHResult or PathNotFoundHResult or InvalidDriveHResult)
+ {
+ // A cloud folder whose drive or path is gone surfaces from the shell as an HRESULT instead
+ _logger?.LogInformation(ex, "Failed to find the cloud folder");
+ }
catch (UnauthorizedAccessException ex)
{
_logger?.LogInformation(ex, " Failed to access the cloud folder");
diff --git a/src/Files.App/ViewModels/ShellViewModel.cs b/src/Files.App/ViewModels/ShellViewModel.cs
index d3a44093007f..ffc13ccd9a85 100644
--- a/src/Files.App/ViewModels/ShellViewModel.cs
+++ b/src/Files.App/ViewModels/ShellViewModel.cs
@@ -1183,7 +1183,9 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() =>
}
catch (Exception ex)
{
- App.Logger.LogWarning(ex, ex.Message);
+ // A newer load cancelled this one or disposed its token source, which is not worth reporting
+ if (ex is not (OperationCanceledException or ObjectDisposedException))
+ App.Logger.LogWarning(ex, ex.Message);
}
}
diff --git a/tests/Files.InteractionTests/Tests/ArchiveTests.cs b/tests/Files.InteractionTests/Tests/ArchiveTests.cs
index 2e2f8ed662a4..4a72e08d20b2 100644
--- a/tests/Files.InteractionTests/Tests/ArchiveTests.cs
+++ b/tests/Files.InteractionTests/Tests/ArchiveTests.cs
@@ -93,8 +93,7 @@ private static void CompressAndExtractRoundTrip(string sourceFolderName, string
foreach (var (name, content) in files)
{
var extractedFilePath = Path.Combine(testFolderPath, name);
- WaitForCondition(() => File.Exists(extractedFilePath), $"'{name}' to be extracted");
- Assert.AreEqual(content, File.ReadAllText(extractedFilePath), $"The extracted file '{name}' does not match the original content.");
+ Assert.AreEqual(content, ReadWhenAvailable(extractedFilePath), $"The extracted file '{name}' does not match the original content.");
}
// The extracted files also show up in the file area
@@ -139,6 +138,27 @@ private static void InvokeItemContextMenuCommand(string itemName, string subMenu
}
}
+ ///
+ /// Reads a file the extractor may still be writing. The entry appears on disk before its
+ /// write handle is closed, so both the missing file and the sharing violation are retried.
+ ///
+ private static string ReadWhenAvailable(string path)
+ {
+ var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(30);
+
+ while (true)
+ {
+ try
+ {
+ return File.ReadAllText(path);
+ }
+ catch (IOException) when (DateTime.UtcNow < deadline)
+ {
+ Thread.Sleep(300);
+ }
+ }
+ }
+
///
/// Waits until the given disk-state condition holds; archive operations run in the
/// background, so their completion is only observable through the file system.