From 1cd15fbf600d0017d0f8f3dad25b08eea1ae467b Mon Sep 17 00:00:00 2001 From: dougbu Date: Mon, 7 Apr 2014 12:04:28 -0700 Subject: [PATCH] Add form HTML helpers and `MVCForm` - copy over from legacy MVC - fixup namespaces, remove copyright notices, ... - temporarily remove `RouteBeginForm() overloads - move main components to `HtmlHelper` and declare in `IHtmlHelper` - change `UrlHelper` to avoid treating an existing dictionary as an object - add `HtmlHelper.Createform()` factory - use in MVC sample; move BeginForm / TextBox samples to a new row --- .../MvcSample.Web/Views/Shared/MyView.cshtml | 44 ++++++---- src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs | 10 ++- .../Html/HtmlHelper.cs | 86 ++++++++++++++++++- .../Html/MvcForm.cs | 49 +++++++++++ .../HtmlHelperFormExtensions.cs | 67 +++++++++++++++ .../IHtmlHelperOfT.cs | 23 +++++ 6 files changed, 259 insertions(+), 20 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Rendering/Html/MvcForm.cs create mode 100644 src/Microsoft.AspNet.Mvc.Rendering/HtmlHelperFormExtensions.cs diff --git a/samples/MvcSample.Web/Views/Shared/MyView.cshtml b/samples/MvcSample.Web/Views/Shared/MyView.cshtml index c299c1628e..de853dd191 100644 --- a/samples/MvcSample.Web/Views/Shared/MyView.cshtml +++ b/samples/MvcSample.Web/Views/Shared/MyView.cshtml @@ -134,8 +134,15 @@ -
-
+
+ @await Component.InvokeAsync("Tags", 15) +
+
+ +
+
+ @using (Html.BeginForm(controllerName: "Home", actionName: "Hello", method: FormMethod.Post)) + { + + + + +
@@ -153,6 +160,16 @@ @Html.TextBoxFor(m => m.Address, htmlAttributes: new { @class = "form-control" })
+ +
+ } + @{ Html.BeginForm(method: FormMethod.Post, htmlAttributes: new { someAttribute = "some value", }); } + + + + +
@@ -191,21 +208,17 @@ @Html.TextBox("Anon.Address.Country", value: null, format: "{0} (4)", - htmlAttributes: new { @class = "form-control" }) + htmlAttributes: new { @class = "form-control" })
+ +
- -
-
- -
-
- -
- -
- @await Component.InvokeAsync("Tags", 15) + @{ Html.EndForm(); }
@@ -318,5 +331,4 @@
@Html.DisplayForModel()
- - \ No newline at end of file + diff --git a/src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs b/src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs index 93da7ffdff..f42183b3bf 100644 --- a/src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs @@ -22,7 +22,15 @@ namespace Microsoft.AspNet.Mvc public string Action(string action, string controller, object values) { - var valuesDictionary = new RouteValueDictionary(values); + var valuesDictionary = values as IDictionary; + if (valuesDictionary == null) + { + valuesDictionary = new RouteValueDictionary(values); + } + else + { + valuesDictionary = new RouteValueDictionary(valuesDictionary); + } if (action != null) { diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs index dc0011e0f8..9647b95e49 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs @@ -139,6 +139,29 @@ namespace Microsoft.AspNet.Mvc.Rendering ViewContext = viewContext; } + public MvcForm BeginForm(string actionName, string controllerName, object routeValues, FormMethod method, + object htmlAttributes) + { + // Only need a dictionary if htmlAttributes is non-null. TagBuilder.MergeAttributes() is fine with null. + IDictionary htmlAttributeDictionary = null; + if (htmlAttributes != null) + { + htmlAttributeDictionary = htmlAttributes as IDictionary; + if (htmlAttributeDictionary == null) + { + htmlAttributeDictionary = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes); + } + } + + return GenerateForm(actionName, controllerName, routeValues, method, htmlAttributeDictionary); + } + + public void EndForm() + { + var mvcForm = CreateForm(); + mvcForm.EndForm(); + } + public string Encode(string value) { return (!string.IsNullOrEmpty(value)) ? WebUtility.HtmlEncode(value) : string.Empty; @@ -149,7 +172,6 @@ namespace Microsoft.AspNet.Mvc.Rendering return value != null ? WebUtility.HtmlEncode(value.ToString()) : string.Empty; } - /// public string FormatValue(object value, string format) { return ViewDataDictionary.FormatValue(value, format); @@ -183,7 +205,6 @@ namespace Microsoft.AspNet.Mvc.Rendering additionalViewData); } - /// public virtual HtmlString Name(string name) { var fullName = ViewData.TemplateInfo.GetFullHtmlFieldName(name); @@ -355,7 +376,6 @@ namespace Microsoft.AspNet.Mvc.Rendering } } - /// public HtmlString TextBox(string name, object value, string format, IDictionary htmlAttributes) { return GenerateTextBox(metadata: null, name: name, value: value, format: format, @@ -367,6 +387,16 @@ namespace Microsoft.AspNet.Mvc.Rendering 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 string EvalString(string key, string format) { return Convert.ToString(ViewData.Eval(key, format), CultureInfo.CurrentCulture); @@ -397,6 +427,56 @@ namespace Microsoft.AspNet.Mvc.Rendering return new Dictionary(); } + /// + /// Writes an opening
tag to the response. When the user submits the form, + /// the request will be processed by an action method. + ///
+ /// The name of the action method. + /// The name of the controller. + /// An object that contains the parameters for a route. The parameters are retrieved + /// through reflection by examining the properties of the object. This object is typically created using object + /// initializer syntax. Alternatively, an instance containing the + /// route parameters. + /// The HTTP method for processing the form, either GET or POST. + /// An instance containing HTML + /// attributes to set for the element. + /// An instance which emits the closing {form} tag when disposed. + protected virtual MvcForm GenerateForm(string actionName, string controllerName, object routeValues, + FormMethod method, IDictionary htmlAttributes) + { + var urlHelper = ViewContext.Url; + var formAction = urlHelper.Action(action: actionName, controller: controllerName, values: routeValues); + + var tagBuilder = new TagBuilder("form"); + tagBuilder.MergeAttributes(htmlAttributes); + + // action is implicitly generated, so htmlAttributes take precedence. + tagBuilder.MergeAttribute("action", formAction); + + // method is an explicit parameter, so it takes precedence over the htmlAttributes. + tagBuilder.MergeAttribute("method", HtmlHelper.GetFormMethodString(method), replaceExisting: true); + + var traditionalJavascriptEnabled = ViewContext.ClientValidationEnabled && + !ViewContext.UnobtrusiveJavaScriptEnabled; + if (traditionalJavascriptEnabled) + { + // TODO revive ViewContext.FormIdGenerator(), WebFx-199 + // forms must have an ID for client validation + var formName = "form" + new Guid().ToString(); + tagBuilder.GenerateId(formName, IdAttributeDotReplacement); + } + + ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag)); + var theForm = CreateForm(); + + if (traditionalJavascriptEnabled) + { + ViewContext.FormContext.FormId = tagBuilder.Attributes["id"]; + } + + return theForm; + } + protected virtual HtmlString GenerateTextBox(ModelMetadata metadata, string name, object value, string format, IDictionary htmlAttributes) { diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Html/MvcForm.cs b/src/Microsoft.AspNet.Mvc.Rendering/Html/MvcForm.cs new file mode 100644 index 0000000000..3f28d1bd08 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/Html/MvcForm.cs @@ -0,0 +1,49 @@ +using System; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public class MvcForm : IDisposable + { + private readonly ViewContext _viewContext; + private bool _disposed; + + public MvcForm([NotNull] ViewContext viewContext) + { + _viewContext = viewContext; + + // Push the new FormContext; GenerateEndForm() does the corresponding pop. + _viewContext.FormContext = new FormContext(); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Renders the closing tag to the response. + /// + public void EndForm() + { + Dispose(disposing: true); + } + + protected virtual void GenerateEndForm() + { + _viewContext.Writer.Write(""); + + // TODO revive viewContext.OutputClientValidation(), this requires GetJsonValidationMetadata(), GitHub #163 + _viewContext.FormContext = null; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + GenerateEndForm(); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Rendering/HtmlHelperFormExtensions.cs b/src/Microsoft.AspNet.Mvc.Rendering/HtmlHelperFormExtensions.cs new file mode 100644 index 0000000000..74ce95ce5a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/HtmlHelperFormExtensions.cs @@ -0,0 +1,67 @@ + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public static class HtmlHelperFormExtensions + { + public static MvcForm BeginForm([NotNull] this IHtmlHelper htmlHelper) + { + // Generates
. + return htmlHelper.BeginForm(actionName: null, controllerName: null, routeValues: null, + method: FormMethod.Post, htmlAttributes: null); + } + + public static MvcForm BeginForm([NotNull] this IHtmlHelper htmlHelper, FormMethod method) + { + return htmlHelper.BeginForm(actionName: null, controllerName: null, routeValues: null, + method: method, htmlAttributes: null); + } + + public static MvcForm BeginForm([NotNull] this IHtmlHelper htmlHelper, FormMethod method, + object htmlAttributes) + { + return htmlHelper.BeginForm(actionName: null, controllerName: null, routeValues: null, + method: method, htmlAttributes: htmlAttributes); + } + + public static MvcForm BeginForm([NotNull] this IHtmlHelper htmlHelper, object routeValues) + { + return htmlHelper.BeginForm(actionName: null, controllerName: null, routeValues: routeValues, + method: FormMethod.Post, htmlAttributes: null); + } + + public static MvcForm BeginForm([NotNull] this IHtmlHelper htmlHelper, string actionName, + string controllerName) + { + return htmlHelper.BeginForm(actionName, controllerName, routeValues: null, + method: FormMethod.Post, htmlAttributes: null); + } + + public static MvcForm BeginForm([NotNull] this IHtmlHelper htmlHelper, string actionName, + string controllerName, object routeValues) + { + return htmlHelper.BeginForm(actionName, controllerName, routeValues, + FormMethod.Post, htmlAttributes: null); + } + + public static MvcForm BeginForm([NotNull] this IHtmlHelper htmlHelper, string actionName, + string controllerName, FormMethod method) + { + return htmlHelper.BeginForm(actionName, controllerName, routeValues: null, + method: method, htmlAttributes: null); + } + + public static MvcForm BeginForm([NotNull] this IHtmlHelper htmlHelper, string actionName, + string controllerName, object routeValues, FormMethod method) + { + return htmlHelper.BeginForm(actionName, controllerName, routeValues, + method, htmlAttributes: null); + } + + public static MvcForm BeginForm([NotNull] this IHtmlHelper htmlHelper, string actionName, + string controllerName, FormMethod method, object htmlAttributes) + { + return htmlHelper.BeginForm(actionName, controllerName, routeValues: null, + method: method, htmlAttributes: htmlAttributes); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Rendering/IHtmlHelperOfT.cs b/src/Microsoft.AspNet.Mvc.Rendering/IHtmlHelperOfT.cs index 4302135189..b00ec094b6 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/IHtmlHelperOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/IHtmlHelperOfT.cs @@ -31,6 +31,29 @@ namespace Microsoft.AspNet.Mvc.Rendering /// ViewDataDictionary ViewData { get; } + /// + /// Writes an opening tag to the response. When the user submits the form, + /// the request will be processed by an action method. + /// + /// The name of the action method. + /// The name of the controller. + /// An object that contains the parameters for a route. The parameters are retrieved + /// through reflection by examining the properties of the object. This object is typically created using object + /// initializer syntax. Alternatively, an instance containing the + /// route parameters. + /// The HTTP method for processing the form, either GET or POST. + /// An object that contains the HTML attributes to set for the element. + /// Alternatively, an instance containing the HTML attributes. + /// + /// An instance which emits the closing {form} tag when disposed. + MvcForm BeginForm(string actionName, string controllerName, object routeValues, FormMethod method, + object htmlAttributes); + + /// + /// Renders the closing tag to the response. + /// + void EndForm(); + /// /// Returns HTML markup for each property in the object that is represented by the expression, using the specified /// template, HTML field ID, and additional view data.