I am writing a simple snake game with Blazor, but I can't figure out how to bind to document events. I know that it is possible to bind events on different elements such as div or input. Example: <input onkeypress="#KeyPressInDiv"/>, where the handler is public void KeyPressInDiv(UIKeyboardEventArgs ev) {...}.
I suppose that there should be some equivalent to the JavaScript method document.onkeydown = function (evt) {}. I have found two approaches for working around this problem:
Use JavaScript for binding and invoke Blazor code (taken from https://github.com/aesalazar/AsteroidsWasm):
document.onkeydown = function (evt) {
evt = evt || window.event;
DotNet.invokeMethodAsync('Test.ClientSide', 'JsKeyDown', evt.keyCode);
//Prevent all but F5 and F12
if (evt.keyCode !== 116 && evt.keyCode !== 123)
evt.preventDefault();
};
document.onkeyup = function (evt) {
evt = evt || window.event;
DotNet.invokeMethodAsync('Test.ClientSide', 'JsKeyUp', evt.keyCode);
//Prevent all but F5 and F12
if (evt.keyCode !== 116 && evt.keyCode !== 123)
evt.preventDefault();
};
... and in C# implement a static class with methods marked by [JSInvokable] and events. This works, but leads to an extreme delay on every key press.
It is possible to add an input tag and bind to its events. This works much faster than the previous approach, but it seems like a hack rather then a solution. Also, we are not able to listen for some actions, such as Up/Down Arrow.
Is there a direct way to bind to document events from Blazor?
Update 1: I created a simple project to better explain what I want to achieve: https://github.com/XelMed/BlazorSnake
There are 3 implementations of Snake:
Pure JS - this has the expected behavior
Using JS with Blazor - invoke a Blazor function from JS code with a JsInterop
Using input tag - bind to events on an input tag to control the snake
Perhaps add a event listener to the document using JsInterop and assign a anonymus function to the event which calls your C# method with the even parameters.
For example your JsInterop.js:
document.addEventListener('onkeypress', function (e) {
DotNet.invokeMethodAsync('Snake', 'OnKeyPress', serializeEvent(e))
});
with serializeEvent being as follos to avoid some quirkiness:
var serializeEvent = function (e) {
if (e) {
var o = {
altKey: e.altKey,
button: e.button,
buttons: e.buttons,
clientX: e.clientX,
clientY: e.clientY,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
movementX: e.movementX,
movementY: e.movementY,
offsetX: e.offsetX,
offsetY: e.offsetY,
pageX: e.pageX,
pageY: e.pageY,
screenX: e.screenX,
screenY: e.screenY,
shiftKey: e.shiftKey
};
return o;
}
};
in your C# code you would have:
[JSInvokable]
public static async Task OnMouseDown(UIMouseEventArgs e){
// Do some stuff here
}
I had the same requirement as you, and I managed to wire up document events (e.g. keydown) to my Razor methods using invokeMethodAsync, but then I found that I missed out on the automatic DOM diffing and updating that Blazor provides if the Razor method changes some state which is bound to an HTML element (e.g. controls the visibility of a div element).
It seems (from my limited understanding of Blazor) that normally Blazor returns whatever state data is necessary to update the DOM, in the return data of a WebAssembly method.
But if you use invokeMethod or invokeMethodAsync from JavaScript, you have to manage returning and consuming this data yourself, but that might be problematic because your updates might conflict with Blazor's view of the DOM state.
So I came up with a hacky approach of generating a hidden button in my Razor view, e.g.:
<button id="arrow-left-button" #onclick="HandleArrowLeftPress"></button>
And then on the JavaScript side, I wired up a document event which finds that button by Id and calls .click() on it:
document.getElementById('arrow-left-button').click();
I know this seems truly awful, but it worked for what I was doing, which was using the arrow keys to move an absolutely positioned element on screen.
If anyone knows a cleaner way (like how to force an update of a Razor view by its name from the JavaScript side, in the handler callback), please let me know.
I encountered similar situation and use the following approach as a workaround. You add event listener to a div tag, then set focus to that div such that your key presses can be captured.
<div tabindex="0" #ref="container" #onkeydown="onKeyDown" id="container">
//your snake game markup
</div>
#code {
ElementReference container;
protected override async Task OnAfterRenderAsync(bool firstRender) {
await container.FocusAsync();
}
private void onKeyPress(KeyboardEventArgs obj) { //your code here. }
}
You may need to remove the focus border of the div tag:
#container:focus-visible {
outline: none;
}
#container:focus {
outline: none;
}
To extend a bit on the answer by Blightbuster
For me at least the returned serialized event object would not cast to its .NET counterpart (KeyboardEventArgs in my case). I had to stringify it first
var serializeEvent = function (e) {
if (e) {
var o = {
//assign properties here
//we can´t just stringify e since it has circular references
};
return JSON.stringify(o);
}
};
And then deserialize in the .NET method called by the JSRuntime
using System.Text.Json;
T deserialized = JsonSerializer.Deserialize<T>(
s,
new JsonSerializerOptions() {
PropertyNameCaseInsensitive = true
});
Also, if you are lazy like me and don´t want to look up the properties you need from the event, you can look up the property names in .NET before calling your js method
var propertyNames = typeof(T)
.GetProperties()
.Where(x => x.CanWrite)
.Select(x => $"{x.Name[..1].ToLower()}{x.Name[1..]}")
.ToArray();
Then pass those to js and build your event object
let o = {};
propertyNames.forEach(propertyName => {
o[propertyName] = e[propertyName];
});
var serialized = JSON.stringify(o);
Finally I did not want to make a static method with the [JSInvokable] attribute, so I just made a little wrapper.
class JSInvokableWrapper<T>
{
Func<T, Task> Func { get; }
public JSInvokableWrapper(
Func<T, Task> func)
{
Func = func;
}
[JSInvokable]
public async Task Invoke(T argument)
{
await Func.Invoke(argument);
}
}
Use that to wrap whatever func you want js to execute and turn the wrapper into a DotNetObjectReference (Maybe in some extension method for the IJSRuntime)
var dotNetObjectReference = DotNetObjectReference
.Create(new JSInvokableWrapper<T>(func));
And call its Invoke method from js
dotNetObjectReference.invokeMethodAsync('Invoke', param);
You can bind the event directly to a C# method, just using the event tag that you need (onkeyup/onmousemove ....) .
#page "/"
<div>
<input type="text" onkeyup=#KeyUp />
</div>
#functions {
void KeyUp(UIKeyboardEventArgs e)
{
Console.WriteLine(e.Key);
}
protected override Task OnInitAsync()
{
return Task.CompletedTask;
}
}
Is there a way to pass a callback function for SignalR? Right now I have on my site.js
messageHub.client.sendProgress = function (progress) {
//doSomething with progress
}
What I would like to have is
messageHub.client.sendProgress = function (progress,callback) {
callback(progress);
}
This would enable me to define different callback functions from web pages for the same signalR method.
Right now I am doing it in kind of a hack-ish way by defining a function on my web page
function fnSendProgress(progress) //define this for site.js.
{
//do something here
}
and then in site.js calling it by
messageHub.client.sendProgress = function (progress) {
debugger;
//have a definition of a function called fnSendProgress(progress)
//if found it will call it else call default
if (typeof fnSendProgress == 'function') {
fnSendProgress(progress);
}
else {
//do something at site.js level
}
};
It works, but I was wondering if there is a cleaner method.
Any help much appreciated.
Use closures:
function createfunction(callback)
{
return function(progress) { callback(progress); }
}
messageHub.client.sendProgress = createFunction(callback)
First you define a function "createFunction", which given a callback returns another function with one argument, which calls the specified callback.
Then you assign the result of calling that function to signalr's sendProgress.
In other words: for each callback you want you dynamically generate a function with one argument, which calls the callback and passes the argument to it.
I am evaluating SignalR (which happens to be used with Knockoutjs) to see if we can use it to notify clients of concurrency issues. Basically user "a" saves a record and users "b,c,d,e,f,g" are notified. I basically have an example working that notifies all clients. So I think I am almost there.
I came across this link and it lead me on the current path that I am on. I have also been looking at the documentation on Github.
Basically I want to exclude the a single client from the Clients.method() call. I dont see a way to loop through the clients and check the ClientId. The only other I can see to accomplish this is to maybe look at using the groups to keep track of it, but that seemed a little cumbersome, but I was having issues with that as well.
public class TicketHub : Hub
{
static int TotalTickets = 10;
public void GetTicketCount()
{
AddToGroup("ticketClients");
Clients.setTicketCount(TotalTickets);
}
public void BuyTicket()
{
if (TotalTickets > 0)
TotalTickets -= 1;
RemoveFromGroup("ticketClients");
// This will call the method ONLY on the calling client
// Caller.updateTicketCountWithNotification(TotalTickets);
// This will call the method on ALL clients in the group
Clients["ticketClients"].updateTicketCountNotify(TotalTickets);
AddToGroup("ticketClients");
Caller.updateTicketCountDontNotify(TotalTickets);
}
}
javascript code:
<script type="text/javascript">
$(document).ready(function () {
var test = $.connection.test;
$("#btnTest").click(function () {
test.testMethod();
});
test.show = function (text, guid) {
if (guid != test.guid) //notify all clients except the caller
alert(text);
};
$.connection.hub.start(function () { test.start(); });
});
</script>
Class :
public class Test : Hub
{
public void Start()
{
Caller.guid = Guid.NewGuid();
}
public void TestMethod()
{
Clients.show("test", Caller.guid);
}
}
If you want to exclude the caller from the call to the client side method you can use:
Clients.Others.clientSideMethod();
There is also Clients.AllExcept(...) that allows excluding certain people.
I have a fairly long process running in a standard postback. Then I have the following page method to return progress to a repeating JavaScript function that is supposed to report on progress.
My simple page method:
[WebMethod]
public static int GetProgress()
{
return (int)(HttpContext.Current.Session["ActivationResources.ImportProgress"] ?? 0);
}
My clientside script:
function startProgress() {
window.setInterval(updateImportProgress(), 500);
}
var importProgress = 0;
function updateImportProgress() {
//debugger;
PageMethods.GetProgress(function (result, response, context) {
if (result == importProgress) {
$("#messageLabel").append(" .");
}
else {
$("#messageLabel").html("Busy importing resources - " + result + "%");
}
importProgress = result;
});
}
The updateImportProgress function is called, but Firebug reports that the POST for GetProgress is 'aborted'. Why could this be? I suspect that this is because the call to the static method is blocked by the actual executing method whose progress I am trying to monitor. A breakpoint in the GetProgress method is never hit.
I had this issue before. As a workaround I've implemented the code in an ashx file and avoided that way to touch the same page. Should work for your code as well, as you simply report a session variable.
Btw, note that your code breaks when you have 2 requests running in a single session. That can happen when you open multiple tabs of the same page in the browser.
I am trying to make an anchor tag cause both client and server validation. I have this code for now:
$(document).ready(function () {
$('div#imgEmailVerifyLoader').hide();
$('a#btn_SubmitContactMessage').click(function ()
{
if (Page_ClientValidate()) // this will trigger all validators on page
{
$('div#imgEmailVerifyLoader').show('slow');
window.Form_OnMasterPage.submit();
return true;
}
else
{
return false;
}
});
});
<a id="btn_SubmitContactMessage" href="Contact.aspx" onclick="Validate();" runat="server">SUBMIT</a>
This performs client validation properly and shows the error message. I have validation controls for each of the textboxes on the page. I also added a server click event handler in code behind for this:
btn_SubmitContactMessage.ServerClick +=new EventHandler(btn_SubmitContactMessage_ServerClick);
}
protected void btn_SubmitContactMessage_ServerClick(object sender, EventArgs e)
{
if (!Page.IsValid)
{
RequiredFieldValidator4.ErrorMessage = "show";
return;
}
}
But when I try to test it by turning off javascript the link(submit) does not postback. Why is that happening?
Now, how do I make sure that validation is being done on the server side to after postback.
I would imagine it's because of the 'onclick=validate()'. Instead of doing that you should register that event inside of '$(document).ready(function ()' like you've got your other JavaScript. That way if JavaScript is not available the form is submitted normally and your server side validation kicks in.