fix(module: table): fix collection modified exception (#570)
This commit is contained in:
Родитель
b257d17844
Коммит
7119351b9f
|
@ -15,6 +15,7 @@
|
|||
<Copyright>James Yeung</Copyright>
|
||||
<Authors>James Yeung</Authors>
|
||||
<PackageIcon>logo.png</PackageIcon>
|
||||
<NoWarn>CA2007</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
|
|
@ -27,10 +27,7 @@ namespace AntDesign
|
|||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count(); i++)
|
||||
{
|
||||
await func(items.ElementAt(i));
|
||||
}
|
||||
foreach (var item in items) await func(item);
|
||||
}
|
||||
|
||||
public static bool IsIn<T>(this T source, params T[] array)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using AntDesign.TableModels;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AntDesign.TableModels;
|
||||
|
||||
namespace AntDesign
|
||||
{
|
||||
|
@ -32,8 +32,8 @@ namespace AntDesign
|
|||
return;
|
||||
|
||||
// Clear cached items that are not on current page
|
||||
var currentPageCacheKeys = _selection.RowSelections.Select(x => x.CacheKey);
|
||||
var deletedCaches = _dataSourceCache.Where(x => x.Value.PageIndex == PageIndex && !x.Key.IsIn(currentPageCacheKeys));
|
||||
var currentPageCacheKeys = _selection.RowSelections.Select(x => x.CacheKey).ToHashSet();
|
||||
var deletedCaches = _dataSourceCache.Where(x => x.Value.PageIndex == PageIndex && !currentPageCacheKeys.Contains(x.Key)).ToList();
|
||||
var needInvokeChange = deletedCaches.Any(x => x.Value.Selected);
|
||||
deletedCaches.ForEach(x => _dataSourceCache.Remove(x));
|
||||
|
||||
|
|
|
@ -110,16 +110,19 @@ namespace AntDesign
|
|||
}
|
||||
else
|
||||
{
|
||||
var query = _dataSource.AsQueryable();
|
||||
foreach (var sort in queryModel.SortModel)
|
||||
if (_dataSource != null)
|
||||
{
|
||||
sort.Sort(query);
|
||||
var query = _dataSource.AsQueryable();
|
||||
foreach (var sort in queryModel.SortModel)
|
||||
{
|
||||
sort.Sort(query);
|
||||
}
|
||||
|
||||
query = query.Skip((PageIndex - 1) * PageSize).Take(PageSize);
|
||||
queryModel.SetQueryableLambda(query);
|
||||
|
||||
_showItems = query;
|
||||
}
|
||||
|
||||
query = query.Skip((PageIndex - 1) * PageSize).Take(PageSize);
|
||||
queryModel.SetQueryableLambda(query);
|
||||
|
||||
_showItems = query;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
<link href="../../components/wwwroot/css/ant-design-blazor.css" rel="stylesheet">
|
||||
<div class="ant-table-wrapper">
|
||||
<div class="ant-spin-nested-loading">
|
||||
<div>
|
||||
|
||||
|
||||
<div class="ant-spin-container ">
|
||||
|
||||
|
||||
<div class=" ant-table ant-table-middle">
|
||||
<div class="ant-table-container">
|
||||
<div class="ant-table-content">
|
||||
<table style="table-layout: auto;">
|
||||
|
||||
|
||||
<colgroup>
|
||||
</colgroup>
|
||||
|
||||
|
||||
|
||||
<thead class="ant-table-thead">
|
||||
<tr>
|
||||
|
||||
|
||||
<th class=" ant-table-cell" style=" " blazor:onclick="4">
|
||||
</th>
|
||||
<th class=" ant-table-cell" style=" " blazor:onclick="5">
|
||||
</th>
|
||||
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="ant-table-tbody">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<tr data-row-id="2" class="ant-table-row ant-table-row-level-0 ">
|
||||
<td class=" ant-table-cell" style=" ">
|
||||
Joe </td>
|
||||
<td class=" ant-table-cell" style=" ">
|
||||
Doe </td>
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ul class="ant-table-pagination ant-table-pagination-right ant-pagination" id:ignore blazor:elementreference="">
|
||||
|
||||
<li class=" ant-pagination-prev" title="上一页" id:ignore blazor:onclick="1" blazor:elementreference="">
|
||||
|
||||
|
||||
<a class="ant-pagination-item-link">
|
||||
<span role="img" class=" anticon anticon-left" id:ignore blazor:onclick="6">
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class=" ant-pagination-item" title="1" id:ignore blazor:onclick="2" blazor:elementreference="">
|
||||
|
||||
|
||||
<a>1</a>
|
||||
</li>
|
||||
<li class=" ant-pagination-item ant-pagination-item-active" title="2" id:ignore blazor:onclick="8" blazor:elementreference="">
|
||||
|
||||
|
||||
<a>2</a>
|
||||
</li>
|
||||
<li class=" ant-pagination-next ant-pagination-disabled" title="下一页" id:ignore blazor:onclick="12" blazor:elementreference="">
|
||||
|
||||
|
||||
<a class="ant-pagination-item-link">
|
||||
<span role="img" class=" anticon anticon-right" id:ignore blazor:onclick="13">
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,104 @@
|
|||
<link href="../../components/wwwroot/css/ant-design-blazor.css" rel="stylesheet">
|
||||
<div class="ant-table-wrapper">
|
||||
<div class="ant-spin-nested-loading">
|
||||
<div>
|
||||
|
||||
|
||||
<div class="ant-spin-container ">
|
||||
|
||||
|
||||
<div class=" ant-table ant-table-middle">
|
||||
<div class="ant-table-container">
|
||||
<div class="ant-table-content">
|
||||
<table style="table-layout: auto;">
|
||||
|
||||
|
||||
<colgroup>
|
||||
</colgroup>
|
||||
|
||||
|
||||
|
||||
<thead class="ant-table-thead">
|
||||
<tr>
|
||||
|
||||
|
||||
<th class=" ant-table-cell" style=" " blazor:onclick="4">
|
||||
</th>
|
||||
<th class=" ant-table-cell" style=" " blazor:onclick="5">
|
||||
</th>
|
||||
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="ant-table-tbody">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<tr data-row-id="1" class="ant-table-row ant-table-row-level-0 ">
|
||||
<td class=" ant-table-cell" style=" ">
|
||||
John </td>
|
||||
<td class=" ant-table-cell" style=" ">
|
||||
Smith </td>
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<tr data-row-id="2" class="ant-table-row ant-table-row-level-0 ">
|
||||
<td class=" ant-table-cell" style=" ">
|
||||
Jane </td>
|
||||
<td class=" ant-table-cell" style=" ">
|
||||
Doe </td>
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ul class="ant-table-pagination ant-table-pagination-right ant-pagination" id:ignore blazor:elementreference="">
|
||||
|
||||
<li class=" ant-pagination-prev ant-pagination-disabled" title="上一页" id:ignore blazor:onclick="1" blazor:elementreference="">
|
||||
|
||||
|
||||
<a class="ant-pagination-item-link">
|
||||
<span role="img" class=" anticon anticon-left" id:ignore blazor:onclick="6">
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class=" ant-pagination-item ant-pagination-item-active" title="1" id:ignore blazor:onclick="2" blazor:elementreference="">
|
||||
|
||||
|
||||
<a>1</a>
|
||||
</li>
|
||||
<li class=" ant-pagination-next ant-pagination-disabled" title="下一页" id:ignore blazor:onclick="3" blazor:elementreference="">
|
||||
|
||||
|
||||
<a class="ant-pagination-item-link">
|
||||
<span role="img" class=" anticon anticon-right" id:ignore blazor:onclick="7">
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,99 @@
|
|||
<link href="../../components/wwwroot/css/ant-design-blazor.css" rel="stylesheet">
|
||||
<div class="ant-table-wrapper">
|
||||
<div class="ant-spin-nested-loading">
|
||||
<div>
|
||||
|
||||
|
||||
<div class="ant-spin-container ">
|
||||
|
||||
|
||||
<div class=" ant-table ant-table-middle">
|
||||
<div class="ant-table-container">
|
||||
<div class="ant-table-content">
|
||||
<table style="table-layout: auto;">
|
||||
|
||||
|
||||
<colgroup>
|
||||
</colgroup>
|
||||
|
||||
|
||||
|
||||
<thead class="ant-table-thead">
|
||||
<tr>
|
||||
|
||||
|
||||
<th class=" ant-table-cell" style=" " blazor:onclick="4">
|
||||
</th>
|
||||
<th class=" ant-table-cell" style=" " blazor:onclick="5">
|
||||
</th>
|
||||
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="ant-table-tbody">
|
||||
|
||||
|
||||
<tr class="ant-table-placeholder">
|
||||
<td colspan="2" class="ant-table-cell">
|
||||
<div class="ant-empty ant-empty-ltr" id:ignore blazor:elementreference:ignore>
|
||||
<div class="ant-empty-image">
|
||||
<svg class="ant-empty-img-simple" width="64" height="41" viewBox="0 0 64 41" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(0 1)" fill="none" fillRule="evenodd">
|
||||
<ellipse class="ant-empty-img-simple-ellipse" cx="32" cy="33" rx="32" ry="7"></ellipse>
|
||||
<g class="ant-empty-img-simple-g" fillRule="nonzero">
|
||||
<path d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"></path>
|
||||
<path d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z" class="ant-empty-img-simple-path"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg> </div>
|
||||
|
||||
<p class="ant-empty-description">暂无数据</p>
|
||||
<div class="ant-empty-footer">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ul class="ant-table-pagination ant-table-pagination-right ant-pagination" id:ignore blazor:elementreference="">
|
||||
|
||||
<li class=" ant-pagination-prev ant-pagination-disabled" title="上一页" id:ignore blazor:onclick="1" blazor:elementreference="">
|
||||
|
||||
|
||||
<a class="ant-pagination-item-link">
|
||||
<span role="img" class=" anticon anticon-left" id:ignore blazor:onclick="6">
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class=" ant-pagination-item ant-pagination-item-active" title="1" id:ignore blazor:onclick="2" blazor:elementreference="">
|
||||
|
||||
|
||||
<a>1</a>
|
||||
</li>
|
||||
<li class=" ant-pagination-next ant-pagination-disabled" title="下一页" id:ignore blazor:onclick="3" blazor:elementreference="">
|
||||
|
||||
|
||||
<a class="ant-pagination-item-link">
|
||||
<span role="img" class=" anticon anticon-right" id:ignore blazor:onclick="7">
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -20,4 +20,8 @@
|
|||
<ProjectReference Include="..\components\AntDesign.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="$Recorded" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AntDesign.Tests
|
||||
{
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace AntDesign.Tests
|
||||
{
|
||||
public static class RecordedTestExtensions
|
||||
{
|
||||
private const string Style =
|
||||
"<link href=\"../../components/wwwroot/css/ant-design-blazor.css\" rel=\"stylesheet\">\n";
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void RecordedMarkupMatches<T>(this IRenderedComponent<T> component) where T : notnull, IComponent
|
||||
{
|
||||
static string Cleanup(string value)
|
||||
{
|
||||
value = Regex.Replace(value, "id=\"ant-blazor-.+?\"", "id:ignore");
|
||||
value = Regex.Replace(value, "blazor:elementreference=\".+?\"", "blazor:elementreference:ignore");
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
var caller = new StackTrace().GetFrame(1)?.GetMethod();
|
||||
RecordedMarkupMatches(
|
||||
component.Markup,
|
||||
caller,
|
||||
expected => component.MarkupMatches(expected),
|
||||
Cleanup
|
||||
);
|
||||
}
|
||||
|
||||
public static void RecordedMarkupMatches(string markup)
|
||||
{
|
||||
var caller = new StackTrace().GetFrame(1)?.GetMethod();
|
||||
RecordedMarkupMatches(
|
||||
markup,
|
||||
caller,
|
||||
expected => Assert.Equal(markup, expected),
|
||||
m => m
|
||||
);
|
||||
}
|
||||
|
||||
private static void RecordedMarkupMatches(string markup, MethodBase caller, Action<string> assert,
|
||||
Func<string, string> transform)
|
||||
{
|
||||
if (caller == null)
|
||||
throw new XunitException("Cannot find caller from StackTrace.");
|
||||
|
||||
if (caller.ReflectedType == null)
|
||||
throw new XunitException("Cannot access ReflectedType for the method.");
|
||||
|
||||
// Here we make an assumption that project is not following an unconventional directory structure.
|
||||
var expectedPath = $"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}";
|
||||
var parts = caller.ReflectedType.Assembly.Location.Split(expectedPath);
|
||||
if (parts.Length == 1)
|
||||
throw new XunitException($"Path does not include ${expectedPath}");
|
||||
|
||||
var recordedTestsPath = Path.Combine(parts[0], "$Recorded");
|
||||
|
||||
if (!Directory.Exists(recordedTestsPath))
|
||||
Directory.CreateDirectory(recordedTestsPath);
|
||||
|
||||
var sanitisedFileName = Path.GetInvalidFileNameChars()
|
||||
.Aggregate(new StringBuilder($"{caller.ReflectedType}{caller.Name}.html"),
|
||||
(builder, c) => builder.Replace(c, '_'))
|
||||
.ToString();
|
||||
|
||||
var testFile = Path.Combine(recordedTestsPath, sanitisedFileName);
|
||||
|
||||
if (File.Exists(testFile))
|
||||
{
|
||||
var expected = File.ReadAllText(testFile);
|
||||
assert(expected.Replace(Style, ""));
|
||||
}
|
||||
else
|
||||
{
|
||||
File.WriteAllText(testFile, Style + transform(markup));
|
||||
throw new XunitException(
|
||||
"Test file for comparison was not found, so a new one was created. Please review the file before re-running the test.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Xunit;
|
||||
|
||||
namespace AntDesign.Tests.Table
|
||||
{
|
||||
public class TableTests : AntDesignTestBase
|
||||
{
|
||||
class Person
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Surname { get; set; }
|
||||
}
|
||||
|
||||
private IRenderedComponent<Table<Person>> CreatePersonsTable(
|
||||
IReadOnlyList<Person> persons,
|
||||
Action<ComponentParameterBuilder<Table<Person>>> callback = null,
|
||||
bool enableSelection = false)
|
||||
{
|
||||
return Context.RenderComponent<Table<Person>>(x =>
|
||||
{
|
||||
x
|
||||
.Add(b => b.DataSource, persons)
|
||||
.Add(b => b.ChildContent, p =>
|
||||
{
|
||||
var selection = new ComponentParameterBuilder<Selection>()
|
||||
.Add(q => q.Key, p.Id.ToString())
|
||||
.Build()
|
||||
.ToComponentRenderFragment<Selection>();
|
||||
|
||||
var nameCol = new ComponentParameterBuilder<Column<string>>()
|
||||
.Add(q => q.Field, p.Name)
|
||||
.Build()
|
||||
.ToComponentRenderFragment<Column<string>>();
|
||||
|
||||
var surnameCol = new ComponentParameterBuilder<Column<string>>()
|
||||
.Add(q => q.Field, p.Surname)
|
||||
.Build()
|
||||
.ToComponentRenderFragment<Column<string>>();
|
||||
|
||||
return builder =>
|
||||
{
|
||||
if (enableSelection) selection(builder);
|
||||
nameCol(builder);
|
||||
surnameCol(builder);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
callback?.Invoke(x);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Renders_an_empty_table()
|
||||
{
|
||||
var persons = Array.Empty<Person>();
|
||||
|
||||
var cut = CreatePersonsTable(persons);
|
||||
|
||||
cut.RecordedMarkupMatches();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Renders_a_table_with_two_rows()
|
||||
{
|
||||
var persons = new[]
|
||||
{
|
||||
new Person {Id = 1, Name = "John", Surname = "Smith"},
|
||||
new Person {Id = 2, Name = "Jane", Surname = "Doe"}
|
||||
};
|
||||
|
||||
var cut = CreatePersonsTable(persons);
|
||||
|
||||
cut.RecordedMarkupMatches();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Can_render_after_changes_to_the_dataSource()
|
||||
{
|
||||
var persons = new List<Person>
|
||||
{
|
||||
new Person {Id = 1, Name = "John", Surname = "Smith"},
|
||||
new Person {Id = 2, Name = "Jane", Surname = "Doe"},
|
||||
new Person {Id = 3, Name = "Joe", Surname = "Doe"}
|
||||
};
|
||||
|
||||
var cut = CreatePersonsTable(persons, b => b
|
||||
.Add(q => q.PageSize, 1)
|
||||
.Add(q => q.PageIndex, 3)
|
||||
);
|
||||
|
||||
persons.RemoveAt(0);
|
||||
|
||||
cut.SetParametersAndRender(b => b.Add(q => q.DataSource, persons));
|
||||
|
||||
cut.RecordedMarkupMatches();
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче