HtmlHelper in ASP.NET MVC3 that uses Action<T> as template: Razor syntax? - asp.net-mvc-3

Let me preface this by saying that perhaps there's a better way to do this, and Razor is lighting the way. In any case, I have an HTML helper that acts as a repeater of sorts, but after an arbitrary number of repeats, it inserts an alternate template. Most obvious use? Tables that start a new row after x cells. The helper looks like this:
public static void SeriesSplitter<T>(this System.Web.Mvc.HtmlHelper htmlHelper, IEnumerable<T> items, int itemsBeforeSplit, Action<T> template, Action seriesSplitter)
{
if (items == null)
return;
var i = 0;
foreach (var item in items)
{
if (i != 0 && i % itemsBeforeSplit == 0)
seriesSplitter();
template(item);
i++;
}
}
And in a Webforms view, the usage looks like this:
<table>
<tr>
<% Html.SeriesSplitter(Model.Photos, 4, photo => { %>
<td><img src="<%=ResolveUrl("~/Thumbnail.ashx?id=" + photo.ID)%>" alt="<%=Html.Encode(photo.Title)%>" /></td>
<% }, () => { %></tr><tr><% }); %>
</tr>
</table>
In this case, you'd have a table that renders four cells, then starts a new row by using the alternate template (the row start and end tags). The problem is that I can't find a way to make this work in Razor. Using a lambda inside of a Razor view seems like a pretty weird construct.
What would you do?

Phil actually had a good post that solves the problem here:
http://haacked.com/archive/2011/02/27/templated-razor-delegates.aspx

This is a job for Func<object, HelperResult> (though I'm doing this from memory so it might need tweaking):
public static void SeriesSplitter<T>(this System.Web.Mvc.HtmlHelper htmlHelper,
IEnumerable<T> items,
int itemsBeforeSplit,
Func<object, HelperResult> template,
Func<object, HelperResult> seriesSplitter) {
//pretty much same code here as before
}
Then you would call it like so
#Html.SeriesSplitter(Model.Items, 3, #<td>item.Id</td>, #:</td><td>
)
Just note that because of how Razor does tag balancing during parsing you need the new line after #:</td><td>

Related

Can you use a #Helper inside an #Helper?

I am not sure this is possible.
I have a bunch of #Helper's inside a view AND in other views:
#helper ViewHelper1()
{
...
}
#helper ViewHelper2()
{
...
}
etc.
I have repetitive code that is used in the view AND in other views:
#if (!(Model.Entity == Model.Enum.One))
{
<td>
#ViewHelper1()
</td>
}
else
{
<td>
#ViewHelper1()
</td>
<td>
#ViewHelper1()
</td>
}
The actual #ViewHelper1 has more complex code, but that's not important (I think).
Well, since each view has a number of #Helper's (30+ views, 10-15 #Helper's each) and the <table> structure is the same, I was wondering how to go about creating a #Helper in App_Code that encapsulates the <td> structure and then would pass the view's #Helper.
Say:
#helper Table(...)
{
...
}
Or whether or not that's even possible and then call it in the view like:
#Table(HelperView1)
If it is I just needed help with the syntax.
As always, much appreciated.
The generated razor helpers are just functions with the return type HelperResult.
You can have delegates which returns HelperResult as parameters in your main helper and call them at the appropriate places.
A small sample to get you started:
#helper View1()
{
<h1>View1</h1>
}
#helper View2()
{
<h2>View2</h2>
}
#helper Table(Func<HelperResult> viewHelper)
{
<text>Reuslt of viewHelper</text>
#viewHelper()
}
#Table(View1)
#Table(View2)
The generated output:
Reuslt of viewHelper
<h1>View1</h1>
Reuslt of viewHelper
<h2>View2</h2>

How to make Html.DisplayFor display line breaks?

