Reputation: 2351
Often I will want to render an ActionLink with content from the database (imagine an Id/Name combination) so I will put the following in my view:
@foreach (var row in Model)
{
<li>@Html.ActionLink(row.Name, "Action", "Controller", new { id = row.Id })</li>
}
However, this throws an exception if the Name is an empty string. Is there any way to prevent this. I would like to override ActionLink, but it is an extension so I cannot override.
Any suggestions?
Edit:
First, I do not control the data, or I would ensure that the Name field is required and always populated. Unfortunately this is not the case.
Second, I realize the user will have nothing to click on, but I believe that rendering an empty link is a better alternative than giving them a YSOD.
Upvotes: 5
Views: 4720
Reputation: 39807
The issue you are going to have is if the name is blank, there is nothing to click on for the user in the UI. Example: <a href="someUrl"></a>
gives no clickable object.
What may be better is if you could provide a default "No Name" string to those rows that have an empty string.
@foreach (var row in Model)
{
<li>@Html.ActionLink(string.isNullOrEmpty(row.Name) ? "No Name" : row.Name, "Action", "Controller", new { id = row.Id })</li>
}
EDIT
If you do not want to put a condition in every @Html.ActionLink, you can create your own @Html.Helper
public static IHtmlString ActionLinkCheckNull(this HtmlHelper htmlHelper, string linkText, string action, string controller, object routeValues, object htmlAttributes)
{
var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
var anchor = new TagBuilder("a") { InnerHtml = string.IsNullOrEmpty(linkText) ? "No Name", linktext };
anchor.Attributes["href"] = urlHelper.Action(action, controller, routeValues);
anchor.MergeAttributes(new RouteValueDictionary(htmlAttributes));
return MvcHtmlString.Create(anchor.ToString());
}
Then in your Views folder web.config, add a reference to the namespace that you place this extension so you don't have to have a using statement at the top of every view. Example:
<add namespace="Core.Extensions"/>
Then in your views, use
@Html.ActionLinkCheckNull(row.Name, "Action", "Controller", new { id = row.Id })
Upvotes: 4
Reputation: 2351
It turns out that an instance method will always be preferred over an extension method with the same signature.
I was able to load a CustomHtmlHelper and put instance method ActionLink methods in the new class:
public abstract class CustomWebViewPage<T> : WebViewPage<T>
{
public new CustomHtmlHelper<T> Html { get; set; }
public override void InitHelpers()
{
Ajax = new AjaxHelper<T>(ViewContext, this);
Url = new UrlHelper(ViewContext.RequestContext);
//Load Custom Html Helper instead of Default
Html = new CustomHtmlHelper<T>(ViewContext, this);
}
}
And the HtmlHelper is as follows (ActionLink methods copied from Reflector without the LinkText error check:
public class CustomHtmlHelper<T> : HtmlHelper<T>
{
public CustomHtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer) :
base(viewContext, viewDataContainer)
{
}
//Instance methods will always be called instead of extension methods when both exist with the same signature...
public MvcHtmlString ActionLink(string linkText, string actionName)
{
return ActionLink(linkText, actionName, null, new RouteValueDictionary(), new RouteValueDictionary());
}
public MvcHtmlString ActionLink(string linkText, string actionName, object routeValues)
{
return ActionLink(linkText, actionName, null, new RouteValueDictionary(routeValues), new RouteValueDictionary());
}
public MvcHtmlString ActionLink(string linkText, string actionName, string controllerName)
{
return ActionLink(linkText, actionName, controllerName, new RouteValueDictionary(), new RouteValueDictionary());
}
public MvcHtmlString ActionLink(string linkText, string actionName, RouteValueDictionary routeValues)
{
return ActionLink(linkText, actionName, null, routeValues, new RouteValueDictionary());
}
public MvcHtmlString ActionLink(string linkText, string actionName, object routeValues, object htmlAttributes)
{
return ActionLink(linkText, actionName, null, new RouteValueDictionary(routeValues), AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public MvcHtmlString ActionLink(string linkText, string actionName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
{
return ActionLink(linkText, actionName, null, routeValues, htmlAttributes);
}
public MvcHtmlString ActionLink(string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes)
{
return ActionLink(linkText, actionName, controllerName, new RouteValueDictionary(routeValues), AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public MvcHtmlString ActionLink(string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
{
return MvcHtmlString.Create(GenerateLink(ViewContext.RequestContext, RouteCollection, linkText, null, actionName, controllerName, routeValues, htmlAttributes));
}
public MvcHtmlString ActionLink(string linkText, string actionName, string controllerName, string protocol, string hostName, string fragment, object routeValues, object htmlAttributes)
{
return ActionLink(linkText, actionName, controllerName, protocol, hostName, fragment, new RouteValueDictionary(routeValues), AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public MvcHtmlString ActionLink(string linkText, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
{
return MvcHtmlString.Create(GenerateLink(ViewContext.RequestContext, RouteCollection, linkText, null, actionName, controllerName, protocol, hostName, fragment, routeValues, htmlAttributes));
}
}
And finally, setting the pageBaseType int the Views/Web.config
file to use the new custom WebViewPage:
<system.web.webPages.razor>
...
<pages pageBaseType="Fully.Qualified.Namespace.CustomWebViewPage">
...
</pages>
</system.web.webPages.razor>
Hope this helps someone else.
Upvotes: 10
Reputation: 67898
To further concrete what Tommy is saying, I wanted to include the ultimately called method that's being protected:
private static string GenerateLinkInternal(RequestContext requestContext, RouteCollection routeCollection, string linkText, string routeName, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes, bool includeImplicitMvcValues)
{
string value = UrlHelper.GenerateUrl(routeName, actionName, controllerName, protocol, hostName, fragment, routeValues, routeCollection, requestContext, includeImplicitMvcValues);
TagBuilder tagBuilder = new TagBuilder("a")
{
InnerHtml = (!string.IsNullOrEmpty(linkText)) ? HttpUtility.HtmlEncode(linkText) : string.Empty
};
tagBuilder.MergeAttributes<string, object>(htmlAttributes);
tagBuilder.MergeAttribute("href", value);
return tagBuilder.ToString(TagRenderMode.Normal);
}
so, since you're building a link, the outer method is throwing an exception because it wouldn't have any text to show on the screen. That code is shown below:
public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
{
if (string.IsNullOrEmpty(linkText))
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText");
}
return MvcHtmlString.Create(HtmlHelper.GenerateLink(htmlHelper.ViewContext.RequestContext, htmlHelper.RouteCollection, linkText, null, actionName, controllerName, routeValues, htmlAttributes));
}
technically speaking that code doesn't have to be protected but it is as a feature.
Here is a workaround for you, but you'll need to build your own ActionLink
extension method like this and place it in a code file somewhere in the project:
using System;
using System.Collections.Generic;
using System.Web.Mvc.Resources;
using System.Web.Routing;
namespace System.Web.Mvc.Html
{
public static class MyLinkExtensions
{
public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, int identifier)
{
return htmlHelper.ActionLink(string.IsNullOrEmpty(linkText) ? "some default text" : linkText, actionName, controllerName, new { id = identifier });
}
}
}
and so now your markup looks like this:
@foreach (var row in Model)
{
<li>@Html.ActionLink(row.Name, "Action", "Controller", row.Id)</li>
}
and you have encapsulated the turnary to determine the state of the linkText
and yet still render your links in a single line.
Upvotes: 0