Model binding in the controller when form is posted - navigation properties are not loaded automatically

陌路散爱 提交于 2019-11-29 15:53:47

In order to get lazy loading for your child collection two requirements must be fulfilled:

  • The parent entity must be attached to an EF context
  • Your parent entity must be a lazy loading proxy

Both requirements are met if you load the parent entity from the database through a context (and your navigation properties are virtual to allow proxy creation).

If you don't load the entity from the database but create it manually you can achieve the same by using the appropriate EF functions:

var parent = context.TestParents.Create();
parent.TestParentID = 1;
context.TestParents.Attach(parent);

Using Create and not new is important here because it creates the required lazy loading proxy. You can then access the child collection and the children of parent with ID = 1 will be loaded lazily:

var children = parent.TestChildren; // no NullReferenceException

Now, the default modelbinder has no clue about those specific EF functions and will simply instantiate the parent with new and also doesn't attach it to any context. Both requirements are not fulfilled and lazy loading cannot work.

You could write your own model binder to create the instance with Create() but this is probably the worst solution as it would make your view layer very EF dependent.

If you need the child collection after model binding I would in this case load it via explicit loading:

// parent comes as parameter from POST action method
context.TestParents.Attach(parent);
context.Entry(parent).Collection(p => p.TestChildren).Load();

If your context and EF is hidden behind a repository you will need a new repository method for this, like:

void LoadNavigationCollection<TElement>(T entity,
    Expression<Func<T, ICollection<TElement>>> navigationProperty) 
    where TElement : class
{
    _context.Set<T>().Attach(entity);
    _context.Entry(entity).Collection(navigationProperty).Load();
}

...where _context is a member of the repository class.

But the better way, as Darin mentioned, is to bind ViewModels and then map them to your entities as needed. Then you would have the option to instantiate the entities with Create().

One possibility is to use hidden fields inside the form that will store the values of the child collection:

@model TestParent
@using (Html.BegniForm())
{
    ... some input fields of the parent

    @Html.EditorFor(x => x.TestChildren)
    <button type="submit">OK</button>
}

and then have an editor template for the children containing the hidden fields (~/Views/Shared/EditorTemplates/TestChild.cshtml):

@model TestChild
@Html.HiddenFor(x => x.TestChildID)
@Html.HiddenFor(x => x.Name)
...

But since you are not following good practices here and are directly passing your domain models to the view instead of using view models you will have a problem with the recursive relationship you have between the children and parents. You might need to manually populate the parent for each children.

But a better way would be to query your database in the POST action and fetch the children that are associated to the given parent since the user cannot edit the children inside the view anyway.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!