Blazor: intercept an EventCallback to add some code - c#

In my Blazor WebAssembly application, I use a lot the TelerikGrid component, provided by Telerik Kendo, but my question could be the same with another component.
I need to execute some code at the end of the execution of the handler of the "OnRead" EventCallback of TelerikGrid, as bellow:
protected async Task OnReadHandler(GridReadEventArgs args)
{
args.Data = _myClient.GetData(); //some code
// This line is the one I need to repeat accros all handlers of OnRead in my application
await JSRuntime.InvokeVoidAsync("attachAllGridCells");
}
I do not know if Blazor can achieve my request. In addition, the component TelerikGrid cannot be modified, as it is from a third-party. I've tried something, with no good result:
Create a component which extend TelerikGrid, and trying to somehow override OnRead. But as it is an EventCallback, and not a method, I cannot do that easily:
public partial class CustomTelerikGrid<T> : TelerikGrid<T>
{
[Parameter]
public new EventCallback<GridReadEventArgs> OnRead { get; set; }
async Task TestAsync()
{
await this.OnRead.InvokeAsync(); // access to my "new" eventcallback
await base.OnRead.InvokeAsync(); // access to the "original" eventcallback
}
}
Is there way I can achieve that? I believe not really with my knowledge of Blazor, but maybe someone can have an idea, thanks in advance for any help.

We have a grid component in our project that extends TelerikGrid. We made class TelerikGridSettings with parameters from base Telerik Grid which we use in our grids across our project like that:
public partial class TelerikGridSettings<TItem> : BaseComponent
{
[Parameter]
public IEnumerable<TItem> Data { get; set; }
[Parameter]
public decimal RowHeight { get; set; }
[Parameter]
public RenderFragment GridColumns { get; set; }
[Parameter]
public EventCallback<GridReadEventArgs> OnRead { get; set; }
/// etc
}
Then in your custom GridComponent you use TelerikGrid and fill parameters like that, and also insert your custom OnReadHandler:
<TelerikGrid TItem="TItem"
Data=#Data
RowHeight=#RowHeight
GridColumns=#GridColumns
OnRead=#OnReadHandler
// etc />
In your OnReadHandler you will invoke TelerikGrid common OnRead event callback and after that use your JS method:
private async ValueTask OnReadHandler()
{
await OnRead.InvokeAsync();
// your js method
}
Then you can use your GridComponent exactly as you use TelerikGrid right now.
Hope this helps. Also this approach will give you more flexibility in customizing your grid.

Related

Two-Way Component Parameter Binding on a class in Blazor?

I have a class MealsQueryInputs that I would like to use as a component parameter with two-way binding capabilities.
All of the demos and sample code I can find are using built-in primitive types and never a class. I can get the MS demos to work but I cannot get binding to a class to work. Is it even possible to do this?
My component FilterSortOptions.razor:
using WhatIsForDinner.Shared.Models
<MudCheckBox Checked="#QueryInputs.Favorite"
Color="Color.Inherit"
CheckedIcon="#Icons.Material.Filled.Favorite"
UncheckedIcon="#Icons.Material.Filled.FavoriteBorder"
T="bool"/>
<MudRating SelectedValue="#QueryInputs.Rating"/>
<MudButton OnClick="#(async () => await OnPropertyChanged())">Apply</MudButton>
#code {
[Parameter]
public MealsQueryInputs QueryInputs { get; set; }
[Parameter]
public EventCallback<MealsQueryInputs> QueryInputsChanged { get; set; }
private async Task OnPropertyChanged()
{
await QueryInputsChanged.InvokeAsync(QueryInputs);
}
}
As MrC said, you should avoid directly binding to the data being supplied as a parameter.
Here is a simple working sample (not MudBlazor) to show the concept
https://blazorrepl.telerik.com/QQEnQjaO54LY3MYK35
You bind to a local variable/property and try not to modify the incoming data directly.
MyComponent
<h1>MyComponent</h1>
<label for="choice">Choose</label>
<input id="choice" type="checkbox" #bind-value=localValue />
#code
{
bool localValue
{
get => Data.SomeChoice;
set {
if (value != localValue)
{
localData = Data with { SomeChoice = value };
InvokeAsync(ValueChanged);
}
}
}
ComplexObject localData;
[Parameter] public ComplexObject Data { get; set; }
[Parameter] public EventCallback<ComplexObject> DataChanged { get; set; }
Task ValueChanged() => DataChanged.InvokeAsync(localData);
}
ComplexObject
public record ComplexObject(bool SomeChoice, string SomeText);
Main
#code
{
ComplexObject data = new(false,"");
}
<MyComponent #bind-Data=data />
You have chosen #data.SomeChoice
Here is how you can bind class objects to a custom razor component
This is FilterSortOptions component
<div>
<label>Rating:</label>
<input type="text" value=#QueryInputs.Rating #oninput=#(val=> {
QueryInputs.Rating=val.Value.ToString();
QueryInputsChanged.InvokeAsync(QueryInputs);
}) />
</div>
<div>
<label>Favourite:</label>
<input type="checkbox" value=#QueryInputs.Rating #onchange=#(val=> {
QueryInputs.Favourite=(bool)val.Value;
QueryInputsChanged.InvokeAsync(QueryInputs);
}) />
</div>
#code {
[Parameter]
public MealsQueryInputs QueryInputs { get; set; }
[Parameter]
public EventCallback<MealsQueryInputs> QueryInputsChanged { get; set; }
}
This is the model to bind, for simplicity Rating is is string type
public class MealsQueryInputs
{
public bool Favourite { get; set; } = false;
public string Rating { get; set; } = "0";
}
Here is the razor page
<h3>Rating: #QueryInputs.Rating</h3>
<h3>Favourite: #QueryInputs.Favourite</h3>
<FilterSortOptions #bind-QueryInputs=#QueryInputs></FilterSortOptions>
#code {
public MealsQueryInputs QueryInputs = new();
}
Updated Answer
Firstly, if your using an object then you are passing around references to the same object. So when you update the object in the sub-component, you're updating the same object the parent is using. You don't need to pass the object back in the callback unless you create a noew copy of it.
Secondly, your not binding the mud controls to the object.
Let's look at your code:
<MudCheckBox Checked="#QueryInputs.Favorite"
Color="Color.Inherit"
CheckedIcon="#Icons.Material.Filled.Favorite"
UncheckedIcon="#Icons.Material.Filled.FavoriteBorder"
T="bool"/>
Checked="#QueryInputs.Favorite" doesn't bind the control to the field. It just sets the initial value.
I think (I don't use Mudblazor and it's a little different from standard Blazor Form Controls) you need to do this:
<MudCheckBox #bind-Checked="#QueryInputs.Favorite"></MudCheckBox>
The same is true for MudRating.
<MudRating #bind-SelectedValue="#QueryInputs.Rating" />
Then the button:
<MudButton OnClick="#(async () => await OnPropertyChanged())">Apply</MudButton>
can be simplified to this. You're wrapping an async method within an async method.
<MudButton OnClick="OnPropertyChanged">Apply</MudButton>
// or
<MudButton OnClick="() => OnPropertyChanged()">Apply</MudButton>
Original Answer
There are a couple of issues here:
QueryInputs is a Parameter and therefore should never be modified by the code within the component. You end up with a mismatch between what the Renderer thinks the value is and what it actually is.
When the parent component renders it will always cause a re-render of any component that is passed a class as a parameter. The Renderer has no way of telling if a class has been modified, so it applies the heavy handed solution - call SetParametersAsync on the component.
A solution is to use a view service to hold the data and events to notify changes. One version of the truth! Search "Blazor Notification Pattern" for examples of how to implement this. I'll post some code if you can't find what you want.

How can I add different controls dynamically to a web page using asp.net core?

