Can MVC Views be transformed for multiple deployements like web.config files can? - asp.net-mvc

In one of my MVC projects, I have a special configuration setup for a test deployment site. Doing this, I was able to add a config tranformation to override various settings in my web.config file. For example, I have the following files:
web.config
web.release.config
web.debug.config
web.testsite.config
When I deploy to my test site, it now overwrites some settings specified in my web.testsite.config
Is it possible to get the same behavior on some of my views? For example, could I have a Index.testsite.cshtml? I could toggle behavior on and off with flags from the configuration, however it seems like a cleaner approach would be to allow for additional transformations/replacement views based on configuration.

This is actually easy to do.
*global.asax - Inside Application_Start()*
var displayModes = DisplayModeProvider.Instance.Modes;
displayModes.Insert(0, new DefaultDisplayMode("TestSite")
{
ContextCondition = (context => IsTestSite())
});
Definition of IsTestSite()
public bool IsTestSite()
{
bool isTestSite;
return bool.TryParse(ConfigurationManager.AppSettings["isTestSite"], out isTestSite);
}
That's it, now your app will use Intex.TestSite.cshtml if present otherwise it will serve Index.cshtml. The same holds true for any other view name as well, just stick TestSite before the extension.

Add to your Base/Controller:
protected override void OnResultExecuting(ResultExecutingContext filterContext)
{
var viewResult = filterContext.Result as ViewResult;
if (viewResult != null)
{
string env = ... // determine your environment somehow
var razorEngine = viewResult.ViewEngineCollection.OfType<RazorViewEngine>().Single();
var viewName = !String.IsNullOrEmpty(viewResult.ViewName) ? viewResult.ViewName : filterContext.RouteData.Values["action"].ToString();
var razorView = razorEngine.FindView(filterContext.Controller.ControllerContext, viewName, viewResult.MasterName, false).View as RazorView;
var currentPath = razorView.ViewPath;
var newPath = currentPath.Replace(".cshtml", env + ".cshtml");
if (razorEngine.FileExists(filterContext.Controller.ControllerContext, newPath))
viewResult.View = new RazorView(filterContext.Controller.ControllerContext, newPath, razorView.LayoutPath, razorView.RunViewStartPages, razorView.ViewStartFileExtensions);
}
base.OnResultExecuting(filterContext);
}
Also, if you're using MVC 4 (hence WebPages 2.0), you can use DisplayModeProvider to achieve this easily.
In your Global.asax:
protected void Application_Start()
{
DisplayModeProvider.Instance.Modes.Add(new DefaultDisplayMode("debug")
{
ContextCondition = (context => context.IsDebuggingEnabled)
});
DisplayModeProvider.Instance.Modes.Add(new DefaultDisplayMode("test")
{
ContextCondition = (context => context.Request.IsLocal)
});
}

You might be able to implement a custom action filter that checks your configuration setting and serves up the correct view based on its value.

Related

Error getting declarated Culture from web.config

I have an ASP.NET MVC4 Application. I use ninject for DI and WebActivator to setup the environment.
Inside the Start method, System.Globalization.CultureInfo.CurrentCulture reads correctly from the web.config as "es-DO" which is the declared locale:
public static void Start()
{
DynamicModuleUtility.RegisterModule(typeof(OnePerRequestHttpModule));
DynamicModuleUtility.RegisterModule(typeof(NinjectHttpModule));
Bootstrapper.Initialize(CreateKernel);
}
Inside the PostStart method where I set routes and minification bundles the locale changes to "en-US". Which I assume is the default locale
public static void PostStart()
{
ValidationSettings.UnobtrusiveValidationMode = UnobtrusiveValidationMode.None;
RouteConfig.RegisterRoutes();
GlobalFilterConfig.RegisterFilters();
BundleConfig.RegisterBundles(BundleTable.Bundles);
AuthConfig.RegisterAuth();
}
Does anyone know why this happens? the BundleConfig.RegisterBundles method relies on the Culture information to load the corresponding javascript files.
We fixed it like this:
//Hack to set the culture again to the one defined on the web.config
var config = WebConfigurationManager.OpenWebConfiguration("/");
var section = (GlobalizationSection)config.GetSection("system.web/globalization");
if(section != null)
{
Thread.CurrentThread.CurrentCulture = new CultureInfo(section.Culture);
Thread.CurrentThread.CurrentUICulture = new CultureInfo(section.UICulture);
}

A localized scriptbundle solution

