MVC3 ActionLink with images (but without MvcFutures)? - c#

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

Related

ASP.NET MVC: Unit Test EditorFor generates an empty MvcHtmlString

I'm working on MVC 5 project and I have an unit test that checks the generated html markup for a custom html helper.
private class Faq
{
[RequiredIfMultiple(new string[] { "SickLeave", "Holidays" }, new object[] { 2, 2 })]
[RequiredIfMultiple(new string[] { "SickLeave1", "SickLeave2", "SickLeave3" }, new object[] { 2, 2, 2 })]
public string Property { get; set; }
}
[Test]
public void HtmlString_With_RequiredIfMultiple_Test()
{
//Arrange
Expression<Func<Faq, string>> expression = (t => t.Property);
//Act
IHtmlString result = htmlHelper.EditorForRequiredIf(expression);
// Assert
Assert.IsTrue(result.ToString().Contains("data-val-requiredifmultiple"));
}
The html helper extension EditorForRequiredIf
public static MvcHtmlString EditorForRequiredIf<TModel, TValue>(this HtmlHelper<TModel> helper
, Expression<Func<TModel, TValue>> expression
, string templateName = null
, string htmlFieldName = null
, object additionalViewData = null)
{
string mvcHtml = EditorExtensions.EditorFor(helper, expression, templateName, htmlFieldName, additionalViewData).ToString();
string element = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(expression));
string key = helper.ViewData.Model.ToString() + "." + element;
if (RequiredIfMultipleAttribute.CountPerField != null)
{
RequiredIfMultipleAttribute.CountPerField.Remove(key);
if (RequiredIfMultipleAttribute.CountPerField.Count == 0)
{
RequiredIfMultipleAttribute.CountPerField = null;
}
}
string pattern = #"data\-val\-requiredif[a-z]+";
return Regex.IsMatch(mvcHtml, pattern) ? MergeClientValidationRules(mvcHtml) : MvcHtmlString.Create(mvcHtml);
}
Once in the view, the EditorFor calls GetClientValidationRules method on a custom attribute RequiredIfMultipleAttribute to generate the html markup with proper data-val tags.
GetClientValidationRules method:
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
int count = 0;
string key = metadata.ContainerType.FullName + "." + metadata.PropertyName;
if (CountPerField == null)
{
CountPerField = new Dictionary<string, int>();
}
if (CountPerField.ContainsKey(key))
{
count = ++CountPerField[key];
}
else
{
CountPerField.Add(key, count);
}
yield return new RequiredIfMultipleValidationRule(ErrorMessageString, Props, Vals, count);
}
So in production, all of this works perfectly. But during tests, I'm having an awful time.
What I get is an empty string on string mvcHtml = EditorExtensions.EditorFor(helper, expression, templateName, htmlFieldName, additionalViewData).ToString(); and on the calls stack, I don't see the method GetClientValidationRules has been called.
On the other hand, if I change EditorExtensions.EditorFor by InputExtensions.TextBoxFor, I see that the MvcHtmlString is correctly generated and GetClientValidationRules was called.
Does anyone have a clue? and I hope I was clear enough.
For the atttribute to work, some piece of code that executes your method has to recognize it and act on it in some way. When you are running your code in production that's what Asp.Net MVC does for you.
Your test is run by NUnit, not by Asp.Net. NUnit doesn't do anything with the attribute because it's not designed to do that. If you think about it, you'll realize that NUnit could not possibly recognize and act on all the various attributes known to all the hosts in which production code may run.
As a result, any use you make of that attribute has to be in your MVC application itself, not in your test.

Passing IEnumerable property in RouteValues of ActionLink

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);
}
}

My HtmlHelper is losing Html Encoding

