Return PartialView in MVC3 Area is not searching in area - partial-views

I am working on an ASP.Net MVC 3 RC project. I have one area named Drivers. I have a LoadPartial() action in a controller in the Drivers area that returns a PartialView(string, object); When this is returned I get an error on my webpage that says "The partial view 'PublicAttendanceCode' was not found." It searched the following locations:
~/Views/AttendanceEvent/PublicAttendanceCode.aspx
~/Views/AttendanceEvent/PublicAttendanceCode.ascx
~/Views/Shared/PublicAttendanceCode.aspx
~/Views/Shared/PublicAttendanceCode.ascx
~/Views/AttendanceEvent/PublicAttendanceCode.cshtml
~/Views/AttendanceEvent/PublicAttendanceCode.vbhtml
~/Views/Shared/PublicAttendanceCode.cshtml
~/Views/Shared/PublicAttendanceCode.vbhtml
Why is it not searching in the Drivers Area?
I have the following pretty basic routes in Global.asax.cs:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home",
action = "Index",
id = UrlParameter.Optional // Parameter defaults
}
);
}
And in DriversAreaRegistration.cs
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Drivers_default",
"Drivers/{controller}/{action}/{id}",
new { action = "RequestLeave", id = UrlParameter.Optional }
);
}
What am I missing that will make it look in the drivers area for the partial?

How are you providing area name to PartialView() method? I think you should be passing it in new { area = "Drivers" } as routeValues parameter.

The way that the MVC view engines know the area that they should look in is based on the route that was used to process the request.
In the case of the controller action that you have, are you certain that the request was processed by the area's route definition, or is it possible that the request was processed by the more general route that you defined in global.asax?

There are only four overloads of the method PartialView and it seems like neither of them accept routeValues as a parameter.
I solved this problem like this:
return PartialView(
VirtualPathUtility.ToAbsolute("~/Areas/MyArea/Views/Shared/MyView.cshtml"));
It works, but looks ugly.

This works too:
return PartialView("~/Areas/Admin/Views/Shared/MyView.cshtml", model);

Related

Alter MVC Routing with dynamic prefix while maintaining backwards url compatibility

As it is not, I have a site where you must come in on a single url and a cookie is set to track which customer you are affiliated with. I want to change this so that certain controllers only use a url like this:
/{friendlyName}/{controller}/{index}/{id}
that friendly name is unique and lets me select the correct customer without using the cookie kludge.
I have controllers: Home, Redirect that I do not want the friendly name part of (and possibly more).
I have a few others that fit this category that I would like to move into their own areas. How can I not include the areas as valid friendly names? For instance, I have a controller that services up content in an iframe called Framed. currently, a url for this looks like /Framed/action/id. I could put this in an area called Framed with a controller the same name as the action, and I should still be able to maintain the same url.
For the controller Error I want the friendly name to be optional
I have other controllers that I want the friendly name to be required: SignIn, SignOut, Account
Once I have the routing, the problem is altering the code so that my redirects maintain the friendlyurl. Any ideas on how to do that?
My problem is just coming up with a good plan of attack on how to change the routing of my site. I must maintain backwards compatibility of some of the urls - namely anything I don't want the friendly url part of, including the controllers I discussed slitting into their own areas. I'm looking for any good suggests on how to lay this out and go about altering the changes.
To accomplish your objectives, you will need a combination of routes and RouteConstraints. Also, you will need to enforce rules that a friendlyName is unique, and is different from the names of any controllers or areas.
The following routes should be sufficient in RegisterRoutes() in Global.asax.cs:
routes.MapRoute(
"WithFriendlyName",
"{friendlyName}/{controller}/{index}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { friendlyName = new MustBeFriendlyName() }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { controller = new MustNotRequireFriendlyName() }
);
The RouteConstraints should look something like this:
using System;
using System.Web;
using System.Web.Routing;
namespace Examples.Extensions
{
public class MustBeFriendlyName : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// return true if this is a valid friendlyName
// MUST BE CERTAIN friendlyName DOES NOT MATCH ANY
// CONTROLLER NAMES OR AREA NAMES
var _db = new DbContext();
return _db.FriendlyNames.FirstOrDefault(x => x.FriendlyName.ToLowerInvariant() ==
values[parameterName].ToString().ToLowerInvariant()) != null;
}
}
public class MustNotRequireFriendlyName : IRouteConstraint
{
private const string controllersRequiringFriendlyNames =
"SignIn~SignOut~Account";
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// return true if this controller does NOT require a friendlyName
return controllersRequiringFriendlyNames.ToLowerInvariant()
.Contains(values[parameterName].ToString().ToLowerInvariant());
}
}
}
This should get you started.
As far as the URLs generated by your redirects, if the routing is set up correctly, the URL generation should follow, so that the only changes you are likely to need are those to insure {friendlyName} is being passed.
You probably will have to add some additional routes and constraints as you get further into your changes.
Just wanted to add to this, as the optional prefix been biting me for the past couple of days. While I want to use the solution provided by #counsellorben, I also needed to be able to address the routes by the same name, which is impossible when using 2 routes.
It took me some headscratching, but finally the solution actually seemed very simple. I just needed to create an intermediate aggregate route:
public class AggregateRoute : RouteBase
{
private readonly RouteBase[] _routes;
public AggregateRoute(params RouteBase[] routes)
{
_routes = routes;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData routeData = null;
foreach (var route in _routes)
{
routeData = route.GetRouteData(httpContext);
if (routeData != null) break;
}
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData virtualPath = null;
foreach (var route in _routes)
{
virtualPath = route.GetVirtualPath(requestContext, values);
if (virtualPath != null) break;
}
return virtualPath;
}
}
This allows me to do:
routes.Add(
"RouteName",
new AggregateRoute(
new Route("{path}", new MvcRouteHandler()),
new Route("{prefix}/{path}", new MvcRouteHandler())
)
);
Which enables resolving either route by the same name, which is impossible when adding both routes separately:
Url.RouteLink("RouteName", new RouteValueDictionary{
new{path="some-path"}});
Url.RouteLink("RouteName", new RouteValueDictionary{
new{path="some-prefix/some-path"}});

