在ASP.NET MVC框架中,开发者可以更贴近HTTP模型来工作,这与Web Forms模型相比,更符合Web的实际工作原理。对于习惯于Web Forms模型的开发者来说,这可能有点令人生畏。要在MVC中有效工作,需要对HTML表单的工作方式有扎实的理解,这是Web Forms从未真正要求或鼓励的(尽管总是可取的)。在本文中,将探讨在MVC中创建一个经典的UI构造:通过从一个列表框移动项目到另一个列表框来指示选择。
需要至少安装了Visual Studio 2008(或相应的Express版本)以及MVC版本2。假设对ASP.NET开发和MVC项目结构有一定的熟悉程度。
为了迎合节日气氛,将构建一个圣诞礼物选择器。当然,如果不是在12月阅读本文,这个话题可能就不太应景了。以圣诞为主题的用户界面如下所示:
看看那些雪花!它是否让感到节日气氛?不用说,已经将样式设计留给读者作为练习。两个列表框的场景很有趣;它相当常用,并且在Web Forms中非常容易实现,但实际上并不是HTML表单的自然选择。为了理解为什么,让稍微深入了解一下列表框的工作原理;一旦理解了这一点,实现用户界面将变得更加容易。
上面显示的其中一个列表框的(略有简化的)HTML源代码如下所示:
<select multiple="multiple" name="AvailableSelected" size="6">
<option value="1">游戏机</option>
<option value="2">MP3播放器</option>
<option value="3">智能手机</option>
<option value="4">数码相框</option>
<option value="5">电子书阅读器</option>
<option value="6">DVD套装</option>
</select>
<select>元素代表列表本身;选项代表列表中的项目。每个选项都有一个指定的value属性,这决定了当项目被选中时将回传到表单数据中的值。假设用户选择了游戏机、数码相框和DVD套装。以下数据将被发布回服务器:
AvailableSelected=1&AvailableSelected=4&AvailableSelected=6
'AvailableSelected'来自给<select>元素的名称;1、4和6分别指选中产品的ID。请注意,如果项目未被选中,则不会发送有关它的任何信息。这为两个列表场景提出了一个问题:对选中的项目不太感兴趣,而是对每个列表的内容感兴趣。因此,需要找到另一种方法来跟踪用户选择的项目。幸运的是,这并不太难,但在继续之前,让花点时间思考一下这在Web Forms场景中将如何工作。
在Web Forms的ListBox中,列表框中存储的项目在表单发布后可用于服务器端代码。对于场景来说,这将非常有用。知道表单数据中没有发送任何信息,那么这是如何工作的呢?ListBox服务器控件自动将其所有内容保存到Web Form的ViewState隐藏字段中。当表单被发布回时,它从这些数据中重新创建其内容,然后检查表单数据,看看是否有任何项目被选中或未被选中。无需开发者进行任何额外工作,状态就可以在表单发布之间自动维护;这很棒,对吗?嗯,是的,也不是。只要不超出Web Forms方法的范围,这种方法工作得很好。但是,如果尝试超出这个范围,就会遇到问题。例如,尝试通过JavaScript操作列表项,会发现更改不会反映在服务器代码中。更糟糕的是,可能会发现应用程序开始抛出异常,除非调整ViewState安全设置。本应是一个简单的修改,却变成了一个巨大的混乱。如果不了解背后的机制,可能会很难修复问题;遇到过ASP.NET开发者真的不明白为什么他们在JavaScript中所做的更改在服务器代码中不可见。
现在已经理解了一些理论,让来看一下示例代码的一些关键部分。Product类代表用户可以选择的礼物,如下所示:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public Decimal Price { get; set; }
}
还将创建一个视图模型,它封装了传递到/从视图的数据。为了填充两个列表框,需要两个产品数据列表:一个用于可用列表,一个用于请求列表。当用户从一个框移动项目到另一个框时,需要获取列表中哪些项目被选中。回想一下之前的解释,列表框数据被表示为重复的键/值对,列表框的名称作为键,每个选中的ID作为值。幸运的是,不需要手动解码这些数据,ASP.NET MVC的模型绑定功能可以为完成这项工作。将数据存储为int数组,这将从表单数据中为填充。还需要一个地方来保存表单发布之间的状态;只需要记住请求列表框中的产品ID。将通过在字符串中保存一个逗号分隔的列表来做到这一点。
public class ViewModel
{
public List<Product> AvailableProducts { get; set; }
public List<Product> RequestedProducts { get; set; }
public int[] AvailableSelected { get; set; }
public int[] RequestedSelected { get; set; }
public string SavedRequested { get; set; }
}
视图的列表框部分如下所示:
<% using (Html.BeginForm()) { %>
<div>
<table>
<thead>
<tr>
<th>可用</th>
<th></th>
<th>请求</th>
</tr>
</thead>
<tbody>
<tr>
<td valign="top">
<%= Html.ListBoxFor(model => model.AvailableSelected,
new MultiSelectList(Model.AvailableProducts, "Id", "Name", Model.AvailableSelected),
new { size = "6" }) %>
</td>
<td valign="top">
<input type="submit" name="add" id="add" value=">>" />
<br />
<input type="submit" name="remove" id="remove" value="<<" />
</td>
<td valign="top">
<%= Html.ListBoxFor(model => model.RequestedSelected,
new MultiSelectList(Model.RequestedProducts, "Id", "Name", Model.RequestedSelected)) %>
</td>
</tr>
</tbody>
</table>
<br />
<%= Html.HiddenFor(model => model.SavedRequested) %>
</div>
<% } %>
注意Html.ListBoxFor格式;第一个参数设置要使用的模型属性(数组之一);第二个参数创建一个MultiSelectList,这是一个类似于Web Forms中ListItem类的对象集合 - 每个项目都有一个值、文本和选中属性。构造函数设置要用作列表的集合,以及要用作值和文本字段的属性,以及一个IEnumerable,表示应该选中的值,将其中一个数组传递给它。因为它是一个MultiSelectList,所以可以一次选择和移动多个项目。一个隐藏字段存储用于在表单发布之间存储状态的信息。
[HttpPost]
public ActionResult Index(ViewModel model, string add, string remove)
{
// 需要清除模型状态,否则它会干扰更新后的模型
ModelState.Clear();
RestoreSavedState(model);
if (!string.IsNullOrEmpty(add))
AddProducts(model);
else if (!string.IsNullOrEmpty(remove))
RemoveProducts(model);
SaveState(model);
return View(model);
}
多亏了模型绑定的魔力,该方法采用ViewModel类型的参数。ASP.NET MVC将自动解析传入的表单值,并尽可能匹配。这意味着数组将有值(如果有项目被选中),SavedRequested字段也将有值,但Product的List将为null。字符串参数代表两个按钮。当按下提交按钮时,其名称和值(文本)包含在表单值中。通过检查哪个字符串值不为空,可以确定哪个按钮被按下。RestoreSavedState方法从SavedRequested字段中获取保存的产品ID,并使用此信息从前一个往返中重新创建请求产品列表。SaveState方法获取当前状态并将其存储在字符串中,以便保存在隐藏字段中。有了这个,列表框功能就完成了,项目可以在它们之间自由移动。可下载的示例包含了完整的应用程序,包括一些简单的验证和请求项目的详细信息列表。
这个页面的行为类似于从典型的Web-Forms页面获得的行为,具有对同一页面的回发和完全由服务器驱动的行为。尽管它要求在处理表单输入方面更接近底层,但因为页面更符合Web的工作原理,所以将来构建和增强将更加容易。在后续文章中,将展示如何添加一个JavaScript层,将大部分功能转移到客户端,同时仍然保持对没有JavaScript的浏览器的兼容性。