how can I refer to a component created via DynamicComponent in Blazor? - dynamic

I'm rendering components using DinamicComponent and I need to call a function found in the child component.
I can't find the equivalent of using #ref for the DinamicComponents so that I can reference to call the function.
This is the parent component
<div class="tab-content">
#foreach (VerticalTabComponent.TabModel oneTabItem in VerticalTabsList)
{
<div class="tab-pane fade show #(oneTabItem.TabIndex == SelectedTabIndex ? "active" : "")" #key=#($"VTabDivDynamic_{TabPrefix}_{oneTabItem.TabIndex.ToString()}")>
<DynamicComponent
Type=#System.Type.GetType(oneTabItem.TabComponent)
Parameters=#oneTabItem.TabParameters>
</DynamicComponent>
</div>
}
</div>
This is the code in Blazor Component Tab
public partial class TabComponent
{
[Parameter]
public EventCallback<string> InsertUpdateCallback { get; set; }
protected override async Task OnInitializedAsync()
{
await CallAnyfunctionAsync();
}
private async Task<bool> LoadDataGrid()
{
//this is the function I need to call from parent
}
}
How can I call the Load Grid function from the parent component?

There is an easy solution. Not sure if that is new but the ref-attribut does exist for the DynamicComponent! You can use it like this:
<DynamicComponent Type="typeof(MyComponent)" Parameters="#MyParameters" #ref="dc" />
and in Code-Behind:
private DynamicComponent? dc;
private MyComponent? MyComponentRef
{
get
{
return (MyComponent?)dc?.Instance;
}
}

Normally in Blazor we use #Ref to get a reference to a component, but as you've seen this won't work with a DynamicComponent.
A workaround for this would be to add a [Parameter] to the component called something like Register which is an action with the generic type set as the component type. You can then add code to handle OnParametersSet to call this method in the component.
You can then add a Register parameter in your TabParameters which gets updated with a reference.
Example code below would be added to the SurveyPrompt component:
/// <summary>
/// will be called when params set
/// </summary>
[Parameter] public Action<SurveyPrompt> Register { get; set; }
protected override void OnParametersSet()
{
if (Register != null)
{
// register this component
Register(this);
}
}
You add a Register parameter with an Action<type> value. Here's an example:
SurveyPrompt sp1 = null;
void Register1(SurveyPrompt survey)
{
sp1 = survey;
Console.WriteLine("SP1 has title " + sp1.Title);
}
protected override void OnInitialized()
{
Action<SurveyPrompt> p1 = Register1;
params1 = new Dictionary<string, object>()
{
{ "Title", "Survey Title Here" },
{ "Register", p1 }
};
}
IDictionary<string, object> params1;

Related

Blazor [Parameter] not update using #ref

I'm using .net6.0 Blazor razor library to create a component.
#using Microsoft.JSInterop;
#inject IJSRuntime _jsRuntime;
<audio id="#Id" src="#Source" />
#code {
[Parameter]
public string Id { get; set; }
[Parameter]
public string Source { get; set; }
}
I use this component in a razor page using this :
<AudioPlayer
Id="reactorAudioElement1"
Source="/audio/lion-roaring.ogg">
</AudioPlayer>
And everything is doing fine at this point.
But, if I try to use #ref like that,
<AudioPlayer
#ref=#_Jukebox2>
</AudioPlayer>
#code {
private AudioPlayer _Jukebox2;
protected override void OnInitialized()
{
_Jukebox2 = new AudioPlayer()
{
Id="reactorAudioElement2",
Source="/audio/Bleep_02.ogg"
};
}
}
nothing is set in the DOM.
But I can read data like this, and I cannot set it...
<AudioPlayer
#ref=#_Jukebox2
Id="reactorAudioElement2"
</AudioPlayer>
#code {
private AudioPlayer _Jukebox2;
protected override void OnAfterRender(bool firstRender)
{
Console.WriteLine(_Jukebox2.Id); //ok
_Jukebox2.Source = "toto.mp3"; //doesn't do anything
}
}
Adding a StateHasChanged(); is not working
What am I doing wrong ?
Your component in DOM is created with this section of code, and your component will have valid #ref value
<AudioPlayer
#ref=#_Jukebox2
Id="reactorAudioElement2"
</AudioPlayer>
but you are creating another object in code with this section of code
_Jukebox2 = new AudioPlayer()
{
Id="reactorAudioElement2",
Source="/audio/Bleep_02.ogg"
};
In above code you can create object, but can not have DOM reference
This part of code will create an element in DOM and set it's reference to _Jukebox2, will also set value of Source
<AudioPlayer
#ref=#_Jukebox2
Id="reactorAudioElement2"
Source="#source"
</AudioPlayer>
private string source="Source="/audio/Bleep_02.ogg"`;
And in code behind you should be able to access public methods or variable in _Jukebox2 like this
_Jukebox.Source="toto.mp3";
If its not updating DOM then issue might be somewhere else

