How can I add a hash fragment to T4MVC route dictionary ActionResult? - asp.net-mvc-3

I have an extension method that returns an ActionResult (simplified for demonstration purposes):
public static ActionResult GetTestActionResult(this HtmlHelper htmlHelper, int productId)
{
return MVC.Products.Details(productId);
}
I'm using this in an Html.ActionLink:
#Html.ActionLink("Product Details", Html.GetTestActionResult(Model.ProductId), new { #class = "button blue" });
I'm using a custom jQuery plugin for tabs, that uses these hash fragments for navigation. I want to add the tab which I want to open, by tagging the hash fragment onto the end of the URL.
Html.ActionLink does have an overload for the Fragment, namely:
public static MvcHtmlString ActionLink(
this HtmlHelper htmlHelper,
string linkText,
string actionName,
string controllerName,
string protocol,
string hostName,
string fragment,
Object routeValues,
Object htmlAttributes
)
However, that is full of nasty magic strings, which T4MVC is designed to remove. Is there anyway to add the fragment to the route dictionary in my static extension method (GetTestActionResult)?
Something like:
return MVC.Products.Details(productId).AddRouteValue(String.Empty, "#tab-similar-products");
I am aware that there are two similar questions and answers on SO, but they don't quite provide me with what I am looking for. I need to wrap the fragment into the ActionResult BEFORE passing it back to the view:
Including hash values in ASP.NET MVC URL routes
Create a T4MVC ActionLink with url fragment
UPDATE:
Using David Ebbo's fix below, I made the following changes. A bit hacky, but it works:
First I altered my internal function that returns an ActionResult so that it would also add the fragment as a route value (not ideal but works):
return MVC.Products.Details(productId).AddRouteValue("tab", "#tab-similar-products");
Then in the view it copies that fragment value out of the route dictionary, then removes that route value for completeness.
// get my ActionResult with the tab fragment tagged on as a route value
var actionResult = Html.GetTestActionResult(item.Key, Model.ClaimId);
// get the tab fragment value
var tabRoute = actionResult.GetRouteValueDictionary().FirstOrDefault(r => r.Key == "tab").Value ?? "none";
// remove the route value, otherwise it will get tagged to the querystring
actionResult.GetRouteValueDictionary().Remove("tab");
// display
#Html.ActionLink("Product Details", Html.GetTestActionResult(Model.ProductId), new { #class = "button blue" }, fragment: tabRoute.ToString());
I'm sure there is a prettier way to return the fragment with the ActionResult, but for the moment this works. Thanks David.

T4MVC needs new overloads to handle this. In T4MVC.tt, try changing:
public static <#=HtmlStringType #> ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, object htmlAttributes) {
return ActionLink(htmlHelper, linkText, result, new RouteValueDictionary(htmlAttributes));
}
public static <#=HtmlStringType #> ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, IDictionary<string, object> htmlAttributes) {
return htmlHelper.RouteLink(linkText, result.GetRouteValueDictionary(), htmlAttributes);
}
to
public static <#=HtmlStringType #> ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, object htmlAttributes, string protocol = null, string hostName = null, string fragment = null) {
return ActionLink(htmlHelper, linkText, result, new RouteValueDictionary(htmlAttributes), protocol, hostName, fragment);
}
public static <#=HtmlStringType #> ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, IDictionary<string, object> htmlAttributes, string protocol = null, string hostName = null, string fragment = null) {
return htmlHelper.RouteLink(linkText, null, protocol, hostName, fragment, result.GetRouteValueDictionary(), htmlAttributes);
}
you'll then be able to write something like:
#Html.ActionLink("Product Details", Html.GetTestActionResult(Model.ProductId), new { #class = "button blue" }, fragment: "#tab-similar-products")
Let me know if that works, and I'll try to get it added to the main template.

Related

Passing parameter from view to controller when button is clicked

