12dd1de9f2
- Add theme system (Dark/Light palettes, ModernTheme, ThemeManager) - Add InputDialog, Spinner common view - Add DuplicatesCsvExportService - Refresh views, dialogs, and view models across tabs - Update localization strings (en/fr) - Tweak services (transfer, permissions, search, user access, ownership elevation, bulk operations) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
114 lines
4.2 KiB
C#
114 lines
4.2 KiB
C#
using System.Runtime.CompilerServices;
|
|
using Microsoft.SharePoint.Client;
|
|
using SharepointToolbox.Core.Models;
|
|
|
|
namespace SharepointToolbox.Core.Helpers;
|
|
|
|
public static class SharePointPaginationHelper
|
|
{
|
|
// Max page size SharePoint honors with Paged='TRUE' (threshold bypass).
|
|
private const int DefaultRowLimit = 5000;
|
|
|
|
/// <summary>
|
|
/// Enumerates all items in a SharePoint list, bypassing the 5,000-item threshold.
|
|
/// Uses CamlQuery with Paged='TRUE' RowLimit and ListItemCollectionPosition for pagination.
|
|
/// Never call ExecuteQuery directly on a list — always use this helper.
|
|
/// </summary>
|
|
public static async IAsyncEnumerable<ListItem> GetAllItemsAsync(
|
|
ClientContext ctx,
|
|
List list,
|
|
CamlQuery? baseQuery = null,
|
|
[EnumeratorCancellation] CancellationToken ct = default)
|
|
{
|
|
var query = baseQuery ?? CamlQuery.CreateAllItemsQuery();
|
|
query.ViewXml = BuildPagedViewXml(query.ViewXml, DefaultRowLimit);
|
|
query.ListItemCollectionPosition = null;
|
|
|
|
do
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
var items = list.GetItems(query);
|
|
ctx.Load(items);
|
|
await ctx.ExecuteQueryAsync();
|
|
|
|
foreach (var item in items)
|
|
yield return item;
|
|
|
|
query.ListItemCollectionPosition = items.ListItemCollectionPosition;
|
|
}
|
|
while (query.ListItemCollectionPosition != null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enumerates items within a specific folder (direct children by default, or
|
|
/// recursive when <paramref name="recursive"/> is true). Uses paginated CAML
|
|
/// with no WHERE clause so it works on libraries above the 5,000-item threshold.
|
|
/// Callers filter by FSObjType client-side via the returned ListItem fields.
|
|
/// </summary>
|
|
public static async IAsyncEnumerable<ListItem> GetItemsInFolderAsync(
|
|
ClientContext ctx,
|
|
List list,
|
|
string folderServerRelativeUrl,
|
|
bool recursive,
|
|
string[]? viewFields = null,
|
|
[EnumeratorCancellation] CancellationToken ct = default)
|
|
{
|
|
var fields = viewFields ?? new[]
|
|
{
|
|
"FSObjType", "FileRef", "FileLeafRef", "FileDirRef", "File_x0020_Size"
|
|
};
|
|
|
|
var viewFieldsXml = string.Join(string.Empty,
|
|
fields.Select(f => $"<FieldRef Name='{f}' />"));
|
|
|
|
var scope = recursive ? " Scope='RecursiveAll'" : string.Empty;
|
|
var viewXml =
|
|
$"<View{scope}>" +
|
|
"<Query></Query>" +
|
|
$"<ViewFields>{viewFieldsXml}</ViewFields>" +
|
|
$"<RowLimit Paged='TRUE'>{DefaultRowLimit}</RowLimit>" +
|
|
"</View>";
|
|
|
|
var query = new CamlQuery
|
|
{
|
|
ViewXml = viewXml,
|
|
FolderServerRelativeUrl = folderServerRelativeUrl,
|
|
ListItemCollectionPosition = null
|
|
};
|
|
|
|
do
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
var items = list.GetItems(query);
|
|
ctx.Load(items);
|
|
await ctx.ExecuteQueryAsync();
|
|
|
|
foreach (var item in items)
|
|
yield return item;
|
|
|
|
query.ListItemCollectionPosition = items.ListItemCollectionPosition;
|
|
}
|
|
while (query.ListItemCollectionPosition != null);
|
|
}
|
|
|
|
internal static string BuildPagedViewXml(string? existingXml, int rowLimit)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(existingXml))
|
|
return $"<View><RowLimit Paged='TRUE'>{rowLimit}</RowLimit></View>";
|
|
|
|
// Replace any existing <RowLimit ...>n</RowLimit> with paged form.
|
|
if (System.Text.RegularExpressions.Regex.IsMatch(
|
|
existingXml, @"<RowLimit[^>]*>\d+</RowLimit>",
|
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
|
|
{
|
|
return System.Text.RegularExpressions.Regex.Replace(
|
|
existingXml, @"<RowLimit[^>]*>\d+</RowLimit>",
|
|
$"<RowLimit Paged='TRUE'>{rowLimit}</RowLimit>",
|
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
|
}
|
|
return existingXml.Replace("</View>",
|
|
$"<RowLimit Paged='TRUE'>{rowLimit}</RowLimit></View>",
|
|
StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
}
|