Microsoft.AspNetCore.Components.Forms.InputRadioGroup` does not support the type xxx

I want to use radio group in blazor so after implementing edit form and select one of the radio button I got this error :
Microsoft.AspNetCore.Components.Forms.InputRadioGroup`1[EGameCafe.SPA.Models.GameModel] does not support the type 'EGameCafe.SPA.Models.GameModel'.
here is my edit form :
<EditForm Model="ViewModel" OnValidSubmit="HandleCreateGroup">
#if (ViewModel.Games.List.Any())
{
<InputRadioGroup Name="GameSelect" #bind-Value="Gamemodelsample">
#foreach (var game in ViewModel.Games.List)
{
<InputRadio Value="game" />
#game.GameName
<br />
}
</InputRadioGroup>
}
</EditForm>
#code{
public GameModel GameModelSample { get; set; } = new();
}
and GameModel is :
public class GameModel
{
public string GameId { get; set; }
public string GameName { get; set; }
}
The InputRadioGroup, like other Blazor components, supports only a limited amount of types like String or Int32. You had the right idea, but unfortunately, you run into a kind of limitation of Blazor.
You could try to create a wrapper field.
private String _selectedGameId = "<Your Default Id>";
public String SelectedGameId
{
get => _selectedGameId;
set
{
_selectedGameId = value;
// Set the property of the ViewModel used in your Model Property of the EditContext or any other property/field
ViewModel.SelectedGame = ViewModel.Games.List?.FirstOrDefault(x => x.GameId == value);
}
}
Use the property SelectedGameId as the bind value of the InputRadioGroup component.
<InputRadioGroup Name="GameSelect" #bind-Value="SelectedGameId" >
#foreach (var game in ViewModel.Games.List)
{
<InputRadio Value="game.GameId" />
#game.GameName
<br />
}
</InputRadioGroup>
As an alternative, you can create a custom component that inheriting from InputRadioGroup to create a kind of GameBasedInputRadioGroup. If you are interested I can post a sample.
Because in your code #bind-Value="Gamemodelsample",you are trying to bind GameName(string) to Gamemodelsaple(object), which will cause type mismatch problems.
You only need to modify your code to:
#bind-Value="GameModelSample.GameName"

Convert input control to bind to multiple types - Blazor (.net v5)

