使用 ASP.NET MVC 4、EF、Knockoutjs 和 Bootstrap 设计和开发网站:第 1 部分






4.92/5 (113投票s)
设计一个必须简单、易于任何 Web 设计师理解的网站架构,使用 asp.net MVC、EF、Knockoutjs 和 Bootstrap
另一个 MVC 应用程序:简介
如今,所有网站都在快速发展,一旦发展起来,就很难编写、组织和维护。当我们向项目中添加新功能或新开发人员时,任何设计不佳的大型 Web 应用程序都可能失控。因此,本文的目的是设计一个必须简单、易于任何 Web 设计师(初学者到中级)和搜索引擎理解的网站架构。在本文中,我将尝试为任何人在线维护其联系人详细信息设计一个网站。但是,将来,相同的应用程序可以通过添加功能和模块供全球大型社区使用。因此,设计应易于适应,以应对业务的未来增长。
在本文中,我将讨论如何创建和设计用户界面 (UI),以便 UI 与业务逻辑分离,并且可以由任何设计师/开发人员独立创建。对于这部分,我们将使用 ASP.Net MVC、Knockout Jquery 和 Bootstrap。
本系列第二部分将更多地关注使用 SQL Server 2008、Entity Framework 和 Castle Windsor 进行依赖注入的结构化层进行数据库设计和实现业务逻辑。
使用 ASP.NET MVC 4、EF、Knockoutjs 和 Bootstrap 设计和开发网站:第 2 部分关注点分离:主要目标
关键概念是剥离大部分或全部逻辑。逻辑不应绑定到页面。如果我们想在另一页上重用一页的逻辑怎么办?在这种情况下,我们会忍不住复制粘贴。如果我们这样做,我们的项目将变得易于维护。另一个重要概念是将数据访问层与任何业务逻辑分离,因为我们计划使用 Entity Framework,这问题不大,因为 EF 应该已经将此端分开。我们应该能够轻松地将所有 EF 文件移动到另一个项目,然后简单地添加对需要它的项目的引用。以下是高层设计

最终解决方案在 Visual Studio 中看起来如下面的图像

一个解决方案中有七个项目:有必要吗?
这完全取决于您的决定… 提出的设计将提供一些相当相关的优势,包括
- 关注点分离:设计应允许清晰定义的层;意味着将应用程序划分为功能不重叠的独立区域。这样 UI 设计师就可以专注于他们的工作而不必处理业务逻辑(Application.Web),而核心开发人员只能处理主要的业务逻辑(Application.DTO 或 Application.Manager)。
- 生产力:向现有软件添加新功能更容易,因为结构已经到位,并且预先知道每个新代码块的位置,因此可以轻松识别并分离任何问题以应对复杂性,并实现所需的工程质量因素,如健壮性、适应性、可维护性和可重用性。
- 可维护性:由于代码的结构清晰且已知,因此更容易维护应用程序,因此更容易查找错误和异常,并以最小的风险进行修改。
- 适应性:新的技术功能,例如不同的前端,或添加业务规则引擎更容易实现,因为您的软件架构创建了清晰的关注点分离。
- 可重用性:可重用性是设计任何应用程序的另一个关注点,因为它是降低总拥有成本的主要因素之一,我们的设计应考虑我们可以在多大程度上重用创建的 Web 应用程序和不同的层。
在本文的最后一节,我们将详细讨论每个单独层的功能。
工具与技术
要实现最终解决方案,我们需要以下工具/dll
- Visual Studio 2012
- ASP.NET MVC 4(带 Razor 视图引擎)
- Entity Framework 5.0
- Castle Windsor 用于 DI
- SQL Server 2008/2012
- Knockout.js & JQuery
- Castle Windsor 用于 DI
- Bootstrap CSS
我们要实现的目标:需求
屏幕 1:联系人列表 - 查看所有联系人
1.1 此屏幕应显示数据库中所有可用的联系人。1.2 用户应该能够删除任何联系人。
1.3 用户应该能够编辑任何联系人详细信息。
1.4 用户应该能够创建新联系人。
初始草图

屏幕 2:创建新联系人
此屏幕应显示一个空白屏幕,提供以下功能。
2.1 用户应该能够输入他的/她的名字、姓氏和电子邮件地址。2.2 用户应该能够通过单击“添加号码”添加任意数量的电话号码。
2.3 用户应该能够删除任何电话号码。
2.4 用户应该能够通过单击“添加新地址”添加任意数量的地址。
2.5 用户应该能够删除任何地址。
2.6 单击保存按钮会将联系人详细信息保存在数据库中,用户将返回到联系人列表页面。
2.7 单击“返回配置文件”按钮会将用户返回到联系人列表页面。
初始草图