I have created my own extension as:
public static MvcHtmlString hSearch(this HtmlHelper helper, string labelName, string labelCaption, string textName, string textValue, string tableName, string buttonId,
string actionName, string controllerName, object routeValues, object htmlAttributes)
{
var textbuilder = new TagBuilder("input");
textbuilder.MergeAttribute("id", textName);
textbuilder.MergeAttribute("name", textName);
textbuilder.MergeAttribute("value", textValue);
textbuilder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
ModelMetadata metadata = ModelMetadata.FromStringExpression(labelName, helper.ViewData);
String innerText = labelCaption ?? (metadata.DisplayName ?? (metadata.PropertyName ?? labelName.Split('.').Last()));
if (String.IsNullOrEmpty(innerText))
{
return MvcHtmlString.Empty;
}
TagBuilder labelbuilder = new TagBuilder("label");
labelbuilder.Attributes.Add("for", TagBuilder.CreateSanitizedId(helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(labelName)));
labelbuilder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
labelbuilder.SetInnerText(innerText);
//return new MvcHtmlString(textbuilder.ToString());
var buttonBuilder = new TagBuilder("button");
buttonBuilder.MergeAttribute("id", buttonId);
buttonBuilder.SetInnerText(buttonId);
var formBuilder = new TagBuilder("form");
var urlHelper = new UrlHelper(helper.ViewContext.RequestContext);
formBuilder.Attributes.Add("action", urlHelper.Action(actionName, controllerName, routeValues));
formBuilder.Attributes.Add("method", "Post");
formBuilder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
formBuilder.InnerHtml = labelbuilder.ToString() + textbuilder.ToString() + buttonBuilder.ToString();
return new MvcHtmlString(formBuilder.ToString());
}
I used the extensions in view as:
#Html.hSearch("lblSrch", "Company", "companyName", (string)TempData["cName"], "CHComp", "Search", "Fetch", "Home", null, null)
Now I want to pass tableName when I click the button to the controller.. my controller looks like this:
public ActionResult Fetch(string search, string tablename)
{
var c = cbo.fetchData(search, tablename);
return PartialView(c.ToList());
}
Waiting for reply.. Thanks..
You haven't given us the code for your helper, but at a guess it writes out a label, a text field (textName), and a button. If this is the case, it will post / get companyName=someValue via HTTP back to your controller.
You would typically need to add a FormCollection to your controller if the fields are dynamically sent from the view. Alternatively, why not keep the name of the text search input static, e.g. name="search", which will bind to your controller's parameter of the same name.
Edit
You can pass tableName back to the controller in a hidden field (<input type='hidden' name='tableName' value='{tableNameGoesHere}')
But as per above, your search string will have different names- the model binder isn't going to recognise it as string search.

How to return #Html.ActionLink

I am writing a Html Helper for my MVC 3 project.
I want to return the MvcHtmlString like "#Html.ActionLink(xxxxx)", what should I write?
Currently I have this code
public static MvcHtmlString SetFeaturedFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper,Expression<Func<TModel, TValue>> expression)
{
var isFeatured =Convert.ToBoolean(ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model.ToString());
string result = "Html.ActionLink(Delete, DeleteComment, Admin, new { Id = #thisComment.CommentId }, null)";
return MvcHtmlString.Create(result);
}
It return the whole string.... but I want the rendered string. So what should I do? Thanks everyone.
UPDATE
Looks like I can return this directly
See below code
public static MvcHtmlString SetFeaturedFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper,Expression<Func<TModel, TValue>> expression)
{
var isFeatured =Convert.ToBoolean(ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model.ToString());
string indicatorText = (isFeatured) ? "Unset Featured" : "Set Featured";
return htmlHelper.ActionLink(indicatorText, "SetFeaturedIncident", "Admin", null, null);
}
Need to import System.Web.Routing namespace.
Remove the quotes (you want to call the function, not just store the code in a string) and the # (that's Razor, not C# anyways). You might need to change Html to whatever you called the helper parameter in your (presumably) extension method.
Also, Html.ActionLink already returns MvcHtmlString so you can just put it directly after return.

T4MVC creates incorrect URL for ActionLink when using Areas

