2017年8月31日 星期四

MVC Custom HtmlHelper Radiobutton for Multiple Question multiple Option in single Survey form

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.0
VS 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>




沒有留言:

張貼留言