MVC 4 - How to pass a template to an html helper method - c#

I need to have all my scripts at the bottom of the page, problem is when I have a Partial View I cannot use the "RenderSection" approach. Found a great example of how to add a HtmlHelper extension which takes the name of a script file, loads into a stack, then another helper renders that out on the base layout:
Razor section inclusions from partial view
That's great - but I don't want to have to create an entire JS file for a little chunk of script, or maybe even HTML, that I want to drop in. And I don't want to pass it all as a string, I want the nice formatting and intellisense, so I want to use a template ie:
#{Html.AddScript("test", #<text>
<script type="text/javascript">
function RefreshPreview() {
$('#AutoReplyHtml_Preview').html(
$('#htmlTemplate').html()
.replace('##MESSAGE_TITLE##', $('#AutoReplySubject').val())
.replace('##PRE_HEADER##', $('#AutoReplyPreHeader').val())
.replace('##MESSAGE_BODY##', $('#AutoReplyHtml').val())
);
$('#AutoReplyPlainText_Preview').html(
$('#plainTextTemplate').html()
.replace('##MESSAGE_BODY##', $('#AutoReplyPlainText').val())
);
}
$(document).ready(function() {
RefreshPreview();
});
</script>
</text>);}
Problem is - how to I get the value of the template into my method, I have this code which complies, but no clue how to get the data out of the "code" parameter:
public static string AddScript(this HtmlHelper htmlHelper, string title, Func<object, object> code) {
var ctx = htmlHelper.ViewContext.HttpContext;
Dictionary<string, string> scripts = ctx.Items["HtmlHelper.AddScript"] as Dictionary<string, string>;
if (scripts == null) {
scripts = new Dictionary<string, string>();
ctx.Items.Add("HtmlHelper.AddScript", scripts);
}
scripts.Add(title, code.ToString()); //Doens't work!
return string.Empty;
}
How do I need to tweak the delegate parameter to get the value inside the template??

The helper architecture is designed so that it can accomodate scenarios where you are providing a template that will operate, for example, on each item in a list. In such a scenario, you'd of course want to be able to pass it the "current" item when iterating through the list.
However, in other scenarios (such as yours), there is no current item. Yet, as you've discovered, you still have to declare a delegate as a parameter to your method that defines a method that consumes one parameter. That's ok -- since you're not consuming that argument in your helper (you don't make use of the somewhat magical item parameter in your template) you can just pass it null in your implementation. Preferably, declare your parameter as a Func<object, IHtmlString> rather than a Func<object, object>, but regardless, just invoke code(null).ToString() to get the HTML-encoded string you need to render.

Related

Creating link with html content

I would like to create some HTML helper functions to create some links with generated HTML content. I would follow the default API as much as possible. This gets tricky when I want to pass in a routevalues object. A routevalue object of type RouteValueDictionary is (intentionally?) cumbersome to create in MVC. I would like to pass in an object routevalues as is done with i.e. Html.ActionLink. The tricky part is that I seem to need UrlHelper.CreateUrl which requires a RouteValueDictionary. I checked how ActionLink does this internally, and it uses TypeHelper.ObjectToDictionary. TypeHelper however is an internal class, so I can't access that. I could copy-paste the thing in, but - apart from that firstly i'd be violating the license if I do that and don't license under the Apache 2.0 license or compatible, and secondly copy-paste programming gives me the heeby-jeebies.
The following is what I'd roughly like to do:
public static MvcHtmlString MyFancyActionLink(this HtmlHelper helper,
Foo foo,
object routevalues){
TagBuilder inner = fancyFooContent(foo);
RouteValueDictionary routedict = TypeHelper.ObjectToDictionary(routevalues);
//alas! TypeHelper is internal!
string url = UrlHelper.GenerateUrl(null,
"myaction",
"mycontroller",
routedict,
helper.ViewContext.RequestContext,
true);
TagBuilder link = new TagBuilder("a");
link.MergeAttribute("href", url);
link.InnerHtml = inner.toString();
return MvcHtmlString.Create(link.ToString());
}
RouteValueDictionary has a constructor that accepts an object and uses its properties to populate the dictionary. Unless I am missing something obvious here, you should be able to use that:
RouteValueDictionary routedict = new RouteValueDictionary(routevalues);

How to pass in a lambda to a Razor helper method?

