I have a situation where I need to write an HTML Helper to extend another html helper. Normally, the helper would look like this.
@Html.TextAreaFor(model => model.Content, new { @class = "some css", @data_bind = "some other stuff..." })
This works fine, but it has to be wrapped in some other HTML that is always the same. I wanted to encapsulate it for convenience, like this.
public static MvcHtmlString CondensedHelperFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes) {
var stringBuilder = new System.Text.StringBuilder();
var tag = new TagBuilder("div"); tag.AddCssClass("some_css");
stringBuilder.Append(toolbar.ToString(TagRenderMode.SelfClosing));
stringBuilder.Append(htmlHelper.TextAreaFor(expression, htmlAttributes));
// more tags and such...
return new MvcHtmlString(stringBuilder.ToString());
}
The line stringBuilder.Append(htmlHelper.TextAreaFor...
is what I want to change. The CSS class that has to go there is always going to be present. So I would rather include it here. However I would like to be able to specify additional CSS classes in the top-level helper. So ...
@Html.CondensedHelperFor(model => model.Content, new { @class = "some_other_css" })
And the sta开发者_运维百科tic css that will always be there get blanketed in through the Helper.
Any ideas?
You might be able to do so with the standard MVC helper method.
HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)
htmlAttributes
is a an object
First, create a method (the best would be to create an extension method) that converts an object to IDictionary via type reflection:
public static IDictionary<string, object> ToDictionary(this object data)
{
if(data == null) return null; // Or throw an ArgumentNullException if you want
BindingFlags publicAttributes = BindingFlags.Public | BindingFlags.Instance;
Dictionary<string, object> dictionary = new Dictionary<string, object>();
foreach (PropertyInfo property in
data.GetType().GetProperties(publicAttributes)) {
if (property.CanRead) {
dictionary.Add(property.Name, property.GetValue(data, null));
}
}
return dictionary;
}
Now, make use of C# 4.0 ExpandoObject, which allows adding properties at runtime. You would end up with something like this:
public static MvcHtmlString CondensedHelperFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes) {
{
var dictAttributes = htmlAttributes.ToDictionary();
var result = new ExpandoObject();
var d = result as IDictionary<string, object>; //work with the Expando as a Dictionary
if(dictAttributes != null)
{
foreach (var pair in dictAttributes)
{
d[pair.Key] = pair.Value;
}
}
// Add other properties to the dictionary d here
// ...
var stringBuilder = new System.Text.StringBuilder();
var tag = new TagBuilder("div"); tag.AddCssClass("some_css");
stringBuilder.Append(toolbar.ToString(TagRenderMode.SelfClosing));
stringBuilder.Append(htmlHelper.TextAreaFor(expression, result));
return new MvcHtmlString(stringBuilder.ToString());
}
@pszmyd: You can use the ViewDataDictionary that already takes an object in the constructor ex.
//
// Summary:
// Initializes a new instance of the System.Web.Mvc.ViewDataDictionary class
// by using the specified model.
//
// Parameters:
// model:
// The model.
public ViewDataDictionary(object model);
Also what you are trying to do is simple enough - merging key values. In the case of html attributes it is not straight forward. ex. element can contain multiple classes ex. 'blue dr ltr', separated by spaces, while the style attribute uses the semi-colon as delimiter ex. 'width:200px; font-size:0.8em;'. It is really up to you to parse and check that the values are correct (not merging css classes instead of splitting them with spaces, same with the style).
I'd suggest from your parameter:object htmlAttributes
you just create: var htmlAttr = new ViewDataDictionary<TModel>(htmlAttributes);
then create custom extension methods to merge the attributes ex.
public static class ViewDataDictionaryExtensions
{
public static ViewDataDictionary<TModel> MergeClasses<TModel>(this ViewDataDictionary<TModel> dict, string classes)
{
if (dict.ContainsKey("class"))
dict["class"] += " " + classes;
else
dict.Add("class", classes);
return dict;
}
public static ViewDataDictionary<TModel> MergeStyles<TModel>(this ViewDataDictionary<TModel> dict, string styles)
{
if (dict.ContainsKey("style"))
dict["style"] += "; " + styles;
else
dict.Add("style", styles);
return dict;
}
}
This is only a really simple implementation not taking into account duplicate attribute values or multiple separators. But hope you get the idea!
When I've to do that, I usually create a partial view Then I use RenderPartial:
@Html.Partial(MVC.Shared.MyPartialView_cshtml, new ViewDataDictionary() {
{ "Content", model.Content},
{ "Css", "some_other_css"},
});
I usually also create a Model class to avoid using magic string ( and ViewDataDictionary) like in the sample above. In your view you can
- use the Content: @ViewBag.Content
- test and use the default css if a custom one is not specified (@ViewBag.Css)
A small note: it's also possible to change the default template used when calling TextAreaFor (or other similar method). For this, have a look at http://bradwilson.typepad.com/blog/2009/10/aspnet-mvc-2-templates-part-3-default-templates.html
精彩评论