I have a requirement to roll my own BeginLabel helper for Mvc. I checked/stole the concept from the Mvc source for the html.beginForm / ajax.beginForm methods.
public static Label BeginLabel(this HtmlHelper htmlHelper)
{
TagBuilder tagBuilder = new TagBuilder("label");
HttpResponseBase response = htmlHelper.ViewContext.HttpContext.Response;
response.Write(tagBuilder.ToString(TagRenderMode.StartTag));
return new Label(response);
}
The Label simply implements IDisposable interface to enable closing off the label:
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
_disposed = true;
_httpResponse.Write("</label>");
}
}
Usage looks like this:
#using (Html.BeginLabel())
{
#Html.TextBoxFor(f => f.FirstName)
#Html.ValidationMessageFor(f => f.FirstName)
}
It looks like i'm missing something as the labels always get rendered at the top of the html and while this is obvious to me because i'm writing to the response, I can't see how the native BeginForm() is achieving this. Can anyone shed any light on this?
public class MvcLabel : IDisposable
{
// Fields
private bool _disposed;
private readonly TextWriter _writer;
public MvcLabel(ViewContext viewContext)
{
if (viewContext == null)
{
throw new ArgumentNullException("viewContext");
}
this._writer = viewContext.Writer;
}
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!this._disposed)
{
this._disposed = true;
this._writer.Write("</label>");
}
}
public void EndLabel()
{
this.Dispose(true);
}
}
and
public static class HtmlHelperExtension
{
// Methods
public static MvcLabel BeginLabel(this HtmlHelper html, string expression)
{
return html.BeginLabel(expression, null);
}
public static MvcLabel BeginLabel(this HtmlHelper html, string expression, string labelText)
{
return LabelHelper(html, ModelMetadata.FromStringExpression(expression, html.ViewData), expression, labelText);
}
public static MvcLabel BeginLabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
return html.BeginLabelFor<TModel, TValue>(expression, null);
}
public static MvcLabel BeginLabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string labelText)
{
return LabelHelper(html, ModelMetadata.FromLambdaExpression<TModel, TValue>(expression, html.ViewData), ExpressionHelper.GetExpressionText(expression), labelText);
}
public static MvcLabel BeginLabelForModel(this HtmlHelper html)
{
return html.BeginLabelForModel(null);
}
public static MvcLabel BeginLabelForModel(this HtmlHelper html, string labelText)
{
return LabelHelper(html, html.ViewData.ModelMetadata, string.Empty, labelText);
}
public static void EndLabel(this HtmlHelper htmlHelper)
{
htmlHelper.ViewContext.Writer.Write("</label>");
}
internal static MvcLabel LabelHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string labelText = null)
{
string str = labelText ?? (metadata.DisplayName ?? (metadata.PropertyName ?? htmlFieldName.Split(new char[] { '.' }).Last<string>()));
TagBuilder tagBuilder = new TagBuilder("label");
tagBuilder.Attributes.Add("for", TagBuilder.CreateSanitizedId(html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName)));
html.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag));
if (!string.IsNullOrEmpty(str))
{
tagBuilder = new TagBuilder("span");
tagBuilder.SetInnerText(str);
html.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.Normal));
}
return new MvcLabel(html.ViewContext);
}
}
Hope i can help others...
Related
I'm wondering if I need to create a custom HtmlHelper to simply display a value as a span.
Currently I'm using:
#model string
#{
var htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(ViewData["htmlAttributes"]);
}
<span class="#htmlAttributes["class"]">#Model</string>
I'm just wondering if there already exists something like #Html.Label so I could do:
#model string
#Html.SomethingLikeSpan("", Model, #ViewData["htmlAttributes"])
Not yet, but you can extend the Helpers object with any tag combination you wish.
Add a class file named HtmlHelperExtensions.cs with the following code:
using System;
using System.Web;
using System.Web.Mvc;
namespace WebApplication1
{
public static class HtmlHelperExtensions
{
public static IHtmlString Span(this HtmlHelper Helper, string Content, string Class = "")
{
string classstring = Class == "" ? "" : string.Format(" class=\"{0}\" ", Class);
string htmlString = String.Format("<span{1}>{0}</span>", Content, classstring);
return new HtmlString(htmlString);
}
}
}
Then in your view, use the following to format anything with the new helper extension:
#using WebApplication1
#Html.Span("Test Content")
#Html.Span("Test with class", "btn btn-primary")
I know this post is quite old but I'm going to post what I used to accomplish a very basic version of the #Html.Span and #Html.SpanFor helpers. I'll post for both .netcore and MVC5. I hope this helps people out!
MVC 5 implementation
using System;
using System.Linq.Expressions;
using System.Web.Mvc;
public static class HMTLHelperExtensions
{
public static MvcHtmlString Span(this HtmlHelper Helper, string Name, string Content, object HtmlAttributes)
{
var htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(HtmlAttributes);
TagBuilder tag = new TagBuilder("Span");
tag.MergeAttribute("name", TagBuilder.CreateSanitizedId(Name));
tag.GenerateId(Name);
tag.InnerHtml = Content;
foreach(var i in htmlAttributes )
{
tag.MergeAttribute(i.Key, i.Value.ToString());
}
return MvcHtmlString.Create(tag.ToString());
}
public static MvcHtmlString Span(this HtmlHelper Helper, string Name, string Content)
{
TagBuilder tag = new TagBuilder("Span");
tag.MergeAttribute("name", TagBuilder.CreateSanitizedId(Name));
tag.GenerateId(Name);
tag.InnerHtml = Content;
return MvcHtmlString.Create(tag.ToString());
}
public static MvcHtmlString SpanFor<TModel, TProperty>(this HtmlHelper<TModel> Helper, Expression<Func<TModel, TProperty>> expression, string Content, object HtmlAttributes)
{
var name = ExpressionHelper.GetExpressionText(expression);
var metaData = ModelMetadata.FromLambdaExpression(expression, Helper.ViewData);
return Span(Helper, name, metaData.Model as string, HtmlAttributes);
}
public static MvcHtmlString SpanFor<TModel, TProperty>(this HtmlHelper<TModel> Helper, Expression<Func<TModel, TProperty>> expression, string Content)
{
var name = ExpressionHelper.GetExpressionText(expression);
var metaData = ModelMetadata.FromLambdaExpression(expression, Helper.ViewData);
return Span(Helper, name, metaData.Model as string);
}
}
and here is the .netcore implementation
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using System;
using System.Linq.Expressions;
public static class HtmlHelperExtensions
{
public static IHtmlContent Span(this IHtmlHelper htmlHelper, string name, string Content, object htmlAttributes)
{
TagBuilder tag = new TagBuilder("Span");
tag.MergeAttribute("name", TagBuilder.CreateSanitizedId(name,""));
tag.GenerateId(name, "");
tag.InnerHtml.Append(Content);
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
foreach (var i in attributes)
{
tag.MergeAttribute(i.Key, i.Value.ToString());
}
return tag;
}
public static IHtmlContent Span(this IHtmlHelper htmlHelper, string name, string Content)
{
TagBuilder tag = new TagBuilder("Span");
tag.MergeAttribute("name", TagBuilder.CreateSanitizedId(name, ""));
tag.GenerateId(name, "");
tag.InnerHtml.Append(Content);
return tag;
}
public static IHtmlContent SpanFor<TModel, TProperty>(this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel,TProperty>> expression, string content, object htmlAttributes)
{
var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, htmlHelper.ViewData, htmlHelper.MetadataProvider);
var name = ExpressionHelper.GetExpressionText(expression);
var metaData = modelExplorer.Metadata;
return Span(htmlHelper, name, modelExplorer.Model as string, htmlAttributes);
}
public static IHtmlContent SpanFor<TModel, TProperty>(this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string content)
{
var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, htmlHelper.ViewData, htmlHelper.MetadataProvider);
var name = ExpressionHelper.GetExpressionText(expression);
var metaData = modelExplorer.Metadata;
return Span(htmlHelper, name, modelExplorer.Model as string);
}
}
I want to point out, these will work with html attributes only if used in this format.
#Html.Span("Test", "Testing Content", new { #class = "test-class", style = "text-align:right" }
#Html.SpanFor(x=>x.ModelProperty, Model.ModelProperty, new { #class = "test-class", style = "text-align:right" }
I try to create some Html Helpers which will have an opening tag and closing tag which will include other contents like the Html.BeginForm does.
For example in Razor we can use the Html.BeginForm helper which has the following syntax:
#using (Html.BeginForm())
{
}
This code will include the contents of curly brackets within a and . The only way that I solved opening and closing a tag with contents is by using two html helpers. I define two html helpers:
public static MvcHtmlString StartForm(this System.Web.Mvc.HtmlHelper helper)
{
return new MvcHtmlString("<form>");
}
public static MvcHtmlString EndForm(this System.Web.Mvc.HtmlHelper helper)
{
return new MvcHtmlString("</form>");
}
Then I use the helpers using the following example:
#Html.StartForm()
contents
#Html.EndForm()
But I would like to be able to make one html helper which will have the following format in the view:
#using (Html.MyForm())
{
<text>contents</text>
}
Can someone help me with this problem because I do not know even how to search it.
You can define a class just like the way the MvcForm is implemented. The class below allows you to create a tag which contains other elements.
public class MvcTag : IDisposable
{
private string _tag;
private bool _disposed;
private readonly FormContext _originalFormContext;
private readonly ViewContext _viewContext;
private readonly TextWriter _writer;
public MvcTag(ViewContext viewContext, string tag)
{
if (viewContext == null)
{
throw new ArgumentNullException("viewContext");
}
_viewContext = viewContext;
_writer = viewContext.Writer;
_originalFormContext = viewContext.FormContext;
viewContext.FormContext = new FormContext();
_tag = tag;
Begin(); // opening the tag
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void Begin()
{
_writer.Write("<" + _tag + ">");
}
private void End()
{
_writer.Write("</" + _tag + ">");
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
_disposed = true;
End(); // Closing the tag
if (_viewContext != null)
{
_viewContext.OutputClientValidation();
_viewContext.FormContext = _originalFormContext;
}
}
}
public void EndForm()
{
Dispose(true);
}
}
To make use of this MvcTag in the way the MvcForm is used, we have to define an extension
public static class HtmlHelperExtensions
{
public static MvcTag BeginTag(this HtmlHelper htmlHelper, string tag)
{
return new MvcTag(htmlHelper.ViewContext, tag);
}
}
And that's it. Now you can use it as:
#using(Html.BeginTag("div")) #* This creates a <div>, alternatively, you can create any tag with it ("span", "p" etc.) *#
{
<p>Contents</p>
}
I have an MVC app that uses dynamic business objects that are inherited from a parent object type. For example the base class Client might have two sub classes called Vendor and ServiceProvider, and these are all handled by the same controller. I have a partial view that I load on the right side of the page when viewing the client's details called _Aside.cshtml. When I load the client I try to look for a specific Aside first and failing that I load a generic one. Below is what the code looks like.
#try
{
#Html.Partial("_" + Model.Type.TypeName + "Aside")
}
catch (InvalidOperationException ex)
{
#Html.Partial("_Aside")
}
The TypeName property would have the word "Vendor" or "ServiceProvider" in it.
Now this works fine but the problem is I only want it to fail over if the view is not found, It's also failing over when there is an actual InvalidOperationException thrown by the partial view (usually the result of a child action it might call). I've thought about checking against Exception.Message but that seems a bit hackish. Is there some other way I can get the desired result without having to check the Message property or is that my only option at this point?
ex.Message = "The partial view '_ServiceProviderAside' was not found or no view
engine supports the searched locations. The following locations were
searched: (... etc)"
UPDATE: This is the class with extension methods I have currently in my project based off of Jack's answer, and Chao's suggestions as well.
//For ASP.NET MVC
public static class ViewExtensionMethods
{
public static bool PartialExists(this HtmlHelper helper, string viewName)
{
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = ViewEngines.Engines.FindPartialView(helper.ViewContext, viewName);
return view.View != null;
}
public static bool PartialExists(this ControllerContext controllerContext, string viewName)
{
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = ViewEngines.Engines.FindPartialView(controllerContext, viewName);
return view.View != null;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName)
{
return PartialExists(helper, viewName) ? helper.Partial(viewName) : HtmlString.Empty;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
return OptionalPartial(helper, viewName, fallbackViewName, null);
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, object model)
{
return PartialExists(helper, viewName) ? helper.Partial(viewName, model) : MvcHtmlString.Empty;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName, object model)
{
return helper.Partial(PartialExists(helper, viewName) ? viewName : fallbackViewName, model);
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName)
{
if (PartialExists(helper, viewName))
{
helper.RenderPartial(viewName);
}
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
helper.RenderPartial(PartialExists(helper, viewName) ? viewName : fallbackViewName);
}
}
UPDATE: If you happen to be using ASP.NET Core MVC, swap the PartialExists() methods for these three methods, and change all of the usages of HtmlHelper for IHtmlHelper in the other methods. Skip this if you're not using ASP.NET Core
//For ASP.NET Core MVC
public static class ViewExtensionMethods
{
public static bool PartialExists(this IHtmlHelper helper, string viewName)
{
var viewEngine = helper.ViewContext.HttpContext.RequestServices.GetService<ICompositeViewEngine>();
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = viewEngine.FindView(helper.ViewContext, viewName, false);
return view.View != null;
}
public static bool PartialExists(this ControllerContext controllerContext, string viewName)
{
var viewEngine = controllerContext.HttpContext.RequestServices.GetService<ICompositeViewEngine>();
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = viewEngine.FindView(controllerContext, viewName, false);
return view.View != null;
}
public static bool PartialExists(this ViewContext viewContext, string viewName)
{
var viewEngine = viewContext.HttpContext.RequestServices.GetService<ICompositeViewEngine>();
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = viewEngine.FindView(viewContext, viewName, false);
return view.View != null;
}
}
In my view...
#Html.OptionalPartial("_" + Model.Type.TypeName + "Aside", "_Aside")
//or
#Html.OptionalPartial("_" + Model.Type.TypeName + "Aside", "_Aside", Model.AsideViewModel)
Came across this answer while trying to solve the problem of nested sections as I wanted to include styles and scripts in an intermediate view. I ended up deciding the simplest approach was convention of templatename_scripts and templatename_styles.
So just to add to the various options here is what I'm using based on this.
public static class OptionalPartialExtensions
{
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName)
{
return PartialExists(helper, viewName) ? helper.Partial(viewName) : MvcHtmlString.Empty;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
return helper.Partial(PartialExists(helper, viewName) ? viewName : fallbackViewName);
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName)
{
if (PartialExists(helper, viewName))
{
helper.RenderPartial(viewName);
}
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
helper.RenderPartial(PartialExists(helper, viewName) ? viewName : fallbackViewName);
}
public static bool PartialExists(this HtmlHelper helper, string viewName)
{
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentNullException(viewName, "View name cannot be empty");
}
var view = ViewEngines.Engines.FindPartialView(helper.ViewContext, viewName);
return view.View != null;
}
}
This brings the my most common use cases in to the extension methods helping keep the views that bit cleaner, the RenderPartials were added for completeness.
I had a similar requirement. I wanted to keep the view markup cleaner and also to avoid generating the dynamic view name twice. This is what I came up with (modified to match your example):
Helper extension:
public static string FindPartial(this HtmlHelper html, string typeName)
{
// If you wanted to keep it in the view, you could move this concatenation out:
string viewName = "_" + typeName + "Aside";
ViewEngineResult result = ViewEngines.Engines.FindPartialView(html.ViewContext, viewName);
if (result.View != null)
return viewName;
return "_Aside";
}
View:
#Html.Partial(Html.FindPartial(Model.Type.TypeName))
or with access to the Model within the partial :
#Html.Partial(Html.FindPartial(Model.Type.TypeName), Model)
You could try the FindPartialView method to check if the view exists. Something along these lines might work (untested):
public bool DoesViewExist(string name)
{
string viewName = "_" + Model.Type.TypeName + "Aside";
ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName , null);
return (viewResult.View != null);
}
Info on the FindPartialView method for ASP MVC 3
Bug fix to handle null viewName or null fallbackViewName (replace appropriate code in OP):
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName, object model)
{
string partialToRender = null;
if (viewName != null && PartialExists(helper, viewName))
{
partialToRender = viewName;
}
else if (fallbackViewName != null && PartialExists(helper, fallbackViewName))
{
partialToRender = fallbackViewName;
}
if (partialToRender != null)
{
return helper.Partial(partialToRender, model);
}
else
{
return MvcHtmlString.Empty;
}
}
I have edited the OP's code (which combines code from multiple answers), but my edit is pending peer review.
I am setting custom attributes to secure objects within a view. Here is the definition for my custom attributes:
[AttributeUsage(AttributeTargets.Property)]
public class SecureObjectAttribute : Attribute, IMetadataAware
{
public void OnMetadataCreated(ModelMetadata metadata)
{
if(!metadata.AdditionalValues.ContainsKey("isSecure"))
{
if (ObjectId == 1)
{
metadata.AdditionalValues.Add("isSecure", true);
}
else
{
metadata.AdditionalValues.Add("isSecure", false);
}
}
}
public int ObjectId { get; set; }
}
Here is my View Model:
public class HomeViewModel
{
[SecureObject(ObjectId = 1)]
[Display(Name = "Name")]
public string Name { get; set; }
[Display(Name = "Address")]
public string Address { get; set; }
}
I am defining helpers for Secure Labels and Secure TextBoxes. Here are my helper functions:
public static MvcHtmlString SecureLabelFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
if ((bool)metadata.AdditionalValues["isSecure"])
{
return null;
}
else
{
return html.LabelFor(expression);
}
}
public static MvcHtmlString SecureTextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
if ((bool)metadata.AdditionalValues["isSecure"])
{
return null;
}
else
{
return html.TextBoxFor(expression);
}
}
All of this works correctly, however, the problem that I am having is that every time I define a secure label and a secure textbox, the OnMetadataCreated method is invoked.
I am making a trip to the database on the OnMetadataCreated method (not shown in the example) to get permissions for the object, and I want to avoid duplicate trips for a single object.
The following is an example of code that is causing the OnMetadataCreated to be invoked twice in my View:
#Html.SecureLabelFor(m => m.Name)
#Html.SecureTextBoxFor(m => m.Name)
Any suggestions on how I can avoid a second call to the OnMetadataCreated method, or avoid a duplicate trip to the database?
You could store the result of the execution of the query in the HttpContext to avoid roundtrips to the database within the same request:
public void OnMetadataCreated(ModelMetadata metadata)
{
// TODO: if you are using a DI framework you could pass the context
// as a constructor argument dependency and use HttpContextBase
HttpContext context = HttpContext.Current;
bool isSecure;
string key = "isSecure_" + ObjectId;
if (context.Items.Contains(key))
{
// The isSecure value was found in the HttpContext =>
// no need to query the database once again within this request
isSecure = (bool)context.Items[key];
}
else
{
// Get the value from the database
isSecure = GetFromDb(context.User.Identity.Name, ObjectId);
// and store into the HttpContext to avoid roundtrips to the database
// within this request
context.Items[key] = isSecure;
}
...
}
If a Get Action returns a View with a "Car" model. The view displays info from the object and takes input to post within a form to another action that takes an object of type "Payment"
The Model on the view is of type Car and gives me stronglytyped html support and some other features like displaytext. But for posting I there is no Htmlhelper support like TextBox(x => x.amount I need to make it like #Html.TextBox("Amount"...
Its possible, but is this the only option?
You can do this:
#{
var paymentHtml = Html.HtmlHelperFor<Payment>();
}
#paymentHtml.EditorFor(p => p.Amount)
with this extension method:
public static class HtmlHelperFactoryExtensions {
public static HtmlHelper<TModel> HtmlHelperFor<TModel>(this HtmlHelper htmlHelper) {
return HtmlHelperFor(htmlHelper, default(TModel));
}
public static HtmlHelper<TModel> HtmlHelperFor<TModel>(this HtmlHelper htmlHelper, TModel model) {
return HtmlHelperFor(htmlHelper, model, null);
}
public static HtmlHelper<TModel> HtmlHelperFor<TModel>(this HtmlHelper htmlHelper, TModel model, string htmlFieldPrefix) {
var viewDataContainer = CreateViewDataContainer(htmlHelper.ViewData, model);
TemplateInfo templateInfo = viewDataContainer.ViewData.TemplateInfo;
if (!String.IsNullOrEmpty(htmlFieldPrefix))
templateInfo.HtmlFieldPrefix = templateInfo.GetFullHtmlFieldName(htmlFieldPrefix);
ViewContext viewContext = htmlHelper.ViewContext;
ViewContext newViewContext = new ViewContext(viewContext.Controller.ControllerContext, viewContext.View, viewDataContainer.ViewData, viewContext.TempData, viewContext.Writer);
return new HtmlHelper<TModel>(newViewContext, viewDataContainer, htmlHelper.RouteCollection);
}
static IViewDataContainer CreateViewDataContainer(ViewDataDictionary viewData, object model) {
var newViewData = new ViewDataDictionary(viewData) {
Model = model
};
newViewData.TemplateInfo = new TemplateInfo {
HtmlFieldPrefix = newViewData.TemplateInfo.HtmlFieldPrefix
};
return new ViewDataContainer {
ViewData = newViewData
};
}
class ViewDataContainer : IViewDataContainer {
public ViewDataDictionary ViewData { get; set; }
}
}
If I understand your question correctly, here's some code I just wrote for one of my projects to do something similar. It doesn't require anything special like what was suggested by Max Toro.
#{
var teamHelper = new HtmlHelper<Team>(ViewContext, this);
}
#using (teamHelper.BeginForm())
{
#teamHelper.LabelFor(p => p.Name)
#teamHelper.EditorFor(p => p.Name)
}
Adding to the implementation by Max Toro, here are a couple more for when you have a non-null model but don't have static type information (these two methods need to be embedded into the implementation Max provides).
These methods work well when you have dynamically retrieved property names for a model and need to call the non-generic HtmlHelper methods that take a name instead of an expression:
#Html.TextBox(propertyName)
for example.
public static HtmlHelper HtmlHelperFor( this HtmlHelper htmlHelper, object model )
{
return HtmlHelperFor( htmlHelper, model, null );
}
public static HtmlHelper HtmlHelperFor( this HtmlHelper htmlHelper, object model, string htmlFieldPrefix )
{
var t = model.GetType();
var viewDataContainer = CreateViewDataContainer( htmlHelper.ViewData, model );
TemplateInfo templateInfo = viewDataContainer.ViewData.TemplateInfo;
if( !String.IsNullOrEmpty( htmlFieldPrefix ) )
templateInfo.HtmlFieldPrefix = templateInfo.GetFullHtmlFieldName( htmlFieldPrefix );
ViewContext viewContext = htmlHelper.ViewContext;
ViewContext newViewContext = new ViewContext( viewContext.Controller.ControllerContext, viewContext.View, viewDataContainer.ViewData, viewContext.TempData, viewContext.Writer );
var gt = typeof( HtmlHelper<> ).MakeGenericType( t );
return Activator.CreateInstance( gt, newViewContext, viewDataContainer, htmlHelper.RouteCollection ) as HtmlHelper;
}
For ASP.NET Core 2
public static class HtmlHelperFactoryExtensions
{
public static IHtmlHelper<TModel> HtmlHelperFor<TModel>(this IHtmlHelper htmlHelper)
{
return HtmlHelperFor(htmlHelper, default(TModel));
}
public static IHtmlHelper<TModel> HtmlHelperFor<TModel>(this IHtmlHelper htmlHelper, TModel model)
{
return HtmlHelperFor(htmlHelper, model, null);
}
public static IHtmlHelper<TModel> HtmlHelperFor<TModel>(this IHtmlHelper htmlHelper, TModel model, string htmlFieldPrefix)
{
ViewDataDictionary<TModel> newViewData;
var runtimeType = htmlHelper.ViewData.ModelMetadata.ModelType;
if (runtimeType != null && typeof(TModel) != runtimeType && typeof(TModel).IsAssignableFrom(runtimeType))
{
newViewData = new ViewDataDictionary<TModel>(htmlHelper.ViewData, model);
}
else
{
newViewData = new ViewDataDictionary<TModel>(htmlHelper.MetadataProvider, new ModelStateDictionary())
{
Model = model
};
}
if (!String.IsNullOrEmpty(htmlFieldPrefix))
newViewData.TemplateInfo.HtmlFieldPrefix = newViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldPrefix);
ViewContext newViewContext = new ViewContext(htmlHelper.ViewContext, htmlHelper.ViewContext.View, newViewData, htmlHelper.ViewContext.Writer);
var newHtmlHelper = htmlHelper.ViewContext.HttpContext.RequestServices.GetRequiredService<IHtmlHelper<TModel>>();
((HtmlHelper<TModel>)newHtmlHelper).Contextualize(newViewContext);
return newHtmlHelper;
}
}
If I understand your problem correctly, try:
#Html.EditorFor(x => x.Amount)
You could also create an editor template for Payment. See this page for details on doing this.
If I'm misunderstanding, some sample code might help.