Hi I am currently using the asp.net MVC 4 rc with System.Web.Optimization. Since my site needs to be localized according to the user preference I am working with the jquery.globalize plugin.
I would very much want to subclass the ScriptBundle class and determine what files to bundle according to the System.Threading.Thread.CurrentThread.CurrentUICulture. That would look like this:
bundles.Add(new LocalizedScriptBundle("~/bundles/jqueryglobal")
.Include("~/Scripts/jquery.globalize/globalize.js")
.Include("~/Scripts/jquery.globalize/cultures/globalize.culture.{0}.js",
() => new object[] { Thread.CurrentThread.CurrentUICulture })
));
For example if the ui culture is "en-GB" I would like the following files to be picked up (minified of course and if possible cached aswell until a script file or the currentui culture changes).
"~/Scripts/jquery.globalize/globalize.js"
"~/Scripts/jquery.globalize/globalize-en-GB.js" <-- if this file does not exist on the sever file system so fallback to globalize-en.js.
I tried overloading the Include method with something like the following but this wont work because it is not evaluated on request but on startup of the application.
public class LocalizedScriptBundle : ScriptBundle
{
public LocalizedScriptBundle(string virtualPath)
: base(virtualPath) {
}
public Bundle Include(string virtualPathMask, Func<object[]> getargs) {
string virtualPath = string.Format(virtualPathMask, getargs());
this.Include(virtualPath);
return this;
}
}
Thanks
Constantinos
That is correct, bundles should only be configured pre app start. Otherwise in a multi server scenario, if the request for the bundle is routed to a different server other than the one that served the page, the request for the bundle resource would not be found.
Does that make sense? Basically all of your bundles need to be configured and defined in advance, and not dynamically registered on a per request basis.
take a look: https://stackoverflow.com/questions/18509506/search-and-replace-in-javascript-before-bundling
I coded this way for my needs:
public class MultiLanguageBundler : IBundleTransform
{
public void Process(BundleContext context, BundleResponse bundle)
{
var content = new StringBuilder();
var uicult = Thread.CurrentThread.CurrentUICulture.ToString();
var localizedstrings = GetFileFullPath(uicult);
if (!File.Exists(localizedstrings))
{
localizedstrings = GetFileFullPath(string.Empty);
}
using (var fs = new FileStream(localizedstrings, FileMode.Open, FileAccess.Read))
{
var m_streamReader = new StreamReader(fs);
var str = m_streamReader.ReadToEnd();
content.Append(str);
content.AppendLine();
}
foreach (var file in bundle.Files)
{
var f = file.VirtualFile.Name ?? "";
if (!f.Contains("localizedstrings"))
{
using (var reader = new StreamReader(VirtualPathProvider.OpenFile(file.VirtualFile.VirtualPath)))
{
content.Append(reader.ReadToEnd());
content.AppendLine();
}
}
}
bundle.ContentType = "text/javascript";
bundle.Content = content.ToString();
}
private string GetFileFullPath(string uicult)
{
if (uicult.StartsWith("en"))
uicult = string.Empty;
else if (!string.IsNullOrEmpty(uicult))
uicult = ("." + uicult);
return Kit.ToAbsolutePath(string.Format("~/Scripts/locale/localizedstrings{0}.js", uicult));
}
}

OutputCache behavior in ASP.NET MVC 3