I have a razor helper method that needs to take in a Func<> that will return some HTML content to print out. This is what I originally had:
#helper node(string title, Func<HelperResult> descriptions)
{
....
<div>#descriptions()</div>
....
}
#node("title",
new Func<HelperResult>(() =>
{
return new HelperResult(
#<text>
<span>"desc1"</span>
<span>"desc2"</span>
</text>);
}))
Unfortunately with this my text never gets printed out. No error either.
So I learned about inline helpers, and changed the calling method to this:
#node("title",
#<text>
<span>"desc1"</span>
<span>"desc2"</span>
</text>)
However now I get a compilation error saying
"Delegate 'System.Func' does not
take 1 arguments".
But I'm not passing in any arguments.
So if I change it to Func<object,HelperResult> and then call it using #descriptions(null) I get the following error:
"Cannot use a lambda expression as an argument to a dynamically
dispatched operation without first casting it to a delegate or
expression tree type"
I'm sure I have something wrong somewhere, but I'm not sure what it is.
Edit: I think I may have solved that problem but it introduces some other issues.
What I did was to cast the lambda before passing into a dynamic method. I guess that's what the error was trying to say:
#node("title",
((Func<dynamic, HelperResult>)(#<text>
<span>"desc1"</span>
<span>"desc2"</span>
</text>))
That works and it prints out the span tags correctly. Unfortunately I have to pass in a useless parameter when calling this Func.
Now the issue I have is that my real function does a bit more than just write some spans. It's more like this:
#node("title",
((Func<dynamic, HelperResult>)(#<text>
<span>#Helpers.Format(resource.Description,"item")</span>
</text>))
Where #Helpers.Format is another helper and resource is a (dynamic) variable from the page model.
Of course now the code runs but nothing is printed out (inside the <span> tag). I put a breakpoint inside my Format helper function, and it hits it and all the parameters are correctly set, so I'm not sure why it wouldn't output correctly. Similarly if I just change it to
resource.Description
then nothing still gets output.
Since it works well outside of this context, I wonder does Razor's inline helpers not capture the outer variables?
Actually HelperResult is something Microsoft would rather you didn't use, as evidenced by documentation:
public class HelperResult : IHtmlString in namespace
System.Web.WebPages
Summary: This type/member supports the .NET Framework infrastructure
and is not intended to be used directly from your code.
A possible solution to your problem might be to wrap your description function in another helper and then pass that helper as a method group to your node helper, like this:
#helper Node(string title, Func<HelperResult> descriptions)
{
<div>#descriptions()</div>
}
#helper Description() {
<span>desc1</span>
<span>desc2</span>
}
#Node("title", Description)
In any case, your first idea shouldn't work because a parameter of type Func is in fact equal to a parameterless function, in which case you need to write the lambda expression like this:
myFunction( () => doSomething)
So your function call would have been:
#node("title", () =>
#<text>
<span>"desc1"</span>
<span>"desc2"</span>
</text>)
Since the future of these helpers is a bit dubious though, I would consider switching to either HtmlHelpers for small snippets of html or Partials for larger chunks.
#Test(new Func<object, HelperResult>[]{#<text>hello</text>})
#Test(new Func<object, HelperResult>[]{#<text>hello</text>,#<text>world</text>})
#helper Test(params Func<object, HelperResult>[] results)
{
foreach (var result in results)
{
#result(null);
}
}

How to use Html.GetUnobtrusiveValidationAttributes()