I am new to .net core - have been using aspx web pages and .net framework 4.x for a number of years. I have a project where we want to display different controls (textbox, dropdown, checkbox) on the page based on values returned from a query. For example, user chooses "A" from a dropdown list and it shows 10 controls, if they choose object B it shows 8 controls, etc. Previously in .net framework, I would use a content placeholder with an ID and then find that ID and start adding controls (controls.Add(newControl)) in the placeholder. It doesn't seem that is an option with .net core. It seems like this would be a common need for various web applications, but I'm not finding many hits.
Another question is whether this can be done in the code behind or if it has to be done on the client-side. If one of the controls in the list is a dropdown, there will be a query that a subroutine will run to get the Key/Value pairs for the dropdown. To me this means it would be more effective on the server side.
I haven't really found any good examples when I do some searching. Can anyone point me to a good resource or provide me with a basic example - either client-side or server-side? Thanks!
There are many options, but I'll describe a simple one, using server side processing. As you explained in your comment, there will be 2 pages:
One that will display the select element that will be used to choose a set of controls.
The page that will be returned according to the previous choise, displaying the selected set of controls.
I assume that you know how to build the first page.
For the second page, you can leverage the ASP.NET Core MVC pattern to achieve the desired result.
You will need the three usual MVC elements:
An Action in a Controler.
A ViewModel for your Razor View.
A Razor View.
The Action does the following:
Receives the id of the selected set of control (via the Action's parameter).
Uses this id to retrieve the information about the corresponding set of controls from your repository.
Builds a ViewModel out of the received information.
Builds a View using the obtained ViewModel.
Return the builded View.
Here is some simplified example code:
In your controller, add the following method:
#!lang-cs
Public IActionResult GetProgramControlSet(int ProgramId)
{
// Here, use the id to get the data from your repository
// that will be used to build set of controls.
// Supposing you have defined a GetControls method,
// it could look like:
var SelectedControls = MyRepository.GetControls(ProgramId);
// If needed, you can build a ViewModel out of the received SelectedControls.
var SelectedControlsViewModel = new ControlSetViewModel(SelectedControls);
return View(SelectedControlsViewModel)
}
Of course, many things are missing here: error handling, etc...
Here is what the ViewModel could be:
#!lang-cs
public class ControlSetViewModel
{
public string Name { get; private set; }
public List<IControl> Controls { get; private set; }
public ControlSetViewModel(...)
{
// Whatever needs to be done to construct the ViewModel
}
}
public enum ControlKind
{
Button,
Select,
Textarea
//...
}
public interface IControl
{
ControlKind Kind { get; }
}
public class ControlButton : IControl
{
public ControlKind Kind => ControlKind.Button;
public string Label { get; set; }
public string Text { get; set; }
public string Color { get; set; }
// ... All other needed properties for the button
}
public class ControlTextarea : IControl
{
public ControlKind Kind => ControlKind.Textarea;
public string Label { get; set; }
public string PlaceholderText { get; set; }
public string RowCount { get; set; }
// ... All other needed properties for the textarea
}
public class ControlSelect : IControl
{
public ControlKind Kind => ControlKind.Select;
public string Label { get; set; }
public string PlaceholderText { get; set; }
public List<SelectOption> Options { get; set; }
// ... All other needed properties for the select
}
public class SelectOption
{
public string Text { get; set; }
public string Value { get; set; }
}
You could also use inheritance instead of interface for the control classes.
Now the view.
It is a Razor page containing something akin to
#model ControlSetViewModel
#*... some HTML ...*#
<div>
<h1>#Model.Name</h1>
#foreach(var control in Model.Controls)
{
<div>
switch(control.GetControlKind())
{
case ControlKind.TextArea:
var Textarea = (ControlTextarea)control;
<label>#Textarea.Label</label>
<textarea rows="#Textarea.RowCount"/>
break;
case ControlKind.Select:
var Select = (ControlSelect)control;
<label>#Select.Label</label>
<select>
#foreach(var option in Select.Options)
{
<option value="#option.Value">#option.Text</option>
}
</select>
break;
#*... etc ...*#
default:
#*... etc ...*#
}
</div>
}
</div>
#*... More HTML ...*#
Of course this is far to be finished. All the infrastructure and code that will actually react to the displayed controls is missing.
Is it a form you that will be posted?
Is it Javascript code that will react to the control manipulation?
Or another mecanism?
This questions will need to be addressed.

One-way [Parameter] binding programatically

I want to set defaults for parameters on a third-party component. Say I have this:
myBasePage.cs:
public class MyBasePage : ComponentBase
{
public IEnumerable MyData { get; set; }
}
myPage.razor:
#inherits MyBasePage
<ThirdPartyComponent Data="#MyData" />
Since Data on ThirdPartyComponent is a [Parameter] with a DataHasChanged() virtual method, by rendering the blazor component like that, I'll get one-way binding, and if I change MyData on my page, programmatically, the component will update. This will work fine.
Now, say I can't modify ThirdPartyComponent, but I want to make some defaults in it based on my base page... like so:
myPage.razor:
#inherits MyBasePage
<MyDerivedComponent PageComponent="#this" />
myDerivedComponent.cs:
public class MyDerivedComponent : ThirdPartyComponent
{
[Parameter] public MyBasePage PageComponent { get; set; }
public override void OnInitialized()
{
/* Set other parameter defaults */
this.OtherParameter = 10;
/* Bind to Data, as if I was passing it as a parameter in the Razor template */
this.Data = PageComponent.MyData;
}
}
This line:
this.Data = PageComponent.MyData;
Doesn't create a binding at all (and if I modify MyData, the blazor component doesn't get updated). Is there any way to programmatically create it?
Note: the real ThirdPartyComponent includes not only tons of parameters but also templates, etc. For many reasons, I'd like MyDerivedComponent to be of a derived type, and not a "parent component" with a child of ThirdPartyComponent, if that's possible at all).
This should work:
public class MyDerivedComponent : ThirdPartyComponent
{
[Parameter] public MyBasePage PageComponent { get; set; }
public override void OnInitialized()
{
/* Set other parameter defaults */
this.OtherParameter = 10;
//this.Data = PageComponent.MyData;
}
}
protected override void OnParametersSet()
{
Data = Data ?? PageComponent?.MyData ; // determine priority
base.OnParametersSet();
}
OnParametersSet() notifies the Component that it has new Parameters. The most-derived class can intervene here.
But there is no easy solution for wwhen this.MyData changes, that's out of sight for Blazor.
Is there any way to programmatically create it? Of course, but that doesn't mean that you should.
Parameters should not be set or manipulated within the component code. Parameters are set externally whenever SetParametersAsync is called by the Renderer. If you change them internally you create two versions of the truth.

