Using SignalR to notify current caller - javascript

The short version
In my POST action routine, how can I send a notification to some JavaScript running in the client? It seems that SignalR should be an easy solution, but I don't know how to do a callback to the SignalR client that lives in JavaScript in the POST action's client page.
Specifically, it seems that I need a "connection ID" in order to ask SignalR to talk to a specific client, but I don't know how to get one of those either in my POST action or in the client JavaScript.
The whole (not so ugly) story
In my MVC app, my POST action may take a long time to complete. If the POST action decides that it will take a long time to complete, I want to notify some JavaScript on the page so that it can display a "please wait..." notification to the user.
This seems like something that SignalR should make really easy for me. Following the basic tutorial, I added a Hub and created a JS callback in my view:
Note that the Hub has no methods. My client only needs a read-only notification. It has no need to call any methods on the Hub to write message to the outside world.
public class MyHub: Hub
{
}
The view just has a form with a submit button and a hidden "Please wait" message that the SignalR routine can display. The view model has a string property that I can use to pass the SignalR "connection ID" to the POST controller (assuming I can figure out how to get it).
#model SignalRTest.Models.MyViewModel
#using (Html.BeginForm()) {
#Html.HiddenFor(m => m.SignalrConnectionId)
<button type="submit" class="btn btn-primary">Go to it!</button>
}
<div id="hidden-msg" hidden="hidden">
<p>Please wait...</p>
</div>
#section scripts {
<!-- Reference the SignalR library. -->
<script src="~/Scripts/jquery.signalR-2.1.2.min.js"></script>
<!-- Reference the autogenerated SignalR hub script. -->
<script src="~/signalr/hubs"></script>
<!-- SignalR script to update the page -->
<script>
$(function () {
// Get a reference to the server "hub" class (camelCase)
var hub = $.connection.interviewDoneHub;
// Get our connection ID and store it in a hidden field
// so that it is sent to the POST action
// var connectionId = $.connection.hub.id; //This doesn't work!
// $('##Html.IdFor(m => m.SignalrConnectionId)').attr(connectionId, '');
// Create a function that the hub can call
hub.client.myCallback = function () {
$('#hidden-msg').show();
};
// Start the connection.
$.connection.hub.start().done(function () {
});
});
</script>
}
Meanwhile back in my POST controller action, I call the JS callback:
[HttpPost]
public async Task<ActionResult> MyAction(MyViewModel model)
{
// We've decided that this will take a while. Tell the client about it...
var context = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
context.Clients.All.myCallback();
//context.Clients.Client(model.SignalrConnectionId).myCallback();
await Task.Delay(2000);
return RedirectToAction("NextPage");
}
Now, my problem: As a proof-of-concept test, I use this code to call the JS callback:
var context = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
context.Clients.All.myCallback();
which works just dandy. But, obviously, I want to call the specific client associated with the POST action. C# Intellisense tells me that I should be able to call
context.Clients.Client("connectionId").myCallback();
but I can't figure out how to get the desired "connectionId" string. I don't think I'll be able to get the client ID in the POST controller because I don't have any sort of SignalR connection to the client at that point. I figured I'd let the client get its connection ID and give it to the POST controller in the view model, but I haven't found the magic JS code that fetches the connection ID out of the SignalR framework.
I found several SO articles that stated matter-of-factly:
connectionId = $.connection.hub.id;
but this returns undefined. I found this SignalR JS Client Wiki page, which says:
connection.id
Gets or sets the client id for the current connection
but that also returns undefined.
Which gets back to my original question. Maybe I'm not understanding something fundamental about what a connection ID is...

