DynamicComponent only works when method is in same component as DynamicComponent - c#

My problem is following:
I need to display content dynamically according to a method from a different component than the
the DynamicComponent tag is in.
Is this by design or did I do sth wrong?
My code:
Platform.razor:
<a href="" #onclick:preventDefault #onclick="#(()=>ChangePage("ListLinks"))>
public Type? selectedPage = typeof(Empty);
public void ChangePage(string page)
{
selectedPage = page.Length > 0 ? Type.GetType($"Namespace.Shared.{page}") : null;
}
Home.razor:
Platform platform = new Platform();
<DynamicComponent Type="#platform.selectedPage"></DynamicComponent>
When I call the method inside the same component, everything works but it's not what I need.

This line:
Platform platform = new Platform();
creates a new instance of Platform which is unconnected with the instance created by the Renderer. You do not manage component instances.
To communicate between components you can use a DI service.
Here's some code that demonstrates something like what you want to do:
First a service:
public class PlatformService
{
public Type? Component { get; private set; }
public event EventHandler? ComponentChanged;
public void SetComponent(Type type)
{
this.Component= type;
this.ComponentChanged?.Invoke(this, EventArgs.Empty);
}
}
Registered in Program as scoped so it persists for the lifetime of the user SPA instance.
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddScoped<PlatformService>();
A component to change the component type:
#inject PlatformService service
<h3>ComponentSelector</h3>
<div class="m-2 p-2">
<button class="btn btn-primary" #onclick="() => Change(typeof(Counter))"> Counter </button>
<button class="btn btn-primary" #onclick="() => Change(typeof(FetchData))"> FetchData </button>
</div>
#code {
private void Change(Type type)
=> service.SetComponent(type);
}
And Index to demo it. Note it implements an event handler registered on the service event to drive the UI update.
#page "/"
#inject PlatformService service
#implements IDisposable
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<ComponentSelector />
<SurveyPrompt Title="How is Blazor working for you?" />
#if (service.Component is not null)
{
<DynamicComponent Type="service.Component" />
}
#code {
protected override void OnInitialized()
=> this.service.ComponentChanged += this.OnUpdated;
private void OnUpdated(object? sender, EventArgs e)
=> this.InvokeAsync(StateHasChanged);
public void Dispose()
=> this.service.ComponentChanged -= this.OnUpdated;
}

As you noticed, if you have the method inside the same component it works, and as soon as you put it to the child level, it doesn't. And that is by design.
Let's assume you have the following structure:
Parent.razor
<h1>#Text</h1>
<Child Text="#text">/
#code {
private string Text = "Hello World";
}
And you update text inside your child, the parent component will not see the change:
Child.razor
<button #onclick="() =>Text = string.Empty">Click Me</button>
#code {
[Parameter]
public string Text { get; set; }= "";
}
So clicking on "Click Me" will not change the header in your parent component. The same applies to your example.
Now, how do we make this work? The most straightforward approach would be to have an event, fired when your dynamic component changes, on which the parent component listens. For that, you can also use a event (this special form is a two-way-binding: https://blazor-university.com/components/two-way-binding/)
[Parameter]
public Type SelectedPage { get; set; } = typeof(Empty);
[Parameter]
public EventCallback<Type> SelectedPageChanged { get; set; }
public async Task ChangePage(string page)
{
selectedPage = page.Length > 0 ? Type.GetType($"Namespace.Shared.{page}") : null;
await SelectedPageChanged.InvokeAsync(selectedPage);
}
I am not sure if the following will work with DynamicComponents.
<DynamicComponent #bind-Type="#platform.selectedPage"></DynamicComponent>
If that doesn't work you can fallback to this:
Platform.razor
<DynamicComponent Type="#platform.SelectedPage" Parameters="#dynamicParameters"/>
#code {
private Dictionary<string, object> dynamicComponentParameters = new();
protected override void OnInitialized()
{
dynamicComponentParameters.Add("SelectedPageChanged", EventCallback.Factory.Create<Type>(this, SelectedTypeChanged));
}
private void SelectedTypeChanged(Type newType)
{
// After the method a re-render will be triggered
platForm.SelectedPage = newType;
}
}

Related

Blazor EventCallback parameter not firing