Blazor onended is not firing

I'm starting to work with Blazor. My intention is playing one random Video after another. Therefore i wanted to use the Blazor Event Listener. But the onended Event is not firing (everything is working fine with onclick).
video Element:
<figure id="video-player">
<video autoplay="autoplay" #onended="NewVideo">
#videoUrl
</video>
</figure>
Codeblock:
private MarkupString videoUrl;
protected override void OnInitialized()
{
NewVideo();
}
private void NewVideo()
{
videoUrl = new MarkupString($"<source src=\"videos/{tv.GetRandomVideoFileName()}\" type=\"video/mp4\">");
}
OnInitialized is working as intended, and if I change the onended to onclick everything is also working fine.
To Mention: I know, only changing the source wouldnt start the next Video. That would be my next Task on the List :). First I only want to Change the source in the DOM.
The short story is that this isn't supported, and did not make the v5 release. See the issue here:
https://github.com/dotnet/aspnetcore/issues/24323
The longer story is that you'll have to fire up some JS interop. Reference is here:
https://learn.microsoft.com/en-us/aspnet/core/blazor/call-dotnet-from-javascript?view=aspnetcore-5.0#component-instance-method-call
On the JS side, you'll need something like this:
var player = document.getElementById('player');
player.onended = (e) => {
DotNet.invokeMethodAsync('{assembly_name_here}', 'SongEnded');
};
The assembly name is probably the name of the project, like MyBlazorApp. Then in your component, you'll need a static action to get the thing wired up:
protected override void OnInitialized()
{
action = UpdateMessage;
}
private static Action action;
private void UpdateMessage()
{
// DO STUFF HERE
}
[JSInvokable]
public static void SongEnded()
{
action.Invoke();
}
It's a little clunky, sure, but essentially you're mapping a JS event to a static method on the component. You're probably thinking, "Well, what if I have multiple instances of the component?" In that case, you'll have to be a little creative about passing context to the static method and finding the right instance. Again, check the reference above, it has plenty of examples.
Came up with a solution to avoid static method. In code part we need to hold reference of DotNetObjectReference and also some index of it (that will play part in JS), this is example class:
public abstract class VideoPlayerBase : ComponentBase, IDisposable
{
[Parameter] public string Source { get; set; }
[Parameter] public EventCallback VideoEndedCallback { get; set; }
[Inject] protected IJSRuntime JS { get; set; }
/// <summary>
/// DotNetObjectReference of current component instance.
/// This is used for catching `onended` event on video tag (which apparently is not supported by Blazor)
/// </summary>
private DotNetObjectReference<VideoPlayerBase>? _dotNetObjectReference;
/// <summary>
/// Index of DotNetObjectReference inside BlazorApp.dotNetObjectReferences array.
/// This is used to be able to relevant DotNetObjectReference from PriskApp.dotNetObjectReferences array.
/// </summary>
protected int DotNetObjectReferenceIndex { get; set; } = -1;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
_dotNetObjectReference = DotNetObjectReference.Create(this);
DotNetObjectReferenceIndex = await JS.InvokeAsync<int>("BlazorApp.addDotNetObjectReference", _dotNetObjectReference);
}
[JSInvokable("VideoEnded")]
public async Task VideoEndedAsync()
{
await VideoEndedCallback.InvokeAsync(null);
}
public void Dispose()
{
_dotNetObjectReference?.Dispose();
}
}
Then we need js part:
var BlazorApp = BlazorApp || {
dotNetObjectReferences: [],
addDotNetObjectReference: function (dotNetObjectReference) {
PriskApp.dotNetObjectReferences.push(dotNetObjectReference);
return PriskApp.dotNetObjectReferences.length - 1;
},
getDotNetObjectReference: function (index) {
return PriskApp.dotNetObjectReferences[index];
},
};
And lastly the component view:
#inherits VideoPlayerBase
<div>
<video style="width:100%;" controls onended="(BlazorApp.getDotNetObjectReference(#DotNetObjectReferenceIndex).invokeMethodAsync('VideoEnded'))()">
<source src="#Source" type="video/mp4" controls>
Your browser does not support HTML video.
</video>
</div>
So basically this work by creating DotNetObjectReference in OnInitializedAsync method and then calling js function BlazorApp.addDotNetObjectReference to add this reference to some js array and to get index of it, which is used in view part. Then once Video ends onended event is triggered and inside it it gets the relevant DotNetObjectReference and calls its method.