Embarrassingly newbie question:
I have a string field in my model that contains line breaks.
#Html.DisplayFor(x => x.MultiLineText)
does not display the line breaks.
Obviously I could do some fiddling in the model and create another field that replaces \n with <br/>, but that seems kludgy. What's the textbook way to make this work?
A HtmlHelper extension method to display string values with line breaks:
public static MvcHtmlString DisplayWithBreaksFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
var model = html.Encode(metadata.Model).Replace("\r\n", "<br />\r\n");
if (String.IsNullOrEmpty(model))
return MvcHtmlString.Empty;
return MvcHtmlString.Create(model);
}
And then you can use the following syntax:
#Html.DisplayWithBreaksFor(m => m.MultiLineField)
i recommend formatting the output with css instead of using cpu consuming server side strings manipulation like .replace,
just add this style property to render multiline texts :
.multiline
{
white-space: pre-wrap;
}
then
<div class="multiline">
my
multiline
text
</div>
newlines will render like br elements, test it here https://refork.codicode.com/xaf4
In your view, you can try something like
#Html.Raw(Html.Encode(Model.MultiLineText).Replace("\n", "<br />"))
The display template is probably the best solution but there is another easy option of using an html helper if you know you're just displaying a string, e.g.:
namespace Shaul.Web.Helpers
{
public static class HtmlHelpers
{
public static IHtmlString ReplaceBreaks(this HtmlHelper helper, string str)
{
return MvcHtmlString.Create(str.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None).Aggregate((a, b) => a + "<br />" + b));
}
}
}
And then you'd use it like:
#using Shaul.Web.Helpers
#Html.ReplaceBreaks(Model.MultiLineText)
You create a display template for your data.
Here's a post detailing how to do it.
How do I create a MVC Razor template for DisplayFor()
In that template you do the actual translating of newlines into and whatever other work needs to be done for presentation.
Inspired by DisplayTemplates for common DataTypes,
I override (introduce?) a default DisplayTemplate for DataType.MultilineText, /Views/Shared/DisplayTemplates/MultilineText.cshtml containing just this line:
<span style="white-space: pre-wrap">#this.Model</span>
(Of course you could replace this style, by a css-class, or replace newlines inside the view, if you prefer that.)
I guess this template is automatically resolved, because I had no need for UIHint or any other reference or registration.
Using the DisplayTemplate instead of introducing a HtmlHelper-method has the advantage, that it trickles down to properties and views that are not explicitly defined.
E.g. DisplayFor(MyClassWithMultilineProperties) will now also correctly display MyClassWithMultilineProperties.MyMultilineTextProperty, if the property was annotated with [DataType(DataType.MultilineText)].
Try using
#Html.Raw("<p>" + Html.LabelFor(x => x.Name) + "</p>")
Here's another extension method option.
public static IHtmlString DisplayFormattedFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, string>> expression)
{
string value = Convert.ToString(ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model);
if (string.IsNullOrWhiteSpace(value))
{
return MvcHtmlString.Empty;
}
value = string.Join("<br/>", value.Split(new[] { Environment.NewLine }, StringSplitOptions.None).Select(HttpUtility.HtmlEncode));
return new HtmlString(value);
}
I had this problem with ASP.NET Core 6. The previous answers here did not work with a linq expression in Html.DisplayFor. Instead I was constantly getting the <br/> tag escaped out in the output HTML. Trying HtmlString helper methods suggestions did not work.
The following solution was discovered through trial and error. The InfoString had CRLF replaced with the <br/> tags as shown in the property code.
Works
#Html.Raw(#Convert.ToString(item.InfoString))
Did not work
#Html.DisplayFor(modelItem => item.InfoString)
FYI - my Info String property:
public string InfoString
{
get { return MyInfo.Replace(Environment.NewLine,"<br />"); }
}

Adding HtmlAttributes to template

