Blazor parent/child components not updating - c#

I've got a blazor project and created some tab components. I want these tabs to be dynamic so I can add and take away from them when new values get added to the view model.
Adding works fine but once the page has rendered and I try to remove one of the tabs it doesn't update the tab header and I can't work out why as the view model has changed so surely the page should re-render?
The count updates correctly but the page is not re-rendering to remove the tab. I'm sure I'm fundamentally missing something.
I've created a fiddle to show this better than I can explain: https://blazorfiddle.com/s/fduse7v2
Thanks in advance.
EDIT - Added code from fiddle as requested.
//INDEX
#page "/"
<h1>Tab Issue</h1>
Data Count: #View.Data.Count
<button #onclick='(() => Add($"Tab {(View.Data.Count + 1)}"))'>Add Tab</button>
<TabControl>
#foreach (var data in View.Data)
{
<TabPage Text='#data'>
#data <button #onclick="(() => Remove(data))">Remove</button>
</TabPage>
}
</TabControl>
#code{
public class ViewModel{
public List<string> Data = new List<string>{
"Tab 1","Tab 2","Tab 3"
};
}
public ViewModel View { get; set; }
protected override async Task OnInitializedAsync()
{
View = new ViewModel();
}
public void Add(string data)
{
View.Data.Add(data);
StateHasChanged();
}
public void Remove(string data)
{
View.Data.Remove(data);
StateHasChanged();
}
}
//TabPage
#if (Parent.ActivePage == this)
{
<div class='tab-page #($"tab-{this.Text.Replace(" ","-").ToLower()}")'>
#ChildContent
</div>
}
#code {
[CascadingParameter] public TabControl Parent { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public string Id { get; set; }
[Parameter] public string Text { get; set; }
protected override void OnInitialized()
{
if (Parent == null)
throw new ArgumentNullException(nameof(Parent), "TabPage must exist within a TabControl");
Parent.AddPage(this);
base.OnInitialized();
}
}
//Tab Control
<div style="display:flex;">
<ul style="display:flex;">
#foreach (TabPage tabPage in Pages)
{
<li style="margin-right:20px; #GetActiveClass(tabPage); list-style:none; " #onclick=#(() => ActivatePage(tabPage))>#tabPage.Text</li>
}
</ul>
</div>
<CascadingValue Value="this">
#ChildContent
</CascadingValue>
#code {
[Parameter]
public RenderFragment ChildContent { get; set; }
public TabPage ActivePage { get; set; }
public List<TabPage> Pages { get; } = new List<TabPage>();
public void AddPage(TabPage tabPage)
{
Pages.Add(tabPage);
if (Pages.Count == 1)
ActivePage = tabPage;
StateHasChanged();
}
public string GetActiveClass(TabPage page)
{
return page == ActivePage ? "border-bottom: 2px solid #263238;" : "";
}
public void ActivatePage(TabPage page)
{
System.Console.WriteLine($"Activating Page: {page.Text} - {page.Id}.");
ActivePage = page;
}
}

Related

binding mudblazor treeview to an object

