使用 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 由任何设计师/开发人员独立创建,而无需了解实际的业务逻辑。!!!太棒了!!!
在下一部分中,我将讨论如何设计数据库以及如何使用结构化层来实现业务逻辑。
如果您有任何问题,请随时提问;我很乐意在评论中进一步讨论。感谢您的时间!


