翻译:使用 ASP.NET MVC 4, EF, Knockoutjs and Bootstrap 设计和开发站点 - 3
原文地址:http://ddmvc4.codeplex.com/
原文名称:Design and Develop a website using ASP.NET MVC 4, EF, Knockoutjs and Bootstrap
Part 1: 创建 Web Application (Knockout.js, Asp.Net MVC and Bootstrap): 前端设计
在开始 UI 部分之前,我们先看一下在 ASP.NET MVC4 中使用 Knockoutjs 和 Bootstrap 有什么好处?
Why Knockoutjs: Knockout 使用 JavaScript ViewModel 实现了 MVVM 模式. 在 MVC 中还有一个很棒的因素是从 Javascript 模型序列化为 Json 和从 Json 反序列为模型都很简单,在 MVC4 中已经包含了这个脚本库, 这使得在我们开发复杂的 UI 的时候,不论怎样修改,都只需要很少的编码,马上我们用它来实现页面。
Why Bootstrap: Twitter 的 Bootstrap 是包括简单并且灵活的 HTML, CSS, 以及广受欢迎的 Javascript 用户界面组件和交互。包括一组 CSS 样式,组件和 JavaScript 插件。提供了跨平台的支持, 消除了不同平台的不一致问题。处理的非常好,良好的文档和 Twitter Bootstrap's 站点本事就是现实中很棒的参考。最后,它节省了我大量的时间,只需要很少的测试,几乎没有浏览器的问题,节约了一半的开发时间,在我们的框架中其它优点还包括。
- 12-列表个, 固定布局, 流式布局以及响应式布局.
- 提供基本的 CSS, 包括:版式, code (使用 Google prettify 的语法高亮), 表格, 表单, 按钮,以及 Glpyhicons 图标 .
- Web UI 组件,例如 按钮, 导航菜单, 标签, 缩略图, 提示, 进度条和其他杂项.
- Javascript 插件,包括模式对话框, 下拉列表, 滚动条, 窗格, 工具提示, 弹出窗口, 提示, 按钮, 收缩, 转轮和提示.
在下面的步骤中,我们将演练使用测试数据来创建布局,设计 UI ,完成上述的目标。Step 1:
创建空白的应用; 命名为 “Application”
Step 2:
在解决方案上右击鼠标,添加一个新的 ASP.NET MVC4 项目,选择 Internet Application 模版,使用 Razor 引擎。
完成第 2 步之后 - 项目的结构如下所示
Step 3:
在 MVC 项目上鼠标右击,选择管理 NuGet 包,在搜索框中输入 Bootstrap ,找到后,点击 Install 按钮。
有的时候,我会联不到 NuGet 网站,你可以直接到 BootStrap 网站下载文件,添加到项目中。
主要是使用了样式文件 bootstrap.css ,下载后,保存到 Content 文件夹中。
Step 4:
将下面的两行代码添加到 App_Start 文件夹中德 BundleConfig.cs 文件中,为所有页面使用 Knockoutjs 和 Bootstrap 提供支持。
bundles.Add(new ScriptBundle("~/bundles/knockout").Include(
"~/Scripts/knockout-{version}.js"));
bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/bootstrap.css"));
在 Views/Shared 文件夹中 _Layout.cshtml 文件中,添加下面的行,注册使用 knockout。
Also in _Layout,cshtml file under Views/Shared folder add below line to register knockout files as :
@Scripts.Render("~/bundles/knockout")
Step 5:
在视图文件夹 Views 中添加一个名为 Contack 的文件夹,在其中添加一个名为 Index.cshtml 的视图文件 ( 这一步可以通过在控制器中的 Index 方法上点击右键完成 )。然后添加一个名为 ContactController 的控制器,在 Scripts 文件夹中添加一个名为 Contact.js 的脚本文件,项目文件夹如下所示:
Step 6:
修改 Route.config 文件,将默认的路由设置到 Contact 控制器。
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Contact", action = "Index", id = UrlParameter.Optional }
);
修改 View/Shared 文件夹中的 _Layout.cshtml 文件,使用 BootStrap 语法,修改后的代码如下所示。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>@ViewBag.Title - Contact manager</title>
<link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
<meta name="viewport" content="width=device-width" />
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/knockout")
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
@RenderSection("scripts", required: false)
</head>
<body>
<div class="container-narrow">
<div class="masthead">
<ul class="nav nav-pills pull-right">
</ul>
<h3 class="muted">Contact Manager</h3>
</div>
<div id="body" class="container">
@RenderSection("featured", required: false)
<section>
@RenderBody()
</section>
</div>
<hr />
<div id="footer">
<div class="container">
<p class="muted credit">© @DateTime.Now.Year - Design and devloped by <a href="http://www.anandpandey.com">Anand Pandey</a>.</p>
</div>
</div>
</div>
</body>
</html>
Step 7:
现在可以运行程序,效果如下:
我们使用这个页面显示 Screen 1 ,显示联系人列表。
Step 8:
首先,我们在 Contact.js 中创建一个模拟的联系人数据数组, (最后我们从数据库中获取), 随后,我们使用这些数据填充表格。
var DummyProfile = [
{
"ProfileId": 1,
"FirstName": "Anand",
"LastName": "Pandey",
"Email": "anand@anandpandey.com"
},
{
"ProfileId": 2,
"FirstName": "John",
"LastName": "Cena",
"Email": "john@cena.com"
}
]
然后,我们创建 ProfilesViewModel, ViewModel 用来保存联系人信息,数组用来保存联系人信息的集合。注意这里使用 ko.observableArray, 相当于常规数组,是观察者模式中的主题,这意味着它可以在其中的项目发生变化的时候,自动更新界面。
最后,我们需要使用 ko.applyBindings() 来激活 knockout.
var ProfilesViewModel = function () {
var self = this;
var refresh = function () {
self.Profiles(DummyProfile);
};
// Public data properties
self.Profiles = ko.observableArray([]);
refresh();
};
ko.applyBindings(new ProfilesViewModel());
Step 9:
下一步,我们需要在 Index.cshtml 页面写一些代码。以显示联系人列表。我们在 tbody 元素上使用 foreach 绑定,使用 knockout 根据联系人数组中的每一个数据生成对应的子元素,然后告诉 knockout 我们希望使用对每个数据生成 tr 来填充 tbody.
<table class="table table-striped table-bordered table-condensed">
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
</tr>
<tbody data-bind="foreach: Profiles">
<tr>
<td data-bind="text: FirstName"></td>
<td data-bind="text: LastName"></td>
<td data-bind="text: Email"></td>
</tr>
</tbody>
</table>
<script src="~/Scripts/Contact.js"></script>
如果现在运行程序,就会看到联系人的简单列表
不要忘了,我们使用 Bootstrap 的样式类来应用到 table 上,在上边的例子中,如下所示。
<table class="table table-striped table-bordered table-condensed">
Step 10:
现在,我们需要为每一行增加编辑和删除功能,表格的上边有一个创建新联系人的按钮,做以下工作:
- 在模版中添加 th 和 td ,在脚本中绑定 removeProfile 函数处理按钮的点击事件处理
- 修改名字单元格,增加编辑联系人的链接,使用 editProfile 函数绑定点击事件
- 在表格的前面添加创建联系人的按钮,使用 createProfile 函数绑定到点击事件处理
页面内容如下所示 :
<input type="button" class="btn btn-small btn-primary" value="New Contact" data-bind="click:$root.createProfile" />
<hr />
<table class="table table-striped table-bordered table-condensed">
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th></th>
</tr>
<tbody data-bind="foreach: Profiles">
<tr>
<td class="name"><a data-bind="text: FirstName, click: $parent.editProfile"></a></td>
<td data-bind="text: LastName"></td>
<td data-bind="text: Email"></td>
<td><button class="btn btn-mini btn-danger" data-bind="click: $parent.removeProfile">remove</button></td>
</tr>
</tbody>
</table>
<script src="~/Scripts/Contact.js"></script>
执行的效果如下所示:
但是现在按钮都不能工作,因为我们还没有编写响应的代码,下一步我们来处理这个问题。
Step 11:
在 Contact.js 中,编写 createProfile 函数, editProfile 函数,以及 removeProfile 函数。
self.createProfile = function () {
alert("Create a new profile");
};
self.editProfile = function (profile) {
alert("Edit tis profile with profile id as :" + profile.ProfileId);
};
self.removeProfile = function (profile) {
if (confirm("Are you sure you want to delete this profile?")) {
self.Profiles.remove(profile);
}
};
现在运行程序,点击删除联系人按钮,将会从数组中删除当前的联系人。由于我们定义了一个观察者数组,所以,UI 界面会与数组保持同步。点击删除按钮可以测试一下。编辑链接和创建联系人链接只是简单地弹出一个提示框,下一步我们实现它们。
Step 12:
下面我们会增加:
- 在 ContactController.cs 类中增加一个 CreateEdit 的 Action 方法,并添加对应的视图。
public ActionResult CreateEdit()
{
return View();
}
- 在 Scripts 文件夹中增加一个名为 CreateEdit.js 的新脚本文件
- 修改Contact.js 中的 createProfile 和 editProfile 方法, 以便指向 CreateEdit 页面。
self.createProfile = function () {
window.location.href = '/Contact/CreateEdit/0';
};
self.editProfile = function (profile) {
window.location.href = '/Contact/CreateEdit/' + profile.ProfileId;
};
重新运行程序,所有的事件处理都可以工作了。创建和编辑联系人会定有参数的重定向到 CreateEdit 页面。
Step 13:
首先,我们从 CreateEdit 页面的添加联系人信息开始,我们需要如下的工作:
我们需要从 Url 中获取联系人的标识,下面的前两行处理这个问题。
var url = window.location.pathname;
var profileId = url.substring(url.lastIndexOf('/') + 1);
var DummyProfile = [
{
"ProfileId": 1,
"FirstName": "Anand",
"LastName": "Pandey",
"Email": "anand@anandpandey.com"
},
{
"ProfileId": 2,
"FirstName": "John",
"LastName": "Cena",
"Email": "john@cena.com"
}
]
Profile 是一个简单的 Javascript 类,用来存储联系人的姓名和电子邮件。
var Profile = function (profile) {
var self = this;
self.ProfileId = ko.observable(profile ? profile.ProfileId : 0);
self.FirstName = ko.observable(profile ? profile.FirstName : '');
self.LastName = ko.observable(profile ? profile.LastName : '');
self.Email = ko.observable(profile ? profile.Email : '');
};
ProfileCollection,是一个 ViewModel 类,用来保存联系人信息,处理 saveProfile 和 backToProfileList 事件。
var ProfileCollection = function () {
var self = this;
//if ProfileId is 0, It means Create new Profile
if (profileId == 0) {
self.profile = ko.observable(new Profile());
}
else {
var currentProfile = $.grep(DummyProfile, function (e) { return e.ProfileId == profileId; });
self.profile = ko.observable(new Profile(currentProfile[0]));
}
self.backToProfileList = function () { window.location.href = '/contact'; };
self.saveProfile = function () {
alert("Date to save is : " + JSON.stringify(ko.toJS(self.profile())));
};
};
最后,使用 ko.applyBindings() 激活 Knockout.
ko.applyBindings(new ProfileCollection());
Step 14:
下一步,我们需要在 CreateEdit.cshtml 页面写代码以支持显示联系人信息。我们需要使用 with 绑定联系人数据,以便它生成特定联系人的响应子元素,并赋予适当的值。代码如下:
<table class="table">
<tr>
<th colspan="3">Profile Information</th>
</tr>
<tr></tr>
<tbody data-bind='with: profile'>
<tr>
<td>
<input class="input-large" data-bind='value: FirstName' placeholder="First Name"/>
</td>
<td>
<input class="input-large" data-bind='value: LastName' placeholder="Last Name"/>
</td>
<td>
<input class="input-large" data-bind='value: Email' placeholder="Email" />
</td>
</tr>
</tbody>
</table>
<button class="btn btn-small btn-success" data-bind='click: saveProfile'>Save Profile</button>
<input class="btn btn-small btn-primary" type="button" value="Back To Profile List" data-bind="click:$root.backToProfileList" />
<script src="~/Scripts/CreateEdit.js"></script>
当创建新联系人的时候。
编辑现有的编号为 1 的联系人的时候:
更新现有的数据,点击保存的时候:
对于这个界面的每个需求,我们已经完成:
2.1 用户可以编辑姓名,电子邮件地址。
2.6 点击保存按钮保存到数据库,返回联系人列表。
2.7 点击返回,回到联系人列表。
下面我们处理下面的两个需求:
2.2 通过点击添加按钮,用户可以为联系人增加多个电话
2.3 用户可以删除电话
Step 15:
为了达到 2.2 和 2.3 的需求,我们需要:
在 CreateEdit.js 中定义一个假的 PhoneType 和 PhoneDTO 数据数组。phoneTypeData 将用来绑定到下列列表.
var phoneTypeData = [
{
"PhoneTypeId": 1,
"Name": "Work Phone"
},
{
"PhoneTypeId": 2,
"Name": "Personal Phone"
}
];
var PhoneDTO = [
{
"PhoneId":1,
"PhoneTypeId": 1,
"ProfileId":1,
"Number": "111-222-3333"
},
{
"PhoneId": 2,
"PhoneTypeId": 2,
"ProfileId": 1,
"Number": "444-555-6666"
}
];
PhoneLine 是一个 JavaScript 类,用来保存一行电话信息,
var PhoneLine = function (phone) {
var self = this;
self.PhoneId = ko.observable(phone ? phone.PhoneId : 0);
self.PhoneTypeId = ko.observable(phone ? phone.PhoneTypeId : 0);
self.Number = ko.observable(phone ? phone.Number : '');
};
修改 ProfileCollection ViewModel 类,对于 addPhone 和 removePhone 事件保存电话号码。
var ProfileCollection = function () {
var self = this;
//if ProfileId is 0, It means Create new Profile
if (profileId == 0) {
self.profile = ko.observable(new Profile());
self.phoneNumbers = ko.observableArray([new PhoneLine()]);
}
else {
var currentProfile = $.grep(DummyProfile, function (e) { return e.ProfileId == profileId; });
self.profile = ko.observable(new Profile(currentProfile[0]));
var currentProfilePhone = $.grep(PhoneDTO, function (e) { return e.ProfileId == profileId; });
self.phoneNumbers = ko.observableArray(ko.utils.arrayMap(currentProfilePhone, function (phone) {
return phone;
}));
}
self.addPhone = function () {
self.phoneNumbers.push(new PhoneLine())
};
self.removePhone = function (phone) { self.phoneNumbers.remove(phone) };
self.backToProfileList = function () { window.location.href = '/contact'; };
self.saveProfile = function () {
alert("Date to save is : " + JSON.stringify(ko.toJS(self.profile())));
};
};
Step 16:
下一步我们在 CreateEdit.cshtml 页面中添加过个节来添加电话信息。一个联系人可以拥有多个不同类型的电话,我们使用 foreach 绑定来处理电话数据,在 CreateEdit.cshtml 中添加下面的节,位置在联系人信息之后,保存按钮之前。
<table class="table">
<tr>
<th colspan="3">Phone Information</th>
</tr>
<tr></tr>
<tbody data-bind='foreach: phoneNumbers'>
<tr>
<td>
<select data-bind="options: phoneTypeData, value: PhoneTypeId, optionsValue: 'PhoneTypeId', optionsText: 'Name', optionsCaption: 'Select Phone Type...'"></select>
</td>
<td>
<input class="input-large" data-bind='value: Number' placeholder="Number" />
</td>
<td>
<a class="btn btn-small btn-danger" href='#' data-bind=' click: $parent.removePhone'>X</a>
</td>
</tr>
</tbody>
</table>
<p>
<button class="btn btn-small btn-primary" data-bind='click: addPhone'>Add New Phone</button>
</p>
现在运行界面如下:
编辑现有联系人的界面:
现在只剩下下面的两个需求:
2.4 用户可以添加多个地址
2.5 用户可以删除地址
Step 17:
需求的 2.4 和 2.5 类似电话处理, 下面是最终的代码:
CreateEdit.js
var url = window.location.pathname;
var profileId = url.substring(url.lastIndexOf('/') + 1);
var DummyProfile = [
{
"ProfileId": 1,
"FirstName": "Anand",
"LastName": "Pandey",
"Email": "anand@anandpandey.com"
},
{
"ProfileId": 2,
"FirstName": "John",
"LastName": "Cena",
"Email": "john@cena.com"
}
];
var PhoneTypeData = [
{
"PhoneTypeId": 1,
"Name": "Work Phone"
},
{
"PhoneTypeId": 2,
"Name": "Personal Phone"
}
];
var PhoneDTO = [
{
"PhoneId":1,
"PhoneTypeId": 1,
"ProfileId":1,
"Number": "111-222-3333"
},
{
"PhoneId": 2,
"PhoneTypeId": 2,
"ProfileId": 1,
"Number": "444-555-6666"
}
];
var AddressTypeData = [
{
"AddressTypeId": 1,
"Name": "Shipping Address"
},
{
"AddressTypeId": 2,
"Name": "Billing Address"
}
];
var AddressDTO = [
{
"AddressId": 1,
"AddressTypeId": 1,
"ProfileId": 1,
"AddressLine1": "10000 Richmond Avenue",
"AddressLine2": "Apt # 1000",
"Country": "USA",
"State": "Texas",
"City": "Houston",
"ZipCode": "70000"
},
{
"AddressId": 2,
"AddressTypeId": 2,
"ProfileId": 1,
"AddressLine1": "20000 Highway 6",
"AddressLine2": "Suite # 2000",
"Country": "USA",
"State": "Texas",
"City": "Houston",
"ZipCode": "80000"
}
];
var Profile = function (profile) {
var self = this;
self.ProfileId = ko.observable(profile ? profile.ProfileId : 0);
self.FirstName = ko.observable(profile ? profile.FirstName : '');
self.LastName = ko.observable(profile ? profile.LastName : '');
self.Email = ko.observable(profile ? profile.Email : '');
self.PhoneDTO = ko.observableArray(profile ? profile.PhoneDTO : []);
self.AddressDTO = ko.observableArray(profile ? profile.AddressDTO : []);
};
var PhoneLine = function (phone) {
var self = this;
self.PhoneId = ko.observable(phone ? phone.PhoneId : 0);
self.PhoneTypeId = ko.observable(phone ? phone.PhoneTypeId : 0);
self.Number = ko.observable(phone ? phone.Number : '');
};
var AddressLine = function (address) {
var self = this;
self.AddressId = ko.observable(address ? address.AddressId : 0);
self.AddressTypeId = ko.observable(address ? address.AddressTypeId : 0);
self.AddressLine1 = ko.observable(address ? address.AddressLine1 : '');
self.AddressLine2 = ko.observable(address ? address.AddressLine2 : '');
self.Country = ko.observable(address ? address.Country : '');
self.State = ko.observable(address ? address.State : '');
self.City = ko.observable(address ? address.City : '');
self.ZipCode = ko.observable(address ? address.ZipCode : '');
};
var ProfileCollection = function () {
var self = this;
//if ProfileId is 0, It means Create new Profile
if (profileId == 0) {
self.profile = ko.observable(new Profile());
self.phoneNumbers = ko.observableArray([new PhoneLine()]);
self.addresses = ko.observableArray([new AddressLine()]);
}
else {
//For Profile information
var currentProfile = $.grep(DummyProfile, function (e) { return e.ProfileId == profileId; });
self.profile = ko.observable(new Profile(currentProfile[0]));
//For Phone number
var currentProfilePhone = $.grep(PhoneDTO, function (e) { return e.ProfileId == profileId; });
self.phoneNumbers = ko.observableArray(ko.utils.arrayMap(currentProfilePhone, function (phone) {
return phone;
}));
//For Address
var currentProfileAddress = $.grep(AddressDTO, function (e) { return e.ProfileId == profileId; });
self.addresses = ko.observableArray(ko.utils.arrayMap(currentProfileAddress, function (address) {
return address;
}));
}
self.addPhone = function () { self.phoneNumbers.push(new PhoneLine()) };
self.removePhone = function (phone) { self.phoneNumbers.remove(phone) };
self.addAddress = function () { self.addresses.push(new AddressLine()) };
self.removeAddress = function (address) { self.addresses.remove(address) };
self.backToProfileList = function () { window.location.href = '/contact'; };
self.saveProfile = function () {
self.profile().AddressDTO = self.addresses;
self.profile().PhoneDTO = self.phoneNumbers;
alert("Date to save is : " + JSON.stringify(ko.toJS(self.profile())));
};
};
ko.applyBindings(new ProfileCollection());
CreateEdit.cshtml
<table class="table">
<tr>
<th colspan="3">Profile Information</th>
</tr>
<tr></tr>
<tbody data-bind='with: profile'>
<tr>
<td>
<input class="input-large" data-bind='value: FirstName' placeholder="First Name"/>
</td>
<td>
<input class="input-large" data-bind='value: LastName' placeholder="Last Name"/>
</td>
<td>
<input class="input-large" data-bind='value: Email' placeholder="Email" />
</td>
</tr>
</tbody>
</table>
<table class="table">
<tr>
<th colspan="3">Phone Information</th>
</tr>
<tr></tr>
<tbody data-bind='foreach: phoneNumbers'>
<tr>
<td>
<select data-bind="options: PhoneTypeData, value: PhoneTypeId, optionsValue: 'PhoneTypeId', optionsText: 'Name', optionsCaption: 'Select Phone Type...'"></select>
</td>
<td>
<input class="input-large" data-bind='value: Number' placeholder="Number" />
</td>
<td>
<a class="btn btn-small btn-danger" href='#' data-bind=' click: $parent.removePhone'>X</a>
</td>
</tr>
</tbody>
</table>
<p>
<button class="btn btn-small btn-primary" data-bind='click: addPhone'>Add New Phone</button>
</p>
<hr />
<table class="table">
<tr><th colspan="5">Address Information</th></tr>
<tbody data-bind="foreach: addresses">
<tr>
<td colspan="5">
<select data-bind="options: AddressTypeData, value: AddressTypeId, optionsValue: 'AddressTypeId', optionsText: 'Name', optionsCaption: 'Select Address Type...'"></select>
</td>
</tr>
<tr>
<td