Rendering Model
It’s very common to inherit your Sitecore MVC models from RenderingModel
. The pipelines will call the Initialize()
on it and you’ll get access to the rendering’s context item via Model.Item
(read more about it here).
public class LoanModel : RenderingModel { public string ReferringWebsite { get; set; } public string ReferringUser { get; set; } // ... }It’s also not uncommon to reuse your rendering models as a submission container for a custom form in your controller (read more about routing with Sitecore MVC here):
public class LoanFormsController: SitecoreController { [HttpPost] public ActionResult Loan(LoanModel loan) { // ... } }WFFM MVC
I blogged about Web Forms For Marketers with MVC before. The module registers a number of customizations or rather extensions (a custom renderer, for example) and one of them made me write this article – extended model metadata provider:
<sitecore> <pipelines> <initialize> ... <processor type="Sitecore.Forms.Mvc.Pipelines.AddCustomMetadataProvider, Sitecore.Forms.Mvc" patch:source="Sitecore.Forms.Mvc.config"/> ... </initialize> </pipelines> </sitecore>It Hurts!
Put the two together and it doesn’t work. With WFFM 2.4+ deployed and your non-WFFM form submission controller coded to receive a
RenderingModel
you will get this error (formatted for the blog post):Exception: System.Reflection.TargetInvocationException Source: System at System.ComponentModel.ReflectPropertyDescriptor.GetValue(Object component) at Sitecore.Forms.Mvc.Models.MetadataProviders.ModelTypeMetadataProvider.CreateMetadata(...) at System.Web.Mvc.AssociatedMetadataProvider.GetMetadataForProperty(...) at System.Web.Mvc.AssociatedMetadataProvider.GetMetadataForProperties(...) at System.Web.Mvc.ModelMetadata.get_Properties() at System.Web.Mvc.ModelBindingContext.get_PropertyMetadata() at System.Web.Mvc.DefaultModelBinder.BindProperty(...) at System.Web.Mvc.DefaultModelBinder.BindProperties(...) at System.Web.Mvc.DefaultModelBinder.BindComplexElementalModel(...) at System.Web.Mvc.DefaultModelBinder.BindComplexModel(...) at System.Web.Mvc.ControllerActionInvoker.GetParameterValue(...) at System.Web.Mvc.ControllerActionInvoker.GetParameterValues(...) at System.Web.Mvc.ControllerActionInvoker.InvokeAction(...) Nested Exception Exception: System.InvalidOperationException Message: MySolution.Models.Forms.LoanModel has not been initialized. Source: Sitecore.Mvc at Sitecore.Mvc.Presentation.RenderingModel.get_Rendering() at Sitecore.Mvc.Presentation.RenderingModel.get_Item()Ouch! That hurts!
The nested exception tells me someone tried to use the
.Item
property that relies on the.Rendering
property that in turn must be initialized before it’s used. When a model is used on the rendering the rendering engine’s pipelines callInitialize()
on it and inject the current rendering object dependency into it. POST-ing to a controller (unless it’s a POST received by a controller rendering) doesn’t go through the rendering lifecycle so nobody is there to initialize the model plust there’s no current rendering at that time anyway. Calling into the model’sRendering
property will obviously fail but why would someone do it?Why
The WFFM’s metadata provider essentially needs to do one thing – initialize special container objects to properly handle the fields (code was simplified and formatted for the blog post):
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) { ModelMetadata metadata = base.CreateMetadata(...); IContainerMetadata container = modelAccessor() as IContainerMetadata; if (typeof (IContainerMetadata).IsAssignableFrom(containerType)) { metadata.AdditionalValues.Add("Container", container); } return metadata; }The
modelAccessor
in this case is:() => property.GetValue(container);Calling into the property’s
GetValue()
will fail for thoseRenderingModel
‘s properties that need to be initialized. I am honestly not sure why MVC is fetching metadata for the properties of the model that were not part of the request but evidently it does so and WFFM’s metadata provider calls intoGetValue()
for all of them.“WFFM Proof” Rendering Model
This is a workaround if you don’t feel like patching WFFM’s metadata provider. You can dress your models into a WFFM proof vest:
public class WffmProofRenderingModel : RenderingModel { public override Item Item { get { var rendering = Rendering; return rendering != null ? rendering.Item : null; } } public override Rendering Rendering { get { Rendering rendering = null; try { rendering = base.Rendering; } catch (InvalidOperationException e) { // protecting against custom WFFM model binder } return rendering; } set { base.Rendering = value; } } }Not pretty but it doesn’t blow up when used prior to (or without) being initialized. You can now inherit your models from the
WffmProofRenderingModel
if you need to use them in your[HttpPost]
controller actions.“WFFM Proof” Metadata Provider
You can also patch WFFM’s metadata provider to only call into
GetValue()
on a property when it’s theirs.First, you patch the registration processor:
<processor patch:instead="processor[@type='Sitecore.Forms.Mvc.Pipelines.AddCustomMetadataProvider, Sitecore.Forms.Mvc']" type="MySolution.Wffm.RegisterWffmProofMetadataProvider, MySolution.Wffm"/>public class RegisterWffmProofMetadataProvider { public virtual void Process(PipelineArgs args) { ModelMetadataProviders.Current = new WffmProofMetadataProvider(); ModelBinders.Binders.Add(typeof(SectionModel), new SectionModelBinder()); ModelBinders.Binders.Add(typeof(FieldModel), new FieldModelBinder()); } }Then, you provide your own implementation of the metadata provider that does its thing only when it needs to, only when a property is one of its
IContainerMetadata
field types:[su_note note_color=”#fafafa”]UPDATE: Ekaterina posted a comment and I updated the solution following her recommendations[/su_note]
public class WffmProofMetadataProvider : DataAnnotationsModelMetadataProvider { protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) { var metadata = base.CreateMetadata(...); if (typeof(IContainerMetadata).IsAssignableFrom(modelType)) { this.containerModel = modelAccessor != null ? modelAccessor.Invoke() : null; } if (typeof(IContainerMetadata).IsAssignableFrom(containerType)) { metadata.AdditionalValues.Add(Constants.Container, this.containerModel); } return metadata; } }I hope the two have lived happily ever after.
[su_divider][/su_divider]
UPDATE: I posted #432761 to the support team and it’s been accepted as a defect.
This solution actually breaks the WFFM logic, the correct one:
protected override ModelMetadata CreateMetadata(IEnumerable attributes, Type containerType, Func modelAccessor, Type modelType, string propertyName)
{
var ret = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
if (typeof(IContainerMetadata).IsAssignableFrom(modelType))
{
this.containerModel = modelAccessor != null ? modelAccessor.Invoke() : null;
}
if (typeof(IContainerMetadata).IsAssignableFrom(containerType))
{
ret.AdditionalValues.Add(Constants.Container, this.containerModel);
}
return ret;
}
Thank you Ekaterina! I will update my blog post. I only tested the second solution on the
RenderingModel
and haven’t looked if my WFFM forms kept working. I quickly put it together when I was writing my blog post. The solution where we experienced this issue went with the “WFFM proof” vest for the model object.You just saved my life !
Hi, I have problem with the WFFMProofMetadataProvider, when implementing it, i have error on line 14: this.containerModel not found, and line 19 : Constants does not have “Container” const. May I know how to solve these issues (pretty much I just need the usings of the class).
Thank you very much.
I don’t have that code in front of my eyes right now. Sorry! Since I posted an issue to Support that was accepted as a Bug and Ekaterina from Sitecore team corrected the original solution I posted maybe send them a note and ask for a fix to #432761? I am sure they will send you a config patch and a DLL. Good luck!
Pavel, thanks a lot for this discovery.
I requested the patch from Sitecore but they are taking too long to respond. Do you know if there is any resource that support patches can be accessed from?
> Any solution Harry?
You guys may want to look into the WFFM code that you are working with. 2.4, for example, had changes in the MVC code between update levels. It may as well be that I worked with one update version and you are working with another.