Converting EditorTemplate into HtmlHelper - c#

I originally created an editor template like this
#model MyModel
var items = // get items
#Html.DropDownListFor(m => m.Id, items, new { id = Html.IdFor(m => m) })
which was invoked by
#Html.EditorFor(m => m.SomeClass)
where SomeClass has an Id property. (The IdFor is one of my HTML helpers).
This would generate something like this
<select name="SomeClass.Id" id="SomeClass" />
Now I want to change the editor template into an HTML helper, so that my call looks like this
#Html.CustomEditorFor(m => m.SomeClass)
I'm changing this from a view to helper because its easier for reusability.
This is loosely what I have:
public static MvcHtmlString CustomEditorFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
{
var idString = htmlHelper.IdFor(expression);
var propertyValue = expression.Compile()(htmlHelper.ViewData.Model);
var items = // get items
return htmlHelper.DropDownListFor(expression, items, new {id = idString});
}
However, when I call the editor, I get this HTML instead of what I want.
<select name="SomeClass" id="SomeClass" />
How can I modify the expression enough to allow it to "access" the Id property?

Try this:
public static MvcHtmlString CustomEditorFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
{
var idString = htmlHelper.IdFor(expression);
var items = // get items
var param = Expression.Parameter(typeof(TModel), "m");
var member = Expression.Property(
Expression.Property(param, ExpressionHelper.GetExpressionText(expression))
, "Id");
var isNullable = Nullable.GetUnderlyingType(member.Type);
if (isNullable != null) {
var expr2 = Expression.Lambda<Func<TModel, int?>>(
member, new[] { param }
);
return htmlHelper.DropDownListFor(expr2, items, new { id = idString });
}
var expr = Expression.Lambda<Func<TModel, int>>(
member, new[] { param }
);
return htmlHelper.DropDownListFor(expr, items, new { id = idString });
}

Related

Generically Render Partial View - Convert PropertyInfo into Concrete Object

