MVC Session global variable - asp.net-mvc

Here is what I'm trying to achieve. Certain options at the navbar should be available only if the user has "subordinates" in the database.
So, at the navbar I have:
The Approvals should be hidden for some users, but available to others. For those whom it should be available, the user must:
A) Be a Supervisor or,
B) Have a subornidate at the DB table
So, as for "A" it's pretty straightforward. I did:
#if (User.IsInRole("Supervisor"))
{
<li>#Html.ActionLink("Approvals", "Index", "Approval")</li>
}
For "B", I was suggested to use Sessions. Well, great. So I came to the question: how can I make a single request to the DB and assign it to a Session["HasSubordinates"] so I can do this check?
#if (User.IsInRole("Supervisor") || (bool)Session["HasSubordinates"])
{
<li>#Html.ActionLink("Approvals", "Index", "Approval")</li>
}
What I tried was to have:
Session["HasSubordinates"] = _uow.ApprovalService.GetSubordinates(User.Identity.Name).Count() > 0;
for every single controller, but that didn't worked well because sometimes I get null pointer and it looks absolutely rubbish.
I know it may sound like a trivial question for some (or most), but I'm really stuck and I do really appreciate any help.

Looking at your code, getting a user subordinates should only happen once. In your Login method:
Session["HasSubordinates"] = _uow.ApprovalService.GetSubordinates(User.Identity.Name).Count() > 0;
Create a new class to extend IPrincipal:
public class IPrincipalExtensions
{
public bool HasSubordinates(this IPrincipal user)
{
return Session != null && Session["HasSubordinates"] != null && Session["HasSubordinates"] > 0;
}
}
Now, in the View:
#if (User.IsInRole("Supervisor") || User.HasSubordinates() )
{
}
Writing from memory, may have left something out, but this should be the cleanest.

Don't use the session for this. What you need is a child action.
[ChildActionOnly]
public ActionResult Nav()
{
var model = new NavViewModel
{
IsSupervisor = User.IsInRole("Supervisor");
HasSubordinates = _uow.ApprovalService.GetSubordinates(User.Identity.Name).Count() > 0;
}
return ParialView("_Nav", model);
}
Then, just create a partial view, _Nav.cshtml and utilize the properties on the view model to render your nav however you like.
If you want, you can even use output caching on the child action, so it's only evaluated once per user. There's no built-in way to vary the cache by user, so first, you'll need to override the following method in Global.asax:
public override string GetVaryByCustomString(System.Web.HttpContext context, string custom)
{
var args = custom.ToLower().Split(';');
var sb = new StringBuilder();
foreach (var arg in args)
{
switch (arg)
{
case "user":
sb.Append(User.Identity.Name);
break;
case "ajax":
if (context.Request.Headers["X-Requested-With"] != null)
{
// "XMLHttpRequest" will be appended if it's an AJAX request
sb.Append(context.Request.Headers["X-Requested-With"]);
}
break;
default:
continue;
}
}
return sb.ToString();
}
With that, you can then just decorate your child action with:
[OutputCache(Duration = 3600, VaryByCustom = "User")]

Related

Handling Next Record nullable reference Controller Code to pass to viewbag