I am trying to get a child component to update a list on the parent. To do this I setup an EventCallback that takes a list and sends it to the parent. The issue is the event never fires and the HasDelegate variable on the callback is false.
Parent .razor.cs:
public async Task UpdateSelectedCompanies(List<CompanyStub> companies)
{
_selectedCompanies = companies;
await InvokeAsync(StateHasChanged);
}
Parent .razor:
<CompanyTable IncludeCheckbox="true" UpdateCompanies="#UpdateSelectedCompanies"></CompanyTable>
Child .razor.cs:
[Parameter] public EventCallback<List<CompanyStub>> UpdateCompanies { get; set; }
private async Task CheckboxRowSelectHandler(RowSelectEventArgs<CompanyStub> args)
{
SelectedCompanies.Add(args.Data);
await UpdateCompanies.InvokeAsync(SelectedCompanies);
}
CheckboxRowSelectHandler does get called, but the UpdateCompanies event never fires.
I am expecting for the event to be fired, but the event never gets fired.
Here's a simplified version of your code that works. Use it to test your child/parent.
One thing to check: Is your razor.cs file correctly linked to the razor component file?
ChildComponent.razor
<div class="m-2 p-2 bg-light">
<h3>ChildComponent</h3>
<button class="btn btn-primary" #onclick=this.OnClick>Click me</button>
</div>
#code {
[Parameter] public EventCallback<string> ValueChanged { get; set; }
private async Task OnClick()
=> await this.ValueChanged.InvokeAsync($"Set at {DateTime.Now.ToLongTimeString()}");
}
#page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<ChildComponent ValueChanged="#this.OnClicked" />
#if (!string.IsNullOrWhiteSpace(this.message))
{
<div class="alert alert-info">
#message
</div>
}
#code {
private string message = string.Empty;
// No call to StateHasChanged required
// The ComponentBase UI handler does it
private void OnClicked(string value)
=> message = value;
}

Passing Cascading Parameters to Blazor component that rendered from JavaScript

I want to pass a CascadingParameter to the Counter page component, but I don't want to use the router. Instead of that, I want to use JavaScript because I want the component to be rendered inside the window of Winbox.js (open-source HTML5 window manager).
The problem is that Cascading Parameters are initialized as null, I think it's happened due to the difference in RenderTree.
Here is my code:
Registration of Counter.razor component for JavaScript in Program.cs:
builder.RootComponents.RegisterForJavaScript<Counter>(identifier: "counter");
For MainLayout.razor
<!-- omitted for brief -->
<CascadingValue Value="_TestCascadingParameter">
<article class="content px-4">
#Body
</article>
</CascadingValue>
<!-- omitted for brief -->
#code{
private TestCascadingParameter _TestCascadingParameter = new() { GlobalCounter = 5 };
public class TestCascadingParameter
{
public int GlobalCounter { get; set; }
}
For Counter page:
#page "/counter"
#using static BlazorApp1.Shared.MainLayout
<PageTitle >Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: #currentCount</p>
<button class="btn btn-primary" #onclick="IncrementCount">Click me</button>
#code {
[CascadingParameter]
public TestCascadingParameter TestCascadingParameter { get; set; }
private int currentCount = 0;
protected override void OnParametersSet()
{
currentCount = TestCascadingParameter.GlobalCounter;
}
private void IncrementCount()
{
currentCount++;
}
}
For Winbox page:
#page "/WinBox"
#layout MainLayout
#inject IJSRuntime _Js
<PageTitle>Win box</PageTitle>
<button #onclick="#OpenWinAsync">Open window</button>
#code{
async Task OpenWinAsync()
{
await _Js.InvokeVoidAsync("MyWinBox.OpenTestWindow");
}
}
And for MyWinBox.js:
window.MyWinBox = {
OpenTestWindow: async () => {
let containerElement = new WinBox("WinBox.js");
await Blazor.rootComponents.add(containerElement.body, 'counter', {});
}
};
When I navigate to Counter page using router everything works fine and the currentCount starts from 5, but when I use JavaScript to render it, it throws a NullReferenceException on TestCascadingParameter.
How to solve this without passing Cascading Parameter value through JavaScript because sometimes the Cascading Parameter has a null value in the caller component as in the above example (eg: if caller component is MudDialog) or sometimes there is more than one Cascading Parameter.
RegisterForJavaScript is new, not well-documented feature and I can't tell if it's possible to pass cascading parameter like that. But I think that you can use State container (or something like Fluxor) to achieve your goals:
public class StateContainer
{
private int? globalCounter;
public int Property
{
get => globalCounter;
set
{
globalCounter = value;
NotifyStateChanged();
}
}
public event Action? OnChange;
private void NotifyStateChanged() => OnChange?.Invoke();
}
More details here: https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management?view=aspnetcore-6.0&pivots=webassembly

