Recently I've been getting into Blazor and I've mostly used ready components from Telerik and Radzen. However in order to properly learn all that good stuff I want to create custom components and simple ones are going well.
However now I needed to code a TabControl component and I have found this guide and followed closely. My problem now is that Visual Studio stops debugging and closes the app when I'm navigating to the page containing my CustomTabControl. This is what VS tells me:
The program '[4428] FactoryVisual.exe' has exited with code 3221225477 (0xc0000005) 'Access violation'.
Now my problem isn't understanding what that means, it's where I have caused this in my code. I am still fairly new to coding so I hope this isn't a waste-of-timey issue. Here it is with uninteresting bits cut out, it's pretty much the same as the guide's code-example logically (or so I hope/believe):
MachineTypeEditView.razor
<CustomTabControl>
<CustomTab Name="Tab1">
This is test-content.<br/>
<button>Testbutton1</button>
</CustomTab>
<CustomTab Name="Tab2">
This is test-content.<br />
<button>Testbutton2</button>
</CustomTab>
</CustomTabControl>
CustomTabControl.razor
<div style="width: @Width ; heigth: @Height ;">
<CascadingValue Value="this">
<div class="btn-group" role="group">
@foreach (var tab in Tabs)
{
<CustomTabButton Text="@tab.Name" Click="@(() => SwapTab(tab))" />
}
</div>
@ChildComponent
</CascadingValue>
</div>
@code
{
[Parameter]
public RenderFragment<CustomTab>? ChildComponent { get; set; }
public CustomTab ActiveTab { get; set; } = new();
public List<CustomTab> Tabs = new();
internal void AddPage(CustomTab newTab)
{
Tabs.Add(newTab);
if (Tabs.Count == 1)
{
ActiveTab = Tabs.First();
}
StateHasChanged();
}
private string GetButtonClass(CustomTab tab)
{
// todo
return "test";
}
private void SwapTab(CustomTab tab)
{
ActiveTab = tab;
}
}
CustomTab.razor
@if (ParentControl.ActiveTab == this)
{
@ChildContent
}
@code
{
[Parameter]
public string Name { get; set; } = string.Empty;
[CascadingParameter]
private CustomTabControl ParentControl { get; set; } = new();
[Parameter]
public RenderFragment? ChildContent { get; set; }
protected override void OnInitialized()
{
ParentCheck();
base.OnInitialized();
}
private void ParentCheck()
{
if (ParentControl == null)
{
throw new ArgumentNullException(nameof(ParentControl), "CustomTab must exist within a CustomTabControl.");
}
TellParent();
}
private void TellParent()
{
ParentControl.AddPage(this);
}
}
I did not include the CustomTabButton since I've tested the code without and the issue still came up. Originally I have used this component in a .MAUI Blazor app, but that told me absolutely nothing and just couldn't load the page. Then I have used it in a Blazor Server App to get the console output from above.
Let me know if you need more code-spamming to give me a hint.
Your problem is trying to new up components and then using them. It took quite a lot of commenting and refactoring to get your code to stop crashing!
I've refactored the code somewhat to:
CustomTabControl
[one of my pet hates].QuickGrid
.Here's CustomTabControl
. It now just cascades the registration Action
.
<div class="container-fluid">
<CascadingValue Value="RegisterTab">
<nav class="nav nav-pills nav-fill">
@foreach (var tab in Tabs)
{
<a class="@this.GetButtonClass(tab)" @onclick="@(() => SwapTab(tab))">@tab.Name</a>
}
</nav>
@if (_firstRender)
{
@*this renders the actual custom Tabs so thay can register - we only do it on the first render*@
@ChildContent
}
else
{
@*After the first render we just render the ChildContent in the active tab*@
@ActiveTab?.ChildContent
}
</CascadingValue>
</div>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
private CustomTab? ActiveTab { get; set; }
private List<CustomTab> Tabs = new();
private bool _firstRender = true;
protected async override Task OnInitializedAsync()
{
// This uses a bit of magic where we use the double render
// built into CompomnentBase's lifecycle process to register the sub-components on the first yield
// provided by Task.Delay
await Task.Delay(1);
_firstRender = false;
}
private void RegisterTab(CustomTab newTab)
{
// check if the tab is already registered
if (Tabs.Any(item => item == newTab))
return;
Tabs.Add(newTab);
// Set the first tab as the active tab
if (Tabs.Count == 1)
SwapTab(Tabs.First());
StateHasChanged();
}
private string GetButtonClass(CustomTab tab)
=> tab == ActiveTab ? "nav-link active" : "nav-link";
private void SwapTab(CustomTab tab)
=> ActiveTab = tab;
}
CustomTab
is now a class. It's content is ChildContent
which we can access from CustomTabControl
.
public class CustomTab : ComponentBase
{
[Parameter] public string Name { get; set; } = string.Empty;
[CascadingParameter] private Action<CustomTab>? Register { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
protected override void OnInitialized()
{
if (Register == null)
throw new ArgumentNullException("No Registation Action cascaded.");
this.Register.Invoke(this);
}
}
And finally Index.razor
@page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
<CustomTabControl>
<CustomTab Name="Content 1">
<div class="alert alert-danger m-2">
This is Content 1.<br />
<button class="btn btn-danger">Stop</button>
</div>
</CustomTab>
<CustomTab Name="Content 2">
<div class="alert alert-success m-2">
This is Content 2.<br />
<button class="btn btn-success">Start</button>
</div>
</CustomTab>
</CustomTabControl>