diff --git a/.github/workflows/publish-me-nuget.yml b/.github/workflows/publish-me-nuget.yml
new file mode 100644
index 000000000000..180548f8f76f
--- /dev/null
+++ b/.github/workflows/publish-me-nuget.yml
@@ -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
diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/DateRangePickerMinMaxDaysTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/DateRangePickerMinMaxDaysTest.razor
index e9c069e962d1..018626c8db4b 100644
--- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/DateRangePickerMinMaxDaysTest.razor
+++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DatePicker/DateRangePickerMinMaxDaysTest.razor
@@ -9,7 +9,7 @@
AdditionalDateClassesFunc="@((DateTime dt)=>((int)dt.DayOfWeek == 0 ? "red-text text-accent-4" : ""))" />
- _picker.RecalculateValidDays())" Color="Color.Primary">Allow Weekends
+ _picker?.RecalculateValidDays())" Color="Color.Primary">Allow Weekends
Include Disabled
diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Table/TableAutoReloadTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Table/TableAutoReloadTest.razor
new file mode 100644
index 000000000000..073f0c878aec
--- /dev/null
+++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Table/TableAutoReloadTest.razor
@@ -0,0 +1,64 @@
+@using System.Collections.Specialized
+@using System.ComponentModel
+
+
+
+ Name
+
+
+ @context.Name
+
+
+
+@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") };
+
+ /// An item that counts its PropertyChanged subscribers so tests can prove unsubscription.
+ 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--; }
+ }
+ }
+
+ /// An ObservableCollection that counts its CollectionChanged subscribers.
+ public sealed class TrackedCollection : System.Collections.ObjectModel.ObservableCollection- , 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);
+ }
+ }
+}
diff --git a/src/MudBlazor.UnitTests/Components/TableAutoReloadTests.cs b/src/MudBlazor.UnitTests/Components/TableAutoReloadTests.cs
new file mode 100644
index 000000000000..b1a02f8b626d
--- /dev/null
+++ b/src/MudBlazor.UnitTests/Components/TableAutoReloadTests.cs
@@ -0,0 +1,225 @@
+using AwesomeAssertions;
+using Bunit;
+using MudBlazor.UnitTests.TestComponents.Table;
+using NUnit.Framework;
+
+namespace MudBlazor.UnitTests.Components
+{
+ ///
+ /// and :
+ /// opt-in re-rendering from INotifyCollectionChanged / INotifyPropertyChanged, parity with MudDataGrid (#11822).
+ ///
+ [TestFixture]
+ public class TableAutoReloadTests : BunitTest
+ {
+ private static int RowCount(IRenderedComponent comp) => comp.FindAll("tbody tr.mud-table-row").Count;
+
+ [Test]
+ public void Default_DoesNotSubscribe_AndDoesNotRerenderOnAdd()
+ {
+ var comp = Context.Render();
+ 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(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(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(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(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(p => p.Add(x => x.AutoReloadOnItemPropertyChanged, true));
+ var rows = comp.FindComponents();
+ 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(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(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(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(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(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(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));
+ }
+ }
+}
diff --git a/src/MudBlazor/Components/Table/MudTable.razor b/src/MudBlazor/Components/Table/MudTable.razor
index 8c288e1af043..4eda4370d2a7 100644
--- a/src/MudBlazor/Components/Table/MudTable.razor
+++ b/src/MudBlazor/Components/Table/MudTable.razor
@@ -101,6 +101,7 @@
if (CurrentPageItems != null && CurrentPageItems.Any())
{
+ @ObservedRow(item, @
@{
var itemIndex = FilteredItems.ToList().IndexOf(item);
var generatedRowId = itemIndex != -1 ? $"{_tableId}_row_{itemIndex}" : null;
@@ -110,7 +111,7 @@
.Build();
var rowStyle = new StyleBuilder().AddStyle(RowStyle).AddStyle(RowStyleFunc?.Invoke(item, itemIndex)).Build();
}
-
@@ -144,6 +145,7 @@
{
@ChildRowContent(item)
}
+ )
}
}
@@ -216,7 +218,9 @@
;
- RenderFragment child() => item =>
+ RenderFragment child() => item => ObservedRow(item, rowContent()(item));
+
+ RenderFragment rowContent() => item =>
@
@{
var itemIndexForId = FilteredItems.ToList().IndexOf(item);
@@ -225,7 +229,7 @@
var rowClass = new CssBuilder(RowClass).AddClass(RowClassFunc?.Invoke(item, itemIndexForId)).AddClass(customClass, !string.IsNullOrEmpty(customClass)).AddClass("mud-table-row-clickable", OnRowClick.HasDelegate && !IsRowDisabled(item)).Build();
var rowStyle = new StyleBuilder().AddStyle(RowStyle).AddStyle(RowStyleFunc?.Invoke(item, itemIndexForId)).Build();
}
-
@if (!ReadOnly && Editable && Equals(_editingItem, item))
diff --git a/src/MudBlazor/Components/Table/MudTable.razor.cs b/src/MudBlazor/Components/Table/MudTable.razor.cs
index cc385ef4ccb4..d9b71a17a719 100644
--- a/src/MudBlazor/Components/Table/MudTable.razor.cs
+++ b/src/MudBlazor/Components/Table/MudTable.razor.cs
@@ -1,4 +1,6 @@
-using System.Diagnostics.CodeAnalysis;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
@@ -30,6 +32,9 @@ public partial class MudTable<[DynamicallyAccessedMembers(DynamicallyAccessedMem
private bool _currentRenderFilteredItemsCached;
private CancellationTokenSource? _cancellationTokenSrc;
private TableData _serverData = new() { TotalItems = 0, Items = [] };
+ private INotifyCollectionChanged? _observedCollection;
+ private int _autoReloadPending;
+ private bool _disposed;
[MemberNotNullWhen(true, nameof(_preEditSort))]
private bool HasPreEditSort => _preEditSort is not null;
@@ -240,6 +245,33 @@ public IEnumerable? Items
}
}
+ ///
+ /// Re-renders this table when raises .
+ ///
+ ///
+ /// Defaults to false. When true and implements
+ /// (such as ObservableCollection<T>), adding, removing or replacing items refreshes the table without
+ /// reassigning or calling StateHasChanged. Bursts of changes are coalesced into one render.
+ /// The event may be raised on any thread; the render is dispatched to the component's synchronization context.
+ /// Has no effect when is set.
+ ///
+ [Parameter]
+ [Category(CategoryTypes.Table.Behavior)]
+ public bool AutoReloadOnCollectionChanged { get; set; }
+
+ ///
+ /// Re-renders this table when an item in raises .
+ ///
+ ///
+ /// Defaults to false. When true, each rendered row observes its own item for as long as the row exists
+ /// (rows are keyed on the item), and a change re-renders that row only. Items not currently rendered — other pages,
+ /// outside the virtualized window — are not observed, so cost is bounded by visible rows, not by the collection.
+ /// Bursts of changes are coalesced into one render per row.
+ ///
+ [Parameter]
+ [Category(CategoryTypes.Table.Behavior)]
+ public bool AutoReloadOnItemPropertyChanged { get; set; }
+
///
/// The function which determines whether an item should be displayed.
///
@@ -918,6 +950,70 @@ private string ClearFilterCache()
return "";
}
+ ///
+ protected override void OnParametersSet()
+ {
+ base.OnParametersSet();
+ UpdateAutoReloadSubscriptions();
+ }
+
+ //AUTO RELOAD (INotifyCollectionChanged; per-item INotifyPropertyChanged lives in MudTableObservedRow):
+
+ ///
+ /// Wraps one row in a scope keyed on the item. Rendered from code because
+ /// the component is internal and Razor tag syntax only discovers public component types.
+ ///
+ private RenderFragment ObservedRow(T item, RenderFragment content) => builder =>
+ {
+ builder.OpenComponent>(0);
+ builder.SetKey(item);
+ builder.AddComponentParameter(1, nameof(MudTableObservedRow.Item), item);
+ builder.AddComponentParameter(2, nameof(MudTableObservedRow.Enabled), AutoReloadOnItemPropertyChanged);
+ builder.AddComponentParameter(3, nameof(MudTableObservedRow.ChildContent), content);
+ builder.CloseComponent();
+ };
+
+ private void UpdateAutoReloadSubscriptions()
+ {
+ var wanted = !HasServerData && AutoReloadOnCollectionChanged ? _items as INotifyCollectionChanged : null;
+ if (ReferenceEquals(_observedCollection, wanted))
+ {
+ return;
+ }
+
+ if (_observedCollection is not null)
+ {
+ _observedCollection.CollectionChanged -= OnObservedCollectionChanged;
+ }
+
+ _observedCollection = wanted;
+
+ if (wanted is not null)
+ {
+ wanted.CollectionChanged += OnObservedCollectionChanged;
+ }
+ }
+
+ private void OnObservedCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ // May arrive on any thread. Render on the next dispatcher turn, once for any number of notifications
+ // that arrive before it — deferring past the current turn is what coalesces a synchronous burst.
+ if (_disposed || Interlocked.CompareExchange(ref _autoReloadPending, 1, 0) != 0)
+ {
+ return;
+ }
+
+ InvokeAsync(async () =>
+ {
+ await Task.Yield();
+ Interlocked.Exchange(ref _autoReloadPending, 0);
+ if (!_disposed)
+ {
+ StateHasChanged();
+ }
+ });
+ }
+
///
/// Releases resources used by this table.
///
@@ -929,6 +1025,13 @@ public void Dispose()
protected virtual void Dispose(bool disposing)
{
+ _disposed = true;
+ if (_observedCollection is not null)
+ {
+ _observedCollection.CollectionChanged -= OnObservedCollectionChanged;
+ _observedCollection = null;
+ }
+
try
{
_cancellationTokenSrc?.Cancel();
diff --git a/src/MudBlazor/Components/Table/MudTableObservedRow.cs b/src/MudBlazor/Components/Table/MudTableObservedRow.cs
new file mode 100644
index 000000000000..1e8b511a6eef
--- /dev/null
+++ b/src/MudBlazor/Components/Table/MudTableObservedRow.cs
@@ -0,0 +1,87 @@
+using System.ComponentModel;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Rendering;
+
+namespace MudBlazor;
+
+///
+/// Internal per-row scope for : observes one item's
+/// for exactly as long as the row exists, and re-renders
+/// only this row when it fires. Blazor's row lifecycle (keyed on the item) is the bookkeeping: a row that
+/// leaves the page, the virtualized window, or the table is disposed and its subscription goes with it — so
+/// there is no per-table set of subscribed items and nothing to reconcile on Reset.
+///
+/// The item type.
+internal sealed class MudTableObservedRow : ComponentBase, IDisposable
+{
+ private INotifyPropertyChanged? _observed;
+ private int _renderPending;
+ private bool _disposed;
+
+ /// The row's item.
+ [Parameter]
+ public T Item { get; set; } = default!;
+
+ /// When false, nothing is observed and the row renders exactly as without this scope.
+ [Parameter]
+ public bool Enabled { get; set; }
+
+ /// The row content.
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ ///
+ protected override void OnParametersSet()
+ {
+ var wanted = Enabled ? Item as INotifyPropertyChanged : null;
+ if (ReferenceEquals(_observed, wanted))
+ {
+ return;
+ }
+
+ if (_observed is not null)
+ {
+ _observed.PropertyChanged -= OnItemPropertyChanged;
+ }
+
+ _observed = wanted;
+
+ if (wanted is not null)
+ {
+ wanted.PropertyChanged += OnItemPropertyChanged;
+ }
+ }
+
+ ///
+ protected override void BuildRenderTree(RenderTreeBuilder builder) => builder.AddContent(0, ChildContent);
+
+ private void OnItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ // May arrive on any thread; render on the next dispatcher turn, once per burst.
+ if (_disposed || Interlocked.CompareExchange(ref _renderPending, 1, 0) != 0)
+ {
+ return;
+ }
+
+ _ = InvokeAsync(async () =>
+ {
+ await Task.Yield();
+ Interlocked.Exchange(ref _renderPending, 0);
+ if (!_disposed)
+ {
+ StateHasChanged();
+ }
+ });
+ }
+
+ ///
+ public void Dispose()
+ {
+ _disposed = true;
+ if (_observed is not null)
+ {
+ _observed.PropertyChanged -= OnItemPropertyChanged;
+ _observed = null;
+ }
+ }
+}
diff --git a/src/MudBlazor/MudBlazor.csproj b/src/MudBlazor/MudBlazor.csproj
index 9c850c615ef4..ddf9520afbc2 100644
--- a/src/MudBlazor/MudBlazor.csproj
+++ b/src/MudBlazor/MudBlazor.csproj
@@ -6,6 +6,15 @@
true
+
+
+ ModelingEvolution.MudBlazor
+ MudBlazor
+ MudBlazor
+ _content/MudBlazor
+
MIT
Nuget.png
@@ -13,10 +22,10 @@
Garderoben, Henon and Contributors
Copyright 2026 MudBlazor
Blazor, MudBlazor, Material, Material Design, Components, Blazor Components, Blazor Library
- Blazor Component Library based on Material Design principles with an emphasis on ease of use and extensibility
+ ModelingEvolution fork of MudBlazor (drop-in, same namespaces and asset paths). Adds MudTable.AutoReloadOnCollectionChanged / AutoReloadOnItemPropertyChanged. Upstream: https://github.com/MudBlazor/MudBlazor
https://mudblazor.com/
README.md
- https://github.com/MudBlazor/MudBlazor
+ https://github.com/modelingevolution/MudBlazor
git
@@ -44,6 +53,7 @@
+
diff --git a/src/MudBlazor/buildTransitive/ModelingEvolution.MudBlazor.targets b/src/MudBlazor/buildTransitive/ModelingEvolution.MudBlazor.targets
new file mode 100644
index 000000000000..c371ee11ff12
--- /dev/null
+++ b/src/MudBlazor/buildTransitive/ModelingEvolution.MudBlazor.targets
@@ -0,0 +1,41 @@
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+