Nested Html.Action calls in Razor - c#

EDIT: Obviously this is a vastly simplified version of my site and if I make a test app, with this pattern it works fine. In our real app, we are using T4MVC and this is all within an area. I'm guessing one of these factors is causing my issue...
EDIT2: All the default routes are defined and if I navigate directly to /AreaName/ControllerName/SubChild?val=123 it renders.
I have a peculiar problem with Mvc and am hoping someone can help...
I have a controller with the following action methods
public ActionResult Index()
{
return View(GetModel());
}
public ActionResult Result Child(string blah)
{
return View(GetModel(blah));
}
public ActionResult Result SubChild(int val)
{
return View(GetModel(val));
}
I then have 3 razor views.
Index.cshtml
<div>
#Html.Action("Child", new { blah = "raaa"})
</div>
Child.cshtml
<div>
#*ERROR HERE*#
#Html.Action("SubChild", new { val = 123})
</div>
SubChild.cshtml
<h1>#Model.val</h1>
When I navigate to / I get an exception thrown saying that
"No route in the route table matches the supplied values." on the Html.Action calling the SubChild Action.
This is all within the same area and the same controller. If I change the markup and use Html.Partial for the call to the Child view (and construct the model and pass it in the view), it renders fine. The issue comes when I call Html.Action within a view that's already being rendered using Html.Action.
I've tried fully qualifying the action using
/area/controller/action, specifying the controller in the Html.Action call, passing the area as a parameter in the route values and combinations of all of these.
Does anyone have any ideas what this might be? I'm assuming that you can call Html.Action in Views that are being rendered using it, I guess I might be wrong...

Well, out of the box MVC 3 has the default route parameter named id. Your SubChild action has a parameter named val, so that is probably the issue.
Either rename the parameter in the Action to id, or add a new route
routes.MapRoute(
"SubChild",
"{controller}/SubChild/{val}",
new
{
controller = "ControllerName",
action = "SubChild",
val = UrlParameter.Optional
}
);

