In ASP.NET WebForms (well, HTML to be honest) we could reference pages within folders. EG, my structure could be (in regards to folders only)
root -> MyProductsFolder -> Shoes -> Ladies
And my website would show
www.mysite.com/MyProducts/Shoes/Ladies/Page.aspx
In MVC, we use a controller and it would appear that we can only ever have 1 level (folder) deep.
Is this right?
Without using URL rewriting, is it possible to have
www.mysite.com/MyProducts/Shoes/Ladies/Page
I assume the only way to do this is in the controller, but I can't create a controller named Shoes/Ladies
You can use MVC routing to created this URL. Your routing table is usually found in your AppStart > RouteConfig.cs class. You can use the route table to create URL maps to your actions in your controllers.
Assuming that MyProducts is your controller, and Shoes, Ladies are variables you want to accept you can do something like:
routes.MapRoute("MyProducts",
"MyProducts/{category}/{subcategory}/Page",
new { controller = "MyProducts", action = "Index" });
Note that your routes should be in order of most to least specific, so add this route above the default route.
When you navigate to /MyProducts/Shoes/Ladies/Page, it will map to your index action result in your MyProducts controller, passing variables for category and subcategory, so your controller will look something like
public class MyProducts : Controller
{
public ActionResult Index(string category, string subcategory)
{
//Do something with your variables here.
return View();
}
}
If my presumption is wrong, you want a view returned just for that URL, your route will look like:
routes.MapRoute("MyProducts", "MyProducts/Shoes/Ladies/Page", new { controller = "MyProducts", action = "LadiesShoes" });
And your Controller:
public class MyProducts : Controller
{
public ActionResult LadiesShoes()
{
//Do something with your variables here.
return View();
}
}
You can safely omit the final "/page" on the URL if you want to.
If I haven't covered your exact scenario with the above examples, let me know and I will extend my answer.
UPDATE
You can still put your views in a folder structure under the views folder if you want - and then reference the view file location in the controller - in the following example, place your view file called Index.cshtml in Views/Shoes/Ladies/ folder:
public class MyProducts : Controller
{
public ActionResult LadiesShoes()
{
//Do something with your variables here.
return View("~/Views/Shoes/Ladies/Index.cshtml");
}
public ActionResult MensShoes()
{
//Do something with your variables here.
return View("~/Views/Shoes/Mens/Index.cshtml");
}
}
You can use Attribute Routing to define the url of each action like below.
public class ShoeController : Controller
{
// eg: /nike/shoes/lady
[Route("{productName}/shoes/{xxx}")]
public ActionResult View(string productName, string xxx)
{
}
}
Routing Attribute offers flexibility and better code organization. You can check the route definition in the same spot.
Related
Is it possible to re-use controllers in ASP MVC? And if so how?
Perhaps re-use isn't the right word. The situation is I have a menu and sub menu navigation bars as shown below (actually there is another nav bar what is shown)- I know the colour scheme needs some work
The upper bar is populated from a database, so there could be more or less than 3 plans.
The lower bar always has the same three entries. The views for each of these entries are the same regardless of which plan is selected, though they are different from each other. Obviously the data within them is different (populated from different tables).
That is Plan A -> Suggested Points view is the same as Plan B -> Suggested Points view.
But Plan A -> Suggested Points view is not same as Plan A -> Accepted Points view
In order to do this with the views I intend to use partial views, so the same view files can be re-used.
However, how can I do the equivalent for the controllers?
What I would like if for url paths such as:
/PlanA/SuggestedPoints
/PlanB/SuggestedPoints
To my mind I just want the Plan links to set a variable that tells the Points views which database they should hook up to. Which may be the wrong way to think of it and I suspect is incompatible with the url path suggestion above.
Suggested Approach
I would suggest it is better to include a controller name in the route, that way you won't get conflicts so easily with other controllers in your app.
You can modify your RouteConfig.cs file and map a new route. Make sure to add the custom route before the "Default" one.
Something like this:
routes.MapRoute(
"Plans",
"Plans/{planName}/{action}",
new { controller = "Plans", action = "Index" }
);
// Default route here.
Then you would have a controller called Plans with each of your actions having a parameter called planName that lets you identify with plan to work with...
public class PlansController : Controller
{
public ActionResult SuggestedPoints(string planName)
{
// create your view here, using the planName to get the correct data.
}
public ActionResult AcceptedPoints(string planName)
{
// create your view here, using the planName to get the correct data.
}
// etc.
}
This method will allow URL's in the following format:
/Plans/PlanA/SuggestedPoints, /Plans/PlanA/SuggestedPoints, /Plans/PlanB/AcceptedPoints, /Plans/PlanB/AcceptedPoints, etc.
NOTE: If your plans are in your database, it may be more beneficial to use an ID for the plan, but the URL's would look less friendly so that is up to you.
Finally, when you want to create your links in your view, you can use the following:
#Html.RouteLink("link text", "SuggestedPoints", new { controller = "Plans", planName = "PlanA" })
Your Exact Request
If you absolutely must use the URL formats you suggested, then you can do the following which requires a route for each action, but be wary that you will need to rely on the uniqueness of the Action names to ensure they don't conflict with other controllers...
routes.MapRoute(
"SuggestedPoints",
"{planName}/SuggestedPoints",
new { controller = "Plans", action = "SuggestedPoints" }
);
routes.MapRoute(
"AcceptedPoints",
"{planName}/AcceptedPoints",
new { controller = "Plans", action = "AcceptedPoints" }
);
routes.MapRoute(
"RejectedPoints",
"{planName}/RejectedPoints",
new { controller = "Plans", action = "RejectedPoints" }
);
// Default route here.
In this instance, the controller will remain the same as my first suggestion above. Which will allows URL's like as follows:
/PlanA/SuggestedPoints, /PlanA/SuggestedPoints, /PlanB/AcceptedPoints, /PlanB/AcceptedPoints, etc.
It can be something like this:
public class PlanController
{
public ActionResult SuggestedPoints(string plan) //or int planID
{
return View();
}
public ActionResult AcceptedPoints(string plan) //or int planID
{
return View();
}
public ActionResult RejectedPoints(string plan) //or int planID
{
return View();
}
}
and example urls next:
/Plan/SuggestedPoints/PlanA
/Plan/AcceptedPoints/PlanB
WebAPI has a naming convention "FooController" for controller names. This is similar to ASP.NET MVC. Our codebase uses underscores to separate words in identifier named, e.g. "Foo_Bar_Object". In order for the controller names to follow this convention, we need a way to name our controllers "Foo_Controller".
Basically, we don't want URL routes to look like "oursite.com/api/foo_/", and we don't want to have to make exceptions everywhere for the underscore (e.g. route config, names of view folders, etc). To do this in MVC, we do:
Global.asax
protected void Application_Start() {
...
ControllerBuilder.Current.SetControllerFactory(new Custom_Controller_Factory());
}
Custom_Controller_Factory.cs
public class Custom_Controller_Factory : DefaultControllerFactory
{
protected override Type GetControllerType(RequestContext request_context, string controller_name)
{
return base.GetControllerType(request_context, controller_name + "_");
}
}
This seems to completely take care of the problem all in once place in MVC. Is there a way to do the same for WebAPI? I've heard rumor of a DefaultHttpControllerFactory but I can't find it anywhere.
I think DefaultHttpControllerSelector is what you are looking for.
class CustomHttpControllerSelector : DefaultHttpControllerSelector {
public CustomHttpControllerSelector(HttpConfiguration configuration)
: base(configuration) { }
public override string GetControllerName(HttpRequestMessage request) {
IHttpRouteData routeData = request.GetRouteData();
string controller = (string)routeData.Values["controller"]
return controller + "_";
}
Global.asax
GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), new sh_Custom_Http_Controller_Selector(GlobalConfiguration.Configuration));
This is a question about a solution provided by #Andre Calil in the following SO
Razor MVC, where to put global variables that's accessible across master page, partiview and view?
I'm using Andre's approach and have a slight problem:
My _Layout is strongly typed as BaseModel and my view is strongly typed as AccountModel which inherits from BaseModel.
Problem is: if I return a view with no model i.e. return View() then I get an exception. It's caused because the BaseController OnActionExecuted event is checking if the model provided is null as in:
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (filterContext.Result is ViewResultBase)//Gets ViewResult and PartialViewResult
{
object viewModel = ((ViewResultBase)filterContext.Result).Model;
if (viewModel != null && viewModel is MyBaseModel)
{
MyBaseModel myBase = viewModel as MyBaseModel;
myBase.Firstname = CurrentUser.Firstname; //available from base controller
}
}
base.OnActionExecuted(filterContext);//this is important!
}
The model is null so this scenario won't always work. My next step was to make sure I always pass a model into a view even if it's an empty BaseModel object. Problem with that is that I get the following exception:
The model item passed into the dictionary is of type 'MyNamespace.BaseModel', but this dictionary requires a model item of type 'MyNamespace.AccountModel'.
Two points that I need to clarify:
I thought this would work because AccountModel is a sub class of BaseModel?
If the model is null in the code above, is there another way that I can inject a model into each view so that I can avoid having to refactor all my code to include return View(BaseModel.Empty)?
You need to look at custom razor views as described by Phil Haacked in this article:
http://haacked.com/archive/2011/02/21/changing-base-type-of-a-razor-view.aspx
So basically in your BaseController you would set up a public variable that will be fetched on every request in the base controller's Initialize event (in my case it is the instance of User):
public class BaseController : Controller
{
public User AppUser;
protected override void Initialize(RequestContext requestContext)
{
base.Initialize(requestContext);
AppUser = _userService.GetUser(id);
ViewBag.User = AppUser;
}
}
So now you have a variable which can be accessed by any controller which inherits from the base controller. The only thing left to do is to figure out how to use this variable inside your view. This is where the article I linked above will help you. By default all your views are generated from System.Web.Mvc.WebViewPage. However you can make a custom implementation of this class by doing the following:
namespace YourApplication.Namespace
{
public abstract class CustomWebViewPage : WebViewPage
{
private User _appUser;
public User AppUser
{
get
{
try
{
_appUser = (User)ViewBag.User;
}
catch (Exception)
{
_appUser = null;
}
return _appUser;
}
}
}
public abstract class CustomWebViewPage<TModel> : WebViewPage<TModel> where TModel : class
{
private User _appUser;
public User AppUser
{
get
{
try
{
_appUser = (User)ViewBag.User;
}
catch (Exception)
{
_appUser = null;
}
return _appUser;
}
}
}
}
You have just defined a custom razor view class which has a property of user and tries to fetch that from the ViewBag.User that we setup in our base controller. The only thing left to do is to tell your app to use this class when it's trying to generate the view. You can do this by setting the following line in your VIEWS web.config file:
<pages pageBaseType="YourApplication.Namespace.CustomWebViewPage">
Now on your view you get a helper for your User property that you can use like this:
#AppUser
Please not that the pages declaration needs to go into the VIEWS web.config files not the main app web.config!
I think this is a much better solution for you since you don't have to provide the base model to all your view via the view model. View model should be reserved for what it is intended.
In my project I have a controller that allows you to create multiple letters of different types. All of these letter types are stored in the database, but each letter type has different required fields and different views.
Right now I have a route set up for the following URL: /Letters/Create/{LetterType}. I currently have this mapped to the following controller action:
public ActionResult Create(string LetterType)
{
var model = new SpecificLetterModel();
return View(model);
}
I also have a View called Create.cshtml and an EditorTemplate for my specific letter type. This all works fine right now because I have only implemented one Letter Type. Now I need to go ahead and add the rest but the way I have my action set up it is tied to the specific letter type that I implemented.
Since each Letter has its own model, its own set of validations, and its own view, what is the best way to implement these actions? Since adding new letter types requires coding for the model/validations and creating a view, does it make more sense to have individual controller actions:
public ActionResult CreateABC(ABCLetterModel model);
public ActionResult CreateXYZ(XYZLetterModel model);
Or is there a way I can have a single controller action and easily return the correct model/view?
You can do one of the following:
Have a different action method for each input. This is because the mvc framework will see the input of the action method, and use the default model binder to easily bind the properties of that type. You could then have a common private method that will do the processing, and return the view.
Assuming XYZLetterModel and ABCLetterModel are subclasses of some base model, your controller code could look like:
public class SomeController : Controller
{
private ISomeService _SomeService;
public SomeController(ISomeService someService)
{
_SomeService = someService;
}
public ViewResult CreateABC(ABCLetterModel abcLetterModel)
{
// this action method exists to allow data binding to figure out the model type easily
return PostToServiceAndReturnView(abcLetterModel);
}
public ViewResult CreateXYZ(XYZLetterModel xyzLetterModel)
{
// this action method exists to allow data binding to figure out the model type easily
return PostToServiceAndReturnView(xyzLetterModel);
}
private ViewResult PostToServiceAndReturnView(BaseLetterModel model)
{
if (ModelState.IsValid)
{
// do conversion here to service input
ServiceInput serviceInput = ToServiceInput(model);
_SomeService.Create(serviceInput);
return View("Success");
}
else
{
return View("Create", model);
}
}
}
The View code could look like:
#model BaseLetterModel
#if (Model is ABCLetterModel)
{
using (Html.BeginForm("CreateABC", "Some"))
{
#Html.EditorForModel("ABCLetter")
}
}
else if (Model is XYZLetterModel)
{
using (Html.BeginForm("CreateXYZ", "Some"))
{
#Html.EditorForModel("XYZLetter")
}
}
You would still have an editor template for each model type.
Another option is to have a custom model binder that figures out the type, based on some value in a hidden field, and then serializes it using that type.
The first approach is much more preferred because the default model binder works well out of the box, and it's a lot of maintenance to build custom model binders.
I want to use Areas so I set up the following:
public class ContentAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Content";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Content_default",
"Content/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
What I would like is for a person who enters the following URL to be directed to a controller inside my Content area.
www.stackoverflow.com/Content/0B020D/test-data
I would like a person entering any URL with "/Content/" followed by six characters to be sent to:
- Page action in a controller named ItemController
- Six characters passed as the parameter id
- Optional text after that (test-data in this case) to be put into parameter title
How can I do this? I am not very familiar with setting up routes when using areas.
the six digits to be put into a variable called ID
So you're looking for something like
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Content_default",
"Content/{id}/{optional}",
new { controller = "ItemController", action = "TheActionYouWantThisToAllRouteTo" }
}
This would default everything to one controller and action method (which you have to specify in your instance). You can then get the data like so:
public ActionResult TheActionYouWantThisToAllRouteTo (string id, string optional)
{
// Do what you need to do
}
The way the routes are setup, you can name the pieces of information you want in a URL by wrapping it in a pair of { } curly braces. If you'd rather the name of optional to be isTestData then you would just change the route to read "Content/{id}/{isTestData}".
Note: Since you didn't specify the default action method you want this to route to, I substituted it with TheActionYouWantThisToAllRouteTo. Change that string to read the action method you want this to all go to. This also means you can't have a "regular" controller named ContentController, either.
Edit
Stephen Walther has a good blog post on custom route constraints. It can be found here. It should be a good start to get done what you need.