Backbone model save triggers error callback on Chrome/FF - ajax

Noob question here:
I'm using ASP.NET MVC 3 and I'm trying to save an entity through Backbone. Here's what I have:
I defined my Backbone model (Program) as such:
var Program = Backbone.Model.extend({
defaults: function () {
return { name: "" };
},
initialize: function (attrs) {
this.set('name', attrs.name);
},
urlRoot: '/program/add'
});
Then I hook up the model save on the click event of a button:
$('.add-program').click(function () {
var programName = $('.program-name').val();
var program = new Program({ name: programName });
program.save(null, {
success: function (model, response) {
alert('success');
},
error: function (model, response) {
alert('error');
}
});
});
It works on IE (surprisingly!) - ProgramController.Add(string name) gets called fine and I get a success response. But I'm having issues on Chrome and FF - They both trigger the error callback with the slight difference that on Chrome my Controller Action doesn't even get hit at all (it does on FF though). The funny thing is that my action breakpoint does get hit on FF, with the appropriate param value, but still get the error callback.
I'm not sure what's going on here. I tried debugging through Firebug/Chromebug and don't see much on the error callback params (the errorStatus is just ... well... "error"!). I also tried looking at the Network tab and Fiddler and I don't see anything that rings a bell (maybe I'm not looking at the right place). I also tried doing a straight jquery ajax call to the controller and still get the same weird behavior.
Just in case, here's the MVC action (although I don't think the issue is here):
[HttpPost]
public JsonResult Add(string name)
{
var stubbedResponse = new {id = Guid.NewGuid()};
return Json(stubbedResponse);
}
Any ideas what could be causing this?

A Fiddle http://jsfiddle.net/Uj5Ae/2 with your client code seems to be OK. Something with your server response? Or Backbone and Underscore versions not matching?
Or maybe the return false at the end of the click handler, if the event propagation is not handled elsewhere.
Spoiler : that was the event propagation :)

Related

Handling session timeouts in ajax calls