Are your parameters really named blahand val? Because normally the first parameter is always called id. Check the method RegisterRoutes(RouteCollection routes) in your global.asax.cs. There must be something like
routes.MapRoute("Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }); // Parameter defaults
That indicates how your parameters have to be named.
I think your Actions have to be like this:
public ActionResult Index()
{
return View(GetModel());
}
public ActionResult Result Child(string id)
{
return View(GetModel(id));
}
public ActionResult Result SubChild(int id)
{
return View(GetModel(id));
}
Then the code in your views has to be:
Index.cshtml
<div>
#Html.Action("Child", new { id = "raaa"})
</div>
Child.cshtml
<div>
#Html.Action("SubChild", new { id = 123})
</div>

It appears the problem is to do with our areas and routing setup.
On the 2nd pass, we are losing the reference to the area in the routevaluedictionary and as such it can't find the correct route. Where we are registering the area, we need to register the correct route.
Thanks for the help with this, I've upvoted the other answers as I think they may help someone else in the future.

Related

MVC Core : Default Index action not triggered when surfing to "Assets" without specifying "Index"

I have two URLs, both should resolve to the same page. One doesn't and returns 404
http://localhost:xxx/Assets = 404 not found
http://localhost:xxx/Assets/Index = works as intended
Here's my index action:
[HttpGet]
public IActionResult Index()
{
var vm = new AssetsIndexModel()
{
/* snipped, not important */
};
return View(vm);
}
and this is my only route in Startup.cs:
app.UseMvc(routes =>
{
routes.MapRoute("default", "{Controller}/{Action}/{id?}", defaults: new { Controller = "Home", Action = "Index" });
});
This is the only action with this problem. All other actions in the same controller resolve properly. Also, my Home/Index route DOES work if I just surf to "/Home". I can omit "/Index" there, but not in my Assets controller.
Does anyone have any ideas what I may be overlooking?
EDIT:
Also, I create the link to the Index action using the following code
<a asp-controller="Assets" asp-action="Index">Manage Assets</a>
This produces "/Assets", so MVC seems to understand that it can omit "Index", but then that link just doesn't work and returns 404.
I have found the answer after finding another problem (that at first seemed unrelated).
In my HomeController, an action was added like this:
[HttpGet("{assetId}")]
public IActionResult AssetDetail(Guid assetId)
{
return View();
}
This was the problem. I suppose any action anywhere was resolving to this action. Changing the route above to this solved it:
[HttpGet("Detail/{assetId}")]

Redirect to Area without changing the URL and TempData

In my project I have to redirect from one controller to another controller which is present inside the Areas named SIP. If use the following method the redirection works successfully and also the TempData value is passed to the other controller:
TempData["sipModel"] = 1;
return RedirectToAction("Index", "Home", new { area = "SIP" });
But in this case the URL gets changed while my requirement is to keep the same URL, to achieve that I went though other answers and used the method TransferToAction() mentioned
in this answer
This works perfectly and I'm able to redirect to the other area without changing the URL with the following code:
TempData["sipModel"] = 1;
return this.TransferToAction("Index", "Home", new { area = "SIP"});
However, in this case the TempData value is not retained and I get Null Reference Exception while trying to read the same.
I tried to use the following code mentioned in the other answer:
public static TransferToRouteResult TransferToAction(this System.Web.Mvc.Controller controller, string actionName, string controllerName, object routeValues)
{
controller.TempData.Keep();
controller.TempData.Save(controller.ControllerContext, controller.TempDataProvider);
return new TransferToRouteResult(controller.Request.RequestContext, actionName, controllerName, routeValues);
}
But this doesn't work out. Can somebody please suggest me how can I fix this or any other better approach to achieve this result. Thanks.
Edited:
The URL is like:
https://myproject/Home/Index?cid=ABC-1234&pid=xyz123456abc
I have a complex data in a class which also needs to be passed from the one controller to the other (which is present in the Area SIP), for that I've been using TempData, I've used an integer here just as a sample.
In the first controller method I've if-else condition, so:
if (companyCode = 'X')
return View();
else
TempData["sipModel"] = 1;
return RedirectToAction("Index", "Home", new { area = "SIP" }); OR (this.TransferToAction("Index", "Home", new { area = "SIP"});)
Server.TransferRequest is completely unnecessary in MVC. This is an antiquated feature that was only necessary in ASP.NET because the request came directly to a page and there needed to be a way to transfer a request to another page. Modern versions of ASP.NET (including MVC) have a routing infrastructure that can be customized to route directly to the resource that is desired. There is no point of letting the request reach a controller only to transfer it to another controller when you can simply make the request go directly to the controller and action you want.
So, given your example is not a complete set of requirements I will make the following assumptions. Adjust these as necessary for your requirements.
If there are no query string parameters passed to the home page, it will stay on the home page.
If there is a query parameter cid or pid on the home page, we will send the request to the Index action of the HomeController of the SID area.
We will pass a metadata parameter "sipModel" with value 1 in the latter case and omit the parameter in the first case.
First of all, we subclass RouteBase and put our custom logic there. A more complete scenario might have dependent services and options passed in through the constructor, and even have its own MapRoute extension methods to wire it together.
public class CustomHomePageRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
// Only handle the home page route
if (httpContext.Request.Path == "/")
{
var cid = httpContext.Request.QueryString["cid"];
var pid = httpContext.Request.QueryString["pid"];
result = new RouteData(this, new MvcRouteHandler());
if (string.IsNullOrEmpty(cid) && string.IsNullOrEmpty(pid))
{
// Go to the HomeController.Index action of the non-area
result.Values["controller"] = "Home";
result.Values["action"] = "Index";
// NOTE: Since the controller names are ambiguous between the non-area
// and area route, this extra namespace info is required to disambiguate them.
// This is not necessary if the controller names differ.
result.DataTokens["Namespaces"] = new string[] { "WebApplication23.Controllers" };
}
else
{
// Go to the HomeController.Index action of the SID area
result.Values["controller"] = "Home";
result.Values["action"] = "Index";
// This tells MVC to change areas to SID
result.DataTokens["area"] = "SID";
// Set additional data for sipModel.
// This can be read from the HomeController.Index action by
// adding a parameter "int sipModel".
result.Values["sipModel"] = 1;
// NOTE: Since the controller names are ambiguous between the non-area
// and area route, this extra namespace info is required to disambiguate them.
// This is not necessary if the controller names differ.
result.DataTokens["Namespaces"] = new string[] { "WebApplication23.Areas.SID.Controllers" };
}
}
// If this isn't the home page route, this should return null
// which instructs routing to try the next route in the route table.
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
var controller = Convert.ToString(values["controller"]);
var action = Convert.ToString(values["action"]);
if (controller.Equals("Home", StringComparison.OrdinalIgnoreCase) &&
action.Equals("Index", StringComparison.OrdinalIgnoreCase))
{
// Route to the Home page URL
return new VirtualPathData(this, "");
}
return null;
}
}
To wire this into MVC, we just edit the RouteConfig as follows:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Add the custom route to the static routing collection
routes.Add(new CustomHomePageRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "WebApplication23.Controllers" }
);
}
}
This passes an extra route value sipModel to the PID area's HomeController.Index method. So, we need to adjust the method signature to accept that parameter.
namespace WebApplication23.Areas.SID.Controllers
{
public class HomeController : Controller
{
// GET: SID/Home
public ActionResult Index(int sipModel)
{
return View();
}
}
}
As you can see, there really is no reason to use TempData, either. TempData relies on session state by default. It has its uses, but you should always think twice about using session state in MVC as it can often be avoided entirely.