If I am passing HtmlAttributes into a template, like this:
#Html.DisplayFor(m => m.FirstName, new { htmlAttributes = new { #class = "orangetxt strongtxt" } })
In my template, how would I inject these into my HTML:
<span #ViewData["htmlAttributes"]>#Model</span>
This almost works, but it does some pretty weird stuff, so I'm assuming this isn't the way to go.
I realize I can accomplish this with an HtmlHelper extension method to render the full HTML element (span, in this case) and pass in the attributes that way, but is there a way to just render attributes straight into an HTML element, like the above example?
The below extension method will allow me to convert HtmlAttributes to a string:
public static MvcHtmlString RenderHtmlAttributes<TModel>(
this HtmlHelper<TModel> htmlHelper, object htmlAttributes)
{
var attrbituesDictionary = new RouteValueDictionary(htmlAttributes);
return MvcHtmlString.Create(String.Join(" ",
attrbituesDictionary.Select(
item => String.Format("{0}=\"{1}\"", item.Key,
htmlHelper.Encode(item.Value)))));
}
Then, to render them within the tag, I can just do this:
<span #Html.RenderHtmlAttributes(ViewData["htmlAttributes"])>#Model</span>
Jerad Rose's answer is good, but I ran into couple of issues with it:
It does not not convert underscores to dashes in attribute names
It does not handle no-value attributes gracefully
To address first issue, use HtmlHelper.AnonymousObjectToHtmlAttributes.
Below is my modification of Jerad's method:
public static MvcHtmlString RenderHtmlAttributes(this HtmlHelper helper, object htmlAttributes)
{
if (htmlAttributes == null) return new MvcHtmlString(String.Empty);
var attrbituesDictionary = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
return new MvcHtmlString(String.Join(" ", attrbituesDictionary.Select(item => string.IsNullOrEmpty((string)item.Value) ? String.Format("{0}", item.Key) : String.Format("{0}=\"{1}\"", item.Key, helper.Encode(item.Value)))));
}
Try this instead,
#Html.DisplayFor(m => m.FirstName,
new { htmlAttributes = "class = orangetxt strongtxt"})
This will render a string, whereas your version did do weird stuff, rendered { } as part of the output.
DisplayFor() is used to render the template that matches the property type.
Display templates are .cshtml files inside /DisplayTemplates folder which in turn is inside a view folder (i.e. any folder from Home, Shared or even a specific controller).
An example.
If you've a String.cshtml template like this inside /Views/Shared:
#model String
#if (string.IsNullOrEmpty(Model)) {
<span>(no string)</span>
}
else {
<span>#Model</span>
}
Every time you call DisplayFor() for a string property:
DisplayFor(model => model.MyStringProperty);
It renders the template accordingly to the string's value. You can be more specific and put /DisplayTemplates inside a specific View folder and them only calls from those views are affected by the template.
In your case you can be even more specific and call DisplayFor() with a particular template.
Suppose you've a template for a particular property, called MyPropertyTemplate.cshtml. You would call DisplayFor() like this:
DisplayFor(model => model.MyProperty, "MyPropertyTemplate");
And them, inside that template you can have whatever HTML attributes you want.
#model MyProperty
<span class="orangetxt strongtxt">#MyProperty.ToString()</span>
PS: When it doesn't find a template I guess it only calls model.Property.ToString() without additional html.
FYI: EditorFor(), for example, works in a similar way but it uses /EditorTemplates folder.

MVC 2 - Use HtmlHelper.BeginForm inside method that creates a dynamic form