屏幕 3:更新现有联系人
此屏幕应显示所选联系人信息的屏幕。
3.1 用户应该能够修改他的/她的名字、姓氏和电子邮件地址。3.2 用户应能够通过单击“添加号码”或删除链接来修改/删除/添加任意数量的电话号码。
3.3 用户应能够通过单击“添加新地址”或删除链接来修改/删除/添加任意数量的地址。
3.4 单击保存按钮会将联系人详细信息更新到数据库中,用户将返回到联系人列表页面。
3.5 单击“返回配置文件”按钮会将用户返回到联系人列表页面。
>
初始草图

第 1 部分:使用 Knockout.js、ASP.NET MVC 和 Bootstrap 创建 Web 应用程序:面向设计者
在开始 UI 部分之前,让我们看看使用 Knockoutjs 和 Bootstrap 以及 ASP.NET MVC 4 会带来哪些好处。
为什么选择 Knockoutjs:Knockout 是一个 MVVM 模式,它与 JavaScript ViewModel 一起工作。这与 MVC 配合良好的原因在于,序列化为 JSON 的 JavaScript 模型非常简单,而且它也包含在 MVC 4 中。它允许我们用更少的代码开发丰富的 UI,并且每当修改数据时,UI 都会立即反映出来。
为什么选择 Bootstrap:Twitter Bootstrap 是一个简单而灵活的 HTML、CSS 和 Javascript 框架,用于流行的用户界面组件和交互。它包含大量的 CSS 样式、组件和 Javascript 插件。它提供跨平台支持,消除了主要的布局不一致。一切都运行良好!良好的文档和 Twitter Bootstrap 网站本身就是真实示例的绝佳参考。最后,它为我节省了大量时间,因为它将开发时间缩短了一半,测试很少,几乎没有浏览器问题。使用此框架还可以获得其他一些好处
- 12 列网格、固定布局、流式或响应式布局。
- 用于排版、代码(语法高亮显示 Google prettify)、表格、表单、按钮的基础 CSS,并使用 Glpyhicons 的图标。
- Web UI 组件,如按钮、导航菜单、标签、缩略图、警报、进度条和杂项。
- 用于模态框、下拉菜单、滚动监视器、选项卡、工具提示、弹出窗口、警报、按钮、折叠、轮播和类型头部的 Javascript 插件。
在以下步骤中,我们将使用虚拟 JavaScript 数据来完成布局和设计,以根据上述要求构建 UI。
步骤 1:创建一个新的空白解决方案项目;将其命名为“Application”

步骤 2:右键单击解决方案文件夹,然后添加一个类型为 ASP.NET MVC 4 的新项目,使用 Internet Application 模板,视图引擎为 Razor。


步骤 2 之后 - 项目结构应如下面的图像所示

步骤 3:右键单击 References,然后单击 Manage NuGet Packages。在搜索栏中键入 Bootstrap,然后单击 Install 按钮。

步骤 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 文件,例如
@Scripts.Render("~/bundles/knockout")
步骤 4:在 Views 下添加一个名为 Contact 的新文件夹,然后在 Controller 文件夹中添加一个名为 ContactController.cs 的新视图页面 Index.cshtml。并在 Scripts 文件夹下添加一个名为 Contact.js 的新文件。请参考下图。

步骤 5:最后修改 Route.config 中的默认映射路由,将其指向 Contact 控制器,如下所示
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Contact", action = "Index", id = UrlParameter.Optional } );
并根据 BootStrap 语法修改 View/Shared 下的 _Layout.cshtml 文件。以下是修改后的代码
<!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>
步骤 6:现在我们已经完成了运行应用程序的初始设置。输出如下

