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.
Related
I'm trying to refine my HTML extension method, so that I can bind both the ID and Value components to work something like this:
#Html.AutoCompleteFor(model => model.Customer.ID, model => model.Customer.Name, "/Customer/Autocomplete")
I currently do it like this:
#Html.AutoCompleteFor(model => model.CustomerID, model => model.CustomerID_display, "/Customer/Autocomplete")
Where I need to extend the model to include CustomerID_display, which is a bit clunky, and requires very specific post processing.
The reason I need to bind on the display value (which is the text name of the entity), is so that if the user enters a new item, it can be detected and potentially auto-generated.
My expected method above doesn't work of course, and I might be way off the mark. But you should know effectively what I'm trying to do. If my expected method above worked, my implementation of AutoCompleteFor would be quite simple (everyone is using the word trivial these days!). I'm expecting that lambda can be a bit more useful, perhaps:
#Html.AutoCompleteFor(model => new AutoCompleteBinding { ID = model.Customer.ID, Name = model => model.Customer.Name }, "/Customer/Autocomplete")
Thanks!
In your HTML "...For" extensions, you normally only have a single Expression parameter.
public static MvcHtmlString AutoCompleteFor<TModel, TValue>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TValue>> expression,
object postingTextBoxHtmlAttributes,
string extraArguments,
string sourceURL
)
{
//...
}
Simply expand this, to include another expression. Adding:
Expression<Func<TModel, TValue>> expression
To make:
public static MvcHtmlString AutoCompleteFor<TModel, TValue>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TValue>> expression,
Expression<Func<TModel, TValue>> expression2,
object postingTextBoxHtmlAttributes,
string extraArguments,
string sourceURL
)
{
//...
}
You can now use expression2, as you would normally with expression. (Of course you should name it something intuitive).
Usage will be as described in the question:
#Html.AutoCompleteFor(model => model.Customer.ID, model => model.Customer.Name, "/Customer/Autocomplete")
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!
Where are the default Razor Editor and Display templates (eg String.cshtml, DateTime.cshtml) located when one installs Asp.Net MVC 3?
If you have DotPeek or Reflector you can look up the type DefaultDisplayTemplates in there you will find the templates. But be warned they are in Code format, not WebForm or razor format, so a bit more difficult to interpret.
StringTemplate
internal static string StringTemplate(HtmlHelper html)
{
return html.Encode(html.ViewContext.ViewData.TemplateInfo.FormattedModelValue);
}
(There was no default DateTime template that I could find)
DecimalTemplate
internal static string DecimalTemplate(HtmlHelper html)
{
if (html.ViewContext.ViewData.TemplateInfo.FormattedModelValue == html.ViewContext.ViewData.ModelMetadata.Model)
html.ViewContext.ViewData.TemplateInfo.FormattedModelValue = (object) string.Format((IFormatProvider) CultureInfo.CurrentCulture, "{0:0.00}", new object[1]
{
html.ViewContext.ViewData.ModelMetadata.Model
});
return DefaultDisplayTemplates.StringTemplate(html);
}
There are no default templates.Razor Editor and Display methods are extension methods of the class HtmlHelper. You can use them or you can develope your own extension methods, like this example.
public static MvcHtmlString MyTextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression)
{
MvcHtmlString result = InputExtensions.TextBoxFor(helper, expression);
// do modification to result
return result;
}
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.
I'm using MVC3 w/ Razor and I have a model that has quite a few properties that are sometimes empty.
Other than a custom htmlHelper, or using an if/then in the view for every LabelFor/DisplayFor pair, is there a way to not display the LabelFor/DisplayFor for a property that is empty or null?
No.... You need the above mentioned solutions or additional view models. Sorry!
I created my own helper: LabelAndDisplayFor that checks for null/empty and then chooses to display the field.
public static MvcHtmlString LabelAndDisplayFor<tModel, tValue>(this HtmlHelper<tModel> html, System.Linq.Expressions.Expression<Func<tModel, tValue>> field,
bool hideIfEmpty = false) {
if (hideIfEmpty) {
var v = field.Compile()(html.ViewData.Model);
if (v == null || string.IsNullOrWhiteSpace(v.ToString())) {
return MvcHtmlString.Empty;
}
}
StringBuilder result = new StringBuilder();
result.Append("<div class='display-line'>");
result.Append("<div class='display-label'>");
result.Append(html.LabelFor(field));
result.Append("</div>");
// ... etc ...