fix(module: table): fix collection modified exception (#570)

This commit is contained in:
Diego Frata 2020-09-05 13:27:52 +01:00 committed by GitHub
parent b257d17844
commit 7119351b9f
11 changed files with 517 additions and 15 deletions

View File

@ -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'">

View File

@ -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)

View File

@ -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));

View File

@ -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();

View File

@ -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="&#x4E0A;&#x4E00;&#x9875;" 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="&#x4E0B;&#x4E00;&#x9875;" 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>

View File

@ -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="&#x4E0A;&#x4E00;&#x9875;" 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="&#x4E0B;&#x4E00;&#x9875;" 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>

View File

@ -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">&#x6682;&#x65E0;&#x6570;&#x636E;</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="&#x4E0A;&#x4E00;&#x9875;" 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="&#x4E0B;&#x4E00;&#x9875;" 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>

View File

@ -20,4 +20,8 @@
<ProjectReference Include="..\components\AntDesign.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="$Recorded" />
</ItemGroup>
</Project>

View File

@ -2,6 +2,7 @@
using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
namespace AntDesign.Tests
{

View File

@ -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.");
}
}
}
}

104
tests/table/TableTests.cs Normal file
View File

@ -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();
}
}
}