I'm attempting to use signalr for a board game app so that when a player moves a piece on one client, all clients can get updated. The problem I'm running into is not having a clear way to abstract the signalr parts of the app so that consumers of it don't need to worry about things like starting connections etc. I decided to skip sing the generated proxy and instead invoke things directly. The only remaining hurdle is that when the request is received by the client from the server, the this context in the event callback is of the hub, not of the owner of the callback. I'd like to know if there's a way to pass in a context.
Code:
Signalr service (like a base class to manage the connection and eventually events):
define('service.signalr', ['jquery'], function ($) {
var connection = $.hubConnection();
var start = function () {
connection.start();
};
return {connection: connection, start: start}
});
Service with specific functionality - in this case handling piece movement:
define('service.game', ['jquery', 'service.signalr'], function ($, signalr) {
var gameHubProxy = signalr.connection.createHubProxy('gameHub');
var addHandler = function (name, callback) {
gameHubProxy.on(name, callback);
}
var moveFigure = function (figureId, targetSpaceId) {
var deferred = $.Deferred();
gameHubProxy.invoke('moveFigure', figureId, targetSpaceId).done(function () { deferred.resolve(); });
return deferred.promise();
};
return { moveFigure: moveFigure, addHandler: addHandler }
});
Calling the server method on the service (the event trigger is for the client performing the action so it doesn't process twice):
define('model.space', ['backbone', 'service.game'], function (Backbone, gameService) {
var Space = Backbone.Model.extend({
defaults: { id: 0, figure: null },
moveFigure: function (figureId) {
var self = this;
var spaceId = self.get('id');
gameService.moveFigure(figureId, spaceId).done(function () {
self.trigger('figureMoved', figureId, spaceId, false);
});
}
});
return Space;
});
And trying to listen to the server's response:
define('view.board', ['jquery', 'underscore', 'backbone', 'helpers', 'bootstrapData', 'service.game', 'aggregate.space'], function ($, _, Backbone, helpers, bootstrapData, gameService, Space) {
var Board = Backbone.View.extend({
initialize: function () {
this.spaces = new Space.Collection(bootstrapData.spaces);
this.listenTo(this.spaces, 'figureMoved', this.updateFigurePosition);
gameService.addHandler('figureMoved', this.updateFigurePosition);
},
updateFigurePosition: function (figureId, spaceId, drawFigure) {
var figure = null;
var oldSpace = this.spaces.find(function (space) {
figure = space.get('figure');
return figure && figure.id == figureId;
});
//code continues, but has already failed on this.spaces
},
});
return Board;
});
The obvious answer here after some sleep is to wire up the callback to a function in the module whose context I want that is just a passthrough to the actual method I want to call, so that this is then set appropriately. Such as:
initialize: function () {
var self = this;
self.spaces = new Space.Collection(bootstrapData.spaces);
self.listenTo(self.spaces, 'figureMoved', self.updateFigurePosition);
gameService.addHandler('figureMoved', function (figureId, spaceId, drawFigure) {
self.updateFigurePosition(figureId, spaceId, drawFigure);
});
},
I still wonder if this can be a viable solution in all cases. Seems odd that I have to wrap my function in another function just so I can reference the variables I want. But I suspect this is less a problem with signalr, backbone, or require and more with a C# guy that's late to the Javascript party and still stumbles on some of the language's "features".
Related
My brother told me to write synchronous call for service, but the code below gives me an error. How can I do this correctly?
Here is my Angular service and Controller
app.service("StateService", function ($http, $q) {
var factory = $q.defer();
factory.getStateData = function () {
return $http.get("/State/GetStateData")
.then(function (response) {
factory.resolve(response.data);
return factory.promise;
}, function (response) {
factory.reject(response);
return factory.promise;
});
};
factory.getCountryById = function (Id) {
return $http.post('/Country/GetCountryById?Id=' + Id)
.then(function (response) {
factory.resolve(response.data);
return factory.promise;
}, function (response) {
factory.reject(response);
return factory.promise;
});
};
};
app.controller("statecontroller", function ($scope, StateService, $http, $timeout) {
// get function
$scope.GetStateData = function () {
StateService.getStateData().then(function (d) {
$scope.States = d.data;
$scope.fillAllData();
}, function (response) {
alert('error occurred' + response.data.ExceptionMesage);
});
};
$scope.fillAllData = function () {
for (var i = 0; i < $scope.States.length; i++) {
if ($scope.States[i].CountryId)
{
// var val = $scope.GetCountryById($scope.States[i].CountryId);
var cName = "";
var Id = $scope.States[i].CountryId;
StateService.getCountryById(Id).then(function (d) {
if (d.data)
cName = d.data.CountryName;
}, function (response) {
console.log('error occurred' + response.data.ExceptionMesage);
});
$scope.States[i].CountryName = cName;
}
else
$scope.States[i].CountryName = null;
}
}
};
Are you sure you want to make synchronous calls? Synchronous calls would halt the browser while waiting for the response and that's almost always never what you want. I reckon you actually mean asynchronous.
You may have misunderstood how services work. It's basically just a singleton object that gets injected by Angular. Additionally, the $http-service already creates promises for you so you could simplify your server drastically by writing it like this:
app.service("StateService", function ($http) {
var service = this;
service.getStateData = function () {
return $http.get("/State/GetStateData");
};
service.getCountryById = function (Id) {
return $http.post('/Country/GetCountryById?Id=' + Id);
};
};
This should work perfectly fine with your implementation in the controller.
Synchronous method - but, really, don't do this
app.service("StateService", function () {
var service = this;
service.getStateData = function () {
var req = new XMLHttpRequest();
req.open('GET', '/State/GetStateData', false); // false makes it synchronous
req.send(null);
if (req.status === 200) {
return req.responseText);
}
};
...
};
Then in your controller you consume it like this:
app.controller("statecontroller", function ($scope, StateService, $http, $timeout) {
// get function
$scope.GetStateData = function () {
$scope.States = StateService.getStateData();
$scope.fillAllData();
};
...
});
All left then is to answer angry support calls from your users whos browser you're blocking the execution from.
Also note that synchronous request may not work in all newer browsers:
Note: Starting with Gecko 30.0 (Firefox 30.0 / Thunderbird 30.0 /
SeaMonkey 2.27), synchronous requests on the main thread have been
deprecated due to the negative effects to the user experience.
From the documentation
Yet another reason not to do this.
I have making a web app using AngularJS, .NET 4.5 and SignalR.
One page in the app includes list of tasks to do, and I getting the list of the tasks from the server by AJAX and web method.
After it, I am registering to SignalR Hub witch the work of it is to update the tasks status.
I have angular service for connecting to SignalR and for the function on,off and invoke:
.factory('hubProxy', ['$rootScope',
function ($rootScope) {
function backendFactory(hubName) {
var connection = $.hubConnection("http://localhost:52158/");
var proxy = connection.createHubProxy(hubName);
proxy.on("dummy", function () { // do not remove - dummy is must here
});
connection.start().done(function () { });
return {
on: function (eventName, callback) {
proxy.on(eventName, function (a,b,c,d,e,f,g,h,i,j) {
$rootScope.$apply(function () {
if (callback) {
callback(a,b,c,d,e,f,g,h,i,j);
}
});
});
},
off: function (eventName, callback) {
proxy.off(eventName, function (result) {
$rootScope.$apply(function () {
if (callback) {
callback(result);
}
});
});
},
invoke: function (methodName, arg1, arg2, callback) {
proxy.invoke(methodName, arg1, arg2)
.done(function (result) {
$rootScope.$apply(function () {
if (callback) {
callback(result);
}
});
});
}
};
};
return backendFactory;
}]);
After it I am getting the task hub:
var tasksHub = hubProxy('taskHub');
Then to change task status I am using the following:
tasksHub.invoke("changeTaskStatus", task, status);
Until here everything works perfect.
The server hub function (changeTaskStatus) doing the following:
task.ChangeStatus((GeneralVars.PROJECT_STATUS)status);
Clients.All.taskStatusChanged(task.t_id,status);
So I am getting the task id and the status and want to change it on client side, I am getting it like this:
tasksHub.on("taskStatusChanged", function (t_id, status) {
var task = null;
for (var i = 0; i < $scope.levelTasks.length; i++) {
if ($scope.levelTasks[i].t_id == t_id) {
task = $scope.levelTasks[i];
break;
}
}
task.status = status;
$scope.$apply();
}
My problem is, that the client function in "taskStatusChanged" function, does not know the updated $scope variables.
The $scope.levelTasks is undefiend there even that I am getting it from server by ajax. And if I am moving the "taskStatusChanged" function to the ajax callback (where $scope.levelTasks is initialize) so the function will know the $scope.levelTasks but will register multiple times (every time I am getting the tasks from server)
Is there a way the client SignalR function will get access to the updated $scope varibles?
So what I did to solve it is to create a service for the tasks, then in the signalr function I am getting the tasks from the task service
I have the below code in angular js to prepare a logger for sending messages to server.
var demo = angular.module('ng-logging', []);
demo.config(function ($provide) {
$provide.decorator('$log', function ($delegate, $sniffer) {
var _info = $delegate.info; //Saving the original behavior
var _error = $delegate.error; //Saving the original behavior
var log = log4javascript.getLogger();
var ajaxAppender = new log4javascript.AjaxAppender('/api/log');
ajaxAppender.setThreshold(log4javascript.Level.ALL);
log.addAppender(ajaxAppender);
$delegate.info = function (msg) {
_info(msg);
log.info(msg);
};
$delegate.error = function (msg) {
_error(msg);
log.error(msg);
};
return $delegate;
});
});
Server side:
public class PaymentController : ApiController
{
[Route("api/log")]
public void Log(string message)
{
}
}
Usage: I am trying to use the $log provider configured above to send a message to server as below.
var payControllers = angular.module('pay.controllers', ['ui.bootstrap']);
payControllers.controller('PaymentCtrl', ['$scope', '$location', '$routeParams', 'payDataService',
function ($scope, $location, $routeParams, service, $log) {
$scope.sendPayment = function () {
$log.info("Sending payment");
};
}]);
When I test this, I get an error saying '$log is undefined'. I am not sure what is the correct way to implement this. I am trying to follow this article.
http://statelessprime.blogspot.com/2013/10/send-logging-and-exceptions-to-server.html
Thanks for any suggestions.
Here is how I accomplished the same thing client side. I overwrote the default $log angular logger by defining a factory method on my main module. (angular already has a logger defined at $log) . This may be why you get a $log is undefined. I also noticed that you did not set a layout for your appender. I reccomend using a json layout. Otherwise you only get an http post and you have to parse the string yourself. I can't help you server side because I have no idea what web framework you're using. Lastly,don't forget to inject the "$log" into your factory/directive whatever.
angular.module('main_module')
.factory('$log', function() {
var logger = log4javascript.getLogger();
var ajaxAppender = new log4javascript.AjaxAppender("/restfulConfigApi/logger.api");
ajaxAppender.addHeader("Content-Type", "application/json");
ajaxAppender.setWaitForResponse(true);
ajaxAppender.setLayout(new log4javascript.JsonLayout());
//ajaxAppender.setRequestSuccessCallback(requestCallback);
logger.addAppender(ajaxAppender);
return logger;
});
Example of injecting the $log
(function() {
'use strict';
angular.module("main_module").directive('a directive', a_directive);
conContextualIntelligencePublisher.$inject = [ "blah"... , "$log"];
function a_directive(blah ... , $log) { ... }
I want to process a stream of data and need to display the processed data near real time. For that I created a hub class
public class AzureGuidanceEventHubReceiver : Hub
{
EventProcessorHost eventProcessorHost;
public async void ProcessEvents()
{
//Do some code here
eventProcessorHost.RegisterEventProcessorAsync<SimpleEventProcessor>();
}
}
And the class which processes the data is,
public class SimpleEventProcessor : IEventProcessor
{
public async Task ProcessEventsAsync(PartitionContext context, IEnumerable<EventData> events)
{
foreach (EventData eventData in events)
{
int data;
var newData = this.DeserializeEventData(eventData);
//how to display newData in the browser????????????????????????????????
}
}
My client side code is
<script type="text/javascript">
$(function () {
var receiverHub= $.connection.azureGuidanceEventHubReceiver;
receiverHub.client.displayMessage = function (data) {
var encodedData = $('<div />').text(data).html();
// Add the message to the page.
$('#discussion').append('<li><strong>' + encodedData + '</li>');
};
//// Start the connection.
$.connection.hub.start().done(function () {
$('#sendmessage').click(function () {
receiverHub.server.processEvents();
});
});
});
Here I made a call to ProcessEvents method in the Hub class and that registers the SimpleEventProcessor. And thus execution comes into ProcessEventsAsync in SimpleEventProcessor. From this ProcessEventsAsync method, I need to call the clientside code to display the data. Do I need to make SimpleEventProcessor also as a hub class?
You can use following code to get hold of HubContext which allows you to invoke client methods from outside of hub istance:
var hubContext = GlobalHost.ConnectionManager.GetHubContext<AzureGuidanceEventHubReceiver>();
hubContext.Clients.All.displayMessage(dataToDisplay);
Here I am facing a problem that The client method DisplayMessage is not executing everytime. I need to display a stream of message. But when I put debugger in the client code, it executes everytime.Here is my client code.
chat.client.displayMessage = function (data) {
// Html encode display data
debugger;
var encodedData = $('<div />').text(data.GPSPosition).html();
var data = encodedData.split("\n");
var varlat = data[0].replace("Latitude:","").trim();
var varlong = data[1].replace("Longitude:", "").trim();
ShowInGoogleMap(varlat, varlong);
};
how can i display a stream of messages? Why it is only working with debugger?
I have made a little "life" notification system, so when a user on my website recieves a new mail they get a notification life at the site.
It works but only some times, so i hope that someone can tell me what happens and how i can fix it.
When I debug and check totalNewMessages always has a value 0 or above but from what i can see, the problem is between the last line in NotificationHub and the jquery part.
If you look in the jquery Code, i added a Alert(totalNewMessages); But it dont always fire. What could be the problem?
NotificationHub.cs
IHubContext context = GlobalHost.ConnectionManager.GetHubContext<NotificationHub>();
return context.Clients.Client(Users.Values.FirstOrDefault(i => i.ProfileId == UserID).ConnectionIds).RecieveNotification(totalNewMessages);
Jquery:
<script type="text/javascript">
$(function () {
// Declare a proxy to reference the hub.
var hiddenID = $('#HiddenID');
var Check = $('#Check');
var notifications = $.connection.notificationHub;
// Create a function that the hub can call to broadcast messages.
notifications.client.recieveNotification = function (totalNewMessages) {
// Add the message to the page.
var notification = $('#lblNotificationNumber')
alert(totalNewMessages);
if (totalNewMessages > 0) {
notification.fadeIn('slow');
notification.text(totalNewMessages);
}
else {
notification.fadeOut('slow');
}
};
// Start the connection.
$.connection.hub.start().done(function () {
if (hiddenID.val() > 0) {
notifications.server.check(hiddenID.val());
notifications.server.sendNotifications(hiddenID.val());
}
}).fail(function (e) {
alert(e);
});
$('#btnLogout').click(function () {
$.removeCookie("test");
});
//$.connection.hub.start();
});
</script>