MVC .net add/edit css class for helper.TextBox extension - asp.net-mvc

I've created a MVC extension to auto apply attributes to html inputs. Which is all working as expected however if i want to add a css class to the html input and it already has a css class the code bombs as the attribute is already set.
Heres my code:
public static MvcHtmlString LockableTextBox(this HtmlHelper helper, string name, object value, object htmlAttributes, bool locked)
{
RouteValueDictionary dic = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
if (locked)
{
dic.Add("readonly", "readonly");
dic.Add("class", "field-locked");
}
return helper.TextBox(name, value, dic);
}
I call it like so:
#Html.LockableTextBox("Initals", Model.Initals, new {}, Model.Locked)
which works, but this call does not
#Html.LockableTextBox("Initals", Model.Initals, new {#Class="field"}, Model.Locked)
How do i change the dic.Add("class", "field-locked") line so that it adds a my extra class to the existing class attribute?

You can use it like simple Dictionary. Check if you have already such key and append your string to existing value, otherwise add new.
public static MvcHtmlString LockableTextBox(this HtmlHelper helper, string name, object value, object htmlAttributes, bool locked)
{
RouteValueDictionary dic = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
if (locked)
{
dic.Add("readonly", "readonly");
if (dic.ContainsKey("class"))
dic["class"] += " field-locked";
else
dic.Add("class", "field-locked");
}
return helper.TextBox(name, value, dic);
}

Related

How can I convert anonymous object HTML attributes to a Dictionary<string, object>

I'm providing an extra overload to RadioButtonFor and want to add a Key Value pair to the HTML Attributes that are passed in.
As an example I am passing in something like:
new { id = "someID" }
When i the use the HtmlHelper.AnonymousObjectToHtmlAttributes method as seems to be the suggestions I'm finding), its resulting in a dictionary with 4 items with Keys of "Comparer", "Count", "Keys", "Values". I then try to use Reflection to iterate over the values in both "Keys" and "Values", but cannot get that to work either.
Essentially all I want to do is to be able to cast the htmlAttributes to an IDictionary , add a item and then pass it on to a regular RadioButtonFor method.
Edit:
Heres what Im actually trying to do. Provide an overload called isDisabled to be able to set the disabled state of the radio button as this cant be easily done directly using HTML attributes because disabled = false stillr esults in disabled being rendered to tag and disables the radio.
public static MvcHtmlString RadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object value, bool isDisabled, object htmlAttributes)
{
var linkAttributes = System.Web.Mvc.HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
Dictionary<string, object> htmlAttributesDictionary = new Dictionary<string, object>();
foreach (var a in linkAttributes)
{
if (a.Key.ToLower() != "disabled")
{
htmlAttributesDictionary.Add(a.Key, a.Value);
}
}
if (isDisabled)
{
htmlAttributesDictionary.Add("disabled", "disabled");
}
return InputExtensions.RadioButtonFor<TModel, TProperty>(htmlHelper, expression, value, htmlAttributesDictionary);
}
Looks like you might be applying the AnonymousObjectToHtmlAttributes either twice or to the wrong item.
Without more of your code, it's hard to tell
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(new { id = "someID" });
attributes.Count = 1
attributes.Keys.First() = id
compared with
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(new { id = "someID" }));
attributes.Count = 3
attributes.Keys.Join = Count,Keys,Values
When writing your overload, make sure your parameter is: object htmlAttributes for the new { } part with an overload with the IDictionary, eg:
Public static MvcHtmlString MyRadioButtonFor(..., object htmlAttributes)
{
return MyRadioButtonFor(...., HtmlHelper.AnonymousObjectToHtmlAttrbites(htmlAttributes);
}
public static MvcHtmlString MyRadioButtonFor(..., IDictionary<string, object> htmlAttributes)
{
htmlAttributes.Add("item", item);
return RadioButtonFor(..., htmlAttributes);
}
(just to be clear, never use My... - it's just for illustration)
Its unclear why you would not just use and existing overload that accepts object htmlAttributes to add the disabled="disabled" attribute, however the following should work
public static MvcHtmlString RadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object value, bool isDisabled, object htmlAttributes)
{
IDictionary<string, object> attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
if (isDisabled && !attributes.ContainsKey("disabled"))
{
attributes.Add("disabled", "disabled");
}
return InputExtensions.RadioButtonFor<TModel, TProperty>(htmlHelper, expression, value, attributes);
}

MVC 5 custom HtmlHelper for lightweight content editing

After years of working with WebForms I recently started the transition to MVC. I'm trying to create a plugable, lightweight content editing module but I've run into some problems.
The idea is simple: create a HtmlHelper named EditableSimpleHtml that can be used in a #using... { } syntax so that the following can be achieved in a razor view:
#using (Html.EditableSimpleHtml("MyKey"))
{
<h3>Test</h3>
<p>
1<br />
</p>
}
The value between the {...} is the default value for when no content can not be found in the data storage.
I've create a HtmlHelper. Below is a simplified version:
public static IDisposable EditableSimpleHtml(this HtmlHelper helper, string key)
{
// Get the content from the data storage using the key (I will not show the provider itself, its just a provider that will access a db)
var provider = ContentEditing.Provider;
string value = provider.GetValue(key);
if (value == null)
{
// No value found in the data storage for the supplied key, we have to use the default value from within the #using... { } statement
// Can I get that value here? I want to to store it initialy in the data storage
value = "..."; // How to get html from within the #using... { }?
}
return new ContentEditableHtmlString(helper, value);
}
public class ContentEditableHtmlString : IDisposable
{
private readonly HtmlHelper _helper;
public ContentEditableHtmlString(HtmlHelper helper, string value)
{
_helper = helper;
var builder = new TagBuilder("div");
var writer = _helper.ViewContext.Writer;
writer.Write(builder.ToString(TagRenderMode.StartTag));
writer.Write(value);
}
public void Dispose()
{
_helper.ViewContext.Writer.Write("</div>");
}
}
The problem is that I can't get the (default) content from within the #using... { } statement in the HtmlHelper, or at least I don't know how. I need it in case I want to store it to the database initially.
Second problem is that the value between the #using... { } statement will always be rendered. In the case when the content can be loaded from the data storage I want the default value to be replaced with the value from the data storage.
Is there a way to achieve this or did I start of on a completely wrong path?
You can not get the html within the #using{...} statement the way you are doing right now.
The closest thing you can do is use Templated Razor Delegates
public static HelperResult EditableSimpleHtml(this HtmlHelper helper, string key,
Func<string, HelperResult> template)
{
var templateResult = template(null);
//you have your value here that you can return directly
//or you can return HelperResult to write to the response directly
var templateResultHtml = templateResult.ToHtmlString();
return new HelperResult(writer =>
{
templateResult.WriteTo(writer);
});
}
And in your view:
#Html.EditableSimpleHtml("MyKey", #<text>
<h3>Test</h3>
<p>#DateTime.Now</p>
</text>)

