MVC user created values -- Is reflection my only hope? - asp.net-mvc

Is it possible to determine if a controller exists before attempting to return the View? In my scenerio, my action value in my URL represents a user created value, and the id represents the controller. i.e.
http://mysite.com/systems/WIN1234/Configure
...where WIN1234 is dynamically routed to the Configure action. Because I would like to keep my URLs completely hackable, I would like to determine if Configure exists before...
return View(action)
...where action is my passed in string containing Configure.
The first thing that pops into my head is looking at the assembly using reflection, but before I go that far, and because of my wet ears in MVC, I would like to know if there is a more elegant way to make this determination. i.e. ...something like:
if(DoesControllerExist(action)) return View(action)
...where DoesControllerExist is a built in MVC function.
Any of you experts have any ideas?
Thanks,
George

I'm not sure I get you completely correct, but is this your situation:
http://mysite.com/systems/WIN1234/Configure
Should go to
public class SystemsController : Controller
{
public ActionResult Configure(string theValue) //<== This would be "WIN123" in your example
{
return View(theValue);
}
}
Where you want to make sure that the following view exists:
YourMVCProject
|
+- Views
|
+- Systems
|
+- theValue.aspx
In that case you can use:
private bool ViewExists(string name)
{
return ViewEngines.Engines.FindView(ControllerContext, name, null).View != null;
}
and alter your action to:
public ActionResult Configure(string theValue) //<== This would be "WIN123" in your example
{
if(ViewExists(theValue))
{
return View(theValue);
}
return View(fallBackView);
}
Disclaimer: all freehand code

I think you may be confused as to how the routing system works.
A URL is mapped to a controller and action. If there's no matching controller and action, you'll get an HTTP 404.
Thankfully, URL routes (and of course controllers) are testable so you may want to simply write unit tests for your routing configuration.
On another note, you probably do not want multiple URLs mapping to the same content without issuing an HTTP 301 (permanently moved) since it works against your PageRank.

Related

ASP.NET MVC Routing: Clean Urls - from camel case to hyphenated words

I currently have an action defined in my controller:
// GET: /schools/:cleanUrlName/data-loggers
public ActionResult DataLoggers(string cleanUrlName)
{
return View();
}
This works when I hit "/schools/brisbane-state-high-school/dataloggers", however - as per the comment - I want to access it via a slightly cleaner url (using hyphens): "/schools/brisbane-state-high-school/data-loggers". I know I could write a route to accomplish this, but I was hoping I wouldn't have to write a new route for every multi-worded action/controller. Is there a better way to address this?
You can use the ActionNameAttribute to create an alias for your action name.
So you just need to annotate your multi worded actions:
[ActionName("data-loggers")]
public ActionResult DataLoggers(string cleanUrlName)
{
return View("DataLoggers");
}
But because this affects also the view discovery therefore you need to return View("DataLoggers") so you are probably better with creating custom routes for your multi worded actions.

Is there simple way to use bookmarks in controller action's generated urls?

Currently I am working with simple Forum module in ASP.NET MVC 3 which i will add later to my main application. The possibility to link to certain blocks like div used to present thread replies is very useful.
I figured out something which work and propably will be enough for my needs, I just wonder if there is something more elegant and simple aswell. I found a solution using ActionFilters, and since I'm quite begginner in MVC I would like to find some easier solution (if exists). Well i will propably learn ActionFilters soon aswell :)
So here is what I've done:
public ActionResult ShowThread(int id, int? postID)
{
var thread = db.ForumThreads.Find(id);
if (postID != null)
{
return Redirect(Url.Action("ShowThread",new {id=id})+"#post-"+postID.ToString());
}
return View(thread);
}
I know it is quite simple, but it is working. Also it doesn't check if the postID is valid yet, but it is not a part of question.
The solution is to use one of RedirectToAction overloads: http://msdn.microsoft.com/en-us/library/dd470154(v=vs.108).aspx.
Perhaps, in your case, the next one would do: http://msdn.microsoft.com/en-us/library/dd460291(v=vs.108).aspx
And the call would be:
return this.RedirectToAction("ShowThread", new { id = id, post = postID });
Another (simpler, but not safer!) solution is to format the URL you need to redirect to and to use the Redirect() method like this:
var redirectUrl = string.Format("/Home/ShowThread/{0}?post={1}", id, postID);
return this.Redirect(redirectUrl);
Pay attention to your URL mappings, though. The examples above assume the method
public ActionResult ShowThread(int id, int? post)
is in HomeController and the default route has not been changed. Otherwise you should
adjust the URL prepending the controller name (w/o Controller), or
change your default route to map to the controller's name.

ASP.NET MVC, Reeling in Unroutable Page Nesting

