I seem to have a problem sending an alert (toast) message from a controller action using SignalR. Unless I add a Thread.Sleep() after the send call, I never see the message. Inevitably, the send call occurs before return View(), so I imagine the alert is visible for a millisecond on the previous view, until the new one is served.
My first, rough solution is to use a timer to keep sending the message, until I get an acknowledgement.
This solution stinks. What else can I do? I can have the 'receiving' pages poll the server to see if they have any alerts, but that defeats the purpose of SignalR.
But then, maybe SignalR isn't suited to my case and I should just send the alerts as json strings on the model.
From the Login action:
....
//ModelState.AddModelError("", "Invalid login attempt.");
AlertsHub.ShowClientAlert(new Alert(AlertLevel.Error, "Invalid login attempt."));
return View(model);
The hub:
public class AlertsHub : Hub
{
private static Alert pendingAlert;
static Timer pollTimer;
internal static void ShowClientAlert(Alert alert)
{
if (pendingAlert != null)
{
return;
}
pendingAlert = alert;
pollTimer = new Timer(_ => SendAlert(pendingAlert), null, 0, 500);
}
static private void SendAlert(Alert alert)
{
IHubContext context = GlobalHost.ConnectionManager.GetHubContext<AlertsHub>();
context.Clients.All.ShowAlert(alert.Level.ToString(), alert.Message, alert.Title);
}
[HubMethodName("AlertReceived")]
public void AlertReceived(Guid alertId)
{
pollTimer.Dispose();
pendingAlert = null;
}
}
From the JS:
var toast;
$(function () {
if (typeof toast != 'undefined' && toast != null) {
showToast(toast);
toast = null;
}
var alertsProxy = $.connection.alertsHub;
alertsProxy.client.showAlert = function(alertId, level, message, title) {
toast.alertId = alertId;
toast.level = level;
toast.message = message;
toast.title = title;
};
$.connection.hub.start()
.done(function () {
console.log('Now connected, connection ID=' + $.connection.hub.id);
})
.fail(function () {
console.log('Could not Connect!');
});
});
function showToast(toast) {
switch (toast.level.toLowerCase()) {
case "success":
toastr.success(toast.message, toast.title);
break;
...
}
alertsProxy.server.AlertReceived(alertId)
.done(function() {
console.log("Alert '" + alertId + "' acknowledged.");
})
.fail(function() {
console.log("Acknowledgement of alert '" + alertId + "' failed.");
});
}
Related
I have application on which I get all username when someone login but I need to click the button to show all users,I want to show all user on real time as soon as someone login.However I am able to logout user on real time.
This below code working well for getting username on click button and real time logout users.
[Authorize]
public class AuthHub : Hub
{
private static ConcurrentDictionary<string, User> ActiveUsers = new ConcurrentDictionary<string, User>(StringComparer.InvariantCultureIgnoreCase);
public IEnumerable<string> GetConnectedUsers()
{
return ActiveUsers.Where(x => {
lock (x.Value.ConnectionIds)
{
return !x.Value.ConnectionIds.Contains(Context.ConnectionId, StringComparer.InvariantCultureIgnoreCase);
}
}).Select(x => x.Key);
}
public override Task OnConnected()
{
string userName = Context.User.Identity.Name;
string connectionId = Context.ConnectionId;
var user = ActiveUsers.GetOrAdd(userName, _ => new User
{
Name = userName,
ConnectionIds = new HashSet<string>()
});
lock (user.ConnectionIds)
{
user.ConnectionIds.Add(connectionId);
}
return base.OnConnected();
}
private User GetUser(string username)
{
User user;
ActiveUsers.TryGetValue(username, out user);
return user;
}
public void forceLogOut(string to)
{
User receiver;
if (ActiveUsers.TryGetValue(to, out receiver))
{
User sender = GetUser(Context.User.Identity.Name);
IEnumerable<string> allReceivers;
lock (receiver.ConnectionIds)
{
allReceivers = receiver.ConnectionIds.Concat(receiver.ConnectionIds);
}
foreach (var cid in allReceivers)
{
Clients.Client(cid).Signout();
}
}
}
}
client side code
$(function () {
var chat = $.connection.authHub;
chat.client.Signout = function () {
$('#logoutForm').submit();
$.connection.hub.stop();
};
$.connection.hub.start().done(function () {
console.log("working");
function PopulateActiveUsers() {
$("#usersTable").empty();
chat.server.getConnectedUsers().done(function (users) {
console.log("working");
$.each(users, function (i, username) {
$("#usersTable").append("<tr><th scope='row'>" + (i + 1) + "</th><td>" + username + "</td><td><a href='javascript:void(0)' data-user='" + username + "' class='btn btn-primary btn-sm btn-logout'>Logout User</a></td></tr>");
});
});
}
$("#displayActiveUsers").on("click", function () {
PopulateActiveUsers();
});
$('body').on('click', 'a.btn-logout', function () {
var username = $(this).attr('data-user');
chat.server.forceLogOut(username);
});
});
});
You would add that same method you call from the client to fire during the OnConnected() event. Example:
public override Task OnConnected()
{
//Get your list of users however you do that now
//Send back to all clients
Client.All.yourClientMethod(users);
}
In your client you just need a client side method to receive the data and process it just as you do now.
I have solved my problem by doing some modification in my code like so
public void GetConnectedUsers()
{
Clients.All.getConnectedUsers(ActiveUsers.Where(x =>
{
lock (x.Value.ConnectionIds)
{
return x.Value.ConnectionIds.Contains(Context.ConnectionId, StringComparer.InvariantCultureIgnoreCase);
}
}).Select(x => x.Key).ToList());
//return Clients.All(ActiveUsers.Select(x => x.Key).ToList());
}
//client side code
$.connection.hub.start().done(function () {
chat.server.getConnectedUsers().done(function (users) { });
});
chat.client.getConnectedUsers = function (users) {
$.each(users, function (i, username) {
$("#usersTable").append("<tr><th scope='row'>" + (i + 1) + "</th><td>" + username + "</td><td><a href='javascript:void(0)' data-user='" + username + "' class='btn btn-primary btn-sm btn-logout'>Logout User</a></td></tr>");
});
};
I'm creating an ASP.NET MVC application which uses "SqlDependecy" and "SignalR" technologies to maintain real-time communication with the server based on database changes. It simply inspect a field value changes in specific database record and then display it on the browser.
The attempt works perfectly fine. But when I monitor the network requests through the browsers "Network" performance, the request count increases by 1 in every refresh of the page.
As in the image.
Initial page load only make one request.
First refresh after the initial load and then db change will lead to make 2 requests.
Second refresh after the initial load and then db change will lead to make 3 requests.
so on...
The js code I tried is given below.
It seams as an problem to me. If this is a real problem, Any advice on this will be highly appreciated. Thank you very much.
<script type="text/javascript">
$(function () {
var jHub = $.connection.journeyHub;
$.connection.hub.start();
jHub.client.ListenChange = function () {
getData();
}
jHub.client.ListenChange();
});
function getData() {
$.ajax({
url: 'GetValue',
type: 'GET',
dataType: 'json',
success: function (data) {
if (data == "pending") {
$("#box").css({ "background-color": "orange" });
}
else if (data == "deny") {
$("#box").css({ "background-color": "red" });
}
else if (data == "success") {
$("#box").css({ "background-color": "green" });
}
}
});
}
</script>
<div id="box" style="width:100px; height:100px; background-color: gray;"></div>
[Edit v1]
Here is my Controller where the event handler is located.
public class TravelController : Controller
{
SqlConnection link = new SqlConnection(ConfigurationManager.ConnectionStrings["linkTraveller"].ConnectionString);
// GET: Travel
public ActionResult Listen()
{
return View();
}
public ActionResult GetValue()
{
using (IDbConnection conn = link)
{
string query = #"SELECT [Status] FROM [dbo].[Journey] WHERE [Id]=1";
SqlCommand command = new SqlCommand(query, link);
SqlDependency sqlDep = new SqlDependency(command);
sqlDep.OnChange += new OnChangeEventHandler((sender, e) => sqlDep_OnChange(sender, e));
conn.Open();
string status = command.ExecuteScalar().ToString();
return Json(status, JsonRequestBehavior.AllowGet);
}
}
private void sqlDep_OnChange(object sender, SqlNotificationEventArgs e)
{
JourneyHub.Start();
}
}
Here is the Hub
public class JourneyHub : Hub
{
public static void Start()
{
var context = GlobalHost.ConnectionManager.GetHubContext<JourneyHub>();
context.Clients.All.ListenChange();
}
}
Off the top of my head, I would say you are not decrementing your trigger handlers, sql dependency triggers only fire once and then they are gone, you have to remember the remove the event handler for it or they just keep adding but, but I will know for sure if you can post your sql dependency trigger code.
Here is a sample from something I did many years ago, but the idea is still the same.
try
{
using (
var connection =
new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString))
{
connection.Open();
using (SqlCommand command = new SqlCommand(#"SELECT [Id]
,[FName]
,[LName]
,[DOB]
,[Notes]
,[PendingReview]
FROM [dbo].[Users]",
connection))
{
// Make sure the command object does not already have
// a notification object associated with it.
command.Notification = null;
SqlDependency dependency = new SqlDependency(command);
dependency.OnChange += new OnChangeEventHandler(dependency_OnChange);
if (connection.State == ConnectionState.Closed)
connection.Open();
command.ExecuteReader();
}
}
}
catch (Exception e)
{
throw;
}
}
private void dependency_OnChange(object sender, SqlNotificationEventArgs e)
{
SqlDependency dependency = sender as SqlDependency;
if (dependency != null) dependency.OnChange -= dependency_OnChange;
//Recall your SQLDependency setup method here.
SetupDependency();
JobHub.Show();
}
I currently have a component that requires to have a pop up upon submit. The response from the API provides a redirectURL and redirectParams to be used as the popped up window.
Simply using window.open defaults its method to GET and not POST. This causes a Not Found page whenever it is loaded this way.
This is how I currently do it:
protected makeDepositPopup(depositRequest: PaymentRequest) {
this.depositService.requestDepositPopup(depositRequest)
.subscribe(
(depositEvent: RedirectData | SuccessfulDepositPopup) => this.handleDepositPopupEvent(depositEvent),
(error: EcommError) => this.handleDepositError(error),
() => this.eventService.depositAttempt.emit()
);
}
then the event is handled via the handleDeposit event:
protected handleDepositPopupEvent(depositEvent: RedirectData | SuccessfulDepositPopup) {
if (depositEvent instanceof RedirectData) {
this.handleRedirect(depositEvent);
} else {
this.handleDepositSuccessPopup(depositEvent, this.redirectParams);
}
}
As soon as it succeeds, it then tries to open a window with the corresponding URL and parameters provided by the API response:
protected handleDepositSuccessPopup(depositEvent: SuccessfulDepositPopup, redirectParams: string): void {
redirectParams = ""; // to remove undefined value
depositEvent.params.forEach(function (data, index) {
if(index == 0) {
redirectParams += "?" + data["key"] + "=" + data["value"];
} else {
redirectParams += "&" + data["key"] + "=" + data["value"];
}
});
window.open(depositEvent.url + redirectParams);
this.router.navigate(
["deposit", this.route.snapshot.data["instrumentTypeCode"], "deposit-details", depositEvent.id], {
relativeTo: this.route.parent.parent
});
}
How do I convert this in such a way that the depositEvent.url and its appended redirectParams to open a new window with a POST method to get a successful page response?
I have a web application that performs a Jquery .post request for database queries. I also have three different web socket connections that are used to push status updates from the server to the client (CPU and Memory stats in a live chart, database status, and query queue). While a query is not running, everything works smoothly, but once a query is started (post request), then the three web socket connections seem to hang/block while waiting for the query to return. I was reading about this and have not found any relevant answers...I suspect that it is probably something really dumb on my part...but this has had me scratching my head for the better part of a day now. I thought I might try moving the web socket connections to web workers...but in theory, the POST should not be blocking to begin with...So, here are the relevant snippets of code...The full source is a couple of thousand lines of code...so I didn't want to inundate anyone with it...but could show it if it is useful. So, the big question is what am I doing wrong here? Or perhaps, am I misunderstanding how AJAX calls work as far as blocking goes?
// query execution button that grabs the query for the most recently focused query source (SPARQL editor, history, or canned)
$("#querySubmitButton").on("click", function(e) {
// Disable the query button
$("#querySubmitButton").attr('disabled',true);
// Let's make sure we are clearing out the work area and the popup contents
$("#viz").empty();
// Get YASQE to tell us what type of query we are running
var queryType = editor.getQueryType();
// refactored so that we can clean up the on-click function and also make other query types in a more modular way
switch(queryType) {
case 'SELECT':
sparqlSelect();
break;
case 'CONSTRUCT':
sparqlConstruct();
break;
case 'ASK':
sparqlAsk();
break;
case 'DESCRIBE':
sparqlDescribe();
break;
case 'INSERT':
sparqlInsert();
break;
default:
popup.show("Unrecognized query type.","error");
break;
}
});
// Functions to do each of the query types (SELECT, CONSTRUCT, ASK, DESCRIBE, INSERT)
// SELECT
function sparqlSelect() {
$.post("sparqlSelect", { database: $("#DB_label").html(),'query': editor.getValue() }).done(function(data, textStatus, xhr) {
// Enable the query button
$("#querySubmitButton").removeAttr('disabled');
// If the query worked, store it
storeQueryHistory(query);
// if the previous query was a CONSTRUCT, then lets hide the graph metrics button
$("#nav-trigger-graphStatistics").fadeOut(800);
// Need to slide the query menu back
sliders("in",$("#nav-trigger-query").attr("id"));
var columns = [];
var fields = [];
var comboboxFields = [];
// Hide the graph search panel
$("#graphSearch").fadeOut(1400);
// Show the results and visualization button/tab
$("#nav-trigger-results").fadeIn(1400);
$("#nav-trigger-visualization").fadeIn(1400);
$.each(data.results.head.vars, function(index, value) {
columns.push({'field': value, 'title': value});
var to = {};
to[value] = {type: "string"};
fields.push(to);
// Let's also populate the two Comboboxes for the Visualization while we are at it
comboboxFields.push({'text': value, 'value': value});
});
// Now, set the two combobox datasources for visualizations
var categoriesDS = new kendo.data.DataSource({
data: comboboxFields
});
vizCategoryAxis.setDataSource(categoriesDS);
var valuesDS = new kendo.data.DataSource({
data: comboboxFields
});
vizValueAxis.setDataSource(valuesDS);
var dataBindings = [];
$.each(data.results.results.bindings, function(index1, value) {
var tempobj = {};
$.each(value, function(k1,v1) {
tempobj[k1] = v1.value;
});
tempobj.id=index1;
dataBindings.push(tempobj);
});
var configuration = {
dataSource: {
data: dataBindings,
pageSize: 25
},
height: 400,
scrollable: true,
sortable: true,
filterable: true,
reorderable: true,
resizable: true,
toolbar: ["excel"],
excel: {
allPages: true,
filterable: true,
proxyURL: "/saveExcel"
},
pageable: {
input: true,
numeric: false,
pageSizes: true
},
'columns': columns,
dataBound: function(e) {
$(e.sender.element).find('td').each(function() {
var temp = $(this).html();
if (isUrl(temp)) {
$(this).html('' + temp + '');
}
});
}
};
// Create the popup window
var gridWindow = $("#resultsPopup").kendoWindow({
width: "70%",
title: "Query Results",
actions: [
"Minimize",
"Maximize",
"Close"
]
}).data('kendoWindow');
// Center and show the popup window
gridWindow.center().open();
// Create/update/refresh the grid
resultsGrid.setOptions(configuration);
resultsGrid.dataSource.page(1);
$("#nav-trigger-results").on('click',function() {
// Center and show the popup window
gridWindow.center().open();
});
}).fail(function(xhr) {
// If we are timed-out
if (xhr.status === 401) {
// First, clear the host, database, and status text
$("#host_label").html('');
$("#DB_label").html('');
$("#status_label").html('');
// Next, disable the query button
$("#querySubmitButton").attr('disabled',true);
// Change "login" tab text color to red so we know we are no longer logged in
var styles = { 'color': "#FFCCD2" };
$("#nav-trigger-login").css(styles);
popup.show("Session for " + host + " has timed out, please log back in.","error");
}
else {
// Enable the query button
$("#querySubmitButton").removeAttr('disabled');
popup.show("Error, no results (" + xhr.status + " " + xhr.statusText + ")","error");
}
});
}
// Function to connect to the query queue websocket
function queueWebsocketConnect() {
var qws = new WebSocket('wss://endeavour:3000/queue');
// Let's disconnect our Websocket connections when we leave the app
$(window).on('unload', function() {
console.log('Websocket connection closed');
qws.close();
});
// Status websocket onopen
qws.onopen = function () {
console.log('Websocket connection opened');
popup.show("Websocket connection opened","success");
};
qws.onclose = function (event) {
console.log('Websocket connection closed');
popup.show("Websocket connection closed","info");
};
qws.onmessage = function (msg) {
var res = JSON.parse(msg.data);
var tableRows = '<thead><tr><td>Query Position</td><td>Query ID</td><td>Kill/Cancel Query</td></tr></thead><tbody>';
if (res.executing != null && res.entry.length > 0) {
$("#queryQueue").empty();
tableRows += '<tr><td>1</td><td>' + res.executing.id + '</td><td><input type="button" class="k-button" value="Kill"></td></tr>';
$.each(res.entry, function(index,object) {
tableRows += '<tr><td>' + (object.pos + 1) + '</td><td>' + object.query.id + '</td><td><input type="button" class="k-button" value="Cancel"></td></tr>';
});
tableRows += '</tbody>';
$("#queryQueue").html(tableRows);
}
else if (res.executing != null) {
$("#queryQueue").empty();
tableRows += '<tr><td>1</td><td>' + res.executing.id + '</td><td><input type="button" class="k-button" value="Kill"></td></tr>';
tableRows += '</tbody>';
$("#queryQueue").html(tableRows);
}
else {
console.log(res);
$("#queryQueue").empty();
}
};
}
// Function to connect to the stats websocket
function websocketConnect () {
// Set up websocket connection for system stats
var ws = new WebSocket('wss://endeavour:3000/stats');
// Let's disconnect our Websocket connections when we leave the app
$(window).on('unload', function() {
console.log('Websocket connection closed');
ws.close();
});
// Status websocket onopen
ws.onopen = function () {
console.log('Websocket connection opened');
popup.show("Websocket connection opened","success");
};
// Status websocket onclose
ws.onclose = function (event) {
// Disable the query button
$("#querySubmitButton").attr('disabled',true);
// Change "login" tab text color to red so we know we are no longer logged in
var styles = { 'color': "#FFCCD2" };
$("#nav-trigger-login").css(styles);
// Clear the host, database, and status text
$("#host_label").html('');
$("#DB_label").html('');
$("#status_label").html('');
console.log('Websocket connection closed');
popup.show("Websocket connection closed","error");
$("#websocketReconnectButtonYes").on('click', function() {
websocketConnect();
queueWebsocketConnect();
websocketReconnect.close();
});
$("#websocketReconnectButtonNo").on('click', function() {
websocketReconnect.close();
});
websocketReconnect.center().open();
};
// When updates are received, push them out to update the details
var logoutCount = 0;
ws.onmessage = function (msg) {
if (msg.data === 'loggedOut') {
// Ensure we only emit this one time instead of a stream of them
if (logoutCount == 0) {
// Disable the query button
$("#querySubmitButton").attr('disabled',true);
// Change "login" tab text color to red so we know we are no longer logged in
var styles = { 'color': "#FFCCD2" };
$("#nav-trigger-login").css(styles);
// Clear the host, database, and status text
$("#host_label").html('');
$("#DB_label").html('');
$("#status_label").html('');
console.log("Session for " + $("#host_label").html() + " has timed out, please log back in.");
popup.show("Session for " + $("#host_label").html() + " has timed out, please log back in.","error");
}
logoutCount = 1;
}
else {
logoutCount = 0;
var res = JSON.parse(msg.data);
var host = $("#host_label").html();
var pdatabase = $("#DB_label").html();
var pstatus = $("#status_label").html();
// Disable the query button unless the database is "CONNECTED"
if ($("#status_label").html() !== res.current.databaseStatus) {
if (res.current.databaseStatus !== "CONNECTED") {
$("#querySubmitButton").attr('disabled',true);
}
else {
$("#querySubmitButton").removeAttr('disabled');
}
if (res.current.databaseStatus == 'CONNECTED' || res.current.databaseStatus == 'STOPPED') {
$("#startDB").removeAttr('disabled');
}
else {
$("#startDB").attr('disabled',true);
}
}
// Maybe a more intelligent way to do this, but need to make sure that if the cookie is still valid, then populate the database login stuff
if ($("#dbConfigHost").val() == "" && $("#dbConfigUser").val() == "") {
$("#dbConfigHost").val(res.host);
$("#dbConfigUser").val(res.user);
// Change "login" tab text color to green so we know we are logged in
var styles = { 'color': "#C5E6CC" };
$("#nav-trigger-login").css(styles);
var databasesDS = new kendo.data.DataSource({
data: res.databases.database
});
databasePicker.setDataSource(databasesDS);
}
// Update the labels when values change
if (res.host != $("#host_label").html()) {
$("#host_label").html(res.host);
popup.show("Host changed to " + res.host,"info");
}
if (pdatabase != res.current.name) {
$("#DB_label").html(res.current.name);
popup.show("Database changed to " + res.current.name ,"info");
}
if (pstatus != res.current.databaseStatus) {
$("#status_label").html(res.current.databaseStatus);
}
// Update the sparklines
cpulog.options.series[0].data = res.system.cpu;
cpulog.refresh();
memlog.options.series[0].data = res.system.mem;
memlog.refresh();
}
};
// Open the websocket connection to listen for changes to the query list
var queryWS = new WebSocket('wss://endeavour:3000/queryList');
queryWS.onmessage = function(msg) {
var res = JSON.parse(msg.data);
var queriesDS = new kendo.data.DataSource({
data: res
});
cannedQuery.setDataSource(queriesDS);
};
}
Well, I guess when one has been heading down one road for a while, one assumes that it is in the right direction. After further head-scratching, I found the issue and it was related to the blocking/non-blocking nature of my backend web-framework. As it turns out, in Mojolicious (Perl web-framework), http calls can be either synchronous or asynchronous depending on how one writes the call.
my $tx = $ua->get('http://foo.bar?query=getSomeFoo');
if($tx->success) {
$self->render($tx->res->content);
}
else {
$self->rendered($tx->res->code);
}
This is a blocking/synchronous request. Nothing happens until after the GET finishes. On the other hand, if one writes the request like so, it is an asynchronous request:
$ua->get('http://foo.bar?query=getSomeFoo' => sub {
my ($ua,$tx) = #_;
if($tx->success) {
$self->render($tx->res->content);
}
else {
$self->rendered($tx->res->code);
}
});
So, if anyone else has encountered this issue...here is the answer. If I am the only idiot on the planet that has committed this blunder...then I guess I have put my shame out there for all to have a good chuckle at.
Bottom line was that this was well documented in the Mojolicious docs...but I had been doing it one way for so long that I completely forgot about it.
Cheers!
I'm making a little chat application with SignalR 2.0 (just like everyone).
I have a win8.1 application and when the application is closed, the hub receives the OnDisconnected event and removes the user from the list on the hub.
The hub sends to every client that the user has left the chat, so we can visualize that the user has left.
But when I'm using SignalR and Javascript in a webpage and the page gets closed, the hub doesn't get notified that the tab/browser is closed...
Anyone any idea how the connection can be closed?
What i've coded:
Startup Hub
[assembly: OwinStartup(typeof(ChatServer.Startup))]
namespace ChatServer
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
// Map all hubs to "/signalr"
app.MapSignalR();
}
}
}
Hub
[HubName("ChatHub")]
public class ChatHub : Hub
{
private static List<User> Users = new List<User>();
public void Send(string name, string message)
{
// Call the broadcastMessage method to update clients.
Clients.All.broadcastMessage(name, message, DateTime.Now);
}
public void UserConnected(string name)
{
Clients.Caller.connected(JsonConvert.SerializeObject(Users));
User user = new User() { Name = name, ConnectionID = Context.ConnectionId };
Clients.Others.userConnected(JsonConvert.SerializeObject(user));
Users.Add(user);
}
public void UserLeft()
{
if(Users.Any(x=>x.ConnectionID == Context.ConnectionId))
{
User user = Users.First(x => x.ConnectionID == Context.ConnectionId);
Users.Remove(user);
Clients.Others.userLeft(JsonConvert.SerializeObject(user), Context.ConnectionId);
}
}
public override System.Threading.Tasks.Task OnDisconnected()
{
// Only called when win8.1 app closes
// Not called when browsers closes page
UserLeft();
return base.OnDisconnected();
}
}
HTML - Javascript:
Javascript:
chat = new function () {
var ChatHub;
// Connecting to the hub
this.attachEvents = function () {
if ($.connection != null) {
ChatHub = $.connection.ChatHub;
$.connection.hub.start({ transport: 'auto' }, function () {
// Register client on hub
ChatHub.server.userConnected("web name");
});
}
this.send = function (name,message) {
if ($.connection != null) {
//Send chat message
ChatHub.server.send(name, message).fail(function (e) {
alert(e);
});
}
}
}
};
window.onbeforeunload = function (e) {
//This is called when we close the page
$.connection.hub.stop();
return "You killed me! :'(";
};
Win8.1 client
internal async void ConnectToHub(string userName)
{
try
{
HubConnection hubConnection = new HubConnection(SERVER_PROXY);
chat = hubConnection.CreateHubProxy("ChatHub");
Context = SynchronizationContext.Current;
MakeHubFunctionsAvailableOnClient();
await hubConnection.Start();
// Register client on hub
await chat.Invoke("UserConnected", userName);
}
catch (Exception)
{
throw;
}
}
private void MakeHubFunctionsAvailableOnClient()
{
//Receive chat messages
chat.On<string, string, DateTime>("broadcastMessage",
(name, message, date) => Context.Post(
delegate {
Messages.Add(new UserMessage() { Message = message, User = name, Date = date });
}, null
)
);
//Receive all online users
chat.On<string>("connected",
(users) => Context.Post(
delegate
{
List<User> userList = JsonConvert.DeserializeObject<List<User>>(users);
foreach (User user in userList)
{
Users.Add(user);
}
Messages.Add(new UserMessage() { Message = "CONNECTED", User = "System", Date = DateTime.Now });
}, null
)
);
//New user connected
chat.On<string>("userConnected",
(user) => Context.Post(
delegate
{
User newUser = JsonConvert.DeserializeObject<User>(user);
Users.Add(newUser);
Messages.Add(new UserMessage() { Message = newUser.Name + " CONNECTED", User = "System", Date = DateTime.Now });
}, null
)
);
//User left, remove user from list on client
chat.On<string>("userLeft",
(user) => Context.Post(
delegate
{
User newUser = JsonConvert.DeserializeObject<User>(user);
User y = Users.First(x=>x.ConnectionID == newUser.ConnectionID);
bool ux = Users.Remove(y);
Messages.Add(new UserMessage() { Message = newUser.Name + " left the conversation", User = "System", Date = DateTime.Now });
}, null
)
);
}
My hub doesn't trigger OnDisconnected, when i close tab / browser / navigating to other site
The site is a singe page website (for the moment)
What browser am i using? Chrome: Version 32.0.1700.68 beta-m
Internet Explorer 11
I guess your question is how to detect whether a specific user left or closed the browser. When a user comes in or leaves, send a notification to other users.
I think the problem is you didn't handle OnConnected and OnDisconnected properly.
To make it work, first you need to know each client connecting to a hub passes a unique connection id. You can retrieve this value in the Context.ConnectionId property of the hub context. When each client comes in, they go through OnConnected, when they leave, they go through OnDisconnected. Below is a working example:
Hub Class
[HubName("ChatHub")]
public class ChatHub : Hub
{
private static List<User> Users = new List<User>();
public void Send(string message)
{
var name = Context.User.Identity.Name;
Clients.All.broadcastMessage(name, message, DateTime.Now.ToShortDateString());
}
public override Task OnConnected()
{
User user = new User() { Name = Context.User.Identity.Name, ConnectionID = Context.ConnectionId };
Users.Add(user);
Clients.Others.userConnected(user.Name);
return base.OnConnected();
}
public override Task OnDisconnected()
{
if (Users.Any(x => x.ConnectionID == Context.ConnectionId))
{
User user = Users.First(x => x.ConnectionID == Context.ConnectionId);
Clients.Others.userLeft(user.Name);
Users.Remove(user);
}
return base.OnDisconnected();
}
}
View
<input type="text" id="message" />
<button class="btn">Send</button>
<ul id="contents"></ul>
#section scripts
{
<script src="~/Scripts/jquery-1.10.2.js"></script>
<script src="~/Scripts/jquery.signalR-2.0.1.js"></script>
<script src="/signalr/hubs"></script>
<script>
var chatHub = $.connection.ChatHub;
chatHub.client.broadcastMessage = function (name, message, time) {
var encodedName = $('<div />').text(name).html();
var encodedMsg = $('<div />').text(message).html();
var encodedTime = $('<div />').text(time).html();
$('#contents').append('<li><strong>' + encodedName + '</strong>: ' + encodedMsg + '</strong>: ' + encodedTime + '</li>');
};
chatHub.client.userConnected = function (data) {
$('#contents').append('<li>Wellcome<strong>' + '</strong>:' + data + '</li>');
};
chatHub.client.userLeft = function (data) {
$('#contents').append('<li>Bye<strong>' + '</strong>:' + data + '</li>');
};
$(".btn").on("click", function() {
chatHub.server.send($("#message").val());
});
$.connection.hub.start().done(function() {
console.log("connected...");
});
</script>
}
You might be running into the issue described here: https://github.com/SignalR/SignalR/issues/2719
tl;dr: There is a bug introduced in Chrome 31 that is already fixed in Chrome Canary, that prevents SignalR from immediately trigger OnDisconnected.
If you are, you can use the workaround I suggested in the issue which is to add the following to your JS code.
window.onbeforeunload = function (e) {
$.connection.hub.stop();
};
Even with this bug and without the workaround I would expect SignalR to raise OnDisconnected approximately 35s after the browser exits the page hosting SignalR.
Can you provide more details about which browsers you are experiencing this issue with and in what situations? (e.g. closing the tab, closing the window, refreshing, navigating to another page on the same site, navigating to a different site, etc.)