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>




How to get value from GetUnobtrusiveValidationAttributes in second time

From Microsoft, by design "On the first call, a collection of attributes is returned. Further calls (on the same field) return an empty collection."


Scenario :

When you have a survey form with multiple question, and options using radio button. Then you will face the problem, the validation message only the first question will be shown.

The reason caused by Microsoft design,

Here is the solution, to set the particular property name after you call the GetUnobtrusiveValidationAttributes function.


htmlHelper.ViewContext.FormContext.RenderedField(htmlHelper.ViewData.TemplateInfo.GetFullHtmlFieldName(optionExpText), false);



Reference : https://msdn.microsoft.com/en-us/library/system.web.mvc.htmlhelper.getunobtrusivevalidationattributes(v=vs.118).aspx

2017年8月8日 星期二

How to allow same email to different member in Umbraco

How to allow same email to different member/user in Umbraco


Environment
- Umbraco version 7.6.3 assembly: 1.0.6361.21154
- VS2015

Add

requiresUniqueEmail="false"

to web.config like below:

<membership defaultProvider="UmbracoMembershipProvider" userIsOnlineTimeWindow="15">
      <providers>
        <clear />
        <add name="UmbracoMembershipProvider" type="Umbraco.Web.Security.Providers.MembersMembershipProvider, Umbraco" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="10" useLegacyEncoding="false" enablePasswordRetrieval="false" enablePasswordReset="false" requiresQuestionAndAnswer="false" defaultMemberTypeAlias="Member" passwordFormat="Hashed" allowManuallyChangingPassword="false"  requiresUniqueEmail="false" />
        <add name="UsersMembershipProvider" type="Umbraco.Web.Security.Providers.UsersMembershipProvider, Umbraco" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="10" useLegacyEncoding="false" enablePasswordRetrieval="false" enablePasswordReset="false" requiresQuestionAndAnswer="false" passwordFormat="Hashed" allowManuallyChangingPassword="false" />
      </providers>
    </membership>

2017年8月7日 星期一

How to get Umbraco current page id in View

How to get Umbraco current page id in View

Environment
- Umbraco version 7.6.3 assembly: 1.0.6361.21154
- VS2015


Value = @UmbracoContext.Current.PageId


Put the page id value in hidden tag for post back purpose
@Html.HiddenFor(m => m.currentPageId, new { Value = @UmbracoContext.Current.PageId })

How to use Umbraco Logger Helper

How to use Umbraco Logger Helper

Environment
- Umbraco version 7.6.3 assembly: 1.0.6361.21154
- VS2015


using Umbraco.Core.Logging;

LogHelper.Info<Your Class Name>("Your Message");

2017年8月2日 星期三

How to use Umbraco Vorto

How to get value from Vorto datatype in umbraco 


Environment
- Umbraco version 7.6.3 assembly: 1.0.6361.21154
- VS2015
- Vorto 1.5.3

using Our.Umbraco.Vorto.Extensions;
using Umbraco.Core.Models;

namespace Intranet.Library.Helpers
{
    public static class VortoHelper
    {
        public static T GetVortoValue<T>(IPublishedContent currentPage, string propertyAlias, string cultureName = null)
        {
            T t;

            t = currentPage.HasVortoValue(propertyAlias, cultureName) == true ? currentPage.GetVortoValue<T>(propertyAlias, cultureName) : currentPage.GetVortoValue<T>(propertyAlias, CommonHelper.GetDefaultCulture());
            
            return t;
        }
    }
}

Reference:
- https://our.umbraco.org/projects/backoffice-extensions/vorto/
- https://24days.in/umbraco-cms/2015/multilingual-vorto-nested-content/

Custom Validation Error Message

Environment

  • VS 2015
  • DOT Net 4.5
  • C#
  • MVC


Background
You may want to custom the error message for support mulit-language


Original

Model:
public class ContactForm
{                
 [Required(ErrorMessage = "Your own custom message")]
 [Display(Name = "First Name")]
 public string FirstName { get; set; }