I am new to blazor and need to create a dynamic treeview where nodes can be created and deleted. All this data is saved in database. After pulling the data from database what kind of object I need to create to bind to Mudtreeview?
If only first level nodes are rendered initially, how can I load the children on node click?
is there any example somewhere? All the sample code on mudblazor site is with static data.
Adding and removeing items is easy if you bind the treeview to a collection in MudBlazor. I made a little demo for you to can play around online: https://try.mudblazor.com/snippet/cuwlvFQcfJTzHuki
Here is the full code of the example:
<MudButton OnClick="AddItems" Variant="Variant.Filled">Add items</MudButton>
<MudButton OnClick="DeleteItems" Variant="Variant.Filled">Delete items</MudButton>
<MudPaper Width="300px" Elevation="0">
<MudTreeView Items="TreeItems" MultiSelection="true" #bind-ActivatedValue="ActivatedValue" #bind-SelectedValues="SelectedValues">
<ItemTemplate>
<MudTreeViewItem #bind-Expanded="#context.IsExpanded" Items="#context.TreeItems" Value="#context"
Icon="#context.Icon" Text="#context.Title" EndText="#context.Number?.ToString()" EndTextTypo="#Typo.caption" />
</ItemTemplate>
</MudTreeView>
</MudPaper>
<div style="width: 100%">
<MudText Typo="#Typo.subtitle1">Activated item: #(ActivatedValue?.Title)</MudText>
<MudText Typo="#Typo.subtitle1">Sum of selected items: #GetSelectedSum()</MudText>
</div>
#code {
private TreeItemData ActivatedValue { get; set; }
private HashSet<TreeItemData> SelectedValues { get; set; }
private HashSet<TreeItemData> TreeItems { get; set; } = new HashSet<TreeItemData>();
public class TreeItemData
{
public string Title { get; set; }
public string Icon { get; set; }
public int? Number { get; set; }
public bool IsExpanded { get; set; }
public HashSet<TreeItemData> TreeItems { get; set; }
public TreeItemData(string title, string icon, int? number = null)
{
Title = title;
Icon = icon;
Number = number;
}
public override bool Equals(object x) {
var other = x as TreeItemData;
if (other==null)
return false;
return other.Title==Title;
}
public override int GetHashCode() {
return Title.GetHashCode();
}
}
protected override void OnInitialized()
{
TreeItems.Add(new TreeItemData("All Mail", Icons.Filled.Email));
TreeItems.Add(new TreeItemData("Trash", Icons.Filled.Delete));
TreeItems.Add(new TreeItemData("Categories", Icons.Filled.Label)
{
IsExpanded = true,
TreeItems = new HashSet<TreeItemData>()
{
new TreeItemData("Social", Icons.Filled.Group, 90),
new TreeItemData("Updates", Icons.Filled.Info, 2294),
new TreeItemData("Forums", Icons.Filled.QuestionAnswer, 3566),
new TreeItemData("Promotions", Icons.Filled.LocalOffer, 733)
}
});
TreeItems.Add(new TreeItemData("History", Icons.Filled.Label));
}
public int GetSelectedSum()
{
return SelectedValues?.Sum(i => i.Number ?? 0) ?? 0;
}
private int i=0;
public void AddItems() {
TreeItems.Add(new TreeItemData("Added Item " + (i++), Icons.Filled.Coronavirus));
}
public void DeleteItems() {
var item=TreeItems.FirstOrDefault(x=>x.Title.StartsWith("Added Item"));
TreeItems.Remove(item);
}
}

Dynamically generated Blazor pages are only partially updated between page navigation

