值绑定将属性设为null
我有一个视图模型,把Genome模型和MatchString模型打包在一起:
public class GenomeMatchingViewModel
{
public Genome MatchBase { get; set; }
public MatchString MatchTarget { get; set; }
public GenomeMatchingViewModel(Genome MatchBase)
{
this.MatchBase = MatchBase;
MatchTarget = new MatchString();
}
public GenomeMatchingViewModel()
{
}
}
在我的控制器中,我创建一个新的 GenomeMatchingViewModel,它会创建一个新的 MatchTarget,并把 MatchBase 设置为刚刚获取的现有 Genome:
public async Task<IActionResult> MatchCreation(int? id)
{
if (id == null) return NotFound();
var genome = await _context.Genomes.FindAsync(id);
var matchVM = new GenomeMatchingViewModel(genome);
// create a viewmodel here and pass to the view
return View(matchVM);
}
然后将新的视图模型传递给视图:
@model GenomeSequencing.ViewModels.GenomeMatchingViewModel
@{
ViewData["Title"] = "MatchCreation";
}
<h1>Match a String</h1>
<h4>Partial Read</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="MatchCreation">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="@Model.MatchBase" name="MatchBase" />
<div class="form-group">
<label asp-for="MatchTarget.StringtoMatch" class="control-label"></label>
<input asp-for="MatchTarget.StringtoMatch" class="form-control" />
<span asp-validation-for="MatchTarget.StringtoMatch" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="MatchTarget.MinimumLength" class="control-label"></label>
<input asp-for="MatchTarget.MinimumLength" class="form-control" />
<span asp-validation-for="MatchTarget.MinimumLength" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
并且视图在这个方法中提交表单:
[HttpPost]
[ValidateAntiForgeryToken]
// currently results in a valid matchtarget and a null matchbase
public async Task<IActionResult> MatchCreation([Bind("MatchBase, MatchTarget")] GenomeMatchingViewModel matchVM)
{
// if (ModelState.IsValid)
if (true)
{
// perform the match and return the result to the view
matchVM.MatchTarget.AddMatches(matchVM.MatchBase);
return RedirectToAction(nameof(MatchResult));
}
return View(matchVM);
}
问题在于post MatchCreation 方法没有正确绑定/接收 MatchBase 属性。它总是返回为null。MatchTarget 属性始终没问题。下面是视图在浏览器中的显示样子:
<form action="/Genomes/MatchCreation/1002" method="post" novalidate="novalidate">
<input type="hidden" name="MatchBase" data-val="true" data-val-required="The MatchBase field is required." id="MatchBase" value="GenomeSequencing.Models.Genome">
<div class="form-group">
<label class="control-label" for="MatchTarget_StringtoMatch">StringtoMatch</label>
<input class="form-control" type="text" data-val="true" data-val-required="The StringtoMatch field is required." id="MatchTarget_StringtoMatch" name="MatchTarget.StringtoMatch" value="">
<span class="text-danger field-validation-valid" data-valmsg-for="MatchTarget.StringtoMatch" data-valmsg-replace="true"></span>
</div>
<div class="form-group">
<label class="control-label" for="MatchTarget_MinimumLength">MinimumLength</label>
<input class="form-control" type="number" data-val="true" data-val-required="The MinimumLength field is required." id="MatchTarget_MinimumLength" name="MatchTarget.MinimumLength" value="0"><input name="__Invariant" type="hidden" value="MatchTarget.MinimumLength">
<span class="text-danger field-validation-valid" data-valmsg-for="MatchTarget.MinimumLength" data-valmsg-replace="true"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary">
</div>
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8KQCo05GaK1HoqyhHDMJx_2yANKARMKg2CTlNOE4wjhRZBdg3t6m9gY5MBeHxeFlbmkZnmwGduNxHE-Lm2NP9Jr5jYGMB7Gd-N2gHUCcDmhnmw8Gb8ijl6tmQEZyqoveA1Sj0_9a_uPuCycfAdWXKIg"></form>
看起来像是 'Genome' 实例已经传到视图,但是如何再把该实例绑定到生成的post?
解决方案
作为处理EF实体和MVC/WebAPI时的一般规则,不要把实体与视图模型混用。不能简单地用一个隐藏输入来表示一个实体,因为该字段实际包含的只是对象的ToString() 结果,也就是类似 "Your.Model.Namespace.Genome" 的字符串。
要让它工作,你需要为GenomeMatchingViewModel.MatchBase的每个属性提供隐藏输入,以便MVC能重新构建GenomeBase。这也是为什么你的MatchTarget能工作,因为有输入绑定每个值以重新构建它;然而任何没有输入的属性都会是null/默认值,因此随着应用行为的发展,这仍可能成为一个陷阱。你可以假设你有一个完整的对象,但后来在实例被MVC充满时,某些情况下它实际是缺失的。
或者对于复杂对象(同样,不建议对实体这样做,下面会说明),你的视图模型可以暴露一个JSON序列化来绑定你的隐藏输入:
public string GenomeBaseJson { get; set; }
private GenomeBase? _genomeBase
[JsonIgnore] // In case the VM is ever serialized
public GenomeBase
{
get => _genomeBase ??= JsonConvert.DeserializeObject<GenomeBase>(GenomBaseJson);
set
{
_genomeBase = value;
GenomeBaseJson = JsonConvert.SerializeObject(_genomeBase);
}
}
其中隐藏输入绑定到GenomeBaseJson。这样隐藏输入就以字符串形式存储JSON表示,当你访问GenomeBase对象时,它会把JSON反序列化,缓存该实例以便重复访问,在设置时同样更新JSON值。
不过,建议保持视图模型的纯净性和POCO,而不要在其中嵌入实体。实体理应不应超出其被跟踪的 DbContext 实例的作用域。处理未跟踪的实体会很混乱,容易引发你未预料的问题,包括在运行时出现的行为,例如跟踪相关错误(具有此Id的实例已被跟踪)或无意的数据覆盖。
在你的示例中,通过视图模型将整个Genome传给视图其实没有多大意义。相反,你可以只传递GenomeId,以及视图可能显示的Genome的其他属性(如名称等)。VM应该是一个安全可序列化的POCO,所以其中不应有 DbContext,也不应有MatchTarget,这部分逻辑应放在控制器或单独的服务层。当你去创建新记录并将其与一个Genome关联时,使用DbContext通过ID找到Genome,就像设置匹配视图模型时一样,进行关联或创建(如果这是一个有效情景)。总体上,我也建议避免把“Upsert”(插入/更新)这类场景混在一起。如果你期望使用现有的Genome,这应该是控制器中的一个工作流,以便验证是否存在所期望的GenomeId;而对于需要创建新Genome的场景,应该有一个单独的工作流和视图模型来强制执行这些验证(例如值的唯一性等)。将它们混合会导致混乱的情景:代码可能期望新值,结果却触发更新已存在行的行为,这些行为并非你打算发生的,但可能因为Bug或用户篡改而产生。代码路径/函数应该只做一件事,而且做得好。"Upsert" 实际上是两件事:更新或插入。