I am trying to return the current dynamic View to allow me to append a css class to an ActionLink if the current View is the same as the ActionLink.
As I am passing the majority of links through a specific route, in this case Pages, the currentAction will always be Pages in most cases, despite the actual View or Template being returned from the ActionResult called.
So for example if the url is http://mytestdomain.com/sport I would like the currentAction to be Sport and not Pages.
Please see my code below:
RouteConfig.cs
routes.MapRoute("Pages", "{mainCategory}/{subCategory}/{pageName}", new { controller = "Home", action = "Pages", subCategory = UrlParameter.Optional, pageName = UrlParameter.Optional });
HomeController
public static MvcHtmlString MenuLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName)
{
var currentController = htmlHelper.ViewContext.ParentActionViewContext.RouteData.GetRequiredString("controller");
var currentAction = htmlHelper.ViewContext.ParentActionViewContext.RouteData.GetRequiredString("action");
var currentView = htmlHelper.CurrentViewName();
var builder = new TagBuilder("li")
{
InnerHtml = htmlHelper.ActionLink(linkText, actionName, controllerName).ToHtmlString()
};
builder.AddCssClass("dropdown");
var actionSplit = actionName.TrimStart('/').Split('/');
actionName = actionSplit[0];
if (controllerName == currentController && actionName == currentAction)
{
return new MvcHtmlString(builder.ToString().Replace("a href", "a class=\"active\" href").Replace("</li>", "").Replace("Home/", ""));
}
return new MvcHtmlString(builder.ToString().Replace("</li>", "").Replace("Home/", ""));
}
public static string CurrentViewName(this HtmlHelper html)
{
return System.IO.Path.GetFileNameWithoutExtension(((RazorView)html.ViewContext.View).ViewPath);
}
public ActionResult Pages(string mainCategory, string subCategory, string pageName)
{
if (!string.IsNullOrEmpty(pageName))
{
subCategory = subCategory + "/" + pageName;
}
Page model;
using (CMSEntities)
{
model = (from f in CMSEntities.GetPage(1, mainCategory, subCategory, "Live") select f).FirstOrDefault();
}
return View(model.Template, model);
}
Navigation.cshtml
#Html.MenuLink(navigation.Title, "/" + Html.ToFriendlyUrl(navigation.Title), "Home")
I have tried using var currentView = htmlHelper.CurrentViewName(); but this will always return Navigation as the ActionLink is being called from within a [ChildActionOnly] public ActionResult Navigation() for example #{ Html.RenderAction("Navigation", "Home"); } from within Views/Shared/_Layout.cshtml
Any help would be much appreciated :-)
In the end I used 'HttpContext.Current.Request.Url.AbsolutePath' to determine the current location to append the active class to the matching page link.
public static MvcHtmlString MenuLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName)
{
var currentController = htmlHelper.ViewContext.ParentActionViewContext.RouteData.GetRequiredString("controller");
var currentUrl = HttpContext.Current.Request.Url.AbsolutePath.TrimStart('/').Split('/');
var mainCategory = currentUrl[0];
var builder = new TagBuilder("li")
{
InnerHtml = htmlHelper.ActionLink(linkText, actionName, controllerName).ToHtmlString()
};
builder.AddCssClass("dropdown");
var actionSplit = actionName.TrimStart('/').Split('/');
actionName = actionSplit[0];
if (actionSplit.Length == 1)
{
if (controllerName == currentController && actionName == mainCategory)
{
return new MvcHtmlString(builder.ToString().Replace("a href", "a class=\"active\" href").Replace("</li>", "").Replace("Home/", ""));
}
}
return new MvcHtmlString(builder.ToString().Replace("</li>", "").Replace("Home/", ""));
}
I hope this proves useful to others :-)
Related
Imagine an object defined like :
public class MyViewModel{
public List<string> MyList { get; set; }
}
In my view, i have this Action link :
#Ajax.ActionLink("<", "Index", new MyViewModel() { MyList = new List<string>() {"foo", "bar"}}, new AjaxOptions())
The html result of the ActionLink will be :
<a class="btn btn-default" data-ajax="true" href="/Index?MyList=System.Collections.Generic.List%601%5BSystem.String%5D"><</a>
My question is, how get this result rather :
<a class="btn btn-default" data-ajax="true" href="/Index?MyList=foo&MyList=bar"><</a>
You can try string.Join. Something like this
#Ajax.ActionLink(
"Your text", -- <
"ActionName", -- Index
new
{
MyList =string.Join(",", new List<string>() {"foo", "bar"}),
otherPropertiesIfyouwant = YourValue
}, -- rounteValues
new AjaxOptions { UpdateTargetId = "..." }, -- Your Ajax option --optional
new { #id = "back" } -- Your html attribute - optional
)
You cannot use #Html.ActionLink() to generate route values for a collection. Internally the method (and all the MVC methods that generate urls) uses the .ToString() method of the property to generate the route/query string value (hence your MyList=System.Collections.Generic.List%601%5BSystem.String%5D" result).
The method does not perform recursion on complex properties or collections for good reason - apart from the ugly query string, you could easily exceed the query string limit and throw an exception.
Its not clear why you want to do this (the normal way is to pass an the ID of the object, and then get the data again in the GET method based on the ID), but you can so this by creating a RouteValueDictionary with indexed property names, and use it in your#Ajax.ActionLink() method.
In the view
#{
var rvd = new RouteValueDictionary();
rvd.Add("MyList[0]", "foo");
rvd.Add("MyList[1]", "bar");
}
#Ajax.ActionLink("<", "Index", rvd, new AjaxOptions())
Which will make a GET to
public ActionResult Index(MyViewModel model)
However you must also make MyList a property (the DefaultModelBinder does not bind fields)
public class MyViewModel{
public List<string> MyList { get; set; } // add getter/setter
}
and then the value of model.MyList in the POST method will be ["foo", "bar"].
With Stephen's anwser, i have develop a helper extension method to do this.
Be careful of the URL query string limit : if the collection has too many values, the URL can be greater than 255 characters and throw an exception.
public static class AjaxHelperExtensions
{
public static MvcHtmlString ActionLinkUsingCollection(this AjaxHelper ajaxHelper, string linkText, string actionName, object model, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
{
var rv = new RouteValueDictionary();
foreach (var property in model.GetType().GetProperties())
{
if (typeof(ICollection).IsAssignableFrom(property.PropertyType))
{
var s = ((IEnumerable<object>)property.GetValue(model));
if (s != null && s.Any())
{
var values = s.Select(p => p.ToString()).Where(p => !string.IsNullOrEmpty(p)).ToList();
for (var i = 0; i < values.Count(); i++)
rv.Add(string.Concat(property.Name, "[", i, "]"), values[i]);
}
}
else
{
var value = property.GetGetMethod().Invoke(model, null) == null ? "" : property.GetGetMethod().Invoke(model, null).ToString();
if (!string.IsNullOrEmpty(value))
rv.Add(property.Name, value);
}
}
return AjaxExtensions.ActionLink(ajaxHelper, linkText, actionName, rv, ajaxOptions, htmlAttributes);
}
}
i have implemented one web grid with pagination enabled using webgridextension.cs file. Same page have multiple buttons to perform different actions like search, get excel, get pdf. in order to perform different form submits i used multiplebutton with action selector attribute. when i click on page number down to the grid it was navigating to default action index.
Here i want to navigate to different action GetRFQData with model (already loaded) in the page..
can you please help me on this.
WebGrid grid = new WebGrid(null, rowsPerPage: 25, canPage: true, defaultSort: "RFQID");
grid.Bind(Model != null ? Model.RFQSearchResults != null ? Model.RFQSearchResults
: new List<Shipsurance.Model.RFQ>()
: new List<Shipsurance.Model.RFQ>(), rowCount: Model != null ? Model.TotalRowsCount :25, autoSortAndPage: false);
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class MultipleButtonAttribute : ActionNameSelectorAttribute
{
public string Name { get; set; }
public string Argument { get; set; }
public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
{
var isValidName = false;
var keyValue = string.Format("{0}:{1}", Name, Argument);
var value = controllerContext.Controller.ValueProvider.GetValue(keyValue);
if (value != null)
{
controllerContext.Controller.ControllerContext.RouteData.Values[Name] = Argument;
isValidName = true;
}
return isValidName;
}
}
[MultipleButton(Name = "action", Argument = "GetRFQData")]
public ActionResult GetData(Shipsurance.Model.RFQ rfqModelData)
{
SerachRFQ getRFQDetails = new SerachRFQ();
return View("Index", getDetails.getResults(rfqModelData));
}
You can use RedirectToAction() method.
return RedirectToAction("Your action", model);
So I have this custom route, which sets up the route table based on culture in the URL, but when I call Url.Action(...), it does not generate the localized URL. Any ideas what I'm doing wrong? The culture is changing on the page and I am able to determine what language user has selected, but Url.Action is not generating localized URL..
This is the custom route, which changes the route table values (not sure if this standard way of doing it):
public class CultureRoute : Route
{
public CultureRoute(string url, object defaults, object contraints)
: base(url, new MvcRouteHandler())
{
base.Defaults = CreateRouteValueDictionary(defaults);
base.Constraints = CreateRouteValueDictionary(contraints);
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var routeData = base.GetRouteData(httpContext);
if (routeData != null)
{
var culture = routeData.Values["culture"].ToString();
var cookie = httpContext.Request.Cookies["culture"];
var areEqual = false;
if (cookie == null || cookie.Value == "" || !(areEqual = string.Equals(culture, cookie.Value, StringComparison.OrdinalIgnoreCase)))
{
routeData.Values["culture"] = culture;
httpContext.Response.Cookies.Add(new HttpCookie("culture", culture));
}
else if (!areEqual)
{
routeData.Values["culture"] = cookie.Value;
}
CultureHelper.SetCurrentCulture(culture);
}
return routeData;
}
private static RouteValueDictionary CreateRouteValueDictionary(object values)
{
var dictionary = values as IDictionary<string, object>;
if (dictionary != null)
{
return new RouteValueDictionary(dictionary);
}
else
{
return new RouteValueDictionary(values);
}
}
}
and this helper class to set the thread culture:
public class CultureHelper
{
public static void SetCurrentCulture(string culture)
{
var info = CultureInfo.CreateSpecificCulture(culture);
Thread.CurrentThread.CurrentCulture = info;
Thread.CurrentThread.CurrentUICulture = info;
}
public static string GetCurrentCulture(bool ignoreRouteData = false)
{
if (!ignoreRouteData)
{
var routeData = HttpContext.Current.Request.RequestContext.RouteData;
object culture;
if (routeData.Values.TryGetValue("culture", out culture))
{
return culture.ToString();
}
}
var cookie = HttpContext.Current.Request.Cookies["culture"];
if (cookie != null && cookie.Value != null)
{
return cookie.Value;
}
return GetThreadCulture();
}
public static string GetThreadCulture()
{
var culture = Thread.CurrentThread.CurrentCulture.Name;
if (culture.IndexOf('-') > -1)
{
culture = culture.Substring(0, 2);
}
return culture;
}
}
and also the RouteConfig class, which is called from the Global.asax and sets up routes using my custom route class:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add("Partial", new CultureRoute(
"{culture}/{cotroller}/partial/{view}",
new { culture = "ka", controller = "home", action = "partial", view = "" },
new { culture = "(ka|en)" }));
routes.Add("Default", new CultureRoute(
"{culture}/{controller}/{action}/{id}",
new { culture = "ka", controller = "home", action = "index", id = UrlParameter.Optional },
new { culture = "(ka|en)" }));
}
}
but without this extension method, I am not able to generate culture based route i.e. Url.Action does not generate URL based on route table the custom route class creates:
public static string Action2(this UrlHelper helper, string action)
{
var culture = CultureHelper.GetThreadCulture();
return helper.Action(action, new { culture = culture });
}
For it to build the URL in ActionLink, you need to override the reverse look-up method named GetVirtualPath as well. Here is an example of how I did it (but I am inheriting RouteBase instead of Route, so yours may need to be done differently).
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData result = null;
if (requestContext.RouteData.IsAreaMatch(this.area))
{
var tenant = this.appContext.CurrentTenant;
// Get all of the pages
var pages = this.routeUrlPageListFactory.GetRouteUrlPageList(tenant.Id);
IRouteUrlPageInfo page = null;
if (this.TryFindMatch(pages, values, out page))
{
result = new VirtualPathData(this, page.VirtualPath);
}
}
return result;
}
private bool TryFindMatch(IEnumerable<IRouteUrlPageInfo> pages, RouteValueDictionary values, out IRouteUrlPageInfo page)
{
page = null;
Guid contentId = Guid.Empty;
var action = Convert.ToString(values["action"]);
var controller = Convert.ToString(values["controller"]);
var localeId = (int?)values["localeId"];
if (localeId == null)
{
return false;
}
if (Guid.TryParse(Convert.ToString(values["id"]), out contentId) && action == "Index")
{
page = pages
.Where(x => x.ContentId.Equals(contentId) &&
x.ContentType.ToString().Equals(controller, StringComparison.InvariantCultureIgnoreCase))
.Where(x => x.LocaleId.Equals(localeId))
.FirstOrDefault();
if (page != null)
{
return true;
}
}
return false;
}
I found that since several of my routes need to be localized and internally I am using the CultureInfo.LCID rather than a culture string to identify culture, that it was better to put the culture parsing code in the Application_BeginRequest event in Global.asax. But that may not be necessary if you are only using the culture string internally.
BTW - I don't think that using a cookie is necessary in your case since the culture can be derived directly from the URL. It seems like unnecessary overhead, especially when you consider that cookies are transferred on every request (including images and javascript files). Not to mention the security implications of doing it this way - you should at the very least encrypt the value in the cookie. Here is an example that shows how to properly sanitize cookie data.
It was actually something else causing the incorrect behavior. I had an extension method that was generating actions to switch the language and it modified the route data.
public static string CultureRoute(this UrlHelper helper, string culture = "ka")
{
var values = helper.RequestContext.RouteData.Values;
string actionName = values["action"].ToString();
if (values.ContainsKey("culture"))
{
values["culture"] = culture;
}
else
{
values.Add("culture", culture);
}
return helper.Action(actionName, HttpContext.Current.Request.QueryString.ToRouteValues());
}
I changed it to this and it works:
public static string CultureRoute(this UrlHelper helper, string culture = "ka")
{
var values = helper.RequestContext.RouteData.Values;
string actionName = values["action"].ToString();
return helper.Action(actionName, new { culture = culture });
}
I do not need to override GetVirtualPath method to get it to work.. I think most of the time we can get away with Route class and just overriding GetRouteData, but would like to hear what others think...
Therefore I faced an issue with MVC ActionLink and bootstrap dropdown. I succeeded with simple menu extension where I passed such a parameters like strings and one bool. But now I am trying to make my own extension which could generate Bootstrap Dropdown and add selected css class to parent of the dropdown - "ONEofTHEdropdownITEMSselected" - when one of those items in dropdown is selected (when selecting dropdown item it routes to different controller there fore can be few or more controllers):
Dropdown <b class="caret"></b>
and
<li class="dropdown">
Dropdown <b class="caret"></b>
<ul class="dropdown-menu">
<li>Action1</li>
<li>Action2</li>
</ul>
</li>
Below is my UI/MenuExtensions.cs what I am trying to achieve - to pass two parameters which could generate the bootstrap dropdown and I can manually insert new menu items in that dropdown.
public static class MenuExtensions
{
public static MvcHtmlString MenuItem(
this HtmlHelper htmlHelper,
string text,
string action,
string controller,
string cssClass = "item",
bool isController = false
)
{
var li = new TagBuilder("li");
var routeData = htmlHelper.ViewContext.RouteData;
var currentAction = routeData.GetRequiredString("action");
var currentController = routeData.GetRequiredString("controller");
if ((string.Equals(currentAction, action, StringComparison.OrdinalIgnoreCase) || isController) &&
string.Equals(currentController, controller, StringComparison.OrdinalIgnoreCase))
li.AddCssClass("am-selected");
li.InnerHtml = htmlHelper.ActionLink(text, action, controller, new { Area = "" }, new { #class = cssClass }).ToHtmlString();
return MvcHtmlString.Create(li.ToString());
}
public static MvcHtmlString SelectMenu(
this HtmlHelper htmlHelper,
string cssClass,
SelectMenuItem[] menuItems
)
{
TagBuilder list = new TagBuilder("li")
{
InnerHtml = ""
};
string currentAction = htmlHelper.ViewContext.RouteData.GetRequiredString("action");
string currentController = htmlHelper.ViewContext.RouteData.GetRequiredString("controller");
foreach (SelectMenuItem menuItem in menuItems)
{
TagBuilder li = new TagBuilder("li")
{
InnerHtml = htmlHelper.ActionLink(menuItem.Text, menuItem.Action, menuItem.Controller, null, new { }).ToHtmlString()
};
ul.InnerHtml += li.ToString();
}
return MvcHtmlString.Create(list.ToString());
}
}
Here is the external class
public class SelectMenuItem
{
public string Text { get; set; }
public string Action { get; set; }
public string Controller { get; set; }
public bool IsVisible { get; set; }
public SelectMenuItem()
{
IsVisible = true;
}
}
After that my html looks like this.
#Html.SelectMenu("dropdown", new []{
new SelectMenuItem{ Text = "ViewOne", Controller = "Controller1", Action = "index", IsVisible = SystemUser.Current().IsAdmin},
new SelectMenuItem{ Text = "ViewTwo", Controller = "Controller2", Action = "index"}
});
The problem is SelectMenu renders only this:
<li></li>
No need to reinvent the wheel. With TwitterBootstrapMVC desired output is achieved with the following syntax:
#using (var dd = Html.Bootstrap().Begin(new DropDown("Dropdown").SetLinksActiveByControllerAndAction()))
{
#dd.ActionLink("Action1", "index", "controller1")
#dd.ActionLink("Action2", "index", "controller2")
}
Notice the extension method SetLinksActiveByControllerAndAction(). That's what makes links active based on current controller/action.
Disclaimer: I'm the author of TwitterBootstrapMVC.
You need to purchase a license if working with Bootstrap 3. For Bootstrap 2 it's free.
I was wondering if anyone knows if it possible to use any of the "out of the box" ASP.NET MVC3 helpers to generate a "link button"...I currently use following:
<a class="button" title="My Action" href="#Url.Action("MyAction", "MyController", new { id = item.Id })">
<img alt="My Action" src="#Url.Content("~/Content/Images/MyLinkImage.png")" />
</a>
I am trying to avoid using MvcFutures, but even if I was able to use them, I don't think there is a extension method it there that will accomplish this either. (I believe solution in this case would be to roll custom helper as seen here)
Finally, this post also has a good idea to handle this via CSS, but that is not what I am asking...
I am using the following to generate action links:
using System;
using System.Linq.Expressions;
using System.Text;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Web.Routing;
using Fasterflect;
namespace StackOverflow.Mvc.Extensions
{
public static class HtmlExtensions
{
#region ActionImage
// href image link
public static string ActionImage( this HtmlHelper helper, string href, string linkText, object htmlAttributes,
string alternateText, string imageSrc, object imageAttributes )
{
var sb = new StringBuilder();
const string format = "<a href=\"{0}\"{1}>{2}</a>";
string image = helper.Image( imageSrc, alternateText, imageAttributes ).ToString();
string content = string.IsNullOrWhiteSpace( linkText ) ? image : image + linkText;
sb.AppendFormat( format, href, GetAttributeString( htmlAttributes ), content );
return sb.ToString();
}
// controller/action image link
public static string ActionImage( this HtmlHelper helper, string controller, string action, string linkText, object htmlAttributes,
string alternateText, string imageSrc, object imageAttributes )
{
bool isDefaultAction = string.IsNullOrEmpty( action ) || action == "Index";
string href = "/" + (controller ?? "Home") + (isDefaultAction ? string.Empty : "/" + action);
return ActionImage( helper, href, linkText, htmlAttributes, alternateText, imageSrc, imageAttributes );
}
// T4MVC ActionResult image link
public static string ActionImage( this HtmlHelper helper, ActionResult actionResult, string linkText, object htmlAttributes,
string alternateText, string imageSrc, object imageAttributes )
{
var controller = (string) actionResult.GetPropertyValue( "Controller" );
var action = (string) actionResult.GetPropertyValue( "Action" );
return ActionImage( helper, controller, action, linkText, htmlAttributes, alternateText, imageSrc, imageAttributes );
}
#endregion
#region Helpers
private static string GetAttributeString( object htmlAttributes )
{
if( htmlAttributes == null )
{
return string.Empty;
}
const string format = " {0}=\"{1}\"";
var sb = new StringBuilder();
htmlAttributes.GetType().Properties().ForEach( p => sb.AppendFormat( format, p.Name, p.Get( htmlAttributes ) ) );
return sb.ToString();
}
#endregion
}
}
Note that the GetAttributeString method relies on the Fasterflect library to make reflection tasks easier, but you can replace that with regular reflection if you prefer not to take the additional dependency.
The Image helper extension used to be part of MvcContrib but appears to have been removed, most likely because the functionality is now built in to MVC. Regardless, I've included it below for completeness:
public static class ImageExtensions {
public static MvcHtmlString Image(this HtmlHelper helper, string imageRelativeUrl, string alt, object htmlAttributes) {
return Image(helper, imageRelativeUrl, alt, new RouteValueDictionary(htmlAttributes));
}
public static MvcHtmlString Image(this HtmlHelper helper, string imageRelativeUrl, string alt, IDictionary<string, object> htmlAttributes) {
if (String.IsNullOrEmpty(imageRelativeUrl)) {
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "imageRelativeUrl");
}
string imageUrl = UrlHelper.GenerateContentUrl(imageRelativeUrl, helper.ViewContext.HttpContext);
return MvcHtmlString.Create(Image(imageUrl, alt, htmlAttributes).ToString(TagRenderMode.SelfClosing));
}
public static TagBuilder Image(string imageUrl, string alt, IDictionary<string, object> htmlAttributes) {
if (String.IsNullOrEmpty(imageUrl)) {
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "imageUrl");
}
TagBuilder imageTag = new TagBuilder("img");
if (!String.IsNullOrEmpty(imageUrl)) {
imageTag.MergeAttribute("src", imageUrl);
}
if (!String.IsNullOrEmpty(alt)) {
imageTag.MergeAttribute("alt", alt);
}
imageTag.MergeAttributes(htmlAttributes, true);
if (imageTag.Attributes.ContainsKey("alt") && !imageTag.Attributes.ContainsKey("title")) {
imageTag.MergeAttribute("title", (imageTag.Attributes["alt"] ?? "").ToString());
}
return imageTag;
}
}
The snippet you have looks quite good. You should wrap it in a general-purpose html helper and call it a day. I'm sure there are other more interesting aspects to your application than nit picking about UI helpers :)
Check at the bottom of this blog post for an example with HTML extension methods from Stephen Walther