ASP.Net MVC 5 .Net Framework 4.6.1
I just added code to detect session timeout which works fine:
public class CheckSessionTimeOutAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(System.Web.Mvc.ActionExecutingContext filterContext)
{
var context = filterContext.HttpContext;
if (context.Session != null)
{
if (context.Session.IsNewSession)
{
string sessionCookie = context.Request.Headers["Cookie"];
if ((sessionCookie != null) && (sessionCookie.IndexOf("ASP.NET_SessionId", StringComparison.Ordinal) >= 0))
{
string redirectTo = "~/Home/Index";
filterContext.HttpContext.Response.Redirect(redirectTo, true);
}
}
}
base.OnActionExecuting(filterContext);
}
}
As you can see, I redirect them to the home screen. I have my [CheckSessionTimeOut] as an attribute on all pertinent controllers. So, I run the app, go to a page other than the home screen, wait 1 minute for session timeout, the code runs as expected in certain situations. Case and point, I have a dropdown and when a selection is made, a redirect is taking place. Heres the method:
$('#selusers').change(function () {
var rd = $(this).find("option:selected").attr('redirect');
location.href = rd;
});
What happens here is when a user is selected from the dropdown, the redirect attribute is read and redirection to that person takes place. If the session times out, redirection to the logged in user takes place and not the newly selected user. This is correct for my app.
However, I make numerous ajax calls in my app. When the session times out and I click on an element that fires an ajax call, I get redirected to the home screen, but the error method gets called in the ajax request. I get a popup with the home screen html inside of it. Here's one example of an ajax call I'm making. I'm on a screen with a save button, the session times out and this code gets fired:
SaveButtonClicked: function (somedata) {
$.ajax({
url: url,
type: 'POST',
contentType: "application/json; charset=utf-8",
dataType: "json",
data: JSON.stringify(some data im sending),
success: function (dataResult) {
if (!dataResult.Ok) {
alert("Error occurred. Please try again");
}
},
error: function (err) {
alert(err.responseText);//this gets called
}
});
}
I have overridden the default alert so styled html will appear. The alert box has the home screen html in it. I do not want that. I want the app to be redirected to the home screen, no popup. So my first question, is there a way to do something at the server to stop the ajax call from running its complete methods(i.e I want to do something at the server so the ajax call's success, error, or complete methods will never be called)? Next question if the first one isn't an option, what would be a very good way to detect that the session has timed out in the complete methods of an ajax call so the user is gracefully redirected to the home screen? If there are any other ways of doing what i'm trying to achieve that I didn't not ask, please share them. Thank you for reading my question.
UPDATE
I removed the error function from one of my ajax calls and added a global error handler instead:
$(document).ajaxError(function (e, xhr, settings) {
debugger;
});
Now, when the session timeout, this error handler gets called, BUT, i look in the xhr variable and it contains the results of the ajax call and corresponding html. The status is a 200, the statusText says parsererror, the readystate is a 4. There is nothing here that tells me the session timed out. What can i do at the global ajaxError method to tell me session timed out?
A good starting points might be:
If your ASP.NET MVC project properly returns status code:
$.ajaxSetup({
statusCode: {
401: function() {
window.location.href = "/login/path";
}
}
});
if not you can try:
$(document).ajaxError(function (e, xhr, settings) {
debugger;
});
Review e & xhr properties and make a decision from there.

Return same view does not seem to refresh page

I have controller methods that do stuff with WMI classes (waking selected machine, opening or closing processes, initiating an RDP session) and then reload the current view. To reload I've tried the following:
return View();
return RedirectToAction("Index", "RenderNodesController");
return RedirectToAction("Index");
return Redirect(Request.UrlReferrer.ToString());
The methods all execute successfully with regards to performing the WMI calls, but the return statement doesn't reload the page. If I refresh the page manually it reloads and displays changed information as expected. Also rather weirdly if I step through the return statements in the debugger I see that the view's Index method executes so debugging seems not to help in this instance.
I'd be grateful for any help.
Just wanted to add some explanation to this answer:
Because you're performing an AJAX request, it won't reload the user's browser page on success, all it will do is return the results of the action method called within the result parameter. $.post is a shorthand version of $.ajax so the behaviour will be the same.
Solution:
Use for example location.reload(true) (as you did).
I still don't know why
return View();
and similar doesn't execute but in the end I got it working using Javascript's window.location.reload(), full script below for completeness:
<script type="text/javascript">
$(document).ready(function () {
$('.checkbox').click(function () {
var url = '#Url.Action("CallProcess", "RenderNodes")';
var networkname = $(this).data("networkname");
var processname = $(this).data("processname");
var filename = $(this).data("filename");
var start = $(this).is(':checked');
$.post(url, { NetworkName: networkname, ProcessName: processname, FileName: filename, Start: start },
function (data) {
$("#CallProcess").html(data);
});
window.location.reload(true);
})
});
</script>
I had the same problem, my solution is a little tricky, but it works. Instead of using a
return View("/MyRedirectView.cshtml");
in the controller, I used a
return RedirectToAction("MyRedicrectAction");
and the action is this:
[AllowAnonymous]
public ActionResult MyRedicrectAction()
{
return View("/MyRedirectView.cshtml");
}

Backbone not evaluates url function before request

I'm trying to use Backbone with REST API:
Here the code
My model:
var PagesModel = Backbone.Model.extend({
idAttribute: 'Guid',
initialize: function () {
this.on('remove', this.destroy);
},
urlRoot: '/api/pages'
});
Collection:
var PagesCollection = Backbone.Collection.extend({
model: PagesModel,
url: '/api/pages'
});
View:
var PagesView = Backbone.View.extend({
el: '#pages',
events: {
'click .removePage': 'remove',
},
initialize: function (collection) {
this.collection = collection;
this.collection.fetch();
this.template = $('#pages-template').html();
this.collection.bind('change reset', this.render, this);
},
render: function () {
var template = _.template(this.template);
$(this.el).html(template({ pages: this.collection.toJSON() }));
return this;
},
remove: function (e) {
e.preventDefault();
var id = $(e.currentTarget).closest('ul').data("id");
var item = this.collection.get(id);
this.collection.remove(item);
$(e.currentTarget).closest('ul').fadeOut(300, function () {
$(this).remove();
});
}
});
And here I'm starting up application:
$(function () {
var pagesCollection = new PagesCollection();
var pagesView = new PagesView(pagesCollection);
});
I'm clicking or Remove and in Network inspector see this link
http://localhost:54286/backbone/function%20()%20%7B%20%20%20%20%20%20var%20base%20=%20getValue(this,%20'urlRoot')%20%7C%7C%20getValue(this.collection,%20'url')%20%7C%7C%20urlError();%20%20%20%20%20%20if%20(this.isNew())%20return%20base;%20%20%20%20%20%20return%20base%20+%20(base.charAt(base.length%20-%201)%20==%20'/'%20?%20''%20:%20'/')%20+%20encodeURIComponent(this.id);%20%20%20%20}
instead of /api/pages/{guid}.
What I'm doing wrong?
I still haven't figured fully why, but you can make it work by destroying your model after the end of its removal (Backbone does one last thing after triggering the remove event: destroy the collection's reference in the model).
But what's even better, is using directly the destroy function on the model, it will remove it from the collection automatically (use {wait: true} if needed).
Edit:
Finally managed to locate the source of the problem. It's rather simple in fact. To override the model's url (calculated with urlRoot but that doesn't matter), you can pass Model#destroy a url option when calling Backbone.sync (or something that'll call it).
Now you're thinking "but I don't!". But you do. The listener (Model#destroy in your case) is given 3 arguments. Model#destroy will take the first one (the model itself) as options.
And here's the fail (I think Backbone needs a patch to this): giving an url option to Backbone.sync is the only time _.result in not used to calculate the url. So you find yourself having as url the url property of your model, which is the function you see in your call.
Now, for a quickfix:
this.on('remove', this.destroy.bind(this, {}));
This will ensure the first argument of your Model#destroy call is {} (as well as binding the context).
Bear with me a little longer.
Now, if you're still willing to call Collection#remove before destroying your model, here's a little hack: because (as I stated above) the remove event is triggered before Backbone makes sure to remove the collection's reference in your model, you don't need the urlRoot property in your model. Indeed, the model won't be in the collection anymore, but Backbone will still take the collection's url into account to get the model's url (as the reference is still there).
Not a definitive answer, but just going by the code in your question and the backbone.js documentation, the problem may be that you named your method remove and this is getting in the way of the remove method in Backbone.View.
http://backbonejs.org/#View-remove
Update:
It also looks like the output you see in the network inspector is that the definition of the Backbone.Model.url function is being appended. Meaning url is not being properly called (Maybe the () is missing by the caller?). Are you overriding Backbone.sync anywhere in your application?

