65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (113投票s)

2013 年 1 月 13 日

CPOL

15分钟阅读

viewsIcon

561260

downloadIcon

16901

设计一个必须简单、易于任何 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">&copy; @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();
     }
    
  • 在 Scripts 文件夹下添加一个名为 CreateEdit.js 的新 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;
        };
    

运行应用程序后,所有 Contact List(屏幕 1)中的事件都将正常工作。Create 和 Edit 应该使用必需的参数重定向到 CreateEdit 页面。

步骤 12:首先,我们将开始在此 CreateEdit 页面上添加配置文件信息。为此,我们需要做

  • 我们需要从 URL 获取 profileId,因此,在 CreateEdit.js 页面的顶部添加以下两行
  • var url = window.location.pathname;
    var profileId = url.substring(url.lastIndexOf('/') + 1);
    
  • 在 CreateEdit.js 中定义一个虚拟 Profile 数据作为数组
  • var DummyProfile = [
        {
            "ProfileId": 1,
            "FirstName": "Anand",
            "LastName": "Pandey",
            "Email": "anand@anandpandey.com"
        },
        {
            "ProfileId": 2,
            "FirstName": "John",
            "LastName": "Cena",
            "Email": "john@cena.com"
        }
    ]
    
  • Profile,一个简单的 JavaScript 类构造函数,用于存储配置文件的 FirstName、LastName 和 Email 选择。
  • 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,一个视图模型类,它保存配置文件(提供 Profile 数据的 JavaScript 对象)以及用于 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());
    

步骤 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"
        }
    ];
    
  • Profile,一个简单的 JavaScript 类构造函数,用于存储 PhoneLine 信息,包括其 Phone Type,即它是“Work Phone”还是“Home Phone”等,以及电话号码。
  • 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 视图模型类,使其也保存 phoneNumbers 以及 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())));
        };
    };
    

步骤 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 由任何设计师/开发人员独立创建,而无需了解实际的业务逻辑。!!!太棒了!!!

下一部分中,我将讨论如何设计数据库以及如何使用结构化层来实现业务逻辑。

如果您有任何问题,请随时提问;我很乐意在评论中进一步讨论。感谢您的时间!

© . All rights reserved.