Component creation order - data-binding

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

Basically all my issues come down to the order in which things are created.
The tabs are not created until after the render and in order to select the ActiveTab we (potentially) need to know about all the Tabs, so its a chicken and egg issue.
So we need to re-write the code in such a way that the tabs aren't needed until after they have been rendered.
TabSet.razor
<!-- Display the tab headers -->
<CascadingValue Value=this>
<ul class="nav nav-tabs">
#ChildContent
</ul>
<!-- Display body for only the active tab -->
<div class="nav-tabs-body p-4">
#if (HasActiveTab)
{
#ActiveTab?.ChildContent
}
</div>
</CascadingValue>
#code {
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public EventCallback<int> TabChanged { get; set; }
[Parameter]
public int SelectedIndex { get; set; }
public List<Tab> Tabs = new List<Tab>();
public void AddTab(Tab tab)
{
Tabs.Add(tab);
StateHasChanged();
}
public void RemoveTab(Tab tab)
{
Tabs.Remove(tab);
}
public bool HasActiveTab { get { return SelectedIndex >= 0 && SelectedIndex < Tabs.Count; } }
public bool IsActiveTab(Tab tab)
{
if (!HasActiveTab)
return false;
// NOTE : We may not have all the tabs loaded at this point
int tabIndex = Tabs.IndexOf(tab);
return tabIndex == SelectedIndex;
}
// Note : unsafe to call before the parent has rendered all its children (i.e. ChildContent)
// instead use IsActiveTab
public Tab ActiveTab
{
get
{
return Tabs[SelectedIndex];
}
set
{
SelectedIndex = this.Tabs.IndexOf(value);
TabChanged.InvokeAsync(SelectedIndex);
StateHasChanged();
}
}
}
Tab.razor
#implements IDisposable
<li>
<a #onclick="Activate" class="nav-link #TitleCssClass" role="button">
#Title
</a>
</li>
#code {
[CascadingParameter]
public TabSet ContainerTabSet { get; set; }
[Parameter]
public string Title { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
// NOTE: use IsActiveTab within the Tab, NOT ActiveTab
private string TitleCssClass => ContainerTabSet.IsActiveTab(this) ? "active" : null;
private void Activate()
{
ContainerTabSet.ActiveTab = this;
}
// attach the tab to the tabset
protected override void OnInitialized()
{
base.OnInitialized();
ContainerTabSet.AddTab(this);
}
// detach the tab to the tabset
public void Dispose()
{
this.ContainerTabSet.RemoveTab(this);
}
}
The following shows the order of the rendering. Note the Tab now only renders its header section. The TabSet takes responsibility for rendering the selected Tabs contents. The interaction between the Tab and the TabSet works fine, but is only aware of the current Tab and the ones already rendered (an unknown number of Tabs have yet to be rendered and as long as we don't write code that assumes they exist, its all good).
// TabSet.Tabs is empty
TabSet <ul class="nav nav-tabs">
TabSet #ChildContent
TabSet </ul>
// TabSet.Tabs is empty
Tab0 <a #onclick="Activate" class="nav-link #TitleCssClass" role="button">
Tab0 #Title
Tab0 </a>
Tab0.OnInitialized ContainerTabSet.AddTab(this);
// TabSet.Tabs contains (Tab0)
Tab1 <a #onclick="Activate" class="nav-link #TitleCssClass" role="button">
Tab1 #Title
Tab1 </a>
Tab1.OnInitialized ContainerTabSet.AddTab(this);
// TabSet.Tabs contains (Tab0, Tab1)
More Tabs ...
// TabSet.Tabs is complete and contains (Tab0, Tab1, ...)
TabSet #if (HasActiveTab)
TabSet {
TabSet #ActiveTab?.ChildContent
TabSet }
There are a number of potential issues still. When a Tab header is rendered we need to know if its the selected tab as the style changes accordingly. So lets look at TitleCssClass.
private string TitleCssClass => ContainerTabSet.IsActiveTab(this) ? "active" : null;
This looks simple enough, but we need to look at IsActiveTab in more detail.
public bool IsActiveTab(Tab tab)
{
if (!HasActiveTab)
return false;
// NOTE : We may not have all the tabs loaded at this point
int tabIndex = Tabs.IndexOf(tab);
return tabIndex == SelectedIndex;
}
This is a safe way of finding out if a given tab is the selected one without needing all the tabs to be loaded. If we had used ActiveTab == this then we would run into trouble as the code for ActiveTab looks something like this 'Tabs[SelectedIndex]' and if Tabs is only partially loaded then we could end up with an out of range index.
Another point is that when a tab is removed we must inform the tabset that its gone. We do this in the Tab.Dispose method. There is an assumption here that the framework will call Dispose at an appropriate time.
One oddity is the need for the #if (HasActiveTab) guard in the TabSet razor code. It seems the the very first run through it either has not tabs or it has not rendered the child Tabs. The guard fixes the issues and is not needed after the first load. If anyone could shed some light on this I'd be interested.

Related

Cant pass parameter in razor file to OnClick event (cannot convert from 'void' to 'Microsoft.AspNetCore.Components.EventCallback')

Need a little help creating custom select component
I am attempting to create a custom form select component. The component will contain my own custom markup rather than using the tag as it needs a completely different UI beyond which I can style with css.
The component should be able to bind it's value to a string / int / decimal model property which is where I am having trouble.
This is what I have so far:
MySelect.razor
#typeparam TValue
#inherits InputBase<TValue>
#namespace Accounting.Web.Components
#foreach (var option in Options)
{
<button #onclick="OnClick(option.Value)">#option.Value</button>
}
MySelect.razor.cs
namespace Accounting.Web.Components
{
public partial class MySelectOption<TValue>
{
public int Id { get; set; }
public TValue Value { get; set; }
}
public partial class MySelect<TValue> : InputBase<TValue>
{
[Parameter]
public string Id { get; set; } = "ESelect";
[Parameter]
public List<MySelectOption<TValue>> Options { get; set; }
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
throw new NotImplementedException();
}
public void OnClick(TValue value)
{
Value = value;
}
}
}
And then in Index.razor:
<MySelect Options="#options" #bind-Value="AddDto.Description" TValue="string">
So when using the component I should be able to bind it to a property of any type (but usually int or string) which I pass as the type param TValue.
However, the line below is causing an issue:
<button #onclick="OnClick(option.Value)">#option.Value</button>
Argument 2: cannot convert from 'void' to 'Microsoft.AspNetCore.Components.EventCallback'
How can I pass the option.Value (which is always a string) to the onCLick event? Or alternatively modify the code above so that I can accomplish my initially stated goal?
You have more that one issue, but the important one is trying to update Value. Value is an "input" into the control. The updated value is passed back to parent by calling ValueChanged. However, calling ValueChanged directly bypasses the built in functionality in InputBase and it's interaction with the EditContext.
This demonstrates the basics of inheriting from InputBase.
To leverage the built in functionality, you need to either:
Set the value by setting CurrentValueAsString from the markup and then providing a custom TryParseValueFromString to convert from a string to your type (there's a BindConverter helper you can use - it's what InputNumber and other input controls use).
Set the value directly by setting CurrentValue. This bypasses TryParseValueFromString.
Your MySelect.
I've prettied up your buttons and abstracted your list to an IEnumerable.
#typeparam TValue
#inherits InputBase<TValue>
#using Microsoft.AspNetCore.Components.Forms;
#using Microsoft.AspNetCore.Components;
#using System.Diagnostics.CodeAnalysis;
<div class="btn-group" role="group">
#foreach (var option in Options)
{
<button class="#btnColour(option.Value)" #onclick="() => OnClick(option.Value)">#option.Value</button>
}
</div>
#code {
[Parameter] public IEnumerable<MySelectOption<TValue>> Options { get; set; } = new List<MySelectOption<TValue>>();
private string btnColour(TValue? value)
{
if (this.Value is null)
return "btn btn-outline-primary";
return this.Value.Equals(value)
? "btn btn-primary"
: "btn btn-outline-primary";
}
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
throw new NotImplementedException();
}
public void OnClick(TValue? value)
{
CurrentValue = value;
}
}
And then here's a demo page to show it in use.
#page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
<EditForm Model=#model>
<MySelect Options="#options" #bind-Value=model.Description TValue="string" />
</EditForm>
<div class="alert alert-info m-3">
Description: #model.Description
</div>
#code {
private Model model = new();
IEnumerable<MySelectOption<string>> options =
new List<MySelectOption<string>>() {
new MySelectOption<string> { Id = 1, Value = "France" },
new MySelectOption<string> { Id = 1, Value = "Spain" },
new MySelectOption<string> { Id = 1, Value = "Portugal" },
};
public class Model
{
public string? Description { get; set; }
}
}
For reference you can find the source code for all the standard InputBase controls here: https://github.com/dotnet/aspnetcore/tree/main/src/Components/Web/src/Forms
With help and suggestions from previous answers, below is the solution I arrived at:
Index.razor
<MySelect Options="#options" #bind-Value="AddDto.InvestmentEntityId">
</MySelect>
#AddDto.InvestmentEntityId // integer property
<MySelect Options="#options" #bind-Value="AddDto.Description">
</MySelect>
#AddDto.Description // string property
MySelect.razor
#typeparam TValue
#inherits InputBase<TValue>
#namespace Accounting.Web.Components
#foreach (var option in Options)
{
<button #onclick="() => OnClick(option.Value)">#option.Value</button>
}
MySelect.razor.cs
namespace Accounting.Web.Components
{
public partial class MySelectOption
{
public int Id { get; set; }
public string Value { get; set; }
}
public partial class MySelect<TValue> : InputBase<TValue>
{
[Parameter]
public List<MySelectOption> Options { get; set; }
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
if (BindConverter.TryConvertTo<TValue>(value, null, out result))
{
validationErrorMessage = null;
}
else
{
validationErrorMessage = "Err : Select value";
}
}
public void OnClick(string value)
{
TValue tmpValue;
BindConverter.TryConvertTo<TValue>(value, null, out tmpValue);
CurrentValue = tmpValue;
}
}
}
It's probably not perfect but I hope it helps anyone looking to do the same.