我们将使用此页面来显示屏幕 1 的要求,即联系人列表 - 查看所有联系人
步骤 7:首先,我们将在 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,这是一个包含 Profiles 的视图模型类,Profiles 是一个包含初始虚拟配置文件数据集合的数组。请注意,它是一个 ko.observableArray,它是普通数组的可观察等价物,这意味着当添加或删除项目时,它可以自动触发 UI 更新。
最后,我们需要使用 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());
步骤 8:接下来,我们将编写 Index.cshtml 页面的代码,该页面应显示配置文件列表。我们需要对 <tbody> 元素使用 foreach 绑定,以便它为 profiles 数组中的每个条目渲染其子元素的副本,然后用一些标记填充该 <tbody> 元素,表示您希望为每个条目创建一个表格行 (<tr>)。
<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 的 css 类来设置表格样式。在上面的示例中,它是;
<table class="table table-striped table-bordered table-condensed">
步骤 9:现在我们需要为每一行添加编辑和删除功能,以及顶部的一个按钮来创建新配置文件。所以我们来做
- 在我们的表模板中再添加一个 <th> 和 <td>,并将其点击事件绑定到 js 中的 removeProfile 函数。
- 修改“名字”行以添加“编辑配置文件”链接,然后将其点击事件绑定到 editProfile 函数。
- 添加一个用于“创建新配置文件”的按钮,并使用 createProfile 函数绑定其点击事件。
所以我们 Index.cshtml 的最终代码是
<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>
输出是

添加的按钮和链接都不会起作用,因为我们还没有编写任何代码,所以让我们在下一步中修复它。
步骤 10:在 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); } };
现在,当我们运行应用程序并单击 Remove 按钮时,它将从当前数组中删除该配置文件。由于我们将此数组定义为可观察的,因此 UI 将与该数组的变化保持同步。尝试单击 Remove 按钮。Edit 链接和 Create Profile 按钮将仅显示警报。因此,让我们在后续步骤中实现此功能
步骤 11:接下来我们将添加
- 在 Views/Contact 中添加一个新的 Razor 视图,名为 CreateEdit.cshtml,并在 ContactController.cs 类中注册它。
public ActionResult CreateEdit() { return View(); }
self.createProfile = function () { window.location.href = '/Contact/CreateEdit/0'; }; self.editProfile = function (profile) { window.location.href = '/Contact/CreateEdit/' + profile.ProfileId; };
运行应用程序后,所有 Contact List(屏幕 1)中的事件都将正常工作。Create 和 Edit 应该使用必需的参数重定向到 CreateEdit 页面。
步骤 12:首先,我们将开始在此 CreateEdit 页面上添加配置文件信息。为此,我们需要做
- 我们需要从 URL 获取 profileId,因此,在 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 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 : ''); };
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(new ProfileCollection());
步骤 13:接下来,我们将编写 CreateEdit.cshtml 页面的代码,该页面应显示配置文件信息。我们需要对 profile 数据使用“with”绑定,以便它为特定配置文件渲染其子元素的副本,然后分配相应的值。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> <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>
运行应用程序将显示以下屏幕
用于创建新

用于现有记录,配置文件 ID 为 1

修改任何现有记录并在点击保存后,会显示以下输出

根据此屏幕的要求,我们已经完成了
2.1 用户应该能够输入他的/她的名字、姓氏和电子邮件地址
2.6 单击保存按钮会将联系人详细信息保存在数据库中,用户将返回到联系人列表页面。
2.7 单击“返回配置文件”按钮会将用户返回到联系人列表页面。
在下一步中,我们将尝试实现以下要求
2.2 用户应该能够通过单击“添加号码”添加任意数量的电话号码。2.3 用户应该能够删除任何电话号码
步骤 14:为了实现要求 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" } ];
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 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()))); }; };
步骤 15:接下来,我们将在 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 用户应该能够删除任何地址
步骤 16:要求 2.4 和 2.5 与电话信息类似,以下是 CreateEdit.js 和 CreateEdit.cshtml 文件的最终代码
对于 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> <input class="input-large" data-bind='value: AddressLine1' placeholder="Address Line1" /> <p style="padding-top: 5px;"><input class="input-large" data-bind='value: State' placeholder="State" /></p> </td> <td> <input class="input-large" data-bind=' value: AddressLine2' placeholder="Address Line2" /> <p style="padding-top: 5px;"><input class="input-large" data-bind='value: Country' placeholder="Country" /></p> </td> <td> <input class="input-large" data-bind='value: City' placeholder="City" /> <p style="padding-top: 5px;"><input class="input-large" data-bind='value: ZipCode' placeholder="Zip Code" /> <a class="btn btn-small btn-danger" href='#' data-bind='click: $root.removeAddress'>X</a></p> </td> </tr> </tbody> </table> <p> <button class="btn btn-small btn-primary" data-bind='click: addAddress'>Add New Address</button> </p> <hr /> <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:创建新联系人 - 此屏幕应显示一个空白屏幕,提供以下功能。

屏幕 3:更新现有联系人 - 此屏幕应显示所选联系人信息的屏幕。