I am currently making a wizard in MVC (c#). But I have an if statement in my Wizard view that goes like this:
if (Model.Wizard.ClientDetails.GetStep() == Model.Wizard.CurrentStep)
{
#Html.PartialFor(x => x.Wizard.ClientDetails, "_Step");
}
else if (Model.Wizard.Preferences.GetStep() == Model.Wizard.CurrentStep)
{
#Html.PartialFor(x => x.Wizard.ClientPreferences, "_Step")
}
else if (Model.Wizard.ClientQuestions.GetStep() == Model.Wizard.CurrentStep)
{
#Html.PartialFor(x => x.Wizard.ClientQuestions, "_Step")
}
The wizards have been set up pretty generically except for this part of the view where I choose which partial to display. As you can see from the code above each if follows the same structure. The only part that changes is the Model.Wizard.**Property** part.
I wanted to try and remove this if statement so I don't have to worry about writing an if statement for each step I add to a new wizard.
I want to change the code to just something like this:
#Html.PartialFor(x => x.ExampleWizardTransaction.GetStepObject(), "_Step");
My current attempt for the GetStepObject method is as follows:
public static T GetStepObject<T>(this IWizardTransaction wizardTransaction)
where T : class, new()
{
var properties = wizardTransaction.GetType().GetProperties()
.Where(x => x.PropertyType.GetCustomAttributes(typeof(StepAttribute), true).Any());
PropertyInfo #object = properties.FirstOrDefault(x => ((StepAttribute)Attribute
.GetCustomAttribute(x.PropertyType, typeof(StepAttribute))).Step == wizardTransaction.CurrentStep);
}
The PropertyInfo #object part is correctly selecting the property info for the current step in the wizard. I need to be able to return the PropertyInfo #object PropertyInfo as its correct type with its current values and return it somehow.
Is this possible?
EDIT #1:
Existing PartialFor that works in normal scenarios.
public static MvcHtmlString PartialFor<TModel, TProperty>(
this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, string partialViewName)
{
var name = ExpressionHelper.GetExpressionText(expression);
var model = ModelMetadata.FromLambdaExpression(expression, helper.ViewData).Model;
var viewData = new ViewDataDictionary(helper.ViewData)
{
TemplateInfo = new TemplateInfo { HtmlFieldPrefix = name }
};
return helper.Partial(partialViewName, model, viewData);
}
EDIT #2:
The reason the values are not getting binded is that the var name = ExpressionHelper.GetExpressionText(expression); part is returning a blank string. If I hard code the name variable to the actual property then the binding works. For example:
public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression, string partialViewName)
{
var compiled = expression.Compile();
var result = compiled.Invoke(helper.ViewData.Model);
var name = ExpressionHelper.GetExpressionText(expression);
//Should be ExampleWizardTransaction.ClientDetails for this step but is blank
var viewData = new ViewDataDictionary(helper.ViewData)
{
TemplateInfo = new TemplateInfo
{
//HtmlFieldPrefix = name
HtmlFieldPrefix = "ExampleWizardTransaction.ClientDetails"
}
//Hard coded this to ExampleWizardTransaction.ClientDetails and the bindings now work
};
return helper.Partial(partialViewName, result, viewData);
}
It seems I need to be able to get the name of the wizard object and the current step object as a string value to pass into TemplateInfo.
I'm gonna take a wild guess at your class structures. Assuming your classes are something like this:
[AttributeUsage(AttributeTargets.Property, AllowMultiple =false)]
public class StepAttribute: Attribute
{
public StepEnum Step { get; set; }
}
public interface IWizardStep
{
}
public interface IWizardTransaction
{
}
public enum StepEnum
{
Previous,
CurrentStep
}
public class WizardStep: IWizardStep
{
public string StepName { get; set; }
public override string ToString()
{
return StepName;
}
}
public class Wizard : IWizardTransaction
{
[Step(Step = StepEnum.Previous)]
public WizardStep ClientDetails => new WizardStep() { StepName = "ClientDetails" };
[Step(Step = StepEnum.CurrentStep)]
public WizardStep ClientQuestions => new WizardStep() { StepName = "ClientQuestions" };
}
Assuming also this implementation of PartialFor method
public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TProperty>> expression, string partialViewName)
{
var compiled = expression.Compile();
var result = compiled.Invoke(html.ViewData.Model);
return html.Partial(partialViewName, result);
}
Then this implementation of GetStepObject will work
public static TProperty GetStepObject<TProperty>(this IWizardTransaction wizardTransaction)
where TProperty : class
{
var properties = wizardTransaction.GetType().GetProperties()
.Where(x => x.GetCustomAttributes(typeof(StepAttribute), true).Any());
PropertyInfo #object = properties.FirstOrDefault(x =>
(x.GetCustomAttributes(typeof(StepAttribute), true).SingleOrDefault()
as StepAttribute).Step == StepEnum.CurrentStep);
return #object.GetValue(wizardTransaction) as TProperty;
}
With this implementation of a partial view named _Step.cshtml like this
#model PartialView.Models.WizardStep
#Model
Your view can call it like this
#model PartialView.Models.Wizard
#using PartialView.Models;
#{
ViewBag.Title = "Partial view calling";
}
#Html.PartialFor(m=>m.GetStepObject<WizardStep>(), "_Step")
And the visual result will be a blank page with the html text ClientQuestions

How can I added #readonly = readonly to an existing additionalViewData property?