Okay I believe I see what the issue is. You want to start the your connection in javascript
$.connection.hub.start()
After you do that, then your should be able to do connection.hub.id - if the connection is not started then there will be no connection id at all, which is why you are getting an "undefined" value because it has not been set until you start the connection.
Actually this could be a bastardization of things, not sure if it will work but should require minimal changes to your code. Each time a client connects to your hub you can add this to the connect method
Groups.Add(Context.ConnectionId, Context.ConnectionId);
// name it the same as your connection id since that is the identified you are using
Then in your controller you can add the following in your action call
var context = GlobalHost.ConnectionManager.GetHubContext<InterviewDoneHub>();
context.Clients.Groups(connectionId).myCallback();
If the above does not work then you are going to have to connect to the hub using .NET Client. Just like you connected from javascript you can connect to the hub using C# and call a method that will then notify the client you want notified. I did this using WebAPI in the past but I am fairly certain that it can be done in Asp.Net MVC as well.

Thanks to GJohn for the reference to the .NET Client docs, which contain this example:
$.connection.hub.start()
.done(function(){ console.log('Now connected, connection ID=' + $.connection.hub.id); })
.fail(function(){ console.log('Could not Connect!'); });
});
So, it turns out that $.connection.hub.id does, in fact, return the connection ID. But I was trying to call that routine at page load time, before the connection is established! The correct JS code for my view looks like this:
#section scripts {
<!-- Reference the SignalR library. -->
<script src="~/Scripts/jquery.signalR-2.1.2.min.js"></script>
<!-- Reference the autogenerated SignalR hub script. -->
<script src="~/signalr/hubs"></script>
<!-- SignalR script to update the page -->
<script>
$(function () {
// Get a reference to the server "hub" class (camelCase)
var hub = $.connection.interviewDoneHub;
// Create a function that the hub can call
hub.client.myCallback = function () {
$('#hidden-msg').show();
};
// Start the connection.
$.connection.hub.start()
.done(function () {
// Get our connection ID and store it in a hidden field
// so that it is sent to the POST action
$('##Html.IdFor(m => m.SignalrConnectionId)').attr('value', $.connection.hub.id);
})
.fail(function () { });
});
</script>
}
But now I have a minor problem: I need to add a bit more JS code to disable the submit button when the form loads, and re-enable it in the connection "done" function; otherwise the user can click on the submit button before the connection ID exists. I'll leave that as an exercise for the reader...

Related

How do I pass a variable to JavaScript using Laravel in order to broadcast on a private channel?

I am building an app in Laravel 9.42.2 and have set up Laravel Echo and Soketi to broadcast events. I can successfully send and receive broadcasts on public channels, but I can't figure out how to broadcast on a private channel.
According to the docs I can do this with the example code below:
Echo.private(`orders.${orderId}`)
.listen('OrderShipmentStatusUpdated', (e) => {
console.log(e.order);
});
What I don't understand is where the ${orderId} is passed to the JavaScript. I can create the private channel on the backend in PHP, but I don't know where the front end should be receiving the variables it needs to fill in the placeholders in the listener.
Options I have considered:
Query the database at the front of every response and send a list of all possible ID's back to the user to store for use as needed (i.e. get all orderId's related to the user as an array and use a loop to create a listener for each of them). I'm concerned this would add a lot of unnecessary trips to the database and overhead to the page load times for something that might not be needed.
Add a line in the Controller to parse the orderId to json just before calling the event dispatch. Not sure why this feels wrong, but it does.
You can do via this tricks.
The orderId can be a part of your page url or can be represented by a hidden element in your template, for example:
<input type="hidden" id="orderId" value="{{$orderId}}" />
In case you choose hidden element, just get its value and pass to the Echo.private() method.
var orderId = document.getElementById('orderId').value;
window.Echo.private(`orders.` + orderId)
.listen('OrderShipmentStatusUpdated', (e) => {
console.log(e);
});

Asp.net MVC, calling C# method from client-side by onclick JS event

