I have a question regarding ASP.NET server controls with client functionalities. I want to improve the client-side event handling: when a child client-control fires an event, I want the parent client-control to be directly notified.
Let's assume we have two server controls, MyWindow and MyTextBox. Both are client-side objects of type "Sys.UI.Control".
The MyTextBox is a child of MyWindow:
public class MyWindow : Control, IScriptControl
{
private MyTextBox _mtb;
//....
protected override void CreateChildControls()
{
_mtb = new MyTextBox();
_mtb.ID = "MTB";
_mtb.OnClientTextChanged = "OnClientTextChanged";
Controls.Add(_mtb);
}
//...
public IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
ScriptControlDescriptor descriptor = new ScriptControlDescriptor("Test.MyWindow", ClientID);
descriptor.AddComponent("myTextBox", _mtb.ClientID);
yield return descriptor;
}
client-side:
function OnClientTextChanged(sender, args) {
alert('test')
}
Now, whenever the text of the textbox changes, the client-event is fired and calls the function "OnClientTextChanged".
What I want is to notify the client object of "MyWindow". I could do that this way:
function OnClientTextChanged(sender, args) {
$find("MyWindow").textBoxChangedItsText();
}
But how can I notify the client object of MyWindow directly without using this global javascript function? What I tested was
_mtb.OnClientTextChanged = string.Format("($find('{0}')).textBoxChangedItsText", ClientID);
but I the "textBoxChangedItsText" function inside the client object can not access the object itself - "this" is the function itself but not the object which I would find using "$find("MyWindow")"
I hope the question is clear to persons with knowledge in client-side enabled AJAX server side controls.
Thanks!
Edit: Using this event hander on the client-side works:
server-side:
_mtb.OnClientTextChanged = string.Format(" ($find('{0}')).textBoxChangedItsTextDelegate", ClientID);
client-side:
Test.MyWindow = function (element) {
Test.MyWindow.initializeBase(this, [element]);
this.textBoxChangedItsTextDelegate= Function.createDelegate(this, function (sender, e) {
this.textBoxChangedItsText();
});
};
Test.MyWindow.prototype = {
initialize: function () {
Test.MyWindow.callBaseMethod(this, 'initialize');
},
textBoxChangedItsText: function () {
alert(this.get_id());
},
dispose: function () {
Test.MyWindow.callBaseMethod(this, 'dispose');
}
};
What I still don't like is the attaching to the event server-side with the $find in the event handler: _mtb.OnClientTextChanged = string.Format(" ($find('{0}')).textBoxChangedItsTextDelegate", ClientID);
I suppose that you need to define events as it basically done in ASP.NET Client Side controls. So it is necessary to add the following to your MyTextBox client side object prototype:
add_textChanged: function(handler) {
this.get_events().addHandler('shown', handler);
}
remove_TextChanged: function (handler) {
this.get_events().removeHandler('textChanged', handler);
}
raise_textChanged: function (eventArgs) {
var handler = this.get_events().getHandler('textChanged');
if (handler) {
handler(this, eventArgs);
}
}
This will define client side event to which client code (parent controls or page) can subscribe and perform required actions. To raise this event in your MyTextBox you can use the code like this:
this.raise_textChanged(Sys.EventArgs.Empty);
And to use this event in MyWindow client side object you need to modify your code to the following:
Test.MyWindow = function (element) {
Test.MyWindow.initializeBase(this, [element]);
this._myTextBoxID = null;
this._onTextChanged$delegate = Function.createDelegate(this, this._onTextChanged);
};
Test.MyWindow.prototype = {
initialize: function () {
Test.MyWindow.callBaseMethod(this, 'initialize');
$find(this._myTextBoxID).add_textChanged(this._onTextChanged$delegate);
},
_onTextChanged: function(sender, eventArgs) {
// Perform required actions.
// this - will be instance of the this client side object.
}
dispose: function () {
$find(this._myTextBoxID).remove_textChanged(this._onTextChanged$delegate);
Test.MyWindow.callBaseMethod(this, 'dispose');
}
};
Note: in the example provided I used this._myTextBox which equals to client id of the child MyTextBox control. You can use property "myTextBox" without performing $find because you initialize it using descriptor.AddComponent("myTextBox", _mtb.ClientID);
Related
I'd like some insight into a little problem I'm encountering with a custom JS plugin that I've made. It's reasonably simple, I'm using a plugin JS template to write out plugins, which I'll attach. I'm then initialising the plugin, and ideally need to figure out if there's a way I can check if the plugin JS file is loaded before doing anything:
plugin template
(function() {
this.MyPlugin = function() {
// default settings
const INBOUND_CONFIG = {
isEnabled: false
}
// Create options by extending defaults with the passed in arugments
if (arguments[0] && typeof arguments[0] === "object") {
this.options = extendDefaults(INBOUND_CONFIG, arguments[0]);
}
// custom public method
HoneycombInbound.prototype.getSettings = function() {
}
// Utility method to extend defaults with user options
function extendDefaults(source, properties) {
var property;
for (property in properties) {
if (properties.hasOwnProperty(property)) {
source[property] = properties[property];
}
}
return source;
}
}
}());
With the above, I'd initialise the plugin in HTML as follows:
<script src="./my-plugin.js"></script>
<script>
const plugin = new MyPlugin()
// this doesn't work if the script file isn't linked:
if (plugin) {
// do something
}
</script>
Unfortunately, a simple check of plugin doesn't actually work if the JS file isn't loaded for instance, I've even tried something like: if (new MyPlugin()) {} with little hope.
I essentially need a way of not throwing an error in the browser. Any solution I can use to check correctly if it's loaded before executing?
A solution that has worked for me is to work with the WebAPI postMessage
I've used to communicate with the client, that the plugin is ready.
So on the plugin you will need something like this:
window.postMessage("The plugin is ready to go" );
And on your client you will need to add a new listener that can catch the message:
window.addEventListener("message", myMessage, false);
const myMessage = (event) => {
if (event.data === "The plugin is ready to go")
// This is your message so do your stuff here
...
}
EDIT
So to achieve this behavio, you will need to enable on the client sidethe initialization:
const myPlugin = {
init: () => {
// Initilize the plugin if you are using the DOM you can add a parameter with an id to mount it whenever you need it
//Otherwise just configure or initilize the variables that you will use
// And here you need to pass the messsage to tell the client that everyting is ready
// Here you need to configure the origin correctly depending on your needs check the documentation for more details
window.addEventListener("message", myMessage, false);
},
someFunctionality: () => {
//This pattern is to expose the funtionalities to the client so you can achieve any
}
};
So now on your client you just need to add your script and tell the plugin to initialize:
<script type='text/javascript' src='/myPlugin.js'></script>
<script>
window.addEventListener("message", myMessage, false);
const myMessage = (event) => {
if (event.data === "The plugin is ready to go")
// This is your message so do your stuff here
...
}
myPlugin.init();
</script>
My web page has
var bc = new BroadcastChannel('Consumer');
bc.onmessage = function(event) {
alert("a");
}
bc.postMessage("hello");
It broadcasts a message, and the page is also required to receive the same message.
However it doesn't work. Did I miss anything?
You can create two instances of BroadcastChannel on your page. One can act as a broadcaster for messages, the other one for receiving messages.
var broadcaster = new BroadcastChannel('Consumer');
var messageReceiver= new BroadcastChannel('Consumer');
messageReceiver.onmessage = function(event) {
alert(event.data);
}
broadcaster.postMessage("hello");
See this in action: https://jsfiddle.net/h56d3y27/
Or wrapped in a reusable class:
(note: class is not supported by all browsers. See : https://caniuse.com/#search=class for browser compatibility)
class AllInclusiveBroadcaster {
constructor(listener, channelName) {
if (!channelName) channelName = "channel";
this.broadcaster = new BroadcastChannel(channelName);
this.messageReceiver = new BroadcastChannel(channelName);
this.messageReceiver.onmessage = (event) => {
listener(event.data);
}
}
postmessage(data) {
this.broadcaster.postMessage(data);
}
}
var broadcaster = new AllInclusiveBroadcaster((data) => alert(data));
broadcaster.postmessage("Hello BroadcastChannel");
See this also in action a JSFiddle
You could dispatch an event (call it what you like) to, say, document, with the same data ... then have a single handler that listens for BroadcastChannel messages and to the event name you created above
in the following, the code creates and listens for fakeBroadcastMessage
created a function to send both the bc message and the "local" message
var bc = new BroadcastChannel('Consumer');
function handleBroadcastMessage(event) {
// do things here
}
bc.addEventHandler('message', handleBroadcastMessage);
document.addEventListener('fakeBroadcastMessage', handleBroadcastMessage);
function sendMessage(data) {
bc.postMessage(data);
var ev = new Event('fakeBroadcastMessage');
ev.data = data;
document.dispatchEvent(ev);
}
sendMessage('hello');
I have a simple app, which displays a list of available signalR hubs. A user selects a hub and it connects to it, this subscribes an event to add messages to a table on the page. The user can then send messaged to that hub which will also fire the subscription adding that message to the table. This all works great.
Now if the user selects another hub, the app connects and sets up a new subscription, however the original subscription still fires causing duplicate messages to be added to the table. Each time the hub is changed further subscriptions get added causing one send to result in many messages in the table.
I have tried disconnecting the hub, disposing the hub and trying to remove the subscription with hubProxy.off(eventName), but nothing seems to work, other than a page reload.
Here is the code I have just added the onHub changed function as this is where everything is happening.
Any ideas appreciated. :)
function HubViewModel() {
var self = this;
self.hubConnection = '';
self.hub = '';
$.getScript("../signalR/hubs");
self.hubs = ko.observableArray();
self.selectedHub = ko.observable();
self.messageText = ko.observable();
self.messageCollection = ko.observableArray();
self.hubChanged = function () {
// Setup hub connection.
$.connection.hub.url = "../signalR";
self.hubConnection = $.hubConnection();
// Get the selected hub name.
var selectedHubName;
_.each(self.hubs(), function(item) {
if (item.hubId == self.selectedHub()) {
selectedHubName = item.hubName;
}
});
// Check for a selected connection
if (self.selectedHub()) {
// Create proxy.
self.hub = self.hubConnection.createHubProxy(selectedHubName);
// Remove any existing listener(s).
self.hub.off('addNewMessageToPage');
// Setup listener.
self.hub.On('addNewMessageToPage', function (sender, message) {
self.messageCollection().push({ hubName: selectedHubName, name: selectedHubName, message: message, dateTime: new Date().toLocaleString() });
$('#hubMessageGrid').dxDataGrid('instance').refresh();
});
// start connection.
self.hubConnection.start()
.done(function() {
toastr.success('hub connected');
$('#sendMessageButton').click(function() {
self.hub.invoke('sendAll', 'hub management page', self.messageText());
self.messageText('');
});
})
.fail(function(error) {
toastr.error('hub connection ' + error);
});
}
};
You can to disconnect the hub first by calling the self.hub.stop(); function
You need to pass the exact same handler instance when unsubscribing. Passing a different instance (even if the function body is the same) will not remove the handler.
See https://learn.microsoft.com/en-us/javascript/api/#microsoft/signalr/hubconnection?view=signalr-js-latest#off-string---args--any-------void-
I have an IdentificationView that has to call a method of another view in different app:
var NameAndIdentificationView = Application.Views.ItemView.extend({
loadMailingAddressView: function() {
//call to mailing address view method setAddress() from here
}
});
Now my MailingAddressView is something like this:
var MailingAddressView= Application.Views.ItemView.extend({
setAddress: function(address) {
if (address != null) {
// this.autoCompleteBox.data('inputData') = address;
ProWeb.EXTRACTED_ADDRESS = address;
this.model.set('mailingAddress', address);
}
}
});
I would like to know what is the way to call a method of one view from another. Please provide suggestions.
EDIT:
This is how my application has been setup:
First App is called.
Then the Controller is called.
From within the Controller, View is called and intialized.
So from within my NameIdentificationView, I'm calling the MailingAddress app that in turn controls all the process of calling the controller and it's view.
You can do it many ways using event channel,triggering and listening events on shared model, the simpler way of doing this is using Bsckbone events.
Trigger a event in caller
loadMailingAddressView: function () {
//call to mailing address view method setAddress() from here
Backbonw.trigger('setAddress')
})
and listen in callee's initalize
var MailingAddressView = Application.Views.ItemView.extend({
initialize: function() {
Backbone.on('setAddress', this.setAddress)
},
setAddress: function(address) {
if (address != null) {
// this.autoCompleteBox.data('inputData') = address;
ProWeb.EXTRACTED_ADDRESS = address;
this.model.set('mailingAddress', address);
}
},
});
make sure callee View is initialized by the time you trigger event
In my page there is a frame that belongs to the same domain. The content of this frame is varied and relatively unpredictable. Whenever a user clicks a button (inside the frame) that performs a post, I need to execute a function that performs some UI tasks. The problem is that I cannot edit the source of these frames for reasons beyond my control. Some of these buttons are simple form submit buttons, but others do not directly submit the form, but instead have an onclick handler that performs some checks and might submit.
Here is the problem: How do I detect if one of these onclick handlers called form.submit()? If there's no handler, then obviously I can set up a handler for onsubmit(), but is not the case for all of these buttons.
This is my code so far:
function addEventBefore(element, type, before, after) {
var old = element['on' + type] || function() {};
before = before || function() {};
after = after || function() {};
element['on' + type] = function () {
before();
old();//I can't modify this old onclick handler
after();
};
}
function setup() {
console.log('setup');
}
function takedown() {
// In this method, I want to know if old() caused a form submit
console.log('takedown');
}
function $includeFrames(jQuery, selector) {
return jQuery(selector).add(jQuery('iframe').contents().find(selector));
}
var a = $includeFrames($, 'input[type="submit"], input[type="button"]').each(function() {
var elem = $(this)[0];
addEventBefore(elem, 'click', setup, takedown);
});
In the onload event of the iframe you'll need to hook up an event listener to each form in the iframed page. You need to do this on every load, as each fresh page needs new listeners.
$("#someIframe").on('load',function() {
$(this).contents().find("form").each(function() {
$(this).on('submit',function() {... your code...})
})
}
The solution that worked for me came from a friend of mine. The solution is to shim the form.submit() function.
$(function() {
var el = document.getElementById('myform');
el.submit = function(fn) {
return function() {
myFunctionGoesHere();
fn.apply(this, arguments);
};
}(el.submit);
});
Here is a working example: http://jsfiddle.net/hW6Z4/9/