Issues with using cascading parameters for passing values

I have two blazor components the first component calls a method
<CascadingValue Value="this">
#ChildContent
</CascadingValue>
#code{
[Parameter] public RenderFragment? ChildContent { get; set; }
public Debtor? CurrentDebtor { get; private set; }
public async Task<Debtor> ConsolitdateCurrentDebtor(int debtorID)
{
Debtor? currentDebtor = null;
if (DebtorStoreList != null && DebtorStoreList.Count > 0)
{
currentDebtor = DebtorStoreList.Where(d => d.ID == debtorID).Single();
}
else
{
currentDebtor = await _debtorService.GetDebtorInformation();
}
CurrentDebtor = currentDebtor;
// StateHasChanged(); unless called no update takes place
return currentDebtor;
}
}
Now I have a property called CurrentDebtor in this AppState class. One blazor component comes and calls this method and sets the Current Debtor.
From the other component, i am doing something like
#code {
Debtor? debtor;
[CascadingParameter]
AppState AppState{ get; set; }
protected override async Task OnInitializedAsync()
{
debtor = AppState.CurrentDebtor;
}
}
However the CurrentDebtor is coming null, the only way it doesn't come null is if StateHasChanged() is called. Is there anyway without forcing it to call Statehaschanged to populate it correctly?
Trying to "wire" multiple components together using cascading parameters and two way binding isn't always the best way to do things, particularly sub-component to sub-component within a page (which it appears you are trying to do from the information shown in the question).
There is an alternative approach using the standard event model.
My Debtor class
public class Debtor
{
public Guid Id { get; set; }
public string? Name { get; set; }
}
A Debtor view service:
public class DebtorViewService
{
public Debtor Record { get; private set; } = default!;
public event EventHandler<Debtor>? DebtorChanged;
public DebtorViewService()
{
// Get and assign the Debtor Store DI service
}
public async ValueTask GetCurrentDebtor(Guid debtor)
{
// Emulate getting the Debtor from the Debtor Service
await Task.Delay(1000);
this.Record = new Debtor() { Name=$"Fred {DateTime.Now.ToLongTimeString()}", Id = Guid.NewGuid()};
this.DebtorChanged?.Invoke(this, this.Record);
}
}
Registered in services like this:
builder.Services.AddScoped<DebtorViewService>();
A Component to get the Debtor:
GetDebtor.razor
#inject DebtorViewService Service
<div class="m-2 p-3 bg-dark text-white">
<h5>GetDebtor Component</h5>
<div>
<button class="btn btn-secondary" #onclick=this.GetCurrentDebtor>Get Debtor</button>
</div>
</div>
#code {
private async Task GetCurrentDebtor()
=> await Service.GetCurrentDebtor(Guid.Empty);
}
A component to show the Debtor:
ShowDebtor.razor
#inject DebtorViewService Service
#implements IDisposable
<div class="m-2 p-3 bg-info">
<h5>ShowDebtor Component</h5>
#if (this.Service.Record is not null)
{
<div>
Id : #this.Service.Record.Id
</div>
<div>
Name : #this.Service.Record.Name
</div>
}
</div>
#code {
protected override void OnInitialized()
=> this.Service.DebtorChanged += this.OnDebtorChanged;
private void OnDebtorChanged(object? sender, Debtor value)
=> this.InvokeAsync(this.StateHasChanged);
public void Dispose()
=> this.Service.DebtorChanged -= this.OnDebtorChanged;
}
And a demo page:
#page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<GetDebtor />
<ShowDebtor />
<SurveyPrompt Title="How is Blazor working for you?" />
#code{
}
The data and the change event is now in a single shared (Scoped) DI service instance that everyone has access to.
You should be able to build your components based on this code.

Is it possible to load dynamically existing page in a Bootstrap Modal body

