Using ASP.NET MVC 5 and Bootstrap 3.2, I am trying to create an Html ActionLink for the current URL like in the Bootstrap example here:
https://www.quackit.com/bootstrap/bootstrap_3/tutorial/bootstrap_breadcrumbs.cfm
<ul class="breadcrumb">
<li>Home</li>
<li>Fruit</li>
<li class="active">Pears</li>
</ul>
Microsoft has a lot of information on their page for ActionLink
https://learn.microsoft.com/en-us/dotnet/api/system.web.mvc.html.linkextensions.actionlink?view=aspnet-mvc-5.2
There doesn't seem to be a version for creating the active list item unless I am missing something in those overloads.
So, how would I create an ActionLink for "Pears" in the unordered list above (no hyperlink and class="active")?
The solution was:
If you don't want a hyperlink, don't use the ActionLink
I still used an ActionLink for Home and for Fruit, but Pears just got the standard <li> treatment like below:
public static string BuildBreadcrumbNavigation(this HtmlHelper helper)
{
var result = string.Empty;
var controllerName = helper.ViewContext.RouteData.Values["controller"].ToString();
// optional condition: I didn't wanted it to show on home and account controller
if ((controllerName != "Home") && (controllerName != "Account"))
{
var homeLink = helper.ActionLink(
linkText: "Home",
actionName: "Index",
controllerName: "Home").ToHtmlString();
var sb = new StringBuilder($"<ol class='breadcrumb'><li>{homeLink}</li>");
var url = HttpContext.Current.Request.Url.ToString();
var urlParts = url.Split(new char[] { '/' });
if (!urlParts.Contains("Console"))
{
var controllerLink = helper.ActionLink(
linkText: controllerName.SplitTitleWords(),
actionName: "Index",
controllerName: controllerName);
sb.Append($"<li>{controllerLink}</li>");
} else
{
var a = $"Console";
sb.Append($"<li>{a}</li>");
}
var actionName = helper.ViewContext.RouteData.Values["action"].ToString();
sb.Append($"<li class=\"active\">{actionName.SplitTitleWords()}</li>");
result = sb.Append("</ol>").ToString();
}
return result;
}
SplitTitleWords is a custom extension that I wrote to break-up words such as AddPDFResource into Add PDF Resource.
public static string SplitTitleWords(this string value)
{
var cList = new List<char>();
if (!string.IsNullOrEmpty(value))
{
cList.Add(value[0]); // just add the first letter, whether caps, no caps, or number
for (var i = 1; i < value.Length; i++)
{
var c = value[i];
if (char.IsUpper(c))
{ // 01234567891234 0123456789012345
// check special cases like class AddPDFResource => Add PDF Resource
var c0 = value[i - 1];
if (char.IsUpper(c0))
{
if (i + 1 < value.Length)
{
var c1 = value[i + 1];
if (!char.IsUpper(c1))
{
cList.Add(' ');
}
}
} else
{
cList.Add(' ');
}
}
cList.Add(c);
}
}
var result = new String(cList.ToArray());
return result;
}
I pull the tags in the news details section. The corresponding code block is below.
NewsDetail:
foreach (var item in etiketler.Take(1))
{
<span>#item</span>
}
foreach (var item in etiketler.Skip(1))
{
<span>#item</span>
}
Controller :
public ActionResult Tag(string tag, int? pageSize)
{
string id = this.RouteData.Values["tag"].ToString();
SectionServices _sectionServices = new SectionServices();
if (!pageSize.HasValue) pageSize = 1;
ViewBag.Current = pageSize;
Models.TagModel model = new Models.TagModel();
var dat = _sectionServices.getNewsByTag((int)pageSize, tag);
ViewData["etiket"] = tag;
if (dat != null)
{
ViewBag.Tag = tag;
model.getNews = dat;
return View(model);
}
return View();
}
Route Config :
routes.MapRoute(
name: "TagPage",
url: "{tag}-haberleri/{pageSize}",
defaults: new { controller = "Page", action = "Tag", pageSize = UrlParameter.Optional }
);
I get errors like "The controller for path '/Mert Hakan_haberleri / 2' was not found or does not implement IController" in log records. what is the cause of this error, clicking on tags works correctly, but I see this error in log records.
I also had this error. When I embedded the classes into a namespace, everything started working for me.
namespace PageControllers { // added this line!
public class PageController {
public ActionResult Tag() {
//code logic
return View();
}
}
}
For the possible duplicate, I already know how to preview my image before uploading it, my issue as detailed below is that when I submit the Form, the image is being received as null.
I am trying to submit a form to an MVC controller that should submit a model, a string, and an Image File,
I made sure that the input has the same name as the parameter within the controller
Following is the Form Initialization code
#using (Html.BeginRouteForm(Sitecore.Mvc.Configuration.MvcSettings.SitecoreRouteName,
new
{
calendar = System.Convert.ToString(Request.QueryString["calendar"]),
ID = System.Convert.ToString(Request.QueryString["id"])
},
FormMethod.Post, new { enctype = "multipart/form-data" }))
{
//Model Input
<input style="opacity: 0;" name="EventImage" type="file" accept="image/jpeg, image/jpg" id="ImageUpload" onchange="readURL(this);" />
}
And the Controller Header
[HttpPost]
[ValidateInput(false)]
public ActionResult AddEvent(Event model, string calendar, HttpPostedFileBase EventImage)
The EventImage Parameter is being returned null and I can't seem to figure out why.
I thought that the ID might be causing the problem so I changed the name attribute to ImageUpload as well as the Parameter within the controller but to no avail as the value is still null.
Additional Info: when a User uploads an image, I let them preview it in an Image box, could that be causing it?
Thanks,
Update:
Here is the code for the readURL function
function readURL(input) {
if (input.files && input.files[0]) {
var ImageCorrect = false;
var file = input.files[0];
var reader = new FileReader();
reader.onload = function (e) {
// Concatenate our HTML image info
var ext = file.name.match(/\.([^\.]+)$/)[1];
switch (ext) {
case 'jpg':
case 'jpeg':
case 'JPG':
case 'JPEG':
{
if ((Math.round(file.size / 1024)) > 500) {
alert('Image is too Large');
}
else {
var image = new Image();
image.src = e.target.result;
image.onload = function () {
var width = parseInt(image.width);
if (width <= 500) {
$('#previewImage').attr('src', e.target.result);
$('#previewImage').show();
}
else {
alert('Image width exceeds maximum width');
}
}
}
}
break;
default:
alert('Image type not allowed')
}
}
reader.readAsDataURL(input.files[0]);
}
}
If I understood your question right you are trying to submit a file from your form to your controller and you get null in the controller.
I did this before, check the following:
cshtml (you can add your attributes to event image like JS call...etc ):
<div class="form-group">
#Sitecore.Globalization.Translate.Text("EventImage")<br>
#Html.TextBoxFor(m => m.EventImage, new { type = "file" })
</div>
Model:
[Display(Name = "Event Image")]
public HttpPostedFileBase EventImage { get; set; }
Controller Signature:
[HttpPost]
[ValidateInput(false)]
public ActionResult AddEvent(Event model)
Catching the Image field:
if (model.EventImage != null && model.EventImage.ContentLength > 0)
{
var fileName = Path.GetFileName(model.EventImage.FileName);
var tempPath = Server.MapPath("~/Temp/uploads");
var path = Path.Combine(tempPath, fileName);
if (!Directory.Exists(tempPath))
Directory.CreateDirectory(tempPath);
model.EventImage.SaveAs(path);
Sitecore.Resources.Media.MediaCreatorOptions options = new Sitecore.Resources.Media.MediaCreatorOptions();
options.FileBased = false;
options.IncludeExtensionInItemName = false;
options.KeepExisting = false;
options.Versioned = false;
options.Destination = "/sitecore/media library/Images/" + ItemUtil.ProposeValidItemName(Path.GetFileName(path));
options.Database = Sitecore.Configuration.Factory.GetDatabase(MasterDatabase);
// Now create the file
Sitecore.Resources.Media.MediaCreator creator = new Sitecore.Resources.Media.MediaCreator();
MediaItem mediaItem = creator.CreateFromFile(path, options);
ImageField _eventImage = (ImageField)_event.Fields[EventImage];
_eventImage.MediaID = mediaItem.ID;
PublishItem(mediaItem);
}
I'm trying to follow the MVC Music Store tutorial , but I got an error which I can't handle. I've created the action:
public ActionResult Browse(string category)
{
using (OnlineStoreDbContext db = new OnlineStoreDbContext())
{
// Get category and its associated products
var categoryModel = db.Categories.Include("Products")
.Single(c => c.Title == category);
return View(categoryModel);
}
}
Than I created and the respective View:
#model OnlineStoreMVC.Core.Models.Category
#{
ViewBag.Title = "Browse";
}
<h2>Browse Category: #Model.Title</h2>
<ul>
#foreach (var product in Model.Products)
{
<li>
#product.Title
</li>
}
</ul>
But when I try to open: http://localhost:51642/Store/Browse?cat=Action, I get error:
"Sequence contains no elements" regarding this line:
var categoryModel = db.Categories.Include("Products")
.Single(c => c.Title == category);
I've alredy tried to replace Single with SingleOrDefault, but then the error was
"Object reference not set to an instance of an object." regarding that line in the View: "<h2>Browse Category: #Model.Title</h2>"
The problem is that you're passing cat as key in you're url and it should be category. So you should call http://localhost:51642/Store/Browse?category=Action
About the the error "Object reference not set to an instance of the object" you have to change you Action method to:
public ActionResult Browse(string category)
{
using (OnlineStoreDbContext db = new OnlineStoreDbContext())
{
// Get category and its associated products
var categoryModel = db.Categories.Include("Products")
.SingleOrDefault(c => c.Title == category);
if (categoryModel == default(Category))
{
categoryModel = new Category();
categoryModel.Products = new List<Product>();
}
return View(categoryModel);
}
}
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.