From ec8954dc295c63f0d9e1372d512fdc90eabdce7c Mon Sep 17 00:00:00 2001 From: "Dixon, Evan" Date: Thu, 13 Aug 2026 14:36:50 -0500 Subject: [PATCH 1/5] Update packages --- Mimeo.DynamicUI.Blazor/Mimeo.DynamicUI.Blazor.csproj | 10 +++++----- .../Mimeo.DynamicUI.Demo.Client.csproj | 6 +++--- .../Mimeo.DynamicUI.Demo.Server.csproj | 2 +- Mimeo.DynamicUI/Mimeo.DynamicUI.csproj | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Mimeo.DynamicUI.Blazor/Mimeo.DynamicUI.Blazor.csproj b/Mimeo.DynamicUI.Blazor/Mimeo.DynamicUI.Blazor.csproj index c3f2ac0..ec1a225 100644 --- a/Mimeo.DynamicUI.Blazor/Mimeo.DynamicUI.Blazor.csproj +++ b/Mimeo.DynamicUI.Blazor/Mimeo.DynamicUI.Blazor.csproj @@ -24,11 +24,11 @@ - - - - - + + + + + diff --git a/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Mimeo.DynamicUI.Demo.Client.csproj b/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Mimeo.DynamicUI.Demo.Client.csproj index 463e700..c1d4672 100644 --- a/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Mimeo.DynamicUI.Demo.Client.csproj +++ b/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Mimeo.DynamicUI.Demo.Client.csproj @@ -9,9 +9,9 @@ - - - + + + diff --git a/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Server/Mimeo.DynamicUI.Demo.Server.csproj b/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Server/Mimeo.DynamicUI.Demo.Server.csproj index 4ac5852..85e1732 100644 --- a/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Server/Mimeo.DynamicUI.Demo.Server.csproj +++ b/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Server/Mimeo.DynamicUI.Demo.Server.csproj @@ -9,7 +9,7 @@ - + diff --git a/Mimeo.DynamicUI/Mimeo.DynamicUI.csproj b/Mimeo.DynamicUI/Mimeo.DynamicUI.csproj index 97cd04b..e47c936 100644 --- a/Mimeo.DynamicUI/Mimeo.DynamicUI.csproj +++ b/Mimeo.DynamicUI/Mimeo.DynamicUI.csproj @@ -17,7 +17,7 @@ - + From bef0691c2d42c6d2c05d2f0a0a36e25c5bb91a67 Mon Sep 17 00:00:00 2001 From: "Dixon, Evan" Date: Thu, 13 Aug 2026 14:36:59 -0500 Subject: [PATCH 2/5] Add link button --- Mimeo Dynamic UI.sln | 4 +- Mimeo.DynamicUI.Blazor/Controls/LinkButton.cs | 84 +++++++++++++++++++ .../Layout/NavMenu.razor | 6 ++ .../Pages/LinkButtonSample.razor | 11 +++ 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 Mimeo.DynamicUI.Blazor/Controls/LinkButton.cs create mode 100644 Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Pages/LinkButtonSample.razor diff --git a/Mimeo Dynamic UI.sln b/Mimeo Dynamic UI.sln index 8c7e53f..24645cb 100644 --- a/Mimeo Dynamic UI.sln +++ b/Mimeo Dynamic UI.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.8.34601.278 +# Visual Studio Version 18 +VisualStudioVersion = 18.8.12023.21 stable MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C6BE560F-B786-4E6A-B529-377579860D15}" ProjectSection(SolutionItems) = preProject diff --git a/Mimeo.DynamicUI.Blazor/Controls/LinkButton.cs b/Mimeo.DynamicUI.Blazor/Controls/LinkButton.cs new file mode 100644 index 0000000..15609d4 --- /dev/null +++ b/Mimeo.DynamicUI.Blazor/Controls/LinkButton.cs @@ -0,0 +1,84 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; +using Radzen.Blazor; + +namespace Mimeo.DynamicUI.Blazor.Controls +{ + /// + /// A that renders as an anchor instead of a button, so that the browser's + /// open-in-new-tab gestures (middle click, ctrl+click) work on it. Styling comes from the base class, + /// so it looks the same as the equivalent . + /// + /// + /// Only the icon, image and text content of is supported; a link cannot be busy. + /// + public class LinkButton : RadzenButton + { + /// + /// The URL to navigate to. + /// + [Parameter] + public string? Path { get; set; } + + /// + /// The browsing context to open the URL in, such as "_blank". + /// + [Parameter] + public string? Target { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + if (!Visible) + { + return; + } + + builder.OpenElement(0, "a"); + builder.AddAttribute(1, "style", Style); + builder.AddAttribute(2, "tabindex", Disabled ? -1 : TabIndex); + builder.AddAttribute(3, "target", Target); + builder.AddMultipleAttributes(4, Attributes); + // An anchor has no disabled state, so drop the destination instead. GetCssClass() still greys it out. + builder.AddAttribute(5, "href", Disabled ? null : Path); + builder.AddAttribute(6, "class", GetCssClass()); + builder.AddAttribute(7, "id", GetId()); + builder.AddElementReferenceCapture(8, element => Element = element); + + builder.OpenElement(9, "span"); + builder.AddAttribute(10, "class", "rz-button-box"); + if (ChildContent != null) + { + builder.AddContent(11, ChildContent); + } + else + { + if (!string.IsNullOrEmpty(Icon)) + { + builder.OpenElement(12, "i"); + builder.AddAttribute(13, "class", "rz-button-icon-left rzi"); + builder.AddAttribute(14, "style", !string.IsNullOrEmpty(IconColor) ? $"color:{IconColor}" : null); + builder.AddContent(15, (MarkupString)Icon); + builder.CloseElement(); + } + if (!string.IsNullOrEmpty(Image)) + { + builder.OpenElement(16, "img"); + builder.AddAttribute(17, "class", "rz-button-icon-left rzi"); + builder.AddAttribute(18, "src", Image); + builder.AddAttribute(19, "alt", ImageAlternateText); + builder.CloseElement(); + } + if (!string.IsNullOrEmpty(Text)) + { + builder.OpenElement(20, "span"); + builder.AddAttribute(21, "class", "rz-button-text"); + builder.AddContent(22, Text); + builder.CloseElement(); + } + } + builder.CloseElement(); + + builder.CloseElement(); + } + } +} diff --git a/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Layout/NavMenu.razor b/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Layout/NavMenu.razor index 80f3103..c31037b 100644 --- a/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Layout/NavMenu.razor +++ b/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Layout/NavMenu.razor @@ -25,6 +25,12 @@ Dynamic Edit Form + + diff --git a/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Pages/LinkButtonSample.razor b/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Pages/LinkButtonSample.razor new file mode 100644 index 0000000..574edd5 --- /dev/null +++ b/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Pages/LinkButtonSample.razor @@ -0,0 +1,11 @@ +@page "/link-button" + +

