Inject `HtmlHelper` property into `RazorView<T>`

- add `IHtmlHelper<T>` and `ICanHasViewContext`
- adjust `HtmlHelper` and `HtmlHelper<T>` to match
- throw if `ViewContext` accessed prior to `Contextualize` call
- XML comments (from old world) all around

Note
- no current need for an `HtmlHelper` copy constructor or `Clone()` method
- expect recursion code to get another injected `IHtmlHelper<T>` and then
  "contextualize" that instance with a new `ViewContext`
This commit is contained in:
dougbu 2014-03-27 15:18:18 -07:00
parent 94db3c392a
commit 2b70156cf4
8 changed files with 229 additions and 33 deletions

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNet.Mvc.Razor
public ViewDataDictionary<TModel> ViewData { get; private set; }
public HtmlHelper<TModel> Html { get; set; }
public IHtmlHelper<TModel> Html { get; set; }
public override Task RenderAsync([NotNull] ViewContext context)
{
@ -46,7 +46,13 @@ namespace Microsoft.AspNet.Mvc.Razor
private void InitHelpers(ViewContext context)
{
Html = new HtmlHelper<TModel>(context.HttpContext, ViewData);
Html = context.ServiceProvider.GetService<IHtmlHelper<TModel>>();
var contextable = Html as ICanHasViewContext;
if (contextable != null)
{
contextable.Contextualize(context);
}
}
}
}

View File

