I am implementing a new ASP.NET MVC 3 application that will use a form of dynamic routing to determine what view to return from a common controller action. I'd like have a default view that is displayed if there is no view at the dynamic location.
Think of it like navigating a tree structure. There is only one TreeController located in the root Controllers folder. It has a Browse action method that accepts the path of the node to browse. Each node can have a custom view so I need to first attempt to locate that view and return it from the action method, like this:
public ViewResult Browse(String path)
{
var model = ...;
return View(path, model);
}
So, if I navigate to "MySite/Tree/A/B/C" then I would expect to find a view at "\Views\Tree\A\B\C.aspx".
However, if there is not a custom view, I need to defer to a standard/default view (such as "\Views\Tree\Browse.aspx").
Since this is only the case for this action method, I don't believe that I should be handling NotFound errors that may result due to other circumstances. And, I'm not looking for dynamic routing as described in other posts because the path to the controller is fixed.
Controllers shouldn't know about physical views.
You do this by writing a custom view engine, e.g.:
public class MyViewEngine: WebFormViewEngine
{
public MyViewEngine()
{
ViewLocationFormats = ViewLocationFormats.Concat(
new [] {"~/Views/{1}/Browse.aspx""}).ToArray();
// similarly for AreaViewLocationFormats, etc., if needed
}
}
See the source code for, e.g., WebFormViewEngine for details.
If you need to do this conditionally (for only a few action) then you can override FindView in that type and look at the route values.
Obviously, if you use Razor, then change that one instead.
Then, in Global.asax.cs, use it:
private void Application_Start(object sender, EventArgs e)
{
// stuff
ViewEngines.Engines.Add(new MyViewEngine());
From within a Controller action this seems to work:
var fullPath = string.Format("~/Views/CustomStuff/{0}.cshtml", viewname);
var mappedPath = Server.MapPath(fullPath);
if( !System.IO.File.Exists(mappedPath) ) return View("Default");
else return View(viewname);
(note: not precompiling views)
Related
I have a simple scenario where if a cart is empty, I'd like to redirect to another 'page'(controller) which states the cart is empty or just send them back to the shop.
Heres my code:
public async Task<IViewComponentResult> InvokeAsync()
{
CartFunctions cartf = new CartFunctions(_logger, AppSettings, _httpContextAccessor);
Cart c = new Cart();
c = cartf.GetShopingCart();
if (c.CartItems == null)
{
// How do I get out of here to a differnet Controller
}
return View(c.CartItems);
}
If it was a controller I could return RedirectToAction
but that is not available here.
I think the main problem is i need to either get out OR return a Cartitems and I can't find a way to do both.
In the good ole days it was simple with response.redirect("Empty.aspx") but now that everything is 'easier' in MVC, it takes days of research to do the simplest things.
A view component does not sound like the ideal option do this redirect. View components are ideal for rendering some partial views. For example, rendering your cart item count or content, using the view component is a good idea.
In your case, you want to redirect to another action method when the cart is empty. You may create an action filter to do that. You can apply it on action method level or controller level as needed.
public class CheckCartValues : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (yourIfConditionToCheckCartIsEmpty)
{
context.Result =
new RedirectToRouteResult(new RouteValueDictionary(new {
controller = "Shop", action = "index" }));
}
base.OnActionExecuting(context);
}
}
You can apply it on the controller level
[CheckCartValues]
public class HomeController : Controller
{
}
Make sure you do not have it on the ShopController or you will get infinite redirects. You can also update the action filter code to not do the redirect when the current request is for the ShopController if needed. I will leave it up to you :)
If you want to use an attribute you can derive from ActionMethodSelectorAttribute https://msdn.microsoft.com/en-us/library/system.web.mvc.actionmethodselectorattribute.isvalidforrequest(v=vs.118).aspx#M:System.Web.Mvc.ActionMethodSelectorAttribute.IsValidForRequest(System.Web.Mvc.ControllerContext,System.Reflection.MethodInfo).
For example you could create an attribute named CartStatus(bool isEmpty) and apply the attribute to the method(s) that need to behave differently based on cart status. Then you have your conditional logic in exactly one place (this attribute) and you can reuse it across your application. Similar to #shyju's approach but instead of redirect you just return true/false from this method for the action that is appropriate.
I have a very typical situation in any application, where i have the following functionality:
create new record
edit existing record
so other irrelevant actions
IMO, creating and editing should be served by the same view, but different actions. But it appears that I have to have the action name match the view name....would you use partial views for this? I would rather not complicate this scenario - which is very simple and appears in virtually every web app.
Action can return a view with a diferent name this way:
public ActionResult OneName()
{
return View("OtherName");
}
If you don't specify the view name (View("") then the view will be the view with the action name
Partial views are an excellent answer. I'd suggest you look at how the MvcScaffold NuGet package does it. See here or get the package in Visual Studio.
I'd simply use the same action altogether and use the ID to determine if this is a new record or updating an existing one:
/Forum/Post/Edit/0 create a new record
/Forum/Post/Edit/10457 update a record with ID 10457
However, since you insist on using different actions, why not simply create 2 actions, both returning the same view?
public class PostController : Controller
{
public ActionResult Create(Post post)
{
// work your magic...
return View("Edit", post);
}
public ActionResult Update(Post post)
{
// work your magic...
return View("Edit", post);
}
}
If this doesn't work in your scenario, you're pretty much left with partial views.
I know how to get the current controller name
HttpContext.Current.Request.RequestContext.RouteData.Values["controller"].ToString();
But is there any way to get the current controller instance in some class (not in an action and not in a view)?
By default you can only access the current Controller inside a controller with ControllerContext.Controller or inside a view with ViewContext.Context. To access it from some class you need to implement a custom ControllerFactory which stores the controller instance somewhere and retrieve it from there. E.g in the Request.Items:
public class MyControllerFactory : DefaultControllerFactory
{
public override IController CreateController(RequestContext requestContext, string controllerName)
{
var controller = base.CreateController(requestContext, controllerName);
HttpContext.Current.Items["controllerInstance"] = controller;
return controller;
}
}
Then you register it in your Application_Start:
ControllerBuilder.Current.SetControllerFactory(new MyControllerFactory());
And you can get the controller instance later:
public class SomeClass
{
public SomeClass()
{
var controller = (IController)HttpContext.Current.Items["controllerInstance"];
}
}
But I would find some another way to pass the controller instance to my class instead of this "hacky" workaround.
Someone will have to correct me if what I am doing is detrimental to the whole Asp.Net page life cycle / whatever but surely you can do this:
In controller
ViewBag.CurrentController = this;
In view
var c = ViewBag.CurrentController;
var m1 = BaseController.RenderViewToString(c, "~/Views/Test/_Partial.cshtml", null);
In my case, I had a base controller that all controllers extend. In that base controller lived a static method called RenderViewToString and it required a controller. Since I figured I could just instantiate a new instance of an empty controller at this point for c, I just sent it to the view in the lovely ViewBag container that exists in the world of Asp.Net MVC. For reasons I could not go into now, I could not retrieve the string in the controller and send just that back to the view (this was what I had done earlier before requirements changed).
The reason I have done it this way is in other languages like PHP and JS, there are similar simple ways to transfer classes around.
I did my research but haven't found any answers.
I'm using Html.RenderAction in a masterpage ( to render page header with links specific to user permissions ). Action is decorated with OutputCache, returns partial control and gets cached as expected.
When the event happens ( let's say permissions are changed ) I want to programmatically invalidate cached partial control.
I'm trying to use RemoveOutputCacheItem method. It takes a path as a parameter. I'm trying to set the path to the action used in Html.RenderAction. That doesn't invalidate the action.
How can I programmatically invalidate the action?
Thanks
The cache for child actions is stored in the OutputCacheAttribute.ChildActionCache property. The problem is that the API generating ids for child actions and storing them in this object is not public (WHY Microsoft??). So if you try to loop through the objects in this collection you will discover that it will also contain the cached value for your child action but you won't be able to identify it unless you reverse engineer the algorithm being used to generate keys which looks something like this (as seen with Reflector):
internal string GetChildActionUniqueId(ActionExecutingContext filterContext)
{
StringBuilder builder = new StringBuilder();
builder.Append("_MvcChildActionCache_");
builder.Append(filterContext.ActionDescriptor.UniqueId);
builder.Append(DescriptorUtil.CreateUniqueId(new object[] { this.VaryByCustom }));
if (!string.IsNullOrEmpty(this.VaryByCustom))
{
string varyByCustomString = filterContext.HttpContext.ApplicationInstance.GetVaryByCustomString(HttpContext.Current, this.VaryByCustom);
builder.Append(varyByCustomString);
}
builder.Append(GetUniqueIdFromActionParameters(filterContext, SplitVaryByParam(this.VaryByParam)));
using (SHA256 sha = SHA256.Create())
{
return Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(builder.ToString())));
}
}
So you could perform the following madness:
public ActionResult Invalidate()
{
OutputCacheAttribute.ChildActionCache = new MemoryCache("NewDefault");
return View();
}
which obviously will invalidate all cached child actions which might not be what you are looking for but I am afraid is the only way other than of course reverse engineering the key generation :-).
#Microsoft, please, I am begging you for ASP.NET MVC 4.0:
introduce the possibility to do donut caching in addition to donut hole caching
introduce the possibility to easily expire the result of a cached controller action (something more MVCish than Response.RemoveOutputCacheItem)
introduce the possibility to easily expire the result of a cached child action
if you do 1. then obviously introduce the possibility to expire the cached donut portion.
You might want to approach this a different way. You could create a custom AuthorizeAttribute -- it would simply allow everyone -- and add override the OnCacheValidation method to incorporate your logic. If the base OnCacheValidation returns HttpValidationStatus.Valid, then make your check to see if the state has changed and if so, return HttpValidationStatus.Invalid instead.
public class PermissionsChangeValidationAttribute : AuthorizeAttribute
{
public override OnAuthorization( AuthorizationContext filterContext )
{
base.OnAuthorization( filterContext );
}
public override HttpValidationStatus OnCacheAuthorization( HttpContextBase httpContext )
{
var status = base.OnCacheAuthorization( httpContext );
if (status == HttpValidationStatus.Valid)
{
... check if the permissions have changed somehow
if (changed)
{
status = HttpValidationStatus.Invalid;
}
}
return status;
}
}
Note that there are ways to pass additional data in the cache validation process if you need to track the previous state, but you'd have to replicate some code in the base class and add your own custom cache validation handler. You can mine some ideas on how to do this from my blog post on creating a custom authorize attribute: http://farm-fresh-code.blogspot.com/2011/03/revisiting-custom-authorization-in.html
We have an a PHP application that we are converting to MVC. The goal is to have the application remain identical in terms of URLs and HTML (SEO and the like + PHP site is still being worked on). We have a booking process made of 3 views and in the current PHP site, all these view post back to the same URL, sending a hidden field to differentiate which page/step in the booking process is being sent back (data between pages is stored in state as the query is built up).
To replicate this in MVC, we could have a single action method that all 3 pages post to, with a single binder that only populates a portion of the model depending on which page it was posted from, and the controller looks at the model and decides what stage is next in the booking process. Or if this is possible (and this is my question), set up a route that can read the POST parameters and based on the values of the POST parameters, route to a differen action method.
As far as i understand there is no support for this in MVC routing as it stands (but i would love to be wrong on this), so where would i need to look at extending MVC in order to support this? (i think multiple action methods is cleaner somehow).
Your help would be much appreciated.
I have come upon two solutions, one devised by someone I work with and then another more elegant solution by me!
The first solution was to specify a class that extends MVcRouteHandler for the specified route. This route handler could examine the route in Form of the HttpContext, read the Form data and then update the RouteData in the RequestContext.
MapRoute(routes,
"Book",
"{locale}/book",
new { controller = "Reservation", action = "Index" }).RouteHandler = new ReservationRouteHandler();
The ReservationRouteHandler looks like this:
public class ReservationRouteHandler: MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var request = requestContext.HttpContext.Request;
// First attempt to match one of the posted tab types
var action = ReservationNavigationHandler.GetActionFromPostData(request);
requestContext.RouteData.Values["action"] = action.ActionName;
requestContext.RouteData.Values["viewStage"] = action.ViewStage;
return base.GetHttpHandler(requestContext);
}
The NavigationHandler actually does the job of looking in the form data but you get the idea.
This solution works, however, it feels a bit clunky and from looking at the controller class you would never know this was happening and wouldn't realise why en-gb/book would point to different methods, not to mention that this doesn't really feel that reusable.
A better solution is to have overloaded methods on the controller i.e. they are all called book in this case and then define your own custome ActionMethodSelectorAttribute. This is what the HttpPost Attribute derives from.
public class FormPostFilterAttribute : ActionMethodSelectorAttribute
{
private readonly string _elementId;
private readonly string _requiredValue;
public FormPostFilterAttribute(string elementId, string requiredValue)
{
_elementId = elementId;
_requiredValue = requiredValue;
}
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
if (string.IsNullOrEmpty(controllerContext.HttpContext.Request.Form[_elementId]))
{
return false;
}
if (controllerContext.HttpContext.Request.Form[_elementId] != _requiredValue)
{
return false;
}
return true;
}
}
MVC calls this class when it tries to resolve the correct action method on a controller given a URL. We then declare the action methods as follows:
public ActionResult Book(HotelSummaryPostData hotelSummary)
{
return View("CustomerDetails");
}
[FormFieldFilter("stepID", "1")]
public ActionResult Book(YourDetailsPostData yourDetails, RequestedViewPostData requestedView)
{
return View(requestedView.RequestedView);
}
[FormFieldFilter("stepID", "2")]
public ActionResult Book(RoomDetailsPostData roomDetails, RequestedViewPostData requestedView)
{
return View(requestedView.RequestedView);
}
[HttpGet]
public ActionResult Book()
{
return View();
}
We have to define the hidden field stepID on the different pages so that when the forms on these pages post back to the common URL the SelectorAttributes correctly determines which action method to invoke. I was suprised that it correctly selects an action method when an identically named method exists with not attribute set, but also glad.
I haven't looked into whether you can stack these method selectors, i imagine that you can though which would make this a pretty damn cool feature in MVC.
I hope this answer is of some use to somebody other than me. :)