验证
我们几乎完成了我们应用程序的设计部分。现在唯一剩下的是管理用户点击“保存”按钮时的验证。验证是任何 Web 应用程序的主要要求,并且如今大多数都被忽略。通过适当的验证,用户可以知道需要输入什么数据。在下一篇文章中,我将讨论 KnockoutJS Validation 库,可以使用 NuGet 下载它。让我们检查一些最常见的验证场景以及如何使用 knockout validation 来实现它们。
场景 1:名字是必填字段
this.FirstName = ko.observable().extend({ required: true });
场景 2:名字的最大字符数不应超过 50,也不应少于 3 个字符
this.FirstName = ko.observable().extend({ maxLength: 50, minLength:3});
场景 3:名字是必填字段,并且名字的最大字符数不应超过 50,也不应少于 3 个字符。
<p>Scenario 4: Age is a required field in form, and should be always greater than 18 and less than 100</p> <pre>this.Age = ko.observable().extend({ required: true, max: 100, min:18 });
场景 5:电子邮件是必填字段,并且应采用正确的电子邮件格式
this.Email = ko.observable().extend({ required: true, email: true });
场景 6:出生日期是必填字段,并且应采用正确的日期格式
this.DateOfBirth = ko.observable().extend({ required: true, date: true });
场景 7:价格是必填字段,并且应采用正确的数字或小数格式
this.Price = ko.observable().extend({ required: true, number: true });
场景 8:电话号码是必填字段,并且应采用正确的美国格式
注意:有效的美国电话号码格式为:1–xdd–xdd–dddd
字符串开头的“1–”是可选的,破折号也是可选的。x 是 2 到 9 之间的任何数字,而 d 可以是任何数字。
this.Phone = ko.observable().extend({ required: true, phoneUS: true });
场景 9:ToDate 字段必须大于 FromDate 字段
this.ToDate = ko.observable().extend({ equal: function () { return (val > $(‘#FromDate’).val()) ? val : val + "|" } });
场景 9:电话号码应仅接受用户输入的 -+ () 0-9
var regex = /\(?([0-9]{3})\)?([ .-]?)([0-9]{3})\2([0-9]{4})/ this.PhoneNumber = ko.observable().extend({ pattern: regex });
到目前为止,我们已经看到了不同类型的验证场景及其语法;现在让我们在我们的应用程序中实现它。为此,首先使用 NuGet 下载 knockout.validation.js 库。现在我们的验证脚本已完全完成,应该如下所示
var Profile = function (profile) { var self = this; self.ProfileId = ko.observable(profile ? profile.ProfileId : 0).extend({ required: true }); self.FirstName = ko.observable(profile ? profile.FirstName : '').extend({ required: true, maxLength: 50 }); self.LastName = ko.observable(profile ? profile.LastName : '').extend({ required: true, maxLength: 50 }); self.Email = ko.observable(profile ? profile.Email : '').extend({ required: true, maxLength: 50, email: true }); 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 : undefined).extend({ required: true }); self.Number = ko.observable(phone ? phone.Number : '').extend({ required: true, maxLength: 25, phoneUS: true }); }; var AddressLine = function (address) { var self = this; self.AddressId = ko.observable(address ? address.AddressId : 0); self.AddressTypeId = ko.observable(address ? address.AddressTypeId : undefined).extend({ required: true }); self.AddressLine1 = ko.observable(address ? address.AddressLine1 : '').extend({ required: true, maxLength: 100 }); self.AddressLine2 = ko.observable(address ? address.AddressLine2 : '').extend({ required: true, maxLength: 100 }); self.Country = ko.observable(address ? address.Country : '').extend({ required: true, maxLength: 50 }); self.State = ko.observable(address ? address.State : '').extend({ required: true, maxLength: 50 }); self.City = ko.observable(address ? address.City : '').extend({ required: true, maxLength: 50 }); self.ZipCode = ko.observable(address ? address.ZipCode : '').extend({ required: true, maxLength: 15 }); };
验证后,单击“保存”按钮后,我们的最终解决方案看起来如下面的屏幕

结论
我们完成了。嗯,你做到了!希望您喜欢本教程并学到了一些东西。
在本文中,我们讨论了如何在不知道实际实现(数据库交互)的情况下实现我们的 UI,也就是说,UI 由任何设计师/开发人员独立创建,而无需了解实际的业务逻辑。!!!太棒了!!!
在下一部分中,我将讨论如何设计数据库以及如何使用结构化层来实现业务逻辑。
如果您有任何问题,请随时提问;我很乐意在评论中进一步讨论。感谢您的时间!