发布表单时在控制器中进行模型绑定 - 不会自动加载导航属性

时间:2012-01-16 16:45:16

标签: asp.net-mvc-3 entity-framework-4.1 model-binding

我正在使用Entity Framework 4.2版。我的小测试应用程序中有两个类:

public class TestParent
{
    public int TestParentID { get; set; }
    public string Name { get; set; }
    public string Comment { get; set; }

    public virtual ICollection<TestChild> TestChildren { get; set; }
}

public class TestChild
{
    public int TestChildID { get; set; }
    public int TestParentID { get; set; }
    public string Name { get; set; }
    public string Comment { get; set; }

    public virtual TestParent TestParent { get; set; }
}

使用数据库中的数据填充对象效果很好。所以我可以在我的代码中使用testParent.TestChildren.OrderBy(tc => tc.Name).First().Name等。

然后我为我的testParents构建了一个标准的编辑表单。控制器看起来像这样:

public class TestController : Controller
{
    private EFDbTestParentRepository testParentRepository = new EFDbTestParentRepository();
    private EFDbTestChildRepository testChildRepository = new EFDbTestChildRepository();

    public ActionResult ListParents()
    {
        return View(testParentRepository.TestParents);
    }

    public ViewResult EditParent(int testParentID)
    {
        return View(testParentRepository.TestParents.First(tp => tp.TestParentID == testParentID));
    }

    [HttpPost]
    public ActionResult EditParent(TestParent testParent)
    {
        if (ModelState.IsValid)
        {
            testParentRepository.SaveTestParent(testParent);
            TempData["message"] = string.Format("Changes to test parents have been saved: {0} (ID = {1})",
                                                        testParent.Name,
                                                        testParent.TestParentID);
            return RedirectToAction("ListParents");
        }
        // something wrong with the data values
        return View(testParent);
    }
}

当表单回发到服务器时,模型绑定似乎运行良好 - 即testParent看起来没问题(id,name和comment按预期设置)。但是导航属性TestChildren保持为NULL。

我想这并不令人惊讶,因为模型绑定只是提取表单值,因为它们是从浏览器发送的,并将它们推送到TestParent类的对象中。然而,填充testParent.TestChildren需要立即往返数据库,这是实体框架的责任。 EF可能不参与绑定过程。

但是当我打电话给testParent.TestChildren.First()时,我却期待延迟加载。相反,它会导致ArgumentNullException。

是否有必要在模型绑定后以特殊方式标记对象,以便实体框架将进行延迟加载?我怎样才能做到这一点?

显然,我可以使用第二个存储库testChildRepository手动检索子项。但是(a)感觉不对,(b)导致我的存储库设置方式出现问题(每个都使用自己的DBContext - 这是一个我还没有达成协议的问题)。

2 个答案:

答案 0 :(得分:3)

为了让您的孩子收集延迟加载,必须满足两个要求:

  • 父实体必须附加到EF上下文
  • 您的父实体必须是延迟加载代理

如果您通过上下文从数据库加载父实体(并且导航属性为virtual以允许代理创建),则满足这两个要求。

如果您没有从数据库加载实体但是手动创建它,您可以通过使用适当的EF函数来实现它:

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

使用Create而非new非常重要,因为它会创建所需的延迟加载代理。然后,您可以访问子集合,ID = 1的父项的子项将被懒惰地加载:

var children = parent.TestChildren; // no NullReferenceException

现在,默认的模型绑定器对这些特定的EF函数没有任何线索,只是使用new实例化父级,并且不会将其附加到任何上下文。这两个要求都没有得到满足,延迟加载也无法正常工作。

您可以使用Create()编写自己的模型绑定器来创建实例,但这可能是最糟糕的解决方案,因为它会使您的视图层非常依赖于EF。

如果在模型绑定后需要子集合,我会在这种情况下通过显式加载来加载它:

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

如果您的上下文和EF隐藏在存储库后面,则需要一个新的存储库方法,例如:

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();
}

...其中_context是存储库类的成员。

但是,正如Darin所说,更好的方法是绑定ViewModels,然后根据需要将它们映射到您的实体。然后,您可以选择使用Create()实例化实体。

答案 1 :(得分:1)

一种可能性是在表单中使用隐藏字段来存储子集合的值:

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

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

然后有一个包含隐藏字段(~/Views/Shared/EditorTemplates/TestChild.cshtml)的子项的编辑器模板:

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

但是,由于您没有遵循此处的良好做法并直接将您的域模型传递给视图而不是使用视图模型,因此您将遇到孩子和父母之间的递归关系问题。您可能需要手动填充每个子项的父项。

但更好的方法是在POST操作中查询数据库并获取与给定父级关联的子级,因为用户无论如何都无法编辑视图中的子级。