I got "MultiLanguageProvider" which is ordinary C# class, not Controller or Model. The idea is when user clicks Change language - it must call back server-side void ChangedLanguage() on MultiLanguageProvider instance. This doesn't work at all:
#MultiLanguageProvider.Instance.SelectAppropriate("на русском", "in english")
- 'cause all the code inside #{ } get executed immideately - at the time of page-load. I am not informed about AJAX, so maybe someone of u can show me the right direction to do this simply job?
I don't understand which one you are trying to invoke on anchor tag click. But if I understand the essence of your question, you are trying to call some server side method to change the language, and here I assume you want to save the language selection that was made on user interface (UI). If this is what you are looking, on client side, you do the changes suggested by Stephen Muecke. On server side, you need to add a [HTTPPOST] action method on controller something like:
public ActionResult SwapLanguage(LanguageViewModel languageViewModel)
{
//do the save action => like saving to database
return Json(new { data = {urlToRedirt: "/someUrl_GeneratedVia_UrlHelper"},
status = "success",
message = "Language Changed Successfully" }, JsonRequestBehavior.AllowGet);
}
}
On Client side:
$('#swaplanguage).on('click', function(event) {
event.preventDefault();
$.post( '#Url.Action("SwapLanguage")', function( data ) {
// Handle the server side response here.
// Like if you want to redirect, use something like:
// window.location = data.urlToRedirt;
});
}
Of course, you need to handle error conditions on both client as well as server side.
If you don't want to save anything, and you just want to redirect user to some url based on the language user selects, then your onclick event handler is something like:
$('#swaplanguage).on('click', function(event) {
urlToRedirect = 'url_to_redirect' + '/' + $('#languageDropDownId').val();
window.location = urlToRedirect;
}
Finally, your anchor tag is:
<a id="swaplanguage">Change Language</a>
Hope that helps.

Call to MapSignalR causes Protocol error

this is a follow-up to my previous question here..
MVC - trouble linking to another Controller/Action
as you can see, i eventually did get my view from another controller to display in a new tab so it was working. that is until i installed SignalR. the simple version using this tutorial as a guide..
http://www.asp.net/signalr/overview/getting-started/tutorial-getting-started-with-signalr-and-mvc
the tutorial worked fine after following the steps to create a project. the only thing i had to do to make it work was change the version of the jquery signalr javascript file to the latest (it was one i didn't have because the tutorial was written in older VS 2012).
in any case, after following the same steps for my site, i now get an error when i click the link for /SignalR/SRStart (new tab)..
Protocol error: Unknown transport
playing around i found that this only happens after calling app.MapSignalR() in the startup.cs file. can't understand why since the tutorial i followed worked fine unless it has something to do with crossing over into another controller on that link. it's in the SRStart view that i placed all the signalr connection code and callback function but i don't think it's ever reached since the page doesn't even load.
this is my code..
startup.cs
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
app.MapSignalR();
}
}
hub
public class SRHub : Hub
{
public void Send(string message)
{
// Call the addNewMessageToPage method to update clients.
var conn = GlobalHost.ConnectionManager.GetHubContext<SRHub>();
conn.Clients.All.addNewMessageToPage(message);
//Clients.All.addNewMessageToPage(message);
}
}
javascript in SRStart.cshtml
$(function () {
// Reference the auto-generated proxy for the hub.
var conn = $.connection.sRHub;
// Create a function that the hub can call back to display messages.
conn.client.addNewMessageToPage = function (message) {
if (!message.contains('[EOF]')) {
populateStreamDialog(message);
}
};
$.connection.hub.start()
.done(function () {
});
});
any help would be appreciated..
I was able to replicate error. Problem is that /SignalR is route used by SignalR itself.
By using MVC controller named SignalRController there is now conflict between SignalR and MVC causing the error.
Just rename you MVC controller SignalRController (and folder containing its views) to something else...

Call model function from JavaScript in Laravel

