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.
Related
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.
I am building a dashboard which is having tiles that loads data on start up. The loading is shown with a loading animation and to prevent repeatable code, I want to manage this in a wrapper tile. This wrapper tile instance is passing itself to the child so the child can set the loading state to false when the data is retrieved. This is the code I have so far:
wrapper tile code
WrapperTile.razor
<MudItem xs="12" sm="6" md="4" lg="3" xl="2" xxl="1">
<MudPaper Height="250px" Elevation="3" Class="pa-4 mud-width-full">
#if (IsLoading)
{
<MudProgressCircular Color="Color.Primary" Size="Size.Large" Indeterminate="true" />
}
else
{
}
#if (ChildContent is not null)
{
#ChildContent(this)
}
</MudPaper>
</MudItem>
Side-note: The second if statement (checking the ChildContent) is currently outside the else statement for testing purposes but will be replaced as soon as the loading state switch process is working.
WrapperTile.razor.cs
public partial class WrapperTile
{
[Parameter]
public RenderFragment<WrapperTile>? ChildContent { get; set; }
public bool IsLoading { get; set; } = true;
}
test tile 1
TestTile1.razor
<WrapperTile>
Tile 1<br/>
<MudButton OnClick="Test">Test</MudButton>
</WrapperTile>
TestTile1.razor.cs
public partial class TestTile1
{
[Parameter]
public WrapperTile? Wrapper { get; set; }
private void Test()
{
ArgumentNullException.ThrowIfNull(Wrapper);
Wrapper.IsLoading = false;
}
}
test tile 2
Is the same as test tile 1, only the tile text is different (tile 2).
What I think that should happen is when calling the ChildContent(this) the wrapper tile is passed to the tile itself and the tile itself can change the IsLoading on the WrapperTile instance, changing the loading animation to the data that must be shown.
What works is the loading animation, it is shown as well as the tile text and the button. But as soon as the button is clicked, an exception is thrown by the ArgumentNullException.Throw() method, meaning that the Wrapper is null.
I cannot figure out why the Wrapper is null in this situation. Do I miss a constructor? Am I using the RenderFragment wrong this way? I also tried to use the CascadingValue component:
#if (ChildContent is not null)
{
<CascadingValue Value="this">
#ChildContent
</CascadingValue>
}
This also has the same issue, the Wrapper property is not set and causing the null reference exception.
What do I need to do to make this work the way I would like?
EDIT:
I have put both scenario's in a blazor fiddle.
RenderFragment with parameter: https://blazorfiddle.com/s/ozatfeb0
Using the CascadeValue/CascadingParameter: https://blazorfiddle.com/s/s6m4anbu
In both situations the Wrapper property is not set.
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.
I have a component that I set a reference of it in a page variable:
<BlazorWebFormsComponents.Button OnClick="#((args) => btnForms_Clicked(formsButton, args))" #ref="formsButton" Text="Forms Button" CssClass="btn btn-primary">
</BlazorWebFormsComponents.Button>
In the event handler I Set a button property (Text):
Button formsButton;
public void btnForms_Clicked(object sender, MouseEventArgs e)
{
if (sender is Button)
(sender as Button).Text = "Good Bye";
}
For most of the Button properties this code is not working, For BackColor works but for Text not.
Also blazor makes the assignment line, a green Underlined and says "Component parameter "zzz" should not be set outside of its component", So why Blazor provides a #Ref while most of the referenced properties can not be set? or is there a way to make this work?
<Button OnClick="#((args) => btnForms_Clicked(formsButton, args))" #ref="formsButton" Text="Forms Button" CssClass="btn btn-primary">
Your Button component should be defined as follows:
#code
{
[Parameter]
public EventCallback<MouseEventArgs> OnClick {get; set;}
[Parameter]
public string Text {get; set;}
}
The above code define two parameter properties that should be assigned from the Parent component. The parent component is the component in which the Button component is instantiated. Note that you should set the above properties from the parent component as attribute properties... you must not set them outside of the component instantiation. Right now it's a warning, but Steve Anderson has already sad that it is going to be a compiler error soon. This is how you instantiate your component in the parent component:
Parent.razor
<Button OnClick="#((args) => btnForms_Clicked(args))" #ref="formsButton"
Text="_text" CssClass="btn btn-primary">
</Button>
#code
{
private Button formsButton;
// Define a local variable wich is bound to the Text parameter
private string _text = "Click me now...";
public void btnForms_Clicked( MouseEventArgs e)
{
_text = "You're a good clicker.";
}
}
Note: When you click on the Button component a click event should be raised in the Button component, and the button component should propagate this event to the parent component; that is to execute the btnForms_Clicked method on the parent component, Here's how you do that:
Button.razor
<div #onclick="InvokeOnClick">#Text</div>
#code
{
[Parameter]
public EventCallback<MouseEventArgs> OnClick {get; set;}
[Parameter]
public string Text {get; set;}
private async Task InvokeOnClick ()
{
await OnClick.InvokeAsync();
}
}
Note that in order to demonstrate how to raise an event in the Button component, and propagate it to the parent component, I'm using a div element, but you can use a button element, etc.
#onclick is a compiler directive instructing to create an EventCallback 'delegate' whose value is the event handler InvokeOnClick. Now, whenever you click on the div element, the click event is raised, and the event handler InvokeOnClick is called... from this event we execute the EventCallback 'delegate' OnClick; in other words, we call the btnForms_Clicked method defined in the parent component.
So what the #ref directive is good for? You may use the #ref directive to get a reference to a component that contain a method you want to call from its parent component: Suppose you define a child component that serves as a dialog widget, and this component define a Show method, that when is called, display the dialog widget. This is fine and legitimate, but never try to change or set parameter properties outside of the component instantiation.
The warning is there because setting [Parameter] properties from code can cause the render tree to get out of sync, and cause double rendering of the component.
If you need to set something from code, you can expose a public method e.g. SetText on the component class, which does that for you.
Internally, the component [Parameter] should reference a local variable.
string _text;
[Parameter]
public string Text { get => _text; set => SetText(value);}
public void SetText(string value)
{
_text = value;
}
I am not promoting this approach, I prefer to use the approach in #Pidon's answer.
Additionally, you could consider - maybe you have too many parameters and should consider an Options parameter to consolidate
I'm not an export on Blazor, but ran into this as well. You can do this by binding to a property on the page.
<BlazorWebFormsComponents.Button OnClick="#((args) => btnForms_Clicked(formsButton, args))" #ref="formsButton" Text="#ButtonText" CssClass="btn btn-primary">
</BlazorWebFormsComponents.Button>
#code{
private string ButtonText { get; set; } = "Forms button";
public void btnForms_Clicked(object sender, MouseEventArgs e)
{
ButtonText = "Good bye";
}
}
I have never used the #Ref and don't yet know how or when to use it.
What I do for Parameters to make them "settable" in the code block/behind is to designate a "setter" method for them. The following code, while it should work, is untested but illustrates the concept.
MyComponent.razor - A simple component that wraps a button and exposes it's OnClick event to consumer components/"pages"
<Button OnClick="btn_OnClick"></Button>
#code{
[Parameter]
public EventCallback<MouseEventArgs?> MyComponent_OnClick { get; set; }
// Handles the Click event from the button and raises the EventCallback assigned to the
// component via the MyComponent_OnClick Parameter.
//
private async Task btn_OnClick(MouseEventArgs args)
{
if(this.MyComponent_OnClick.HasDelegate)
{
await this.MyComponent_OnClick.InvokeAsync(args);
}
}
// Assigns the eventCallback argument to the MyComponent_OnClick Parameter
// without violating compiler rules.
//
public void AddEventListener_MyComponentOnClick(EventCallback<MouseEventArgs?> eventCallback)
{
this.MyComponent_OnClick = eventCallback;
}
}
Page.razor - Consumer component/"page". Notice that the "Click" event handler is not assigned declaratively in the tag but is instead assigned when the component/"page" is first initialized.
#page "/Test"
<MyComponent #ref="myComponent"></MyComponent>
#code {
// Reference to the MyComponent instance
//
protected MyComponent? myComponent;
// Runs when the "page" is first loaded.
//
protected override Task OnInitializedAsync()
{
// Use the public "AddEventListener" method to programmatically assign an
// EventCallback to the OnClick event of the MyComponent.
//
mtComponent?.AddEventListener_MyComponentOnClick(EventCallback.Factory.Create<int?>(this, this.ComponentOnClick));
return base.OnInitializedAsync();
}
protected void ComponentOnClick(MouseEventArgs args)
{
// ... Handle the event
}
}
This way you could potentially assign an appropriate event handler based on some condition. Or when passing a component reference to descendent components via the CascadingValue tag, it allows you to apply the event handler when the declarative syntax is not available.
For example ...
MainLayout.razor
<MyComponent #ref="myComponent"></MyComponent>
<CascadingValue Value=#myComponent Name="MyComponent">
#body
</CascadingValue>
#code {
public MyComponent? myComponent;
}
I'm not a technical writer but I hope that someone was able to find this useful.
I'm trying to access properties on the RootVisual object:
[ScriptableType]
public class ApplicationInfo
{
public string Name { get; set; }
public string Version { get; set; }
}
[ScriptableType]
public partial class MainPage : UserControl
{
[ScriptableMember]
public ApplicationInfo ApplicationInfo { get; set; }
public MainPage()
{
InitializeComponent();
this.ApplicationInfo = new ApplicationInfo();
this.ApplicationInfo.Name = "My Application";
this.ApplicationInfo.Version = "0.1";
HtmlPage.RegisterScriptableObject("myapp", this);
}
}
In my ASPX hosting page I've the following JavaScript snippet:
<script type="text/javascript">
function onPluginLoaded(plugin) {
alert('in plugin');
alert(plugin.Name); //This gives me the x:Name of the RootVisual object
var appInfo = plugin.ApplicationInfo;
alert(appInfo);
alert(plugin.myapp);
document.title = appInfo.Name + " " + appInfo.Version;
}
</script>
<object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%" OnPluginLoaded="onPluginLoaded" >
<param name="source" value="ClientBin/SLVersion.xap"/>
<param name="onload" value="onPluginLoaded" />
This doesn't work. I'd like to know why! Thanks in advance.
There are two things wrong.
Silverlight Documentation
The Silverlight documentation about this area of functionality is really quite confused. Here is thing, the object provided as the sender parameter in the onLoad method isn't what the documentation says that it is, it's not the silverlight plugin.
At least its not the plugin as seen by the HTML DOM / Javascript. It seems to be some form of the Javascript API version of a Framework element. In order to get the plugin object that is useful to us we need to call the getHost method on it.
function onPluginLoaded(sender) {
var plugin = sender.getHost();
}
That gets us one step closer.
Accessing Registered Scriptable Objects
Scriptable objects that have been registered on HTMLPage are accessed as properties of the Plugin's Content property. Hence to access the ApplicationInfo object you would need:-
function onPluginLoaded(sender) {
var plugin = sender.getHost();
var appInfo = plugin.Content.myapp.ApplicationInfo;
alert(appInfo.Name + " " + appInfo.Version);
}
That'll get you going.
ScriptableType
Remove [ScriptableType] from MainPage, in this case you only want to mark specific members as available to script hence you use the [ScriptableMember]. By using [ScriptableType] you expose all public members automatically as scriptable. You are correctly doing that on your ApplicationInfo.