I'm new to a Blazor and now I'm working to Blazor WebAssembly project.
I have couple razor page with a table where I'm displaying data from SQL. When I click in one of the table rows it opens a page where I can do the CRUD operation.
Now, instead of opening a page to do the CRUD operation I need to open a bootstarp modal and do the CRUD operation.
I'm doing a generic ModalComponent where I have the header and the footer of the modal.
Is it possibile to load dynamically the body of the model, which in this case it will be the CRUD operation pages that I already had done?
The code below demonstrates how to implement a generic modal dialog i.e. a wrapper for any component/page that you want to display in modal mode. The concrete implementation of IModalDialog shows how to implement a Bootstrap version.
I use a more complex version of this. My edit/View components are written to run in modal or full page mode.
Support Classes
First three support classes that are fairly self-evident:
public class ModalResult
{
public ModalResultType ResultType { get; private set; } = ModalResultType.NoSet;
// Whatever object you wish to pass back
public object? Data { get; set; } = null;
// A set of static methods to build a BootstrapModalResult
public static ModalResult OK() => new ModalResult() { ResultType = ModalResultType.OK };
public static ModalResult Exit() => new ModalResult() { ResultType = ModalResultType.Exit };
public static ModalResult Cancel() => new ModalResult() { ResultType = ModalResultType.Cancel };
public static ModalResult OK(object data) => new ModalResult() { Data = data, ResultType = ModalResultType.OK };
public static ModalResult Exit(object data) => new ModalResult() { Data = data, ResultType = ModalResultType.Exit };
public static ModalResult Cancel(object data) => new ModalResult() { Data = data, ResultType = ModalResultType.Cancel };
}
public class ModalOptions
{
// Whatever data you want to pass
// my complex version uses a <string, object> dictionary
}
public enum ModalResultType
{
NoSet,
OK,
Cancel,
Exit
}
IModalDialog Interface
Now the modal dialog Interface.
using Microsoft.AspNetCore.Components;
public interface IModalDialog
{
ModalOptions Options { get; }
// Method to display a Modal Dialog
Task<ModalResult> ShowAsync<TModal>(ModalOptions options) where TModal : IComponent;
// Method to update the Modal Dialog during display
void Update(ModalOptions? options = null);
// Method to dismiss - normally called by the dismiss button in the header bar
void Dismiss();
// Method to close the dialog - normally called by the child component TModal
void Close(ModalResult result);
}
The Base Bootstrap Implementation
There's no JS.
I use a TaskCompletionSource to make the open/close an async process.
The component to display is passed as part of the open call.
#inherits ComponentBase
#implements IModalDialog
#if (this.Display)
{
<CascadingValue Value="(IModalDialog)this">
<div class="modal" tabindex="-1" style="display:block;">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" #onclick="() => Close(ModalResult.Exit())"></button>
</div>
<div class="modal-body">
#this.Content
</div>
</div>
</div>
</div>
</CascadingValue>
}
#code {
public ModalOptions Options { get; protected set; } = new ModalOptions();
public bool Display { get; protected set; }
protected RenderFragment? Content { get; set; }
protected TaskCompletionSource<ModalResult> _ModalTask { get; set; } = new TaskCompletionSource<ModalResult>();
public Task<ModalResult> ShowAsync<TModal>(ModalOptions options) where TModal : IComponent
{
this.Options = options ??= this.Options;
this._ModalTask = new TaskCompletionSource<ModalResult>();
this.Content = new RenderFragment(builder =>
{
builder.OpenComponent(1, typeof(TModal));
builder.CloseComponent();
});
this.Display = true;
InvokeAsync(StateHasChanged);
return this._ModalTask.Task;
}
public void Update(ModalOptions? options = null)
{
this.Options = options ??= this.Options;
InvokeAsync(StateHasChanged);
}
public async void Dismiss()
{
_ = this._ModalTask.TrySetResult(ModalResult.Cancel());
this.Display = false;
this.Content = null;
await InvokeAsync(StateHasChanged);
}
public async void Close(ModalResult result)
{
_ = this._ModalTask.TrySetResult(result);
this.Display = false;
this.Content = null;
await InvokeAsync(StateHasChanged);
}
}
Demo
A simple "Edit" component:
#inject NavigationManager NavManager
<h3>EditForm</h3>
<div class="p-3">
<button class="btn btn-success" #onclick=Close>Close</button>
</div>
#code {
[CascadingParameter] private IModalDialog? modal { get; set; }
private void Close()
{
if (modal is not null)
modal.Close(ModalResult.OK());
else
this.NavManager.NavigateTo("/");
}
}
And a test page:
#page "/"
<div class="p-2">
<button class="btn btn-primary" #onclick=OpenDialog>Click</button>
</div>
<div class="p-2">
result: #Value
</div>
<ModalDialog #ref=this.modal />
#code {
private string Value { get; set; } = "Fred";
private IModalDialog? modal;
private IModalDialog Modal => modal!;
private async void OpenDialog()
{
var options = new ModalOptions();
var ret = await Modal.ShowAsync<EditorForm>(new ModalOptions());
if (ret is not null)
{
Value = ret.ResultType.ToString();
}
StateHasChanged();
}
}