I have a list of pages, each using the same Blazor page component, of which the content is generated dynamically based on the page name. The content is split across different tabs. Due to some Blazor rendering issue, when navigating between pages, not everything is updated, resulting in the update of my tab content, but my tab headers are not.
Everything is done in .NET Core 3.1 LTS, as I am not able to upgrade to .NET 5 yet due to various other constraints.
Given, as an example, the following page content:
pageContent.Add("page-1", new Dictionary<string, string>
{
{ "tab a", "This is page 1, tab a" },
{ "tab b", "This is page 1, tab b" },
{ "tab c", "This is page 1, tab c" }
});
pageContent.Add("page-2", new Dictionary<string, string>
{
{ "tab d", "This is page 2, tab d" },
{ "tab e", "This is page 2, tab e" }
});
The result is the following:
As you can see, on page 2 the content is updated, but the tab headers are not. However, if I move back from page 2 to page 1, the content of page 1 is displayed, but now with the tab headers of page 2 are shown.
My tab control is based on various samples found online, and although it contains a lot more functionality, I tried to reduce it to a very basic working example which reproduces the problem.
PageContentService.cs
public Dictionary<string, string> GetPageContent(string pageName)
=> pageContent.ContainsKey(pageName) ? pageContent[pageName] : new Dictionary<string, string>();
DynamicPage.razor
#page "/dynamic/{PageName}"
<MyDynamicContent PageName="#PageName"/>
#code {
[Parameter]
public string PageName { get; set; }
}
MyDynamicContent.razor
#if (isLoading)
{
<p>Loading...</p>
}
else
{
<MyTabs SelectedTab="#selectedTabTitle" OnSelectedTabChanged="OnSelectedTabChanged">
#foreach (var (tabId, tabContent) in currentPage)
{
<MyTabItem Title="#tabId">
#tabContent
</MyTabItem>
}
</MyTabs>
}
#code {
private bool isLoading;
private string selectedTabTitle;
private Dictionary<string, string> currentPage;
[Parameter]
public string PageName { get; set; }
[Inject]
public IPageContentService PageContentService { get; set; }
protected override void OnParametersSet()
{
base.OnParametersSet();
isLoading = true;
currentPage = PageContentService.GetPageContent(PageName);
if (currentPage != null && currentPage.Count > 0)
{
selectedTabTitle = currentPage.First().Key;
}
isLoading = false;
}
public void OnSelectedTabChanged(string title)
=> selectedTabTitle = title;
}
MyTabs.razor
<CascadingValue Value="#(this)" IsFixed="true">
<CascadingValue Value="#selectedTabName">
<div>
<ul class="nav nav-tabs">
#foreach (var item in tabItems)
{
var aCss = "nav-link";
if (item.Active)
{
aCss += " active show";
}
<li class="nav-item">
<a class="#aCss" tabindex="0" #onclick="async () => await item.OnTabClicked()">
#item.Title
</a>
</li>
}
</ul>
<div class="tab-content">
#ChildContent
</div>
</div>
</CascadingValue>
</CascadingValue>
#code {
private readonly List<MyTabItem> tabItems = new List<MyTabItem>();
private string selectedTabName;
[Parameter]
public string SelectedTab
{
get => selectedTabName;
set
{
if (selectedTabName != value)
{
selectedTabName = value;
OnSelectedTabChanged.InvokeAsync(value);
}
}
}
[Parameter]
public EventCallback<string> OnSelectedTabChanged { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
public IReadOnlyList<MyTabItem> TabItems
=> tabItems;
public async Task ChangeSelectedTab(string title)
{
SelectedTab = title;
await InvokeAsync(StateHasChanged);
}
public async Task AddTab(MyTabItem tabItem)
{
if (tabItems.All(x => x.Title != tabItem.Title))
{
tabItems.Add(tabItem);
await InvokeAsync(StateHasChanged);
}
}
public async Task RemoveTab(MyTabItem tabItem)
{
if (tabItems.Any(x => x.Title == tabItem.Title))
{
tabItems.Remove(tabItem);
await InvokeAsync(StateHasChanged);
}
}
}
MyTabItem.razor
#implements IAsyncDisposable
#{
var css = "tab-pane";
if (Active)
{
css += " active show";
}
}
<div class="#css">
#if (Active)
{
#ChildContent
}
</div>
#code {
[Parameter]
public string Title { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
[CascadingParameter]
public MyTabs ParentTabs { get; set; }
public bool Active
=> ParentTabs.SelectedTab == Title;
public async Task OnTabClicked()
{
if (ParentTabs != null)
{
await ParentTabs.ChangeSelectedTab(Title);
}
}
protected override async Task OnInitializedAsync()
{
if (ParentTabs != null)
{
await ParentTabs.AddTab(this);
}
await base.OnInitializedAsync();
}
public ValueTask DisposeAsync()
=> ParentTabs != null ? new ValueTask(ParentTabs.RemoveTab(this)) : new ValueTask(Task.CompletedTask);
}
My question is very simple: Why is my tab content updating, but my tab headers are not? And how do I get them to update when navigating between pages?
Although adding the #key property to each list isn't a bad idea, as #MisterMagoo suggested in the comments, it wasn't wat ultimately fixed it.
The trick was to make every async method synchronous.
MyTabs.razor
public void ChangeSelectedTab(string title)
{
SelectedTab = title;
StateHasChanged;
}
public void AddTab(MyTabItem tabItem)
{
if (tabItems.All(x => x.Title != tabItem.Title))
{
tabItems.Add(tabItem);
StateHasChanged;
}
}
public void RemoveTab(MyTabItem tabItem)
{
if (tabItems.Any(x => x.Title == tabItem.Title))
{
tabItems.Remove(tabItem);
StateHasChanged;
}
}
MyTabItem.razor
public void OnTabClicked()
{
ParentTabs?.ChangeSelectedTab(Title);
}
protected override void OnInitialized()
{
ParentTabs?.AddTab(this);
base.OnInitialized();
}
public void Dispose()
{
ParentTabs?.RemoveTab(this);
}
So the async methods seem to really change how the render pipeline behaves, and do things seemingly out of order. I guess that makes a bit sense, and might have some benefits too it, but it isn't what I needed in this scenario.