List parameter on child component not updating

I have a child component for filtering a search (DropdownFilter) which takes an input of a list of suggestions and a function to update that list.
For some reason DropdownFilter.Suggestions isn't being updated after it is initially set and I don't know how to update it again. Any information about how to update the property after it is initially bound would be great!
DropdownFilter.razor:
<input id="search" #onfocus="SearchFocused" #onblur="SearchUnfocused" #oninput="UpdateSearchText" />
#foreach (var suggestion in Suggestions)
{
<p>#suggestion</p>
}
#code {
[Parameter]
public Action<string> SearchFieldChanged { get; set; }
//[Parameter]
//public RenderFragment<TSuggestion> SuggestionTemplate { get; set; }
[Parameter]
public List<string> Suggestions { get; set; }
private bool searchFocus = false;
private void SearchFocused(FocusEventArgs args) {
searchFocus = true;
//UpdateSearchText();
}
private void SearchUnfocused(FocusEventArgs args) => searchFocus = false;
private void UpdateSearchText(ChangeEventArgs args)
{
SearchFieldChanged.Invoke((string)args.Value);
}
public void Refresh() {
StateHasChanged();
}
}
Index.razor:
#page "/example"
<div class="container-fluid dropdown-holder">
<DropdownFilter #ref="dropdown" Suggestions="#maskResults" SearchFieldChanged="UpdateSearchResults" />
</div>
#code {
DropdownFilter dropdown;
public class MaskResult {
public string name;
}
static readonly string[] allMasks = {
"Electric",
"Water",
"Ground",
"Fire",
"Bug"
};
public List<string> maskResults = allMasks.ToList();
private void UpdateSearchResults(string search)
{
search = search.ToLower();
maskResults = allMasks.Where((mask) =>
{
return mask.ToLower().StartsWith(search);
}).ToList();
dropdown.Refresh();
}
}
I think that you are trying to create a Datalist, please check this answer:"
datalist How to bind selected item to object
If you add a StateHasChanged() call just here it should work:
private void UpdateSearchResults(string search)
{
search = search.ToLower();
maskResults = allMasks.Where((mask) =>
{
return mask.ToLower().StartsWith(search);
}).ToList();
StateHasChanged(); // Add this line
dropdown.Refresh();
}
As I understand, if you update manually a Parameter of a component, there are some cases where Blazor does not get automatically the info that it needs to update its components. So if you call StateHasChanged, it will reevaluate all Parameters of the childreen of the component where you do the call.
I'll let someone correct me if I am wrong.
Thanks and good luck :)

