yohsii/puck-cms-platform

Improvement -> passing ViewModel in GET & POST actions

MichalGrzegorzak opened this issue · 9 comments

Hi, very interesting CMS, thanks for sharing it!
Spend few hours on it and not sure how to pass my custom ViewModel from Controller into view (let say I need to pre-populate it in Controller). Could you update sample website to cover full scenario? ViewModel passing in GET & POST actions.
Thanks in advance!

hi @MichalGrzegorzak , thanks for giving Puck a try.

mixing a couple of examples out of the wiki page for intercepting requests this will probably do the trick:

[HttpPost]
[Route("members/register")]
public ActionResult Register(RegisterModel model) {
{
        var result = base.Puck();
        var viewResult = result as ViewResult;
        if (viewResult != null)
        {
            if (viewResult.Model != null) {
               var model = viewResult.Model as puckweb.ViewModels.Page; /*cast to appropriate type for the current page at this intercepted route*/
               //modify viewmodel here
               model.Title = "Pre-populated Title";
            }
        }
        return result;
}

that's an example of how to intercept a request but i feel as though it might be best to handle pre-populating at ViewModel creation time or on Save so here are a couple of examples of how to register event handlers for Create and Save.

this code can go at the bottom of the Startup.cs Configure method:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHttpContextAccessor httpContextAccessor){
       //... lots of stuff omitted
       puck.core.Services.ContentService.AfterCreate += (object sender, puck.core.Events.CreateEventArgs e)=>
            {
                if (e.Node.GetType() == typeof(puckweb.ViewModels.Page))
                {
                    var vm = e.Node as puckweb.ViewModels.Page;
                    vm.Title = "Pre-populated Title";
                }
            };

            puck.core.Services.ContentService.RegisterBeforeSaveHandler<puckweb.ViewModels.Page>("name_of_event_handler", async (object o, puck.core.Events.BeforeIndexingEventArgs args) =>
            {
                var vm = args.Node as puckweb.ViewModels.Page;
                vm.Title = "Pre-populated Title";
            }, true);

}

let me know if you need any more examples

Thanks Simon,
Yes have seen 'Intercepting requests' on wiki. Basically I try to handle scenario when user needs to fill up a Form.
On GET method I need to read previously provided data (if any), and fill up the ViewModel. Where on POST need to handle the validation.

In the samples, have seen something like this:

[HttpPost]
[Route("members/register")]
public IActionResult Register(MyFormViewModel model)
{	
       if (ModelState.IsValid) {
		Response.Redirect("/members/register/success");
	}
	--if invalid, return current puck page
	ViewBag.RegisterModel = model;
	return base.Puck();
}

Not quite sure why ViewBag is involved? Any chance to avoid it?

np @MichalGrzegorzak

the reason you put your Form Model in ViewBag (and not in Model) is that the Model property is used by Puck to send the current ViewModel to the View.

then, in your View you can do something like this where you pass the Form Model from ViewBag to your Form Partial:

@await Html.PartialAsync("FormPartial", ViewBag.FormModel as puckweb.Models.FormModel)

the Partial where you render your Form will be able to read ModelState and apply error messages and populate current values from last Post etc.

if you need to pre-populate your Form Model in the Get, you can do something like this:

[HttpGet]
[Route("members/register")]
public IActionResult Register()
{	
       var formModel = new puckweb.Models.FormModel();
	//pre-populate here

	ViewBag.FormModel = formModel;
	return base.Puck();
}

unfortunately, since Puck is passing the ViewModel for the current page to the View using the Model property, you end up using ViewBag. that said, i hope it's still straightforward to use.

hope that helps

Yes that's exactly what I was missing, make sense and it's clever :) Thank you @yohsii ! Cheers

Sorry, I pre-populated ViewModel on GET method, and passed that in ViewBag to View, then using partial as you suggested.
@await Html.PartialAsync("/Views/MyFormSample/FormPartial.cshtml", ViewBag.FormModel as MyFormPost)

ViewBag.FormModel is not existing. ViewBag works fine for passing simple string, but passing a simple class fails for me (no key in Viewbag dictionary). I'm confused, as far as I know this should work fine. Any ideas?

See my last comment

Can you post full error and the relevant controller and view code please because I've tried it and it works

Thanks for checking @yohsii, I checked on your clean solution - and it was working fine. Means I broke something. There is no error, just empty ViewBag for complex objects, very odd as I did mainly styling changes. Will post an update later, closing for now.

Cool. Also, try 'ViewData' if 'ViewBag' is giving you issues