Problem generate list of checkboxes from list of data models in Blazor

Anyone who knows how to initialize a list of checkboxes with only myPicketItemsStoredInDabase items marked as checked and all other items marked as not checked?
When I try the code example below all checkboxes is marked as checked. I have tried with different solutions but non of them ends upp with a list of checkboxes with the right items marked as checked.
public class CheckboxItem
{
public Guid Id { get; set; }
public string Name { get; set; }
public bool Checked { get; set; }
}
public class ItemFromDatabase
{
public Guid Id { get; set; }
public string Name { get; set; }
}
Page.razor
#foreach (var item in itemsToEdit)
{
<CheckBoxItemComponent Items="#itemsToEdit" Value="#item.Name" Item="#item" />
}
#code {
private IEnumerable<ItemFromDatabase> myPickedItemsStoredInDatabase;
private IEnumerable<ItemFromDatabase> allItems;
private List<CheckboxItem> itemsToEdit;
protected override async Task OnInitializedAsync()
{
myPickedItemsStoredInDatabase = await getFromJsonAsync<ItemFromDatabase>.....
allItems= await getFromJsonAsync<ItemFromDatabase>.....
InitCheckboxItems();
}
private void InitCheckboxItems()
{
itemsToEdit = new List<CheckboxItem>();
for (int i = 0; i < allItems.Length; i++)
{
CheckboxItem item = new CheckboxItem()
{
Id = allItems[i].Id,
Name = allItems[i].Name,
Checked = false
};
if (myPickedItemsStoredInDatabase.Any(p => p.Id == item.Id))
{
item.Checked = true;
}
itemsToEdit.Add(item);
}
}
}
CheckBoxItemComponent.razor
Activate multiple checkbox in a loop using Blazor
<input type="checkbox" #bind=isChecked />#Item.Name<br />
#code
{
[Parameter]
public string Value { get; set; }
[Parameter]
public CheckboxItem Item { get; set; }
[Parameter]
public List<CheckboxItem> Items { get; set; }
private bool isChecked
{
get => Items.Any(el => el.Name == Value);
set
{
if (value)
{
if (!Items.Any(el => el.Name == Value))
Items.Add(Item);
}
else
Items.Remove(Item);
}
}
}`
I don't understand what should happen in isChecked.
To set checked you can modify the CheckBoxItemComponent.razor like this:
<input type="checkbox" #bind=isChecked /> #Item.Name
<br />
#code
{
[Parameter]
public CheckboxItem Item { get; set; }
public bool isChecked
{
get => this.Item.Checked;
set => this.Item.Checked = value;
}
}
And use it in Page.razor:
#foreach (var item in itemsToEdit)
{
<CheckBoxItemComponent Item="#item" />
}
You need to be very careful when using:
#foreach (var item in itemsToEdit)
{
<CheckBoxItemComponent Items="#itemsToEdit" Value="#item.Name" Item="#item" />
}
This is Razor, not a C# code file. You can find and examine the C# pre-compiled file in obj/debug/net5.0/razor/pages/page.razor.g.cs if your razor file is called page in the pages directory.
if you consume item during the loop - like setting a value to a primitive type, all works fine. BUT any object references - such as Item="#item" - can easily end up pointing to the last item - the one after the foreach finishes. This may be what's happening - your testing checking on the last item. Try setting a local variable in the loop to item and then using that for the component parameters. See below.
#foreach (var item in itemsToEdit)
{
var thisitem = item;
<CheckBoxItemComponent Items="#itemsToEdit" Value="#thisitem.Name" Item="#thisitem" />
}

Blazor implementation of Bootstrap Vue B-Checkbox with array model

I'm trying to come up with a Blazor implementation of using an array type model with multiple checkboxes.
Vue component:
<template>
<div>
<b-form-group label="Using sub-components:">
<b-form-checkbox-group id="checkbox-group-2" v-model="selected" name="flavour-2">
<b-form-checkbox value="orange">Orange</b-form-checkbox>
<b-form-checkbox value="apple">Apple</b-form-checkbox>
<b-form-checkbox value="pineapple">Pineapple</b-form-checkbox>
<b-form-checkbox value="grape">Grape</b-form-checkbox>
</b-form-checkbox-group>
</b-form-group>
<div>Selected: <strong>{{ selected }}</strong></div>
</div>
</template>
<script>
export default {
data() {
return {
selected: [], // Must be an array reference!
}
}
}
</script>
Blazor component:
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="#id" name="#name" #onchange="#((ChangeEventArgs) => CheckedChanged(ChangeEventArgs, value))">
<label class="custom-control-label" for="#id">#label</label>
</div>
#code {
[Parameter]
public string id { get; set; }
[Parameter]
public string name { get; set; }
[Parameter]
public object value { get; set; }
[Parameter]
public List<object> model { get; set; }
[Parameter]
public EventCallback<List<object>> modelChanged { get; set; }
[Parameter]
public string label { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
}
protected void CheckedChanged(ChangeEventArgs args, object value)
{
if(!model.Any(i => i == value))
{
model.Add(value);
}
else
{
model.Remove(value);
}
}
}
Usage:
#foreach (string timezone in DistinctTimezones)
{
<BCheckbox #bind-model="#FilterTimezones" value="#timezone" label="#timezone" id="#(string.Format("timezone_{0}", timezone))" name="#(string.Format("timezone_{0}", timezone))" />
}
<p>Selected:</p>
#foreach(var timezone in FilterTimezones)
{
#timezone
}
#code {
protected List<string> DistinctTimezones { get; set; } = new List<string>{"Central", "Eastern"};
protected List<object> FilterTimezones { get; set; } = new List<object>();
}
When I check the checkboxes, the FilterTimezone object doesn't get updated with the values from checked checkboxes. Is this something that is already possible and I am overcomplicating it? I'm only aware of binding values to a non-collection type.
I'd do it like that (string is here just to simplify to get the idea)
CollectionCheckBox.razor
<input type="checkbox" #bind=isChecked />
#code
{
[Parameter]
public string Value { get; set; }
[Parameter]
public List<string> Model {get; set;}
private bool isChecked
{
get => Model.Any(el => el == Value);
set
{
if(value)
{
if(!Model.Any(el => el == Value) Model.Add(Value);
}
else
Model.Remove(Value);
}
}
}
Then in parent component you just do
<CollectionCheckBox Model="#Model", Value="Orange" />
<CollectionCheckBox Model="#Model", Value="Apple" />
<CollectionCheckBox Model="#Model", Value="Pineapple" />
<CollectionCheckBox Model="#Model", Value="Grape" />
#code
{
private List<string> Model = new List<string();
}

Select Multi Option Blazor custom component javascript free

At this time, blazor doesn't support the binding for <select multiple> ..., it returns only the first selected option and not all of them.
https://github.com/aspnet/AspNetCore/issues/5519
So i'm trying to make my own component to workaround this problem.
Here is what I have for now:
<select #bind="optionSelected">
#foreach (KeyValuePair<string, string> option in Options) {
<option value="#option.Key" style="#(Value.Contains(option.Key) ? "background-color: dodgerblue;" : "")">#option.Value</option>
}
</select>
#code {
[Parameter] public List<string> Value { get; set; }
[Parameter] public EventCallback<List<string>> ValueChanged { get; set; }
[Parameter] public Dictionary<string, string> Options { get; set; }
private string _optionSelected;
private string optionSelected {
get { return _optionSelected; }
set {
if (optionSelected != null)
OnOptionSelected(value);
_optionSelected = "";
}
}
protected override void OnInitialized() {
optionSelected = null;
}
private void OnOptionSelected(string value) {
if (!Value.Contains(value))
Value.Add(value);
else
Value.Remove(value);
ValueChanged.InvokeAsync(Value);
}
}
It works well but I'm wondering how I could upgrade this component, because actually I have to click the select again and again and I would like to click it once and then only use the shortcut ctrl + click on each option to add them into the List<string> Value without using Javascript

Categories

Resources