I am trying to work around the fact that when they wrote asp.net MVC 3 they forgot to include code to add the unobtrusive validation attributes to select lists and their "fix" for this is to include it in MVC 4, which is no bloody use to anyone using MVC 3.
My proposed work around is to use Html.GetUnobtrusiveValidationAttributes() to add them myself, just like any other custom attributes, but i can't work out the correct syntax for calling the method. There are 2 overloads, one takes a string and the other takes a string and a ModelMetaData class. I understand the metadata param, I presume I just pass in ViewData.ModelMetadata but what should the string be? The MSDN documentation says it is "the specified HTML name attribute" which makes no sense to me. The HTML name attribute of what? The select list? Why would it need that and how does that help it know what property on my model i want the validation for? Looking at examples of usage they all seem to pass in the name of the property on my model that i want the validation attributes for, which makes sense. Unfortunately I can't get the method to return anything but an empty collection no matter what i pass in.
My model class is called Event and my property is called EventTypeID. I am using a slightly different viewmodel class as the basis for the view because i need to display a list of Events and also also allow a new event to be entered on the same view so i have a simple viewmodel class as below:
public class EventViewModel
{
public Model.Event NewEvent { get; set; }
public IEnumerable<Model.Event> Events { get; set; }
}
The dropdown list is mapped to the property like: #Html.DropDownListFor(model => model.NewEvent.EventTypeID what do I pass as the string to Html.GetUnobtrusiveValidationAttributes(string) or Html.GetUnobtrusiveValidationAttributes(string, ModelMetadata) to get the attributes for this property. I have tried:
Html.GetUnobtrusiveValidationAttributes("EventTypeID")
Html.GetUnobtrusiveValidationAttributes("EventTypeID",ViewData.ModelMetadata)
Html.GetUnobtrusiveValidationAttributes("NewEvent.EventTypeID")
Html.GetUnobtrusiveValidationAttributes("NewEvent.EventTypeID",ModelMetadata)
They all return an empty collection.
I know that my model is correct because if i change the call from Html.DropDownListFor to Html.TextBoxFor then the validation "just works" without me having to do anything other than add the validation attributes to my model class.
EDIT:
Just tried turning client side validation off, the validation works fine server side for all select lists.
For those still looking for an answer, this works for me:
public static IDictionary<string, object> UnobtrusiveValidationAttributesFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> propertyExpression)
{
var propertyName = html.NameFor(propertyExpression).ToString();
var metadata = ModelMetadata.FromLambdaExpression(propertyExpression, html.ViewData);
var attributes = html.GetUnobtrusiveValidationAttributes(propertyName, metadata);
return attributes;
}
Note that I'm using .Net MVC 4, you don't have the html.NameFor method in MVC 3. However, I believe this can be done in MVC 3 with the following method:
var propertyName = ExpressionHelper.GetExpressionText(propertyExpression);
You can use it inline
Example for select element
<select name="#Html.NameFor(m=> m.MyProperty)"
id="#Html.IdFor(m=> m.MyProperty)"
#Html.Raw(string.Join(" ", Html.GetUnobtrusiveValidationAttributes(Html.NameFor(m => m.MyProperty).ToString()).Select(x => x.Key.ToString() + "=\"" + x.Value + "\"")))
>
Here is a link to an answer I posted, showing an HtmlHelper I wrote to provide unobtrusive validation for dropdownlists: MVC 3 dropdownlist validation not working for complex view model
UPDATE
Are you trying to get the attributes in an HtmlHelper, or in-line in your view?
Assuming you are trying to get the attributes in your view, that is the problem.
First, you need to understand that ModelMetadata does not represent a single object available across your entire model. Rather, it represents the metadata for a particular element, be it your model, or any property within the model. A better descriptive name would be ObjectMetadata, since ModelMetadata is the metadata for a specified object, be it a model, a nested model, or a specific property.
ModelMetadata in the view is only the metadata for the top-level model. You must get the ModelMetadata for the property to which the dropdownlist is bound. If you use a helper, then the helper is passed the correct ModelMetadata as a matter of course. If you use your view, you need to engage in some gymnastics to get the correct ModelMetadata, see for example my answer here: Validating and editing a “Changeable”/optional type in Asp.net MVC 3

How to deal with more than one value per key in ASP.NET MVC 3?