How to create an MVC helper method to populate a drop down from an enum

The MVC HtmlHelper.DropDownFor method can be downright frustrating to use. More often than not your selection does not remain or your control is not bound correctly. How would you write a custom HTML helper to populate a dropdown from an enum?
I spent the last few hours trying to figure this one out, so might as well share what I found. After trying all sorts of permutations, creating a test app to try out various options and searching many articles, I got something that works for me.
First point to mention. The SelectList class takes four parameters (last three are optional). If you don't specify the selected value (last param), it will clear out any selected values you had set in your SelectListItem objects (assuming you created a list of those). This frustrated me for a while because I was setting one of the items Selected property to true, but once I create the SelectList object it was always set to false.
Here's the MVC source for SelectList for reference:
public class SelectList : MultiSelectList
{
public SelectList(IEnumerable items)
: this(items, null /* selectedValue */)
{
}
public SelectList(IEnumerable items, object selectedValue)
: this(items, null /* dataValuefield */, null /* dataTextField */, selectedValue)
{
}
public SelectList(IEnumerable items, string dataValueField, string dataTextField)
: this(items, dataValueField, dataTextField, null /* selectedValue */)
{
}
public SelectList(IEnumerable items, string dataValueField, string dataTextField, object selectedValue)
: base(items, dataValueField, dataTextField, ToEnumerable(selectedValue))
{
SelectedValue = selectedValue;
}
public object SelectedValue { get; private set; }
private static IEnumerable ToEnumerable(object selectedValue)
{
return (selectedValue != null) ? new object[] { selectedValue } : null;
}
}
Once I got past that little point I got my helper to correctly select the item from the list and correctly bind the value back. So here's the helper method I created (The initial method was from another post, but that did nor work correctly for me):
public static MvcHtmlString EnumDropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes = null) where TProperty : struct, IConvertible
{
if (!typeof(TProperty).IsEnum)
throw new ArgumentException("TProperty must be an enumerated type");
var selectedValue = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model.ToString();
var selectList = new SelectList(from value in EnumHelper.GetValues<TProperty>()
select new SelectListItem
{
Text = value.ToDescriptionString(),
Value = value.ToString()
}, "Value", "Text", selectedValue);
return htmlHelper.DropDownListFor(expression, selectList, htmlAttributes);
}
(The EnumHelper.GetValues and ToDescriptionString are my helper methods to return a list of enum values of a specified type and to get the EnumMember value property for the description for the enum) I can post that code if anyone wants it.
The trick in that above code was telling SelectList what the value and text properties are as well as the selected value.