javascript Highcharts object as a C# class

I've been working with javascript Highcharts and I made a basic 'Chart Builder' app. One of my goals is to have the user create and modify as many options as they like and save those to the db. The main problem I'm having is trying to convert the Highcharts object to a c# class. I've been building it slowly(ie manually) with the parts I need, as I need them, but to eventually get the whole thing converted will take a long time.
Ideally, I'd like to create and setup the whole highcharts options object server side and just send it 100% complete to highcharts
Is there any easy way to do this? Has anyone already done this?
Here is the Highcharts reference page: http://www.highcharts.com/ref/
and this is what I've done so far.
public class Highchart
{
public title title { get; set; }
public plotOptions plotOptions { get; set; }
}
public class title
{
public string text { get; set; }
}
public class plotOptions
{
public series series { get; set; }
}
public class series
{
public string stacking { get; set; }
public string borderColor { get; set; }
public bool shadow { get; set; }
public int borderWidth { get; set; }
}
As you can see, I just started ^_^
Update : The Highcharts .Net library has been updated in December, and is nearly feature complete as per V2.1.9 of the Javascript library.
The .Net library currently has support for multiple axes, point objects, viewstate management after postbacks, click events for points, series etc, and a built in implementation of an AJAX datasource ;) You don't need to write a single line of JS code unless you want to handle click events; you simply code in C#, and the appropriate JS is rendered automatically for you..
Click here to view the Live Demo

Categories

Resources