How to use RouteConfig to append a page URL

I am struggling to get my code work, but I think I've read enough to suggest this is the correct way to approach this.
On my intranet, I'd like the user to type in a single word to search into a textbox, and check a checkbox. When the new page loads, I'd like the URL rewritting services of ASP.NET MVC to kick in and change a value from
mysite.com/?id=blah&isChecked=true
to
mysite.com/home/index/blah/true
My code isn't working in the sense of it gives no error, but doesn't do what I am explaining. So, I've removed the check box to just focus on the textbox.
My only route is
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{MyType}",
defaults: new { controller = "Home", action = "Index", MyType = UrlParameter.Optional }
);
My Controller
public ActionResult Index()
{
ViewBag.Message = "Modify this";
return View();
}
[HttpGet]
public ActionResult Index(string MyType)
{
ViewBag.Message = "..." + MyType;
return View();
}
and my View has
#using (Html.BeginForm("Index", "Home",FormMethod.Get))
{
<input name="MyType" /><br />
<input type="submit" />
}
#Html.ActionLink("Click me", "Index", new { #MyType = "Blah" }) //renders correctly
The problem is, it shows the querystring still in the address bar
mysite.com/?MyType=MySearchValue
instead of
mysite.com/Home/Index/MySearchValue
You can't do this purely with routing because the browser will always send form values as query string parameters when they are part of a GET request. Once the request has been sent to the server, the MVC framework can't do anything about the URL that was used.
This leaves you with only one real option (assuming you don't want to send a custom request using JavaScript), which is to explicitly redirect to the desired URL (meaning you will always have two requests when this form is submitted).
The simplest way of doing this is simply in the controller (rather, in a separate controller to ensure that there is no conflict in method signatures):
public class FormController : Controller
{
public ActionResult Index(string MyType)
{
return RedirectToAction("Index", "MyProperController", new { MyType });
}
}
If you direct your form to this controller action, MVC will then use the routing engine to generate the proper URL for the real action and redirect the browser accordingly.
You could do this from the same controller action but it would involve inspecting the request URL to check whether a query string was used or not and redirecting back to the same action, which is a little odd.

ASP.NET MVC2 Custom routing with wildcard or free text url

I have a requirement to add specific functionality to an asp.net mvc2 web site to provide addtional SEO capability, as follows:
The incoming URL is plain text, perhaps a containing a sentence as follows
"http://somesite.com/welcome-to-our-web-site" or
"http://somesite.com/cool things/check-out-this-awesome-video"
In the MVC pipeline, I would like to take this URL, strip off the website name, look up the remaining portion in a database table and call an appropriate controller/view based on the content of the data in the table. All controllers will simply take a single parameter bieng the unique id from the lookup table. A different controller may be used depnding on different urls, but this must be derieved from the database.
If the url cannot be resolved a 404 error needs to be provided, if the url is found but obsolete then a 302 redirect needs to be provided.
Where the url is resolved it must be retained in the browser address bar.
I have had a look at the routing model, and custom routing and can't quite work out how to do it using these, as the controller would not be predefined, based on a simple route. I am also unsure of what to do to provide 404, 302 back to the headers also. Perhpas I need a custom httpmodule or similar but going there went beyond my understanding.
This must be possible somehow... we did it years ago in Classic ASP. Can anyone help with some details on how to achieve this?
Well, the simplest way would be to have an id somewhere in the url (usually the first option)
routes.MapRoute(
"SEORoute", // Route name
"{id}/{*seostuff}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional, seostuff = UrlParameter.Optional } // Parameter defaults
);
In your controller you'd have something like
public class HomeController {
public ActionResult Index(int id) {
//check database for id
if(id_exists) {
return new RedirectResult("whereever you want to redirect", true);
} else {
return new HttpNotFoundResult();
}
}
}
If you don't want to use the id method you could do something else like...
routes.MapRoute(
"SEORoute", // Route name
"{category}/{page_name}", // URL with parameters
new { controller = "Home", action = "Index", category = UrlParameter.Optional, pagename = UrlParameter.Optional } // Parameter defaults
);
public ActionResult Index(string category, string page_name) {
//same as before but instead of looking for id look for pagename
}
The problem with the latter is that you would need to account for all types of routes and it can get really difficult if you have a lot of parameters that match various types.
This should get you in the right direction. If you neeed some clarification let me know and I'll see if I can write a specific route to help you
Additional
You could probably do what you're looking for like
public ActionResult Index() {
//Create and instance of the new controlle ryou want to handle this request
SomeController controller = new SomeController();
controller.ControllerContext = this.ControllerContext;
return controller.YourControllerAction();
}
but I don't know any of the side effects by doing that...so it's probably not a good idea - but it seems to work.

ASP.NET MVC multiple url's pointing to the same action

How do i map multiple url's to the same action in asp.net mvc
I have:
string url1 = "Help/Me";
string url2 = "Help/Me/Now";
string url3 = "Help/Polemus";
string url1 = "Help/Polemus/Tomorow";
In my global.asax.cs file i want to map all those url to the following action:
public class PageController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}
}
Now in MVC 5 this can be achieved by using Route Attribute.
[Route("Help/Me")]
[Route("Help/Me/Now")]
[Route("Help/Polemus")]
[Route("Help/Polemus/Tomorow")]
public ActionResult Index()
{
return View();
}
Add the following line to your routing table:
routes.MapRoute("RouteName", "Help/{Thing}/{OtherThing}", new { controller = "Page" });
EDIT:
foreach(string url in urls)
routes.MapRoute("RouteName-" + url, url, new { controller = "Page", action = "Index" });
In my case I was looking to simply combine two 'hardcoded' routes into one and stumbled upon this post. I wanted to clean out my RouteConfig.cs a little - because it had so many similar routes.
I ended up using some simple 'or' logic in a regular expression and basically changed:
routes.MapRoute(
"UniqueHomePage",
"Default",
new { controller = "Redirector", action = "RedirectToRoot" }
);
routes.MapRoute(
"UniqueHomePage2",
"Home",
new { controller = "Redirector", action = "RedirectToRoot" }
);
Into a single route:
routes.MapRoute(
"UniqueHomePageGeneric",
"{url}",
new { controller = "Redirector", action = "RedirectToRoot" },
new { url = "Home|Default" }
);
Note for the SEO-savy or -interested: The reason for pointing multiple URL's to one and the same action, is to then redirect them to one and the same page again. In this case the homepage. So the idea is to prevent duplicate content issues. When you use this method for pointing for NON redirecting actions, but actions that show their own views, then you might be CAUSING duplicate content issues :P.
You can just add the routes into your route table as you need them, same as any other route. Just give them unique names, hard coded URL and point them to the same controller / action. It will work fine.
If you use pattern matching instead of hard coded URLs just make sure you get all your routes in the right order so the correct one is selected from the list. So /Help/Me should appear before /Help/{Page} if the hard coded route goes to a different page to the pattern matched one. If you put /help/{page} in the route tabel 1st this will match to /help/me and your hard coded named action for that route would never fire.
On a side note, if this is a public facing site and SEO is important please be careful if you have multiple URLs returning the same data, it will be flagged as duplicate. If this is the case, then use the Canonical tag, this gives all the page rank from all the URLS that go to that single page to the one you name and removes the duplicate content issue for you.

Categories