Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/publish-me-nuget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# ModelingEvolution fork: publishes ModelingEvolution.MudBlazor on tags "me/X.Y.Z[.N]".
# Upstream's deploy-mudblazor-nuget.yml fires on "v*" tags only, so the two never collide.
name: publish-me-nuget

on:
push:
tags:
- "me/[0-9]+.[0-9]+.[0-9]+*"

env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Get version
id: version
run: echo "VERSION=${GITHUB_REF_NAME#me/}" >> $GITHUB_OUTPUT

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json

- name: Restore
working-directory: src/MudBlazor
run: dotnet restore

- name: Pack
working-directory: src/MudBlazor
run: dotnet pack -c Release --no-restore --output nupkgs /p:Version=${{ steps.version.outputs.VERSION }}

- name: Check package contains css
shell: pwsh
run: ./tools/CheckPackageContainsStaticAssets.ps1 ./src/MudBlazor/nupkgs MudBlazor.min.css

- name: Check package contains js
shell: pwsh
run: ./tools/CheckPackageContainsStaticAssets.ps1 ./src/MudBlazor/nupkgs MudBlazor.min.js

- name: Push to ModelingEvolution NuGet
working-directory: src/MudBlazor
run: |
dotnet nuget push nupkgs/*.nupkg \
--api-key ${{ secrets.NUGET_API_KEY_ME }} \
--source https://nuget.modelingevolution.com/v3/index.json \
--skip-duplicate
continue-on-error: true

- name: Push to NuGet.org
working-directory: src/MudBlazor
run: |
dotnet nuget push nupkgs/*.nupkg \
--api-key ${{ secrets.NUGET_API_KEY }} \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
AdditionalDateClassesFunc="@((DateTime dt)=>((int)dt.DayOfWeek == 0 ? "red-text text-accent-4" : ""))" />

<MudStack Row>
<MudSwitch @bind-Value="AllowWeekends" @bind-Value:after="@(() => _picker.RecalculateValidDays())" Color="Color.Primary">Allow Weekends</MudSwitch>
<MudSwitch @bind-Value="AllowWeekends" @bind-Value:after="@(() => _picker?.RecalculateValidDays())" Color="Color.Primary">Allow Weekends</MudSwitch>
<MudSwitch @bind-Value="CountDisabledDays" Color="Color.Primary">Include Disabled</MudSwitch>
</MudStack>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
@using System.Collections.Specialized
@using System.ComponentModel

<MudTable T="Item" Items="Source" AutoReloadOnCollectionChanged="AutoReloadOnCollectionChanged" AutoReloadOnItemPropertyChanged="AutoReloadOnItemPropertyChanged">
<HeaderContent>
<MudTh>Name</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Name</MudTd>
</RowTemplate>
</MudTable>

@code {
public static string __description__ = "AutoReloadOnCollectionChanged / AutoReloadOnItemPropertyChanged re-render the table from INotifyCollectionChanged / INotifyPropertyChanged without reassigning Items.";

[Parameter] public bool AutoReloadOnCollectionChanged { get; set; }
[Parameter] public bool AutoReloadOnItemPropertyChanged { get; set; }
[Parameter] public TrackedCollection Source { get; set; } = new() { new Item("a"), new Item("b"), new Item("c") };

/// <summary>An item that counts its PropertyChanged subscribers so tests can prove unsubscription.</summary>
public sealed class Item(string name) : INotifyPropertyChanged
{
private string _name = name;
private PropertyChangedEventHandler? _propertyChanged;

public int SubscriberCount { get; private set; }

public string Name
{
get => _name;
set
{
_name = value;
_propertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}

public event PropertyChangedEventHandler? PropertyChanged
{
add { _propertyChanged += value; SubscriberCount++; }
remove { _propertyChanged -= value; SubscriberCount--; }
}
}

/// <summary>An ObservableCollection that counts its CollectionChanged subscribers.</summary>
public sealed class TrackedCollection : System.Collections.ObjectModel.ObservableCollection<Item>, INotifyCollectionChanged
{
private NotifyCollectionChangedEventHandler? _handler;

public int SubscriberCount { get; private set; }

event NotifyCollectionChangedEventHandler? INotifyCollectionChanged.CollectionChanged
{
add { _handler += value; SubscriberCount++; }
remove { _handler -= value; SubscriberCount--; }
}

protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
_handler?.Invoke(this, e);
}
}
}
225 changes: 225 additions & 0 deletions src/MudBlazor.UnitTests/Components/TableAutoReloadTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
using AwesomeAssertions;
using Bunit;
using MudBlazor.UnitTests.TestComponents.Table;
using NUnit.Framework;

namespace MudBlazor.UnitTests.Components
{
/// <summary>
/// <see cref="MudTable{T}.AutoReloadOnCollectionChanged"/> and <see cref="MudTable{T}.AutoReloadOnItemPropertyChanged"/>:
/// opt-in re-rendering from INotifyCollectionChanged / INotifyPropertyChanged, parity with MudDataGrid (#11822).
/// </summary>
[TestFixture]
public class TableAutoReloadTests : BunitTest
{
private static int RowCount(IRenderedComponent<TableAutoReloadTest> comp) => comp.FindAll("tbody tr.mud-table-row").Count;

[Test]
public void Default_DoesNotSubscribe_AndDoesNotRerenderOnAdd()
{
var comp = Context.Render<TableAutoReloadTest>();
var source = comp.Instance.Source;
RowCount(comp).Should().Be(3);
source.SubscriberCount.Should().Be(0, "both flags default to false → upstream behaviour is unchanged");
source[0].SubscriberCount.Should().Be(0);

var renders = comp.RenderCount;
source.Add(new TableAutoReloadTest.Item("d"));
Thread.Sleep(50);
comp.RenderCount.Should().Be(renders);
RowCount(comp).Should().Be(3, "without opt-in the table only repaints when its parent re-renders");
}

[Test]
public void CollectionChanged_On_RerendersOnAddRemoveClear()
{
var comp = Context.Render<TableAutoReloadTest>(p => p.Add(x => x.AutoReloadOnCollectionChanged, true));
var source = comp.Instance.Source;
source.SubscriberCount.Should().Be(1);

source.Add(new TableAutoReloadTest.Item("d"));
comp.WaitForAssertion(() => RowCount(comp).Should().Be(4));

source.RemoveAt(0);
comp.WaitForAssertion(() => RowCount(comp).Should().Be(3));
comp.Markup.Should().NotContain(">a<");

source.Clear();
comp.WaitForAssertion(() => RowCount(comp).Should().Be(0));
}

[Test]
public void CollectionChanged_On_DoesNotObserveItemProperties()
{
var comp = Context.Render<TableAutoReloadTest>(p => p.Add(x => x.AutoReloadOnCollectionChanged, true));
var item = comp.Instance.Source[0];
item.SubscriberCount.Should().Be(0);

var renders = comp.RenderCount;
item.Name = "changed";
Thread.Sleep(50);
comp.RenderCount.Should().Be(renders);
}

[Test]
public void ItemPropertyChanged_On_RerendersOnPropertyChange()
{
var comp = Context.Render<TableAutoReloadTest>(p => p.Add(x => x.AutoReloadOnItemPropertyChanged, true));
var item = comp.Instance.Source[1];
item.SubscriberCount.Should().Be(1);

item.Name = "renamed";
comp.WaitForAssertion(() => comp.Markup.Should().Contain("renamed"));
}

[Test]
public void ItemPropertyChanged_On_ObservesOnlyRenderedRows_AndFollowsRowLifecycle()
{
var comp = Context.Render<TableAutoReloadTest>(p => p
.Add(x => x.AutoReloadOnCollectionChanged, true)
.Add(x => x.AutoReloadOnItemPropertyChanged, true));
var source = comp.Instance.Source;

// A row exists per item on the page → each is observed once.
source.Should().OnlyContain(i => i.SubscriberCount == 1);

// Added item: observed as soon as its row renders …
var added = new TableAutoReloadTest.Item("d");
source.Add(added);
comp.WaitForAssertion(() => added.SubscriberCount.Should().Be(1));
added.Name = "d2";
comp.WaitForAssertion(() => comp.Markup.Should().Contain("d2"));

// … removed item: its row is disposed and the subscription goes with it.
var removed = source[0];
source.RemoveAt(0);
comp.WaitForAssertion(() => removed.SubscriberCount.Should().Be(0));
RowCount(comp).Should().Be(3);
}

[Test]
public void ItemPropertyChanged_On_RerendersOnlyThatRow()
{
var comp = Context.Render<TableAutoReloadTest>(p => p.Add(x => x.AutoReloadOnItemPropertyChanged, true));
var rows = comp.FindComponents<MudTr>();
Thread.Sleep(200); // let the table finish its own post-render settling
var before = rows.Select(r => r.RenderCount).ToArray();

comp.Instance.Source[2].Name = "only-me";
comp.WaitForAssertion(() => comp.Markup.Should().Contain("only-me"));

// bUnit bumps RenderCount on every ancestor whose markup changed, so the table's count is not
// evidence either way; sibling rows are — they must not have been rendered again.
rows[0].RenderCount.Should().Be(before[0]);
rows[1].RenderCount.Should().Be(before[1]);
rows[2].RenderCount.Should().BeGreaterThan(before[2]);
}

[Test]
public void ItemPropertyChanged_Burst_IsCoalescedPerRow()
{
var comp = Context.Render<TableAutoReloadTest>(p => p.Add(x => x.AutoReloadOnItemPropertyChanged, true));
var item = comp.Instance.Source[0];
var renders = comp.RenderCount;

comp.InvokeAsync(() =>
{
for (var i = 0; i < 500; i++)
{
item.Name = $"n{i}";
}
});

comp.WaitForAssertion(() => comp.Markup.Should().Contain("n499"));
(comp.RenderCount - renders).Should().BeLessThan(10);
}

[Test]
public void Burst_IsCoalescedIntoFewRenders()
{
var comp = Context.Render<TableAutoReloadTest>(p => p.Add(x => x.AutoReloadOnCollectionChanged, true));
var source = comp.Instance.Source;
var renders = comp.RenderCount;

comp.InvokeAsync(() =>
{
for (var i = 0; i < 500; i++)
{
source.Add(new TableAutoReloadTest.Item($"i{i}"));
}
});

comp.WaitForAssertion(() => RowCount(comp).Should().Be(503));
(comp.RenderCount - renders).Should().BeLessThan(10, "500 notifications must not schedule 500 renders");
}

[Test]
public async Task Items_Swapped_MovesSubscriptionsToNewCollection()
{
var comp = Context.Render<TableAutoReloadTest>(p => p
.Add(x => x.AutoReloadOnCollectionChanged, true)
.Add(x => x.AutoReloadOnItemPropertyChanged, true));
var old = comp.Instance.Source;
old.SubscriberCount.Should().Be(1);
old[0].SubscriberCount.Should().Be(1);

var next = new TableAutoReloadTest.TrackedCollection { new TableAutoReloadTest.Item("x") };
await comp.SetParametersAndRenderAsync(p => p.Add(x => x.Source, next));

old.SubscriberCount.Should().Be(0);
old[0].SubscriberCount.Should().Be(0);
next.SubscriberCount.Should().Be(1);
next[0].SubscriberCount.Should().Be(1);
RowCount(comp).Should().Be(1);
}

[Test]
public async Task Flags_TurnedOff_Unsubscribe()
{
var comp = Context.Render<TableAutoReloadTest>(p => p
.Add(x => x.AutoReloadOnCollectionChanged, true)
.Add(x => x.AutoReloadOnItemPropertyChanged, true));
var source = comp.Instance.Source;

await comp.SetParametersAndRenderAsync(p => p
.Add(x => x.AutoReloadOnCollectionChanged, false)
.Add(x => x.AutoReloadOnItemPropertyChanged, false));

source.SubscriberCount.Should().Be(0);
source.Should().OnlyContain(i => i.SubscriberCount == 0);
}

[Test]
public async Task Dispose_Unsubscribes_AndLateNotificationsAreIgnored()
{
var comp = Context.Render<TableAutoReloadTest>(p => p
.Add(x => x.AutoReloadOnCollectionChanged, true)
.Add(x => x.AutoReloadOnItemPropertyChanged, true));
var source = comp.Instance.Source;
source.SubscriberCount.Should().Be(1);

await Context.DisposeComponentsAsync();

source.SubscriberCount.Should().Be(0);
source.Should().OnlyContain(i => i.SubscriberCount == 0);
var act = () => { source.Add(new TableAutoReloadTest.Item("late")); source[0].Name = "late"; };
act.Should().NotThrow();
}

[Test]
public void Clear_DisposesRows_AndReleasesItemSubscriptions()
{
var comp = Context.Render<TableAutoReloadTest>(p => p
.Add(x => x.AutoReloadOnCollectionChanged, true)
.Add(x => x.AutoReloadOnItemPropertyChanged, true));
var source = comp.Instance.Source;
var items = source.ToList();
items.Should().OnlyContain(i => i.SubscriberCount == 1);

source.Clear(); // Reset: no OldItems — the row lifecycle, not event args, releases the subscriptions

comp.WaitForAssertion(() => RowCount(comp).Should().Be(0));
comp.WaitForAssertion(() => items.Should().OnlyContain(i => i.SubscriberCount == 0));
}
}
}
Loading
Loading