How to create an ActionLink with Properties for the View Model

I have a ViewModel with a Filter property that has many properties that I use to filter my data
Example:
class MyViewModel : IHasFilter
{
public MyData[] Data { get; set; }
public FilterViewModel Filter { get; set; }
}
class FilterViewModel
{
public String MessageFilter { get; set; }
//etc.
}
This works fine when using my View. I can set the properties of Model.Filter and they are passed to the Controller. What I am trying to do now, is create an ActionLink that has a query string that works with the above format.
The query string generated by my View from above looks like this:
http://localhost:51050/?Filter.MessageFilter=Stuff&Filter.OtherProp=MoreStuff
I need to generate an ActionLink in a different View for each row in a grid that goes to the View above.
I have tried:
Html.ActionLink(
item.Message,
"Index",
"Home",
new { Filter = new { MessageFilter = item.Message, }, },
null);
I also tried setting the routeValues argument to:
new MyViewModel { Filter = new FilterViewModel { MessageFilter = item.Message, }, },
But these do not generate the query string like the above one.
Interesting question (+1). I'm assuming that the purpose is to use the default model binder to bind the querystring parameters to to your Action parameters.
Out of the box I do not believe that the ActionLink method will do this for you (of course there is nothing stopping you from rolling your own). Looking in reflector we can see that when the object is added to the RouteValueDictionary, only key value pairs are added. This is the code that adds the key value pairs and as you can see there is no traversing the object properties.
foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(values))
{
object obj2 = descriptor.GetValue(values);
this.Add(descriptor.Name, obj2);
}
So for your object
var values = new { Filter = new Filter { MessageFilter = item.Message } }
the key being added is Filter and the value is your Filter object which will evaluate to the the fully qualified name of your object type.
The result of this is Filter=Youre.Namespace.Filter.
Edit possible solution depending on your exact needs
Extension Method does the work
Note that it uses the static framework methods ExpressionHelper and ModelMetadata (which are also used by the existing helpers) to determine the appropriate names that the default model binder will understand and value of the property respectively.
public static class ExtentionMethods
{
public static MvcHtmlString ActionLink<TModel, TProperty>(
this HtmlHelper<TModel> helper,
string linkText,
string actionName,
string controllerName,
params Expression<Func<TModel, TProperty>>[] expressions)
{
var urlHelper = new UrlHelper(helper.ViewContext.HttpContext.Request.RequestContext);
var url = urlHelper.Action(actionName, controllerName);
if (expressions.Any())
{
url += "?";
foreach (var expression in expressions)
{
var result = ExpressionHelper.GetExpressionText(expression);
var metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, helper.ViewData);
url = string.Concat(url, result, "=", metadata.SimpleDisplayText, "&");
}
url = url.TrimEnd('&');
}
return new MvcHtmlString(string.Format("<a href='{0}'>{1}</a>", url, linkText));
}
}
Sample Models
public class MyViewModel
{
public string SomeProperty { get; set; }
public FilterViewModel Filter { get; set; }
}
public class FilterViewModel
{
public string MessageFilter { get; set; }
}
Action
public ActionResult YourAction(MyViewModel model)
{
return this.View(
new MyViewModel
{
SomeProperty = "property value",
Filter = new FilterViewModel
{
MessageFilter = "stuff"
}
});
}
Usage
Any number of your view model properties can be added to the querystring through that last params parameter of the method.
#this.Html.ActionLink(
"Your Link Text",
"YourAction",
"YourController",
x => x.SomeProperty,
x => x.Filter.MessageFilter)
Markup
<a href='/YourAction/YourController?SomeProperty=some property value&Filter.MessageFilter=stuff'>Your Link Text</a>
Instead of using string.Format you could use TagBuilder, the querystring should be encoded to be safely passed in a URL and this extension method would need some additional validation but I think it could be useful. Note also that, though this extension method is built for MVC 4, it could be easily modified for previous versions. I didn't realize that that one of the MVC tags was was for version 3 until now.
You could create one RouteValueDictionary from a FilterViewModel instance and then use ToDictionary on that to pass to another RouteValues with all the keys prefixed with 'Filter.'.
Taking it further, you could construct a special override of RouteValueDictionary which accepts a prefix (therefore making it more useful for other scenarios):
public class PrefixedRouteValueDictionary : RouteValueDictionary
{
public PrefixedRouteValueDictionary(string prefix, object o)
: this(prefix, new RouteValueDictionary(o))
{ }
public PrefixedRouteValueDictionary(string prefix, IDictionary<string, object> d)
: base(d.ToDictionary(kvp=>(prefix ?? "") + kvp.Key, kvp => kvp.Value))
{ }
}
With that you can now do:
Html.ActionLink(
item.Message,
"Index",
"Home",
new PrefixedRouteValueDictionary("Filter.",
new FilterViewModel() { MessageFilter = item.Message }),
null);
The caveat to this, though, is that the Add, Remove, TryGetValue and this[string key] methods aren't altered to take into account the prefix. That can be achieved by defining new versions of those methods, but because they're not virtual, they'd only work from callers that know they're talking to a PrefixedRouteValueDictionary instead of a RouteValueDictionary.