I was just testing Output Caching in the RC build of ASP.NET MVC 3.
Somehow, it is not honoring the VaryByParam property (or rather, I am not sure I understand what is going on):
public ActionResult View(UserViewCommand command) {
Here, UserViewCommand has a property called slug which is used to look up a User from the database.
This is my OutputCache declaration:
[HttpGet, OutputCache(Duration = 2000, VaryByParam = "None")]
However, when I try and hit the Action method using different 'slug' values (by manupulating the URL), instead of serving wrong data (which I am trying to force by design), it is instead invoking the action method.
So for example (in order of invocation)
/user/view/abc -> Invokes action method with slug = abc
/user/view/abc -> Action method not invoked
/user/view/xyz -> Invokes action method again with slug = xyz! Was it not supposed to come out of the cache because VaryByParam = none?
Also, what is the recommended way of OutputCaching in such a situation? (example above)
Just wanted to add this information so that people searching are helped:
The OutputCache behavior has been changed to be 'as expected' in the latest release (ASP.NET MVC 3 RC 2):
http://weblogs.asp.net/scottgu/archive/2010/12/10/announcing-asp-net-mvc-3-release-candidate-2.aspx
Way to go ASP.NET MVC team (and Master Gu)! You all are awesome!
VaryByParam only works when the values of the url look like /user/view?slug=abc. The params must be a QueryString parameter and not part of the url like your above examples. The reason for this is most likely because Caching happens before any url mapping and that mapping isn't included in the cache.
Update
The following code will get you where you want to go. It doesn't take into account stuff like Authorized filters or anything but it will cache based on controller/action/ids but if you set ignore="slug" it will ignore that particular attribute
public class ActionOutputCacheAttribute : ActionFilterAttribute {
public ActionOutputCacheAttribute(int cacheDuration, string ignore) {
this.cacheDuration = cacheDuration;
this.ignore = ignore;
}
private int cacheDuration;
private string cacheKey;
private string ignore;
public override void OnActionExecuting(ActionExecutingContext filterContext) {
string url = filterContext.HttpContext.Request.Url.PathAndQuery;
this.cacheKey = ComputeCacheKey(filterContext);
if (filterContext.HttpContext.Cache[this.cacheKey] != null) {
//Setting the result prevents the action itself to be executed
filterContext.Result =
(ActionResult)filterContext.HttpContext.Cache[this.cacheKey];
}
base.OnActionExecuting(filterContext);
}
public override void OnActionExecuted(ActionExecutedContext filterContext) {
//Add the ActionResult to cache
filterContext.HttpContext.Cache.Add(this.cacheKey, filterContext.Result,null, DateTime.Now.AddSeconds(cacheDuration),
System.Web.Caching.Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
//Add a value in order to know the last time it was cached.
filterContext.Controller.ViewData["CachedStamp"] = DateTime.Now;
base.OnActionExecuted(filterContext);
}
private string ComputeCacheKey(ActionExecutingContext filterContext) {
var keyBuilder = new StringBuilder();
keyBuilder.Append(filterContext.ActionDescriptor.ControllerDescriptor.ControllerName);
keyBuilder.Append(filterContext.ActionDescriptor.ActionName);
foreach (var pair in filterContext.RouteData.Values) {
if (pair.Key != ignore)
keyBuilder.AppendFormat("rd{0}_{1}_", pair.Key.GetHashCode(), pair.Value.GetHashCode());
}
return keyBuilder.ToString();
}
}

How test that ASP.NET MVC route redirects to other site?

Due to a prinitng error in some promotional material I have a site that is receiving a lot of requests which should be for one site arriving at another.
i.e.
The valid sites are http://site1.com/abc & http://site2.com/def but people are being told to go to http://site1.com/def.
I have control over site1 but not site2.
site1 contains logic for checking that the first part of the route is valid in an actionfilter, like this:
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
if ((!filterContext.ActionParameters.ContainsKey("id"))
|| (!manager.WhiteLabelExists(filterContext.ActionParameters["id"].ToString())))
{
if (filterContext.ActionParameters["id"].ToString().ToLowerInvariant().Equals("def"))
{
filterContext.HttpContext.Response.Redirect("http://site2.com/def", true);
}
filterContext.Result = new ViewResult { ViewName = "NoWhiteLabel" };
filterContext.HttpContext.Response.Clear();
}
}
I'm not sure how to test the redirection to the other site though.
I already have tests for redirecting to "NoWhiteLabel" using the MvcContrib Test Helpers, but these aren't able to handle (as far as I can see) this situation.
How do I test the redirection to antoher site?
I would recommend you using RedirectResult instead of calling Response.Redirect:
if (youWantToRedirect)
{
filterContext.Result = new RedirectResult("http://site2.com/def")
}
else
{
filterContext.Result = new ViewResult { ViewName = "NoWhiteLabel" };
}
Now if you know how to test ViewResult with MVCContrib TestHelper you will be able to test the RedirectResult the same way. The tricky part is mocking the manager to force it to satisfy the if condition.
UPDATE:
Here's how a sample test might look like:
// arrange
var mock = new MockRepository();
var controller = mock.StrictMock<Controller>();
new TestControllerBuilder().InitializeController(controller);
var sut = new MyFilter();
var aec = new ActionExecutingContext(
controller.ControllerContext,
mock.StrictMock<ActionDescriptor>(),
new Dictionary<string, object>());
// act
sut.OnActionExecuting(aec);
// assert
aec.Result.ShouldBe<RedirectResult>("");
var result = (RedirectResult)aec.Result;
result.Url.ShouldEqual("http://site2.com/def", "");
Update (By Matt Lacey)
Here's how I actually got this working:
// arrange
var mock = new MockRepository();
// Note that in the next line I create an actual instance of my real controller - couldn't get a mock to work correctly
var controller = new HomeController(new Stubs.BlankContextInfoProvider(), new Stubs.BlankWhiteLabelManager());
new TestControllerBuilder().InitializeController(controller);
var sut = new UseBrandedViewModelAttribute(new Stubs.BlankWhiteLabelManager());
var aec = new ActionExecutingContext(
controller.ControllerContext,
mock.StrictMock<ActionDescriptor>(),
// being sure to specify the necessary action parameters
new Dictionary<string, object> { { "id", "def" } });
// act
sut.OnActionExecuting(aec);
// assert
aec.Result.ShouldBe<RedirectResult>("");
var result = (RedirectResult)aec.Result;
result.Url.ShouldEqual("http://site2.com/def", "");

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