using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq.Expressions; using System.Net; using System.Reflection; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Rendering.Expressions; namespace Microsoft.AspNet.Mvc.Rendering { /// /// Default implementation of non-generic portions of . /// public class HtmlHelper : ICanHasViewContext { public static readonly string ValidationInputCssClassName = "input-validation-error"; public static readonly string ValidationInputValidCssClassName = "input-validation-valid"; public static readonly string ValidationMessageCssClassName = "field-validation-error"; public static readonly string ValidationMessageValidCssClassName = "field-validation-valid"; public static readonly string ValidationSummaryCssClassName = "validation-summary-errors"; public static readonly string ValidationSummaryValidCssClassName = "validation-summary-valid"; private const string HiddenListItem = @"
  • "; private ViewContext _viewContext; private IViewEngine _viewEngine; /// /// Initializes a new instance of the class. /// public HtmlHelper([NotNull] IViewEngine viewEngine, [NotNull] IModelMetadataProvider metadataProvider) { _viewEngine = viewEngine; MetadataProvider = metadataProvider; // Underscores are fine characters in id's. IdAttributeDotReplacement = "_"; } public IModelMetadataProvider MetadataProvider { get; private set; } public string IdAttributeDotReplacement { get; set; } public HttpContext HttpContext { get; private set; } public ViewContext ViewContext { get { if (_viewContext == null) { throw new InvalidOperationException(Resources.HtmlHelper_NotContextualized); } return _viewContext; } private set { _viewContext = value; } } public dynamic ViewBag { get { return ViewContext.ViewBag; } } public ViewDataDictionary ViewData { get { return ViewContext.ViewData; } } protected IModelMetadataProvider MetadataProvider { get; private set; } /// /// Creates a dictionary from an object, by adding each public instance property as a key with its associated /// value to the dictionary. It will expose public properties from derived types as well. This is typically used /// with objects of an anonymous type. /// /// /// new { property_name = "value" } will translate to the entry { "property_name" , "value" } /// in the resulting dictionary. /// /// The object to be converted. /// The created dictionary of property names and property values. public static IDictionary ObjectToDictionary(object obj) { IDictionary result; var valuesAsDictionary = obj as IDictionary; if (valuesAsDictionary != null) { result = new Dictionary(valuesAsDictionary, StringComparer.OrdinalIgnoreCase); } else { result = new Dictionary(StringComparer.OrdinalIgnoreCase); if (obj != null) { foreach (var prop in obj.GetType().GetRuntimeProperties()) { var value = prop.GetValue(obj); result.Add(prop.Name, value); } } } return result; } /// /// Creates a dictionary of HTML attributes from the input object, /// translating underscores to dashes. /// /// new { data_name="value" } will translate to the entry { "data-name" , "value" } /// in the resulting dictionary. /// /// /// Anonymous object describing HTML attributes. /// A dictionary that represents HTML attributes. public static IDictionary AnonymousObjectToHtmlAttributes(object htmlAttributes) { // NOTE: This should be doing more than just returning a generic conversion from obj -> dict // Once GitHub #80 has been completed this will do more than be a call through. return ObjectToDictionary(htmlAttributes); } public virtual void Contextualize([NotNull] ViewContext viewContext) { ViewContext = viewContext; } public string Encode(string value) { return (!string.IsNullOrEmpty(value)) ? WebUtility.HtmlEncode(value) : string.Empty; } public string Encode(object value) { return value != null ? WebUtility.HtmlEncode(value.ToString()) : string.Empty; } /// public string FormatValue(object value, string format) { return ViewDataDictionary.FormatValue(value, format); } public string GenerateIdFromName([NotNull] string name) { return TagBuilder.CreateSanitizedId(name, IdAttributeDotReplacement); } public HtmlString Display(string expression, string templateName, string htmlFieldName, object additionalViewData) { var metadata = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); return GenerateDisplay(metadata, htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), templateName, additionalViewData); } /// public virtual HtmlString Name(string name) { var fullName = ViewData.TemplateInfo.GetFullHtmlFieldName(name); return new HtmlString(Encode(fullName)); } public async Task PartialAsync([NotNull] string partialViewName, object model, ViewDataDictionary viewData) { using (var writer = new StringWriter(CultureInfo.CurrentCulture)) { await RenderPartialCoreAsync(partialViewName, model, viewData, writer); return new HtmlString(writer.ToString()); } } public Task RenderPartialAsync([NotNull] string partialViewName, object model, ViewDataDictionary viewData) { return RenderPartialCoreAsync(partialViewName, model, viewData, ViewContext.Writer); } protected virtual HtmlString GenerateDisplay(ModelMetadata metadata, string htmlFieldName, string templateName, object additionalViewData) { var templateBuilder = new TemplateBuilder(_viewEngine, ViewContext, ViewData, metadata, templateName, templateName, readOnly: true, additionalViewData: additionalViewData); var templateResult = templateBuilder.Build(); return new HtmlString(templateResult); } protected virtual async Task RenderPartialCoreAsync([NotNull] string partialViewName, object model, ViewDataDictionary viewData, TextWriter writer) { // Determine which ViewData we should use to construct a new ViewData var baseViewData = viewData ?? ViewData; var newViewData = new ViewDataDictionary(baseViewData, model); var newViewContext = new ViewContext(ViewContext) { ViewData = newViewData, Writer = writer }; var viewEngineResult = _viewEngine.FindPartialView(newViewContext.ViewEngineContext, partialViewName); await viewEngineResult.View.RenderAsync(newViewContext); } public virtual HtmlString ValidationSummary(bool excludePropertyErrors, string message, IDictionary htmlAttributes) { var formContext = ViewContext.ClientValidationEnabled ? ViewContext.FormContext : null; if (ViewData.ModelState.IsValid == true) { if (formContext == null || ViewContext.UnobtrusiveJavaScriptEnabled && excludePropertyErrors) { // No client side validation/updates return HtmlString.Empty; } } string messageSpan; if (!string.IsNullOrEmpty(message)) { var spanTag = new TagBuilder("span"); spanTag.SetInnerText(message); messageSpan = spanTag.ToString(TagRenderMode.Normal) + Environment.NewLine; } else { messageSpan = null; } var htmlSummary = new StringBuilder(); var modelStates = ValidationHelpers.GetModelStateList(ViewData, excludePropertyErrors); foreach (var modelState in modelStates) { foreach (var modelError in modelState.Errors) { string errorText = ValidationHelpers.GetUserErrorMessageOrDefault(modelError, modelState: null); if (!string.IsNullOrEmpty(errorText)) { var listItem = new TagBuilder("li"); listItem.SetInnerText(errorText); htmlSummary.AppendLine(listItem.ToString(TagRenderMode.Normal)); } } } if (htmlSummary.Length == 0) { htmlSummary.AppendLine(HiddenListItem); } var unorderedList = new TagBuilder("ul") { InnerHtml = htmlSummary.ToString() }; var divBuilder = new TagBuilder("div"); divBuilder.MergeAttributes(htmlAttributes); if (ViewData.ModelState.IsValid == true) { divBuilder.AddCssClass(HtmlHelper.ValidationSummaryValidCssClassName); } else { divBuilder.AddCssClass(HtmlHelper.ValidationSummaryCssClassName); } divBuilder.InnerHtml = messageSpan + unorderedList.ToString(TagRenderMode.Normal); if (formContext != null) { if (ViewContext.UnobtrusiveJavaScriptEnabled) { if (!excludePropertyErrors) { // Only put errors in the validation summary if they're supposed to be included there divBuilder.MergeAttribute("data-valmsg-summary", "true"); } } else { // client validation summaries need an ID divBuilder.GenerateId("validationSummary", IdAttributeDotReplacement); formContext.ValidationSummaryId = divBuilder.Attributes["id"]; formContext.ReplaceValidationSummary = !excludePropertyErrors; } } return divBuilder.ToHtmlString(TagRenderMode.Normal); } /// /// Returns the HTTP method that handles form input (GET or POST) as a string. /// /// The HTTP method that handles the form. /// The form method string, either "get" or "post". public static string GetFormMethodString(FormMethod method) { switch (method) { case FormMethod.Get: return "get"; case FormMethod.Post: return "post"; default: return "post"; } } /// public HtmlString TextBox(string name, object value, string format, IDictionary htmlAttributes) { return GenerateTextBox(metadata: null, name: name, value: value, format: format, htmlAttributes: htmlAttributes); } public HtmlString Value([NotNull] string name, string format) { return GenerateValue(name, value: null, format: format, useViewData: true); } protected string EvalString(string key, string format) { return Convert.ToString(ViewData.Eval(key, format), CultureInfo.CurrentCulture); } protected object GetModelStateValue(string key, Type destinationType) { ModelState modelState; if (ViewData.ModelState.TryGetValue(key, out modelState) && modelState.Value != null) { return modelState.Value.ConvertTo(destinationType, culture: null); } return null; } protected IDictionary GetValidationAttributes(string name) { return GetValidationAttributes(name, metadata: null); } // Only render attributes if unobtrusive client-side validation is enabled, and then only if we've // never rendered validation for a field with this name in this form. Also, if there's no form context, // then we can't render the attributes (we'd have no
    to attach them to). protected IDictionary GetValidationAttributes(string name, ModelMetadata metadata) { // TODO: Add validation attributes to input helpers. return new Dictionary(); } protected virtual HtmlString GenerateTextBox(ModelMetadata metadata, string name, object value, string format, IDictionary htmlAttributes) { return GenerateInput(InputType.Text, metadata, name, value, useViewData: (metadata == null && value == null), isChecked: false, setId: true, isExplicitValue: true, format: format, htmlAttributes: htmlAttributes); } protected virtual HtmlString GenerateInput(InputType inputType, ModelMetadata metadata, string name, object value, bool useViewData, bool isChecked, bool setId, bool isExplicitValue, string format, IDictionary htmlAttributes) { // Not valid to use TextBoxForModel() in a top-level view; would end up with an unnamed input elements. // But we support the *ForModel() methods in any lower-level template, once HtmlFieldPrefix is non-empty. var fullName = ViewData.TemplateInfo.GetFullHtmlFieldName(name); if (string.IsNullOrEmpty(fullName)) { throw new ArgumentException(Resources.ArgumentNullOrEmpty, "name"); } var tagBuilder = new TagBuilder("input"); tagBuilder.MergeAttributes(htmlAttributes); tagBuilder.MergeAttribute("type", GetInputTypeString(inputType)); tagBuilder.MergeAttribute("name", fullName, replaceExisting: true); var valueParameter = FormatValue(value, format); switch (inputType) { case InputType.Text: default: var attributeValue = (string)GetModelStateValue(fullName, typeof(string)); if (attributeValue == null) { attributeValue = useViewData ? EvalString(fullName, format) : valueParameter; } tagBuilder.MergeAttribute("value", attributeValue, replaceExisting: isExplicitValue); break; } if (setId) { tagBuilder.GenerateId(fullName, IdAttributeDotReplacement); } // If there are any errors for a named field, we add the CSS attribute. ModelState modelState; if (ViewData.ModelState.TryGetValue(fullName, out modelState) && modelState.Errors.Count > 0) { tagBuilder.AddCssClass(ValidationInputCssClassName); } tagBuilder.MergeAttributes(GetValidationAttributes(name, metadata)); return tagBuilder.ToHtmlString(TagRenderMode.SelfClosing); } protected virtual HtmlString GenerateValue(string name, object value, string format, bool useViewData) { var fullName = ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name); var attemptedValue = (string)GetModelStateValue(fullName, typeof(string)); string resolvedValue; if (attemptedValue != null) { // case 1: if ModelState has a value then it's already formatted so ignore format string resolvedValue = attemptedValue; } else if (useViewData) { if (name.Length == 0) { // case 2(a): format the value from ModelMetadata for the current model var metadata = ViewData.ModelMetadata; resolvedValue = FormatValue(metadata.Model, format); } else { // case 2(b): format the value from ViewData resolvedValue = EvalString(name, format); } } else { // case 3: format the explicit value from ModelMetadata resolvedValue = FormatValue(value, format); } return new HtmlString(Encode(resolvedValue)); } private static string GetInputTypeString(InputType inputType) { switch (inputType) { case InputType.CheckBox: return "checkbox"; case InputType.Hidden: return "hidden"; case InputType.Password: return "password"; case InputType.Radio: return "radio"; case InputType.Text: return "text"; default: return "text"; } } public HtmlString Raw(string value) { return new HtmlString(value); } public HtmlString Raw(object value) { return new HtmlString(value == null ? null : value.ToString()); } } }