I have a Canvas element which has some custom functionality written in Javascript. I'm trying to create a wrapper for this functionality in a Blazor Webassembly component.
The Javascript code generates custom events using the dispatchEvent method:
const event = new CustomEvent("uvrectadded", {
bubbles: true,
detail: { uvrect: uvr }
});
const result = this.canvas.dispatchEvent(event);
How to I listen for this event in a Blazor serverside component? I have tried the following:
<canvas id="map-canvas" #uvrectadded="OnUVRectAdded"></canvas>
#code{
private void OnUVRectAdded(EventArgs e)
{
Console.WriteLine("Blazor: ONUVRectAdded");
}
}
In the code above, the directive is not compiled, but I can't find out how to register an event directive.
Aside: I have also tried raising a change event from the Javascript component, and this does get registered by the Blazor code with an #onchange directive, however, an exception is thrown saying the system cannot deserialize the event.
In Blazor, you can register a directive using an EventHandlerAttribute like this:
[EventHandler("onuvrectadded", typeof(EventArgs))]
public static class EventHandlers
{
}
However, I am having some issues deserializing the detail property of the CustomEvent.
Im struggling too. I can see the JS event being raised (have added a JS listener to check is actually raised).
I have registered the event, and defined the C# part of it as well.
[EventHandler("oncustomjsevent", typeof(CustomJSEventArgs), enableStopPropagation: true, enablePreventDefault: true)]
public static class EventHandlers
{
// This static class doesn't need to contain any members. It's just a place where we can put
// [EventHandler] attributes to configure event types on the Razor compiler. This affects the
// compiler output as well as code completions in the editor.
}
public class CustomJSEventArgs : System.EventArgs
{
// Data for these properties will be supplied by custom JavaScript logic
public string EventData { get; set; }
}
But cant get Blazor to see the event anywhere. Have tried adding to divs, buttons, labels, etc still doesnt work., eg.
<Button #oncustomjsevent="OnLinkClick" />
private void OnLinkClick(CustomJSEventArgs eventArgs)
{
throw new InvalidCastException(eventArgs.EventData);
var x = 1;
}
Related
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;
}
}
I have a custom control for building forms to automatically generate a label-input field pair. But I need to be able to modify attributes of the generated label-input field pair in my vb code behind file. The problem is that the code behind file seems to be applied before the custom control. How do I work around this issue?
You could expose the inner controls as public properties of your custom control.
For example:
public class MyCustomControl : CustomControl
{
Label _label;
// initialize _label in OnInit() ...
public string LabelText { get { return _label.Text; } set { _label.Text = value; } }
}
Consider a scenario where I have a WebBrowser Control in WPF application.
A web page is loaded inside WebBrowser Control. The web page contains a button.
The web page is of ASP.NET application.
I want to capture the button click event of the webpage into WPF Form (which hosts WebBrowser Control). Is there any way to achieve this functionality ?
Thanks,
Tapan
Here is code that should do exactly what you want with comments to explain what is going on:
public partial class MainWindow : Window
{
/// <summary>
/// This is a helper class. It appears that we can't mark the Window as ComVisible
/// so instead, we'll use this seperate class to be the C# code that gets called.
/// </summary>
[ComVisible(true)]
public class ComVisibleObjectForScripting
{
public void ButtonClicked()
{
//Do whatever you need to do. For now, we'll just show a message box
MessageBox.Show("Button was clicked in web page");
}
}
public MainWindow()
{
InitializeComponent();
//Pass an instance of our helper class as the target object for scripting
webBrowser1.ObjectForScripting = new ComVisibleObjectForScripting();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
//Navigate to your page somehow
webBrowser1.Navigate("http://www.somewhere.com/");
}
private void webBrowser1_LoadCompleted(object sender, NavigationEventArgs e)
{
//Once the document is loaded, we need to inject some custom JavaScript.
//Here is the JavaScript
var javascript = #"
//This is the JavaScript method that will forward the click to the WPF app
function htmlButtonClicked()
{
//Do any other procession...here we just always call to the WPF app
window.external.ButtonClicked();
}
//Find the button that you want to watch for clicks
var searchButton = document.getElementById('theButton');
//Attach an onclick handler that executes our function
searchButton.attachEvent('onclick',htmlButtonClicked);
";
//Grab the current document and cast it to a type we can use
//NOTE: This interface is defined in the MSHTML COM Component
// You need to add a Reference to it in the Add References window
var doc = (IHTMLDocument2)webBrowser1.Document;
//Once we have the document, execute our JavaScript in it
doc.parentWindow.execScript(javascript);
}
}
Some of this was taken from http://beensoft.blogspot.com/2010/03/two-way-interaction-with-javascript-in.html
I am trying to make a validator that will make sure that at least 2 items are selected. The validator works correctly on the server side but the client side code never gets executed.
Here is the code:
Sys.Mvc.ValidatorRegistry.validators["country"] = function (rule) {
var min = rule.ValidationParameters["min"];
return function (value, context) {
if (value >= min) return true;
return rule.ErrorMessage;
};
};
And here is the validator code:
public class CountryValidator : DataAnnotationsModelValidator<CustomValidations.CountryAttribute>
{
private int _minimum;
private string _message;
public CountryValidator(ModelMetadata metadata, ControllerContext context, CustomValidations.CountryAttribute attribute) : base(metadata,context,attribute)
{
_minimum = attribute.Minimum;
_message = attribute.ErrorMessage;
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var rule = new ModelClientValidationRule()
{
ErrorMessage = _message,
ValidationType = "country"
};
rule.ValidationParameters.Add("min", _minimum);
return new[] { rule };
}
}
I have even registered the validation adapter in global.asax file:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(AgeAttribute), typeof(AgeValidator));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(CountryAttribute),typeof(CountryValidator));
}
I am thinking that the validator only works with the elements that have a value property like textboxes etc.
UPDATE 1:
EnableClientValidation is invoked correctly and all the required JS files are included in the project. It seems like I need to attach the onblur to the context. I will try that and post the results.
<% =Html.EnableClientValidation(); %> needs to be in your view somewhere. Also make sure you reference MicrosoftAjax.js and MicrosoftMvcValidation.js in the same view (before your js function).
Either your missing MicrosoftMvcAjax.js or you need to implement your custom validation in jQuery as described on Mr. Haack's website http://haacked.com/archive/2009/11/19/aspnetmvc2-custom-validation.aspx.
I think it is because the default validation is invoked on the onblur event of the input textbox. And for a listbox this event was not being thrown.
I recently read Phil Haack's post where he gives an example of implementing Model View Presenter for ASP.NET. One of the code snippets shows how the code for the view class.
public partial class _Default : System.Web.UI.Page, IPostEditView
{
PostEditController controller;
public _Default()
{
this.controller = new PostEditController(this, new BlogDataService());
}
}
However, here the view constructs the instance of the BlogDataService and passes it along to the presenter. Ideally the view should not know about BlogDataService or any of the presenter's lower layer dependencies. But i also prefer to keep the BlogDataService as a constructor injected dependency of the presenter as it makes the dependencies of the presenter explicit.
This same question has been asked on stackoverflow here.
One of the answers suggests using a service locator to get the instance of the BlogDataService and passing it along to the presenter's constructor.This solution however does not solve the problem of the view knowing about the BlogDataService and needing to explicitly get a reference to it.
Is there a way to automatically construct the presenter object using an IoC or DI container tool such that the view does not have to deal with explicitly creating the BlogDataService object and also injecting the view and service instances into the presenter's constructor. I prefer to use the constructor injection pattern as far as possible.
Or is there a better design available to solve the problem?. Can there be a better way to implement this If i am building a WinForms application instead of a ASP.NET WebForms application?
Thanks for any feedback.
Yes there is. For example using StructureMap in a webform constructor:
public partial class AttributeDetails : EntityDetailView<AttributeDetailPresenter>, IAttributeDetailView
{
public AttributeDetails()
{
_presenter = ObjectFactory.With<IAttributeDetailView>(this).GetInstance<AttributeDetailPresenter>();
}
....
}
and as you can see here presenter needs view and service injected
public AttributeDetailPresenter(IAttributeDetailView view, IAttributeService attributeService)
{
MyForm = view;
AppService = attributeService;
}
You can also use StructureMap BuildUp feature for webforms so that you can avoid using ObjectFactory directly in your view.
I did exactly this. The solution is based on Autofac, but can be implemented on top of any container.
First, define an interface representing the authority for presenting views in a request to the MVP system:
public interface IMvpRequest
{
void Present(object view);
}
Next, create a base page which has a property of that type:
public abstract class PageView : Page
{
public IMvpRequest MvpRequest { get; set; }
}
At this point, set up dependency injection for pages. Most containers have ASP.NET integration, usually in the form of HTTP modules. Because we don't create the page instance, we can't use constructor injection, and have to use property injection here only.
After that is set up, create event arguments representing a view which is ready to be presented:
public class PresentableEventArgs : EventArgs
{}
Now, catch the events in PageView and pass them to the request (present the page as well):
protected override bool OnBubbleEvent(object source, EventArgs args)
{
var cancel = false;
if(args is PresentableEventArgs)
{
cancel = true;
Present(source);
}
else
{
cancel = base.OnBubbleEvent(source, args);
}
return cancel;
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
Present(this);
}
private void Present(object view)
{
if(MvpRequest != null && view != null)
{
MvpRequest.Present(view);
}
}
Finally, create base classes for each type of control you'd like to serve as a view (master pages, composite controls, etc.):
public abstract class UserControlView : UserControl
{
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
EnsureChildControls();
RaiseBubbleEvent(this, new PresentableEventArgs());
}
}
This connects the control tree to the MVP system via IMvpRequest, which you'll now have to implement and register in the application-level container. The ASP.NET integration should take care of injecting the implementation into the page. This decouples the page entirely from presenter creation, relying on IMvpRequest to do the mapping.
The implementation of IMvpRequest will be container-specific. Presenters will be registered in the container like other types, meaning their constructors will automatically be resolved.
You will have some sort of map from view types to presenter types:
public interface IPresenterMap
{
Type GetPresenterType(Type viewType);
}
These are the types you will resolve from the container.
(The one gotcha here is that the view already exists, meaning the container doesn't create the instance or ever know about it. You will have to pass it in as a resolution parameter, another concept supported by most containers.)
A decent default mapping might look like this:
[Presenter(typeof(LogOnPresenter))]
public class LogOnPage : PageView, ILogOnView
{
// ...
}