Is there any solution for accessing TempData property in the HttpResponseBase.WriteSubstitution() method
This does not work:
<%= Response.WriteSubstitution(x => Html.Encode(TempData["message"].ToString())) %>
But this works:
<%= Response.WriteSubstitution(x => DateTime.Now.ToString()) %>
The problem is in request processing for once cached pages. According to http://msdn.microsoft.com/en-us/library/system.web.httpresponse.writesubstitution.aspx:
On the first request to the page, the WriteSubstitution calls the HttpResponseSubstitutionCallback delegate to produce the output. Then, it adds a substitution buffer to the response, which retains the delegate to call on future requests. Finally, it degrades client-side cacheability from public to server-only, ensuring future requests to the page re-invoke the delegate by not caching on the client.
In other words the delegate does not have access to Session property (SessionStateTempDataProvider stores TempData in session) because there is no "normal" request life cycle. As I understand it is processed at HttpApplication.ResolveRequestCache/HttpApplication.PostResolveRequestCache event, but the current state is acquired at the HttpApplication.AcquireRequestState event (http://msdn.microsoft.com/en-us/library/ms178473.aspx)
Maybe I need some kind of "advanced custom TempDataProvider" :) Any ideas?
I've found the solution:
The main idea is in saving copy of TempData in the Cache and retreiving it on every request. The solution is a combination of custom TempDataProvider and simple http module. Plus there are couple of helpers and static classes.
Here is the code:
CustomTempDataProvider:
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Caching;
using System.Web.Mvc;
public class CustomTempDataProvider : SessionStateTempDataProvider, IHttpModule
{
public void Init(HttpApplication application)
{
application.BeginRequest += new EventHandler(application_BeginRequest);
}
void application_BeginRequest(object sender, EventArgs e)
{
var httpContext = HttpContext.Current;
var tempData = httpContext.Cache[TempDataKey] ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
httpContext.Items.Add("TempData", tempData);
httpContext.Cache.Remove(TempDataKey);
}
public override void SaveTempData(ControllerContext controllerContext,
IDictionary<string, object> values)
{
HttpContext.Current.Cache.Insert(TempDataKey, values, null, DateTime.Now.AddMinutes(5), Cache.NoSlidingExpiration, CacheItemPriority.NotRemovable, null);
base.SaveTempData(controllerContext, values);
}
public static string TempDataKey
{
get
{
string sessionID = "0";
var httpContext = HttpContext.Current;
if(httpContext.Session != null)
{
sessionID = httpContext.Session.SessionID;
}
else if (httpContext.Request.Cookies["ASP.NET_SessionId"] != null)
{
sessionID = httpContext.Request.Cookies["ASP.NET_SessionId"].Value;
}
return "TempData-For-Session-" + sessionID;
}
}
public void Dispose()
{
}
}
Register it in the web.config:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.web>
<httpModules>
<add name="CustomTempDataProvider" type="CustomTempDataProvider" />
</httpModules>
</system.web>
<system.webServer>
<modules>
<remove name="CustomTempDataProvider" />
<add name="CustomTempDataProvider" type="CustomTempDataProvider" />
</modules>
</system.webServer>
</configuration>
CustomControllerFactory:
using System.Web.Routing;
using System.Web.Mvc;
public class CustomControllerFactory : DefaultControllerFactory
{
public override IController CreateController(
RequestContext requestContext, string controllerName)
{
var controller = (Controller)base.CreateController(requestContext, controllerName);
controller.TempDataProvider = new CustomTempDataProvider();
return controller;
}
}
register it in the Global.asax:
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ControllerBuilder.Current.SetControllerFactory(typeof(CustomControllerFactory));
}
Static class for accessing TempData:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
public static class CustomTempData
{
public static object Get(string key)
{
var tempData = HttpContext.Current.Items["TempData"] as IDictionary<string, object>;
var item = tempData.FirstOrDefault(x => x.Key == key).Value ?? String.Empty;
return item;
}
}
Helper for Post-cache Substitution:
using System;
using System.Web;
using System.Web.Mvc;
public static class Html
{
public delegate object MvcResponseSubstitutionCallback(HttpContextBase context);
public static object MvcResponseSubstitute(this HtmlHelper html, MvcResponseSubstitutionCallback callback)
{
html.ViewContext.HttpContext.Response.WriteSubstitution(
context =>
HttpUtility.HtmlEncode(
(callback(new HttpContextWrapper(context)) ?? String.Empty).ToString()
)
);
return null;
}
}
Now this works successfully:
<h3><%= Html.MvcResponseSubstitute(context => CustomTempData.Get("message")) %></h3>
If you understand russian, read this
Hope this helps
Related
I am trying to implement a global exception handling technique to avoid using try/catch blocks on every action of my controllers but my application crashes before the overridden OnException method or the ErrorHandleAttribute are reached.
Here is a simplified testing code:
[HandleError]
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult Test()
{
throw new Exception("Exception Test");
}
protected override void OnException(ExceptionContext filterContext)
{
if (filterContext == null || filterContext.ExceptionHandled)
{
return;
}
var message = filterContext.Exception.Message;
filterContext.Result = new HttpStatusCodeResult(HttpStatusCode.BadRequest, message);
filterContext.ExceptionHandled = true;
}
}
and in Web.Config:
<system.web>
<customErrors mode="On" defaultRedirect="Error"/>
If I set a break point on the 'throw' line and try to access /Home/Test my application will crash and both OnException method and the HandleErrorAttribute will be reached only after that.
Am I missing something?
Thank you.
You need to register the HandleError filter.
filters.Add(new HandleErrorAttribute{View = "Error"});
I have a custom ValidationAttribute that implements IClientValidatable. But the GetClientValidationRules is never called to actually output the validation rules to the client side.
There is nothing special about the attribute but for some reason it is never called. I've tried registering an adapter in Application_Start() but that also doesnt work.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class CustomAttribute : ValidationAttribute, IClientValidatable
{
public override bool IsValid(object value)
{
return true;
}
#region IClientValidatable Members
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
string errorMessage = FormatErrorMessage(metadata.GetDisplayName());
yield return new ModelClientValidationRule { ErrorMessage = errorMessage, ValidationType = "custom" };
}
#endregion
}
public class CustomAdapter : DataAnnotationsModelValidator<CustomAttribute>
{
public CustomAdapter(ModelMetadata metadata, ControllerContext context, CustomAttribute attribute)
: base(metadata, context, attribute)
{
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
return this.Attribute.GetClientValidationRules(this.Metadata, this.ControllerContext);
}
}
In Application_Start() I have:
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(CustomAttribute), typeof(CustomAdapter));
When I put a breakpoint inside GetClientValidationRules it is never hit.
In order GetClientValidationRules() method to get called you must enable client-side validation support. It can be done in the following ways:
In the web.config (for all pages of application):
<appSettings>
<add key="ClientValidationEnabled" value="true" />
Or on particular view only:
either
#{ Html.EnableClientValidation(); }
or
#(ViewContext.ClientValidationEnabled = true)
Please note it must go before
#using (Html.BeginForm())
statement.
If you are using jquery unobtrusive validation (which seems to be a standard currently), you'll also need to enable it:
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
in web.config or
#Html.EnableUnobtrusiveJavaScript()
for particular views.
In an ASP.NET MVC application, changing the Thread.CurrentThread.CurrentCulture[UI] can change the way MVC picks messages from resources. In the example application I've created to present the problem, there are two resource file - Res.resx for the English messages and Res.es.resx for the Spanish messages.
However, error messages resulting from the model validation always display in English.
My question is, how can I control the language in which the model validation error messages are displayed?
Below are parts of an example application I've wrote (based on the default ASP.NET MVC application) to demonstrate this problem.
Screenshots of how it looks in the browser:
https://dl.dropboxusercontent.com/u/4453002/SO_LanguageOfValidation.png
ViewModel and Controller - HomeController.cs :
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Threading;
using System.Web.Mvc;
namespace SO_ValidationMessageInEnglish.Controllers {
/// <summary>
/// A very basic view model.
/// </summary>
public class ViewModel {
[Display(Name = "Message", ResourceType = typeof(Res))]
[DisplayName("Message")]
[Required(AllowEmptyStrings = false, ErrorMessageResourceName = "MessageRequired", ErrorMessageResourceType = typeof(Res))]
public string Message { get; set; }
public string Language { get; set; }
}
public class HomeController : Controller {
public ActionResult Index(string language = "en") {
Thread.CurrentThread.CurrentCulture = new CultureInfo(language);
Thread.CurrentThread.CurrentUICulture = new CultureInfo(language);
return View();
}
[HttpPost]
[ActionName("Index")]
public ActionResult IndexPost(ViewModel foo) {
Thread.CurrentThread.CurrentCulture = new CultureInfo(foo.Language ?? "en");
Thread.CurrentThread.CurrentUICulture = new CultureInfo(foo.Language ?? "en");
return View(foo);
}
}
}
View - Index.cshtml :
#model SO_ValidationMessageInEnglish.Controllers.ViewModel
#using SO_ValidationMessageInEnglish
#{ ViewBag.Title = Res.Title; }
#Res.CurrentMessage:<br />
<h2>#((Model != null) ? Model.Message : Res.Default)</h2>
<p />
#using (Html.BeginForm("Index", "Home", FormMethod.Post, null)) {
#Html.LabelFor(m => m.Message)
#Html.TextBoxFor(m => m.Message)
#Html.HiddenFor(m => m.Language)
#Html.ValidationMessageFor(m => m.Message)
<input type="submit" value="#Res.Submit" />
}
I also run into the same problem. When the model binder has invalid data it runs before the ActionFilter(s).
I didn't like the proposed solutions because messing with the routing was not my preferred solution. Listen for Application_AcquireRequestState is problematic because this event fire for each and every request, not just for requests that will be routed into MVC controllers.
I've end up writing a custom implementation of IControllerFactory that use DefaultControllerFactory internally and execute the localization code inside CreateController method.
This is not ideal either, hope it helps.
public class PluggableControllerFactory : IControllerFactory {
private readonly IControllerFactory innerControllerFactory;
public PluggableControllerFactory() {
innerControllerFactory = new DefaultControllerFactory();
}
public IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName) {
// Run your culture localization here
return innerControllerFactory.CreateController(requestContext, controllerName);
}
public System.Web.SessionState.SessionStateBehavior GetControllerSessionBehavior(System.Web.Routing.RequestContext requestContext, string controllerName) {
return innerControllerFactory.GetControllerSessionBehavior(requestContext, controllerName);
}
public void ReleaseController(IController controller) {
innerControllerFactory.ReleaseController(controller);
}
}
}
.NET uses full culture code to pick up the resource file as first choice. Rename Res.es.resx to Res.es-ES.resx and see if that works.
I guess, one way to do that is to use your own model binder (although that might cause other problems in some situations). That will execute only when there is a model on the action, thus you might have to set the culture in two places (one for all actions and this for the model validation in particular):
public class MyModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
{
var un = HttpContext.Current.User.Identity.Name;
if (!string.IsNullOrWhiteSpace(un))
{
var userLanguageCode = .......
var culture = CultureInfo.GetCultureInfo(userLanguageCode ?? "en-US");
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
I found a different and in my opinion better solution that requires less work. In the Global.asax file:
protected void Application_AcquireRequestState(object sender, EventArgs e)
{
// Run your culture localization here
}
Is it possible to iterate the OutputCache keys? I know you can remove them individually via HttpResponse.RemoveOutputCacheItem(), but is there a way I can iterate all the keys to see what's in the collection?
I searched through Object Viewer, but didn't see anything.
Worst case, I can maintain my own index. Since I'm doing everything by VaryByCustom, they get "fed" through a method in global.asax. It just strikes me that there has to be a more elegant way of doing this.
If you're using ASP.NET 4.0 you could do this by writing your own OutputCacheProvider. That would give you the ability to store the keys at the point that the item is cached:
namespace StackOverflowOutputCacheProvider
{
public class SOOutputCacheProvider: OutputCacheProvider
{
public override object Add(string key, object entry, DateTime utcExpiry)
{
// Do something here to store the key
// Persist the entry object in a persistence layer somewhere e.g. in-memory cache, database, file system
return entry;
}
...
}
}
You'd then be able to read the keys out from wherever you've stored them.
This can be accomplished by inheriting MemoryCache and exposing the enumerator via a custom OutputCacheProvider implementation. Keep in mind that the enumerator locks the cache. Enumerating over the cache should be executed infrequently.
namespace Caching
{
internal class MemoryCacheInternal : System.Runtime.Caching.MemoryCache
{
public MemoryCacheInternal(string name, System.Collections.Specialized.NameValueCollection config = null) : base(name, config)
{
}
public System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<string, object>> Enumerator()
{
return base.GetEnumerator();
}
}
}
Implement a custom OutputCacheProvider
using System.Web.Caching;
using System.Collections.Generic;
namespace Caching
{
public class EnumerableMemoryOutputCacheProvider : OutputCacheProvider, IEnumerable<KeyValuePair<string, object>>, IDisposable
{
private static readonly MemoryCacheInternal _cache = new MemoryCacheInternal("EnumerableMemoryOutputCache");
public override object Add(string key, object entry, System.DateTime utcExpiry)
{
return _cache.AddOrGetExisting(key, entry, UtcDateTimeOffset(utcExpiry));
}
public override object Get(string key)
{
return _cache.Get(key);
}
public override void Remove(string key)
{
_cache.Remove(key);
}
public override void Set(string key, object entry, System.DateTime utcExpiry)
{
_cache.Set(key, entry, UtcDateTimeOffset(utcExpiry));
}
public IEnumerator<KeyValuePair<string,object>> GetEnumerator()
{
return _cache.Enumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _cache.Enumerator();
}
private DateTimeOffset UtcDateTimeOffset(System.DateTime utcExpiry)
{
DateTimeOffset dtOffset = default(DateTimeOffset);
if ((utcExpiry.Kind == DateTimeKind.Unspecified)) {
dtOffset = DateTime.SpecifyKind(utcExpiry, DateTimeKind.Utc);
} else {
dtOffset = utcExpiry;
}
return dtOffset;
}
#region "IDisposable Support"
// To detect redundant calls
private bool disposedValue;
// IDisposable
protected virtual void Dispose(bool disposing)
{
if (!this.disposedValue) {
if (disposing) {
_cache.Dispose();
}
}
this.disposedValue = true;
}
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above.
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}
Configure the custom OutputCacheProvider
<system.web>
<caching>
<outputCache defaultProvider="EnumerableMemoryCache">
<providers>
<add name="EnumerableMemoryCache"
type="Caching.EnumerableMemoryOutputCacheProvider, MyAssemblyName"/>
</providers>
</outputCache>
<outputCacheSettings>
<outputCacheProfiles>
<add name="ContentAllParameters" enabled="false" duration="14400" location="Any" varyByParam="*"/>
</outputCacheProfiles>
</outputCacheSettings>
</caching>
</system.web>
Enumerate over the cache, in this case removing cache items.
OutputCacheProvider provider = OutputCache.Providers[OutputCache.DefaultProviderName];
if (provider == null) return;
IEnumerable<KeyValuePair<string, object>> keyValuePairs = provider as IEnumerable<KeyValuePair<string, object>>;
if (keyValuePairs == null) return;
foreach (var keyValuePair in keyValuePairs)
{
provider.Remove(keyValuePair.Key);
}
I have use this
http://www.codeproject.com/KB/session/exploresessionandcache.aspx
to view the cache and the session data. I only have to say that show only one pools data. If you have more pools, then you just see the one you are on it.
I have this code in my project:
var UI =
{
Layouts:
{
ShowLayoutSettings: function(pid, lid) {
My.PageServices.GetPageLayout(lid, pid, UI.Layouts._onShowLayoutSettings);
},
_onShowLayoutSettings: function(obj) {
alert(obj.ID);
}
}
}
and in my asp.net project a web service named PageServices:
namespace My
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
[System.Web.Script.Services.ScriptService]
public class PageServices : System.Web.Services.WebService
{
public PageServices()
{
}
[WebMethod]
[ScriptMethod(UseHttpGet = false, XmlSerializeString = true)]
[GenerateScriptType(typeof(PageLayout))]
public PageLayout GetPageLayout(string lid, int pid)
{
if (!SystemModel.UserManager.HasLogin())
return null;
var o = SystemModel.Layouts.GetPageLayout(pid);
o.Tag = lid;
return o;
}
}
}
I should mention that my PageLayout class is a linq class and it's serialization mode is Unidirectional.
and finally a anchor link:
Test
My problem is it is correct and sends ajax request to my service when I click this link and my service returns the object as needed, but it does not fire _onShowLayoutSettings as the call back function for this request.
I tested this work when I create a web servive which just returns and String object, and it was all correct, but I don't know why for my PageLayout object, it's not correct.
Please help me.
Thank you.
if it worked with returning a string, then you probably need to tell the ajax extension to create javascript code for the object you're trying to return. Add an attribute above your webmethod
[GenerateScriptType(typeof(PageLayout))]
where PageLayout is the name of the class the GetPageLayout returns
I found the solution, but it's very hard, I wrote a JavaScriptConverter named PageLayoutJavaScriptConverter like this:
public class PageLayoutJavaScriptConverter : JavaScriptConverter
{
public override IEnumerable<Type> SupportedTypes
{
get
{
return new Type[] { typeof(PageLayout) };
}
}
public override object Deserialize(
IDictionary<string, object> dictionary,
Type type,
JavaScriptSerializer serializer)
{
throw new NotImplementedException();
}
public override IDictionary<string, object> Serialize(
object obj,
JavaScriptSerializer serializer)
{
PageLayout e = obj as PageLayout;
Dictionary<string, object> result = new Dictionary<string, object>();
if (e != null)
{
result.Add("ID", e.ID);
}
return result;
}
}
and add this tag to web.config:
<system.web.extensions>
<scripting>
<webServices>
<jsonSerialization>
<converters>
<add name="PageLayoutJavaScriptConverter" type="WebApp.Code.PageLayoutJavaScriptConverter"/>
</converters>
</jsonSerialization>
</webServices>
<scriptResourceHandler enableCaching="true" enableCompression="true"/>
</scripting>
</system.web.extensions>
and all was correct.
I have a question, Isn't any other simpler way to do this work?