Blazor Howto - looking for winform MessageBox() alike function

I m using VS 2019 and Blazor Server Net5. The included bootstrap 4 environment offers modal dialogs and I would like to replicate something like the MessageBox() from good old Windows Forms. This means you execute (withhin a button click event) something like
DialogResult x = [await] MessageBox[Async]("SomeMsg", SomeButtons+SomeIcon);
And in x you would find what option the user did click (DialogResult.OK, Cancel, ...).
So I found several samples how to show the model dialog itself, and write it even as a component.
I have a simple component MessageBox.razor
public enum ModalResultType { Closed = 0, OK = 1, Cancel = 2, }
#if (ShowMessageBox == true)
{
<div class="modal fade show d-block" id="MessageBox" tabindex="-1"
role="dialog" aria-hidden="true">
.... and so forth ....
<button type="button" class="close" data-dismiss="modal" aria-label="Close"
#onclick="() => OnButtonClick(ModalResultType.Closed)">X</button>
... and so forth ...
<button type="button" class="btn btn-primary" data-dismiss="modal"
#onclick="() => OnButtonClick(ModalResultType.OK)">OK</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal"
#onclick="() => OnButtonClick(ModalResultType.Cancel)">Cancel</button>
.. and so forth ...
</div>
}
and in the cs behind file I can turn on the component show flag and display the component.
public async Task<ModalResultType> ShowAsync(string title, string messagetext)
{
Title = title;
Message = messagetext;
ShowMessageBox = true;
StateHasChanged();
//
// Now I m at a loss... how to await here what the User did click???
//
return whatTheUserDidClick;
}
// Click event from button, called with the appropiate ModalResultType
//
public async Task OnButtonClicked(ModalResultType value)
{
ShowMessageBox = false;
//
// Now I am at a loss - how to pass the clicked value into the waiting context
// of the UI above from and "complete" the awaiting ShowAsync();
//
}
The overall idea is to put into the framing App.Razor-Component this MessageBox component and so every "page" or other component has (via cascading parameter) access to the message box. And if it would need to create a modal MessageBox dialog, for example from some button click event, it could simply do so by calling
[CascadingParameter] public MessageBoxComponent AppRazorMessageComonent {get;set;}
public async Task SomeClickEvent()
{
// get some form data
// process them
// question arises ask user if to proceed or defer
if (await AppRazorMessageComponent.ShowAsync("Attention", "Shall we proceed?") == ModalResultType.OK)
{
// do stuff
}
else
{
// do other stuff
}
}
I found sample of modal dialogs where the event handler then is bound right to action to be carried out - like deleting a record. But this is not what I want - I would need to specifically bind the html always to the specifics of the page or component I m in at the time. Or I would need to supply a callback function, which would break my current track; like to set the ShowMessageBox flag, return from the click event, and then proceed along with the logic in another method.
So the question is: how can I await for an event withhin an event handler, which is triggered by an other UI event?
Do I need threads for this - I dont think so. It should be possible by Task, async and await only. But how to create an awaitable object, "signal" the completion, or cancellation, of such a Task? And in a way that it works withhin the Blazor UI component environment.
I made use of the System.Threading.SemaphoreSlim class to acheive the awaitable result in ValueTask<ModalResult<T>> OpenModal() in Modal.cs
BlazorRepl
ModalLauncher.razor
<CascadingValue Value="this">
#if (ModalContent is not null)
{
#ModalContent
}
#ChildContent
</CascadingValue>
ModalLauncher.razor.cs
using Microsoft.AspNetCore.Components;
public partial class ModalLauncher : ComponentBase
{
[Parameter]
public RenderFragment ChildContent { get; set; }
public RenderFragment ModalContent { get; set; }
public void ShowModal(RenderFragment renderFragment)
{
ModalContent = renderFragment;
StateHasChanged();
}
public void CloseModal()
{
ModalContent = null;
StateHasChanged();
}
}
Wrap this around your Layout.
#inherits LayoutComponentBase
<ModalLauncher>
<div class="page">
...
</div>
</ModalLauncher>
Modal.cs
public class Modal<T> : ComponentBase
{
[Parameter]
public RenderFragment<ModalContext<T>> ChildContent { get; set; }
[Parameter]
public T Value { get; set; }
[CascadingParameter]
public ModalLauncher Launcher { get; set; }
public async ValueTask<ModalResult<T>> OpenModal(T value)
{
var modalContext = new ModalContext<T> { Modal = this, Value = value };
RenderFragment renderFragment = ChildContent.Invoke(modalContext);
Launcher.ShowModal(renderFragment);
await semaphore.WaitAsync();
return new ModalResult<T> { ModalAction = modalAction, Value = value };
}
public void CancelModal() => CloseModal(ModalAction.Cancel);
public void CloseModal() => CloseModal(ModalAction.Close);
public void OkModal() => CloseModal(ModalAction.Ok);
private void CloseModal(ModalAction action)
{
modalAction = action;
Launcher.CloseModal();
semaphore.Release();
}
private ModalAction modalAction;
private SemaphoreSlim semaphore = new SemaphoreSlim(0, 1);
}
public enum ModalAction
{
Cancel,
Close,
Ok,
}
public class ModalContext<T>
{
public T Value { get; set; }
public Modal<T> Modal { get; set; }
}
public class ModalResult<T>
{
public T Value { get; set; }
public ModalAction ModalAction { get; set; }
}
public class SomeClass
{
public int SomeValue { get; set; }
}
Usage
Note: When I define the modals I only use a type, They are not bound to an instance. When you call OpenModal(...) you can pass an instance then.
#page "/"
<button #onclick="#OpenSomeClassModal">Run Demo</button>
<Modal #ref="someClassModal" T="SomeClass">
...
<input type="number" #bind-value="#context.Value.SomeValue" />
...
<button type="button" class="btn btn-secondary" #onclick="#context.Modal.CancelModal">Cancel</button>
<button type="button" class="btn btn-primary" #onclick="#context.Modal.OkModal">Save changes</button>
...
</Modal>
<Modal #ref="someStringModal" T="string">
...
<p> #context.Value</p>
...
<button type="button" class="btn btn-secondary" #onclick="#context.Modal.OkModal">Close</button>
...
</Modal>
#code {
Modal<SomeClass> someClassModal;
Modal<string> someStringModal;
async Task OpenSomeClassModal()
{
var someClass = new SomeClass { SomeValue = 9 };
var result1 = await someClassModal.OpenModal(someClass);
var result2 = await someStringModal.OpenModal($"The value was set to {result1.Value.SomeValue}, you pressed {result1.ModalAction}");
}
}
You also need to override part of the bootstrap .modal class. Put this in wwwroot\css\app.css:
.modal {
display: block;
}