I have a method like this:
public MvcHtmlString RenderStuff(HtmlHelper<TModel> htmlHelper)
{
TagBuilder div = new TagBuilder("div");
//Dynamically add elements to form e.g.
div.InnerHtml += System.Web.Mvc.Html.InputExtensions.TextBox(htmlHelper, "MyTextBox").ToHtmlString();
return MvcHtmlString.Create(div.ToString());
}
It all works correctly but I can't work out how I can add a form around it within the method - that is to say that at this point in time I am adding the form in the view like so:
<% using (Html.BeginForm()) { %>
<%=(ViewData["MyStuff"] as MyStuff).RenderStuff(Html)%>
<% } %>
I guess I am trying to do something like this but I am thinking that I will need to somehow render the form to the div and then append the textbox to the form...:
using (htmlHelper.BeginForm())
{
div.InnerHtml += div.InnerHtml += System.Web.Mvc.Html.InputExtensions.TextBox(htmlHelper, "MyTextBox").ToHtmlString();
}
Please don't suggest using TagBuilder("form") and then appending the items to that - I am using the MVC Client Validation and it seems to require using Html.BeginForm - If I can't figure out a way to do this I will leave the form in my view rather than spend time trying to hack the validation to make it work as it is working very nicely at the moment.
Any suggestion greatly appreciated, thanks in advance :-)
Cheers
Rob
I slept on this and had some clarity in the morning here is the answer for anyone who want's to do this in the future at all:
You can't return a string and then render it from the View - return void and render it in the method so that as you are rendering the htmlHelper ViewContext is current/relevant:
public void RenderStuff(HtmlHelper<TModel> htmlHelper)
{
TagBuilder div = new TagBuilder("div");
using(htmlHelper.BeginForm()){
//Dynamically add elements to form e.g.
div.InnerHtml += System.Web.Mvc.Html.InputExtensions.TextBox(htmlHelper, "MyTextBox").ToHtmlString();
htmlHelper.ViewContext.HttpContext.Response.Write(div.ToString());
}
}

Syntax for ASP.Net MVC Phil Haack's Repeater syntax using Razor (MVC 3)?

I have recently started using ASP.Net MVC 3 RC 2 and have attempted to migrate an existing website in MVC 2 across using the Razor syntax. In the MVC 2 application I am using the code base repeater that Phil Haack kindly provided in the following:
Phil Haack's Code Based Repeater
My question is around the syntax for Razor. I dont understand how the template in the following block can be rewritten in Razor and cannot find any documentation to help out (early days for documentation or my simplicity...):
<% Html.Repeater<ObjectToUse>(Model, "", "alt", (item, css) =>
{ %>
<tr class="<%= item.Enabled ? css : "disabled" %>">
<td><%= item.Name%></td>
<td><%= item.Description%></td>
<td><%= Html.RouteLink("Edit", item.ObjectToUseRouteValues("Edit"))%></td>
<td></td>
<td><%= Html.RouteLink("Select", item.ObjectToUseRouteValues())%></td>
</tr>
<% }); %>
The problem comes when applying the template between the braces (the tr's). I have attempted using the WebGrid control, however it doesnt provide the functionality I require for setting a "disabled" row (I think).
I wrote #helper version.
#helper do not use Generic method.
#helper ForEach(IEnumerable<int> items, Func<object, HelperResult> template){
foreach(var item in items){
Write(template(item));
}
}
<div>
<ul>
#ForEach(Enumerable.Range(1,5),
#<li>#item</li>
)
</ul>
</div>
hope this code.
Actually, now that I think about it some more I don't think you can use Action parameters like that in Razor. I recall running into this before.
Updated
With answer from Andrew Nurse:
"Unfortunately this is by design in the current parser, though I should note that we'd like to improve upon it. The issue is that markup is only valid at the start of a statement (which technically is where you've put it), but our C# "parser" is not intelligent enough to detect lambdas at the moment."
Though that might be out dated :)
#Html.Repeater(Model, "row", "row-alt",
#<tr class="#item.ClassType : "disabled"">
<td>#item.Name</td>
<td>#item.Description</td>
<td>#Html.RouteLink("Edit", item.ObjectToUseRouteValues("Edit"))</td>
<td></td>
<td>#Html.RouteLink("Select", item.ObjectToUseRouteValues())</td>
</tr>
)
public static IHtmlString Repeater<T>(this HtmlHelper html, IEnumerable<T> items,
string className, string classNameAlt, Func<T, HelperResult> render) {
if (items == null)
return new HtmlString("");
int i = 0;
StringBuilder sb = new StringBuilder();
foreach (var item in items) {
item.ClassType = item.Enabled ? (i++ % 2 == 0 ? className : classNameAlt) : "disabled";
sb.Append(render(item).ToHtmlString());
}
return new HtmlString(sb.ToString());
}
}

Resources