I'm actually running into little problems with my current project. Following case:
I've got a model called "Posting" with relations:
public function subscribers(){
return $this->belongsToMany('User');
}
In my view-file there is a table containing all Postings and also a checkbox for subscribing/unsubscribing with the matching value to the posting-id:
<input class="click" type="checkbox" name="mobileos" value="{{{$posting->id}}}"
#if($posting->subscribers->find(Auth::User()->id))
checked="checked"
#endif
>
Now the thing I want to archive:
A JavaScript is going to watch if the checkbox is checked or not. According to that, the current user subscribes/unsubscribes to the posting. Something like:
$('.click').on('click',function() {
// $posting->find(---$(this).prop('checked')---)->subscribers()->attach(---Auth::user()->id---);
// $posting->find(---$(this).prop('checked')---)->subscribers()->detach(---Auth::user()->id---);
});
Is there any possibility to archieve that or any other ways? I couldn't get my head around this so far.
Cheers,
Chris
If you want to use Ajax to achieve this, you will need a REST endpoint in Laravel for the subscriptions, e.g.:
http://localhost/subscribe/{{userid}}
When this Endpoint is called, the database can be updated. The function could also return a JSON showing, if the saving database in the database successful.
Use this endpoint to make an Ajax Call on click:
var user = {
id: 0 // retrieve the correct ID from wherever it is stored
}
$('.click').on('click',function() {
$.GET('http://localhost/subscribe/' + user.id,
function () { // this is the success callback, that is called, if the Ajax GET did not return any errors
alert('You are subsribed')
});
});
Ideally you won't be using the GET method, but instead use POST and send the user ID as data. Also you would need to retrieve the user ID from session or wherever it is stored.
Take care that as you are using Ajax it can easily be manipulated from the client side. So on the server you should check, if the user ID that was sent is the same as in the Session. Maybe you don't need to send the user id at all, but that depends on how your backend is built.

value from javascript is not returning to c# in the application in Visual Web GUI

I have written a java script function in the skin file of the visual web Gui application which returns some value too. Now i am invoking the java script method from code behind.
public void XYZ( string message)
{
this.InvokeMethodWithId("testCall", message);
}
And javascript function is:--
function testCall(strGuid, txt) {
alert("hai Java script fired..");
return txt+ 'returned from JavaScript';
}
I want the value returned from JavaScript in the application. how can i achieve it. Is there in other method to invoke the methods of JavaScript?
I want something like this:--
public void Conect( string message)
{
string returnedvalue = this.InvokeMethodWithId("testCall", message);
}
Javascript is executed on the client so the return won't make it to the server.
A solution could be to use AJAX to send that value to the server. Stack Overflow is full of answers about AJAX.
Here's a good example.
#Amish Kumar,
As noted by other replies already, the client-side and server-side are not directly connected in web programming. The client is always the initiator of every request, and the server-side's "purpose" is to render a response, which will then be returned to the client for processing, in Visual WebGui this is usually some UI update processing. This basically means that your client script will not execute until the server-side has finished rendering the response, and the only way the client can get some message back to the server is to issue another request.
Think about how you need to use the MessageBox in Visual WebGui for instance. In order to receive the "response" from the MessageBox, you need to supply a callback handler in your server-side code, and then your server-side code will have completed creating the response, which is returned to the client. The client updates its UI and on some action to the MessageBox dialog, it sends a new request to the server, which interpretes the action and invokes your callback handler. In the callback handler you use Form.DialogResult to get the user action.
A very basic way to make this work in custom Visual WebGui code could be like the following code on a Form:
private void button1_Click(object sender, EventArgs e)
{
SendClientMessage("This is a test");
}
public void SendClientMessage(string strMessage)
{
System.Text.StringBuilder sb = new StringBuilder();
sb.AppendLine("var objEvent = mobjApp.Events_CreateEvent('{0}', 'MessageEvent');");
sb.AppendLine("mobjApp.Events_SetEventAttribute(objEvent, 'Msg', '{1}');");
sb.AppendLine("mobjApp.Events_RaiseEvents();");
this.InvokeScript(string.Format(sb.ToString(), this.ID, strMessage));
}
protected override void FireEvent(Gizmox.WebGUI.Common.Interfaces.IEvent objEvent)
{
if (objEvent.Type == "MessageEvent")
MessageBox.Show(objEvent["Msg"]);
else
base.FireEvent(objEvent);
}
This code will not work unless you set your Visual WebGui applicaton for no Obscuring. In order for this code to work on an obscured application, you would need to add the JavaScript as an obscured JavaScript resource and it would work fine.
Palli
enter code here

Categories