diff --git a/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs b/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs
index 4138b507013b..56f438cee950 100644
--- a/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs
+++ b/src/MudBlazor/Components/Tooltip/MudTooltip.razor.cs
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Components;
using MudBlazor.State;
using MudBlazor.Utilities;
+using MudBlazor.Utilities.Debounce;
namespace MudBlazor
{
@@ -10,9 +11,16 @@ public partial class MudTooltip : MudComponentBase
private readonly ParameterState
_visibleState;
private Origin _anchorOrigin;
private Origin _transformOrigin;
-
+ internal DebounceDispatcher _showDebouncer;
+ internal DebounceDispatcher _hideDebouncer;
+ internal double _previousDelay;
+ internal double _previousDuration;
public MudTooltip()
{
+ _previousDelay = Delay;
+ _showDebouncer = new DebounceDispatcher(TimeSpan.FromMilliseconds(Delay));
+ _previousDuration = Duration;
+ _hideDebouncer = new DebounceDispatcher(TimeSpan.FromMilliseconds(Duration));
using var registerScope = CreateRegisterScope();
_visibleState = registerScope.RegisterParameter(nameof(Visible))
.WithParameter(() => Visible)
@@ -162,21 +170,50 @@ public MudTooltip()
///
/// Register and Show the Popover for the tooltip if it is not disabled, set to be visible, the content or Text is not empty or null
///
- private bool ShowToolTip()
+ internal bool ShowToolTip()
{
if (_anchorOrigin == Origin.TopLeft || _transformOrigin == Origin.TopLeft)
ConvertPlacement();
return !Disabled && _visibleState.Value && (TooltipContent is not null || !string.IsNullOrEmpty(Text));
}
- private Task HandlePointerEnterAsync()
+ protected override void OnParametersSet()
+ {
+ base.OnParametersSet();
+
+ if (Math.Abs(_previousDelay - Delay) > .001)
+ {
+ _showDebouncer = new DebounceDispatcher(TimeSpan.FromMilliseconds(Delay));
+ _previousDelay = Delay;
+ }
+
+ if (Math.Abs(_previousDuration - Duration) > .001)
+ {
+ _hideDebouncer = new DebounceDispatcher(TimeSpan.FromMilliseconds(Duration));
+ _previousDuration = Duration;
+ }
+ }
+
+ internal Task HandlePointerEnterAsync()
{
- return ShowOnHover ? _visibleState.SetValueAsync(true) : Task.CompletedTask;
+ if (!ShowOnHover)
+ {
+ return Task.CompletedTask;
+ }
+
+ _hideDebouncer.Cancel();
+ return _showDebouncer.DebounceAsync(() => _visibleState.SetValueAsync(true));
}
- private Task HandlePointerLeaveAsync()
+ internal Task HandlePointerLeaveAsync()
{
- return ShowOnHover ? _visibleState.SetValueAsync(false) : Task.CompletedTask;
+ if (!ShowOnHover)
+ {
+ return Task.CompletedTask;
+ }
+
+ _showDebouncer.Cancel();
+ return _hideDebouncer.DebounceAsync(() => _visibleState.SetValueAsync(false));
}
private Task HandleFocusInAsync()
diff --git a/src/MudBlazor/Components/TreeView/MudTreeViewItem.razor.cs b/src/MudBlazor/Components/TreeView/MudTreeViewItem.razor.cs
index 584c9f6607af..a90040c2dbf7 100644
--- a/src/MudBlazor/Components/TreeView/MudTreeViewItem.razor.cs
+++ b/src/MudBlazor/Components/TreeView/MudTreeViewItem.razor.cs
@@ -438,8 +438,10 @@ private async Task OnItemClickedAsync(MouseEventArgs ev)
}
if (!GetReadOnly())
{
- Debug.Assert(MudTreeRoot != null);
- await MudTreeRoot.OnItemClickAsync(this);
+ if (MudTreeRoot is not null)
+ {
+ await MudTreeRoot.OnItemClickAsync(this);
+ }
}
await OnClick.InvokeAsync(ev);
}
@@ -457,8 +459,10 @@ private async Task OnItemDoubleClickedAsync(MouseEventArgs ev)
}
if (!GetReadOnly())
{
- Debug.Assert(MudTreeRoot != null);
- await MudTreeRoot.OnItemClickAsync(this);
+ if (MudTreeRoot is not null)
+ {
+ await MudTreeRoot.OnItemClickAsync(this);
+ }
}
await OnDoubleClick.InvokeAsync(ev);
}
diff --git a/src/MudBlazor/Extensions/ElementReferenceExtensions.cs b/src/MudBlazor/Extensions/ElementReferenceExtensions.cs
index ecffbbfc9d49..0327ca85b78f 100644
--- a/src/MudBlazor/Extensions/ElementReferenceExtensions.cs
+++ b/src/MudBlazor/Extensions/ElementReferenceExtensions.cs
@@ -1,7 +1,5 @@
-using System;
-using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
-using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor.Interop;
@@ -71,5 +69,10 @@ public static ValueTask RemoveDefaultPreventingHandlers(this ElementReference el
this ElementReference elementReference,
DotNetObjectReference obj) where T : class =>
elementReference.GetJSRuntime()?.InvokeVoidAsync("mudElementRef.addOnBlurEvent", elementReference, obj) ?? ValueTask.CompletedTask;
+
+ public static ValueTask MudDetachBlurEventWithJS<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>(
+ this ElementReference elementReference,
+ DotNetObjectReference obj) where T : class =>
+ elementReference.GetJSRuntime()?.InvokeVoidAsync("mudElementRef.removeOnBlurEvent", elementReference, obj) ?? ValueTask.CompletedTask;
}
}
diff --git a/src/MudBlazor/Extensions/ResizeOptionsExtensions.cs b/src/MudBlazor/Extensions/ResizeOptionsExtensions.cs
index cc27300c4f1f..3f4219d7d78b 100644
--- a/src/MudBlazor/Extensions/ResizeOptionsExtensions.cs
+++ b/src/MudBlazor/Extensions/ResizeOptionsExtensions.cs
@@ -2,9 +2,6 @@
// MudBlazor licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using System;
-using System.Collections.Generic;
-using System.Linq;
using MudBlazor.Services;
namespace MudBlazor.Extensions;
diff --git a/src/MudBlazor/GlobalUsings.cs b/src/MudBlazor/GlobalUsings.cs
new file mode 100644
index 000000000000..e422b2651961
--- /dev/null
+++ b/src/MudBlazor/GlobalUsings.cs
@@ -0,0 +1 @@
+global using static MudBlazor.Utilities.StringHelpers;
diff --git a/src/MudBlazor/Interop/ElementSize.cs b/src/MudBlazor/Interop/ElementSize.cs
index 393c93e3db44..6417f40ecef7 100644
--- a/src/MudBlazor/Interop/ElementSize.cs
+++ b/src/MudBlazor/Interop/ElementSize.cs
@@ -14,4 +14,9 @@ public class ElementSize
/// The width of the Element.
///
public required double Width { get; init; }
+
+ ///
+ /// The timestamp of the size change, used to help filter out of order events for server mode.
+ ///
+ public long Timestamp { get; set; }
}
diff --git a/src/MudBlazor/Services/Browser/BrowserViewportService.cs b/src/MudBlazor/Services/Browser/BrowserViewportService.cs
index cbb4c1510d90..16ff3effeb1b 100644
--- a/src/MudBlazor/Services/Browser/BrowserViewportService.cs
+++ b/src/MudBlazor/Services/Browser/BrowserViewportService.cs
@@ -101,22 +101,16 @@ public async Task SubscribeAsync(IBrowserViewportObserver observer, bool fireImm
optionsClone.BreakpointDefinitions = BreakpointGlobalOptions.GetDefaultOrUserDefinedBreakpointDefinition(optionsClone, ResizeOptions);
var subscription = await CreateJavaScriptListener(optionsClone, observer.Id);
- if (_observerManager.IsSubscribed(subscription))
- {
- // Only re-subscribe
- _observerManager.Subscribe(subscription, observer);
- }
- else
+
+ if (!_observerManager.TryGetOrAddSubscription(subscription, observer, out var newObserver))
{
- // Subscribe and fire if necessary
- _observerManager.Subscribe(subscription, observer);
if (fireImmediately)
{
// Not waiting for Browser Size to change and RaiseOnResized to fire and post event with current breakpoint and browser window size
var latestWindowSize = await GetCurrentBrowserWindowSizeAsync();
var latestBreakpoint = await GetCurrentBreakpointAsync();
// Notify only current subscription
- await observer.NotifyBrowserViewportChangeAsync(new BrowserViewportEventArgs(subscription.JavaScriptListenerId, latestWindowSize, latestBreakpoint, isImmediate: true));
+ await newObserver.NotifyBrowserViewportChangeAsync(new BrowserViewportEventArgs(subscription.JavaScriptListenerId, latestWindowSize, latestBreakpoint, isImmediate: true));
}
}
}
diff --git a/src/MudBlazor/Services/KeyInterceptor/KeyInterceptorService.cs b/src/MudBlazor/Services/KeyInterceptor/KeyInterceptorService.cs
index 0308e12b124e..7eecf6389578 100644
--- a/src/MudBlazor/Services/KeyInterceptor/KeyInterceptorService.cs
+++ b/src/MudBlazor/Services/KeyInterceptor/KeyInterceptorService.cs
@@ -54,17 +54,9 @@ public async Task SubscribeAsync(IKeyInterceptorObserver observer, KeyIntercepto
return;
}
- if (!_observerManager.IsSubscribed(observer.ElementId))
+ if (!_observerManager.TryGetOrAddSubscription(observer.ElementId, observer, out var newObserver))
{
- var isConnected = await _keyInterceptorInterop.Connect(_dotNetReferenceLazy.Value, observer.ElementId, options);
- if (isConnected)
- {
- _observerManager.Subscribe(observer.ElementId, observer);
- }
- }
- else
- {
- _observerManager.Subscribe(observer.ElementId, observer);
+ _ = await _keyInterceptorInterop.Connect(_dotNetReferenceLazy.Value, newObserver.ElementId, options);
}
}
diff --git a/src/MudBlazor/Styles/components/_input.scss b/src/MudBlazor/Styles/components/_input.scss
index df260c064e05..b795a92f12dd 100644
--- a/src/MudBlazor/Styles/components/_input.scss
+++ b/src/MudBlazor/Styles/components/_input.scss
@@ -370,7 +370,7 @@
}
&.mud-input-root-margin-dense {
- margin-top: 3px;
+ padding-top: 3px;
}
&.mud-input-root-type-search {
@@ -429,15 +429,15 @@
}
&.mud-input-root-adorned-start {
- margin-left: 0;
- margin-inline-start: 0;
- margin-inline-end: 12px;
+ padding-left: 0;
+ padding-inline-start: 0;
+ padding-inline-end: 12px;
}
&.mud-input-root-adorned-end {
- margin-right: 0;
- margin-inline-end: unset;
- margin-inline-start: 12px;
+ padding-right: 0;
+ padding-inline-end: unset;
+ padding-inline-start: 12px;
}
}
@@ -480,15 +480,15 @@
}
&.mud-input-root-adorned-start {
- margin-left: 0;
- margin-inline-start: 0;
- margin-inline-end: 14px;
+ padding-left: 0;
+ padding-inline-start: 0;
+ padding-inline-end: 14px;
}
&.mud-input-root-adorned-end {
- margin-right: 0;
- margin-inline-end: 0;
- margin-inline-start: 14px;
+ padding-right: 0;
+ padding-inline-end: 0;
+ padding-inline-start: 14px;
}
}
}
diff --git a/src/MudBlazor/Styles/components/_popover.scss b/src/MudBlazor/Styles/components/_popover.scss
index 8ddd8aa6b43e..8294cff79cea 100644
--- a/src/MudBlazor/Styles/components/_popover.scss
+++ b/src/MudBlazor/Styles/components/_popover.scss
@@ -23,6 +23,10 @@
transition-delay: 0ms !important;
}
+ &:empty {
+ box-shadow: none !important;
+ }
+
.mud-list {
max-height: inherit;
overflow-y: auto;
@@ -60,7 +64,7 @@
}
.mud-select {
- .mud-popover-cascading-value {
+ .mud-popover-cascading-value {
z-index: calc(var(--mud-zindex-select) + 5);
}
}
diff --git a/src/MudBlazor/Styles/components/_select.scss b/src/MudBlazor/Styles/components/_select.scss
index 9cb64bebc7eb..cd5f8b064281 100644
--- a/src/MudBlazor/Styles/components/_select.scss
+++ b/src/MudBlazor/Styles/components/_select.scss
@@ -97,3 +97,12 @@
border-bottom: 1px solid lightgray;
padding-bottom: 18px;
}
+
+.mud-select-filler {
+ white-space: nowrap;
+ height: 0px;
+}
+
+.mud-width-content {
+ max-width: min-content;
+}
diff --git a/src/MudBlazor/Styles/components/_switch.scss b/src/MudBlazor/Styles/components/_switch.scss
index 0f9238b52f9d..677224940383 100644
--- a/src/MudBlazor/Styles/components/_switch.scss
+++ b/src/MudBlazor/Styles/components/_switch.scss
@@ -42,7 +42,7 @@
z-index: -1;
transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,background-color 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
border-radius: 9px;
- background-color: var(--mud-palette-black);
+ background-color: var(--mud-palette-action-default);
}
}
diff --git a/src/MudBlazor/Styles/components/_table.scss b/src/MudBlazor/Styles/components/_table.scss
index 1ba5f58d0704..24613149afb3 100644
--- a/src/MudBlazor/Styles/components/_table.scss
+++ b/src/MudBlazor/Styles/components/_table.scss
@@ -31,18 +31,6 @@
color: var(--mud-palette-text-primary);
font-weight: 500;
line-height: 1.5rem;
-
- & .mud-button-root {
- @media(hover: hover) and (pointer: fine) {
- &:hover {
- color: var(--mud-palette-action-default);
-
- .mud-table-sort-label-icon {
- opacity: 0.8;
- }
- }
- }
- }
}
}
@@ -65,15 +53,30 @@
}
.mud-table-sort-label {
- user-select: auto;
display: inline-flex;
align-items: center;
flex-direction: inherit;
justify-content: flex-start;
+ &.mud-clickable {
+ cursor: pointer;
+ user-select: none;
+ transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+
+ @media(hover: hover) and (pointer: fine) {
+ &:hover {
+ opacity: 0.64;
+
+ .mud-table-sort-label-icon {
+ opacity: 1;
+ }
+ }
+ }
+ }
+
.mud-table-sort-label-icon {
font-size: 18px;
- transition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,transform 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+ transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,transform 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
margin-left: 4px;
user-select: none;
margin-right: 4px;
@@ -101,7 +104,6 @@
}
-
.mud-table-cell {
display: table-cell;
padding: 16px;
diff --git a/src/MudBlazor/TScripts/mudElementReference.js b/src/MudBlazor/TScripts/mudElementReference.js
index 478b116321b3..a810cde4813e 100644
--- a/src/MudBlazor/TScripts/mudElementReference.js
+++ b/src/MudBlazor/TScripts/mudElementReference.js
@@ -150,7 +150,8 @@ class MudElementReference {
// ios doesn't trigger Blazor/React/Other dom style blur event so add a base event listener here
// that will trigger with IOS Done button and regular blur events
addOnBlurEvent(element, dotNetReference) {
- function onFocusOut(e) {
+ element._mudBlurHandler = function (e) {
+ if (!element) return;
e.preventDefault();
element.blur();
if (dotNetReference) {
@@ -160,7 +161,13 @@ class MudElementReference {
console.error("No dotNetReference found for iosKeyboardFocus");
}
}
- element.addEventListener('blur', onFocusOut);
+ element.addEventListener('blur', element._mudBlurHandler);
+ }
+ // dispose event
+ removeOnBlurEvent(element, dotnetRef) {
+ if (!element || !element._mudBlurHandler) return;
+ element.removeEventListener('blur', element._mudBlurHandler);
+ delete element._mudBlurHandler;
}
};
window.mudElementRef = new MudElementReference();
diff --git a/src/MudBlazor/TScripts/mudHelpers.js b/src/MudBlazor/TScripts/mudHelpers.js
index f589e7d16579..e997a62732a8 100644
--- a/src/MudBlazor/TScripts/mudHelpers.js
+++ b/src/MudBlazor/TScripts/mudHelpers.js
@@ -88,6 +88,8 @@ window.serializeParameter = (data, spec) => {
// mudGetSvgBBox is a helper function to get the size of an svgElement
window.mudGetSvgBBox = (svgElement) => {
+ if (svgElement == null) return null;
+
const bbox = svgElement.getBBox();
return {
x: bbox.x,
@@ -108,13 +110,13 @@ window.mudObserveElementSize = (dotNetReference, element, functionName = 'OnElem
// Throttled notification function.
const throttledNotify = (width, height) => {
- const now = Date.now();
- const timeSinceLast = now - lastNotifiedTime;
+ const timestamp = Date.now();
+ const timeSinceLast = timestamp - lastNotifiedTime;
if (timeSinceLast >= debounceMillis) {
// Enough time has passed, notify immediately.
- lastNotifiedTime = now;
+ lastNotifiedTime = timestamp;
try {
- dotNetReference.invokeMethodAsync(functionName, { width, height });
+ dotNetReference.invokeMethodAsync(functionName, { width, height, timestamp });
}
catch (error) {
this.logger("[MudBlazor] Error in mudObserveElementSize:", { error });
@@ -128,7 +130,7 @@ window.mudObserveElementSize = (dotNetReference, element, functionName = 'OnElem
lastNotifiedTime = Date.now();
scheduledCall = null;
try {
- dotNetReference.invokeMethodAsync(functionName, { width, height });
+ dotNetReference.invokeMethodAsync(functionName, { width, height, timestamp });
}
catch (error) {
this.logger("[MudBlazor] Error in mudObserveElementSize:", { error });
@@ -139,6 +141,8 @@ window.mudObserveElementSize = (dotNetReference, element, functionName = 'OnElem
// Create the ResizeObserver to notify on size changes.
const resizeObserver = new ResizeObserver(entries => {
+ if (element.isConnected === false) { return; } // Element is no longer in the DOM.
+
// Use the last entry's contentRect (or element's client dimensions).
let width = element.clientWidth;
let height = element.clientHeight;
diff --git a/src/MudBlazor/TScripts/mudPopover.js b/src/MudBlazor/TScripts/mudPopover.js
index 249450c9625f..318c46e43e0c 100644
--- a/src/MudBlazor/TScripts/mudPopover.js
+++ b/src/MudBlazor/TScripts/mudPopover.js
@@ -427,6 +427,10 @@ window.mudpopoverHelper = {
},
updatePopoverOverlay: function (popoverContentNode) {
+ // tooltips don't have an overlay
+ if (popoverContentNode.classList.contains("mud-tooltip")) {
+ return;
+ }
// set any associated overlay to equal z-index
const provider = popoverContentNode.closest('.mud-popover-provider');
if (provider && popoverContentNode.classList.contains("mud-popover")) {
@@ -635,11 +639,10 @@ class MudPopover {
observer.observe(popoverContentNode, config);
- // Optimize resize observer
- const throttledResize = window.mudpopoverHelper.rafThrottle(entries => {
+ const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
const target = entry.target;
- for (let childNode of target.childNodes) {
+ for (const childNode of target.childNodes) {
if (childNode.id && childNode.id.startsWith('popover-')) {
window.mudpopoverHelper.placePopover(childNode);
}
@@ -647,16 +650,16 @@ class MudPopover {
}
});
- const resizeObserver = new ResizeObserver(throttledResize);
resizeObserver.observe(popoverNode.parentNode);
- const throttledContent = window.mudpopoverHelper.rafThrottle(entries => {
+ const contentNodeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
- window.mudpopoverHelper.placePopoverByNode(entry.target);
+ const target = entry.target;
+ if (target)
+ window.mudpopoverHelper.placePopoverByNode(target);
}
});
- const contentNodeObserver = new ResizeObserver(throttledContent);
contentNodeObserver.observe(popoverContentNode);
this.map[id] = {
diff --git a/src/MudBlazor/Utilities/Debounce/DebounceDispatcher.cs b/src/MudBlazor/Utilities/Debounce/DebounceDispatcher.cs
index 7ca3b97cefe1..827bda1c8fd5 100644
--- a/src/MudBlazor/Utilities/Debounce/DebounceDispatcher.cs
+++ b/src/MudBlazor/Utilities/Debounce/DebounceDispatcher.cs
@@ -2,10 +2,6 @@
// MudBlazor licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-
namespace MudBlazor.Utilities.Debounce;
#nullable enable
@@ -15,7 +11,7 @@ namespace MudBlazor.Utilities.Debounce;
///
internal class DebounceDispatcher
{
- private readonly int _interval;
+ private readonly TimeSpan _interval;
private CancellationTokenSource? _cancellationTokenSource;
///
@@ -23,6 +19,15 @@ internal class DebounceDispatcher
///
/// The minimum interval in milliseconds between invocations of the debounced function.
public DebounceDispatcher(int interval)
+ : this(TimeSpan.FromMilliseconds(interval))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with the specified interval.
+ ///
+ /// The minimum interval as a between invocations of the debounced function.
+ public DebounceDispatcher(TimeSpan interval)
{
_interval = interval;
}
@@ -39,7 +44,7 @@ public DebounceDispatcher(int interval)
/// A Task representing the asynchronous operation with minimal delay.
public async Task DebounceAsync(Func action, CancellationToken cancellationToken = default)
{
- // ReSharper disable MethodHasAsyncOverload (not available in .net7)
+ // ReSharper disable MethodHasAsyncOverload (we should not use it as it behaves differently)
// Cancel the previous debounce task if it exists
_cancellationTokenSource?.Cancel();
// ReSharper restore MethodHasAsyncOverload
@@ -59,4 +64,9 @@ public async Task DebounceAsync(Func action, CancellationToken cancellatio
// If the task was canceled, ignore it
}
}
+
+ ///
+ /// Cancel the current debounced task.
+ ///
+ public void Cancel() => _cancellationTokenSource?.Cancel();
}
diff --git a/src/MudBlazor/Utilities/ObserverManager/ObserverManager.cs b/src/MudBlazor/Utilities/ObserverManager/ObserverManager.cs
index ed76188cd1b2..501c2a1aa1ce 100644
--- a/src/MudBlazor/Utilities/ObserverManager/ObserverManager.cs
+++ b/src/MudBlazor/Utilities/ObserverManager/ObserverManager.cs
@@ -25,15 +25,23 @@ namespace MudBlazor.Utilities.ObserverManager;
///
internal class ObserverManager : IEnumerable where TIdentity : notnull
{
- private readonly ConcurrentDictionary _observers = new();
+ private readonly ConcurrentDictionary _observers;
private readonly ILogger _log;
///
/// Initializes a new instance of the class.
///
- public ObserverManager(ILogger log)
+ public ObserverManager(ILogger log) : this(log, null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ObserverManager(ILogger log, IEqualityComparer? comparer)
{
_log = log ?? throw new ArgumentNullException(nameof(log));
+ _observers = new(comparer);
}
///
@@ -84,16 +92,13 @@ public IEnumerable FindObserverIdentities(Func predicate(kvp.Key, kvp.Value.Observer)).Select(kvp => kvp.Key);
///
- /// Ensures that the provided is subscribed, renewing its subscription.
+ /// Tries to get the existing subscription for the specified identity, or subscribes the observer if it does not exist.
///
- ///
- /// The observer's identity.
- ///
- ///
- /// The observer.
- ///
- /// A delegate callback throws an exception.
- public void Subscribe(TIdentity id, TObserver observer)
+ /// The identity of the observer.
+ /// The observer to subscribe if it does not already exist.
+ /// When this method returns, contains the observer associated with the specified identity, whether it was already subscribed or newly subscribed.
+ /// True if the observer was already subscribed; otherwise, false.
+ public bool TryGetOrAddSubscription(TIdentity id, TObserver observer, out TObserver newObserver)
{
// Add or update the subscription.
if (_observers.TryGetValue(id, out var entry))
@@ -103,15 +108,35 @@ public void Subscribe(TIdentity id, TObserver observer)
{
_log.LogTrace("Updating entry for {Id}/{Observer}. {Count} total observers.", id, observer, _observers.Count);
}
+
+ newObserver = entry.Observer;
+ return true;
}
- else
+
+ var newEntry = new ObserverEntry(observer);
+ _observers[id] = newEntry;
+ if (_log.IsEnabled(LogLevel.Trace))
{
- _observers[id] = new ObserverEntry(observer);
- if (_log.IsEnabled(LogLevel.Trace))
- {
- _log.LogTrace("Adding entry for {Id}/{Observer}. {Count} total observers after add.", id, observer, _observers.Count);
- }
+ _log.LogTrace("Adding entry for {Id}/{Observer}. {Count} total observers after add.", id, observer, _observers.Count);
}
+
+ newObserver = newEntry.Observer;
+ return false;
+ }
+
+ ///
+ /// Ensures that the provided is subscribed, renewing its subscription.
+ ///
+ ///
+ /// The observer's identity.
+ ///
+ ///
+ /// The observer.
+ ///
+ /// A delegate callback throws an exception.
+ public void Subscribe(TIdentity id, TObserver observer)
+ {
+ _ = TryGetOrAddSubscription(id, observer, out _);
}
///
diff --git a/src/MudBlazor/Utilities/StringHelpers.cs b/src/MudBlazor/Utilities/StringHelpers.cs
new file mode 100644
index 000000000000..2d8451ee405c
--- /dev/null
+++ b/src/MudBlazor/Utilities/StringHelpers.cs
@@ -0,0 +1,20 @@
+using System.Globalization;
+
+namespace MudBlazor.Utilities;
+
+#nullable enable
+internal static class StringHelpers
+{
+ ///
+ /// Converts a double value to its string representation, rounded to 4 decimal places.
+ ///
+ /// The double value to convert.
+ /// An optional format string.
+ /// The string representation of the double value.
+ public static string ToS(double value, string? format = null)
+ {
+ return string.IsNullOrEmpty(format)
+ ? Math.Round(value, 4).ToString(CultureInfo.InvariantCulture)
+ : Math.Round(value, 4).ToString(format);
+ }
+}