Failover to an alternate View when Partial View is not found? - asp.net-mvc

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.

Related

ASP.NET MVC Html Helper

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>
}

ASP.NET bundling/minification: including dynamically generated Javascript

I have a site that dynamically generates Javascript. The generated code describes type-metadata and some server-side constants so that the clients can easily consume the server's services - so it's very cacheable.
The generated Javascript is served by an ASP.NET MVC controller; so it has a Uri; say ~/MyGeneratedJs.
I'd like to include this Javascript in a Javascript bundle with other static Javascript files (e.g. jQuery etc): so just like static files I want it to be referenced separately in debug mode and in minified form bundled with the other files in non-debug mode.
How can I include dynamically generated Javascript in a bundle?
With VirtualPathProviders this is now possible. Integration of dynamic content into the bundling process requires the following steps:
Writing the logic that requests / builds the required content. Generating content from Controller directly requires a bit of work:
public static class ControllerActionHelper
{
public static string RenderControllerActionToString(string virtualPath)
{
HttpContext httpContext = CreateHttpContext(virtualPath);
HttpContextWrapper httpContextWrapper = new HttpContextWrapper(httpContext);
RequestContext httpResponse = new RequestContext()
{
HttpContext = httpContextWrapper,
RouteData = RouteTable.Routes.GetRouteData(httpContextWrapper)
};
// Set HttpContext.Current if RenderActionToString is called outside of a request
if (HttpContext.Current == null)
{
HttpContext.Current = httpContext;
}
IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory();
IController controller = controllerFactory.CreateController(httpResponse,
httpResponse.RouteData.GetRequiredString("controller"));
controller.Execute(httpResponse);
return httpResponse.HttpContext.Response.Output.ToString();
}
private static HttpContext CreateHttpContext(string virtualPath)
{
HttpRequest httpRequest = new HttpRequest(string.Empty, ToDummyAbsoluteUrl(virtualPath), string.Empty);
HttpResponse httpResponse = new HttpResponse(new StringWriter());
return new HttpContext(httpRequest, httpResponse);
}
private static string ToDummyAbsoluteUrl(string virtualPath)
{
return string.Format("http://dummy.net{0}", VirtualPathUtility.ToAbsolute(virtualPath));
}
}
Implement a virtual path provider that wraps the existing one and intercept all virtual paths that should deliver the dynamic content.
public class ControllerActionVirtualPathProvider : VirtualPathProvider
{
public ControllerActionVirtualPathProvider(VirtualPathProvider virtualPathProvider)
{
// Wrap an existing virtual path provider
VirtualPathProvider = virtualPathProvider;
}
protected VirtualPathProvider VirtualPathProvider { get; set; }
public override string CombineVirtualPaths(string basePath, string relativePath)
{
return VirtualPathProvider.CombineVirtualPaths(basePath, relativePath);
}
public override bool DirectoryExists(string virtualDir)
{
return VirtualPathProvider.DirectoryExists(virtualDir);
}
public override bool FileExists(string virtualPath)
{
if (ControllerActionHelper.IsControllerActionRoute(virtualPath))
{
return true;
}
return VirtualPathProvider.FileExists(virtualPath);
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies,
DateTime utcStart)
{
AggregateCacheDependency aggregateCacheDependency = new AggregateCacheDependency();
List<string> virtualPathDependenciesCopy = virtualPathDependencies.Cast<string>().ToList();
// Create CacheDependencies for our virtual Controller Action paths
foreach (string virtualPathDependency in virtualPathDependenciesCopy.ToList())
{
if (ControllerActionHelper.IsControllerActionRoute(virtualPathDependency))
{
aggregateCacheDependency.Add(new ControllerActionCacheDependency(virtualPathDependency));
virtualPathDependenciesCopy.Remove(virtualPathDependency);
}
}
// Aggregate them with the base cache dependency for virtual file paths
aggregateCacheDependency.Add(VirtualPathProvider.GetCacheDependency(virtualPath, virtualPathDependenciesCopy,
utcStart));
return aggregateCacheDependency;
}
public override string GetCacheKey(string virtualPath)
{
return VirtualPathProvider.GetCacheKey(virtualPath);
}
public override VirtualDirectory GetDirectory(string virtualDir)
{
return VirtualPathProvider.GetDirectory(virtualDir);
}
public override VirtualFile GetFile(string virtualPath)
{
if (ControllerActionHelper.IsControllerActionRoute(virtualPath))
{
return new ControllerActionVirtualFile(virtualPath,
new MemoryStream(Encoding.Default.GetBytes(ControllerActionHelper.RenderControllerActionToString(virtualPath))));
}
return VirtualPathProvider.GetFile(virtualPath);
}
public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
{
return VirtualPathProvider.GetFileHash(virtualPath, virtualPathDependencies);
}
public override object InitializeLifetimeService()
{
return VirtualPathProvider.InitializeLifetimeService();
}
}
public class ControllerActionVirtualFile : VirtualFile
{
public CustomVirtualFile (string virtualPath, Stream stream)
: base(virtualPath)
{
Stream = stream;
}
public Stream Stream { get; private set; }
public override Stream Open()
{
return Stream;
}
}
You also have to implement CacheDependency if you need it:
public class ControllerActionCacheDependency : CacheDependency
{
public ControllerActionCacheDependency(string virtualPath, int actualizationTime = 10000)
{
VirtualPath = virtualPath;
LastContent = GetContentFromControllerAction();
Timer = new Timer(CheckDependencyCallback, this, actualizationTime, actualizationTime);
}
private string LastContent { get; set; }
private Timer Timer { get; set; }
private string VirtualPath { get; set; }
protected override void DependencyDispose()
{
if (Timer != null)
{
Timer.Dispose();
}
base.DependencyDispose();
}
private void CheckDependencyCallback(object sender)
{
if (Monitor.TryEnter(Timer))
{
try
{
string contentFromAction = GetContentFromControllerAction();
if (contentFromAction != LastContent)
{
LastContent = contentFromAction;
NotifyDependencyChanged(sender, EventArgs.Empty);
}
}
finally
{
Monitor.Exit(Timer);
}
}
}
private string GetContentFromControllerAction()
{
return ControllerActionHelper.RenderControllerActionToString(VirtualPath);
}
}
Register your virtual path provider:
public static void RegisterBundles(BundleCollection bundles)
{
// Set the virtual path provider
BundleTable.VirtualPathProvider = new ControllerActionVirtualPathProvider(BundleTable.VirtualPathProvider);
bundles.Add(new Bundle("~/bundle")
.Include("~/Content/static.js")
.Include("~/JavaScript/Route1")
.Include("~/JavaScript/Route2"));
}
Optional: Add Intellisense support to your views. Use <script> tags within your View and let them be removed by a custom ViewResult:
public class DynamicContentViewResult : ViewResult
{
public DynamicContentViewResult()
{
StripTags = false;
}
public string ContentType { get; set; }
public bool StripTags { get; set; }
public string TagName { get; set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (string.IsNullOrEmpty(ViewName))
{
ViewName = context.RouteData.GetRequiredString("action");
}
ViewEngineResult result = null;
if (View == null)
{
result = FindView(context);
View = result.View;
}
string viewResult;
using (StringWriter viewContentWriter = new StringWriter())
{
ViewContext viewContext = new ViewContext(context, View, ViewData, TempData, viewContentWriter);
View.Render(viewContext, viewContentWriter);
if (result != null)
{
result.ViewEngine.ReleaseView(context, View);
}
viewResult = viewContentWriter.ToString();
// Strip Tags
if (StripTags)
{
string regex = string.Format("<{0}[^>]*>(.*?)</{0}>", TagName);
Match res = Regex.Match(viewResult, regex,
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.Singleline);
if (res.Success && res.Groups.Count > 1)
{
viewResult = res.Groups[1].Value;
}
else
{
throw new InvalidProgramException(
string.Format("Dynamic content produced by View '{0}' expected to be wrapped in '{1}' tag.", ViewName, TagName));
}
}
}
context.HttpContext.Response.ContentType = ContentType;
context.HttpContext.Response.Output.Write(viewResult);
}
}
Use an extension method or add an helper function to your controller:
public static DynamicContentViewResult JavaScriptView(this Controller controller, string viewName, string masterName, object model)
{
if (model != null)
{
controller.ViewData.Model = model;
}
return new DynamicContentViewResult
{
ViewName = viewName,
MasterName = masterName,
ViewData = controller.ViewData,
TempData = controller.TempData,
ViewEngineCollection = controller.ViewEngineCollection,
ContentType = "text/javascript",
TagName = "script",
StripTags = true
};
}
The steps are similiar for other type of dynamic contents. See Bundling and Minification and Embedded Resources for example.
I added a proof of concept repository to GitHub if you want to try it out.
Darin is right, currently bundling only works on static files. But if you can add a placeholder file with up to date content, bundling does setup file change notifications which will detect automatically when the placeholder file changes.
Also we are going to be moving to using VirtualPathProviders soon which might be a way to serve dynamically generated content.
Update: The 1.1-alpha1 release is out now which has support for VPP
This is not possible. Bundles work only with static files.