ASP.NET - Blazor - returning clicked generic type from templated component

I'm learning Blazor and ASP.NET and have been learning C# for the last 6 months.
I have made a simple templated component:
#typeparam GenericType
<ul>
#foreach (GenericType item in Items)
{
<li #onclick="(x)=> ItemClicked(item)">FragmentToRender(item)</li>
}
</ul>
#code {
[Parameter] public RenderFragment<GenericType> FragmentToRender { get; set; }
[Parameter] public IReadOnlyList<GenericType> Items { get; set; }
public void ItemClicked(GenericType item)
{
//To figure out...
}
}
And I'm using it in a page component:
<TestComponent GenericType="Thing" Items="ListOfThings">
<FragmentToRender>
<p>#context.Field</p>
</FragmentToRender>
</TestComponent>
#code
{
private List<Thing> ListOfThings =
new List<Thing> {
new Thing("Test"),
new Thing("Test2")
};
public class Thing
{
public readonly string Field;
public Thing(string field) => Field = field;
}
}
When the OnClick event of the li element in the component is triggered, how can I pass the specific instance of the item back to the page component (i.e so a different component can do something with the clicked item like upload it's data somewhere)?
Many Thanks
You should use an EventCallback to pass the data.
#typeparam GenericType
<ul>
#foreach (GenericType item in Items)
{
<li #onclick="(x)=> ItemClicked(item)">FragmentToRender(item)</li>
}
</ul>
#code {
[Parameter] public RenderFragment<GenericType> FragmentToRender { get; set; }
[Parameter] public IReadOnlyList<GenericType> Items { get; set; }
// Added EventCallback parameter
[Parameter] public EventCallback<GenericType> OnClick { get; set; }
public void ItemClicked(GenericType item)
{
// Checking if EventCallback is set
if(OnClick.HasDelegate)
{
// Calling EventCallback
OnClick.InvokeAsync(item);
}
}
}
And then just pass the parameter OnClick to that component to get the item
#* Passing the OnClick parameter *#
<TestComponent GenericType="Thing" Items="ListOfThings" OnClick="#HandleClick">
<FragmentToRender>
<p>#context.Field</p>
</FragmentToRender>
</TestComponent>
#code
{
private void HandleClick(Thing item)
{
// Do what you want with the item
}
private List<Thing> ListOfThings =
new List<Thing> {
new Thing("Test"),
new Thing("Test2")
};
public class Thing
{
public readonly string Field;
public Thing(string field) => Field = field;
}
}
Note: I've made some alteration in your code sample...
Point to note:
Add a # symbol before FragmentToRender(item). It instructs the compiler to treat FragmentToRender(item) as executable code. Otherwise, it is used as the content of the li element.
In the second version of the li element, we place the the event call back in
the body of the lambda expression. If you use this version, comment out the
ItemClicked method.
TemplatedComponent.razor
#typeparam GenericType
<ul>
#foreach (GenericType item in Items)
{
<li #onclick="() => ItemClicked(item)">#FragmentToRender(item)</li>
#*<li #onclick="#(() => SelectedItem.InvokeAsync(item))">#FragmentToRender(item)</li>*#
}
</ul>
#code {
[Parameter] public RenderFragment<GenericType> FragmentToRender { get; set; }
[Parameter] public IReadOnlyList<GenericType> Items { get; set; }
// Define an event call back property.
[Parameter] public EventCallback<GenericType> SelectedItem { get; set; }
public async Task ItemClicked(GenericType item)
{
// Check if the event call back property contains a delegate. It's
// important to understand that the EventCallback type is not a true
// delegate. It is actually a struct that may contain a delegate
if(SelectedItem.HasDelegate)
{
await SelectedItem.InvokeAsync(item);
}
}
}
TestComponent.razor
<TemplatedComponent GenericType="Thing" Items="ListOfThings"
SelectedItem="SelectedItem">
<FragmentToRender>
<p>#context.Field</p>
</FragmentToRender>
</TemplatedComponent>
#code
{
// Define a method that will be called by the event call back 'delegate'. It
// receives a single parameter from the calling delegate.
private async Task SelectedItem(Thing item)
{
Console.WriteLine(item.Field);
await Task.CompletedTask;
}
private List<Thing> ListOfThings =
new List<Thing> {
new Thing("Test"),
new Thing("Test2")
};
public class Thing
{
public readonly string Field;
public Thing(string field) => Field = field;
}
}
Index.razor
#page "/"
<TestComponent/>
Hope this helps...