@ -8,7 +8,10 @@ using Microsoft.AspNet.Abstractions;
namespace Microsoft.AspNet.Mvc.Rendering
{
public class HtmlHelper
/// <summary>
/// Default implementation of non-generic portions of <see cref="IHtmlHelper{T}">.
/// </summary>
public class HtmlHelper : ICanHasViewContext
{
public static readonly string ValidationInputCssClassName = "input-validation-error";
public static readonly string ValidationInputValidCssClassName = "input-validation-valid";
@ -17,11 +20,13 @@ namespace Microsoft.AspNet.Mvc.Rendering
public static readonly string ValidationSummaryCssClassName = "validation-summary-errors";
public static readonly string ValidationSummaryValidCssClassName = "validation-summary-valid";
public HtmlHelper([NotNull] HttpContext httpContext, ViewDataDictionary viewData)
{
HttpContext = httpContext;
ViewData = viewData;
private ViewContext _viewContext;
/// <summary>
/// Initializes a new instance of the <see cref="HtmlHelper"/> class.
/// </summary>
public HtmlHelper()
{
// Underscores are fine characters in id's.
IdAttributeDotReplacement = "_";
}
@ -30,10 +35,37 @@ namespace Microsoft.AspNet.Mvc.Rendering
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;
private set;
get
{
return ViewContext.ViewData;
}
}
/// <summary>
@ -71,6 +103,11 @@ namespace Microsoft.AspNet.Mvc.Rendering
return result;
}
public virtual void Contextualize([NotNull] ViewContext viewContext)
{
ViewContext = viewContext;
}
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")]
public string Encode(string value)
{
@ -88,6 +125,11 @@ namespace Microsoft.AspNet.Mvc.Rendering
return TagBuilder.CreateSanitizedId(name, IdAttributeDotReplacement);
}
/// <summary>
/// Returns the HTTP method that handles form input (GET or POST) as a string.
/// </summary>
/// <param name="method">The HTTP method that handles the form.</param>
/// <returns>The form method string, either "get" or "post".</returns>
public static string GetFormMethodString(FormMethod method)
{
switch (method)
@ -120,24 +162,12 @@ namespace Microsoft.AspNet.Mvc.Rendering
}
}
/// <summary>
/// Wraps HTML markup in an IHtmlString, which will enable HTML markup to be
/// rendered to the output without getting HTML encoded.
/// </summary>
/// <param name="value">HTML markup string.</param>
/// <returns>An IHtmlString that represents HTML markup.</returns>
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")]
public HtmlString Raw(string value)
{
return new HtmlString(value);
}
/// <summary>
/// Wraps HTML markup from the string representation of an object in an IHtmlString,
/// which will enable HTML markup to be rendered to the output without getting HTML encoded.
/// </summary>
/// <param name="value">object with string representation as HTML markup</param>
/// <returns>An IHtmlString that represents HTML markup.</returns>
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")]
public HtmlString Raw(object value)
{

View File

@ -1,18 +1,40 @@
using Microsoft.AspNet.Abstractions;
using System;
using Microsoft.AspNet.Abstractions;
namespace Microsoft.AspNet.Mvc.Rendering
{
public class HtmlHelper<TModel> : HtmlHelper
public class HtmlHelper<TModel> : HtmlHelper, IHtmlHelper<TModel>
{
public HtmlHelper([NotNull]HttpContext httpContext, ViewDataDictionary<TModel> viewData)
: base(httpContext, viewData)
/// <summary>
/// Initializes a new instance of the <see cref="HtmlHelper{TModel}"/> class.
/// </summary>
public HtmlHelper()
: base()
{
ViewData = viewData;
}
public new ViewDataDictionary<TModel> ViewData
/// <inheritdoc />
public new ViewDataDictionary<TModel> ViewData { get; private set;}
public override void Contextualize([NotNull] ViewContext viewContext)
{
get; private set;
if (viewContext.ViewData == null)
{
throw new ArgumentException(Resources.FormatArgumentPropertyNull("ViewData"), "viewContext");
}
ViewData = viewContext.ViewData as ViewDataDictionary<TModel>;
if (ViewData == null)
{
// viewContext may contain a base ViewDataDictionary instance. So complain about that type, not TModel.
throw new ArgumentException(Resources.FormatArgumentPropertyUnexpectedType(
"ViewData",
viewContext.ViewData.GetType().FullName,
typeof(ViewDataDictionary<TModel>).FullName),
"viewContext");
}
base.Contextualize(viewContext);
}
}
}

View File

@ -0,0 +1,7 @@
namespace Microsoft.AspNet.Mvc.Rendering
{
public interface ICanHasViewContext
{
void Contextualize([NotNull] ViewContext viewContext);
}
}

View File

@ -0,0 +1,68 @@
using System;
namespace Microsoft.AspNet.Mvc.Rendering
{
/// <summary>
/// An <see cref="IHtmlHelper"/> for Linq expressions.
/// </summary>
/// <typeparam name="TModel">The <see cref="Type"/> of the model.</typeparam>
public interface IHtmlHelper<TModel>
{
/// <summary>
/// Gets or sets the character that replaces periods in the ID attribute of an element.
/// </summary>
string IdAttributeDotReplacement { get; set; }
/// <summary>
/// Gets the view bag.
/// </summary>
dynamic ViewBag { get; }
/// <summary>
/// Gets the context information about the view.
/// </summary>
ViewContext ViewContext { get; }
/// <summary>
/// Gets the current view data.
/// </summary>
ViewDataDictionary<TModel> ViewData { get; }
/// <summary>
/// Converts the value of the specified object to an HTML-encoded string.
/// </summary>
/// <param name="value">The object to encode.</param>
/// <returns>The HTML-encoded string.</returns>
string Encode(object value);
/// <summary>
/// Converts the specified string to an HTML-encoded string.
/// </summary>
/// <param name="value">The string to encode.</param>
/// <returns>The HTML-encoded string.</returns>
string Encode(string value);
/// <summary>
/// Creates an HTML element ID using the specified element name.
/// </summary>
/// <param name="name">The name of the HTML element.</param>
/// <returns>The ID of the HTML element.</returns>
string GenerateIdFromName(string name);
/// <summary>
/// Wraps HTML markup in an <see cref="HtmlString"/>, which will enable HTML markup to be
/// rendered to the output without getting HTML encoded.
/// </summary>
/// <param name="value">HTML markup string.</param>
/// <returns>An <see cref="HtmlString"/> that represents HTML markup.</returns>
HtmlString Raw(string value);
/// <summary>
/// Wraps HTML markup from the string representation of an object in an <see cref="HtmlString"/>,
/// which will enable HTML markup to be rendered to the output without getting HTML encoded.
/// </summary>
/// <param name="value">object with string representation as HTML markup.</param>
/// <returns>An <see cref="HtmlString"/> that represents HTML markup.</returns>
HtmlString Raw(object value);
}
}

View File

@ -26,6 +26,38 @@ namespace Microsoft.AspNet.Mvc.Rendering
return string.Format(CultureInfo.CurrentCulture, GetString("ArgumentNullOrEmpty"), p0);
}
/// <summary>
/// Property '{0}' must not be null.
/// </summary>
internal static string ArgumentPropertyNull
{
get { return GetString("ArgumentPropertyNull"); }
}
/// <summary>
/// Property '{0}' must not be null.
/// </summary>
internal static string FormatArgumentPropertyNull(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ArgumentPropertyNull"), p0);
}
/// <summary>
/// Property '{0}' is of type '{1}', but this method requires a value of type '{2}'.
/// </summary>
internal static string ArgumentPropertyUnexpectedType
{
get { return GetString("ArgumentPropertyUnexpectedType"); }
}
/// <summary>
/// Property '{0}' is of type '{1}', but this method requires a value of type '{2}'.
/// </summary>
internal static string FormatArgumentPropertyUnexpectedType(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ArgumentPropertyUnexpectedType"), p0, p1, p2);
}
/// <summary>
/// The partial view '{0}' was not found or no view engine supports the searched locations. The following locations were searched:{1}
/// </summary>
@ -59,7 +91,23 @@ namespace Microsoft.AspNet.Mvc.Rendering
}
/// <summary>
/// The model item passed is null, but this ViewData instance requires a non-null model item of type '{0}'.
/// Must call 'Contextualize' method before using this HtmlHelper instance.
/// </summary>
internal static string HtmlHelper_NotContextualized
{
get { return GetString("HtmlHelper_NotContextualized"); }
}
/// <summary>
/// Must call 'Contextualize' method before using this HtmlHelper instance.
/// </summary>
internal static string FormatHtmlHelper_NotContextualized()
{
return GetString("HtmlHelper_NotContextualized");
}
/// <summary>
/// The model item passed is null, but this ViewDataDictionary instance requires a non-null model item of type '{0}'.
/// </summary>
internal static string ViewData_ModelCannotBeNull
{
@ -67,7 +115,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
}
/// <summary>
/// The model item passed is null, but this ViewData instance requires a non-null model item of type '{0}'.
/// The model item passed is null, but this ViewDataDictionary instance requires a non-null model item of type '{0}'.
/// </summary>
internal static string FormatViewData_ModelCannotBeNull(object p0)
{
@ -75,7 +123,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
}
/// <summary>
/// The model item passed into the ViewData is of type '{0}', but this ViewData instance requires a model item of type '{1}'.
/// The model item passed into the ViewDataDictionary is of type '{0}', but this ViewDataDictionary instance requires a model item of type '{1}'.
/// </summary>
internal static string ViewData_WrongTModelType
{
@ -83,7 +131,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
}
/// <summary>
/// The model item passed into the ViewData is of type '{0}', but this ViewData instance requires a model item of type '{1}'.
/// The model item passed into the ViewDataDictionary is of type '{0}', but this ViewDataDictionary instance requires a model item of type '{1}'.
/// </summary>
internal static string FormatViewData_WrongTModelType(object p0, object p1)
{

View File

@ -120,12 +120,21 @@
<data name="ArgumentNullOrEmpty" xml:space="preserve">
<value>The argument '{0}' is null or empty.</value>
</data>
<data name="ArgumentPropertyNull" xml:space="preserve">
<value>Property '{0}' must not be null.</value>
</data>
<data name="ArgumentPropertyUnexpectedType" xml:space="preserve">
<value>Property '{0}' is of type '{1}', but this method requires a value of type '{2}'.</value>
</data>
<data name="Common_PartialViewNotFound" xml:space="preserve">
<value>The partial view '{0}' was not found or no view engine supports the searched locations. The following locations were searched:{1}</value>
</data>
<data name="DynamicViewData_ViewDataNull" xml:space="preserve">
<value>ViewData value must not be null.</value>
</data>
<data name="HtmlHelper_NotContextualized" xml:space="preserve">
<value>Must call 'Contextualize' method before using this HtmlHelper instance.</value>
</data>
<data name="ViewData_ModelCannotBeNull" xml:space="preserve">
<value>The model item passed is null, but this ViewDataDictionary instance requires a non-null model item of type '{0}'.</value>
</data>

View File

@ -88,7 +88,13 @@ namespace Microsoft.AspNet.Mvc
typeof(NestedProviderManagerAsync<>),
implementationInstance: null,
lifecycle: LifecycleKind.Transient);
yield return
describe.Describe(
typeof(IHtmlHelper<>),
typeof(HtmlHelper<>),
implementationInstance: null,
lifecycle: LifecycleKind.Transient);
}
}
}