Im attempting to create an input element that I can use inside AND outside an EditForm component as Chris Sainty explored in this excellent blog post.
The difference is I would like to create an input that can also be bound to any type.
The following code i tried for the child component:
#typeparam T
<input value="#Value" #oninput="HandleInput" />
#code {
private FieldIdentifier _fieldIdentifier;
[Parameter] public T Value { get; set; }
[Parameter] public EventCallback<T> ValueChanged { get; set; }
[Parameter] public Expression<Func<T>> ValueExpression { get; set; }
[CascadingParameter] private EditContext CascadedEditContext { get; set; }
protected override void OnInitialized()
{
_fieldIdentifier = FieldIdentifier.Create(ValueExpression);
}
private async Task HandleInput(ChangeEventArgs args)
{
#* How do I get args into a T type?
await ValueChanged.InvokeAsync(args.Value);*#
CascadedEditContext?.NotifyFieldChanged(_fieldIdentifier);
}
}
But what im struglling with is how can we convert args in the HandleInput function to type T?
Any ideas will be really appreciated!
Ok if this helps anyone else, ive tested a few types including nullables and replacing HandleInput with this works:
private async Task HandleInput(ChangeEventArgs args)
{
T argsValue = (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromInvariantString(args.Value.ToString());
await ValueChanged.InvokeAsync(argsValue);
CascadedEditContext?.NotifyFieldChanged(_fieldIdentifier);
}
Note - you will just need to handle errors converting types.

How to generate a Razor Page url within a custom TagHelper

I have a custom tag helper which should render something like this:
<ol>
<li>Some text
</ol>
If I were to do this within a Razor Page I would simply do something like this: <a asp-page="MyRazorPage">Some text</a>
Is there a way to do something similar inside of the TagHelper?
I found the answer.
Inject IUrlHelperFactory into the constructor as well as use the following property:
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
Then you can create an IUrlHelper this way:
var urlHelper = _urlHelperFactory.GetUrlHelper(ViewContext);
var url = urlHelper.Page("/Clients/Edit", new { Id = myClientId });
output.Content.AppendHtmlLine($"<a href='{url}'>Edit</a>");
TagHelper provides HtmlTargetElement to add attributes to specified tags. Take adding asp-cuspage to the tag <a> as an example. The method Init is used to receive the parameters in the instruction asp-cuspage="". This method Process provides output attributes.
Create class CusAnchorTagHelper:
[HtmlTargetElement("a")]
public class CusAnchorTagHelper : TagHelper
{
private const string CuspageAttributeName = "asp-cuspage";
[HtmlAttributeName(CuspageAttributeName)]
public string Cuspage { get; set; }
public string Value { get; set; }
public override void Init(TagHelperContext context)
{
if (context.AllAttributes[0].Value != null)
{
Value = context.AllAttributes[0].Value.ToString();
}
base.Init(context);
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var reg = new Regex("(?<!^)(?=[A-Z])");
string attr="";
foreach(var a in reg.Split(Value))
{
attr += a + "/";
}
output.Attributes.SetAttribute("href", attr);
}
}
Then, inject custom taghelper assembly into the page. And it will be drawn in the view.
This is the rendered result.

ASP.NET Core- Is there a way to render group of elements each using custom tag-helpers?

I noticed in my project that all my form fields follow the same pattern. A typical example is:
<div class="col-x-x">
<label asp-for="Property"></label>
<span message="description">
<input asp-for="Property" />
<span asp-validation-for="Property"></span>
</div>
I would love to have some way of grouping this code so that i simply pass it the property on the model and it outputs the correct HTML. e.g.:
<form-field for="Property" ...>
or
#Html.StringFormField(...)
The issue I am having is that whatever method I try, the html outputted is the original html above, and not the html that is generated from the tag helpers. I have tried both methods and neither have been successful. Additionally I have tried to create a razor function, but all my attempts fail to compile, and I can't make a partial view work as I haven't been able to find a way to get the property information after passing a string to a view.
My latest attempt was using a tag helper, however this had the same issue mentioned previously. The latest version of the code is as follows:
[HtmlTargetElement("form-field", Attributes = "for")]
public class FormFieldTagHelper : TagHelper
{
[HtmlAttributeName("for")]
public ModelExpression For { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "div";
output.TagMode = TagMode.StartTagAndEndTag;
var contentBuilder = new HtmlContentBuilder();
contentBuilder.AppendHtmlLine($"<label asp-for=\"{For}\"></label>");
contentBuilder.AppendHtmlLine($"<span message=\"description.\"></span>");
contentBuilder.AppendHtmlLine($"<input asp-for=\"{For}\"/>");
contentBuilder.AppendHtmlLine($"<span asp-validation-for=\"{For}\"/></span>");
output.Content.SetHtmlContent(contentBuilder);
}
}
There is an issue addressing this (with no solution) which suggested the order of the imports was a potential issue, so my imports are as follows:
#addTagHelper Project.Web.Features.Shared.*, Project.Web
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Any solution would be welcome, either for a tag helper or another method.
You could use IHtmlGenerator to generate these elements, refer to my below demo code:
[HtmlTargetElement("form-field", Attributes = "for")]
public class FormFieldTagHelper : TagHelper
{
[HtmlAttributeName("for")]
public ModelExpression For { get; set; }
private readonly IHtmlGenerator _generator;
[ViewContext]
public ViewContext ViewContext { get; set; }
public FormFieldTagHelper(IHtmlGenerator generator)
{
_generator = generator;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
using (var writer = new StringWriter())
{
writer.Write(#"<div class=""form-group"">");
var label = _generator.GenerateLabel(
ViewContext,
For.ModelExplorer,
For.Name, null,
new { #class = "control-label" });
label.WriteTo(writer, NullHtmlEncoder.Default);
writer.Write(#"<span message=""description.""></span>");
var textArea = _generator.GenerateTextBox(ViewContext,
For.ModelExplorer,
For.Name,
For.Model,
null,
new { #class = "form-control" });
textArea.WriteTo(writer, NullHtmlEncoder.Default);
var validationMsg = _generator.GenerateValidationMessage(
ViewContext,
For.ModelExplorer,
For.Name,
null,
ViewContext.ValidationMessageElement,
new { #class = "text-danger" });
validationMsg.WriteTo(writer, NullHtmlEncoder.Default);
writer.Write(#"</div>");
output.Content.SetHtmlContent(writer.ToString());
}
}
}
View:
<form-field for="ManagerName"></form-field>
Result:
It seems the easiest way to do this without duplicating custom tag helper code with the html generator is by simply creating new instances of the custom tag helpers from within a new tag helper.
e.g.
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "div";
output.TagMode = TagMode.StartTagAndEndTag;
//create label tag
LabelForTagHelper labelTagHelper = new LabelForTagHelper(ValidatorFactory)
{
For = this.For,
IgnoreRequired = this.IgnoreRequired
};
TagHelperOutput labelOutput = new TagHelperOutput(
tagName: tagName,
attributes: attributes ?? new TagHelperAttributeList(),
getChildContentAsync: (s, t) =>
{
return Task.Factory.StartNew<TagHelperContent>(() => new DefaultTagHelperContent());
}
);
var labelElement = await labelTagHelper.ProcessAsync(context, labelOutput);
output.Content.AppendHtml(labelElement );
//repeat for other tags
}