Blazor Server side, set parent class name based on child class name

I have a EditForm with MatBlazor Expansion panels. I am trying to set the individual panels border color if any of the form fields are invalid inside it. I am trying to achieve following..
<MatExpansionPanel Class="#(<bool>ChildElementHasValidationMessage() ? "invalid-red-border": "")">.....</MatExpansionPanel>
I am OK with simple equivalent css solution to find a parent element. Please advice.
Just use Style instead Class to override css:
<MatExpansionPanel Style="#(your expression);"
I answered a related question about MatExpansionPanel: Mat Blazor mat-expansion-panel remove elevation/border
Edited
I wrote my own component to send EditContext on changes. I pasted it below. This is how it works:
<h1>#ShowDemo</h1>
<EditForm Model="#model" OnValidSubmit="#SaveItem">
<DataAnnotationsValidator />
<ValidationSummary />
<MyValidationSneak UpdateDelegate="#( (ctx)=>UpdateUI(ctx) )" />
<InputText id="ItemName" #bind-Value="#model.ItemName" />
<ValidationMessage For="#(() => model.ItemName)" />
<button type="submit">Submit</button>
</EditForm>
#code {
string ShowDemo = "res de res";
protected void UpdateUI(EditContext ctx)
{
var _fieldIdentifier = FieldIdentifier.Create( () => model.ItemName );
ShowDemo = string.Join(", ", ctx.GetValidationMessages(_fieldIdentifier) );
}
ItemModel model = new ItemModel();
private void SaveItem() { }
public class ItemModel
{
[Required]
public string ItemName{ get; set; }
}
}
See it in action at BlazorFiddle:
The MyValidationSneak:
public class MyValidationSneak: ComponentBase, IDisposable
{
[CascadingParameter] EditContext CurrentEditContext { get; set; }
[Parameter] public Action<EditContext> UpdateDelegate { get; set; }
private readonly EventHandler<ValidationStateChangedEventArgs> _validationStateChangedHandler;
public MyValidationSneak()
{
_validationStateChangedHandler = (sender, eventArgs) => GoUpdate();
}
private void GoUpdate() => UpdateDelegate(CurrentEditContext);
private EditContext _previousEditContext;
protected override void OnParametersSet()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(ValidationSummary)} requires a cascading parameter " +
$"of type {nameof(EditContext)}. For example, you can use {nameof(ValidationSummary)} inside " +
$"an {nameof(EditForm)}.");
}
if (CurrentEditContext != _previousEditContext)
{
DetachValidationStateChangedListener();
CurrentEditContext.OnValidationStateChanged += _validationStateChangedHandler;
GoUpdate();
_previousEditContext = CurrentEditContext;
}
}
protected virtual void Dispose(bool disposing) {}
void IDisposable.Dispose()
{
DetachValidationStateChangedListener();
this.Dispose(disposing: true);
}
private void DetachValidationStateChangedListener()
{
if (_previousEditContext != null)
{
_previousEditContext.OnValidationStateChanged -= _validationStateChangedHandler;
GoUpdate();
}
}
}
Get code at github

Resources