 [Required]
 [Display(Name = "Last Name")]
 public string LastName { get; set; }

 [Required]
 [EmailAddress(ErrorMessage = "You must provide a valid email address")]
 [Display(Name = "Email Address")]
 public string EmailAddress { get; set; }
}

View:
@inherits System.Web.Mvc.WebViewPage<ContactForm>

<div id="fh5co-contact-section">
    <div class="container">         
  @{ Html.RenderAction("RenderContactForm", "Contact");}                    
    </div>        
</div>


Partial View:
@inherits System.Web.Mvc.WebViewPage<ContactForm>

<div id="formOuter">
    @using (Ajax.BeginForm("SubmitForm", "Contact", null, new AjaxOptions
    {
        HttpMethod = "POST",
        InsertionMode = InsertionMode.Replace,
        UpdateTargetId = "formOuter"
    }))
    {
        @Html.AntiForgeryToken()
        
        <div class="col-md-8 col-md-push-1 col-sm-12 col-sm-push-0 col-xs-12 col-xs-push-0">
            <div class="row">
                <div class="col-md-6">
                    <div class="form-group">
                        @Html.TextBoxFor(m => m.FirstName, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.FirstName, "", new { @class = "text-danger" })
                    </div>
                </div>
                <div class="col-md-6">
                    <div class="form-group">
                        @Html.TextBoxFor(m => m.LastName, new { @class = "form-control" } )
                        @Html.ValidationMessageFor(m => m.LastName, "", new { @class = "text-danger" })
                    </div>
                </div>
                <div class="col-md-12">
                    <div class="form-group">
                        @Html.TextBoxFor(m => m.EmailAddress, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.EmailAddress, "", new { @class = "text-danger" })                        
                    </div>
                </div>
                <div class="col-md-12">
                    <div class="form-group">
                        @Html.TextAreaFor(m => m.Message, new { @class = "form-control", id = "", cols = "30", rows = "7" })
                        @Html.ValidationMessageFor(m => m.Message, "", new { @class = "text-danger" })
                    </div>
                </div>
                <div class="col-md-12">                    
                    <br />
                    <div class="form-group">
                        <button value="Send Message" class="btn btn-primary use-ajax" type="submit">SendMessage</button>
                    </div>
                </div>
            </div>
        </div>
    }
</div>


Controller:
public ActionResult RenderContactForm()
{
 ContactForm model = new ContactForm();
 return PartialView("_ContactForm.cshtml", model);
}



After Implement Custom MetatdataProvider

Model:
public class ContactForm
{                
 [Required(ErrorMessage = "Your own custom message")]
 [Display(Name = "First Name")]
 public string FirstName { get; set; }

 [Required]
 [Display(Name = "Last Name")]
 public string LastName { get; set; }

 [Required]
 [EmailAddress(ErrorMessage = "You must provide a valid email address")]
 [Display(Name = "Email Address")]
 public string EmailAddress { get; set; }


 public ContactForm()
 {
           ModelMetadataProviders.Current = new CustomModelMetadataProvider();
 }
}

public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
 private UmbracoContext _umbracoContext;
 public CustomModelMetadataProvider(UmbracoContext umbracoContext)
 {
  _umbracoContext = umbracoContext;
 }

 protected override ModelMetadata CreateMetadata(
              IEnumerable<Attribute> attributes,
              Type containerType,
              Func<object> modelAccessor,
              Type modelType,
              string propertyName)
 {

  var data = base.CreateMetadata(
           attributes,
           containerType,
           modelAccessor,
           modelType,
           propertyName);

  var description = attributes.SingleOrDefault(a => typeof(DescriptionAttribute) == a.GetType());
  if (description != null)
  {
   data.Description = ((DescriptionAttribute)description).Description;
  }

  
  foreach (ValidationAttribute attribute in attributes.OfType<ValidationAttribute>())
  {                
   attribute.ErrorMessage = "Your Own Message for each attribute";
  }

  return data;
 }
 
}

View:
No Change

Partial View:
No Change

Controller:
No Change