In my project I'm trying to create an URL in a view inside an area.
The code is this:
#Html.ActionLink("Cancel", MVC.MyArea.MyController.Index(), new { #class = "btn" })
When I view the result in the browser, the generated code is the following:
<a class="btn" href="/MyArea/MyController/MyCurrentAction?Count=3&Keys=System.Collections.Generic.Dictionary%602%2BKeyCollection%5BSystem.String%2CSystem.Object%5D&Values=System.Collections.Generic.Dictionary%602%2BValueCollection%5BSystem.String%2CSystem.Object%5D">Cancel</a>
I went looking for the code in T4MVC and found that this was the method generating the code above (inside the T4Extensions class):
public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, object htmlAttributes, string protocol = null, string hostName = null, string fragment = null) {
return htmlHelper.RouteLink(linkText, null, protocol, hostName, fragment, result.GetRouteValueDictionary(), htmlAttributes);
}
Apparently, the RouteLink method is not being able to use the result.GetRouteValueDictionary() as it should.
So, I checked the ASP.NET MVC source code and tried to replicate the same functionality. I've changed my T4MVC to this:
public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, object htmlAttributes, string protocol = null, string hostName = null, string fragment = null) {
var t4mvcResult = result.GetT4MVCResult();
var actionName = t4mvcResult.Action;
var controllerName = t4mvcResult.Controller;
var routeValues = new RouteValueDictionary(t4mvcResult.RouteValueDictionary);
var htmlAttribs = new RouteValueDictionary(htmlAttributes);
return new MvcHtmlString(HtmlHelper.GenerateLink(htmlHelper.ViewContext.RequestContext, htmlHelper.RouteCollection, linkText, null /* routeName */, actionName, controllerName, protocol, hostName, fragment, routeValues, htmlAttribs));
}
Now it's working (which is great, of course), at least for the tests I've made, but I'm afraid that I'm doing something wrong in the first place and that this path can lead me into some problems ahead.
Darn, this looks like a regression in 2.6.69. Can you try 2.6.68 to verify that indeed it worked before? I have hidden 2.6.69 for now so others don't get it automatically in new projects (and when updating).
This was the bug that triggered the bad fix: http://mvccontrib.codeplex.com/workitem/7191
Also, could you try the exact fix mentioned in the last comment in that bug? Change the method to:
public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, object htmlAttributes, string protocol = null, string hostName = null, string fragment = null)
{
return htmlHelper.RouteLink(linkText, null, protocol, hostName, fragment, result.GetRouteValueDictionary(), HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
Sorry about the break!

MVC3 - specifying htmlAttributes parameter to Html.ActionLink causes routevalues to be rendered incorrectly

I have this snippet of code in a Html helper extension
routeValues["Page"] = info.PageIndex - 1;
builder.Append("<li>")
.Append(LinkExtensions.ActionLink(html, "<<", actionName, routeValues, new { #class = "pageMore" }))
.Append("</li>");
he overloaded version of the ActionLink method I wish to use is this:
public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper,
string linkText, string actionName, object routeValues, object htmlAttributes);
The output of the link, however is:
http://localhost/Admin/Group/Details/61?Count=2&Keys=System.Collections.Generic.Dictionary%602%2BKeyCollection[System.String%2CSystem.Object]&Values=System.Collections.Generic.Dictionary%602%2BValueCollection[System.String%2CSystem.Object]
Somehow when I specify the htmlAttributes, the routevalues are serialized.
What should I do to get the right output?

ASP.NET MVC - Extending TextBoxFor without re-writing the method

Is there any possible way to extend the basic html helpers (TextBoxFor, TextAreaFor, etc) using extension methods on their output, instead of just re-writing the entire methods completely? For instance, adding in ...
#Html.TextBoxFor( model => model.Name ).Identity("idName")
I know I can achieve this using the following, already..
#Html.TextBoxFor( model => model.Name, new { #id = "idName" })
But that gets clunky and frustrating to manage when you have to start adding a lot of properties. Is there any way to add extensions to these inherently without just passing in htmlAttributes for every little detail?
As #AaronShockley says, because TextBoxFor() returns an MvcHtmlString, your only option for developing a 'fluid API' style of amending the output would be to operate on the MvcHtmlStrings returned by the helper methods. A slightly different way of doing this which I think approaches what you're after would be to use a 'property builder' object, like this:
public class MvcInputBuilder
{
public int Id { get; set; }
public string Class { get; set; }
}
...and to set up extension methods like this:
public static MvcHtmlString TextBoxFor<TModel, TProp>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProp>> expression,
params Action<MvcInputBuilder>[] propertySetters)
{
MvcInputBuilder builder = new MvcInputBuilder();
foreach (var propertySetter in propertySetters)
{
propertySetter.Invoke(builder);
}
var properties = new RouteValueDictionary(builder)
.Select(kvp => kvp)
.Where(kvp => kvp.Value != null)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
return htmlHelper.TextBoxFor(expression, properties);
}
You can then do stuff like this in your View:
#this.Html.TextBoxFor(
model => model.Name,
p => p.Id = 7,
p => p.Class = "my-class")
This gives you strong typing and intellisense for input properties, which you could customise for each extension method by adding properties to an appropriate MvcInputBuilder subclass.
All of the basic html helpers return an object of type System.Web.Mvc.MvcHtmlString. You can set up extension methods for that class. Here is an example:
public static class MvcHtmlStringExtensions
{
public static MvcHtmlString If(this MvcHtmlString value, bool check)
{
if (check)
{
return value;
}
return null;
}
public static MvcHtmlString Else(this MvcHtmlString value, MvcHtmlString alternate)
{
if (value == null)
{
return alternate;
}
return value;
}
}
Then you can use these in a view like:
#Html.TextBoxFor(model => model.Name)
.If(Model.Name.StartsWith("A"))
.Else(Html.TextBoxFor(model => model.LastName)
To make extension methods that modify attributes on the rendered HTML tag, you'll have to convert the result to a string, and find and replace the value you're looking for.
using System.Text.RegularExpressions;
public static MvcHtmlString Identity(this MvcHtmlString value, string id)
{
string input = value.ToString();
string pattern = #"(?<=\bid=")[^"]*";
string newValue = Regex.Replace(input, pattern, id);
return new MvcHtmlString(newValue);
}
public static MvcHtmlString Name(this MvcHtmlString value, string id)
{
string input = value.ToString();
string pattern = #"(?<=\bname=")[^"]*";
string newValue = Regex.Replace(input, pattern, id);
return new MvcHtmlString(newValue);
}
The id and name attributes are always added by the html helpers, but if you want to work with attributes that may not be there (and you'll have to add them instead of just replacing them), you'll need to modify the code.

Resources