I'm trying to implement a menu for my MVC3 solution. In this menu, I have 2 kind of links:
classic links based on action + controller
Example 1: action = "Studies" + controller = "Main"
Example 2: action = "Contact" + controller = "Main"
a little more complex links based on action + controller + routeValues
Example 3: action = "List" + controller = "Project" + routeValues =
new { category = "BANK" }
Example 4: action = "List" + controller = "Project" + routeValues =
new { category = "PHARMA" }
The menu is displayed like this:
Studies
Contact
Bank
Pharma
...
I would like to select the currently active menu item based on the active page. To achieve this, I implement an htmlHelper like this:
public static MvcHtmlString ActionMenuItem(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes)
{
var route = htmlHelper.ViewContext.RequestContext.RouteData;
var controller = route.GetRequiredString("controller");
var action = route.GetRequiredString("action");
// some code here...
if ((controller == controllerName) && (action == actionName))
{
tag.AddCssClass("active");
}
else
{
tag.AddCssClass("inactive");
}
// some code here...
}
The problem with this basic implementation is that the condition to activate/inactivate menu item is based only on the action and controller values. I also need to check my routeValues for the "complex links" (example 3 & 4).
How can I implement this?
Thanks for your help.
public static MvcHtmlString ActionMenuItem(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes)
{
var route = htmlHelper.ViewContext.RequestContext.RouteData;
var rvd = HtmlHelper.AnonymousObjectToHtmlAttributes(routeValues);
// some code here...
if (IsRouteValuesMatch(rvd, route))
{
tag.AddCssClass("active");
}
else
{
tag.AddCssClass("inactive");
}
// some code here...
}
private static bool IsRouteValuesMatch(RouteValueDictionary rvd, RouteData routeData)
{
foreach (var item in rvd)
{
var value1 = item.Value as string;
var value2 = routeData.Values[item.Key] as string;
if (!string.Equals(value1, value2, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
return true;
}
Related
I'm using an extension method to maintain a css class on active links on the menu.
However I've got an issue where the htmlAttributes and the object values are causing errors.
I have the below in my Razor page but I don't understand how I'm meant to be parsing the htmlAttributes.
#Html.MenuLink("Summary", "Summary", "Graphs", null, new { #class = "dropdown-toggle caret", data_target = "#", data_toggle = "dropdown" })
From looking at the HtmlHelper the method should have IDictionary<object, string> as the type for the htmlAttributes. The new { #class = "dropdown-toggle caret", data_target = "#", data_toggle = "dropdown" } syntax isn't typical for dictionaries so is this correct?
Obviously I'm doing something wrong as it's returning the below error:
Argument 6: cannot convert from '<anonymous type: string class, string data_target, string data_toggle>' to 'System.Collections.Generic.IDictionary<object, string>'
Extension method I'm trying to get working below:
public static MvcHtmlString MenuLink(this HtmlHelper htmlHelper, string text, string action, string controller, RouteValueDictionary routeValues, IDictionary<object, string> htmlAttributes)
{
var routeData = htmlHelper.ViewContext.RouteData.Values;
var currentController = routeData["controller"];
var currentAction = routeData["action"];
if (string.Equals(action, currentAction as string, StringComparison.OrdinalIgnoreCase) &&
string.Equals(controller, currentController as string, StringComparison.OrdinalIgnoreCase))
{
return htmlHelper.ActionLink(text, action, controller, null, new { #class = "currentMenu" });
}
return htmlHelper.ActionLink(text, action, controller);
}
Change the parameter from IDictionary<object, string> htmlAttributes to object htmlAttributes since your passing the attributes as an object.
You can then convert the object using
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
However, no where in your extension method do you ever use the attributes. All your generating is class = "currentMenu" depending on the current controller and action names. If you intention is to add the attributes plus the class name (depending on the condition), you can then use
attributes.Add("class", "currentMenu");
Your complete method to allow defining both route values and html attributes, and to conditionally include the "currentMenu" class name should be
public static MvcHtmlString MenuLink(this HtmlHelper htmlHelper, string text, string action, string controller, object routeValues, object htmlAttributes)
{
var routeData = htmlHelper.ViewContext.RouteData.Values;
string currentController = (string)routeData["controller"];
string currentAction = (string)routeData["action"];
if (string.Equals(action, currentAction, StringComparison.OrdinalIgnoreCase) && string.Equals(controller, currentController, StringComparison.OrdinalIgnoreCase))
{
if (htmlAttributes == null)
{
return htmlHelper.ActionLink(text, action, controller, routeValues, new { #class = "currentMenu" });
}
else
{
// convert object to RouteValueDictionary
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
if (attributes.ContainsKey("class"))
{
// append the class name
attributes["class"] = string.Format("{0} currentMenu", attributes["class"]);
}
else
{
// add the class name
attributes.Add("class", "currentMenu");
}
return htmlHelper.ActionLink(text, action, controller, new RouteValueDictionary(routeValues), attributes);
}
}
return htmlHelper.ActionLink(text, action, controller, routeValues, htmlAttributes);
}
Side note: You should also consider including other overloads to accept RouteValueDictionary routeValues and IDictionary<String, Object>) htmlAttributes as per the in-built ActionLink() methods and you can inspect the source code to see how the various overloads fall through to the other overloads.
I am using ASP.NET MVC 5.2.3 and I would like to know the custom name I gave to an action in a controller from another action within the same controller.
Other than storing the short name of the action in a variable myself or getting it using reflection by finding the ActionName attribute on all actions within the controller type, is there a better way to get this name?
Please consider this example.
class FooController : Controller
{
[ActionName("shortName")]
public ActionResult LongActionNameIDoNotWantToExposeInTheUri()
{
}
public ActionResult AnotherAction()
{
// This make the Uri as
// /Foo/LongActionNameIDoNotWantToExposeInTheUri
// Instead, I want it to be /Foo/shortName
// I can, of course, hardcode or store the short name
// in a variable and get it but is there a better way?
var url = Url.Action("LongActionNameIDoNotWantToExposeInTheUri", "Foo");
}
}
Try this
Type controllerType = typeof(FooController);
string actionMethodName = "LongActionNameIDoNotWantToExposeInTheUri";
MethodInfo methodInfo = controllerType.GetMethod(actionMethodName);
var attributes = methodInfo.GetCustomAttributes(typeof(ActionNameAttribute), false);
string actionName = string.empty;
if (attributes.Length > 0)
{
actionName = ((ActionNameAttribute)attributes[0]).Name;
}
or if you want to use it with a method
public string GetActionName(Controller controller, string actionMethodName)
{
Type controllerType = controller.GetType();
MethodInfo methodInfo = controllerType.GetMethod(actionMethodName);
var attributes = methodInfo.GetCustomAttributes(typeof(ActionNameAttribute), false);
if (attributes.Length > 0)
{
return ((ActionNameAttribute)attributes[0]).Name;
}
else
{
throw new IndexOutOfRangeException("This controller doesnt have Action Name");
}
}
// if you are in the Controller class
string actionName = GetActionName(this, "LongActionNameIDoNotWantToExposeInTheUri");
I am trying to understand how I can add an extra parameter to URL.Action, and have it as part of the resultant link.
Lets assume the following:
myParm = "myTestParameterValue";
#Url.Action("Edit", "Order", new { id=item.Id}, null,myParm)
which would result in:
/Order/Edit/1/myTestParameterValue
I would really appreciate some sample code of the extension method for this Action Sample to see how the parameters are taken in and how the link is generated.
I guess it would start something like:
public static MvcHtmlString Action(this HtmlHelper helper, string actionName, string controllerName, object routeValues, boolean IsHashRequired)
If (IsHashRequired)
{
String myHash = GetHash();
}
// Pseudocode .... string myNewLink = ... + myHash
Many thanks in advance
EDIT
I need to calculate hash to add to resultant link. A better parameter would be a boolean. I have edited code accordingly.
EDIT2:
public static IHtmlString Action(this UrlHelper urlHelper, string actionName, string controllerName, object routeValues, string protocol, bool isHashRequired )
{
if (isHashRequired)
{
routeValues["hash"] = "dskjdfhdksjhgkdj"; //Sample value.
}
return urlHelper.Action(???); // Resultant URL = /Order/Edit/1/dskjdfhdksjhgkdj
}
EDIT3:
Struggling with :
return urlHelper.Action(actionName, controllerName, routeValues, protocol);
Apparently needs converting to IHtmlString??
EDIT4:
public static String Action(this UrlHelper urlHelper, string actionName, string controllerName, object routeValues, string protocol, bool isHashRequired )
{
RouteValueDictionary rvd = new RouteValueDictionary(routeValues);
if (isHashRequired)
{
string token = "FDSKGLJDS";
rvd.Add("urltoken", token);
}
return urlHelper.Action(actionName, controllerName, rvd, protocol); //rvd is incorrect I believe
}
EDIT5
return urlHelper.Action(actionName, controllerName, rvd, protocol,null);
where
rvd is the RouteValueDictionary
hostname is null.
Thanks...
You should consider modifying your routes
Where you have your routing configured add something like this:
routes.MapRoute(
"hash", // Route name
"{controller}/{action}/{id}/{hash}", // URL with parameters
new { controller = "Home", action = "Index", id = "", hash = "" } // Parameter defaults
);
And use URL.Action like this:
myParm = "myTestParameterValue";
#Url.Action("Edit", "Order", new { id=item.Id, hash = myParm}, null);
You can easily add this with a new extension method class
public static class MyExtensions
{
public static IHtmlString ActionWithHash(this UrlHelper urlHelper, ....)
{
if (hashRequired)
{
routeParameters["hash"] = ...
}
return urlHelper.Action(...);
}
}
Since the validation summary just displays the modelstate errors in a html list, does this mean that I can have only one validation summary? Or is there a way I can associate some kind of context to say that these modelstate errors show up on this summary and these go to the other?
Here's a way to do this without having to code your own ValidationSummary and instead just rely on the underlying MVC validation summary to do the output. This also ensures that the Unobtrusive MVC validation is wired up correctly, etc...
The way this works is that the underlying ValidationSummary iterates through the ModelState based on it's HtmlHelper's IViewContainer. So, this just creates a new HtmlHelper with a filtered IViewContainer to filter out all of the ModelState that matches the prefix. So there's a few extension methods below to achieve all of this.
public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper,
string prefix = "",
bool excludePropertyErrors = false,
string message = "",
IDictionary<string, object> htmlAttributes = null)
{
if (prefix.IsNullOrWhiteSpace())
{
return htmlHelper.ValidationSummary(excludePropertyErrors, message, htmlAttributes);
}
//if there's a prefix applied, we need to create a new html helper with a filtered ModelState collection so that it only looks for
//specific model state with the prefix.
var filteredHtmlHelper = new HtmlHelper(htmlHelper.ViewContext, htmlHelper.ViewDataContainer.FilterContainer(prefix));
return filteredHtmlHelper.ValidationSummary(excludePropertyErrors, message, htmlAttributes);
}
private class ViewDataContainer : IViewDataContainer
{
public ViewDataContainer()
{
ViewData = new ViewDataDictionary();
}
public ViewDataDictionary ViewData { get; set; }
}
public static IViewDataContainer FilterContainer(this IViewDataContainer container, string prefix)
{
var newContainer = new ViewDataContainer();
newContainer.ViewData.ModelState.Merge(container.ViewData.ModelState, prefix);
return newContainer;
}
public static void Merge(this ModelStateDictionary state, ModelStateDictionary dictionary, string prefix)
{
if (dictionary == null)
return;
foreach (var keyValuePair in dictionary.Where(keyValuePair => keyValuePair.Key.StartsWith(prefix + ".")))
{
state[keyValuePair.Key] = keyValuePair.Value;
}
}
That will require you to write your own Validation Summary, either a HTML helper or Partial View. That will look something like this if you take the partial view way.
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%
string prefix = ViewData["prefix"].ToString();
var l = ViewData.ModelState.Where(e => e.Value.Errors.Count != 0 && e.Key.Contains(prefix)).ToList();
if (l.Count() > 0)
{
Response.Write("<div>");
Response.Write("<span>Please fix fields marked with an asteristk</span>");
Response.Write("<ul>");
foreach (KeyValuePair<string, ModelState> keyValuePair in l)
{
foreach (ModelError modelError in keyValuePair.Value.Errors)
{
%>
<li><%= Html.Encode(modelError.ErrorMessage)%></li>
<%
}
} Response.Write("</ul>");
Response.Write("</div>");
}
%>
I Assume that you will pass through ViewData some kind of identification (prefix) to your summary, so it know which errors to display on each Summary.
And it will be used like a normal partial View:
<% ViewData["prefix"] = "YOUR_PREFIX_HERE"; %>
<% Html.RenderPartial("CustomValidationSummary"); %>
PD: you can take this same logic and implement a HTML Helper instead, to make it smoother.
EDIT: added HTML Helper Implementation.
public static class CustomValidationSummary
{
public static string ValidationSummaryFor(this HtmlHelper htmlHelper, string message, string prefix, IDictionary<string, object> htmlAttributes)
{
if (htmlHelper.ViewData.ModelState.IsValid)
{
return null;
}
var l = htmlHelper.ViewData.ModelState.Where(e => e.Value.Errors.Count != 0 && e.Key.StartsWith(prefix)).ToList();
// Nothing to do if there aren't any errors
if (l.Count() == 0)
{
return null;
}
string messageSpan;
if (!String.IsNullOrEmpty(message))
{
TagBuilder spanTag = new TagBuilder("span");
spanTag.MergeAttributes(htmlAttributes);
spanTag.MergeAttribute("class", HtmlHelper.ValidationSummaryCssClassName);
spanTag.SetInnerText(message);
messageSpan = spanTag.ToString(TagRenderMode.Normal) + Environment.NewLine;
}
else
{
messageSpan = null;
}
StringBuilder htmlSummary = new StringBuilder();
TagBuilder unorderedList = new TagBuilder("ul");
unorderedList.MergeAttributes(htmlAttributes);
unorderedList.MergeAttribute("class", HtmlHelper.ValidationSummaryCssClassName);
foreach (KeyValuePair<string, ModelState> keyValuePair in l)
{
foreach (ModelError modelError in keyValuePair.Value.Errors)
{
var errorText = modelError.ErrorMessage;
if (!String.IsNullOrEmpty(errorText))
{
TagBuilder listItem = new TagBuilder("li");
listItem.SetInnerText(errorText);
htmlSummary.AppendLine(listItem.ToString(TagRenderMode.Normal));
}
}
}
unorderedList.InnerHtml = htmlSummary.ToString();
return messageSpan + unorderedList.ToString(TagRenderMode.Normal);
}
}
And to use it:
<% = Html.ValidationSummaryFor("Please fix fields marked with an asteristk","prefix",null) %>
Anyone understand why the following doesn't work?
What I want to do is copy current route data plus whatever I add via an anonymous object into new routedata when forming new links on the view.
For example if I have the parameter "page" as a non route path (i.e. so it overflows the route path and its injected into the method parameter if a querystring is present) e.g.
public ActionResult ChangePage(int? page) { }
and I want the View to know the updated page when building links using helpers. I thought the best way to do this is with the following:
public ActionResult ChangePage(int? page)
{
if(page.HasValue)
RouteData.Values.Add("Page", page);
ViewData.Model = GetData(page.HasValue ? page.Value : 1);
}
Then in the view markup I can render my next, preview, sort, showmore (any links relevant) with this overload:
public static class Helpers
{
public static string ActionLinkFromRouteData(this HtmlHelper helper, string linkText, string actionName, object values)
{
RouteValueDictionary routeValueDictionary = new RouteValueDictionary();
foreach(var routeValue in helper.ViewContext.RouteData.Values)
{
if(routeValue.Key != "controller" && routeValue.Key != "action")
{
routeValueDictionary[routeValue.Key] = routeValue;
}
}
foreach(var prop in GetProperties(values))
{
routeValueDictionary[prop.Name] = prop.Value;
}
return helper.ActionLink(linkText, actionName, routeValueDictionary;
}
private static IEnumerable<PropertyValue> GetProperties(object o)
{
if (o != null) {
PropertyDescriptorCollection props = TypeDescriptor.GetProperties(o);
foreach (PropertyDescriptor prop in props) {
object val = prop.GetValue(o);
if (val != null) {
yield return new PropertyValue { Name = prop.Name, Value = val };
}
}
}
}
private sealed class PropertyValue
{
public string Name { get; set; }
public object Value { get; set; }
}
}
I have posted the code only to illustrate the point. This doesn't work and doesn't feel right... Pointers?
Pass the page info into ViewData?
PagedResultsInfo (or something) sounds like a class you could write too... we do.