After Integrate with Umbraco

Model:
public class ContactForm
{
 #region private variable
 private UmbracoContext _umbracoContext;
 #endregion

 #region Form variable
 [Required]
 [Display(Name = "First Name")]
 public string FirstName { get; set; }

 [Required]
 [Display(Name = "Last Name")]
 public string LastName { get; set; }

 [Required]
 [EmailAddress(ErrorMessage = "You must provide a valid email address")]
 [Display(Name = "Email Address")]
 public string EmailAddress { get; set; }

 [Required]
 [Display(Name = "Message")]
 public string Message { get; set; }
 #endregion
 
 public ContactForm(UmbracoContext umbracoContext)
 {
  _umbracoContext = umbracoContext;
  ModelMetadataProviders.Current = new CustomModelMetadataProvider(_umbracoContext);            
 }
}

public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
 private UmbracoContext _umbracoContext;
 public CustomModelMetadataProvider(UmbracoContext umbracoContext)
 {
  _umbracoContext = umbracoContext;
 }

 protected override ModelMetadata CreateMetadata(
              IEnumerable<Attribute> attributes,
              Type containerType,
              Func<object> modelAccessor,
              Type modelType,
              string propertyName)
 {

  var data = base.CreateMetadata(
           attributes,
           containerType,
           modelAccessor,
           modelType,
           propertyName);

  var description = attributes.SingleOrDefault(a => typeof(DescriptionAttribute) == a.GetType());
  if (description != null)
  {
   data.Description = ((DescriptionAttribute)description).Description;
  }

  
  foreach (ValidationAttribute attribute in attributes.OfType<ValidationAttribute>())
  {                
   attribute.ErrorMessage = GetDictionaryValue("Your Umbraco Dictionary Key", "en-US", _umbracoContext);;
  }

  return data;
 }
 
}


View:
@inherits UmbracoViewPage<ContactForm>

<div id="fh5co-contact-section">
    <div class="container">
        <div class="row">
            @{ Html.RenderAction("RenderContactForm", "Contact");}            
        </div>
    </div>    
</div>


Partial View:
@inherits UmbracoViewPage<ContactForm>

<div id="formOuter">
    @using (Ajax.BeginForm("SubmitForm", "Contact", null, new AjaxOptions
    {
        HttpMethod = "POST",
        InsertionMode = InsertionMode.Replace,
        UpdateTargetId = "formOuter"
    }))
    {
        @Html.AntiForgeryToken()
        
        <div class="col-md-8 col-md-push-1 col-sm-12 col-sm-push-0 col-xs-12 col-xs-push-0">
            <div class="row">
                <div class="col-md-6">
                    <div class="form-group">
                        @Html.TextBoxFor(m => m.FirstName, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.FirstName, "", new { @class = "text-danger" })
                    </div>
                </div>
                <div class="col-md-6">
                    <div class="form-group">
                        @Html.TextBoxFor(m => m.LastName, new { @class = "form-control" } )
                        @Html.ValidationMessageFor(m => m.LastName, "", new { @class = "text-danger" })
                    </div>
                </div>
                <div class="col-md-12">
                    <div class="form-group">
                        @Html.TextBoxFor(m => m.EmailAddress, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.EmailAddress, "", new { @class = "text-danger" })                        
                    </div>
                </div>
                <div class="col-md-12">
                    <div class="form-group">
                        @Html.TextAreaFor(m => m.Message, new { @class = "form-control", id = "", cols = "30", rows = "7" })
                        @Html.ValidationMessageFor(m => m.Message, "", new { @class = "text-danger" })
                    </div>
                </div>
                <div class="col-md-12">                    
                    <br />
                    <div class="form-group">
                        <button value="Send Message" class="btn btn-primary use-ajax" type="submit">SendMessage</button>
                    </div>
                </div>
            </div>
        </div>
    }
</div>


Controller:
public ActionResult RenderContactForm()
{
 ContactForm model = new ContactForm(_umbracoContext);
 return PartialView("_ContactForm.cshtml", model);
}