DeboraThaise
DeboraThaise

Reputation: 63

NullReferenceException when trying to pass a nested ViewModel to a Partial

My login and signup forms are in the same page, and they are separated by a tab element.

Each one of the forms have a ViewModel:

public class RegisterViewModel
{
    public string FullName { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
    public string ConfirmPassword { get; set; }
}

public class LoginViewModel
{
    public string Email { get; set; }
    public string Password { get; set; }
}

And since both forms are on the same page, I have a parent ViewModel:

public class LoginAndRegisterViewModel
{
    public LoginViewModel LoginViewModel{ get; set; }
    public RegisterViewModel RegisterViewModel{ get; set; }
}

// Actions...
public ActionResult Register()
{
    return View("Login");
}
public async Task<ActionResult> Register(LoginAndRegisterViewModel model)
{
    // blah.. do stuff here...
    return View("Login", model);
}

But in the main view when I try to render the partials with the forms I get a System.NullReferenceException (Additional information: Object reference not set to an instance of an object.) in Model.LoginViewModel and Model.RegisterViewModel. This is the view:

Login.cshtml:

@model Models.LoginAndRegisterViewModel
...
@Html.Partial("_LoginPartial", Model.LoginViewModel)
...
@Html.Partial("_RegisterPartial", Model.RegisterViewModel)

I have seen that it is common practice to follow this kind of convention, as detailed here in this SO question. I even tried using this PartialOrNull helper but I still get the exception. I also tried the solution based on this "Forms for Deep View Model graphs" article.

Also this didn't work:

@Html.Partial("_LoginPartial", Model.LoginViewModel, new ViewDataDictionary(ViewData){Model = null});

How can I have a register/login form like this? As I said this is EXACTLY the same situation in that other SO question, and everyone reported that the solution worked, but that was for MVC3.

EDIT 1: added the _LoginPartial as reference.

@model Models.LoginViewModel

@{
    using (Html.BeginForm("Login", "Account", new {ReturnUrl = ViewBag.ReturnUrl}, FormMethod.Post))
    {
        @Html.TextBoxFor(m => m.Email)
        @Html.PasswordFor(m => m.Password)
    }
}

EDIT 2: tried the ?? operator and still getting the Exception.

@Html.Partial("_LoginPartial", Model.LoginViewModel ?? new LoginViewModel())

UPDATE: With the accepted answer I made some tests and now it is working without an initialization ViewModel constructor and without sending an empty ViewModel in the Get action.

The only needed requirement is to declare the partial view to have the main model as the @model:

@model Models.LoginAndRegisterViewModel

Then to access the nested ViewModel in the partial:

@model Models.LoginAndRegisterViewModel

@{
    using (Html.BeginForm("Login", "Account", new {ReturnUrl = ViewBag.ReturnUrl}, FormMethod.Post))
    {
        @Html.TextBoxFor(m => m.LoginViewModel.Email)
        @Html.PasswordFor(m => m.LoginViewModel.Password)
    }
}

Upvotes: 1

Views: 2004

Answers (1)

user3559349
user3559349

Reputation:

You should initialize properties LoginViewModel and RegisterViewModel in the view models constructor

public class LoginAndRegisterViewModel
{
  public LoginAndRegisterViewModel()
  {
    LoginViewModel = new LoginViewModel();
    RegisterViewModel  = new RegisterViewModel ();
  }
  public LoginViewModel LoginViewModel{ get; set; }
  public RegisterViewModel RegisterViewModel{ get; set; }
}

and in the GET method, pass a new instance of the view model

public ActionResult Register()
{
  var model = new LoginAndRegisterViewModel();
  return View("Login", model);
}

and to ensure you model properties post back, pass the model to the partials

@Html.Partial("_LoginPartial", Model)

and change the partial to use LoginAndRegisterViewModel

@model Models.LoginAndRegisterViewModel
@using (Html.BeginForm("Login", "Account", new {ReturnUrl = ViewBag.ReturnUrl}, FormMethod.Post))
{
  @Html.TextBoxFor(m => m.LoginViewModel.Email)
  @Html.PasswordFor(m => m.LoginViewModel.Password)
}

This ensures the that the controls are bound to the view model and generate the correct name attributes

<input type="text: name="LoginViewModel.Email"..>

whereas passing on a property of the model would have generated

<input type="text: name="Email"..>

which would not bind to LoginAndRegisterViewModel on post back and all properties would have been null

Upvotes: 2

Related Questions