How do I access HtmlHelper methods from within MY OWN HtmlHelper?

I am writing my own HtmlHelper extenstion for ASP.NET MVC:
public static string CreateDialogLink (this HtmlHelper htmlHelper, string linkText,
string contentPath)
{
// fix up content path if the user supplied a path beginning with '~'
contentPath = Url.Content(contentPath); // doesn't work (see below for why)
// create the link and return it
// .....
};
Where I am having trouble is tryin to access UrlHelper from within my HtmlHelper's definition. The problem is that the way you normally access HtmlHelper (via Html.MethodName(...) ) is via a property on the View. This isn't available to me obviously from with my own extension class.
This is the actual MVC source code for ViewMasterPage (as of Beta) - which defines Html and Url.
public class ViewMasterPage : MasterPage
{
public ViewMasterPage();
public AjaxHelper Ajax { get; }
public HtmlHelper Html { get; }
public object Model { get; }
public TempDataDictionary TempData { get; }
public UrlHelper Url { get; }
public ViewContext ViewContext { get; }
public ViewDataDictionary ViewData { get; }
public HtmlTextWriter Writer { get; }
}
I want to be able to access these properties inside an HtmlHelper.
The best I've come up with is this (insert at beginning of CreateDialogLink method)
HtmlHelper Html = new HtmlHelper(htmlHelper.ViewContext, htmlHelper.ViewDataContainer);
UrlHelper Url = new UrlHelper(htmlHelper.ViewContext.RequestContext);
Am I missing some other way to access the existing HtmlHelper and UrlHelper instances - or do i really need to create a new one? I'm sure there isn't much overhead but I'd prefer to use the preexisting ones if I can.
Before asking this question I had looked at some of the MVC source code, but evidently I missed this, which is how they do it for the Image helper.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "The return value is not a regular URL since it may contain ~/ ASP.NET-specific characters")]
public static string Image(this HtmlHelper helper, string imageRelativeUrl, string alt, IDictionary<string, object> htmlAttributes) {
if (String.IsNullOrEmpty(imageRelativeUrl)) {
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "imageRelativeUrl");
}
UrlHelper url = new UrlHelper(helper.ViewContext);
string imageUrl = url.Content(imageRelativeUrl);
return Image(imageUrl, alt, htmlAttributes).ToString(TagRenderMode.SelfClosing);
}
Looks like instantiating a new UrlHelper is the correct approach after all. Thats good enough for me.
Update: RTM code from ASP.NET MVC v1.0 Source Code is slightly different as pointed out in the comments.
File: MVC\src\MvcFutures\Mvc\ImageExtensions.cs
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "The return value is not a regular URL since it may contain ~/ ASP.NET-specific characters")]
public static string Image(this HtmlHelper helper, string imageRelativeUrl, string alt, IDictionary<string, object> htmlAttributes) {
if (String.IsNullOrEmpty(imageRelativeUrl)) {
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "imageRelativeUrl");
}
UrlHelper url = new UrlHelper(helper.ViewContext.RequestContext);
string imageUrl = url.Content(imageRelativeUrl);
return Image(imageUrl, alt, htmlAttributes).ToString(TagRenderMode.SelfClosing);
}
I faced a similar issue and decided that it would be easier to just call the UrlHelper in the view and pass the output to my HtmlHelper extension. In your case it would look like:
<%= Html.CreateDialogLink( "text", Url.Content( "~/...path.to.content" ) ) %>
If you want to access the extension methods on the existing HtmlHelper that is passed into your class, you should only need to import System.Web.Mvc.Html in your source code file and you will get access to them (that's where the extension classes are defined). If you want a UrlHelper, you'll need to instantiate that as the HtmlHelper you are getting doesn't have a handle for the ViewPage that it's coming from.
If you need to create a UrlHelper in a utility class you can do the following :
string url = "~/content/images/foo.jpg";
var urlHelper = new UrlHelper(new RequestContext(
new HttpContextWrapper(HttpContext.Current),
new RouteData()), RouteTable.Routes);
string absoluteUrl = urlHelper.Content(url);
This allows you to use routing or '~ expansion' away from an MVC context.
Well, you can always pass the instance of the page to the extension method. I think that is a much better way of doing this than creating new instances in your method.
You could also define this method on a class that derives from MasterPage/ViewMasterPage and then derive the page from that. This way, you have access to all the properties of the instance and don't have to pass them around.

Resources