I'm trying to create a Tab component that I can databind to a singleton model object.
So I have a state object
public class AppState
{
private List<TabValue> _tabValues = new List<TabValue>();
private TabValue _selectedValue = null;
public AppState()
{
Add(new TabValue { Title = "Tab 0", Contents = "Contents 0" });
Add(new TabValue { Title = "Tab 1", Contents = "Contents 1" });
Add(new TabValue { Title = "Tab 2", Contents = "Contents 2" });
}
public TabValue[] TabValues { get { return _tabValues.ToArray(); } }
public void Add(TabValue tabValue)
{
this._tabValues.Add(tabValue);
if (_selectedValue == null)
_selectedValue = tabValue;
OnChanged();
}
public TabValue SelectedValue
{
get { return _selectedValue; }
set { _selectedValue = value; OnChanged(); }
}
public event Action Changed;
protected virtual void OnChanged() { if (Changed != null) Changed(); }
}
public class TabValue
{
public string Title { get; set; }
public string Contents { get; set; }
}
I want to render this as a Tab control, and I want 2 way binding, i.e. when I change the AppState object the UI reflects it, and when the active tab in the UI Tab control is changed the state in the AppState is updated.
@inject AppState MyAppState
@{Debug.WriteLine("Page.BuildRenderTree " + MyAppState.TabValues.Count());}
<TabControl TabPageChanged="@MyTabIndexChanged">
<ChildContent>
@foreach (var tabData in MyAppState.TabValues)
{
Debug.WriteLine("Page.BuildRenderTree TabPage: " + tabData.Title);
<TabPage @key="@tabData" Text="@tabData.Title" Selected="@(MyAppState.SelectedValue == tabData)">
@tabData.Contents
</TabPage>
}
</ChildContent>
</TabControl>
@code {
private void MyTabIndexChanged(int tabIndex)
{
MyAppState.SelectedValue = MyAppState.TabValues[tabIndex];
this.StateHasChanged();
}
protected override void OnInitialized()
{
MyAppState.Changed += this.StateHasChanged;
}
public void Dispose()
{
MyAppState.Changed -= this.StateHasChanged;
}
}
I've added this code to the 'counter' page in the default VS generated project.
The TabControl code is based on the Blazor-Univerity code.
@{Debug.WriteLine("TabControl.BuildRenderTree " + Pages.Count);}
<div class="btn-group" role="group">
@foreach (TabPage tabPage in Pages)
{
Debug.WriteLine("TabControl.BuildRenderTree TabHeader " + tabPage.Text);
<button type="button"
class="btn @GetButtonClass(tabPage)"
@onclick="@(() => OnTabPageChanged(tabPage))">
@tabPage.Text
</button>
}
</div>
<CascadingValue Value="this">
@{Debug.WriteLine("TabControl.BuildRenderTree ChildContent " + Pages.Count);}
@ChildContent
</CascadingValue>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public EventCallback<int> TabPageChanged { get; set; }
List<TabPage> Pages = new List<TabPage>();
public TabPage ActivePage
{
get { return this.Pages.Where(t => t.Selected).FirstOrDefault() ?? this.Pages.FirstOrDefault(); }
}
protected virtual void OnTabPageChanged(TabPage tabPage)
{
TabPageChanged.InvokeAsync(Pages.IndexOf(tabPage));
this.StateHasChanged();
}
internal void AddPage(TabPage tabPage)
{
Pages.Add(tabPage);
}
private string GetButtonClass(TabPage page)
{
return page.Selected ? "btn-primary" : "btn-secondary";
}
protected override void OnInitialized()
{
Debug.WriteLine("TabControl.OnInitialized " + Pages.Count);
base.OnInitialized();
}
}
And finally the TabPage
@{Debug.WriteLine($"TabPage.BuildRenderTree {Text}"); }
@if (Parent.ActivePage == this)
{
<div>
@ChildContent
</div>
}
@code {
private bool _Selected = false;
[CascadingParameter]
private TabControl Parent { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public string Text { get; set; }
[Parameter]
public bool Selected
{
get { return _Selected; }
set
{
if (_Selected == value)
return;
_Selected = value;
StateHasChanged();
}
}
protected override void OnInitialized()
{
if (Parent == null)
throw new ArgumentNullException(nameof(Parent), "TabPage must exist within a TabControl");
Debug.WriteLine($"TabPage.OnInitialized {Text}");
base.OnInitialized();
Parent.AddPage(this);
}
}
So when I run this up it do not display my Tab Control, but with the help of the tracing I've added its obvious why
Page.BuildRenderTree 3
TabControl.OnInitialized 0 <-- NO TABS
TabControl.BuildRenderTree 0
TabControl.BuildRenderTree ChildContent 0
Page.BuildRenderTree TabPage: Tab 0
Page.BuildRenderTree TabPage: Tab 1
Page.BuildRenderTree TabPage: Tab 2
TabPage.OnInitialized Tab 0
TabPage.OnInitialized Tab 1
TabPage.OnInitialized Tab 2
TabPage.BuildRenderTree Tab 0
TabPage.BuildRenderTree Tab 1
TabPage.BuildRenderTree Tab 2
I seems at the point when the TabControl is rendered it does not yet know about its TabPages.
The bit I can't figure out is how to fix this....
I've tried a number of iterations of this and a number of 3rd party tab controls, and my UI always seems to be one refresh off of being up to date (note pressing the 'counter' button will cause the page to refresh as expected).
So my question is how do ensure the child TabPages are initialized when the TabControl is rendered.
This is currently running blazor-server-side.
Whats rendered on load
Whats rendered after forcing a refresh with the Counter 'Click me' Button


Selectedproperty of your page when you click on a button, but in theTabPageyou test theParent.ActivePage.But this doesn't change. Set your test on_SelectedofSelectedproperty field of yourTabPage- agua from mars