Link Button Sample

+ +

A simple wrapper around a RadzenButton that can be middle-clicked to open in a new tab.

+ + + +@code { + +} From 81b33ef86175c993df2618d5ec925994c99ba8e4 Mon Sep 17 00:00:00 2001 From: "Dixon, Evan" Date: Thu, 13 Aug 2026 15:03:46 -0500 Subject: [PATCH 3/5] Let custom menu items be links --- .../Forms/CustomMenuButton.cs | 17 +++++++++++++++++ ...{CustomMenuItem.cs => CustomMenuItemBase.cs} | 11 ++--------- Mimeo.DynamicUI.Blazor/Forms/CustomMenuLink.cs | 8 ++++++++ Mimeo.DynamicUI.Blazor/Forms/ODataGrid.razor | 9 ++++++++- Mimeo.DynamicUI.Blazor/Forms/ODataGrid.razor.cs | 6 +++--- .../Pages/ODataGridSample.razor | 6 +++--- 6 files changed, 41 insertions(+), 16 deletions(-) create mode 100644 Mimeo.DynamicUI.Blazor/Forms/CustomMenuButton.cs rename Mimeo.DynamicUI.Blazor/Forms/{CustomMenuItem.cs => CustomMenuItemBase.cs} (59%) create mode 100644 Mimeo.DynamicUI.Blazor/Forms/CustomMenuLink.cs diff --git a/Mimeo.DynamicUI.Blazor/Forms/CustomMenuButton.cs b/Mimeo.DynamicUI.Blazor/Forms/CustomMenuButton.cs new file mode 100644 index 0000000..29375e1 --- /dev/null +++ b/Mimeo.DynamicUI.Blazor/Forms/CustomMenuButton.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Components; + +namespace Mimeo.DynamicUI.Blazor.Forms +{ + public class CustomMenuButton : CustomMenuItemBase + { + /// + /// Callback when the menu item is clicked + /// + public required EventCallback Callback { get; set; } + } + + [Obsolete("Use CustomMenuItemBase instead for lists, use CustomMenuButton instead for buttons.", error: true)] + public class CustomMenuItem : CustomMenuButton + { + } +} diff --git a/Mimeo.DynamicUI.Blazor/Forms/CustomMenuItem.cs b/Mimeo.DynamicUI.Blazor/Forms/CustomMenuItemBase.cs similarity index 59% rename from Mimeo.DynamicUI.Blazor/Forms/CustomMenuItem.cs rename to Mimeo.DynamicUI.Blazor/Forms/CustomMenuItemBase.cs index a5e2414..f91499d 100644 --- a/Mimeo.DynamicUI.Blazor/Forms/CustomMenuItem.cs +++ b/Mimeo.DynamicUI.Blazor/Forms/CustomMenuItemBase.cs @@ -1,8 +1,6 @@ -using Microsoft.AspNetCore.Components; - -namespace Mimeo.DynamicUI.Blazor.Forms +namespace Mimeo.DynamicUI.Blazor.Forms { - public class CustomMenuItem + public abstract class CustomMenuItemBase { /// /// Name of the action to be performed. @@ -10,11 +8,6 @@ public class CustomMenuItem /// public required string Name { get; set; } - /// - /// Callback when the menu item is clicked - /// - public required EventCallback Callback { get; set; } - /// /// Name of the material icon used to represent the action /// diff --git a/Mimeo.DynamicUI.Blazor/Forms/CustomMenuLink.cs b/Mimeo.DynamicUI.Blazor/Forms/CustomMenuLink.cs new file mode 100644 index 0000000..025d5b8 --- /dev/null +++ b/Mimeo.DynamicUI.Blazor/Forms/CustomMenuLink.cs @@ -0,0 +1,8 @@ +namespace Mimeo.DynamicUI.Blazor.Forms +{ + public class CustomMenuLink : CustomMenuItemBase + { + public required string Path { get; set; } + public string? Target { get; set; } + } +} diff --git a/Mimeo.DynamicUI.Blazor/Forms/ODataGrid.razor b/Mimeo.DynamicUI.Blazor/Forms/ODataGrid.razor index 8aa69ac..f7ebe7b 100644 --- a/Mimeo.DynamicUI.Blazor/Forms/ODataGrid.razor +++ b/Mimeo.DynamicUI.Blazor/Forms/ODataGrid.razor @@ -92,7 +92,14 @@ Density="@Density"> @foreach (var action in CustomRowActions) { - + if (action is CustomMenuButton actionButton) + { + + } + else if (action is CustomMenuLink actionLink) + { + + } } @if (CanCopy) diff --git a/Mimeo.DynamicUI.Blazor/Forms/ODataGrid.razor.cs b/Mimeo.DynamicUI.Blazor/Forms/ODataGrid.razor.cs index ea20442..581c245 100644 --- a/Mimeo.DynamicUI.Blazor/Forms/ODataGrid.razor.cs +++ b/Mimeo.DynamicUI.Blazor/Forms/ODataGrid.razor.cs @@ -84,7 +84,7 @@ public partial class ODataGrid public bool AllowView { get; set; } = true; [Parameter] - public List CustomRowActions { get; set; } = []; + public List CustomRowActions { get; set; } = []; /// /// For form fields of type , a dictionary matching form field property language keys to controls that can display them. @@ -471,11 +471,11 @@ private class GridItem public Func ViewRow { get; set; } = default!; public Action ShowTooltip { get; set; } = default!; - public async Task OnCustomAction(MouseEventArgs _, CustomMenuItem customMenuItem) + public async Task OnButtonCustomAction(MouseEventArgs _, CustomMenuButton customMenuItem) { await customMenuItem.Callback.InvokeAsync(ListModel); } - public void OnCustomActionMouseEnter(ElementReference element, CustomMenuItem customMenuItem) => ShowTooltip(element, customMenuItem.Name); + public void OnCustomActionMouseEnter(ElementReference element, CustomMenuItemBase customMenuItem) => ShowTooltip(element, customMenuItem.Name); public async Task OnEditClick(MouseEventArgs _) => await EditRow(ListModel); public void OnEditMouseEnter(ElementReference element) => ShowTooltip(element, "edit"); diff --git a/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Pages/ODataGridSample.razor b/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Pages/ODataGridSample.razor index e0f0bdb..6339dea 100644 --- a/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Pages/ODataGridSample.razor +++ b/Mimeo.DynamicUI.Demo/Mimeo.DynamicUI.Demo.Client/Pages/ODataGridSample.razor @@ -23,7 +23,7 @@ CustomRowActions="CustomRowActions" /> ["testviewmodel_customformfield"] = typeof(CustomFormField) }; - protected List CustomRowActions = []; + protected List CustomRowActions = []; protected override void OnInitialized() { @@ -32,9 +32,9 @@ CustomRowActions="CustomRowActions" /> DemoOptions.OnValueChanged = EventCallback.Factory.Create(this, () => StateHasChanged()); CustomRowActions = [ - new() + new CustomMenuButton() { - Name = "Log Item to Browser Console", + Name = "Log Item to Browser Console", Icon = "cruelty_free", // 🐰 Callback = new EventCallback(this, new Action(vm => Console.WriteLine($"Custom action for view model '{(vm as TestViewModel)?.Name}' has been clicked."))) } From b507cdf2befd522375877ac04e7ff25e2e7a149f Mon Sep 17 00:00:00 2001 From: "Dixon, Evan" Date: Thu, 13 Aug 2026 15:07:32 -0500 Subject: [PATCH 4/5] Update link button radzen compatibility, add tests --- .../Controls/LinkButtonTests.cs | 80 +++++++++++++++++++ .../Mimeo.DynamicUI.Blazor.Tests.csproj | 2 + Mimeo.DynamicUI.Blazor/Controls/LinkButton.cs | 21 ++--- 3 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 Mimeo.DynamicUI.Blazor.Tests/Controls/LinkButtonTests.cs diff --git a/Mimeo.DynamicUI.Blazor.Tests/Controls/LinkButtonTests.cs b/Mimeo.DynamicUI.Blazor.Tests/Controls/LinkButtonTests.cs new file mode 100644 index 0000000..f86e068 --- /dev/null +++ b/Mimeo.DynamicUI.Blazor.Tests/Controls/LinkButtonTests.cs @@ -0,0 +1,80 @@ +using AwesomeAssertions; +using Bunit; +using Microsoft.AspNetCore.Components; +using Mimeo.DynamicUI.Blazor.Controls; +using Radzen; +using Radzen.Blazor; + +namespace Mimeo.DynamicUI.Blazor.Tests.Controls +{ + public class LinkButtonTests + { + [Fact] + public void RendersAnAnchorPointingAtPath() + { + using var context = CreateContext(); + + var component = context.Render(RenderLinkButton("/orders/MEM/12345")); + + var anchor = component.Find("a"); + anchor.GetAttribute("href").Should().Be("/orders/MEM/12345"); + } + + [Fact] + public void LooksTheSameAsTheEquivalentRadzenButton() + { + using var context = CreateContext(); + + var link = context.Render(RenderLinkButton("/orders/MEM/12345")).Find("a"); + var button = context.Render(RenderRadzenButton()).Find("button"); + + link.GetAttribute("class").Should().Be(button.GetAttribute("class")); + link.InnerHtml.Should().Be(button.InnerHtml); + } + + [Fact] + public void DisabledLinkHasNoDestination() + { + using var context = CreateContext(); + + var component = context.Render((RenderFragment)(builder => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(LinkButton.Path), "/orders/MEM/12345"); + builder.AddAttribute(2, nameof(LinkButton.Disabled), true); + builder.CloseComponent(); + })); + + component.Find("a").HasAttribute("href").Should().BeFalse(); + } + + private static RenderFragment RenderLinkButton(string path) => builder => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(LinkButton.ButtonStyle), ButtonStyle.Base); + builder.AddAttribute(2, nameof(LinkButton.Icon), "arrow_forward"); + builder.AddAttribute(3, nameof(LinkButton.Size), ButtonSize.Small); + builder.AddAttribute(4, nameof(LinkButton.Text), "View Recipient"); + builder.AddAttribute(5, nameof(LinkButton.Path), path); + builder.CloseComponent(); + }; + + private static RenderFragment RenderRadzenButton() => builder => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(RadzenButton.ButtonStyle), ButtonStyle.Base); + builder.AddAttribute(2, nameof(RadzenButton.Icon), "arrow_forward"); + builder.AddAttribute(3, nameof(RadzenButton.Size), ButtonSize.Small); + builder.AddAttribute(4, nameof(RadzenButton.Text), "View Recipient"); + builder.CloseComponent(); + }; + + private static BunitContext CreateContext() + { + var context = new BunitContext(); + context.JSInterop.Mode = JSRuntimeMode.Loose; + return context; + } + } + +} diff --git a/Mimeo.DynamicUI.Blazor.Tests/Mimeo.DynamicUI.Blazor.Tests.csproj b/Mimeo.DynamicUI.Blazor.Tests/Mimeo.DynamicUI.Blazor.Tests.csproj index 2aa81d2..9827733 100644 --- a/Mimeo.DynamicUI.Blazor.Tests/Mimeo.DynamicUI.Blazor.Tests.csproj +++ b/Mimeo.DynamicUI.Blazor.Tests/Mimeo.DynamicUI.Blazor.Tests.csproj @@ -10,6 +10,8 @@ + + diff --git a/Mimeo.DynamicUI.Blazor/Controls/LinkButton.cs b/Mimeo.DynamicUI.Blazor/Controls/LinkButton.cs index 15609d4..3f066d2 100644 --- a/Mimeo.DynamicUI.Blazor/Controls/LinkButton.cs +++ b/Mimeo.DynamicUI.Blazor/Controls/LinkButton.cs @@ -55,24 +55,25 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) if (!string.IsNullOrEmpty(Icon)) { builder.OpenElement(12, "i"); - builder.AddAttribute(13, "class", "rz-button-icon-left rzi"); - builder.AddAttribute(14, "style", !string.IsNullOrEmpty(IconColor) ? $"color:{IconColor}" : null); - builder.AddContent(15, (MarkupString)Icon); + builder.AddAttribute(13, "class", "notranslate rz-button-icon-left rzi"); + builder.AddAttribute(14, "aria-hidden", "true"); + builder.AddAttribute(15, "style", !string.IsNullOrEmpty(IconColor) ? $"color:{IconColor}" : null); + builder.AddContent(16, Icon); builder.CloseElement(); } if (!string.IsNullOrEmpty(Image)) { - builder.OpenElement(16, "img"); - builder.AddAttribute(17, "class", "rz-button-icon-left rzi"); - builder.AddAttribute(18, "src", Image); - builder.AddAttribute(19, "alt", ImageAlternateText); + builder.OpenElement(17, "img"); + builder.AddAttribute(18, "class", "notranslate rz-button-icon-left rzi"); + builder.AddAttribute(19, "src", Image); + builder.AddAttribute(20, "alt", ImageAlternateText); builder.CloseElement(); } if (!string.IsNullOrEmpty(Text)) { - builder.OpenElement(20, "span"); - builder.AddAttribute(21, "class", "rz-button-text"); - builder.AddContent(22, Text); + builder.OpenElement(21, "span"); + builder.AddAttribute(22, "class", "rz-button-text"); + builder.AddContent(23, Text); builder.CloseElement(); } } From 07371d6749f204d310208de990c56e698663665d Mon Sep 17 00:00:00 2001 From: "Dixon, Evan" Date: Thu, 13 Aug 2026 15:15:16 -0500 Subject: [PATCH 5/5] Let custom menu link use different url per view model --- Mimeo.DynamicUI.Blazor/Forms/CustomMenuLink.cs | 4 ++-- Mimeo.DynamicUI.Blazor/Forms/ODataGrid.razor | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Mimeo.DynamicUI.Blazor/Forms/CustomMenuLink.cs b/Mimeo.DynamicUI.Blazor/Forms/CustomMenuLink.cs index 025d5b8..b6e7f62 100644 --- a/Mimeo.DynamicUI.Blazor/Forms/CustomMenuLink.cs +++ b/Mimeo.DynamicUI.Blazor/Forms/CustomMenuLink.cs @@ -2,7 +2,7 @@ { public class CustomMenuLink : CustomMenuItemBase { - public required string Path { get; set; } - public string? Target { get; set; } + public required Func Path { get; set; } + public Func? Target { get; set; } } } diff --git a/Mimeo.DynamicUI.Blazor/Forms/ODataGrid.razor b/Mimeo.DynamicUI.Blazor/Forms/ODataGrid.razor index f7ebe7b..f9aaa71 100644 --- a/Mimeo.DynamicUI.Blazor/Forms/ODataGrid.razor +++ b/Mimeo.DynamicUI.Blazor/Forms/ODataGrid.razor @@ -98,7 +98,7 @@ Density="@Density"> } else if (action is CustomMenuLink actionLink) { - + } }