1
votes

I have a Document type, Template, and a page in the CMS content tree which uses these for a Contact page. The document type has no CMS data properties, because it doesn't need any. I use Models Builder for other pages with no issue, but for this page I've created my own custom model within my MVC project.

I've read every tutorial I can find, and looked at every forum post and issue on the Umbraco forums and Stackoverflow, and for the life of me I can't figure out what I'm doing wrong. The model name and namespace do not conflict with the autogenerated Models builder one.

My understanding is for posting forms a SurfaceController is the way to go - a RenderController is intended more for presenting stuff. So my controller extends SurfaceController. uses Umbraco.BeginUmbracoForm(etc)

I've tried every combination of SurfaceController and RenderController with UmbracoTemplatePage, UmbracoViewPage and every way of changing my model to extend both RenderModel and IPublishedContent to test each. When trying RenderController I've overridden default Index method with RenderModel parameter to create an instance of my model with the renderModel parameter.

Usually the error I get is "Cannot bind source type Umbraco.Web.Models.RenderModel to model type xxx". Sometimes combinations I've attempted allow the Get to succeed, then give this error on Post.

I've even tried to remove the page from the CMS and use a standard MVC controller and route - this allows me to display the page, and even using a standard Html.BeginForm on my view, I get an error when trying to post the form (despite a breakpoint in the code in controller being hit) which also states it "Cannot bind source type Umbraco.Web.Models.RenderModel to model type xxx"

This CANNOT be this difficult. I'm ready to throw laptop out window at this stage.

What am I doing wrong???? Or without seeing my code at least can anyone tell me how this is supposed to be done? How do you post a custom model form to an Umbraco 7.5 controller, with no CMS published content properties required?

As it stands, my View looks like this:

@inherits UmbracoViewPage<Models.Contact>
... 
using (Html.BeginUmbracoForm<ContactController>("Contact", FormMethod.Post

My Controller looks like this:

public class ContactController : SurfaceController
{
    public ActionResult Contact()
    {
        return View("~/Views/Contact.cshtml");
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Contact(Contact contactForm)
    {
        ...
    }

And my model looks like this:

public class Contact : RenderModel
{
    public Contact() : base(UmbracoContext.Current.PublishedContentRequest.PublishedContent, UmbracoContext.Current.PublishedContentRequest.Culture)
    {

    }

    public Contact(IPublishedContent content) : base(content, CultureInfo.CurrentUICulture) 
    {

    }

    [Display(Name = "First Name", Prompt = "First Name")]
    public string FirstName { get; set; }

...

Update: If I use the model for my CMS page created automatically by models builder, the Get and Post work ok. However when I customise the model (i.e. I put a partial class of the same name in ~/App_Data/Models and regenerate models on Developer tab), the custom properties in my posted model are always null.

I can populate these manually from the request form variables, however this seems wrong and messy. What's going on here?

public class ContactPageController : SurfaceController
{
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Contact(ContactPage contactForm)
    {
        try
        {
            contactForm.FirstName = Request.Form["FirstName"];
            contactForm.LastName = Request.Form["LastName"];
            contactForm.EmailAddress = Request.Form["EmailAddress"];
            contactForm.Telephone = Request.Form["Telephone"];
            contactForm.Message = Request.Form["Message"];

            var captchaIsValid = ReCaptcha.Validate(ConfigurationManager.AppSettings["ReCaptcha:SecretKey"]);

            if (ModelState.IsValid && captchaIsValid)
            {
                // Do what you need
                TempData["EmailSent"] = true;

                return RedirectToCurrentUmbracoPage();
            }

            if (!captchaIsValid)
            {
                ModelState.AddModelError("ReCaptchaError", "Captcha validation failed - Please try again.");
            }

            return RedirectToCurrentUmbracoPage();
        }
        catch (Exception ex)
        {
            LogHelper.Error(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType, null, ex);
            return new HttpStatusCodeResult(HttpStatusCode.InternalServerError);
        }
    }

Further Info: Robert's approach first time, thanks for that. I had tried one approach using PartialViews and ChildActions but clearly I didn't do it correctly. Would I be right in saying the reason this approach is required at all (i.e. why you can't add the custom properties to the model the main view is bound to) is because I am using Models Builder?

So among the strange errors I received was one about 2 classes both wanting to represent the contact page, and another about it expecting one type of class in a dictionary (ContactPage) but receiving another (Contact) - even though I had made no reference to ContactPage in either view or controller. This suggests to me ModelsBuilder adds a mapping for document types to models on app startup behind the scenes? Which is maybe why you're better to take this approach of letting ModelsBuilder do its thing, and build your own model on top of that with a partial view in this way?

I've found the quality of documentation on this topic very poor indeed. Not sure if maybe the good stuff is behind closed doors, i.e. requires a paid Umbraco membership? For a supposedly open source system, that feels kinda shady to me.

Easy when you know how!!

1
Hi Breeno, It's not so much about ModelsBuilder builder and custom properties; it's more to do with the routing of the page - the model gets re-initialised at some point (out of scope for here) and any value posted to a custom property is going to be lost. Using Child Actions and Partial Views for the form is generally a more accepted way to go with this scenario.Robert Foster
On another point - it's entirely possible to add customisations to ModelsBuilder generated models - refer to the GitHub wiki for documentation, tips and tricks: github.com/zpqrtbnk/Zbu.ModelsBuilder/wiki/…Robert Foster

1 Answers

4
votes

For your situation, SurfaceController is the most likely candidate as you've surmised, however think of the actions in that Controller as applying to partial views, not the full view used by the page.

Don't try to use the ModelsBuilder ContactPage model, but rather create your own as you were originally (the original Contact model) - think of it as a Data Transfer Object perhaps if you do need to apply any of the properties back to the ContactPage model for any reason.

I've found I've had the greatest success with SurfaceController with the following conditions:

  1. The Page Template does not inherit from the model intended for the Form; it inherits directly from the standard Umbraco PublishedContentModel or a ModelsBuilder generated model.
  2. Implement a Partial View for the action defined in your SurfaceController - this view inherits from Umbraco.Web.Mvc.UmbracoViewPage<Contact> in your example.
  3. The Partial View utilises BeginUmbracoForm<ContactController>, specifying POST action as one of the parameters (depending on which signature you're using)

You shouldn't need to populate any model properties using Request.Form like this.

For example, the code in one of my projects looks something like this:

SurfaceController

public class FormsController : SurfaceController
{
    [ChildActionOnly]
    public ActionResult ContactUs()
    {
        return PartialView(new ContactForm ());
    }

    [HttpPost]
    public async Task<ActionResult> HandleContactUs(ContactForm model)
    {
        if (ModelState.IsValid)
        {
            if (!await model.SendMail(Umbraco)) // Do something with the model.
            {

            }
        }

        return RedirectToCurrentUmbracoPage(); // Send us back to the page our partial view is on
    }

}

Partial View:

@inherits Umbraco.Web.Mvc.UmbracoViewPage<ContactForm>
@using Digitalsmith.ReCaptcha
@using (Html.BeginUmbracoForm<FormsController>("HandleContactUs"))
{
    ...
}

Contact Page Template:

@inherits Umbraco.Web.Mvc.UmbracoTemplatePage<ContactPage>
@{
    Layout = "_Layout.cshtml";
}
@Html.Action("ContactUs", "Forms")