Having trouble getting Blazor to rerender component on change in state shared between components

Context
I'm following a pattern that's something like https://chrissainty.com/3-ways-to-communicate-between-components-in-blazor/ or https://jonhilton.net/blazor-state-management/
So I have two razor components Hen.razor and Basket.razor as child components inside index.razor. A button inside Hen adds to the number of eggs displayed inside Basket.
The button calls a service I've injected called EggService that handles the number of eggs in Basket by storing it in local storage using Blazored.LocalStorage.
Problem
Clicking the button increases the number of eggs in local storage but doesn't update the Basket component unless I refresh.
Code
Repository for convenience: https://github.com/EducatedStrikeCart/EggTest/
EggService:
using Blazored.LocalStorage;
namespace BlazorSandbox.Services
{
public class EggService
{
public event Action OnChange;
private readonly ILocalStorageService _localStorageService;
public EggService(ILocalStorageService localStorageService)
{
this._localStorageService = localStorageService;
}
public async Task<int> GetEggs()
{
int currentEggs = await _localStorageService.GetItemAsync<int>("Eggs");
return currentEggs;
}
public async Task AddEgg()
{
int newEggs = await GetEggs();
if (newEggs == null)
{
newEggs = 0;
} else
{
newEggs += 1;
}
await _localStorageService.SetItemAsync("Eggs", newEggs);
OnChange?.Invoke();
}
}
}
Hen:
#using BlazorSandbox.Services
#inject EggService EggService
<div>
<h3>Hen</h3>
<button #onclick="TakeAnEgg">Take an egg</button>
</div>
#code {
public async Task TakeAnEgg()
{
await EggService.AddEgg();
}
}
Egg:
#using BlazorSandbox.Services
#inject EggService EggService
#implements IDisposable
<div>
<h3>Basket</h3>
Eggs: #Eggs
</div>
#code {
public int Eggs { get; set; }
protected override async Task OnInitializedAsync()
{
Eggs = await EggService.GetEggs();
EggService.OnChange += StateHasChanged;
}
public void Dispose()
{
EggService.OnChange -= StateHasChanged;
}
}
Index:
#page "/"
#using BlazorSandbox.Services
#inject EggService EggService
<h1>
Eggs!
</h1>
<div class="d-flex flex-row justify-content-around">
<Hen />
<Basket />
</div>
#code {
}
Program.cs:
using BlazorSandbox;
using BlazorSandbox.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Blazored.LocalStorage;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<EggService>();
builder.Services.AddBlazoredLocalStorage();
await builder.Build().RunAsync();
Solution
Special thank you to person who deleted their comment. I'm kind of new to asking questions on StackOverflow so I'm sorry if I should've selected your answer as the Answer!
#code {
public int Eggs { get; set; }
protected override void OnInitialized()
{
//Subscribe
EggService.OnChange += UpdateEgg;
//Set value of Eggs on load
UpdateEgg();
}
public void UpdateEgg()
{
// Set value of Eggs to new value and trigger component rerender
InvokeAsync(async () => {Eggs = await EggService.GetEggs(); StateHasChanged(); });
}
public void Dispose()
{
// Unsubscribe
EggService.OnChange -= UpdateEgg;
}
}
There are a few oddities in your code.
if (newEggs == null)
This is an int, so it can never be null. The default value for int is 0. You should be seeing a warning for this.
Eggs = await EggService.GetEggs();
After you set Eggs here, you never update it anywhere in your code! So even if you call StateHasChanged, there is nothing to update.
What you will want to do is keep track of the egg count inside of your EggService and then inside of your Basket component you will need a way to know that the egg count has increased so you can update your Egg property and then call StateHasChanged. Let me know if you need help with this.

Categories