I'm doing something like this:
public static MvcHtmlString DimensionEditorFor<TModel, TValue>(this HtmlHelper<TModel> a_html, Expression<Func<TModel, TValue>> a_expression, DimensionLock a_lock, object a_additionalViewData)
{
var dictionary = new RouteValueDictionary(a_additionalViewData);
if (a_lock.IsLocked)
{
object htmlAttributes;
if (dictionary.TryGetValue("htmlAttributes", out htmlAttributes))
{
var htmlAttributesDict = new RouteValueDictionary(htmlAttributes);
htmlAttributesDict["#readonly"] = "readonly";
htmlAttributes = htmlAttributesDict;
}
else
{
htmlAttributes = new {#readonly = "readonly"};
}
dictionary["htmlAttributes"] = htmlAttributes;
}
return a_html.EditorFor(a_expression, dictionary);
}
This doesn't work because RouteValueDictionary object are not allowable values for additionalViewData in the EditorFor extension method. I want to set readonly on the rendered text box if a_lock.IsLocked is true.
Updates:
I have tried the following just to see if ViewDataDictionary would work. It does not.
public static MvcHtmlString DimensionEditorFor<TModel, TValue>(this HtmlHelper<TModel> a_html, Expression<Func<TModel, TValue>> a_expression, DimensionLock a_lock, object a_additionalViewData)
{
var dictionary = new ViewDataDictionary();
foreach (KeyValuePair<string, object> pair in new RouteValueDictionary(a_additionalViewData))
dictionary.Add(pair.Key, pair.Value);
return a_html.EditorFor(a_expression, dictionary);
}
It's not exactly elegant, but if all you need is for the object to be a ViewDataDictionary instead of a RouteValueDictionary. You could always do:
var viewData = new ViewDataDictionary();
foreach (var key in dictionary.Keys)
{
viewData[key] = dictionary[key];
}

Create dropdown list for enum with description C#

I want to create a dropdown list using description of enum instead of its value.
I'd like to know how to get descriptions instead of values in the following code which creates a dropdown list for enum :
public static MvcHtmlString DropDownListForEnum<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression)
{
// get expression property description
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
IEnumerable<TEnum> values = Enum.GetValues(typeof(TEnum)).Cast<TEnum>();
IEnumerable<SelectListItem> items =
values.Select(value => new SelectListItem
{
Text = value.ToString(),
Value = value.ToString(),
Selected = value.Equals(metadata.Model)
});
return htmlHelper.DropDownListFor(
expression,
items
);
}
First, make a new method to get the description like shown below:
public static string GetDescription<T>(string value)
{
Type type = typeof(T);
if (typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(Nullable<>))
{
type = Nullable.GetUnderlyingType(type);
}
T enumerator = (T)Enum.Parse(type, value);
FieldInfo fi = enumerator.GetType().GetField(enumerator.ToString());
DescriptionAttribute[] attributtes =
(DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
if (attributtes != null && attributtes.Length > 0)
return attributtes[0].Description;
else
return enumerator.ToString();
}
And then use it in your helper:
public static MvcHtmlString DropDownListForEnum<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression)
{
// get expression property description
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
IEnumerable<TEnum> values = Enum.GetValues(typeof(TEnum)).Cast<TEnum>();
IEnumerable<SelectListItem> items =
values.Select(value => new SelectListItem
{
Text = value.ToString(),
Value = GetDescription<TEnum>(value.ToString()),
Selected = value.Equals(metadata.Model)
});
return htmlHelper.DropDownListFor(
expression,
items
);
}
Use Enum getnames to get the names
http://msdn.microsoft.com/en-us/library/system.enum.getnames(v=vs.110).aspx

How to validate custom ASP.NET MVC helper