I have researched various nullable reference handling posts, but not finding anything helpful. So what I am doing below to handle this null reference (it's a hack for now to stop the error page from displaying to users) is to essentially return the current id if a next record does not exist in my edit controller.
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var myInClause = new string[] { "a", "c", "k" };
var myQueryResults = await _context.MyClass.FindAsync(id);
int? NextIdObject = (from i in _context.MyClass
where myInClause.Contains(i.RowType) && (i.Id > myclass.Id)
select new { i.Id }).DefaultIfEmpty().First().Id;
if (!NextIdObject.Equals(0))
{
ViewBag.nextID = NextIdObject;
}
else
{
ViewBag.nextID = id;
}
if (myQueryResults == null)
{
return NotFound();
}
return View(myQueryResults);
}
I would prefer to just redirect to the index page (if they hit this error, it means they are done working through a queue anyway, no next record would ever exist here). Or maybe just keep the code as is and display a message on the button to indicate end of list. Any thoughts here. Note, using any +1 increment on the id does not work for me, as I don't need the user to see all id records, just the one's with a/c/k which is why I bring the myInclause variable in. If there is a better way to use the SQL sytanx of "IN" for Linq queries, I am all ears.
I would prefer to just redirect to the index page (if they hit this
error, it means they are done working through a queue anyway, no next
record would ever exist here)
You could use try...catch block simply like
try
{
var myInClause = new string[] { "a", "c", "k" };
var myQueryResults = await _context.MyClass.FindAsync(id);
int NextIdObject = (from i in _context.MyClass
where myInClause.Contains(i.RowType) && (i.Id > myclass.Id)
select new { i.Id }).DefaultIfEmpty().First().Id;
ViewBag.nextID = NextIdObject;
}
catch(Exception ex)
{
return RedirectToAction("Index");
//or return View("Index");
}
return View(myQueryResults);

Outputcache 1 action, 2 views

So I have the following action which I am trying to add output caching to:
[OutputCache(CacheProfile = OutputCacheProfileNames.Hours24)]
public ActionResult ContactUs()
{
ContactUsModel model = _modelBuilder.BuildContactUsModel();
if (Request.IsAjaxRequest())
{
return Json(StringFromPartial(partialTemplate, model), JsonRequestBehavior.AllowGet);
}
else
{
return View(model);
}
}
But this seem to cache the first view that is requested - ie either the json OR the normal view.
Is there a way to get the output caching to work for both views, without having to split them out of the same action?
You beat me to the punch in answering your own question, but I thought this code may still be helpful. Since varying by user is such a common scenario, you should probably account for being able to do that and your AJAX vary. This code will allow you vary on any number of custom parameters, by appending to a single string to vary on.
public override string GetVaryByCustomString(System.Web.HttpContext context, string custom)
{
var args = custom.ToLower().Split(';');
var sb = new StringBuilder();
foreach (var arg in args)
{
switch (arg)
{
case "user":
sb.Append(User.Identity.Name);
break;
case "ajax":
if (context.Request.Headers["X-Requested-With"] != null)
{
// "XMLHttpRequest" will be appended if it's an AJAX request
sb.Append(context.Request.Headers["X-Requested-With"]);
}
break;
default:
continue;
}
}
return sb.ToString();
}
Then, you would just do something like the following if you need to vary by multiple custom params.
[OutputCache(CacheProfile = OutputCacheProfileNames.Hours24, VaryByCustom = "User;Ajax")]
Then, if you ever need additional custom vary params, you just keep adding case statements to cover those scenarios.
Thanks to the comments by REDEVI_ for pointing me in the right direction, I have been able to solve this.
I changed my output caching to:
[OutputCache(CacheProfile = OutputCacheProfileNames.Hours24, VaryByCustom = "IsAjax")]
And then in my global.asax file, I added the following override:
public override string GetVaryByCustomString(HttpContext context, string custom)
{
if (context != null)
{
switch (custom)
{
case "IsAjax":
return new HttpRequestWrapper(context.Request).IsAjaxRequest() ? "IsAjax" : "IsNotAjax";
}
}
return base.GetVaryByCustomString(context, custom);
}

View/Model data isn't refreshing/changing after post/postback, even though I'm using the PRG pattern

