I want to get the last modified time of any part of a view prior to it rendering. This includes layout pages, partial views etc.
I want to set a proper time for
Response.Cache.SetLastModified(viewLastWriteUtcTime);
to properly handle http caching. Currently I have this working for the view itself however if there are any changes in the layout pages, or child partial views those are not picked up by
var viewLastWriteUtcTime = System.IO.File.GetLastWriteTime(
Server.MapPath(
(ViewEngines.Engines.FindView(ControllerContext, ViewBag.HttpMethod, null)
.View as BuildManagerCompiledView)
.ViewPath)).ToUniversalTime();
Is there any way I can get the overall last modified time?
I don't want to respond with 304 Not Modified after deployments that modified a related part of the view as users would get inconsistent behavior.
I'm not going to guarantee that this is the most effective way to do it, but I've tested it and it works. You might need to adjust the GetRequestKey() logic and you may want to choose an alternate temporary storage location depending on your scenario. I didn't implement any caching for file times since that seemed like something you wouldn't be interested in. It wouldn't be hard to add if it was ok to have the times be a small amount off and you wanted to avoid the file access overhead on every request.
First, extend RazorViewEngine with a view engine that tracks the greatest last modified time for all the views rendered during this request. We do this by storing the latest time in the session keyed by session id and request timestamp. You could just as easily do this with any other view engine.
public class CacheFriendlyRazorViewEngine : RazorViewEngine
{
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
UpdateLatestTime(controllerContext, GetLastModifiedForPath(controllerContext, viewPath));
var pathToMaster = masterPath;
if (string.IsNullOrEmpty(pathToMaster))
{
pathToMaster = "~/Views/Shared/_Layout.cshtml"; // TODO: derive from _ViewStart.cshtml
}
UpdateLatestTime(controllerContext, GetLastModifiedForPath(controllerContext, pathToMaster));
return base.CreateView(controllerContext, viewPath, masterPath);
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
UpdateLatestTime(controllerContext, GetLastModifiedForPath(controllerContext, partialPath));
return base.CreatePartialView(controllerContext, partialPath);
}
private DateTime GetLastModifiedForPath(ControllerContext controllerContext, string path)
{
return System.IO.File.GetLastWriteTime(controllerContext.HttpContext.Server.MapPath(path)).ToUniversalTime();
}
public static void ClearLatestTime(ControllerContext controllerContext)
{
var key = GetRequestKey(controllerContext.HttpContext);
controllerContext.HttpContext.Session.Remove(key);
}
public static DateTime GetLatestTime(ControllerContext controllerContext, bool clear = false)
{
var key = GetRequestKey(controllerContext.HttpContext);
var timestamp = GetLatestTime(controllerContext, key);
if (clear)
{
ClearLatestTime(controllerContext);
}
return timestamp;
}
private static DateTime GetLatestTime(ControllerContext controllerContext, string key)
{
return controllerContext.HttpContext.Session[key] as DateTime? ?? DateTime.MinValue;
}
private void UpdateLatestTime(ControllerContext controllerContext, DateTime timestamp)
{
var key = GetRequestKey(controllerContext.HttpContext);
var currentTimeStamp = GetLatestTime(controllerContext, key);
if (timestamp > currentTimeStamp)
{
controllerContext.HttpContext.Session[key] = timestamp;
}
}
private static string GetRequestKey(HttpContextBase context)
{
return string.Format("{0}-{1}", context.Session.SessionID, context.Timestamp);
}
}
Next, replace the existing engine(s) with your new one in global.asax.cs
protected void Application_Start()
{
System.Web.Mvc.ViewEngines.Engines.Clear();
System.Web.Mvc.ViewEngines.Engines.Add(new ViewEngines.CacheFriendlyRazorViewEngine());
...
}
Finally, in some global filter or on a per-controller basis add an OnResultExecuted. Note, I believe OnResultExecuted in the controller runs after the response has been sent, so I think you must use a filter. My testing indicates this to be true.
Also, note that I am clearing the value out of the session after it is used to keep from polluting the session with the timestamps. You might want to keep it in the Cache and set a short expiration on it so you don't have to explicitly clean things out or if your session isn't kept in memory to avoid the transaction costs of storing it in the session.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class UpdateLastModifiedFromViewsAttribute : ActionFilterAttribute
{
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
var cache = filterContext.HttpContext.Response.Cache;
cache.SetLastModified(CacheFriendlyRazorViewEngine.GetLatestTime(filterContext.Controller.ControllerContext, true));
}
}
Finally, apply the filter to the controller you want to use it on or as a global filter:
[UpdateLastModifiedFromViews]
public class HomeController : Controller
{
...
}
Related
I need some advice on how to proceed with the mvc app I'm building. On my page I type out who is logged in to the page. This I first did by creating a base class where I created a user class which contained the users name and a image representing the user. Then I passed this class on to my views. But I also need to pass other models to my views depending on what view I'm in. Sure I could build a class that contain all different models I need to use on each page but there should be a easy way to pass name and image values across the pages and be persistant? I tried TempData together with TempData.Keep() but that was not persistant. How can I pass theses values between pages?
public ActionResult Validate(AccountModels.LoginModel model)
{
if (ModelState.IsValid)
{
if (Membership.ValidateUser(model.UserName, model.Password))
{
var mu = _repo.GetUser(Membership.GetUser().ProviderUserKey.ToString());
TempData["Name"] = mu.Name;
TempData["Image"] = mu.Image;
TempData.Keep();
FormsAuthentication.RedirectFromLoginPage(model.UserName, model.RememberMe);
}
}
return View("Index");
}
As #Jyoti said, you could use of Keep() method.
To make it easy to work with TempData, I wrote these methods in my BaseController, and I use it in every controller when I need to transfer data between actions or between view and controller.
protected TReturnType GetTempDataValue<TReturnType>(PsmConstants.TempDataKey sessionName, bool peekData =false )
{
object value = peekData ? TempData.Peek(sessionName.ToString()) : TempData[sessionName.ToString()];
return (TReturnType) value;
}
protected void RemoveTempData(PsmConstants.TempDataKey sessionName)
{
if (TempData.ContainsKey(sessionName.ToString()) && TempData[sessionName.ToString()] == null) return;
TempData[sessionName.ToString()] = null;
}
protected void SetTempDataValue(PsmConstants.TempDataKey sessionName, object value)
{
if(TempData.ContainsKey(sessionName.ToString()))
TempData[sessionName.ToString()]=null;
TempData[sessionName.ToString()] = value;
}
protected void KeepTempDataValue(PsmConstants.TempDataKey sessionName)
{
if (TempData.ContainsKey(sessionName.ToString()))
TempData.Keep(sessionName.ToString());
}
And this is the Keys enumeration :
public enum TempDataKey
{
PageError = 1,
PageInfo = 2
}
And this is, the usage of these methods(Set value and Get value from TempData):
SetTempDataValue(PsmConstants.TempDataKey.PageError , 'your error message' );
var originalValues = GetTempDataValue<MyModel>(PsmConstants.TempDataKey.Info, true);
Use session instead of Temp if it is not working.but i think it should work.
TempData["Name"] = mu.Name;TempData["Image"] = mu.Image;TempData.Keep();
How you are passing this into other models,Please share the source code so that it will easy to identify.
In the pursuit of solving an absurd problem, I need to append a value to my querystring (did that in javascript) and test if it exists on the server (because this may come from ajax or an iframe, so there's no header potential, sadly, and I'm trying to avoid adding a value to my <form> element). To that end, I've devised this little snippet, and I'm not sure where to put the "setter" block:
using System.Web.Mvc;
namespace Company.Client.Framework.Mvc
{
class CreateFormDialogResponseAttribute : ActionFilterAttribute
{
private bool SetContentType { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
SetContentType = !filterContext.HttpContext.Request.Params["FormDialogRequest"].IsEmpty();
base.OnActionExecuting(filterContext);
}
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
//do I set it here? (A)
if (SetContentType)
filterContext.HttpContext.Response.ContentType = "text/json";
base.OnResultExecuting(filterContext);
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
//do I set it here? (B)
if (SetContentType)
filterContext.HttpContext.Response.ContentType = "text/json";
base.OnResultExecuted(filterContext);
}
}
}
If I set it at (A) then it seems like the client still has time to "override" the attribute, if need be. Whereas (B) looks like the "I don't care what you think you want to do" position. Is that an accurate understanding?
self-answer:
Because I need to override the value set by the ActionResult itself, I have to come in after the method as already done it's work. Because I want to avoid the situation where the developer has manually set the type, I'm doing two checks, one to see if we should set it, and one to see if it's "application/json" (the default).
In our code we have an MVC controller action that is decorated with the OutputCacheAttribute. Is there any way in some other action to clear the cache for the first action?
It depends. If it is a child action the cache is stored in the MemoryCache and the only way to clear it is undocumented and involves busting the whole memory cache:
OutputCacheAttribute.ChildActionCache = new MemoryCache("NewDefault");
The drawback of course is that this removes all cached child actions and not just the cached output of this child action. If it is a normal action then you could use the Response.RemoveOutputCacheItem method by passing it the url of the action that was cached. You might also find the following article interesting.
Caching in ASP.NET MVC 3 has still a very long way to go. Hopefully they are improving many things in ASP.NET MVC 4 and simplifying it.
Yes, it is possible
I have found the answer in this book.
See 14.3 Output Cache In ASP.NET MVC
page 372 - Deterministically removing items from cache
When we decorate an action with the OutputCacheAttribute , OutputCache
stores its result into ASP.NET Cache and automatically recovers it
when it must serve a subse- quent analogous request. If we knew which
cache key the page belongs to, we could easily remove it.
Unfortunately, this isn’t easily possible, and even if it were, we
aren’t supposed to know it because it resides in the internal logic
of the caching infrastruc- ture and might change without notice in
future releases. What we can do, though, is leverage the cache
dependencies mechanism to achieve a similar result. This feature is
similar to the change monitors we’re going to talk about in section
14.4. Leveraging cache dependencies consists of tying one cache entry to another to automatically remove the first one when the latter is invalidated.
And a piece of code
public class DependencyOutputCacheAttribute : OutputCacheAttribute
{
public string ParameterName { get; set; }
public string BasePrefix { get; set; }
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
base.OnResultExecuting( filterContext );
string key = string.IsNullOrEmpty( BasePrefix )
? filterContext.RouteData.Values["action"] + "_" + filterContext.RouteData.Values["controller"]
: BasePrefix;
key = AddToCache( filterContext, key, ParameterName);
}
private string AddToCache(ResultExecutingContext filterContext, string key, string parameter)
{
if ( !string.IsNullOrEmpty( parameter ) && filterContext.RouteData.Values[parameter] != null) {
key += "/" + filterContext.RouteData.Values[parameter];
filterContext.HttpContext.Cache.AddBig( key, key );
filterContext.HttpContext.Response.AddCacheItemDependency( key );
}
return key;
}
}
And Remove cache dependency attribute
public class RemoveCachedAttribute : ActionFilterAttribute
{
public string ParameterName { get; set; }
public string BasePrefix { get; set; }
public override void OnResultExecuting( ResultExecutingContext filterContext)
{
base.OnResultExecuting(filterContext);
string key = string.IsNullOrEmpty(BasePrefix) ?
filterContext.RouteData.Values["action"].ToString() + "_" +
filterContext.RouteData.Values["controller"].ToString() : BasePrefix;
if (!string.IsNullOrEmpty(ParameterName))
key += filterContext.RouteData.Values[ParameterName];
filterContext.HttpContext.Cache.Remove(key);
}
}
and finally use it
[DependencyCache( BasePrefix = "Story", ParameterName = "id" )]
public virtual ActionResult ViewStory(int id){
//load story here
}
[HttpPost, RemoveCache( BasePrefix = "Story", ParameterName = "id" )]
public virtual ActionResult DeleteStory(int id){
//submit updated story version
}
[HttpPost, RemoveCache( BasePrefix = "Story", ParameterName = "id" )]
public virtual ActionResult EditStory(Story txt){
//submit updated story version
}
where
public class Story {
int id {get;set;} //case is important
string storyContent{get;set;}
}
Basically, I was wondering if anyone knows of a way that you can set up MVC3 in a way that it will first look for an action, and if none exists, it will automatically return the view at that location. Otherwise each time I make a page, I will have to rebuild it after adding the action.
It isn't something that's stopping the project from working nor is it an issue, it would just be a very nice thing to include in the code to help with speed of testing more than anything.
EDIT:
Just for clarity purposes, this is what I do every time I create a view that doesn't have any logic inside it:
public ActionResult ActionX()
{
return View();
}
Sometimes I will want some logic inside the action, but majority of the time for blank pages I will just want the above code.
I would like it if there was any way to always return the above code for every Controller/Action combination, UNLESS I have already made an action, then it should use the Action that I have specified.
Thanks,
Jake
Why not just create a single action for this. This will look for a view with the specified name and return a 404 if it doesn't exist.
[HttpGet]
public ActionResult Page(string page)
{
ViewEngineResult result = ViewEngines.Engines.FindView(ControllerContext, page, null);
if (result == null)
{
return HttpNotFound();
}
return View(page);
}
Then make your default route fall back to this:
routes.MapRoute("", "{page}", new { controller = "Home", action = "Page" });
So a request to http://yoursite.com/somepage will invoke Page("somepage")
I'm not altogether sure how useful this will be (or whether its really a good idea) but I guess if you have pages which are purely static content (but maybe use a layout or something so you can't use static html) it could be useful
This is how it could be done though anyway (as a base class, but it doesn't have to be)
public abstract class BaseController : Controller
{
public ActionResult Default()
{
return View();
}
protected override IActionInvoker CreateActionInvoker()
{
return new DefaultActionInvoker();
}
private class DefaultActionInvoker : ControllerActionInvoker
{
protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
{
var actionDescriptor = base.FindAction(controllerContext, controllerDescriptor, actionName);
if (actionDescriptor == null)
actionDescriptor = base.FindAction(controllerContext, controllerDescriptor, "Default");
return actionDescriptor;
}
}
}
I am trying to test a web page that has ajax calls to update a price.
An ajax call is fired on page load to update an initially empty div.
This is the extension method I'm using to wait for a change in the inner text of the div.
public static void WaitForTextChange(this IE ie, string id)
{
string old = ie.Element(id).Text;
ie.Element(id).WaitUntil(!Find.ByText(old));
}
However it's not pausing even though when I write out the old value and ie.Element(id).Text after the wait, they are both null. I can't debug as this acts as a pause.
Can the Find.ByText not handle nulls or have I got something wrong.
Has anyone got some code working similar to this?
I found my own solution in the end after delving into WatiN's constraints.
Here's the solution:
public class TextConstraint : Constraint
{
private readonly string _text;
private readonly bool _negate;
public TextConstraint(string text)
{
_text = text;
_negate = false;
}
public TextConstraint(string text, bool negate)
{
_text = text;
_negate = negate;
}
public override void WriteDescriptionTo(TextWriter writer)
{
writer.Write("Find text to{0} match {1}.", _negate ? " not" : "", _text);
}
protected override bool MatchesImpl(IAttributeBag attributeBag, ConstraintContext context)
{
return (attributeBag.GetAdapter<Element>().Text == _text) ^ _negate;
}
}
And the updated extension method:
public static void WaitForTextChange(this IE ie, Element element)
{
string old = element.Text;
element.WaitUntil(new TextConstraint(old, true));
}
It assumes that the old value will be read before the change so there's a slight chance of a race condition if you use it too long after setting off the update but it works for me.