Scenario
When you need to built a survey form with multiple questions and each question with multiple options. Then you may encounter a problem with radiobutton name attribute and validation message. Here is a solution, may not perfect solution, but a solution for you reference.
Environment
System.Web.MVC 5.2.3.0VS 2015
Bootstrap v3.3.5
Model
Question Class
public class SurveyQuestion
{
public string Name { get; set; }
[Required]
[Display(Name = "ID")]
public string ID { get; set; }
[Required]
[Display(Name = "Option")]
public IList<SurveyOption> Options { get; set; }
public IList<string> Answers { get; set; }
}
Option Class
public class SurveyOption
{
public string Name { get; set; }
public string Value { get; set;}
public SurveyQuestion Parent { get; set; }
public bool IsFirstOption
{
get
{
if (Parent != null && Parent.Options.FirstOrDefault() == this)
{
return true;
}
else
{
return false;
}
}
}
public SurveyOption(SurveyQuestion parent,
string name,
string value)
{
Parent = parent;
Name = name;
Value = value;
}
}
View
@inherits System.Web.Mvc.WebViewPage<SurveyQuestion>@Model.Name
@{ @Html.RadioButtonsListFor(m => m.ID, m => m.Options, m => m.Options.FirstOrDefault().Name, m => m.Options.FirstOrDefault().Value, m => m.Options.FirstOrDefault().IsFirstOption, isMandatory: true, optionHtmlAttributes: null, labelHtmlAttributes: new { @class = "radio-inline SurveryMC" }) @Html.ValidationMessage(Model.ID, "", new { @class = "text-danger" }) }
Custom htmlhelper Class
public static MvcHtmlString RadioButtonsListFor<TModel, TProperty, TOptions, TOptionName, TOptionValue, TIsFirstOption>
(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expQuestionName,
Expression<Func<TModel, TOptions>> expOptions,
Expression<Func<TModel, TOptionName>> expOptionName,
Expression<Func<TModel, TOptionValue>> expOptionValue,
Expression<Func<TModel, TIsFirstOption>> expIsFirstOption,
bool isMandatory = true)
{
return RadioButtonsListFor(htmlHelper, expQuestionName, expOptions, expOptionName, expOptionValue, expIsFirstOption, isMandatory: isMandatory, optionHtmlAttributes: null, labelHtmlAttributes: null);
}
public static MvcHtmlString RadioButtonsListFor<TModel, TProperty, TOptions, TOptionName, TOptionValue, TIsFirstOption>
(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expQuestionName,
Expression<Func<TModel, TOptions>> expOptions,
Expression<Func<TModel, TOptionName>> expOptionName,
Expression<Func<TModel, TOptionValue>> expOptionValue,
Expression<Func<TModel, TIsFirstOption>> expIsFirstOption,
object optionHtmlAttributes,
object labelHtmlAttributes,
bool isMandatory)
{
return RadioButtonsListFor(htmlHelper, expQuestionName, expOptions, expOptionName, expOptionValue, expIsFirstOption, isMandatory: isMandatory, optionHtmlAttributes: HtmlHelper.AnonymousObjectToHtmlAttributes(optionHtmlAttributes), labelHtmlAttributes: HtmlHelper.AnonymousObjectToHtmlAttributes(labelHtmlAttributes));
}
public static MvcHtmlString RadioButtonsListFor<TModel, TProperty, TOptions, TOptionName, TOptionValue, TIsFirstOption>
(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expQuestionName,
Expression<Func<TModel, TOptions>> expOptions,
Expression<Func<TModel, TOptionName>> expOptionName,
Expression<Func<TModel, TOptionValue>> expOptionValue,
Expression<Func<TModel, TIsFirstOption>> expIsFirstOption,
IDictionary<string, object> optionHtmlAttributes,
IDictionary<string, object> labelHtmlAttributes,
bool isMandatory)
{
if (expQuestionName == null
|| expOptions == null
|| expOptionName == null
|| expOptionValue == null
|| expIsFirstOption == null)
{
throw new ArgumentNullException("expression");
}
var sb = new StringBuilder();
var metadataExp1 = ModelMetadata.FromLambdaExpression(expQuestionName, htmlHelper.ViewData);
var metadataExp2 = ModelMetadata.FromLambdaExpression(expOptions, htmlHelper.ViewData);
string questionExpText = ExpressionHelper.GetExpressionText(expQuestionName);
string questionName = GetExpValue(htmlHelper, expQuestionName) as string;
string optionsExpText = ExpressionHelper.GetExpressionText(expOptions);
IList options = GetExpValue(htmlHelper, expOptions) as IList;
string optionNameExpText = ExpressionHelper.GetExpressionText(expOptionName);
string optionValueExpText = ExpressionHelper.GetExpressionText(expOptionValue);
string isFirstOptionExpText = ExpressionHelper.GetExpressionText(expIsFirstOption);
RouteValueDictionary optionAttributes = ToRouteValueDictionary(optionHtmlAttributes);
RouteValueDictionary labelAttributes = ToRouteValueDictionary(labelHtmlAttributes);
// Get Validation Attributes
var optionValidationAttributes = htmlHelper.GetUnobtrusiveValidationAttributes(optionsExpText);
// Reset Option Field to un-rendered, in case GetUnobtrusiveValidationAttributes can get value in second call with same property name
htmlHelper.ViewContext.FormContext.RenderedField(htmlHelper.ViewData.TemplateInfo.GetFullHtmlFieldName(optionsExpText), false);
foreach (var item in options)
{
string optionName = GetPropValue(item, optionNameExpText) as string;
string optionValue = GetPropValue(item, optionValueExpText) as string;
string optionId = questionName + optionName;
bool isFirstOption = GetPropValue(item, isFirstOptionExpText) as bool? ?? false;
TagBuilder tagOption = new TagBuilder("input");
tagOption.MergeAttributes(optionAttributes);
tagOption.MergeAttribute("id", optionId);
tagOption.MergeAttribute("name", questionName);
tagOption.MergeAttribute("type", "radio");
tagOption.MergeAttribute("value", optionValue);
if (isFirstOption && isMandatory)
{
tagOption.AddCssClass("valid");
foreach (var v in optionValidationAttributes)
{
tagOption.MergeAttribute(v.Key, v.Value.ToString());
}
}
TagBuilder tagLabel = new TagBuilder("label");
tagLabel.MergeAttributes(labelAttributes);
tagLabel.MergeAttribute("for", optionId);
tagLabel.InnerHtml += optionName;
sb.Append(tagOption);
sb.Append(Environment.NewLine);
sb.Append(tagLabel);
sb.Append(Environment.NewLine);
sb.Append(TAG_BR);
sb.Append(Environment.NewLine);
}
return new MvcHtmlString(sb.ToString());
}
private static object GetPropValue(object src, string propName)
{
return src.GetType().GetProperty(propName).GetValue(src, null);
}
private static object GetExpValue<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
{
string expText = ExpressionHelper.GetExpressionText(expression);
expText = !string.IsNullOrEmpty(expText) ? expText : "empty";
return htmlHelper.ViewData.Eval(expText) as object;
}
private static RouteValueDictionary ToRouteValueDictionary(IDictionary<string, object> dictionary)
{
return dictionary == null ? new RouteValueDictionary() : new RouteValueDictionary(dictionary);
}
Output HTML
<div class="row">
<h4>問題1</h4>
<input class="valid" data-val="true" data-val-required="必須作答" id="Q[1]選擇1" name="Q[1]" type="radio" value="選擇2">
<label class="radio-inline SurveryMC" for="Q[1]選擇1">選擇1</label>
<br>
<input id="Q[1]選擇2" name="Q[1]" type="radio" value="選擇2">
<label class="radio-inline SurveryMC" for="Q[1]選擇2">選擇2</label>
<br>
<input id="Q[1]選擇3" name="Q[1]" type="radio" value="選擇3">
<label class="radio-inline SurveryMC" for="Q[1]選擇3">選擇3</label>
<br>
<span class="field-validation-valid text-danger" data-valmsg-for="Q[1]" data-valmsg-replace="true"></span>
</div>