// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Rendering.Expressions; namespace Microsoft.AspNet.Mvc.Rendering { /// /// Default implementation of . /// public class HtmlHelper : IHtmlHelper, 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 readonly IHtmlGenerator _htmlGenerator; private readonly ICompositeViewEngine _viewEngine; private ViewContext _viewContext; /// /// Initializes a new instance of the class. /// public HtmlHelper( [NotNull] IHtmlGenerator htmlGenerator, [NotNull] ICompositeViewEngine viewEngine, [NotNull] IModelMetadataProvider metadataProvider) { _viewEngine = viewEngine; _htmlGenerator = htmlGenerator; MetadataProvider = metadataProvider; } /// public Html5DateRenderingMode Html5DateRenderingMode { get { return ViewContext.Html5DateRenderingMode; } set { ViewContext.Html5DateRenderingMode = value; } } /// public string IdAttributeDotReplacement { get { return _htmlGenerator.IdAttributeDotReplacement; } set { _htmlGenerator.IdAttributeDotReplacement = value; } } /// 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; } } /// public 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. /// /// If the object is already an instance, then it is /// returned as-is. /// /// new { data_name="value" } will translate to the entry { "data_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) { return TypeHelper.ObjectToDictionary(obj); } /// /// Creates a dictionary of HTML attributes from the input object, /// translating underscores to dashes in each public instance property. /// /// If the object is already an instance, then it is /// returned as-is. /// /// 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) { var dictionary = htmlAttributes as IDictionary; if (dictionary != null) { return new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); } dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); if (htmlAttributes != null) { foreach (var helper in HtmlAttributePropertyHelper.GetProperties(htmlAttributes)) { dictionary[helper.Name] = helper.GetValue(htmlAttributes); } } return dictionary; } public virtual void Contextualize([NotNull] ViewContext viewContext) { ViewContext = viewContext; } /// public HtmlString ActionLink( [NotNull] string linkText, string actionName, string controllerName, string protocol, string hostname, string fragment, object routeValues, object htmlAttributes) { var tagBuilder = _htmlGenerator.GenerateActionLink( linkText, actionName, controllerName, protocol, hostname, fragment, routeValues, htmlAttributes); if (tagBuilder == null) { return HtmlString.Empty; } return tagBuilder.ToHtmlString(TagRenderMode.Normal); } /// public HtmlString AntiForgeryToken() { var tagBuilder = _htmlGenerator.GenerateAntiForgery(ViewContext); if (tagBuilder == null) { return HtmlString.Empty; } return tagBuilder.ToHtmlString(TagRenderMode.SelfClosing); } /// public MvcForm BeginForm( string actionName, string controllerName, object routeValues, FormMethod method, object htmlAttributes) { return GenerateForm(actionName, controllerName, routeValues, method, htmlAttributes); } /// public MvcForm BeginRouteForm(string routeName, object routeValues, FormMethod method, object htmlAttributes) { return GenerateRouteForm(routeName, routeValues, method, htmlAttributes); } /// public void EndForm() { var mvcForm = CreateForm(); mvcForm.EndForm(); } /// public HtmlString CheckBox(string name, bool? isChecked, object htmlAttributes) { return GenerateCheckBox(metadata: null, name: name, isChecked: isChecked, htmlAttributes: htmlAttributes); } /// public string Encode(string value) { return _htmlGenerator.Encode(value); } /// public string Encode(object value) { return _htmlGenerator.Encode(value); } /// public string FormatValue(object value, string format) { return _htmlGenerator.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 string DisplayName(string expression) { var metadata = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); return GenerateDisplayName(metadata, expression); } /// public string DisplayText(string name) { var metadata = ExpressionMetadataProvider.FromStringExpression(name, ViewData, MetadataProvider); return GenerateDisplayText(metadata); } /// public HtmlString DropDownList(string name, IEnumerable selectList, string optionLabel, object htmlAttributes) { return GenerateDropDown( metadata: null, expression: name, selectList: selectList, optionLabel: optionLabel, htmlAttributes: htmlAttributes); } /// public HtmlString Editor(string expression, string templateName, string htmlFieldName, object additionalViewData) { var metadata = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); return GenerateEditor( metadata, htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), templateName, additionalViewData); } /// public HtmlString Hidden(string name, object value, object htmlAttributes) { return GenerateHidden(metadata: null, name: name, value: value, useViewData: (value == null), htmlAttributes: htmlAttributes); } /// public string Id(string name) { return GenerateId(name); } /// public HtmlString Label(string expression, string labelText, object htmlAttributes) { var metadata = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); return GenerateLabel( metadata, expression, labelText, htmlAttributes); } /// public HtmlString ListBox(string name, IEnumerable selectList, object htmlAttributes) { return GenerateListBox(metadata: null, name: name, selectList: selectList, htmlAttributes: htmlAttributes); } /// public string Name(string name) { return GenerateName(name); } /// public async Task PartialAsync([NotNull] string partialViewName, object model, ViewDataDictionary viewData) { using (var writer = new StringCollectionTextWriter(Encoding.UTF8)) { await RenderPartialCoreAsync(partialViewName, model, viewData, writer); return new HtmlString(writer); } } /// 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, htmlFieldName, 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 viewEngineResult = _viewEngine.FindPartialView(ViewContext, partialViewName); if (!viewEngineResult.Success) { var locations = string.Empty; if (viewEngineResult.SearchedLocations != null) { locations = Environment.NewLine + string.Join(Environment.NewLine, viewEngineResult.SearchedLocations); } throw new InvalidOperationException( Resources.FormatViewEngine_PartialViewNotFound(partialViewName, locations)); } var view = viewEngineResult.View; using (view as IDisposable) { var viewContext = new ViewContext(ViewContext, view, newViewData, writer); await viewEngineResult.View.RenderAsync(viewContext); } } /// public HtmlString Password(string name, object value, object htmlAttributes) { return GeneratePassword(metadata: null, name: name, value: value, htmlAttributes: htmlAttributes); } /// public HtmlString RadioButton(string name, object value, bool? isChecked, object htmlAttributes) { return GenerateRadioButton(metadata: null, name: name, value: value, isChecked: isChecked, htmlAttributes: htmlAttributes); } /// public HtmlString Raw(string value) { return new HtmlString(value); } /// public HtmlString Raw(object value) { return new HtmlString(value == null ? null : value.ToString()); } /// public HtmlString RouteLink( [NotNull] string linkText, string routeName, string protocol, string hostName, string fragment, object routeValues, object htmlAttributes) { var tagBuilder = _htmlGenerator.GenerateRouteLink( linkText, routeName, protocol, hostName, fragment, routeValues, htmlAttributes); if (tagBuilder == null) { return HtmlString.Empty; } return tagBuilder.ToHtmlString(TagRenderMode.Normal); } /// public HtmlString ValidationMessage(string expression, string message, object htmlAttributes, string tag) { return GenerateValidationMessage(expression, message, htmlAttributes, tag); } /// public HtmlString ValidationSummary( bool excludePropertyErrors, string message, object htmlAttributes, string tag) { return GenerateValidationSummary(excludePropertyErrors, message, htmlAttributes, tag); } /// /// 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 TextArea(string name, string value, int rows, int columns, object htmlAttributes) { var metadata = ExpressionMetadataProvider.FromStringExpression(name, ViewData, MetadataProvider); if (value != null) { metadata.Model = value; } return GenerateTextArea(metadata, name, rows, columns, htmlAttributes); } /// public HtmlString TextBox(string name, object value, string format, object htmlAttributes) { return GenerateTextBox(metadata: null, name: name, value: value, format: format, htmlAttributes: htmlAttributes); } /// public string Value(string name, string format) { return GenerateValue(name, value: null, format: format, useViewData: true); } /// /// Override this method to return an subclass. That subclass may change /// behavior. /// /// A new instance. protected virtual MvcForm CreateForm() { return new MvcForm(ViewContext); } protected virtual HtmlString GenerateCheckBox(ModelMetadata metadata, string name, bool? isChecked, object htmlAttributes) { var checkbox = _htmlGenerator.GenerateCheckBox( ViewContext, metadata, name, isChecked, htmlAttributes); var hidden = _htmlGenerator.GenerateHiddenForCheckbox(ViewContext, metadata, name); if (checkbox == null || hidden == null) { return HtmlString.Empty; } var elements = checkbox.ToString(TagRenderMode.SelfClosing) + hidden.ToString(TagRenderMode.SelfClosing); return new HtmlString(elements); } protected virtual string GenerateDisplayName([NotNull] ModelMetadata metadata, string htmlFieldName) { // We don't call ModelMetadata.GetDisplayName here because // we want to fall back to the field name rather than the ModelType. // This is similar to how the GenerateLabel get the text of a label. var resolvedDisplayName = metadata.DisplayName ?? metadata.PropertyName; if (resolvedDisplayName == null) { resolvedDisplayName = string.IsNullOrEmpty(htmlFieldName) ? string.Empty : htmlFieldName.Split('.').Last(); } return resolvedDisplayName; } protected virtual string GenerateDisplayText(ModelMetadata metadata) { return metadata.SimpleDisplayText ?? string.Empty; } protected HtmlString GenerateDropDown(ModelMetadata metadata, string expression, IEnumerable selectList, string optionLabel, object htmlAttributes) { var tagBuilder = _htmlGenerator.GenerateSelect( ViewContext, metadata, optionLabel, name: expression, selectList: selectList, allowMultiple: false, htmlAttributes: htmlAttributes); if (tagBuilder == null) { return HtmlString.Empty; } return tagBuilder.ToHtmlString(TagRenderMode.Normal); } protected virtual HtmlString GenerateEditor(ModelMetadata metadata, string htmlFieldName, string templateName, object additionalViewData) { var templateBuilder = new TemplateBuilder( _viewEngine, ViewContext, ViewData, metadata, htmlFieldName, templateName, readOnly: false, additionalViewData: additionalViewData); var templateResult = templateBuilder.Build(); return new HtmlString(templateResult); } /// /// Renders a <form> start tag to the response. When the user submits the form, the /// method will process the request. /// /// The name of the action method. /// The name of the controller. /// /// An that contains the parameters for a route. The parameters are retrieved through /// reflection by examining the properties of the . This is typically /// created using initializer syntax. Alternatively, an /// instance containing the route parameters. /// /// The HTTP method for processing the form, either GET or POST. /// /// An that contains the HTML attributes for the element. Alternatively, an /// instance containing the HTML attributes. /// /// /// An instance which renders the </form> end tag when disposed. /// /// /// In this context, "renders" means the method writes its output using . /// protected virtual MvcForm GenerateForm( string actionName, string controllerName, object routeValues, FormMethod method, object htmlAttributes) { var tagBuilder = _htmlGenerator.GenerateForm( ViewContext, actionName, controllerName, routeValues, GetFormMethodString(method), htmlAttributes); if (tagBuilder != null) { ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag)); } return CreateForm(); } /// /// Renders a <form> start tag to the response. When the user submits the form, the /// route will forward the request to an action method. /// /// The name of the route. /// /// An that contains the parameters for a route. The parameters are retrieved through /// reflection by examining the properties of the . This is typically /// created using initializer syntax. Alternatively, an /// instance containing the route parameters. /// /// The HTTP method for processing the form, either GET or POST. /// /// An that contains the HTML attributes for the element. Alternatively, an /// instance containing the HTML attributes. /// /// /// An instance which renders the </form> end tag when disposed. /// /// /// In this context, "renders" means the method writes its output using . /// protected virtual MvcForm GenerateRouteForm( string routeName, object routeValues, FormMethod method, object htmlAttributes) { var tagBuilder = _htmlGenerator.GenerateRouteForm( ViewContext, routeName, routeValues, GetFormMethodString(method), htmlAttributes); if (tagBuilder != null) { ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag)); } return CreateForm(); } protected virtual HtmlString GenerateHidden( ModelMetadata metadata, string name, object value, bool useViewData, object htmlAttributes) { var tagBuilder = _htmlGenerator.GenerateHidden( ViewContext, metadata, name, value, useViewData, htmlAttributes); if (tagBuilder == null) { return HtmlString.Empty; } return tagBuilder.ToHtmlString(TagRenderMode.SelfClosing); } protected virtual string GenerateId(string expression) { var fullName = DefaultHtmlGenerator.GetFullHtmlFieldName(ViewContext, name: expression); var id = TagBuilder.CreateSanitizedId(fullName, IdAttributeDotReplacement); return id; } protected virtual HtmlString GenerateLabel([NotNull] ModelMetadata metadata, string htmlFieldName, string labelText, object htmlAttributes) { var tagBuilder = _htmlGenerator.GenerateLabel( ViewContext, metadata, name: htmlFieldName, labelText: labelText, htmlAttributes: htmlAttributes); if (tagBuilder == null) { return HtmlString.Empty; } return tagBuilder.ToHtmlString(TagRenderMode.Normal); } protected HtmlString GenerateListBox( ModelMetadata metadata, string name, IEnumerable selectList, object htmlAttributes) { var tagBuilder = _htmlGenerator.GenerateSelect( ViewContext, metadata, optionLabel: null, name: name, selectList: selectList, allowMultiple: true, htmlAttributes: htmlAttributes); if (tagBuilder == null) { return HtmlString.Empty; } return tagBuilder.ToHtmlString(TagRenderMode.Normal); } protected virtual string GenerateName(string name) { var fullName = DefaultHtmlGenerator.GetFullHtmlFieldName(ViewContext, name); return fullName; } protected virtual HtmlString GeneratePassword(ModelMetadata metadata, string name, object value, object htmlAttributes) { var tagBuilder = _htmlGenerator.GeneratePassword( ViewContext, metadata, name, value, htmlAttributes); if (tagBuilder == null) { return HtmlString.Empty; } return tagBuilder.ToHtmlString(TagRenderMode.SelfClosing); } protected virtual HtmlString GenerateRadioButton(ModelMetadata metadata, string name, object value, bool? isChecked, object htmlAttributes) { var tagBuilder = _htmlGenerator.GenerateRadioButton( ViewContext, metadata, name, value, isChecked, htmlAttributes); if (tagBuilder == null) { return HtmlString.Empty; } return tagBuilder.ToHtmlString(TagRenderMode.SelfClosing); } protected virtual HtmlString GenerateTextArea(ModelMetadata metadata, string name, int rows, int columns, object htmlAttributes) { var tagBuilder = _htmlGenerator.GenerateTextArea( ViewContext, metadata, name, rows, columns, htmlAttributes); if (tagBuilder == null) { return HtmlString.Empty; } return tagBuilder.ToHtmlString(TagRenderMode.Normal); } protected virtual HtmlString GenerateTextBox(ModelMetadata metadata, string name, object value, string format, object htmlAttributes) { var tagBuilder = _htmlGenerator.GenerateTextBox( ViewContext, metadata, name, value, format, htmlAttributes); if (tagBuilder == null) { return HtmlString.Empty; } return tagBuilder.ToHtmlString(TagRenderMode.SelfClosing); } protected virtual HtmlString GenerateValidationMessage(string expression, string message, object htmlAttributes, string tag) { var tagBuilder = _htmlGenerator.GenerateValidationMessage( ViewContext, name: expression, message: message, tag: tag, htmlAttributes: htmlAttributes); if (tagBuilder == null) { return HtmlString.Empty; } return tagBuilder.ToHtmlString(TagRenderMode.Normal); } protected virtual HtmlString GenerateValidationSummary( bool excludePropertyErrors, string message, object htmlAttributes, string tag) { var tagBuilder = _htmlGenerator.GenerateValidationSummary( ViewContext, excludePropertyErrors, message, headerTag: tag, htmlAttributes: htmlAttributes); if (tagBuilder == null) { return HtmlString.Empty; } return tagBuilder.ToHtmlString(TagRenderMode.Normal); } protected virtual string GenerateValue(string name, object value, string format, bool useViewData) { var fullName = DefaultHtmlGenerator.GetFullHtmlFieldName(ViewContext, name); var attemptedValue = (string)DefaultHtmlGenerator.GetModelStateValue(ViewContext, 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 (string.IsNullOrEmpty(name)) { // 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 = DefaultHtmlGenerator.EvalString(ViewContext, name, format); } } else { // case 3: format the explicit value from ModelMetadata resolvedValue = FormatValue(value, format); } return resolvedValue; } /// public IEnumerable GetClientValidationRules( ModelMetadata metadata, string name) { return _htmlGenerator.GetClientValidationRules(ViewContext, metadata, name); } } }