v9.0.0 Migration Guide
Previous migration guide: v7.0.0 Migration Guide, v8.0.0 Migration Guide.
MudBlazor version 9.0.0 brings significant *breaking changes. This migration guide will help you upgrade from v8 to v9.
Note: Please limit discussion strictly to migration or reporting errors in this guide. For general feedback about version 9.0.0, use the appropriate discussion channels.
[!WARNING] Many obsolete APIs marked for removal in v8 have been removed in v9. The compiler will NOT always catch these changes at compile time if you're using dynamic invocation or reflection. Ensure thorough testing after migration.
Converters: Complete Rework
The converter system has been completely redesigned for better performance, type safety, and extensibility. This is one of the most significant breaking changes in v9.
Removed:
Converter<T, U>classConverter<T>classDefaultConverter(old implementation)BoolConverter(old implementation)DateConverterNumericConverter.AreEqualmethodConvertersstatic class
Replaced with:
IConverter<TInput, TOutput>interfaceICultureAwareConverter<TInput, TOutput>interfaceIReversibleConverter<TInput, TOutput>interfaceDefaultConverter<T>(new implementation)BoolConverter<T>(new implementation)RangeConverter<T>DeferredConverter<TInput, TOutput>EmptyConverter<TInput, TOutput>ConversionResult<T>for error handlingConverterExtensionsfor fluent APIConversionsstatic class for common conversions
Breaking Changes:
- Custom converters must implement interfaces:
Before (v8):
public class MyConverter : Converter<MyType>
{
public MyConverter()
{
SetFunc = value => value?.ToString() ?? string.Empty;
GetFunc = str => MyType.Parse(str);
}
}After (v9):
public class MyConverter : IReversibleConverter<MyType, string>
{
public string Convert(MyType input)
{
// ...
}
public MyType ConvertBack(string input)
{
// ...
}
}- Inline converters
Before (v8):
private Converter<ConverterElement?> _elementConverter = new Converter<ConverterElement?>
{
SetFunc = value => value?.ToString(),
GetFunc = text => new ConverterElement { Name = text }
};After (v9):
private IConverter<ConverterElement?, string?> _elementConverter = Conversions
.From((ConverterElement? value) => value?.ToString(),
text => new ConverterElement { Name = text });- Component Converter property changes:
Components like MudTextField<T>, MudNumericField<T>, etc. still have a Converter property, but it now expects the new converter types. The framework provides automatic conversion for most built-in types.
- GetDefaultConverter() method added (PR #12365):
All components inheriting from MudFormComponent must now implement a GetDefaultConverter() method instead of setting the converter in the constructor. This provides compile-time safety and allows wrapper components to use null for the Converter parameter.
❌ Before (v8):
public class MyInput : MudFormComponent<MyType, string>
{
public MyInput()
{
Converter = new DefaultConverter<MyType>
{
Culture = GetCulture,
Format = GetFormat
};
}
}✅ After (v9):
public class MyInput : MudFormComponent<MyType, string>
{
// Constructor no longer sets Converter
protected override IConverter<MyType?, string?> GetDefaultConverter()
{
return new DefaultConverter<MyType>
{
Culture = GetCulture,
Format = GetFormat
};
}
}Key changes:
- Converter parameter is now nullable: You can set
Convertertonull, and the component will useGetDefaultConverter()as fallback - Compile-time safety: Forgetting to implement
GetDefaultConverter()now causes a compile error instead of a runtime exception - Better for wrappers: Wrapper components no longer need to explicitly pass a converter if they want default behavior
Important: The Converter parameter is checked first. If it's null, GetDefaultConverter() is called once and cached. Use GetConverter() method (not the Converter property) when you need to access the active converter in component logic.
Example accessing the converter:
// ❌ Don't access Converter directly if it might be null
var converter = Converter; // May be null!
// ✅ Use GetConverter() which handles the fallback
var converter = GetConverter(); // Always returns non-nullMigration tip: If you created custom converters, you'll need to rewrite them to implement the new interfaces. See PR #12177 for detailed examples. If you inherit from MudFormComponent, implement GetDefaultConverter() (PR #12365).
Remove All Obsolete/Deprecated Code
All code marked with [Obsolete] or [Deprecated] in v8 has been removed in v9.
Removed from DialogService:
Show(Type)- useShowAsync(Type)Show<T>()- useShowAsync<T>()ShowMessageBox()- useShowMessageBoxAsync()ShowForm<T>()- useShowFormAsync<T>()Close()- useCloseAsync()
Removed from MudDataGrid:
ExpandAllGroups()- useExpandAllGroupsAsyncCollapseAllGroups()- useCollapseAllGroupsAsync
Removed from MudSelect:
Clear- useClearAsync
Removed from MudTabs:
ActivatePanel- useActivatePanelAsync
Removed from MudMenu:
Stylename
Removed from ElementReferenceExtensions:
MudDetachBlurEventWithJS- useUse mudElementRef.removeOnBlurEvent via js invoke instead
More details: #12142
MudGlobal: Theming Properties Removed
All theming-related properties have been removed from MudGlobal. These experimental properties created maintenance burden and blurred the boundary between behavioral and visual concerns. Use CSS variables, theme configuration, or explicit component parameters instead.
Removed from MudGlobal:
MudGlobal.Rounded(static property)MudGlobal.ButtonDefaults.Color(default:Color.Default)MudGlobal.ButtonDefaults.Variant(default:Variant.Text)MudGlobal.InputDefaults.ShrinkLabel(default:false)MudGlobal.InputDefaults.Variant(default:Variant.Text)MudGlobal.InputDefaults.Margin(default:Margin.None)MudGlobal.LinkDefaults.Color(default:Color.Primary)MudGlobal.LinkDefaults.Typo(default:Typo.body1)MudGlobal.LinkDefaults.Underline(default:Underline.Hover)MudGlobal.GridDefaults.Spacing(default:6)MudGlobal.StackDefaults.Spacing(default:3)MudGlobal.PopoverDefaults.Elevation(default:8)
Retained non-theming properties in MudGlobal:
DialogDefaults.DefaultFocusMenuDefaults.HoverDelayPopoverDefaults.ModalOverlayTooltipDefaults.Delay/DurationTransitionDefaults.Delay/DurationUnhandledExceptionHandler
Components affected:
All affected components now use hard-coded defaults matching the previous MudGlobal default values:
MudButton,MudIconButton,MudToggleIconButton- now default toColor.DefaultandVariant.TextMudBaseInputand all derived inputs (MudTextField,MudNumericField, etc.) - now default toVariant.Text,Margin.None, andShrinkLabel = falseMudLink- now defaults toColor.Primary,Typo.body1, andUnderline.HoverMudGrid- now defaults toSpacing = 6MudStack- now defaults toSpacing = 3MudPopover- now defaults toElevation = 8MudPickerand all derived pickers - now default toElevation = 8for the popover- Components with
Square/Roundedparameters (MudAlert,MudAvatar,MudAvatarGroup,MudCard,MudDataGrid,MudExpansionPanels,MudNavMenu,MudPaper,MudPicker,MudPopover,MudProgressCircular,MudProgressLinear,MudSimpleTable,MudTable,MudTabs) - no longer respectMudGlobal.Rounded
Migration: Users relying on global theming should migrate to:
- Explicit component parameters - Set properties directly on each component
- Theme tokens - Use theme configuration for colors, typography, and shape
- Wrapper components - Create app-specific wrapper components for shared styling
- CSS - Apply custom styles via CSS classes or variables
Example migration:
Before (v8):
// Program.cs or Startup.cs
MudGlobal.ButtonDefaults.Variant = Variant.Filled;
MudGlobal.InputDefaults.Variant = Variant.Outlined;After (v9) - Option 1: Explicit parameters:
<MudButton Variant="Variant.Filled">Click Me</MudButton>
<MudTextField Variant="Variant.Outlined" />After (v9) - Option 2: Wrapper component:
@* AppButton.razor *@
<MudButton Variant="Variant.Filled" Class="@Class" @attributes="AdditionalAttributes">
@ChildContent
</MudButton>
@code {
[Parameter] public string? Class { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter(CaptureUnmatchedValues = true)]
public IDictionary<string, object>? AdditionalAttributes { get; set; }
}More details: #12141
DialogService
ShowMessageBox Renamed
Replace ShowMessageBox with ShowMessageBoxAsync:
More details: #12292
Dialog.DefaultFocus Moved
MudGlobal.DialogDefaults.DefaultFocus has been moved to MudDialogProvider.
Before (v8):
MudGlobal.DialogDefaults.DefaultFocus = DefaultFocus.FirstChild;After (v9):
<MudDialogProvider DefaultFocus="DefaultFocus.FirstChild" />Or set it via DialogOptions:
var options = new DialogOptions { DefaultFocus = DefaultFocus.FirstChild };More details: #12297
MudTheme: Palette Type Changes
PaletteLight and PaletteDark are now of type Palette instead of their specific types.
Before (v8):
PaletteLight PaletteLight { get; set; }
PaletteDark PaletteDark { get; set; }After (v9):
Palette PaletteLight { get; set; }
Palette PaletteDark { get; set; }This should have minimal impact as both derive from Palette.
More details: #12148
Transition Defaults Moved
Popover transition defaults moved from MudGlobal to PopoverOptions.
Before (v8):
MudGlobal.PopoverDefaults.TransitionDuration = 300;After (v9):
builder.Services.AddMudServices(config =>
{
config.PopoverOptions.TransitionDuration = 300;
});More details: #12300
MudMenu: MenuContext Replaces IActivatable
MudMenu.ActivatorContent now receives a MenuContext parameter instead of using IActivatable via cascading value. The MenuContext provides explicit async methods (OpenAsync, CloseAsync, ToggleAsync, CloseAllAsync) for controlling menus.
Breaking Changes:
ActivatorContentsignature changed fromRenderFragment?toRenderFragment<MenuContext>?- Menu is no longer opened implicitly - You must explicitly call context methods in event handlers
IActivatable.Activatemethod removed fromMudMenu- Root div event handlers only fire for default activators (Button, Icon, Label)
MenuContext API:
public sealed class MenuContext
{
public Task OpenAsync(EventArgs? args = null);
public Task CloseAsync();
public Task ToggleAsync(EventArgs? args = null);
public Task CloseAllAsync();
}Migration Examples:
❌ Before (v8) - Implicit activation:
<MudMenu>
<ActivatorContent>
<MudButton Variant="Variant.Filled">Open Menu</MudButton>
</ActivatorContent>
<ChildContent>
<MudMenuItem>Item 1</MudMenuItem>
</ChildContent>
</MudMenu>✅ After (v9) - Explicit context usage:
<MudMenu>
<ActivatorContent>
<MudButton Variant="Variant.Filled" OnClick="@context.ToggleAsync">Open Menu</MudButton>
</ActivatorContent>
<ChildContent>
<MudMenuItem>Item 1</MudMenuItem>
</ChildContent>
</MudMenu>Left Click:
<MudMenu ActivationEvent="MouseEvent.LeftClick">
<ActivatorContent>
<MudChip OnClick="@(() => context.ToggleAsync())">Click Me</MudChip>
</ActivatorContent>
</MudMenu>Right Click:
<MudMenu ActivationEvent="MouseEvent.RightClick">
<ActivatorContent>
<div @oncontextmenu="@context.ToggleAsync" @oncontextmenu:preventDefault="true">
<MudChip>Right Click Me</MudChip>
</div>
</ActivatorContent>
</MudMenu>Mouse Over:
<MudMenu ActivationEvent="MouseEvent.MouseOver">
<ActivatorContent>
<div @onpointerenter="@context.OpenAsync" @onpointerleave="@context.CloseAsync">
<MudChip>Hover Over Me</MudChip>
</div>
</ActivatorContent>
</MudMenu>Positioned at Cursor:
<MudMenu PositionAtCursor="true">
<ActivatorContent>
@* Pass event args to OpenAsync/ToggleAsync for cursor positioning *@
<div @onclick="@context.ToggleAsync" style="cursor: pointer">
<MudCard>
<MudCardContent>Click anywhere on this card</MudCardContent>
</MudCard>
</div>
</ActivatorContent>
</MudMenu>Non-interactive Activators:
For non-interactive elements like MudAvatar, wrap them in a div with event handlers:
<MudMenu>
<ActivatorContent>
<div @onclick="@context.ToggleAsync" style="cursor: pointer">
<MudAvatar>
<MudImage Src="avatar.jpg" />
</MudAvatar>
</div>
</ActivatorContent>
</MudMenu>More details: #12145
MudFormComponent & MudBaseInput: API Changes
The API had inconsistent naming between MudFormComponent and MudBaseInput that has been fixed for consistency.
MudFormComponent: Error and ErrorId Two-Way Bindable
Error and ErrorId are now two-way bindable parameters.
New capability (v9):
<MudTextField @bind-Error="myError" @bind-ErrorId="myErrorId" />Method Naming Changes
Several methods have been renamed for consistency:
Reset()→ResetAsync()(was already marked async)Validate()→ValidateAsync()(if async)ReadValue()→ReadValue(property-style, no parentheses)
WriteValueAsync and SetValueAsync Renamed
Breaking Changes (PR #12312):
MudFormComponent.WriteValueAsyncrenamed toSetValueAsyncthen toSetValueCoreAsync:
To match the ReadValue property pattern (Read/Set), WriteValueAsync was renamed, then later renamed again with Core suffix.
❌ Before (v8):
protected internal virtual T? ReadValue { get; }
protected virtual Task WriteValueAsync(T? value)✅ After (v9 - Final):
protected internal virtual T? ReadValue { get; }
protected virtual Task SetValueCoreAsync(T? value) // was WriteValueAsync → SetValueAsync → SetValueCoreAsyncMudBaseInput.SetValueAsync(T?, bool, bool)renamed toSetValueAndUpdateTextAsync:
To mirror the existing SetTextAndUpdateValueAsync method and avoid conflict with the base SetValueAsync method.
SetTextAsyncrenamed toSetTextCoreAsync:
For consistency with the Core suffix pattern.
❌ Before (v8):
// Text API
protected internal string? ReadText { get; }
protected Task SetTextAsync(string? text);
protected Task SetTextAndUpdateValueAsync(string? text, bool updateValue = true);
// Value API (inconsistent)
protected internal T? ReadValue { get; }
protected Task SetValueAsync(T? value, bool updateText = true, bool force = false);✅ After (v9 - Final):
// Text API
protected internal string? ReadText { get; }
protected Task SetTextCoreAsync(string? text); // was SetTextAsync
protected Task SetTextAndUpdateValueAsync(string? text, bool updateValue = true);
// Value API
protected internal T? ReadValue { get; }
protected virtual Task SetValueCoreAsync(T? value); // was WriteValueAsync → SetValueAsync
protected Task SetValueAndUpdateTextAsync(T? value, bool updateText = true, bool force = false); // was SetValueAsync(T?, bool, bool)Migration:
If you override WriteValueAsync in a custom component:
❌ Before (v8):
protected override Task WriteValueAsync(MyType? value)
{
_value = value;
return Task.CompletedTask;
}✅ After (v9):
protected override Task SetValueCoreAsync(MyType? value)
{
_value = value;
return Task.CompletedTask;
}If you call SetValueAsync(value, updateText, force) in a custom input:
❌ Before (v8):
await SetValueAsync(newValue, updateText: true, force: false);✅ After (v9):
await SetValueAndUpdateTextAsync(newValue, updateText: true, force: false);If you were calling SetTextAsync internally:
❌ Before (v8):
await SetTextAsync(newText);✅ After (v9):
await SetTextCoreAsync(newText);All are protected APIs, so external impact should be minimal. Only affects custom components that inherit from MudFormComponent or MudBaseInput and override these methods.
MudSelect
Comparer
Now requires a proper GetHashCode implementation that doesn’t throw an exception.
If you are using EqualityComparer<T>.Create(equals, getHashCode), you must provide the getHashCode parameter, otherwise, a runtime exception will occur.
SelectedValues Changed to IReadOnlyCollection
Before (v8):
ICollection<T> SelectedValues { get; set; }After (v9):
IReadOnlyCollection<T> SelectedValues { get; set; }More details: #12619
EventListener / EventManager Removed
The EventListener, EventListenerFactory, and related event management infrastructure have been completely removed.
Removed:
IEventListener/EventListenerIEventListenerFactory/EventListenerFactoryIEventManagerWebEventJsonContext
More details: #12532
Range and DateRange: Setters Removed
Range<T>.Start, Range<T>.End, DateRange.Start, and DateRange.End properties no longer have setters. These classes should now be treated as immutable to ensure proper GetHashCode() behavior and thread safety.
Breaking Changes:
Range<T>properties are now read-only (init-only)DateRangeproperties are now read-only (init-only)- Must create new instances instead of mutating existing ones
Complete API Changes:
❌ Before (v8):
public class Range<T>
{
public T? Start { get; set; } // Had setter
public T? End { get; set; } // Had setter
}
public class DateRange : Range<DateTime?>
{
// Inherited mutable properties
}✅ After (v9):
public classSource: MudBlazor/MudBlazor