Optional route parameters and action selection

I use the default route definition:
{controller}/{action}/{id}
where id = UrlParameter.Optional. As much as I understand it this means when id is not being part of the URL this route value will not exists in the RouteValues dictionary.
So this also seems perfectly possible (both GET):
public ActionResult Index() { ... } // handle URLs: controller/action
public ActionResult Index(int id) { ... } // handle URLs: controller/action/id
When id is missing the first action would be executed, but when id is present, the second one would execute. Fine, but it doesn't work. It can't resolve actions.
How can I accomplish this?
I'm thinking of writing a custom action method selector attribute like:
[RequiresRouteValue(string valueName)]
This would make it possible to use this kind of action methods. But is this the only way of doing it?
Is there something built-in I can hang on to?
Use either:
[HttpGet]
public ActionResult Index() { ... } // handle URLs: controller/action
[HttpPost]
public ActionResult Index(int id) { ... } // handle URLs: controller/action/id
Or just have one with a nullable param:
public ActionResult Index(int? id) { ... } // handles both instances
EDIT:
Would something like this work?
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/", // URL with parameters
new { controller = "Login", action = "Index" } // Parameter defaults
);
routes.MapRoute(
"DefaultWithValue", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Login", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
Well from the exception that action can't be determines is pretty clear that actions are resolved first then data binder comes into play and examines action's parameters and tries to data bind values to them. Makes perfect sense.
This makes perfect sense. There would be no point in first trying to data bind values to all possible types and see what we get and then look for an appropriate action. That would be next to impossible.
So. Since action selection is the problem here I guess the best (and only) way to solve this (if I don't want to use a multifaceted single action method) is to write a custom action method selector attribute.
You can read all the details and get the code on my blog:
Improving Asp.net MVC maintainability and RESTful conformance

actionlink not resolving urls related to route

I have the following route ( it's the first in my global.asax )
routes.MapRoute(
"AdminCompany", // Route name
"{controller}.aspx/{action}/{companyId}/{id}", // URL with parameters
new { controller = "Home", action = "Index", companyId = "", id = "" } // Parameter defaults
);
if i navigation to
"Order/DisplayAdmin/2/79000180" it resolves correctly
However if i do the following
Html.ActionLink("View", "DisplayAdmin", new {companyId = Model.CompanyId, id = order.OrderNumber }, new { #class = "button add" })
it displays
/Order.aspx/DisplayAdmin/39068760?companyId=0
which also works, but isn't so pretty :)
Here is my Controller Method
public ActionResult DisplayAdmin(int companyId, [DefaultValue(0)]int id, [DefaultValue(0)] int orderItemStatusId)
{
var viewModel = DisplayAdminViewModel(companyId, id, _statusResponses);
return View(viewModel);
}
Am i calling ActionLink the wrong way? how do i get the nice Urls?
only thing I can think of that is happening is that its falling back to the Default route, I did a copy paste of both your route and the html.ActionLink and it works perfectly for me displaying it like "/Order.aspx/DisplayAdmin/39068760/45456", i did replicate the same fault like you get if the naming isn't the same in the route and action link.
Use the overload of ActionLink that has a RouteValueDictionary argument.
It appears that you are currently using the overload with the "object" argument and it's going to a workable, but not as clean, url.

Simple ASP.NET MVC views without writing a controller

We're building a site that will have very minimal code, it's mostly just going to be a bunch of static pages served up. I know over time that will change and we'll want to swap in more dynamic information, so I've decided to go ahead and build a web application using ASP.NET MVC2 and the Spark view engine. There will be a couple of controllers that will have to do actual work (like in the /products area), but most of it will be static.
I want my designer to be able to build and modify the site without having to ask me to write a new controller or route every time they decide to add or move a page. So if he wants to add a "http://example.com/News" page he can just create a "News" folder under Views and put an index.spark page within it. Then later if he decides he wants a /News/Community page, he can drop a community.spark file within that folder and have it work.
I'm able to have a view without a specific action by making my controllers override HandleUnknownAction, but I still have to create a controller for each of these folders. It seems silly to have to add an empty controller and recompile every time they decide to add an area to the site.
Is there any way to make this easier, so I only have to write a controller and recompile if there's actual logic to be done? Some sort of "master" controller that will handle any requests where there was no specific controller defined?
You will have to write a route mapping for actual controller/actions and make sure the default has index as an action and the id is "catchall" and this will do it!
public class MvcApplication : System.Web.HttpApplication {
public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "catchall" } // Parameter defaults
);
}
protected void Application_Start() {
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ControllerBuilder.Current.SetControllerFactory(new CatchallControllerFactory());
}
}
public class CatchallController : Controller
{
public string PageName { get; set; }
//
// GET: /Catchall/
public ActionResult Index()
{
return View(PageName);
}
}
public class CatchallControllerFactory : IControllerFactory {
#region IControllerFactory Members
public IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName) {
if (requestContext.RouteData.Values["controller"].ToString() == "catchall") {
DefaultControllerFactory factory = new DefaultControllerFactory();
return factory.CreateController(requestContext, controllerName);
}
else {
CatchallController controller = new CatchallController();
controller.PageName = requestContext.RouteData.Values["action"].ToString();
return controller;
}
}
public void ReleaseController(IController controller) {
if (controller is IDisposable)
((IDisposable)controller).Dispose();
}
#endregion
}
This link might be help,
If you create cshtml in View\Public directory, It will appears on Web site with same name. I added also 404 page.
[HandleError]
public class PublicController : Controller
{
protected override void HandleUnknownAction(string actionName)
{
try
{
this.View(actionName).ExecuteResult(this.ControllerContext);
}
catch
{
this.View("404").ExecuteResult(this.ControllerContext);
}
}
}
Couldn't you create a separate controller for all the static pages and redirect everything (other than the actual controllers which do work) to it using MVC Routes, and include the path parameters? Then in that controller you could have logic to display the correct view based on the folder/path parameter sent to it by the routes.
Allthough I don't know the spark view engine handles things, does it have to compile the views? I'm really not sure.
Reflecting on Paul's answer. I'm not using any special view engines, but here is what I do:
1) Create a PublicController.cs.
// GET: /Public/
[AllowAnonymous]
public ActionResult Index(string name = "")
{
ViewEngineResult result = ViewEngines.Engines.FindView(ControllerContext, name, null);
// check if view name requested is not found
if (result == null || result.View == null)
{
return new HttpNotFoundResult();
}
// otherwise just return the view
return View(name);
}
2) Then create a Public directory in the Views folder, and put all of your views there that you want to be public. I personally needed this because I never knew if the client wanted to create more pages without having to recompile the code.
3) Then modify RouteConfig.cs to redirect to the Public/Index action.
routes.MapRoute(
name: "Public",
url: "{name}.cshtml", // your name will be the name of the view in the Public folder
defaults: new { controller = "Public", action = "Index" }
);
4) Then just reference it from your views like this:
YourPublicPage <!-- and this will point to Public/YourPublicPage.cshtml because of the routing we set up in step 3 -->
Not sure if this is any better than using a factory pattern, but it seems to me the easiest to implement and to understand.
I think you can create your own controller factory that will always instantiate the same controller class.