I am trying to use a custom HtmlHelper in my view to display a link, but it's getting HTML Encoded.
In my view, I'm calling my helper like this:
<td>
#Html.Urls(item.TaskUrl)
</td>
And my helper looks like this:
public static class MkpHelpers
{
public static string Urls(this HtmlHelper helper, string value)
{
var items = value.Split(';'); // use your delimiter
var sb = new StringBuilder();
foreach (var i in items)
{
var linkBuilder = new TagBuilder("a");
linkBuilder.MergeAttribute("href",i);
linkBuilder.InnerHtml = i;
sb.Append(linkBuilder.ToString());
}
return sb.ToString();
}
}
Rendered out, it looks like this:
<a href="http://localhost:63595/project/reviewresource/99ddb0d8-238a-e511-8172-00215e466552">
http://localhost:63595/project/reviewresource/99ddb0d8-238a-e511-8172-00215e466552
</a>
I'm guessing I'm doing something wrong that should be pretty simple/obvious.
Be careful here of Injection attacks. That being said, you need to return an HtmlString:
public static class MkpHelpers
{
public static HtmlString Urls(this HtmlHelper helper, string value)
{
var items = value.Split(';'); // use your delimiter
var sb = new StringBuilder();
foreach (var i in items)
{
var linkBuilder = new TagBuilder("a");
linkBuilder.MergeAttribute("href",i);
linkBuilder.InnerHtml = i;
sb.Append(linkBuilder.ToString());
}
return new HtmlString(sb.ToString());
}
}
HtmlString derrives from IHtmlString:
Represents an HTML-encoded string that should not be encoded again.

C# MVC Get Current View/Dynamic Template

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 :-)

Dynamic Razor code execution - how?

In this post I wondered about cleaner code when internationalising an app. that leads to this second query... supposing I wanted to call a function like this:
#Html.RenderWithTags("Help",
new Dictionary<string, string>() { "HelpPage", "#Html.ActionLink(...)" }
)
such that I look up a string in my local resource file containing embedded "tags" e.g. resource name "Help" contains:
We suggest you read our [HelpPage]
before proceeding
and then my .RenderWithTags() method will expand the tags but dynamically executing the code in the dictionary passed e.g. replace [HelpPage] with whatever #Html.ActionLink(...) produces.
I know I can use Microsoft.CSharp.CSharpCodeProvider().CreateCompiler() to compile C# code on the fly, but what about Razor code?
This will be rather difficult to do.
Instead, you should put delegates in your dictionary.
For example:
new Dictionary<string, Func<string>>() {
{ "HelpPage", () => Html.ActionLink(...).ToString() }
}
If you're creating the dictionary in a Razor page, you could also use inline helpers:
new Dictionary<string, Func<Something, HelperResult>>() {
{ "HelpPage", #Html.ActionLink(...) }
}
This will allow to use arbitrary Razor markup in the values.
However, I would probably recommend that you create a single global dictionary in code, so that you don't need to repeat definitions across pages. (Depending on how you use it)
in the end, the solution turned out pretty slick. would not have been possible without SLaks, I'm much obliged for the help (though I didn't end up using inline helpers (but thanks for the intro (they're very cool))).
Now my page contains this:
#{
Dictionary<string, MvcHtmlString> tokenMap = new Dictionary<string, MvcHtmlString>() {
{"HelpPage", Html.ActionLink("help page", "Help", "Home") }
};
}
and somewhere below I have:
#this.Resource("Epilogue", tokenMap)
To accomplish this simplicity:
public static class PageExtensions
{
public static MvcHtmlString Resource(this WebViewPage page, string key)
{
HttpContextBase http = page.ViewContext.HttpContext;
string ret = (string) http.GetLocalResourceObject(page.VirtualPath, key);
return MvcHtmlString.Create(ret);
}
public static MvcHtmlString Resource(
this WebViewPage page, string key,
Dictionary<string, MvcHtmlString> tokenMap
) {
HttpContextBase http = page.ViewContext.HttpContext;
string text = (string) http.GetLocalResourceObject(page.VirtualPath, key);
return new TagReplacer(text, tokenMap).ToMvcHtmlString();
}
}
...and:
public class TagReplacer
{
Dictionary<string, MvcHtmlString> tokenmap;
public string Value { get; set; }
public TagReplacer(string text, Dictionary<string, MvcHtmlString> tokenMap)
{
tokenmap = tokenMap;
Regex re = new Regex(#"\[.*?\]", RegexOptions.IgnoreCase);
Value = re.Replace(text, new MatchEvaluator(this.Replacer));
}
public string Replacer(Match m)
{
return tokenmap[m.Value.RemoveSet("[]")].ToString();
}
public MvcHtmlString ToMvcHtmlString()
{
return MvcHtmlString.Create(Value);
}
}
...with a little extra help:
public static class ObjectExtensions
{
public static string ReplaceSet(this string text, string set, string x)
{
for (int i = 0; i < set.Length; i++)
{
text = text.Replace(set[i].ToString(), x);
}
return text;
}
public static string RemoveSet(this string text, string set)
{
return text.ReplaceSet(set, "");
}
}
comments or feedback on how it could have been better most welcome!

Categories

Resources