Update I have saved my problem a long time ago. The problem was that I was trying to call the view model on the wrong view method! I was calling the base view method (Document), instead of one of it's derived method (like NewDocument, PDFDocument, etc.) Thus it was only giving me the Documents data, which didn't change. I was looking and using the wrong view method all the time... Stephen, when you asked me
"Why do you create derived classes in a method but then return only the base class"
I couldn't answer the question at the time because I didn't even know myself, until I remember that originally, the method wasn't returning the base class. I only changed it so that it can work with the base view method, which was wrong in the first place!
That's what I get for only getting 3-4 hours of sleep in 3 days. Everything works right now. Thanks.
I'm having a hard time trying to figure out why the data in my view isn't changing after I do a post. Originally I was doing it via return View() and it worked, but since it was a partial view, the page didn't look great, so I was reading up and saw that it was better to do it by Post-Redirect-Get pattern (PRG) and to use an id value to retrieve the values instead of sending the entire model via Tempdata. I even used ModelState.Clear() and that didn't even work. When I debugged the code, the model only has the values from when I first called it.
Here's part of my Get controller:
NewDocument Get Controller
[DocumentAuthenticationFilter]
public ActionResult NewDocument(int? id = null)
{
// This doesn't work. The view keeps on showing the data from View(Services.CreateNewDocument()).
if (id != null)
{
return View(Services.GetdocumentViewModelData(DocEnum.Section.NEW_DOC_INDEX, (int)id));
}
// This works fine
return View(Services.CreateNewDocument());
}
And here's the post that calls the redirect:
NewDocument Post controller
[HttpPost]
[ValidateAntiForgeryToken]
[MultipleButton(Name = "action", Argument = "AddDocuments")]
//[OutputCache(Duration = 30, VaryByParam = "*")]
public ActionResult AddDocumentViewModel(FormCollection frm, DocumentViewModel dvm)
{
try
{
if (ModelState.IsValid)
{
int? DocID = Services.AddingNewDocument(dvm);
// See, I even tried to clear it.
ModelState.Clear();
return base.RedirectToAction("NewDocument", new { id = DocID });
}
else
{
// Display errors in the modal
}
return base.RedirectToAction("NewDocument");
}
And here's the old way I did it:
NewDocument Post controller
[HttpPost]
[ValidateAntiForgeryToken]
[MultipleButton(Name = "action", Argument = "AddDocuments")]
//[OutputCache(Duration = 30, VaryByParam = "*")]
public ActionResult AddDocumentViewModel(FormCollection frm, DocumentViewModel dvm)
{
try
{
if (ModelState.IsValid)
{
Services.AddingNewDocument(ref dvm);
dvm.NewRecordMode = DocEnum.Action.UPDATE;
// It worked, but only the partial view showed, and not the entire view.
return PartialView("_NewDocument", dvm);
}
else
{
// Display errors in the model
}
return base.RedirectToAction("NewDocument");
}
Could it be because I'm using a custom model binding?
My Custom Model Binding
public class BaseClassModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var modelType = bindingContext.ModelType;
var modelTypeValue = controllerContext.Controller.ValueProvider.GetValue("ViewModel");
if (modelTypeValue == null)
throw new Exception("View does not contain the needed derived model type name");
var modelTypeName = modelTypeValue.AttemptedValue;
var type = modelType.Assembly.GetTypes().SingleOrDefault(x => x.IsSubclassOf(modelType) && x.Name == modelTypeName);
if (type == null)
{
throw new Exception(String.Format("Derived model type {0} not found", modelTypeName));
}
var instance = bindingContext.Model ?? base.CreateModel(controllerContext, bindingContext, type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, type);
return base.BindModel(controllerContext, bindingContext);
}
}
EDIT: And here's the GetDocumentViewModelData code:
GetDocumentFromViewModelData
public static DocumentViewModel GetDocumentViewModelData(DocEnum.Section docType, int id)
{
switch (docType)
{
case DocEnum.Section.NEW_DOCUMENT_INDEX:
// NewDocumentTypeViewModel is a child to DocumentTypeViewModel
DocumentTypeViewModel nd = NewDocumentService.GetViewModelByID(id);
return nd;
case DocEnum.Section.PDF_DOCUMENT:
DocumentTypeViewModel pdfvm = PDFDocumentService.GetViewModelByID(id);
return pdfvm;
case DocEnum.Section.XLS_DOCUMENT:
DocumentTypeViewModel xlsvm = XLSDocumentService.GetViewModelByID(id);
return xlsvm;
}
return null;
}
Edit: Also adding the GetViewModelByID function
GetViewModelByID
public static DocumentTypeViewModel GetViewModelByID(int id)
{
docEntities db = new docEntities();
NewDocumentTypeViewModel vm = new NewDocumentTypeViewModel();
// Calls a stored procedure called Select_Documents_ByID(id) to get the note entry
// that was submitted.
List<Select_Documents_ByID_Result> prevNotes = db.Select_Documents_ByID(id).ToList();
StringBuilder sNotes = new StringBuilder();
foreach (var note in prevNotes)
{
sNotes.AppendFormat("{0} - {1}: {2}\n\n", note.CreatedDate.ToString("yyyy-MM-dd HH:mm"), note.username, note.Entry);
}
vm.PreviousNotes = sNotes.ToString();
return vm;
}
Edit: I did a direct creation of the view model inside the Get controller, and it's the same result. when i debugged the view itself, the values from the new view model don't show up. Instead, the values from the initial view model, View(Services.CreateNewDocument()), shows.
[DocumentAuthenticationFilter]
public ActionResult NewDocument(int? id = null)
{
// Right here I created the view model to test thing, but I'm getting the same results. Nothing has changed.
if (id != null)
{
var d = new NewDocumentTypeViewModel(1, "Help!");
// This property is from the base class, DocumentTypeViewModel
d.DocumentTitle = "Testing!";
return View(d);
// Inside the view itself, none of the values in the view model, including the one
// belonging to the base class. It still shows the initial values.
}
// This works fine
// Or maybe not...
return View(Services.CreateNewDocument());
}
Edit: I wanted to see if it was also doing the same thing for the initial call to the view return View(Services.CreateNewDocument()), and decided to change the value for documentTitle in the base class from New Document to a randomly-generated number, after the object has been created.
Here's the code for DocumentTypeViewModel's default constructor:
public DocumentTypeViewModel()
{
DocumentTitle = "New Document";
NewRecordMode = DocEnum.Action.ADD;
DocumentID = 0;
}
And here's the Services.CreateNewDocument() code where I change the DocumentTitle after the View Model has been created.
public DocumentTypeViewModel CreateNewDocument()
{
DocumentTypeViewModel dtvm = new DocumentTypeViewModel();
Random r = new Random();
dtvm.DocumentTitle = r.Next(5, Int32.MaxValue).ToString();
return dtvm;
}
Now in the View, when I call DocumentTitle:
<div class="label-text-group">
#Html.LabelFor(model => model.DocumentTitle)
#Html.EditorFor(model => model.DocumentTitle)
</div>
You would expect to see a randomly-generated number every time the View gets called. Nope, what you would see is "New Document". Weird.
It's seems that Services.GetDocumentViewModelData() is not exactly working correctly. It only carries the values created by the base class' constructor when a view is created, not any values that have been added or changed within GetDocumentViewModelData() itself. Why is that? What's going on? Please help anybody!
I have solved it. Look at the Update section on top. Thanks Stephen.