ASP.NET MVC 2.0: How to read querystring value

I am trying to build a small ASP.NET MVC 2 application.I have a controller class with the below method in it
public ActionResult Index()
{
TestMvc.Models.PersonalInformation objPerson = new TestMvc.Models.PersonalInformation();
objPerson.FirstName = "Shyju";
objPerson.LastName = "K";
objPerson.EmailId="shyju#company.com";
return View(objPerson);
}
And when the page (View) being called, i can see this data there as my view has these data's displaying. Now i want to know how can i pass a query string in the url and use that id to build the PersonalInformation object.Hoe can i read the querystring value ? Where to read ?
I want the quesrtstring to be like
http://www.sitename/user/4234 where 4234 is the user id
http://www.sitename/user/4234 is not a querystring. The querystring is the part of the URL that comes after the ?, as in http://www.sitename/user?userId=42
However, the default routes that come with the MVC project template should allow you to simply change the signature of your action method to
public ActionResult Index(int id)
and you should get the desired result. You should look into how routing works in MVC if you want full control of your URLs.
Also, note that the index action is usually used for showing a list of all objects, so you probably want the Details action for showing 1 user object.
What you want is to modify your action to accept an id like so:
public ActionResult Index(string id)
{
TestMvc.Models.PersonalInformation objPerson = new TestMvc.Models.PersonalInformation();
if (!string.isNullOrEmpty(id))
{
objPerson = getPerson(id);
}
return View(objPerson);
}
Then add a route to your global.asax:
routes.MapRoute(
"MyRoute", // Route name
"user/{id}", // URL with parameters
new { controller = "mycontroller", action = "index", id = ""} // Parameter defaults
);