MVC Routes based on POST parameters - model-view-controller

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. :)

Related

How do you 'jump' out of a MVC ViewComponent to another controller?

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.

VS Express MVC3 newbie redirect issue

I'm brand new to MVC (having done classic ASP for many years). I'm not sure I know how to ask this question. Basically, I want the actions of one controller to seamlessly transfer/redirect to another view/controller. I have tried
public class SetupController : Controller
{
...
public ActionResult Bicycles()
{
return RedirectToAction("Index", "Bicycles");
}
}
but the problem is that this takes me to localhost/Bicycles (which doesn't exist). What I want is to go to localhost/Setup/Bicycles. I tried this (adding "Setup" parent folder to controller name):
public class SetupController : Controller
{
...
public ActionResult Bicycles()
{
return RedirectToAction("Index", "Setup/Bicycles");
}
}
but this created an infinite redirect loop, which the browser rightly refused to do.
Hope it makes sense what I'm trying to do.
I believe what you are looking for is:
public ActionResult Bicycles()
{
return RedirectToAction("Bicycles", "Setup");
}
The first parameter is the Action, the second the Controller.
Since you already are in SetupController in Bicycles action, you would get an infinite redirect. However, from what you mentioned, that is where you are attempting to redirected to.
protected internal RedirectToRouteResult RedirectToAction(
string actionName,
string controllerName
)
So your first example redirects to Index action in Bicycles controller, hence the localhost/Bicycles.

Asp.net mvc 3- get the current controller instance (not just name)

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.

Weird MVC Issue

I have this code and I can't understand why it works this way
I have a model and view which is arbitrary and a very simple (but weird) controller
Here is my controller:
public partial class RouteController : Controller
{
[HttpGet]
public virtual ActionResult Create()
{
Create create = new Create();
return View("Create", create);
}
[HttpPost]
public virtual ActionResult Create(Create route)
{
return Create();
}
}
The first create method loads the view as normal. When the view posts back it runs the 2nd action which runs the first (as expected). The wierd part is the view is (re-)loaded with my previously entered data with errors (if any). I dont understand this because my model is empty. I was expecting it to post back with the same form as if it was loaded for the first time but with errors possibly.
Please explain.
That's the normal behavior of HTML helpers and it is by design. They first look at values contained in the ModelState and after that in the actual model. If you intend to modify some values on the model in a POST action you need to remove them from modelstate first:
For example:
[HttpPost]
public virtual ActionResult Create(Create route)
{
ModelState.Remove("SomeProperty");
route.SomeProperty = "some new value";
return View(route);
}
If you intend to completely modify everything as in your example you could clear the modelstate entirely:
[HttpPost]
public virtual ActionResult Create(Create route)
{
ModelState.Clear();
return Create();
}
Another possibility is to write your own TextBoxFor, HiddenFor, CheckBoxFor, ... helpers that will use the value in the model and not the one in the model state. Or yet another (non-recommended) possibility:
<input type="text" name="SomeProperty" value="#Model.SomeProperty" />
Of course in this case client validation among other things provided by the standard helpers won't work.

MVC3 Routing Issues - How to re-use a View for all Controller Methods?

I'm trying to implement a common controller in MVC3 to return various JSON feeds, example -
public class AjaxController : Controller
{
public ActionResult Feed1()
{
ViewBag.Json = LogicFacade.GetFeed1Json();
return View();
}
public ActionResult Feed2()
{
ViewBag.Json = LogicFacade.GetFeed2Json();
return View();
}
}
This class has 30+ methods in it, the problem is this requires implementing an IDENTICAL View for each of the Controller's methods (sigh) that writes out ViewBag.Json.
I'm assuming this is a routing issue but I'm struggling with that. The following didn't work -
Tried setting ViewBag.Json then using RedirectToAction() but that seems to reset ViewBag.Json.
Note JsonResult is not appropriate for my needs, I'm using a different JSON serialiser.
So the objective here is to maintain one View file but keep this class with seperate methods that are called by routing, and not a crappy switch statement implementation.
Any help appreciated.
Use the same view and just specify the name. You can store in the controller's view folder, if only used by one controller, or in the Shared view folder if used by more than one.
return View("SharedJsonView");
Another, perhaps better, solution would be to create your own result -- maybe deriving from JsonResult, maybe directly from ActionResult -- that creates the JSON response that you need. Look at the source code for JsonResult on http://www.codeplex.com/aspnet for ideas on how to do it.

Resources