ExtJS 4.1 Call One Controller From Another

Note: I'm a total ignoramus regarding javascript.
I've broken my ExtJS 4.1 MVC app out into several controllers like:
/app/controller/Auth
| |Quiz
| |Result
| |Blah...
|model/...
I want to respond to an "event", not a DOM Event, rather a Ext.form.action.Submit.success event by calling functions in both my Auth and Quiz controllers. The summarized code for the first part is here:
// File: app/controller/Auth.js
attemptLogin : function() {
var form = Ext.ComponentQuery.query('#loginpanel')[0].form;
if (form.isValid()) {
form.submit({
success : function(form, action) {
// THIS IS THE FUNCTION FROM THE CURRENT CONTROLLER
Assessor.controller.Auth.prototype.finishLogin();
// THIS IS THE FUNCTION FROM THE OTHER CONTROLLER
Assessor.controller.Quiz.prototype.setupAssessment();
},
This works but feels wrong. Is there a proper way to do this? It seems like I should fire a unique event that is listened to by both controllers, but I can't understand how to do that with Ext.Event. Any guidance?
Thanks! I'm really grateful for all the great ideas and advice.
It makes sense to me to fire a custom event from the form and simply listen to it in both your controllers, like what you said here:
It seems like I should fire a unique event that is listened to by both
controllers
// File: app/controller/Auth.js
attemptLogin : function() {
var form = Ext.ComponentQuery.down('#loginpanel').form;
if (form.isValid()) {
form.submit({
success : function(form, action) {
// fire the event from the form panel
form.owner.fireEvent('loginsuccess', form.owner);
},
Then in each of your controllers you can listen to it with Controller#control, like this:
Ext.define('YourApp.controller.Auth', {
extend: 'Ext.app.Controller',
init: function() {
var me = this;
me.control({
'#loginpanel': {
loginsuccess: me.someHandler
}
});
},
someHandler: function(form) {
//whatever needs to be done
console.log(form);
}
}
And then add the same thing to your Quiz controller:
Ext.define('YourApp.controller.Quiz', {
extend: 'Ext.app.Controller',
init: function() {
var me = this;
me.control({
'#loginpanel': {
loginsuccess: me.someOtherHandler
}
});
},
someOtherHandler: function(form) {
//whatever needs to be done
console.log(form);
}
}
I've used this approach successfully in 4.1.0 and 4.1.1
It really should be
Assessor.controller.Auth.prototype.finishLogin.apply(this, arguments)
or something along these lines (in order to have a correct this reference that points to the 'owner' of the method, the controller object)
However, why do you use this unorthodox way to call the current controller's method. Just set the scope for the success callback, then call this.finishLogin().
form.submit({
success : function(form, action) {
// THIS IS THE FUNCTION FROM THE CURRENT CONTROLLER
this.finishLogin();
...
},
scope: this
});
Also, you can retrieve another controller instance using Controller#getController.
this.getController('Assessor.controller.quiz').setupAssignment();
Then, if your controller methods are not depending on each other, you could make them both listen to the same event.
Another solution is to fire a custom event once the login is finished. You could do that on the application object
this.application.fireEvent('logincomplete');
and in your controller's init method:
this.application.mon('logincomplete', this.setupAssignment, this);
Please note that you cannot listen to those events via Controller#control - see Alexander Tokarev's blog post for a patch to Ext to achieve this.
There is no standard way to fire events between controllers, but it's possible with some custom hacks. See my recent blog post.
I have also been looking for this and all you need is Asanda.app.getController('quiz').setupAssignment();, where Asanda is the name of your app
You should use a MessageBus if you have to send events between controllers:
Ext.define('MyApp.utils.MessageBus', {
extend : 'Ext.util.Observable'
});
store the message bus in a global var
MsgBus = Ext.create('MyApp.utils.MessageBus');
Where you have to send events:
MsgBus.fireEvent('eventName',eventArg_1,eventArg_2);
Where you have to receive events:
MsgBus.on('eventName', functionHandler,scope); //scope is not mandatory
...
functionHandler:function(eventArg_1,eventArg_2){
...
//do whatever you want
...
}

uploadify + backbone events problem

I have a multiple files uploadify setting with:
'onComplete' : function(event, ID, fileObj, response, data) {
myCollection.add({params parsed from response json});
}
which triggers (trough this.collection.bind('add', this.add)) this collection view method:
add: function(obj) {
var view = new MyModelView({model: obj});
this.$('.insert-models-here').append(view.render().el);
return this;
},
The new MyModelView call triggers: MyModelView::initialize() which is here:
initialize: function() {
var t = $('#photo-template').html();
this.template = _.template(t);
this.model.view = this;
},
And every _.template() calls jumps inside __flash__toXML() method from which all thread is stopped.
The result is no model added inside my collection from any uploadify event.
Does anyone knows why and how to avoid this?
Ok, I found solution.
Problem was in using underscore in uploadify events so I replace underscore _.templates with icanhaz and rewrite my add() collection view method this way to workaround any underscore functionality:
add: function(obj) {
var view = new MyModelView({model: obj});
$('.insert-models-here').first().append(view.render().el);
return this;
},
Hope someone will call my name in future..

Resources