I am using this example for MVC:
http://www.asp.net/mvc/tutorials/mvc-4/aspnet-mvc-4-mobile-features
When I deploy the site, it looks fine on my Windows Phone. When I visit my site in an iPad, I see no jQuery mobile. How do I enable tablet support?
I had to add iPad as a registered device. Here is my global.asax
using System;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using System.Web.WebPages;
using MvcMobile;
using System.Web.Http;
using System.Collections.Generic;
using System.Web;
using System.Linq;
namespace JMA.Web
{
// Note: For instructions on enabling IIS6 or IIS7 classic mode,
// visit http://go.microsoft.com/?LinkId=9394801
public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
}
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
);
}
protected void Application_Start()
{
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
BundleMobileConfig.RegisterBundles(BundleTable.Bundles);
RegisterCustomDisplayModes();
}
private void RegisterCustomDisplayModes()
{
Dictionary<string, Func<HttpContextBase, bool>> displayModes = new Dictionary<string, Func<HttpContextBase, bool>>
{
{"Mobile", c => c.Request.UserAgent.IndexOf("iPad", StringComparison.InvariantCultureIgnoreCase) > 0}
};
displayModes.Keys.ToList().ForEach(
key => DisplayModeProvider.Instance.Modes.Insert(0, new DefaultDisplayMode(key)
{
ContextCondition = c =>
{
if (c == null || c.Request == null || string.IsNullOrEmpty(c.Request.UserAgent))
{
return false;
}
return displayModes[key](c);
}
}));
}
}
}
The word mobile redirects users to Shared/Layout/_Layout.Mobile. The word iPad looks for the device. More info can be found here:
http://www.asp.net/mvc/tutorials/mvc-4/aspnet-mvc-4-mobile-features
"Browser Specific Views"
If I were to change Mobile to iPad, the browser redirects to _Layout.iPad
Go to ~/Shared/_ViewSwitcher and change the code:
#if (Request.Browser.IsMobileDevice && Request.HttpMethod == "GET" ||
Request.UserAgent.IndexOf("iPad", StringComparison.InvariantCultureIgnoreCase) > 0 )
{
<div class="view-switcher ui-bar-d" data-theme="d">
#if (ViewContext.HttpContext.GetOverriddenBrowser().IsMobileDevice || Request.UserAgent.IndexOf("iPad", StringComparison.InvariantCultureIgnoreCase) > 0 )
{
#: Displaying mobile view
#Html.ActionLink("Desktop view", "SwitchView", "ViewSwitcher", new { mobile = false, returnUrl = Request.Url.PathAndQuery }, new { rel = "external" })
}
else
{
#: Displaying desktop view
#Html.ActionLink("Mobile view", "SwitchView", "ViewSwitcher", new { mobile = true, returnUrl = Request.Url.PathAndQuery }, new { rel = "external" })
}
</div>
}
Related
I have this ASP.NET MVC 5 project which I'm converting over to AngularJS with MS Web Api.
Now in the old project I have these c# controllers of type Controller, however in my new project I've created some new Web Api controllers of type ApiController.
Now I'd like to reuse the old controller code in my new project. Herein lies my confusion.
As I attempt to port the old controller code over to my Web Api controller, I'm getting some front-end $http request errors.
Here's a function from my Angular dataService factory which makes an http req down to 'api/Whatif/SummaryPortfolios':
function getCurrPortfoliosLIst() {
var deferred = $q.defer();
var url = 'api/Whatif/SummaryPortfolios';
var req={
method: 'POST',
url: url,
headers: {
'Content-Type': 'application/json',
},
data:{}
};
$http(req).then(function (resp){
deferred.resolve(resp.data);
}, function(err){
console.log('Error from dataService: ' + resp);
});
}
But the $http error section is returning this exception:
data: Object
ExceptionMessage: "Multiple actions were found that match the request:
↵SummaryPortfolios on type MarginWorkbenchNG.Controllers.WhatifController
↵Post on type MarginWorkbenchNG.Controllers.WhatifController"
ExceptionType: "System.InvalidOperationException"
Message: "An error has occurred."
StackTrace: " at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.SelectAction(HttpControllerContext controllerContext)
↵ at System.Web.Http.Controllers.ApiControllerActionSelector.SelectAction(HttpControllerContext controllerContext)
↵ at System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)
↵ at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()
Here's the c# API controller I'm calling down to, but I need to figure out how to create methods other than straight Get() and Post() methods:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web;
using System.Web.Http;
using Microsoft.AspNet.Identity;
using NLog;
using Microsoft.AspNet.Identity.Owin;
using MarginWorkbenchNG.Models;
using Rz.DAL;
using Rz.DAL.Integration;
using Rz.DAL.Models;
using Rz.DAL.Models.Rz;
namespace MarginWorkbenchNG.Controllers
{
public class WhatifController : ApiController
{
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
[HttpPost]
public List<WhatifSummaryViewModel> SummaryPortfolios(string filterValue = "", int? whatIfBatchNumber = null, bool includeBaseline = true)
{
// Get portfolios from Rz
IEnumerable<Portfolio> portfolios = GetPortfolios(filterValue, whatIfBatchNumber, includeBaseline)
.Where(x => x.PortfolioKeys.Any(k => k.Type == Settings.Whatif.SidebarPortfolioKey && k.DisplayValue == filterValue));
// View Model
List<WhatifSummaryViewModel> model = new List<WhatifSummaryViewModel> { };
/// additional code here...
return model;
}
}
}
The old controller (from the MVC5 project) looks slightly different of course because the _Summary method is of type ActionResult and returns a Partial:
public class WhatifController : Controller
{
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult _Summary(string filterValue = "", int? whatIfBatchNumber = null, bool includeBaseline = true)
{
// Get portfolios from Razor
IEnumerable<Portfolio> portfolios = GetPortfolios(filterValue, whatIfBatchNumber, includeBaseline)
.Where(x => x.PortfolioKeys.Any(k => k.Type == Settings.Whatif.SidebarPortfolioKey && k.DisplayValue == filterValue));
// View Model
List<WhatifSummaryViewModel> model = new List<WhatifSummaryViewModel> { };
// additional code removed for brevity...
return PartialView(model.OrderBy(x => x.Title).ThenBy(x => x.SubTitle));
}
My RouteConfig.cs :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace MarginWorkbenchNG
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
}
The old project also uses Html forms to pull the URL, for example:
<form id="whatif-summary-form" action="#Url.Action("_Summary", "WhatIf")" method="POST"></form>
and then pulls the action attrib to get the URL when building out the ajax request in JavaScript (non-Angular) :
url: form.prop("action")
Is this your entire ApiController? The error message you are receiving is because your ApiController has several methods that are of the same type and it can't tell which one to route to. To test this: comment out all of your controller's methods except the one you are calling. You shouldn't receive that error anymore.
This is an easy fix, just tell web api how to map your route. Add the attribute '[Route("yourroute')]' to your method and it should work.
public class WhatifController : ApiController
{
[HttpPost, Route("Your Route Goes here 'SummaryPortfolios'")]
public IHttpActionResult SummaryPortfolios(string filterValue = "", int? whatIfBatchNumber = null, bool includeBaseline = true)
{
// Get portfolios from Rz
IEnumerable<Portfolio> portfolios = GetPortfolios(filterValue, whatIfBatchNumber, includeBaseline)
.Where(x => x.PortfolioKeys.Any(k => k.Type == Settings.Whatif.SidebarPortfolioKey && k.DisplayValue == filterValue));
// View Model
List<WhatifSummaryViewModel> model = new List<WhatifSummaryViewModel> { };
/// additional code here...
return Ok(model);
}
}
OK so I would love to know how to check witch routes are currently in place in my MVC app, because currently I have this setup for MVC3 app and as far as I know everything is correct, but the "Data" route still does not work:
global.asax
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using InteractiveAnalysis.Common;
using System.Data.SqlClient;
using System.Data;
using System.Configuration;
using System.Diagnostics;
using InteractiveAnalysis.App_Start;
using System.Web.Optimization;
namespace InteractiveAnalysis
{
// Note: For instructions on enabling IIS6 or IIS7 classic mode,
// visit http://go.microsoft.com/?LinkId=9394801
public class MvcApplication : ToolsFramework.Mvc.ToolsFrameworkHttpApplicationBase
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Excel", // Route name
"excel", // URL with parameters
new { controller = "Excel", action = "Index"} // Parameter defaults
);
routes.MapRoute(
"Data", // Route name
"data", // URL with parameters
new { controller = "Data", action = "Index" } // Parameter defaults
);
routes.MapRoute(
"Version", // Route name
"version/{action}", // URL with parameters
new { controller = "Version", action = "Index" } // Parameter defaults
);
routes.MapRoute(
"Main", // Route name
"{ver}", // URL with parameters
new { controller = "Main", action = "Index", ver = UrlParameter.Optional } // Parameter defaults
);
/*
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("cache/{action}/{id}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Main", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
*/
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
UnityObject.Register();
ToolsFramework.Cache.OutputCacheHandeler.addCacheKey(UnityObject.APPLICATION_NAME);
//OldBundleConfig.RegisterBundles(BundleTable.Bundles);
}
protected void Application_Error(object sender, EventArgs e)
{
Response.Clear();
Exception exception = Server.GetLastError();
HttpException httpException = (exception.GetBaseException() as HttpException);
if (httpException != null)
{
switch (httpException.GetHttpCode())
{
case 501: //function not implemented
Server.ClearError();
Response.Write(exception.Message);
return;
}
}
#if !DEBUG
if (Euroland.Azure.Utilities.AzureEnvironment.IsAvailable)
{
EventLog.WriteEntry(InteractiveAnalysis.Common.UnityObject.APPLICATION_NAME, "Error:\r\n\r\n" + exception.Message + "\r\n\r\nStack Trace:\r\n" + exception.StackTrace, EventLogEntryType.Error);
}
else
{
Exception exx = Server.GetLastError().GetBaseException();
System.Diagnostics.StackTrace trace = new System.Diagnostics.StackTrace(exx, true);
string Category = "ASP.NET error";
string ASPCode = "N/A";
string ASPDescription = exx.ToString();
if (ASPDescription.Length > 500) { ASPDescription = ASPDescription.Substring(0, 500); }
string Column = trace.GetFrame(0).GetFileColumnNumber().ToString();
string Description = exx.Message.ToString();
if (Description.Length > 500) { Description = Description.Substring(0, 500); }
string File = trace.GetFrame(0).GetFileName();
string Line = trace.GetFrame(0).GetFileLineNumber().ToString();
string Number = "N/A";
string Source = trace.GetFrame(0).GetMethod().Name;
if (Source.Length > 500) { Source = Source.Substring(0, 250); }
string ServerName = HttpContext.Current.Server.MachineName; //Request.ServerVariables["SERVER_NAME"];
string ServerIP = Request.ServerVariables["LOCAL_ADDR"];
string RemoteIP = Request.ServerVariables["REMOTE_ADDR"];
string UserAgent = Request.ServerVariables["HTTP_USER_AGENT"];
string Referer = Request.ServerVariables["REFERER"];
string URL = Request.Url.ToString();
//currently it can function in the local enviroment
SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["LocaleErrorConnectionString"].ToString());
SqlCommand myCommand = new SqlCommand();
myCommand.CommandType = CommandType.StoredProcedure;
myCommand.CommandText = "spInsertServerError";
myCommand.Connection = conn;
myCommand.Parameters.AddWithValue("#Category", Category);
myCommand.Parameters.AddWithValue("#ASPCode", ASPCode);
myCommand.Parameters.AddWithValue("#ASPDescription", ASPDescription);
myCommand.Parameters.AddWithValue("#Column", Column);
myCommand.Parameters.AddWithValue("#Description", Description);
myCommand.Parameters.AddWithValue("#File", File);
myCommand.Parameters.AddWithValue("#Line", Line);
myCommand.Parameters.AddWithValue("#Number", Number);
myCommand.Parameters.AddWithValue("#Source", Source);
myCommand.Parameters.AddWithValue("#ServerName", ServerName);
myCommand.Parameters.AddWithValue("#ServerIP", ServerIP);
myCommand.Parameters.AddWithValue("#RemoteIP", RemoteIP);
myCommand.Parameters.AddWithValue("#UserAgent", UserAgent);
myCommand.Parameters.AddWithValue("#Referer", Referer);
myCommand.Parameters.AddWithValue("#URL", URL);
try
{
conn.Open();
myCommand.ExecuteNonQuery();
}
catch { }
finally
{
if (conn.State != ConnectionState.Closed) { conn.Close(); }
//Server.ClearError();
}
}
#endif
}
}
}
DataController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Text;
using System.Globalization;
using InteractiveAnalysis.Models;
using ToolsFramework;
namespace InteractiveAnalysis.Controllers
{
public class DataController : Controller
{
//
// GET: /Data/
public string Index()
{
return "something";
}
}
}
But every time I check "localhost:62570/data/" I get a 404 and I just do not get it. What am I missing why hasn't the "Data" route taken hold? As far as I know I have done everything correctly.
The best and easiest way to check/debug routes is using the Phil Haack's route debugger, you can install it with following nuget package
I'm using ASPxGridView and updating with data from DataTable which, works fine.
But when I'm trying to use edit, update, delete, paging, sorting functions I get in trouble.
The buttons are displayed but the function doesn't work when I click them.
Here is my Sample.aspx file:
[ASPx]
<dx:aspxgridview ID="grid" ClientInstanceName="grid" runat="server"
OnDataBinding="grid_DataBinding" Width="100%" EnableCallBacks="false">
<Settings ShowTitlePanel="True" />
<SettingsText Title="Tabla" />
<SettingsPager PageSize ="10" ShowSeparators="true" PageSizeItemSettings-Items="20" >
<PageSizeItemSettings Visible="true" />
</SettingsPager>
</dx:aspxgridview>
The code behind Sample.aspx.cs :
[C#]
using System;
using System.Data;
using System.Web.Mvc;
using DataConnector;
using DevExpress.Web.ASPxGridView;
using DevExpress.Web.Data;
namespace Sample.Views.Actions
{
public partial class Sample: ViewPage
{
private DataTable dt;
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
grid.DataBind();
grid.RowValidating += new ASPxDataValidationEventHandler(grid_RowValidating);
if (!this.IsPostBack)
{
GridViewCommandColumn c = new GridViewCommandColumn();
grid.Columns.Add(c);
c.Caption = "Operations";
c.EditButton.Visible = true;
c.UpdateButton.Visible = true;
c.NewButton.Visible = true;
c.DeleteButton.Visible = true;
c.CancelButton.Visible = true;
}
}
DataTable BindGrid()
{
DataConnection DM = new DataConnection();
if (DM.login("ADMIN", "PASS"))
dt = DM.getDataSet("SELECT * FROM Table_Name");
grid.DataSource = dt;
dt.PrimaryKey = new DataColumn[] { dt.Columns[0] };
return dt;
}
protected void grid_DataBinding(object sender, EventArgs e)
{
// Assign the data source in grid_DataBinding
BindGrid();
}
protected void grid_RowUpdating(object sender, ASPxDataUpdatingEventArgs e)
{
int id = (int)e.OldValues["id"];
DataRow dr = dt.Rows.Find(id);
dr[0] = e.NewValues[""];
dr[1] = e.NewValues[""];
dr[2] = e.NewValues[""];
dr[3] = e.NewValues[""];
dr[4] = e.NewValues[""];
ASPxGridView g = sender as ASPxGridView;
UpdateData(g);
g.CancelEdit();
e.Cancel = true;
}
void grid_RowValidating(object sender, ASPxDataValidationEventArgs e)
{
int id = (int)e.NewValues["id"];
if ((!e.OldValues.Contains("id") || ((int)e.OldValues["id"] != id))
&& (dt.Rows.Find(id) != null))
{
ASPxGridView grid = sender as ASPxGridView;
e.Errors[grid.Columns["id"]] = String.Format("Column 'Id' is constrained to be unique. Value '{0}' is already present.", id);
}
}
private void UpdateData(ASPxGridView g)
{
g.DataBind();
}
}}
The Sample is called on the controller page:
[C#]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Data;
using DataConnector;
namespace Sample.Controllers
{
public class ActionsController : Controller
{
public ActionResult Sample()
{
return View();
}
}
}
The data displayed on the Grid.
I created a new solution where is only the Sample.aspx and Sample.aspx.cs and I'm using without MVC and the functions are working. The difference is "public partial class Sample: System.Web.UI.Page ".
If I try without System.Web.UI.Page on my mentioned code it gives me the following error:
The view must derive from ViewPage, ViewPage<TModel>, ViewuserControl,or ViewuserControl<TModel>.
What am I doing wrong? Why does not the function of the ASPxGridView working?
I'm sorry in advance if it is poorly drafted but not my mother tongue is English, and I am a new programmer :)
The ASPxGridView is a WebForms control and cannot be used within ASP.NET MVC. Instead you should use the DevExpress MVC extensions. Specifically, the DevExpress MVC GridView extension
Watch my "Getting started with DevExpress MVC video" to learn more.
I am new to asp.net mvc and now struggling with url routing. I'm using asp.net mvc 3 RC2.
How can I create a url routing that IGNORES the very end extension in url. the extension can be: .html, .aspx, .php, .anything.
For example, these urls:
/Home.html
/Home.en
/Home.fr
/Home
should go to Home controller?
one more example:
/Home/About.html
/Home/About.en
/Home/About.fr
/Home/About
should go to Home controller and About action.
thank you :)
I'm not sure if you're using IIS7, but if so, then I would recommend a rewrite rule which checks for urls ending in .xyz and then doing a rewrites for them without the .xyz.
Something like this:
<rewrite>
<rules>
<rule name="HtmlRewrite">
<match url="(.*)(\.\w+)$" />
<action type="Rewrite" url="{R:1}" />
</rule>
</rules>
</rewrite>
This will handle the use cases you suggested. Anything that ends with an extension and some characters will be rewritten to a url without the extension. The benefit of this is that you will only need one route because everything goes into your application without one.
You just need to tweak the default route in Global.asax.cs, try this:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}.{extension}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional });
The {extension} value from the url will be included in the route data, but you can just safely ignore it if you don't need it
Either create your own route class, or use this regex route implementation: http://blog.sb2.fr/post/2009/01/03/Regular-Expression-MapRoute-With-ASPNET-MVC.aspx
You could handle this in IIS instead of ASP.NET MVC using IIS Url rewriting. See for example: http://learn.iis.net/page.aspx/496/iis-url-rewriting-and-aspnet-routing/
I started to work on this question as a weekend assignment :D
below code will work as requested in question. please refer below references
1] MyUrlRoute Class : RouteBase
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace MvcIgnoreUrl
{
#region //References
// SO question /http://stackoverflow.com/questions/4449449/asp-net-mvc-to-ignore-html-at-the-end-of-all-url
// Implementing Custom Base entry - Pro Asp.Net MVc Framework
//- http://books.google.com/books?id=tD3FfFcnJxYC&pg=PA251&lpg=PA251&dq=.net+RouteBase&source=bl&ots=IQhFwmGOVw&sig=0TgcFFgWyFRVpXgfGY1dIUc0VX4&hl=en&ei=z61UTMKwF4aWsgPHs7XbAg&sa=X&oi=book_result&ct=result&resnum=6&ved=0CC4Q6AEwBQ#v=onepage&q=.net%20RouteBase&f=false
// SO previous Question on ihttphandler - http://stackoverflow.com/questions/3359816/can-asp-net-routing-be-used-to-create-clean-urls-for-ashx-ihttphander-handle
// phil haack's Route Debugger http://haacked.com/archive/2008/03/13/url-routing-debugger.aspx
#endregion
public class MyUrlRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
//~/Account/LogOn
//~/Home.aspx - Works fine
//~/home/index.aspx -Works Fine
//http://localhost:57282/home/index/1/2/3 - Works fine
//http://localhost:57282/Account/Register http://localhost:57282/Account/LogOn - Works Fine
string url = httpContext.Request.AppRelativeCurrentExecutionFilePath;
//check null for URL
const string defaultcontrollername = "Home";
string[] spliturl = url.Split("//".ToCharArray());
string controllername = String.Empty;
string actionname = "Index";
if (spliturl.Length == 2) //for ~/home.aspx and ~/
{
if (String.IsNullOrEmpty(spliturl[1])) //TODO: http://localhost:57282/ not working - to make it working
{
controllername = defaultcontrollername;
}
else
{
controllername = spliturl[1];
if (controllername.Contains("."))
{
controllername = controllername.Substring(0, controllername.LastIndexOf("."));
}
}
}
else if (spliturl.Length == 3) // For #/home/index.aspx and /home/about
{
controllername = spliturl[1];
actionname = spliturl[2];
if (actionname.Contains("."))
{
actionname = actionname.Substring(0, actionname.LastIndexOf("."));
}
}
else //final block in final case sned it to Home Controller
{
controllername = defaultcontrollername;
}
RouteData rd = new RouteData(this, new MvcRouteHandler());
rd.Values.Add("controller", controllername);
rd.Values.Add("action", actionname);
rd.Values.Add("url", url);
return rd;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
}
in global.asax.cs add below code
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new MyUrlRoute()); // Add before your default Routes
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
working as expected.
possibly you can improve if/elseif code.
Using the Application_BeginRequest, will allow you to intercept all incoming requests, and allow you to trim the extension. Make sure to ignore requests for your content, such as .css, .js, .jpg, etc. Otherwise those requests will have their extensions trimmed as well.
protected void Application_BeginRequest(object sender, EventArgs e)
{
String originalPath = HttpContext.Current.Request.Url.AbsolutePath;
//Ignore content files (e.g. .css, .js, .jpg, .png, etc.)
if (!Regex.Match(originalPath, "^/[cC]ontent").Success)
{
//Search for a file extension (1 - 5 charaters long)
Match match = Regex.Match(originalPath, "\\.[a-zA-Z0-9]{1,5}$");
if (match.Success)
{
String modifiedPath = String.Format("~{0}", originalPath.Replace(match.Value, String.Empty));
HttpContext.Current.RewritePath(modifiedPath);
}
}
}
If you are using IIS 7, you should look at Dan Atkinson's answer.
I'm using IIS 6, so, in my case, I have the option to install isapi rewrite for IIS 6 or create custom route. I prefer to create my simple custom route class
AndraRoute.cs
// extend Route class,
// so that we can manipulate original RouteData
// by overriding method GetRouteDate
public class AndraRoute : Route
{
// constructor
public AndraRoute(
string url,
RouteValueDictionary defaults,
RouteValueDictionary constraints,
IRouteHandler routeHandler)
: base(url, defaults, constraints, routeHandler)
{
}
// get original RouteData
// check if any route data value has extension '.html' or '.anything'
// remove the extension
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var data = base.GetRouteData(httpContext);
if (data == null) return null;
// from original route data, check
foreach (var pair in data.Values)
{
if (pair.Value.ToString().Contains('.'))
{
var splits = pair.Value.ToString().Split('.');
if (splits[1] == "html" || splits[1] == "anything")
{
data.Values[pair.Key] = splits[0];
}
break;
}
}
return data;
}
}
RouteCollectionExtensionHelper.cs
public static class RouteCollectionExtensionHelper
{
public static Route MapAndraRoute(this RouteCollection routes,
string name, string url, object defaults, object constraints,
string[] namespaces)
{
if (routes == null)
{
throw new ArgumentNullException("routes");
}
if (url == null)
{
throw new ArgumentNullException("url");
}
var route = new AndraRoute(url,
new RouteValueDictionary(defaults),
new RouteValueDictionary(constraints),
new MvcRouteHandler());
if ((namespaces != null) && (namespaces.Length > 0))
{
route.DataTokens = new RouteValueDictionary();
route.DataTokens["Namespaces"] = namespaces;
}
routes.Add(name, route);
return route;
}
}
RegisterRoutes method in Global.asax
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("Content/{*pathInfo}");
routes.MapAndraRoute(
"Product",
"product/{id}/{slug}",
new { controller = "product", action = "detail" },
null, null
);
routes.MapAndraRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "home", action = "index", id = UrlParameter.Optional },
null, null
);
}
How can dynamic breadcrumbs be achieved with ASP.net MVC?
If you are curious about what breadcrumbs are:
What are breadcrumbs? Well, if you have ever browsed an online store or read posts in a forum, you have likely encountered breadcrumbs. They provide an easy way to see where you are on a site. Sites like Craigslist use breadcrumbs to describe the user's location. Above the listings on each page is something that looks like this:
s.f. bayarea craigslist > city of san francisco > bicycles
EDIT
I realize what is possible with the SiteMapProvider. I am also aware of the providers out there on the net that will let you map sitenodes to controllers and actions.
But, what about when you want a breadcrumb's text to match some dynamic value, like this:
Home > Products > Cars > Toyota
Home > Products > Cars > Chevy
Home > Products > Execution Equipment > Electric Chair
Home > Products > Execution Equipment > Gallows
... where the product categories and the products are records from a database. Some links should be defined statically (Home for sure).
I am trying to figure out how to do this, but I'm sure someone has already done this with ASP.net MVC.
Sitemap's are definitely one way to go... alternatively, you can write one yourself! (of course as long as standard MVC rules are followed)... I just wrote one, I figured I would share here.
#Html.ActionLink("Home", "Index", "Home")
#if(ViewContext.RouteData.Values["controller"].ToString() != "Home") {
#:> #Html.ActionLink(ViewContext.RouteData.Values["controller"].ToString(), "Index", ViewContext.RouteData.Values["controller"].ToString())
}
#if(ViewContext.RouteData.Values["action"].ToString() != "Index"){
#:> #Html.ActionLink(ViewContext.RouteData.Values["action"].ToString(), ViewContext.RouteData.Values["action"].ToString(), ViewContext.RouteData.Values["controller"].ToString())
}
Hopefully someone will find this helpful, this is exactly what I was looking for when I searched SO for MVC breadcrumbs.
ASP.NET 5 (aka ASP.NET Core), MVC Core Solution
In ASP.NET Core, things are further optimized as we don't need to stringify the markup in the extension method.
In ~/Extesions/HtmlExtensions.cs:
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace YourProjectNamespace.Extensions
{
public static class HtmlExtensions
{
private static readonly HtmlContentBuilder _emptyBuilder = new HtmlContentBuilder();
public static IHtmlContent BuildBreadcrumbNavigation(this IHtmlHelper helper)
{
if (helper.ViewContext.RouteData.Values["controller"].ToString() == "Home" ||
helper.ViewContext.RouteData.Values["controller"].ToString() == "Account")
{
return _emptyBuilder;
}
string controllerName = helper.ViewContext.RouteData.Values["controller"].ToString();
string actionName = helper.ViewContext.RouteData.Values["action"].ToString();
var breadcrumb = new HtmlContentBuilder()
.AppendHtml("<ol class='breadcrumb'><li>")
.AppendHtml(helper.ActionLink("Home", "Index", "Home"))
.AppendHtml("</li><li>")
.AppendHtml(helper.ActionLink(controllerName.Titleize(),
"Index", controllerName))
.AppendHtml("</li>");
if (helper.ViewContext.RouteData.Values["action"].ToString() != "Index")
{
breadcrumb.AppendHtml("<li>")
.AppendHtml(helper.ActionLink(actionName.Titleize(), actionName, controllerName))
.AppendHtml("</li>");
}
return breadcrumb.AppendHtml("</ol>");
}
}
}
~/Extensions/StringExtensions.cs remains the same as below (scroll down to see the MVC5 version).
In razor view, we don't need Html.Raw, as Razor takes care of escaping when dealing with IHtmlContent:
....
....
<div class="container body-content">
<!-- #region Breadcrumb -->
#Html.BuildBreadcrumbNavigation()
<!-- #endregion -->
#RenderBody()
<hr />
...
...
ASP.NET 4, MVC 5 Solution
=== ORIGINAL / OLD ANSWER BELOW ===
(Expanding on Sean Haddy's answer above)
If you want to make it extension-driven (keeping Views clean), you can do something like:
In ~/Extesions/HtmlExtensions.cs:
(compatible with MVC5 / bootstrap)
using System.Text;
using System.Web.Mvc;
using System.Web.Mvc.Html;
namespace YourProjectNamespace.Extensions
{
public static class HtmlExtensions
{
public static string BuildBreadcrumbNavigation(this HtmlHelper helper)
{
// optional condition: I didn't wanted it to show on home and account controller
if (helper.ViewContext.RouteData.Values["controller"].ToString() == "Home" ||
helper.ViewContext.RouteData.Values["controller"].ToString() == "Account")
{
return string.Empty;
}
StringBuilder breadcrumb = new StringBuilder("<ol class='breadcrumb'><li>").Append(helper.ActionLink("Home", "Index", "Home").ToHtmlString()).Append("</li>");
breadcrumb.Append("<li>");
breadcrumb.Append(helper.ActionLink(helper.ViewContext.RouteData.Values["controller"].ToString().Titleize(),
"Index",
helper.ViewContext.RouteData.Values["controller"].ToString()));
breadcrumb.Append("</li>");
if (helper.ViewContext.RouteData.Values["action"].ToString() != "Index")
{
breadcrumb.Append("<li>");
breadcrumb.Append(helper.ActionLink(helper.ViewContext.RouteData.Values["action"].ToString().Titleize(),
helper.ViewContext.RouteData.Values["action"].ToString(),
helper.ViewContext.RouteData.Values["controller"].ToString()));
breadcrumb.Append("</li>");
}
return breadcrumb.Append("</ol>").ToString();
}
}
}
In ~/Extensions/StringExtensions.cs:
using System.Globalization;
using System.Text.RegularExpressions;
namespace YourProjectNamespace.Extensions
{
public static class StringExtensions
{
public static string Titleize(this string text)
{
return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(text).ToSentenceCase();
}
public static string ToSentenceCase(this string str)
{
return Regex.Replace(str, "[a-z][A-Z]", m => m.Value[0] + " " + char.ToLower(m.Value[1]));
}
}
}
Then use it like (in _Layout.cshtml for example):
....
....
<div class="container body-content">
<!-- #region Breadcrumb -->
#Html.Raw(Html.BuildBreadcrumbNavigation())
<!-- #endregion -->
#RenderBody()
<hr />
...
...
There is a tool to do this on codeplex: http://mvcsitemap.codeplex.com/ [project moved to github]
Edit:
There is a way to derive a SiteMapProvider from a database: http://www.asp.net/Learn/data-access/tutorial-62-cs.aspx
You might be able to modify the mvcsitemap tool to use that to get what you want.
I built this nuget package to solve this problem for myself:
https://www.nuget.org/packages/MvcBreadCrumbs/
You can contribute here if you have ideas for it:
https://github.com/thelarz/MvcBreadCrumbs
For those using ASP.NET Core 2.0 and looking for a more decoupled approach than vulcan's HtmlHelper, I recommend having a look at using a partial view with dependency injection.
Below is a simple implementation which can easily be molded to suit your needs.
The breadcrumb service (./Services/BreadcrumbService.cs):
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System;
using System.Collections.Generic;
namespace YourNamespace.YourProject
{
public class BreadcrumbService : IViewContextAware
{
IList<Breadcrumb> breadcrumbs;
public void Contextualize(ViewContext viewContext)
{
breadcrumbs = new List<Breadcrumb>();
string area = $"{viewContext.RouteData.Values["area"]}";
string controller = $"{viewContext.RouteData.Values["controller"]}";
string action = $"{viewContext.RouteData.Values["action"]}";
object id = viewContext.RouteData.Values["id"];
string title = $"{viewContext.ViewData["Title"]}";
breadcrumbs.Add(new Breadcrumb(area, controller, action, title, id));
if(!string.Equals(action, "index", StringComparison.OrdinalIgnoreCase))
{
breadcrumbs.Insert(0, new Breadcrumb(area, controller, "index", title));
}
}
public IList<Breadcrumb> GetBreadcrumbs()
{
return breadcrumbs;
}
}
public class Breadcrumb
{
public Breadcrumb(string area, string controller, string action, string title, object id) : this(area, controller, action, title)
{
Id = id;
}
public Breadcrumb(string area, string controller, string action, string title)
{
Area = area;
Controller = controller;
Action = action;
if (string.IsNullOrWhiteSpace(title))
{
Title = Regex.Replace(CultureInfo.CurrentCulture.TextInfo.ToTitleCase(string.Equals(action, "Index", StringComparison.OrdinalIgnoreCase) ? controller : action), "[a-z][A-Z]", m => m.Value[0] + " " + char.ToLower(m.Value[1]));
}
else
{
Title = title;
}
}
public string Area { get; set; }
public string Controller { get; set; }
public string Action { get; set; }
public object Id { get; set; }
public string Title { get; set; }
}
}
Register the service in startup.cs after AddMvc():
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddTransient<BreadcrumbService>();
Create a partial to render the breadcrumbs (~/Views/Shared/Breadcrumbs.cshtml):
#using YourNamespace.YourProject.Services
#inject BreadcrumbService BreadcrumbService
#foreach(var breadcrumb in BreadcrumbService.GetBreadcrumbs())
{
<a asp-area="#breadcrumb.Area" asp-controller="#breadcrumb.Controller" asp-action="#breadcrumb.Action" asp-route-id="#breadcrumb.Id">#breadcrumb.Title</a>
}
At this point, to render the breadcrumbs simply call Html.Partial("Breadcrumbs") or Html.PartialAsync("Breadcrumbs").
Maarten Balliauw's MvcSiteMapProvider worked pretty well for me.
I created a small mvc app to test his provider: MvcSiteMapProvider Test (404)
For whoever is interested, I did an improved version of a HtmlExtension that is also considering Areas and in addition uses Reflection to check if there is a Default controller inside an Area or a Index action inside a Controller:
public static class HtmlExtensions
{
public static MvcHtmlString BuildBreadcrumbNavigation(this HtmlHelper helper)
{
string area = (helper.ViewContext.RouteData.DataTokens["area"] ?? "").ToString();
string controller = helper.ViewContext.RouteData.Values["controller"].ToString();
string action = helper.ViewContext.RouteData.Values["action"].ToString();
// add link to homepage by default
StringBuilder breadcrumb = new StringBuilder(#"
<ol class='breadcrumb'>
<li>" + helper.ActionLink("Homepage", "Index", "Home", new { Area = "" }, new { #class="first" }) + #"</li>");
// add link to area if existing
if (area != "")
{
breadcrumb.Append("<li>");
if (ControllerExistsInArea("Default", area)) // by convention, default Area controller should be named Default
{
breadcrumb.Append(helper.ActionLink(area.AddSpaceOnCaseChange(), "Index", "Default", new { Area = area }, new { #class = "" }));
}
else
{
breadcrumb.Append(area.AddSpaceOnCaseChange());
}
breadcrumb.Append("</li>");
}
// add link to controller Index if different action
if ((controller != "Home" && controller != "Default") && action != "Index")
{
if (ActionExistsInController("Index", controller, area))
{
breadcrumb.Append("<li>");
breadcrumb.Append(helper.ActionLink(controller.AddSpaceOnCaseChange(), "Index", controller, new { Area = area }, new { #class = "" }));
breadcrumb.Append("</li>");
}
}
// add link to action
if ((controller != "Home" && controller != "Default") || action != "Index")
{
breadcrumb.Append("<li>");
//breadcrumb.Append(helper.ActionLink((action.ToLower() == "index") ? controller.AddSpaceOnCaseChange() : action.AddSpaceOnCaseChange(), action, controller, new { Area = area }, new { #class = "" }));
breadcrumb.Append((action.ToLower() == "index") ? controller.AddSpaceOnCaseChange() : action.AddSpaceOnCaseChange());
breadcrumb.Append("</li>");
}
return MvcHtmlString.Create(breadcrumb.Append("</ol>").ToString());
}
public static Type GetControllerType(string controller, string area)
{
string currentAssembly = Assembly.GetExecutingAssembly().GetName().Name;
IEnumerable<Type> controllerTypes = Assembly.GetExecutingAssembly().GetTypes().Where(o => typeof(IController).IsAssignableFrom(o));
string typeFullName = String.Format("{0}.Controllers.{1}Controller", currentAssembly, controller);
if (area != "")
{
typeFullName = String.Format("{0}.Areas.{1}.Controllers.{2}Controller", currentAssembly, area, controller);
}
return controllerTypes.Where(o => o.FullName == typeFullName).FirstOrDefault();
}
public static bool ActionExistsInController(string action, string controller, string area)
{
Type controllerType = GetControllerType(controller, area);
return (controllerType != null && new ReflectedControllerDescriptor(controllerType).GetCanonicalActions().Any(x => x.ActionName == action));
}
public static bool ControllerExistsInArea(string controller, string area)
{
Type controllerType = GetControllerType(controller, area);
return (controllerType != null);
}
public static string AddSpaceOnCaseChange(this string text)
{
if (string.IsNullOrWhiteSpace(text))
return "";
StringBuilder newText = new StringBuilder(text.Length * 2);
newText.Append(text[0]);
for (int i = 1; i < text.Length; i++)
{
if (char.IsUpper(text[i]) && text[i - 1] != ' ')
newText.Append(' ');
newText.Append(text[i]);
}
return newText.ToString();
}
}
If can definitely can be improved (probably does not cover all the possible cases), but it did not failed me until now.