MVC ModelState.IsValid Incorrect When Using View Model that Extends PageViewModel

Vote:
 

Hello,

We're in the process of moving over from Webforms to MVC and I'm currently working on two forms that are used by a customer to updated either their billing or their shipping address.  I've gotten the form to the point that I'm able to update or create a new address without issue.  However, the issue comes in when I'm trying to validate the fields based off of the ViewModel attributes for each of the fields.  We've been using the Alloy PageViewModel and were having issues finding a way to create a separate ViewModel and still get the pages to load properly and ended up having to extend the page view for the form with the PageViewModel instead of the model of the page.  The code that I have is below.

ProfileBillingAddressEditPage.cs (edited as fields are just for content areas on the page)

public class ProfileBillingAddressEditPage : SitePageData
{
    // Content Areas
}

ProfileBillingAddressEditViewModel.cs (edited to keep short)

public class ProfileBillingAddressEditViewModel : PageViewModel where T1 : ProfileBillingAddressEditPage
{
	public ProfileBillingAddressEditViewModel(T1 currentPage) : base(currentPage)
	{
		CurrentPage = currentPage;
	}

	public new T1 CurrentPage { get; private set; }

        // For the form fields
	[Required]
	public string txtBillingFirstName { get; set; }
	
	// Additional Fields
}

public class ProfileBillingAddressEditViewModel
{
	public static ProfileBillingAddressEditViewModel Create(T page) where T : ProfileBillingAddressEditPage
	{
		return new ProfileBillingAddressEditViewModel(page);
	}
}

Index.cshtml (edited to keep short)

@model ProfileBillingAddressEditViewModel

@using (Html.BeginForm("Index", "ProfileBillingAddressEditPageTemplate", FormMethod.Post))
{
	@Html.AntiForgeryToken()
	

Required

@Html.TextBoxFor(m => m.txtBillingFirstName, new { // Additional html attributes })
@Html.ValidationMessageFor(m => m.txtBillingFirstName)
// Additional Fields Below
}

Controller (edited to keep short)

public class ProfileBillingAddressEditPageTemplateController : PageControllerBase
{
	public ActionResult Index(ProfileBillingAddressEditPage currentPage)
	{
		var model = ProfileBillingAddressEditViewModel.Create(currentPage);
		return View(model);
	}

	[HttpPost]
	[ValidateAntiForgeryToken]
	public ActionResult Index()
	{
		if (!ModelState.IsValid)
		{
			return View();
		}
		// Do stuff if ModelState is valid

		return RedirectToAction("Index");
	}
}

For pages that we don't create a view model for the ActionResult Index and Index.cshtml model call would look like below.

// ActionResult
var model = PageViewModel.Create(currentPage);
return View(model);

//Index.cshtml model
@model PageViewModel

As can be seen we had to extend the PageViewModel and essentially duplicate it and then also include our additional data properties that we wanted to add.  Trying to do this by just extending the view model with ProfileBillingAddressEditPage would result in either the view not being found (I believe because it went to the DefaultPageController also from the Alloy project), or would throw another error saying the models were incorrect.  We don't like having to do it this way, but it was the only way we could figure out how to get it to work and wish there was a different way.

The issue with the validation on the server side is that when the form is submitted with invalid data or no data at all, in the [HttpPost] ActionResult Index the ModelState comes back as true for IsValid.  I tried passing in the model multiple different ways as show below, with none of them working or the last one coming back with a "No parameterless constructor defined for this object" error.

// ModelState comes back valid
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult (ProfileBillingAddressEditPage model) { }

// ModelState comes back valid
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult (ProfileBillingAddressEditViewModel model) { }

// Throws "no parameterless constructor" error
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult (ProfileBillingAddressEditViewModel model) { }

Is the way that we are doing the view model (extending the PageViewModel) causing the ModelState issues?  Is there a better way of doing the view model that we don't have to extend the PageViewModel (note I extended based off of the IPageViewModel with similar results, but didn't get to the point of testing the ModelState)?

Any help is greatly appreciated.

Thank you,

Kevin Larsen

#197864
Oct 15, 2018 21:48
Vote:
 

Any thoughts or ideas on what we may be doing wrong or could improve?

Thank you!

#197902
Oct 17, 2018 4:42
Vote:
 

is it so that you do not receive model in action decorated with `[HttpPost]`? is that left blank intentionally?

#197932
Oct 17, 2018 14:26
Vote:
 

The action that recieves the HttpPost is left blank intentionally because when filled in with the different models from my initial post (and below) the model state still comes back as valid or comes back with an error.

// ModelState comes back valid
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult (ProfileBillingAddressEditPage model) { }

// ModelState comes back valid
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult (ProfileBillingAddressEditViewModel model) { }

// Throws "no parameterless constructor" error
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult (ProfileBillingAddressEditViewModel<ProfileBillingAddressEditPage> model) { }

Thank you,

Kevin Larsen

#197934
Oct 17, 2018 15:22
Vote:
 

couple of cents:

  • you cannot "bind" ProfileBillingAddressEditViewModel<ProfileBillingAddressEditPage> model straight in mvc action. and binder has no idea how to do that. episerver uses value providers to pass in episerver page on which this post back has been made - argument should be called "currentPage" and should match page type.
  • if you wanna combine episerver page and view model together - then one of the way is to have parameterless constructor (this will be needed for mvc model binder) and also receive currentPage from value provider and then just set it to the model (via property or method). maybe this could be done also automatically in mvc binding pipeline, but I'm lazy enough to do a research
  • what I usually try to do is to split view model and episerver page - former is data coming back and forth from the clientside, latter is just a container to store content and maybe other stuff
  • you have to have something back in postback action to work on (you could access all data from Reequest directly, but then it would not make sense even use mvc :)

what is specifically inside ModelState? which property is reported as error?

#197978
Oct 18, 2018 13:27
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.