Manually parse urls in ASP.NET-MVC-3 - asp.net-mvc-3

I have a Controller with syntax like this:
public class CrudController<TEntity> : Controller
Now if I need a CrudController for Entity User, I just need to extend the CrudController like this
UserCrudController : CrudController<User>
Its working just fine. But, the thing is, the UserCrudController is simply empty. Also, there are some other CrudControllers which are empty too.
Now, I am looking for a way to avoid writing the empty crud controllers. I simply want to create Instances of CrudController with appropriate generic argument. Perhaps by a strict naming convention as described bellow.
The URL will be like: #Html.ActionLink("Create", "UserCrud")
When the URL will be received, it will try to locate the controller named UserCrud (The default thing)
If it fails to locate UserCrud, Crud<User> will be created.
Now, I can do the things I want to do. But exactly where do I do these? Where is the url parsed in mvc?

With help of Craig Stuntz's comment on the question and this question and its accepted answer, I have solved my problem.
I have implemented a custom CotrollerFactory
public class CrudControllerFactory : DefaultControllerFactory {
protected override Type GetControllerType(System.Web.Routing.RequestContext requestContext, string controllerName) {
Type controllerType = base.GetControllerType(requestContext, controllerName);
if(controllerType == null) {
int indexOfEntityEnd = controllerName.LastIndexOf("Crud");
if(indexOfEntityEnd >= 0) {
string entityName = controllerName.Substring(0, controllerName.Length - indexOfEntityEnd - 1);
// Get type of the CrudController and set to controller tye
}
}
return controllerType;
}
}
And then in Application_Start(), I added this line:
ControllerBuilder.Current.SetControllerFactory(typeof(CrudControllerFactory));

Related

How to assign multiple MVC routes to one method and determine the used route?

I have the following code:
public IActionResult Up(int id)
{
return Move(id, "up");
}
private IActionResult Move(int id, string action)
{
// Do something based on the action
switch (action)
{
case "up":
break;
case "down":
break;
}
}
This works fine and is easy to maintain. But now I've thought that I could refactor the code by combining the methods:
[Route("item/up/{id}")]
[Route("item/down/{id}")]
public IActionResult Move([FromRoute]string action, [FromRoute]int id)
{
}
The link /item/up/155 will hit this method while the link /item/up2/155 returns NotFound, as expected.
The problem now is, that when I check the value of action, then it does not contain the value up, but the value Move.
This seems odd as the controller name is not transformed into ItemController. So one could argue that the value should be up. But assuming this is the design and won't change, then how can I find the actual value?
I can parse Request.Path.Value (="/item/up/155"), but I'd prefer not to as I would like to keep things simple.
So I am looking for an easy way to assign multiple routes to one method and then determine what the value of the action parameter in the used route path was.
Instead of hard coding the direction as 'up' and 'down' in the route declaration, you can treat that as a parameter. Since you used the keyword 'action' as a parameter, during the parameter binding the actual action method selected by the framework will be assigned to it.
You can try the following code -
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
[Route("item/{direction}/{id}")]
public string Move([FromRoute]string controller, [FromRoute]string action, string direction, string id)
{
return string.Format("Controller={0} Action={1} Direction={2} Id={3}", controller, action, direction, id);
}
}

Can't get log4net working in MVC3 sub-classed controller

I have an MVC3 application that I'd like to get log4net working in.
I got the code I'm using from this site
I can get the logging to work if I add this code to an Action method.
public ActionResult Index() {
log4net.ILog log = log4net.LogManager.GetLogger(this.GetType());
log.Info("Here I am in Index.");
return View();
}
I'd like to enable logging in all my controllers, so I added the following class to my project,
public class LoggingController :Controller {
protected log4net.ILog Log;
public LoggingController () {
Log = log4net.LogManager.GetLogger(GetType());
}
}
and I'm having my Home controller inherit from this class, LoggingController.
I've put a break point on my LoggingController's constructor and determined the constructor is being called, however my logging isn't working when done this way.
My question then is:
Why isn't this working?
OR
Is there a better way of accomplishing what I'm trying to do here?
Thanks in advance.
Why isn't this working?
Because the type argument you pass to the GetLogger method must match that of the containing type. You are passing HomeController (by using GetType()) instead of LoggingController which is where the variable is declared. Try like this:
public LoggingController () {
Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
}

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.

Get current controller in view

I have a View - _Edit which lives in News M/V/C.
I reuse the V/M via the CategoryController as:
return PartialView("/Views/News/_Edit.cshtml", model);
How from within the View - _Edit can I alert the controller name?
When I:
alert('#ViewContext. RouteData.Values["controller"].ToString()');
The Value is: News
However, the URL is: /Category/foobar
Is there a way to get the value 'Category' to alert? thanks
I have put this in my partial view:
#HttpContext.Current.Request.RequestContext.RouteData.Values["controller"].ToString()
in the same kind of situation you describe, and it shows the controller described in the URL (Category for you, Product for me), instead of the actual location of the partial view.
So use this alert instead:
alert('#HttpContext.Current.Request.RequestContext.RouteData.Values["controller"].ToString()');
I do it like this:
#ViewContext.RouteData.Values["controller"]
Create base class for all controllers and put here name attribute:
public abstract class MyBaseController : Controller
{
public abstract string Name { get; }
}
In view
#{
var controller = ViewContext.Controller as MyBaseController;
if (controller != null)
{
#controller.Name
}
}
Controller example
public class SampleController: MyBaseController
{
public override string Name { get { return "Sample"; }
}
Other way to get current Controller name in View
#ViewContext.Controller.ValueProvider.GetValue("controller").RawValue
Just use:
ViewContext.Controller.GetType().Name
This will give you the whole Controller's Name
You are still in the context of your CategoryController even though you're loading a PartialView from your Views/News folder.
You can use any of the below code to get the controller name
#HttpContext.Current.Request.RequestContext.RouteData.Values["controller"].ToString();
If you are using MVC 3 you can use
#ViewContext.Controller.ValueProvider.GetValue("controller").RawValue
For anyone looking for this nowadays (latest versions) of ASP.NET Core MVC, you can use:
#Context.Request.RouteValues["controller"].ToString()

MVC Routes based on POST parameters

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

Resources