问题
先看下面一个简单的 ASP.NET MVC 5
的 demo:
model
public class TestModel { public List<int> Ints { get; set; } }
controller
public ActionResult Index() { var testModel = new TestModel(); return View(testModel); } [ActionName("Index"), HttpPost] public ActionResult Post(TestModel testModel) { return View(testModel); }
view
@model Test.Controllers.TestModel <form action="@Url.Action("Index")" method="post"> @for (var i = 0; i < 10; i++) { @Html.TextBoxFor(model => model.Ints[i]) } <input type="submit" value="Submit" /> </form>
有没有看出什么问题?View
里面的
@for (var i = 0; i < 10; i++)
{
Html.TextBoxFor(model => model.Ints[i])
}
在 Model.Ints
并没有初始化的情况下被使用了。
正常情况下可能会这么写:
@{
if (Model.Ints == null)
{
Model.Ints = new List<int>();
}
for (var i = 0; i < Model.Ints.Count; i++)
{
@Html.TextBoxFor(model => model.Ints[i])
}
}
如果我们需要 10 个 input
,可能还得费心给 Model.Ints
初始化并添加 10 个 元素。
然而前面的写法真的会报错吗?
其实并不会,it works well.
为什么呢? @Html.TextBoxFor(model => model.Ints[i])
在 Model.Ints
并未初始化的时候就使用了,那么应该会抛出异常才对?
原因
那么,我们来看看为什么没有报错。
这就要从源代码上找原因。
幸好,ASP.NET MVC 已经在 Github 上开源了,地址在这里
我们很容易根据 namespace 找到
Html.TextBoxFor
的实现,参考 https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/Html/InputExtensions.cs#L425简略的说,根据方法签名追踪,可以找到
InputHelper
方法,即https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/Html/InputExtensions.cs#L483重点在这一段:
string attemptedValue = (string)htmlHelper .GetModelStateValue(fullName, typeof(string)); tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(fullName, format) : valueParameter), isExplicitValue);
如果要报错,那么应该报错在
htmlHelper.GetModelStateValue
,因为很明显这是获取Model.Ints[i]
的值的地方继续找到
HtmlHelper.GetModelStateValue
方法,即 https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/HtmlHelper.cs#L391internal object GetModelStateValue(string key, Type destinationType) { ModelState modelState; if (ViewData.ModelState.TryGetValue(key, out modelState)) { if (modelState.Value != null) { return modelState.Value.ConvertTo(destinationType, null /* culture */); } } return null; }
重点就在于
ViewData.ModelState.TryGetValue
了,显然ModelState
主结构是一个Dictionary
来存储所有的值,这个想必大部分人都知道,所以我们绕了一圈最终找到了这里
也就是说,实际上是通过Dictionary.TryGetValue(key, out value)
这样的形式来获取对应的值
具体到我们的问题,即i == 0
时,在ModelState
中寻找key == "Ints[0]"
的值,当然,其值为 null 并且并不会报错
所以整个流程中并不会因为 Model.Ints
未初始化而报错,因为 Html.TextBoxFor(model => model.Ints[i])
并不是通过直接访问而是从 expression 数据结构和 ModelState 数据绑定中取值。虽然这背后机制并不复杂,但是这个问题突然冒出来的时候,没有完整看过这部分实现,我也并没有想到这其中的关联。
最后,在使用之前初始化一定是一个好习惯!
附送
其实比起看源码,通过 Visual Studio 来 debug 可能更方便。
那么步骤如下:
- 找到 Tool -> Options -> Debugging -> General
- Uncheck
Enable Just My Code
- Check
Enable Source Server Support
- 转到 Tool -> Options -> Debugging -> Symbols
- Check
Microsoft Symbol Servers
- Add
http://referencesource.microsoft.com/symbols
- Add
http://msdl.microsoft.com/download/symbols
- Add
http://srv.symbolsource.org/pdb/Public
- 我也不知道哪个 symbol server 对你有效,所以就都加上吧~
- 如果你只需要一部分的 modules,可以选择
Only specified modules
,比如添加System.Web.Mvc.dll
接下来进入调试时,只要右键在当前断点上选择 Step Into Specific
就可以选择进入调试源码了~