Stronglytyped html helper with different model for get and post

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.

Question about ViewEngines.Engines.FindView method and the masterName parameter

This method works great if I pass null to the last param masterName, My views setup in my class derivved from RazorViewEngine work and all is good. Out of curiosity what is the masterName parameter used for? I first thought maybe it was for a layout.cshtml, however; when I pass it a layout it throws an exception.... Any ideas on how this is supposed to be used, what is it looking for?
Custom View Engine (Hardly LOL)
public class CustomRazorViewEngine : RazorViewEngine
{
private readonly string[] NewViewFormats = new[]
{
"~/Views/Messaging/{0}.cshtml"
};
public CustomRazorViewEngine()
{
base.ViewLocationFormats = base.ViewLocationFormats.Union(NewViewFormats).ToArray();
}
}
public string RenderViewToString(string viewName, object model, ControllerContext controllerContext,
string masterName)
{
if (string.IsNullOrEmpty(viewName))
viewName = controllerContext.RouteData.GetRequiredString("action");
controllerContext.Controller.ViewData.Model = model;
using (var stringWriter = new StringWriter())
{
ViewEngineResult viewEngineResult = ViewEngines.Engines.FindView(controllerContext, viewName, masterName);
var viewContext = new ViewContext(controllerContext, viewEngineResult.View,
controllerContext.Controller.ViewData,
controllerContext.Controller.TempData,
stringWriter);
viewEngineResult.View.Render(viewContext, stringWriter);
return stringWriter.GetStringBuilder().ToString();
}
}
So after some more debugging I have found what appears to be the correct answer. First let me state that the masterName parameter is the name of the "Layout" so to say that the view being rendered will use. The catch here is that layout must be able to be located. So instead of the code for the ViewEngine in my original post the following code works as desired.
public string RenderViewToString(string viewName, object model, ControllerContext controllerContext,
string masterName)
{
if (string.IsNullOrEmpty(viewName))
viewName = controllerContext.RouteData.GetRequiredString("action");
controllerContext.Controller.ViewData.Model = model;
using (var stringWriter = new StringWriter())
{
ViewEngineResult viewEngineResult = ViewEngines.Engines.FindView(controllerContext, viewName, masterName);
var viewContext = new ViewContext(controllerContext, viewEngineResult.View,
controllerContext.Controller.ViewData,
controllerContext.Controller.TempData,
stringWriter);
viewEngineResult.View.Render(viewContext, stringWriter);
return stringWriter.GetStringBuilder().ToString();
}
}
public class CustomRazorViewEngine : RazorViewEngine
{
private readonly string[] NewMasterViewFormats = new[]
{
"~/Views/Messaging/Layouts/{0}.cshtml"
};
private readonly string[] NewViewFormats = new[]
{
"~/Views/Messaging/{0}.cshtml"
};
public CustomRazorViewEngine()
{
base.ViewLocationFormats = base.ViewLocationFormats.Union(NewViewFormats).ToArray();
base.MasterLocationFormats = base.MasterLocationFormats.Union(NewMasterViewFormats).ToArray();
}
}
Now when calling
string returnViewToString = _viewUtils.RenderViewToString("RegistrationEmail", new RegistrationEmailModel
{ UserName = userName
},
this.ControllerContext,"_RegistrationEmailLayout");
Everything is happy and my layout for the passed in view, if it exists in the folder gets used. This was the highlight of my day... LOL

