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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ MudBlazor.Extensions/wwwroot/**/*.min.js
# Generated files
MudBlazor.Extensions/version.generated.props
MudBlazor.Extensions/*Undefined*/
/.claude
125 changes: 125 additions & 0 deletions BREAKING_CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Breaking Changes - MudBlazor.Extensions v9

This document outlines the breaking changes introduced in MudBlazor.Extensions v9 to align with [MudBlazor v9 API conventions](https://github.com/MudBlazor/MudBlazor/issues/12666).

## Overview

MudBlazor v9 introduced consistent async naming across its APIs, renaming methods like `Show` to `ShowAsync` and `ShowMessageBox` to `ShowMessageBoxAsync`. MudBlazor.Extensions v9 aligns its own public API with these conventions.

All renamed methods retain their previous versions marked with `[Obsolete]` to ease migration. The obsolete methods delegate to the new async implementations and will be removed in a future major version.

---

## Dialog Service Extension Methods

### `Show<TDialog>` → `ShowAsync<TDialog>`

The `DialogServiceExt.Show<TDialog>` extension methods on `IDialogService` have been renamed to `ShowAsync<TDialog>` and now return `Task<IMudExDialogReference<TDialog>>` instead of `IMudExDialogReference<TDialog>`.

**Reason:** Aligns with MudBlazor v9's rename of `IDialogService.Show` to `IDialogService.ShowAsync`. The new methods also properly call MudBlazor's `ShowAsync` internally, fixing a potential issue with the previous synchronous wrappers.

**Before:**
```csharp
var dialog = dialogService.Show<MyDialog>("Title", dialogParameters, options);
```

**After:**
```csharp
var dialog = await dialogService.ShowAsync<MyDialog>("Title", dialogParameters, options);
```

**Affected overloads:**

| Old Method | New Method |
|---|---|
| `Show<TDialog>(title, Action<TDialog>, Action<DialogOptions>)` | `ShowAsync<TDialog>(title, Action<TDialog>, Action<DialogOptions>)` |
| `Show<TDialog>(title, TDialog, Action<DialogOptions>)` | `ShowAsync<TDialog>(title, TDialog, Action<DialogOptions>)` |
| `Show<TDialog>(title, TDialog, DialogOptions)` | `ShowAsync<TDialog>(title, TDialog, DialogOptions)` |
| `Show<TDialog>(title, Action<TDialog>, DialogOptions)` | `ShowAsync<TDialog>(title, Action<TDialog>, DialogOptions)` |

---

## Dialog Close/Cancel Extension Methods

### `CloseAnimated` → `CloseAnimatedAsync`

### `CancelAnimated` → `CancelAnimatedAsync`

### `CloseAnimatedIf` → `CloseAnimatedIfAsync`

### `CancelAnimatedIf` → `CancelAnimatedIfAsync`

The `MudDialogInstanceExtensions` close/cancel methods have been renamed with an `Async` suffix and now return `Task` instead of `void`.

**Reason:** Aligns with .NET and MudBlazor v9 conventions where asynchronous methods use the `Async` suffix. The previous `void` methods discarded the underlying async operation's result, which could mask exceptions. The new methods allow callers to properly `await` the close animation.

**Before:**
```csharp
MudDialog.CloseAnimatedIf(DialogResult.Ok(true), JsRuntime);
MudDialog.CancelAnimatedIf(JsRuntime);
```

**After:**
```csharp
await MudDialog.CloseAnimatedIfAsync(DialogResult.Ok(true), JsRuntime);
await MudDialog.CancelAnimatedIfAsync(JsRuntime);
```

**Affected methods on `IMudDialogInstance`:**

| Old Method | New Method |
|---|---|
| `CloseAnimated(jsRuntime)` | `CloseAnimatedAsync(jsRuntime)` |
| `CancelAnimated(jsRuntime)` | `CancelAnimatedAsync(jsRuntime)` |
| `CloseAnimated(result, jsRuntime)` | `CloseAnimatedAsync(result, jsRuntime)` |
| `CloseAnimated<T>(result, jsRuntime)` | `CloseAnimatedAsync<T>(result, jsRuntime)` |
| `CloseAnimatedIf(jsRuntime)` | `CloseAnimatedIfAsync(jsRuntime)` |
| `CancelAnimatedIf(jsRuntime)` | `CancelAnimatedIfAsync(jsRuntime)` |
| `CloseAnimatedIf(result, jsRuntime)` | `CloseAnimatedIfAsync(result, jsRuntime)` |
| `CloseAnimatedIf<T>(result, jsRuntime)` | `CloseAnimatedIfAsync<T>(result, jsRuntime)` |

**Affected methods on `IDialogReference`:**

| Old Method | New Method |
|---|---|
| `CloseAnimated(jsRuntime)` | `CloseAnimatedAsync(jsRuntime)` |
| `CancelAnimated(jsRuntime)` | `CancelAnimatedAsync(jsRuntime)` |
| `CloseAnimated(result, jsRuntime)` | `CloseAnimatedAsync(result, jsRuntime)` |
| `CloseAnimated<T>(result, jsRuntime)` | `CloseAnimatedAsync<T>(result, jsRuntime)` |
| `CloseAnimatedIf(jsRuntime)` | `CloseAnimatedIfAsync(jsRuntime)` |
| `CancelAnimatedIf(jsRuntime)` | `CancelAnimatedIfAsync(jsRuntime)` |
| `CloseAnimatedIf(result, jsRuntime)` | `CloseAnimatedIfAsync(result, jsRuntime)` |
| `CloseAnimatedIf<T>(result, jsRuntime)` | `CloseAnimatedIfAsync<T>(result, jsRuntime)` |

---

## Previously Deprecated Methods (Reminder)

The following methods were already deprecated in earlier versions and continue to point to their async replacements:

| Old Method | Replacement |
|---|---|
| `ShowEx<TDialog>(...)` | `ShowExAsync<TDialog>(...)` |
| `ShowMessageBoxEx(...)` | `ShowMessageBoxExAsync(...)` |
| `ShowFileDisplayDialog(...)` | `ShowFileDisplayDialogAsync(...)` |
| `ShowObject<TModel>(...)` | `ShowObjectAsync<TModel>(...)` |
| `EditObject<TModel>(...)` | `EditObjectAsync<TModel>(...)` |
| `ShowStructuredDataString(...)` | `ShowStructuredDataStringAsync(...)` |
| `EditStructuredDataString(...)` | `EditStructuredDataStringAsync(...)` |

---

## Migration Guide

1. **Find and replace** method calls in your code:
- `CloseAnimated(` → `CloseAnimatedAsync(`
- `CancelAnimated(` → `CancelAnimatedAsync(`
- `CloseAnimatedIf(` → `CloseAnimatedIfAsync(`
- `CancelAnimatedIf(` → `CancelAnimatedIfAsync(`
- `.Show<` (on `IDialogService`) → `.ShowAsync<`

2. **Add `await`** to all renamed method calls, since they now return `Task`.

3. **Update method signatures** if the calling method was `void` — change it to `async Task` or `async void` (for event handlers only).

4. The old method names still work but will produce compiler warnings. They will be removed in a future major version.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8;net9</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>true</IsPackable>
Expand Down
14 changes: 7 additions & 7 deletions MudBlazor.Extensions.Tests/MudBlazor.Extensions.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.Playwright" Version="1.51.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" />
<PackageReference Include="FluentAssertions" Version="8.8.0" />
<PackageReference Include="Microsoft.Playwright" Version="1.58.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Condition="'$(Configuration)' == 'Release'" Include="MudBlazor.Extensions" Version="*-*" />

<PackageReference Include="bunit" Version="1.39.5" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<PackageReference Include="bunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PackageReference Include="coverlet.collector" Version="8.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ public void ShouldDisplaysIcons()
context.JSInterop.SetupVoid("mudPopover.initialize", _ => true);
context.JSInterop.SetupVoid("mudKeyInterceptor.connect", _ => true);

var cut = context.RenderComponent<MudExIconPicker>();
var cut = context.Render<MudExIconPicker>();

cut.Find("button.mud-button-root").Click();
Assert.NotEmpty(cut.Find(".mud-icon-root")?.ToMarkup() ?? string.Empty);
Assert.NotEmpty(cut.Find(".mud-icon-root")?.OuterHtml ?? string.Empty);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public void StartThumb_ShouldRespectMinLength_WhenDraggingTowardsEnd()
var minLength = new RangeLength<int>(minSpan);
var initialValue = new MudExRange<int>(initialStart, initialEnd);

var cut = context.RenderComponent<MudExRangeSlider<int>>(parameters => parameters
var cut = context.Render<MudExRangeSlider<int>>(parameters => parameters
.Add(p => p.SizeRange, sizeRange)
.Add(p => p.StepLength, stepLength)
.Add(p => p.MinLength, minLength)
Expand Down Expand Up @@ -93,7 +93,7 @@ public void EndThumb_ShouldRespectMinLength_WhenDraggingTowardsStart()
var minLength = new RangeLength<int>(minSpan);
var initialValue = new MudExRange<int>(initialStart, initialEnd);

var cut = context.RenderComponent<MudExRangeSlider<int>>(parameters => parameters
var cut = context.Render<MudExRangeSlider<int>>(parameters => parameters
.Add(p => p.SizeRange, sizeRange)
.Add(p => p.StepLength, stepLength)
.Add(p => p.MinLength, minLength)
Expand Down Expand Up @@ -142,7 +142,7 @@ public void WithoutMinLength_ThumbsShouldMoveFreelyWithinSizeRange()
var stepLength = new RangeLength<int>(stepSize);
var initialValue = new MudExRange<int>(initialStart, initialEnd);

var cut = context.RenderComponent<MudExRangeSlider<int>>(parameters => parameters
var cut = context.Render<MudExRangeSlider<int>>(parameters => parameters
.Add(p => p.SizeRange, sizeRange)
.Add(p => p.StepLength, stepLength)
.Add(p => p.Value, initialValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ private void OnRenderFinishTimerElapsed(object sender, ElapsedEventArgs e)
});
}

/// <inheritdoc />
protected override IConverter<TData, U> GetDefaultConverter() => new DefaultConverter<TData>() as IConverter<TData, U>;

/// <summary>
/// Refreshes this component and forces render
/// </summary>
Expand All @@ -251,8 +254,4 @@ public virtual async ValueTask DisposeAsync()
else
_renderFinishTimer?.Dispose();
}

/// <inheritdoc />
protected MudExBaseFormComponent(Converter<TData, U> converter = null) : base(converter ?? new Converter<TData, U>())
{}
}
6 changes: 6 additions & 0 deletions MudBlazor.Extensions/Components/Base/MudExPickerBase.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public partial class MudExPickerBase<T>
[Parameter]
public AnimationType PopverAnimation { get; set; } = AnimationType.Pulse;

[Parameter]
public OverflowBehavior? OverflowBehavior { get; set; }

/// <summary>
/// Contains all parameters before init
/// </summary>
Expand Down Expand Up @@ -318,6 +321,9 @@ protected virtual void RaiseChangedIf()
RaiseChanged();
}

/// <inheritdoc />
protected override IConverter<T, string> GetDefaultConverter() => new DefaultConverter<T>();

private string GetPopOverStyle()
{
return MudExStyleBuilder.Default
Expand Down
6 changes: 0 additions & 6 deletions MudBlazor.Extensions/Components/MudExBaseInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,6 @@ public abstract class MudExBaseInput<T> : MudBaseInput<T>
[SafeCategory(CategoryTypes.FormComponent.Validation)]
[Parameter] public EventCallback ErrorStateChanged { get; set; }

/// <summary>
/// Callback when the error changes
/// </summary>
[SafeCategory(CategoryTypes.FormComponent.Validation)]
[Parameter] public EventCallback<bool> ErrorChanged { get; set; }


/// <summary>
/// Callback when the validation errors change
Expand Down
5 changes: 3 additions & 2 deletions MudBlazor.Extensions/Components/MudExCardList.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,16 @@ public MudExCardHoverMode? HoverMode
/// <summary>
/// Methods returns List of MudExCardHoverMode, where hover modes are applied.
/// </summary>
public List<MudExCardHoverMode> AllAppliedHoverModes => Enum.GetValues(typeof(MudExCardHoverMode)).Cast<MudExCardHoverMode>().Where(HoverModeMatches).ToList();
private static readonly MudExCardHoverMode[] AllHoverModeValues = (MudExCardHoverMode[])Enum.GetValues(typeof(MudExCardHoverMode));
public List<MudExCardHoverMode> AllAppliedHoverModes => AllHoverModeValues.Where(HoverModeMatches).ToList();

private string GetCss()
{
var res = CssBuilder.Default("mud-ex-card-list")
.AddClass($"mud-ex-card-list-{_id}");

foreach (var mode in AllAppliedHoverModes)
res.AddClass($"mud-ex-card-list-{mode.ToString().ToLower()}");
res.AddClass($"mud-ex-card-list-{mode.ToString().ToLowerInvariant()}");

res.AddClass(Class);
return res.Build();
Expand Down
47 changes: 25 additions & 22 deletions MudBlazor.Extensions/Components/MudExCodeView.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ namespace MudBlazor.Extensions.Components;
/// </summary>
public partial class MudExCodeView
{
private static readonly Regex LambdaStartRegex = new(@"^\s*(?:async\s+)?\([^)]*\)\s*=>\s*{?", RegexOptions.Compiled | RegexOptions.Singleline);
private static readonly Regex LambdaEndRegex = new(@"\s*}\s*$", RegexOptions.Compiled | RegexOptions.Singleline);
private static readonly Regex GenericTypeRegex = new(@"^(?<class>\w+)<(?<type>.+)>$", RegexOptions.Compiled);
private static readonly Regex EmptyTagRegex = new(@"<(\w+)([^>]*)>\s*</\1>", RegexOptions.Compiled);
private static readonly Regex HtmlTagRegex = new(@"(\<[^/][^>]*\>)|(\<\/[^>]*\>)", RegexOptions.Compiled);
private static readonly Regex MultiNewlineRegex = new(@"[\r\n]+", RegexOptions.Compiled);
private static readonly Regex BeforeTagRegex = new(@"([^\r\n])<", RegexOptions.Compiled);
private static readonly Regex AfterTagRegex = new(@">([^\r\n])", RegexOptions.Compiled);
private ElementReference _element;
private string _markdownCode;
private string _code;
Expand Down Expand Up @@ -319,10 +327,8 @@ public static (string CodeStr, Action Func) FuncAsString(Action func, bool repla
/// </summary>
public static string ReplaceLambdaInFuncString(string caller)
{
//caller = Regex.Replace(caller, @"^\s*\([^)]*\)\s*=>\s*{?", "", RegexOptions.Singleline);
//caller = Regex.Replace(caller, @"\s*}\s*$", "", RegexOptions.Singleline);
caller = Regex.Replace(caller, @"^\s*(?:async\s+)?\([^)]*\)\s*=>\s*{?", "", RegexOptions.Singleline);
caller = Regex.Replace(caller, @"\s*}\s*$", "", RegexOptions.Singleline);
caller = LambdaStartRegex.Replace(caller, "");
caller = LambdaEndRegex.Replace(caller, "");

return caller;
}
Expand All @@ -332,8 +338,9 @@ public static string ReplaceLambdaInFuncString(string caller)
public static string GenerateBlazorMarkupFromInstance<TComponent>(TComponent componentInstance, string comment = "", bool hideDefaults = true)
{
// TODO: Move to central place with ApiMemberInfo
var componentName = componentInstance?.GetType().FullName?.Replace(componentInstance.GetType().Namespace + ".", string.Empty);
var properties = componentInstance?.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
var componentType = componentInstance?.GetType();
var componentName = componentType?.FullName?.Replace(componentType.Namespace + ".", string.Empty);
var properties = componentType?.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => ObjectEditMeta.IsAllowedAsPropertyToEdit(p) && ObjectEditMeta.IsAllowedAsPropertyToEditOnAComponent<TComponent>(p));

var props = properties?.ToDictionary(info => info, info => info.GetValue(componentInstance))
Expand Down Expand Up @@ -386,7 +393,7 @@ public static (string StartTag, string EndTag) GetComponentTagNames(string input
return (input, input);
}
input = ApiMemberInfo.GetGenericFriendlyTypeName(input);
var match = Regex.Match(input, @"^(?<class>\w+)<(?<type>.+)>$");
var match = GenericTypeRegex.Match(input);


var className = match.Groups["class"].Value;
Expand All @@ -404,16 +411,17 @@ private static string MarkupValue(object value, string propName, bool ignoreComp
if (value == null || string.IsNullOrEmpty(value.ToString()))
return null;

if (value is EventCallback || (value.GetType().IsGenericType && value.GetType().GetGenericTypeDefinition() == typeof(EventCallback<>)))
var valueType = value.GetType();
if (value is EventCallback || (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(EventCallback<>)))
{
return $"@Your{propName}EventHandler";
}

if (value is bool)
return value.ToString()?.ToLower();

if (value.GetType().IsEnum)
return $"{value.GetType().FullName}.{value}";
if (valueType.IsEnum)
return $"{valueType.FullName}.{value}";

if (value is DateTime dt)
return $"@(System.DateTime.Parse(\"{dt}\"))";
Expand All @@ -435,18 +443,16 @@ private static string MarkupValue(object value, string propName, bool ignoreComp
return name != null ? $"@{name}" : value.ToString()?.Replace("\"", "\\\"");
}

if (value is string || MudExObjectEditHelper.HandleAsPrimitive(value.GetType()))
if (value is string || MudExObjectEditHelper.HandleAsPrimitive(valueType))
{
return value.ToString();
}

if (ignoreComplexTypes)
return null;

//return $"{p.Value?.GetType().FullName}.{p.Value}";

// Create a json string from the object and deserialize it in the markup
var fullName = value.GetType().FullName;
var fullName = valueType.FullName;
var friendlyTypeName = ApiMemberInfo.GetGenericFriendlyTypeName(fullName);
var json = JsonConvert.SerializeObject(value);
return $"@(Newtonsoft.Json.JsonConvert.DeserializeObject<{friendlyTypeName}>(\"{json.Replace("\"", "\\\"")}\"))";
Expand All @@ -464,7 +470,7 @@ public static string ShortenMarkup(string markup)
string replacement = @"<$1$2 />";

// Replace empty tags with self-closing tags
return RemoveEmptyLines(Regex.Replace(markup, pattern, replacement));
return RemoveEmptyLines(EmptyTagRegex.Replace(markup, replacement));
}

/// <summary>
Expand All @@ -489,15 +495,12 @@ public static string FormatHtml(string html)
html = html.Replace(">>", ">");
}

string pattern = @"(\<[^/][^>]*\>)|(\<\/[^>]*\>)";
string replacement = "$1\r\n$2";

Regex rgx = new Regex(pattern);

string result = rgx.Replace(html, replacement);
result = Regex.Replace(result, @"[\r\n]+", "\r\n");
result = Regex.Replace(result, @"([^\r\n])<", "$1\r\n<");
result = Regex.Replace(result, @">([^\r\n])", ">\r\n$1");
string result = HtmlTagRegex.Replace(html, replacement);
result = MultiNewlineRegex.Replace(result, "\r\n");
result = BeforeTagRegex.Replace(result, "$1\r\n<");
result = AfterTagRegex.Replace(result, ">\r\n$1");

int indentLevel = 0;
var lines = result.Split('\n');
Expand Down
Loading
Loading