How to mock test with asp.net mvc

I am writing an app that I have been deploying to appharbor. I am having trouble getting my project to build now because I have expanded my tests. I believe the issue is that I am using a db initializer to populate the database with test seed data. These tests pass on my local box but once I deploy the tests fail on appharbor. I suspect I need to mock data but I am not sure how to do this. As an example, here is a controller test that I have for one of my action methods.
Controller
// GET: /Lead/Url
// TODO: Add optional url parameters
public ActionResult Url(string pfirstname, string plastname, string phone, int leadsource)
{
var lead = new Lead();
//store
lead.parent_FirstName = pfirstname;
lead.parent_LastName = plastname;
lead.parent_Phone = phone;
lead.LeadSourceID = leadsource;
lead.AgentID = 1;
if (ModelState.IsValid)
{
leadRepository.InsertLead(lead);
leadRepository.Save();
ViewBag.Message = "Success";
}
return View(lead);
}
//
// POST: /Lead/URL
[HttpPost, ActionName("Url")]
public ActionResult Url(Lead lead)
{
return View();
}
Unit Test
[TestMethod]
public void LeadUrl()
{
//ARRANGE
ILeadRepository leadrepository = new LeadRepository(new LeadManagerContext());
Database.SetInitializer<LeadManagerContext>(new LeadManagerInitializer());
LeadController controller = new LeadController(leadrepository);
//ACT
ViewResult result = controller.Url("Brad", "woods","465-456-4965",1) as ViewResult;
var lead = (Lead)result.ViewData.Model;
//ASSERT
Assert.AreEqual("Success" ,result.ViewBag.Message);
/*check for valid data */
Assert.AreEqual("Brad", lead.parent_FirstName);
}
Could someone please explain what I need to do next in order to improve code like this and get it to run again on app harbor successfully?
Actually you haven't verified interactions between controller and it's dependencies (repository). And this is the most important part - controller should pass your Lead object to repository. And then call Save (consider also to Unit Of Work pattern).
Also you should test controller in isolation, only this way you could be sure, that failing controller's test is an issue of controller, not of LeadRepository or LeadManagerInitializer.
// Arrange
Lead expected = CreateBrad();
var repository = new Mock<ILeadRepository>();
LeadController controller = new LeadController(repository.Object);
// Act
ViewResult result = (ViewResult)controller.Url("Brad", "woods", "465-456", 1);
// Assert
Lead actual = (Lead)result.ViewData.Model;
// All fields should be equal, not only name
Assert.That(actual, Is.EqualTo(expected));
Assert.AreEqual("Success", result.ViewBag.Message);
// You need to be sure, that expected lead object passed to repository
repository.Verify(r => r.InsertLead(expected));
repository.Verify(r => r.Save());
BTW I'd moved expected Lead creation to separate method:
private Lead CreateBrad()
{
Lead lead = new Lead();
lead.parent_FirstName = "Brad";
lead.parent_LastName = "woods";
lead.parent_Phone = "465-456";
lead.LeadSourceID = 1;
lead.AgentID = 1;
return lead;
}
Also you should override Equals method for Lead instances comparison:
public class Lead
{
// your current code here
public override bool Equals(object obj)
{
Lead other = obj as Lead;
if (other == null)
return false;
return other.parent_FirstName == parent_FirstName &&
other.parent_LastName == parent_LastName &&
// compare other properties here
other.AgentID == AgentID;
}
// also override GetHashCode method
}
BTW Why you don't pass Lead object to your action method (via POST message)?
You have to stub your repository. The easiest way to do that is to use mocking framework (I prefer Moq), and stub each method.
Something like this (for Moq):
var repository = new Mock<ILeadReporisory>();
repository.Setup(r => r.InsertLead(It.IsAny<Lead>()));
//raise, rinse, repeat
LeadController controller = new LeadController(repository.Object);