I need to get working validation of the custom ASP.NET MVC helper.
Helper
public static class AutocompleteHelper
{
public static MvcHtmlString AutocompleteFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, string actionUrl)
{
return CreateAutocomplete(helper, expression, actionUrl, null, null);
}
public static MvcHtmlString AutocompleteFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, string actionUrl, bool? isRequired, string placeholder)
{
return CreateAutocomplete(helper, expression, actionUrl, placeholder, isRequired);
}
private static MvcHtmlString CreateAutocomplete<TModel, TValue>(HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, string actionUrl, string placeholder, bool? isRequired)
{
var attributes = new Dictionary<string, object>
{
{ "data-autocomplete", true },
{ "data-action", actionUrl }
};
if (!string.IsNullOrWhiteSpace(placeholder))
{
attributes.Add("placeholder", placeholder);
}
if (isRequired.HasValue && isRequired.Value)
{
attributes.Add("required", "required");
}
attributes.Add("class", "form-control formControlAutocomplete");
attributes.Add("maxlength", "45");
Func<TModel, TValue> method = expression.Compile();
var value = method((TModel)helper.ViewData.Model);
var baseProperty = ((MemberExpression)expression.Body).Member.Name;
var hidden = helper.Hidden(baseProperty, value);
attributes.Add("data-value-name", baseProperty);
var automcompleteName = baseProperty + "_autocomplete";
var textBox = helper.TextBox(automcompleteName, null, string.Empty, attributes);
var builder = new StringBuilder();
builder.AppendLine(hidden.ToHtmlString());
builder.AppendLine(textBox.ToHtmlString());
return new MvcHtmlString(builder.ToString());
}
}
HTML
#Html.AutocompleteFor(x => x.ProductUID, Url.Action("AutocompleteProducts", "Requisition"), true, "Start typing Product name...")
#Html.ValidationMessageFor(x => x.ProductUID)
I seems like validating but no message appears.
Any clue?
The name of your text field is ProductUID_autocomplete but your ValidationMessageFor which is supposed to display the error message is bound to ProductUID.
So make sure that you are binding your error message to the same property:
#Html.ValidationMessage("ProductUID_autocomplete")
It appears that whatever custom logic you might have to validate this field is injecting the error under the ProductUID_autocomplete key in the ModelState.
This being said, why not just invoke the ValidationMessage helper inside your custom helper? This way you will have less things to type in your view and the logic with those names being suffixed with _autocomplete will stay inside the helper only.

How to add a htmlAttributes in this HtmlHelper?

I'm using right now this code to implement a RadioButtonList using MVC4.
And as you can see, that function does not have htmlAttributes parameter. So I'd like to add it and here is the problem. Check please that the htmlAttributes for RadioButtonFor() is occupied by the id.
I was trying to add it but throws me errors because the id already exists for the loop.
public static class HtmlExtensions
{
public static MvcHtmlString RadioButtonForSelectList<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> listOfValues)
{
return htmlHelper.RadioButtonForSelectList(expression, listOfValues, null);
}
public static MvcHtmlString RadioButtonForSelectList<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> listOfValues,
object htmlAttributes)
{
return htmlHelper.RadioButtonForSelectList(expression, listOfValues, new RouteValueDictionary(htmlAttributes));
}
public static MvcHtmlString RadioButtonForSelectList<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> listOfValues,
IDictionary<string, object> htmlAttributes)
{
var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var sb = new StringBuilder();
if (listOfValues != null)
{
foreach (SelectListItem item in listOfValues)
{
var id = string.Format(
"{0}_{1}",
metaData.PropertyName,
item.Value
);
var radio = htmlHelper.RadioButtonFor(expression, item.Value, new { id = id }).ToHtmlString();
sb.AppendFormat(
"{0}<label for=\"{1}\">{2}</label>",
radio,
id,
HttpUtility.HtmlEncode(item.Text)
);
}
}
return MvcHtmlString.Create(sb.ToString());
}
}
In the third method, it looks like the html attributes being passed to the radion button being created is new { id = id }. Try to replace that with the parameter from the method.
UPDATED
Include id in the html attributes and assign a new value to id in each loop iteration.
if (listOfValues != null)
{
if (!htmlAttributes.ContainsKey("id"))
{
htmlAttributes.Add("id", null);
}
foreach (SelectListItem item in listOfValues)
{
var id = string.Format(
"{0}_{1}",
metaData.PropertyName,
item.Value
);
htmlAttributes["id"] = id;
var radio = htmlHelper.RadioButtonFor(expression, item.Value, htmlAttributes).ToHtmlString();
sb.AppendFormat(
"{0}<label for=\"{1}\">{2}</label>",
radio,
id,
HttpUtility.HtmlEncode(item.Text)
);
}
}

Categories

Resources