Back-End Development

WFFM Hurt My Rendering Model

Abstract Chaos@1x.jpg

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 call Initialize() 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’s Rendering 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;
}
Covid 19
COVID-19: Digital Insights For Enterprise Action

Access Perficient’s latest insights into how you can leverage digital technologies to not only respond to the pandemic, but drive your operations forward and deliver experiences your customers need.

Get Informed

The modelAccessor in this case is:

() => property.GetValue(container);

Calling into the property’s GetValue() will fail for those RenderingModel‘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 into GetValue() 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.

About the Author

As a Sitecore MVP, Brian spends most of his time consulting and architecting software solutions for enterprise-level Sitecore projects.

More from this Author

Thoughts on “WFFM Hurt My Rendering Model”

  1. 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;
    }

  2. 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.

  3. 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.

  4. 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!

  5. 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?

  6. 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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe to the Weekly Blog Digest:

Sign Up