I have the following problem: one of the system I'm working in most important features is a search page. In this page I have some options, like records per page, starting date, ending date, and the problematic one: type. One must have the possibility to choose more than one type (most of the time, all of them will be selected). To make that work, i created the following:
<div>
<label>Eventos:</label>
<div>
#Html.ListBox("events", Model.Events, new { style = "width: 100%" })
</div>
</div>
It creates a listbox where I can choose more than one option, and when the form is submited, my query string will look like this:
/5?period=9&events=1&events=3&recordsPerPage=10
There it is possible to see that two events (which is the type I was talking before) are created. The action method to this page takes a List<long> as one of its arguments, which represents that two events values. The problem begins when I want to use that with MVC Contrib. Their pager works just fine, but as I was requested, I created another pager, which displays links to five pages after and before the one the user is at. To do this, in a part of my code I have to do the following (which is very similar to the MVC Contrib pager, that works):
public RouteValueDictionary GetRoute(int page)
{
var routeValues = new RouteValueDictionary();
foreach (var key in Context.Request.QueryString.AllKeys.Where(key => key != null))
{
routeValues[key] = Context.Request.QueryString[key];
}
routeValues["page"] = page;
return routeValues;
}
And then:
#Html.ActionLink(page.ToString(), action, controller, GetRoute(page), null)
The problem is that it is a Dictionary, which makes the second time I set the value for routeValues["events"] erase the previous.
Do you guys have any idea on how to work with it?
Very good question. Unfortunately it is not easy to generate an url which has multiple query string parameters with the same name using the Html.ActionLink helper. So I can see two possible solutions:
Write a custom model binder for long[] that is capable of parsing a comma separated values. This way you can keep your GetRoute method which will generate the following url: period=9&events=1%2C3&recordsPerPage=10&page=5.
public class CommaSeparatedLongArrayModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var values = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (values != null && !string.IsNullOrEmpty(values.AttemptedValue))
{
// TODO: A minimum of error handling would be nice here
return values.AttemptedValue.Split(',').Select(x => long.Parse(x)).ToArray();
}
return base.BindModel(controllerContext, bindingContext);
}
}
which you will register in Application_Start:
ModelBinders.Binders.Add(typeof(long[]), new CommaSeparatedLongArrayModelBinder());
and then the following controller action will be able to understand the previous URL:
public ActionResult Foo(long[] events, int page, int period, int recordsPerPage)
{
...
}
Manually generate this anchor:
abc
Try looking at WinTellect's PowerCollections, allows you to create a MultiDictionary, still can't have duplicate keys, but you can have multiple values per key.
You should write either extension methods that target the routeValue collection or a custom model binder that transforms your Event parameter into a list always. If you view Event always being a list, just a commonly length 1 list will alleviate most of the problems you face.
At this point you will just interact with a list interface. You could then write a custom binder to allow you to directly place that into the route correctly or you could unpack the list back into the query string. There's a software project based on this called Unbinder for unpacking objects into property/value pairs that you can easily use in query strings or other purposes.

Dynamically Loading a UserControl with LoadControl Method (Type, object[])

I'm trying to return the html representation of a user/server control through a page method. It works when I call the overload which takes the virtual path to the user control, but not when I try to call the overload which takes a type. The sample code is below. Any suggestions?
[WebMethod]
public static string LoadAlternates(string productId, string pnlId)
{
object[] parameters = new object[] { pnlId, productId };
return ControlAsString(typeof(PopupControl), parameters);
}
private static string ControlAsString(Type controlType, object[] parameters)
{
Page page = new Page();
UserControl controlToLoad;
/*
*calling load control with the type results in the
*stringwriter returning an empty string
*/
controlToLoad = page.LoadControl(controlType, parameters) as UserControl;
/*
*However, calling LoadControl with the following overload
*gives the correct result, ie the string rep. of the control.
*/
controlToLoad = page.LoadControl(#"~/RepeaterSamples/PopupControl.ascx") as UserControl;
//some more code, then this...
page.Controls.Add(controlToLoad);
StringWriter sw = new StringWriter();
HttpContext.Current.Server.Execute(page, sw, false);
return sw.ToString();
}
Any ideas why this StringWriter would return an empty string? I should point out that all the "page" lifecycle execute correctly irrespective of the method chosen to call LoadControl.
Wanted to add -
I have to use the LoadControl(Type, object[]) overload. :-(
On the MSDN page for LoadControl there is this comment at the bottom:
Description A page that loads a user control using the
Page.LoadControl(Type, Object[]) does not seem to create its children
added in the ascx file. Using Page.LoadControl(String) works as
expected.
Comments Thank you for submitting this issue. We're investigating and
will provide an update on status when we have more information.
-The Web Platform & Tools Team Posted by Microsoft on 8/06/2005 at 11:08 AM This is by-design since the type "TestUC" is actually the
base type used by the partial class, it does not contain the proper
code to instantiate TextBox1 reference, which is actually defined in
the derived type. There are two workarounds: 1. Use
LoadControl("TestControl.ascx"), for all practical, this behaves
identically to LoadControl(type) but it instantiates the derived type,
which knows how to instantiate TextBox1. 2. Use a single file page and
adds <%# Reference %> directive to the page to reference the user
control, and assign a classname to the ascx page. Then it's safe to
use LoadControl(type)
Thanks for reporting the issue. Web Platform and Tools Team. Posted by
Microsoft on 14/06/2005 at 6:31 PM
That overload instantiates the base class, but doesn't instantiate any of the controls on it, so it doesn't work.
I did a quick blog post on a workaround for passing parameters if you're interested.
If you want to have the control completely rendered one way to do this is to instantiate the control as you are now with LoadControl and then temporarily add it to the control collection of another control or the page itself. This will initialize the life-cycle for that control and cause all of the proper events to be raised. Once you have done this then you can get the rendered HTML or whatever you need from it.
Yes, this is a hack but it will work in a pinch.

Categories

Resources