The concept of routes is nothing new, and it works great for the concept of {area}/{controller}/{action}/{parameter}, but few sites are standalone UI interaction.
Websites often need parts of themselves that aren't really dedicated to taking data, but presenting it. For instance one of the sites I am working on has a large part of itself dedicated to user interaction (which the MVC system solves expertly. A Membership area, a place to manage information, a way to purchase items, etc.) - but it also needs a part that functions more like an old-fashioned website, where you're simply looking at pages like a folder structure.
one solution I have found is to try a custom view engine. This worked, but I quick found myself lost in a convoluted routing scheme. Another I guess I could go with is to just have an IgnoreRoute and put files in the ignored folder like normal html/aspx, but I'd really rather have the option of using Controllers so that there is a chance I can have data returned from a database, etc in the future.
So let me show you my current scenario...
Areas
Membership
Rules
Controllers
HomeController
FileView(string folder, string file)
Views
Home
General
Customize
Content
yyy.cshtml
xxx.cshtml
#Html.Partial("Content/yyy.cshtml")
xxx.cshtml
xxx.cshtml
etc. The Rules area is basically setup to function like a normal /folder/file/ structure. So here is my Controller for it..
public class HomeController : Controller
{
//
// GET: /Information/Home/
public ActionResult Index()
{
return View();
}
// **************************************
// URL: /Rules/{controller}/{folder}/{file}
// **************************************
public ViewResult FileView(string folder, string filename)
{
return View(String.Format("{0}/{1}", folder, filename));
}
}
Now, if I have a category, I simply have a lightweight controller that inherits from that Area's HomeController, like this...
public class GeneralController : Rules.Controllers.HomeController
{
// **************************************
// URL: /Rules/General/Customize/{id}
// **************************************
public ViewResult Customize(string id)
{
return FileView("Customize", id);
}
}
So then, for each folder in the 'sub' controller, I have a single Route that takes in the name of the file.
This works, but I feel it's excessively clunky. Can anyone suggest a better alternative? There are just too many pages, and too much nesting, to have a full ActionResult for each one. I also want to maintain clean urls.
Perhaps you can use a catch-all route for the Membership area, route it to a controller (MembershipController?) and have that controller just render the view that is catched by the route, like this:
public class MembershipController : Controller
{
public ActionResult Index(string pageTitle)
{
return View(pageTitle);
}
}
And the route:
routes.MapRoute(
"Membership",
"Membership/{*pageTitle}",
new {controller = "Membership", action = "Index", pageTitle = "NotFound"});
Of course, in the controller you should check whether the view exists or not, but this one should get you moving. Although I don't see why you want to have MVC in front of this when you just want to display (static?) content.

How to get currently executing area?

I have a class used by controllers at [Project].Controllers and by controllers at different areas. How could I determine where the controller is at? (I guess I could look at the HttpContext.Current.Request's properties -but I am looking for a "proper" MVC way). Thank you.
That is:
[Project].Helpers // called by:
[Project].Controllers
[Project].Areas.[Area].Controllers
// how could I determine the caller from [Project].Helpers?
We purposefully did not expose a way to get the current area name from an MVC request since "area" is simply an attribute of a route. It's unreliable for other uses. In particular, if you want your controllers to have some attribute (think of the abstract term, not the System.Attribute class) which can be used by the helper, then those attributes must be found on the controllers themselves, not on the area.
As a practical example, if you want some logic (like an action filter) to run before any controllers in a particular area, you must associate the action filter with those controllers directly. The easiest way to do this is to attribute some MyAreaBaseController with that filter, then to have each controller that you logically want to associate with that area to subclass that type. Any other usage, such as a global filter which looks at RouteData.DataTokens["area"] to make a decision, is unsupported and potentially dangerous.
If you really, really need to get the current area name, you can use RouteData.DataTokens["area"] to find it.
You should be able to get the area string from RouteData:
// action inside a controller in an area
public ActionResult Index()
{
var area = RouteData.DataTokens["area"];
....
return View();
}
.. so you can make an extension method for helpers like this:
public static class SomeHelper // in [Project].Helpers
{
public static string Area(this HtmlHelper helper)
{
return (string)helper.ViewContext.RouteData.DataTokens["area"];
}
}

How to handle null {id} on route?

What if a user hits my site with http://www.mysite.com/Quote/Edit rather than http://www.mysite.com/Quote/Edit/1000 In other words, they do not specify a value for {id}. If they do not, I want to display a nice "Not Found" page, since they did not give an ID. I currentl handle this by accepting a nullable int as the parameter in the Controller Action and it works fine. However, I'm curious if there a more standard MVC framework way of handling this, rather than the code I presently use (see below). Is a smoother way to handle this, or is this pretty mush the right way to do it?
[HttpGet]
public ActionResult Edit(int? id)
{
if (id == null)
return View("QuoteNotFound");
int quoteId = (int)id;
var viewModel = new QuoteViewModel(this.UserId);
viewModel.LoadQuote(quoteId);
if (viewModel.QuoteNo > 0)
{
return View("Create", viewModel.Quote.Entity);
}
else
return View("QuoteNotFound");
}
Your other options would be
Having two Edit actions ; One with int id as its parameters and another without any parameters.
Only having Edit(int id) as your action and letting your controller's HandleUnknownAction method to do what it's supposed to do when your entity is not found (this is a bit more complicated).
But I like your approach the best, as it's simple and correctly handles the situation.
BTW, you don't need that local variable, you can just do this for better readability :
//...
if (!id.HasValue)
return View("QuoteNotFound");
var viewModel = new QuoteViewModel(this.UserId);
viewModel.LoadQuote(id.Value);
//...
No real issue with the way you have it but semantically it's not really an invalid quote number, its that they have navigated to an invalid route that they should not have gone to.
In this case, I would tend to redirect to /quote and if you really want to show a message to the user just show an error banner or similar (assuming you have that functionality in your master page etc).
public ActionResult Edit(int? id)
{
if (id == null)
{
// You will need some framework in you master page to check for this message.
TempData["error"] = "Error Message to display";
return RedirectToAction("Index");
}
...
}
Use route constraint
You can always define a route constraint in your routes.MapRoute() call that won't pass through any requests with undefined (or non-numeric) id:
new { id = "\d+" }
This is a regular expression that checks the value of id to be numeric.
You will probably have to create a new route that defines this, because for other controller actions you probably don't want routes to be undefined. In this case, your controller action wouldn't need a nullable parameter, because id will always be defined.
Don't be afraid of using multiple routes. With real life applications this is quite common.

Resources