MVC3 Client Validation - Only show " * required "

Is there a quick way to default the error message for all your fields in a model?
I want the validation to return the text:
" * required "
...but dont want to manually set it on each field.
Thanks Paul
you can write your custom Required Attribute
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public sealed class AEMRequiredAttribute: ValidationAttribute
{
private const string _defaultErrorMessage = "* required";
public AEMRequiredAttribute()
: base(_defaultErrorMessage)
{ }
public override string FormatErrorMessage(string name)
{
return String.Format(CultureInfo.CurrentUICulture, "* required", name);
}
public override bool IsValid(object value)
{
if (value == null || String.IsNullOrWhiteSpace(value.ToString())) return false;
else return true;
}
}
call this attribute as below :
public partial class AEMClass
{
[DisplayName("Dis1")]
[AEMRequiredAttribute]
public string ContractNo { get; set; }
}
You could create a new HTML helper and then call into the underlying ValidationMessage or ValidationMessageFor helpers setting the message text as you do so.
Something based on ValidationMessageFor would look like this:
public static class HtmlHelperExtensions {
public static IHtmlString ValidatorMessageWithMyTextFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) {
return htmlHelper.ValidationMessageFor<TModel, TProperty>(expression, "required *");
}
}
And you can add that to your view using
#Html.ValidatorMessageWithMyTextFor(m=>m.MyModelPropertyToValidate)
Of course that all works from the view side of the app and not the model side so it all depends where you would like to embed the messages. If it's the model side then AEM's solution is a good one.

Resources