Is there a way to maintain IsAjaxRequest() across RedirectToAction?

If you don't want any context or an example of why I need this, then skip to The question(s) at the bottom!
In a bid to keep things tidy I initially built my application without JavaScript. I am now attempting to add a layer of unobtrusive JavaScript on the top of it.
In the spirit of MVC I took advantage of the easy routing and re-routing you can do with things like RedirectToAction().
Suppose I have the following URL to kick off the sign up process:
http://www.mysite.com/signup
And suppose the sign up process is two steps long:
http://www.mysite.com/signup/1
http://www.mysite.com/signup/2
And suppose I want, if JavaScript is enabled, the sign up form to appear in a dialog box like ThickBox.
If the user leaves the sign up process at step 2, but later clicks the "sign up" button, I want this URL:
http://www.mysite.com/signup
To perform some business logic, checking the session. If they left a previous sign up effort half way through then I want to prompt them to resume that or start over.
I might end up with the following methods:
public ActionResult SignUp(int? step)
{
if(!step.HasValue)
{
if((bool)Session["SignUpInProgress"] == true)
{
return RedirectToAction("WouldYouLikeToResume");
}
else
{
step = 1;
}
}
...
}
public ActionResult WouldYouLikeToResume()
{
if(Request.IsAjaxRequest())
{
return View("WouldYouLikeToResumeControl");
}
return View();
}
The logic in WouldYouLikeToResume being:
If it's an AJAX request, only return the user control, or "partial", so that the modal popup box does not contain the master page.
Otherwise return the normal view
This fails, however, because once I redirect out of SignUp, IsAjaxRequest() becomes false.
Obviously there are very easy ways to fix this particular redirect, but I'd like to maintain the knowledge of the Ajax request globally to resolve this issue across my site.
The question(s):
ASP.NET MVC is very, very extensible.
Is it possible to intercept calls to RedirectToAction and inject something like "isAjaxRequest" in the parameters?
OR
Is there some other way I can detect, safely, that the originating call was an AJAX one?
OR
Am I going about this the completely wrong way?
As requested by #joshcomley, an automated answer using the TempData approach:
This assumes that you have a BaseController and your controllers are inheriting from it.
public class AjaxianController : /*Base?*/Controller
{
private const string AjaxTempKey = "__isAjax";
public bool IsAjax
{
get { return Request.IsAjaxRequest() || (TempData.ContainsKey(AjaxTempKey)); }
}
protected override RedirectResult Redirect(string url)
{
ensureAjaxFlag();
return base.Redirect(url);
}
protected override RedirectToRouteResult RedirectToAction(string actionName, string controllerName, System.Web.Routing.RouteValueDictionary routeValues)
{
ensureAjaxFlag();
return base.RedirectToAction(actionName, controllerName, routeValues);
}
protected override RedirectToRouteResult RedirectToRoute(string routeName, System.Web.Routing.RouteValueDictionary routeValues)
{
ensureAjaxFlag();
return base.RedirectToRoute(routeName, routeValues);
}
private void ensureAjaxFlag()
{
if (IsAjax)
TempData[AjaxTempKey] = true;
else if (TempData.ContainsKey(AjaxTempKey))
TempData.Remove(AjaxTempKey);
}
}
To use this, make your controller inherit from AjaxianController and use the "IsAjax" property instead of the IsAjaxRequest extension method, then all redirects on the controller will automatically maintain the ajax-or-not flag.
...
Havn't tested it though, so be wary of bugs :-)
...
Another generic approach that doesn't require using state that I can think of may requires you to modify your routes.
Specifically, you need to be able to add a generic word into your route, i.e.
{controller}/{action}/{format}.{ajax}.html
And then instead of checking for TempData, you'd check for RouteData["ajax"] instead.
And on the extension points, instead of setting the TempData key, you add "ajax" to your RouteData instead.
See this question on multiple format route for more info.
This worked for me.
Please note that this doesn't require any session state which is a potential concurrency issue:
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
if (this.Request.IsAjaxRequest)
{
if (filterContext.Result is RedirectToRouteResult)
{
RedirectToRouteResult rrr = (RedirectToRouteResult)filterContext.Result;
rrr.RouteValues.Add("X-Requested-With",Request.Params["X-Requested-With"]);
}
}
}
}
Perhaps you can add a AjaxRedirected key in the TempData property before doing the redirection?
One way to transfer state is to add an extra route parameter i.e.
public ActionResult WouldYouLikeToResume(bool isAjax)
{
if(isAjax || Request.IsAjaxRequest())
{
return PartialView("WouldYouLikeToResumeControl");
}
return View();
}
and then in the Signup method:
return RedirectToAction("WouldYouLikeToResume", new { isAjax = Request.IsAjaxRequest() });
// Don't forget to also set the "ajax" parameter to false in your RouteTable
// So normal views is not considered Ajax
Then in your RouteTable, default the "ajax" parameter to false.
Or another way to go would be override extension points in your BaseController (you do have one, right?) to always pass along the IsAjaxRequest state.
..
The TempData approaches are valid too, but I'm a little allergic of states when doing anything that looks RESTful :-)
Havn't tested/prettify the route though but you should get the idea.
I would just like to offer what I believe is a MUCH better answer than the current accepted one.
Use this:
public class BaseController : Controller
{
private string _headerValue = "X-Requested-With";
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
var ajaxHeader = TempData[_headerValue] as string;
if (!Request.IsAjaxRequest() && ajaxHeader != null)
Request.Headers.Add(_headerValue, ajaxHeader);
}
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
if (Request.IsAjaxRequest() && IsRedirectResult(filterContext.Result))
TempData[_headerValue] = Request.Headers[_headerValue];
}
private bool IsRedirectResult(ActionResult result)
{
return result.GetType().Name.ToLower().Contains("redirect");
}
}
Then make all your controllers inherit from this.
What it does:
Before an action executes this checks to see if there is a value in TempData. If there is then it manually adds its value to the Request object's header collection.
After an action executes it checks if the result was a redirect. If it was a redirect and the request was an Ajax Request before this action was hit then it reads the value of the custom ajax header that was sent and stores it in temp data.
This is better because of two things.
It is shorter and cleaner.
It adds the request header to the Request object after reading the temp data. This allows Request.IsAjaxRequest() to work normally. No calling a custom IsAjax property.
Credit to: queen3 for his question containing this solution. I did modify it to clean it up a bit but it is his solution originally.
The Problem is in the Client-Cache.
To overcome this, just add a cachebreaker
like "?_=XXXXXX" to Location Url in the 302 Response.
Here is my working Filter. Regisiter it in the GlobalFilter Collection.
I added the Location Header to the Redirected Response, so the client script can get the destination url, in the ajax call. (for Google-Analytics)
public class PNetAjaxFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var request = filterContext.HttpContext.Request;
if(request.QueryString["_"] == "ajax")
{
filterContext.HttpContext.Request.Headers["X-Requested-With"] = "XMLHttpRequest";
request.QueryString.Remove("_");
}
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
//public override void OnResultExecuting(ResultExecutingContext filterContext)
{
var context = filterContext.HttpContext;
if (!context.Request.IsAjaxRequest())
return;
var request = context.Request;
String noCacheQuery = String.Empty;
if (request.HttpMethod == "GET")
{
noCacheQuery = request.QueryString["_"];
}
else if (context.Response.IsRequestBeingRedirected)
{
var pragma = request.Headers["Pragma"] ?? String.Empty;
if (pragma.StartsWith("no-cache", StringComparison.OrdinalIgnoreCase))
{
noCacheQuery = DateTime.Now.ToUnixTimestamp().ToString();
}
else
{
//mode switch: one spezial cache For AjaxResponse
noCacheQuery = "ajax";
}
}
if (!String.IsNullOrEmpty(noCacheQuery))
{
if (context.Response.IsRequestBeingRedirected)
{
var location = context.Response.RedirectLocation;
if (location.Contains('?'))
location += "&_=" + noCacheQuery;
else
location += "?_=" + noCacheQuery;
context.Response.RedirectLocation = location;
}
else
{
var url = new UriBuilder(request.Url);
if (url.Port == 80 && url.Scheme == Uri.UriSchemeHttp)
url.Port = -1;
else if(url.Port == 443 && url.Scheme == Uri.UriSchemeHttps)
url.Port = -1;
if(!String.IsNullOrEmpty(url.Query))
url.Query = String.Join("&", url.Query.Substring(1).Split('&').Where(s => !s.StartsWith("_=")));
context.Response.AppendHeader("Location", url.ToString());
}
}
}
}
And here the jQuery:
var $form = $("form");
var action = $form.attr("action");
var $item = $("body");
$.ajax({
type: "POST",
url: action,
data: $form.serialize(),
success: function (data, status, xhr) {
$item.html(data);
var source = xhr.getResponseHeader('Location');
if (source == null) //if no redirect
source = action;
$(document).trigger("partialLoaded", { source: source, item: $item });
}
});

Resources