制作添加页面

通过两节的实践,大家了解到了从 Controller 到 Action 到 View 的添加和编写,Service 类如何实现,并且如何在 Controller 里使用 Service 类中编写的方法,以及使用 Razor 语法来编写视图,甚至还做出了列表和编辑页面,实现了保存功能!那现在再接再厉,把添加页面也搞定吧!
本节的目标为,在 List 页面上添加一个超链接,点击该超链接会访问 /MyUser/Add 并展示出添加页面,点击“保存”可以向 MyUser 表中添加一条新纪录,并将页面重定向至这条记录的编辑页面。

制作添加页面

上一节中,大家应该对新建 Action 之后要做哪些事情有了更深的印象:添加 Action、View Model、View、实体类到 ViewModel 的映射,这三步需要在 Web 项目中完成,其次需要在 Service 项目中编写需要用到的业务逻辑代码,最后在 Controller 中实现相应的 Action;如果是首次添加某个实体类,还需要在 Data 项目中编写实体类映射。本节中就不再一步一步带领大家做上面的工作了。
可以先自己动手做,然后把遇到的问题记录下来,最后和下面列举出所有新增加的代码进行比较。这是添加页面的最终效果,相比编辑页面,多出了“Password”的输入框,其它都是一样的。
https://dev.shijinet.cn/trac/Kunlun/raw-attachment/wiki/bill.nong/blog/20170307173236/Add page.png

Services/Users/MyUserService.cs 在 Service 层中实现插入操作

    public void Add(MyUser myUser)
    {
        _myUserRepository.Insert(myUser);
    }

Web/Models/MyUser/AddMyUserModel.cs 添加 View Model

    using System;
    using System.Collections.Generic;
    using System.Web.Mvc;

    namespace Kunlun.CRS.Web.Models.MyUser
    {
        public class AddMyUserModel
        {
            public int Id { get; set; }

            public string Username { get; set; }

            public string Password { get; set; }

            public int Gender { get; set; }

            public DateTime? Birthday { get; set; }

            public DateTime? LastLoginDate { get; set; }

            public List<SelectListItem> Genders { get; set; }
        }
    }

Web/Controllers/MyUserController.cs 添加 Action

    public ActionResult Add()
    {
        var genderDataSource = new List<SelectListItem>();
        genderDataSource.Add(new SelectListItem { Value = "0", Text = "男" });
        genderDataSource.Add(new SelectListItem { Value = "1", Text = "女" });

        var model = new AddMyUserModel();
        model.Genders = genderDataSource;

        return View(model);
    }

    [HttpPost]
    public ActionResult Add(AddMyUserModel model)
    {
        var entity = model.MapTo<AddMyUserModel, MyUser>();
        _myUserService.Add(entity);

        SuccessNotification("添加成功");
        return RedirectToAction("Edit", new { Id = entity.Id });
    }

Web/Views/MyUser/Add.cshtml 添加视图

    @model Kunlun.CRS.Web.Models.MyUser.AddMyUserModel
    @{
        ViewBag.Title = "Add";
    }

    @using (Html.BeginForm())
    {
        @Html.AntiForgeryToken()

        <div class="box box-solid">
            <div class="box-body">
                <div class="row">
                    <div class="col-xs-12 col-sm-6 col-md-6">
                        <div class="form-group">
                            @Html.LabelFor(m => m.Username)
                            @Html.TextBoxFor(m => m.Username, new { @class = "form-control" })
                            @Html.ValidationMessageFor(m => m.Username)
                        </div>
                    </div>
                </div>

                <div class="row">
                    <div class="col-xs-12 col-sm-6 col-md-6">
                        <div class="form-group">
                            @Html.LabelFor(m => m.Password)
                            @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
                            @Html.ValidationMessageFor(m => m.Password)
                        </div>
                    </div>
                </div>

                <div class="row">
                    <div class="col-xs-12 col-sm-6 col-md-6">
                        <div class="form-group">
                            @Html.LabelFor(m => m.Gender)
                            @Html.DropDownListFor(m => m.Gender, Model.Genders, new { @class = "form-control full-width" })
                            @Html.ValidationMessageFor(m => m.Gender)
                        </div>
                    </div>
                </div>

                <div class="row">
                    <div class="col-xs-12 col-sm-6 col-md-6">
                        <div class="form-group">
                            @Html.LabelFor(m => m.Birthday)
                            @Html.KLDateTextBoxFor(m => m.Birthday, new { @class = "form-control kl-date-picker full-width" })
                            @Html.ValidationMessageFor(m => m.Birthday)
                        </div>
                    </div>
                </div>
            </div>

            <div class="box-footer">
                <button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
                <a href="@Url.Action("List")" class="btn btn-default"><i class="fa fa-undo"></i> 返回</a>
            </div>
        </div>
    }

    @section scripts{
        @Html.Partial("_KendoDatePickerPartial")
        @Html.Partial("_KendoDropdownListPartial")

        <script>

        </script>
    }

Web/Infrastructure/AutoMapperStartupTask.cs 添加 AutoMapper 映射

    Mapper.CreateMap<AddMyUserModel, MyUser>();

此外别忘了回到 List 页面的代码中加上添加页面的入口,在 List.cshtml 的 <table> 上面加上

    <div>
        <a class="btn btn-primary" href="@Url.Action("Add")">添加</a>
    </div>

这样就能从 List 页面进入添加页面了。

完善添加页面

让我们进入 Add 页面,直接点击“保存”——你会看到一个熟悉的错误

不能将值 NULL 插入列 'Username',表 'ECRS_20170124.dbo.MyUser';列不允许有 Null 值。INSERT 失败。

因为 MyUser 表的 Username 列是不可空的。上一节在制作编辑页面时 Password 也遇到了这个问题,不过当时 Password 的值已经存在于数据库中了,所以采用了 AutoMapper 忽略映射的方案。而作为添加页面,数据库中是没有这条记录的,我们只能阻止用户将 NULL 提交给服务器。

添加验证

所谓阻止用户将 NULL 提交给服务器,就是说当 Username 文本框没有任何内容时,用户点击“保存”按钮将无法进行保存,并看到一行错误提示告诉他 Username 是必填的。这样的功能我们是通过验证来实现的,并使用了 Fluent Validation 这款第三方类库,它能很好的与 ASP.NET MVC 自带的验证结合,并以更流畅(Fluent)的方式编写验证规则。
我们这里需要为 AddMyUserModel 类的 UsernamePassword 添加不允许为空的验证。眼尖的童鞋一定看到了 Web 项目下有一个 Validators 文件夹,验证规则就存放在这里面。在这里新建一个 MyUser 文件夹并在里面添加一个 AddMyUserValidator 类文件,按照下面这样编写内容。(这个类文件的命名一般采用 View Model 的名字,然后将后缀 Model 改为 Validator 即可。)

    using FluentValidation;
    using Kunlun.CRS.Web.Models.MyUser;

    namespace Kunlun.CRS.Web.Validators.MyUser
    {
        public class AddMyUserValidator : AbstractValidator<AddMyUserModel>
        {
            public AddMyUserValidator()
            {

            }
        }
    }

大体和 Entity Framework 的映射规则的套路是一致的,我们的类是从另一个泛型类(AbstractValidator<T>)派生的,泛型参数即为需要配置验证规则的类。也就是说,我们要为 AddMyUserModel 类的属性添加验证,那么这里的泛型类型 T 就是 AddMyUserModel
接着在构造函数中添加规则:

    public AddMyUserValidator()
    {
        RuleFor(m => m.Username)
            .NotEmpty().WithMessage("Username 不能为空");

        RuleFor(m => m.Password)
            .NotEmpty().WithMessage("Password 不能为空");
    }

请一定要了解 string 的值为 NULL 和 Empty 的区别:NULL(null) 说明根本不存在,而 Empty("") 则是“空白”的意思,也就是说字符串有内容,但是是空白的。String 类提供了 String.IsNullOrEmpty() 方法来判断一个字符串是否是 NULL 或空白。上面编写验证规则时,在调用 NotEmpty() 的时候还有很多其它规则,其中有一个叫做 NotNull() 的方法。这两个方法的区别在于,NotNull()仅仅判断不许为 NULL,而 NotEmpty() 即不允许为 NULL 也不允许为 Empty 还不许为默认值。string 是一个引用类型,它的默认值为 NULL。但对于一些诸如 int 的值类型,它的默认值为 0,那么当使用 NotEmpty() 验证一个 int 类型时,它既不能留空,也不能为 0,请留意这点区别。

之后千万别忘了回到 View Model AddMyUserModel 中,为它添加 Validator 特性,这样才能为 AddMyUserModel应用验证规则:

    namespace Kunlun.CRS.Web.Models.MyUser
    {
        [Validator(typeof(AddMyUserValidator))]
        public class AddMyUserModel
        {
            // 后略
            // ...

重新编译后回到页面上刷新,然后什么也不输入点击“保存”按钮,接下来就是见证奇迹的时候了:
https://dev.shijinet.cn/trac/Kunlun/raw-attachment/wiki/bill.nong/blog/20170307173236/Validator.png

当按下“保存”按钮的时候,Username 和 Password 文本框的下方立刻出现了一行红字提示用户这两个字段不能为空。往这两个文本框中输入一些内容后,下面的提示会自动消失,然后点击“保存”按钮我们就会向数据库中插入这条记录,并跳转到上一节中制作的 Edit 页面了。

理解前端验证

其实验证分为前端验证后台验证两个部分,刚才我们点击“保存”按钮后,页面并没有提交(可观测的现象就是页面没有重新加载),这种方式就是前端验证。前端验证依赖于 jQuery ValidationjQuery Validation Unobtrusive 这两个类库,看名字就能知道它是依赖于 jQuery 的,而上面提到的 Fluent Validation 是一个 .NET 类库。
但为什么我们没有写任何 JavaScript 代码,页面就自动出现了前端验证呢?这是因为 Fluent Validation 最终也是向 ASP.NET MVC 的验证中添加规则的,这些规则的一部分能被 ASP.NET MVC 验证框架识别,并添加到生成的 HTML 页面中。我们可以通过浏览器的 F12 工具,对比一下往 AddMyUserModel 类上添加 ValidatorAttribute 前后,视图页中的 Username、Password 部分最终生成的 HTML 代码。

https://dev.shijinet.cn/trac/Kunlun/raw-attachment/wiki/bill.nong/blog/20170307173236/jQuery Validation attribute.png

可以看到,我们没有修改 Add.cshtml 中的任何代码,生成的 HTML 却发生了改变,这就是因为 Html.TextboxFor() 内部的代码从 ASP.NET MVC 验证模块了解到了我们添加的验证规则导致的。
生成代码的差异主要在于 <input> 标签上多出了 data-valdata-val-required。很容易发现 data-val-required 的内容是我们添加的提示信息。其实 data-val-* 的 * 这一部分可以代表很多验证规则,这里的 required 表示的是必填验证。jQuery Validation Unobtrusive 会在页面加载时找到所有的带有 data-val=true 属性的表单元素,然后分别读取他们的 data-val-* 属性,最后把这些规则发送给 jQuery Validation,由jQuery Validation 来生成前端验证。即 Unobtrusive 只是做了一些幕后工作,最终验证想显示出来得依靠 jQuery Validation。

触发后台验证

由于有前端验证的存在,轻易不会让后台验证产生错误提示,但我们只用把前端验证相关的代码移除即可。上面已经了解到前端验证主要依赖于 jQuery Validation 和 jQuery Validation Unobtrusive 这两个 js 插件,那我们将这两个插件从页面中移除。这需要前往模板页,它位于 Web 项目的 Views/Shared/_Layout.cshtml。打开这个文件,然后搜索 @Scripts.Render("~/bundles/jqueryval") ,然后把这整行注释掉。
此外我们还有一个阻碍,回到 MyUserControllerAdd(AddMyUserModel model) Action。这个方法的实现非常简单,直接从 View Model 映射出了实体类然后往数据库保存。这有一个问题,我们不应该保存 Username 和 Password 为空的内容!但因为我们添加了验证规则,所以这里可以通过 ModelState 属性(它定义在 Controller 类中,可以直接使用)的 IsValid 属性来判断当前传入的 View Model 是否符合验证规则——它会返回一个布尔类型,如果验证通过则会为 true。让我们来改写 Add(AddMyUserModel model) 方法:

    [HttpPost]
    public ActionResult Add(AddMyUserModel model)
    {
        if (ModelState.IsValid)
        {
            var entity = model.MapTo<AddMyUserModel, MyUser>();
            _myUserService.Add(entity);

            SuccessNotification("添加成功");
            return RedirectToAction("Edit", new { Id = entity.Id });
        }

        return View(model);
    }

当验证通过,才往数据库中保存,不通过的话则通过 return View() 方法,再直接将 View Model 传给页面。让我们编译后到页面中什么也不输入直接“保存”看看效果。
非常棒,我们看见了熟悉的错误页面,上面写道

具有键“Gender”的 ViewData 项属于类型“System.Int32”,但它必须属于类型“IEnumerable<SelectListItem>”。

其实这很好理解,看一眼 GET 的 Add() 方法就能明白,我们在 return View(model) 之前,手动为 View Model 的 Genders 属性(Gender 下拉菜单的数据源)赋了值,而在 Add(AddMyUserModel model) 中却没有!
你可能需要问,为什么 GET 时已经为 Genders 赋过值了,当它 POST 回服务器时却没有了呢?这是因为我们并没有使用表单元素(比如隐藏域)去储存 Genders 的内容(我们只有一个下拉菜单,它的 name 叫 Gender)。所以当 Add(AddMyUserModel model) 方法收到 View Model 时,它里面的 Genders 属性是个 NULL(可以设断点亲自看一看)。
也就是说,需要在 Add(AddMyUserModel model) 中验证失败时,再次为 Genders 属性赋值。这一点很重要!下面来改写Add(AddMyUserModel model) 中的代码:

    [HttpPost]
    public ActionResult Add(AddMyUserModel model)
    {
        if (ModelState.IsValid)
        {
            var entity = model.MapTo<AddMyUserModel, MyUser>();
            _myUserService.Add(entity);

            SuccessNotification("添加成功");
            return RedirectToAction("Edit", new { Id = entity.Id });
        }

        var genderDataSource = new List<SelectListItem>();
        genderDataSource.Add(new SelectListItem { Value = "0", Text = "男" });
        genderDataSource.Add(new SelectListItem { Value = "1", Text = "女" });

        model.Genders = genderDataSource;
        return View(model);
    }

这时候再重新编译,到页面上保存试试,我们能看到页面刷新了一下,然后和前端验证一样在 Username 和 Password 下方出现了提示信息。
好了,不要忘了回到模板页中将注释掉的前端验证 js 类库引用给恢复。通过对比我们看到,前端验证因为不需要刷新页面,用户体验更好;但后台验证是必不可少的,居心不良的用户可以想尽办法绕过前端验证,所以后台验证是我们最后一道防线。

请记住并非所有后台验证都能生成前端验证,例如我们在 Fluent Validation 中使用 Must() 规则时就只会生成后台验证。此时我们需要手动编写一个校验逻辑与后台一致的自定义前端验证,并在表单提交前触发它。

移除重复代码

通过几次改进,我们实现了验证功能,但代价是 Genders 的初始化语句到处都是,而且代码都是一样的,这时就可以考虑将它们封装一下。在 MyUserController 中添加一个私有方法 GetGenders(),它返回 List<SelectListItem> 类型,然后将重复代码移动进去:

    public ActionResult Add()
    {
        var model = new AddMyUserModel();
        model.Genders = GetGenders();

        return View(model);
    }

    [HttpPost]
    public ActionResult Add(AddMyUserModel model)
    {
        if (ModelState.IsValid)
        {
            var entity = model.MapTo<AddMyUserModel, MyUser>();
            _myUserService.Add(entity);

            SuccessNotification("添加成功");
            return RedirectToAction("Edit", new { Id = entity.Id });
        }

        model.Genders = GetGenders();
        return View(model);
    }

    private List<SelectListItem> GetGenders()
    {
        var genderDataSource = new List<SelectListItem>();
        genderDataSource.Add(new SelectListItem { Value = "0", Text = "男" });
        genderDataSource.Add(new SelectListItem { Value = "1", Text = "女" });

        return genderDataSource;
    }

可以看到代码一下清爽了许多。当我们开发过程中发现相同代码到处都是的时候,就可以考虑封装、重构一下,以减少代码重复率。这样最大的好处就是,某天性别中加入了一项“其它”,不需要修改每一个性别数据源,修改封装好的这个方法就行了!但也需要注意,不要过度封装,合理、便捷,关键是方法名需要可读。

防伪标记

上一节在介绍 HTML 帮助类时,提到了 Html.AntiForgeryToken() 方法,当时只是简单的介绍了它会生成一个 name 值为 __RequestVerificationToken 的隐藏域,它称之为防伪标记。防伪标记主要用于防范 CSRF(跨站请求伪造)攻击
在我们的项目中,凡是使用到 POST 请求的地方,都需要添加防伪标记,不论是提交表单,还是 Ajax 的 POST 请求。
你可能会感到困惑,在 POST 的 Action 中,我们并没有做任何特殊处理。那我们先来看看,在 POST 请求中缺少防伪标记会怎么样,那让我们一起看一下。
前往 Add.cshtml,删除 @Html.AntiForgeryToken() 这行代码,保存后到浏览器中尝试添加一条 MyUser 记录。果不其然,当点击“保存”时我们看到了熟悉的错误页面:

所需的防伪 Cookie“Kunlun.CCM.Anti”不存在。

凡是看到这个错误,你就知道是因为自己遗漏了防伪标记了。
在第一节中,我们创建好 MyUserController 之后,将它改为从 BaseController 类派生。在 BaseController 上按下 F12 键转到定义,可以看到在 BaseController 上有一个 Attribute 叫做 KunlunAntiForgery。它的内部会判断当前请求是否是一个 POST 请求,如果是则会触发验证。

本地化

我们的页面一直以来都有一个遗憾,那就是文本框上方的描述都是英文,而按钮却又是中文……CCM 项目有本地化的能力,在这里需要提到 LocaleStringResource 表。这张表中储存了本地化中与多语言相关的内容。查看一下这张表,主要关注这几列

  • LanguageId 语言类别。目前只有两种,C 表示中文,E 表示英文
  • ResourceName 语言资源名称。这个名称是一个占位符
  • ResourceValue 语言资源值。这是真正的内容

在 CCM 页面的右下角有一个链接可以用来切换界面显示的是中文还是英文。我们在程序中不能像之前那样直接把中文内容(如“保存”)写在 View 中,而是将 ResourceName 写在页面或后台代码中,程序框架会根据当前语言和语言资源名称来找到一条语言资源值,并用资源值替换掉资源名称,这样就做到了切换语言时页面显示的内容跟着改变了。

在 View 中实现多语言

首先我们把 Add View 的“保存”按钮改为支持多语言的。到 LocaleStringResource 表中找一下是否已经有“保存”这个多语言资源了

    SELECT *
    FROM LocaleStringResource 
    WHERE resourcevalue LIKE '%保存%'

注意,“保存”是最终要显示到页面上的内容,所以是语言资源的 Value。通过它我们知道了名为“Save”的语言资源名表示“保存”的意思。接下来打开 Add.cshtml 中有“保存”按钮的一行,将保存两个字替换为下方代码相应位置的内容:

    <button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> @T("Save")</button>

保存后刷新页面,似乎没有什么变化,现在点击右下角的“EN”链接切换为英文界面,可以看到“保存”变为了“Save”。上面的代码中,通过为 T 这个属性传入 ResourceName 的方式,实现了多语言的功能。我们在 View 中,一般采用这种方式显示多语言内容。

在 View Model 中实现多语言

接下来为 Username 文本框上方的描述实现多语言显示,这里不需要修改 View,而是到 View Model 中添加。打开 AddMyUserModel.cs并找到 Username 属性,为它添加 ResourceDisplayName Attribute,并将相应的资源名传递给它(“用户名”的资源名为“Username”)

    [ResourceDisplayName("Username")]
    public string Username { get; set; }

重新编译代码然后到页面上刷新一下,便能看到“Username”显示成了“用户名”了。对于通过 View Model 和 Html 帮助类生成的 Label 可以使用这种方式。

在 Controller 中实现多语言

接着是保存成功的提示信息,它位于 POST 的 Add() Action 中,我们打开 MyUserController.cs。首先需要通过依赖注入获取到本地化服务类 LocalizationService,然后将 POST 的 Add() 方法中硬编码的“添加成功”几个字修改一下(“添加成功”的资源名为“InsertSuccess”):

    // 有部分内容被省略 ..

    public class MyUserController : BaseController
    {
        private readonly MyUserService _myUserService;
        private readonly LocalizationService _localizationService;

        public MyUserController(MyUserService myUserService,
            LocalizationService localizationService)
        {
            _myUserService = myUserService;
            _localizationService = localizationService;
        }

    // 省略

    [HttpPost]
    public ActionResult Add(AddMyUserModel model)
    {
        // 省略 ..

        SuccessNotification(_localizationService.GetResource("InsertSuccess"));

        // 省略 ..
    }

    // 省略 ..

在后台代码中,我们使用注入

添加多语言资源

上面介绍的语言资源恰好是系统中已经预置好的,现在我们需要添加一个系统中没有的语言资源。这里以 AddMyUserModel 中的 Birthday 属性为例。预计要添加一个语言资源名为“Birthday”的语言资源,支持中文和英文,中问下显示为“生日”,英文则显示为“Birthday”。首先按上面的方式修改 Birthday 属性。然后到数据库中执行这段脚本:

    IF NOT EXISTS(SELECT id FROM LocaleStringResource WHERE LanguageId='E' AND ResourceName='Birthday')
    BEGIN
        INSERT INTO LocaleStringResource(LanguageId,ResourceName,ResourceValue,insert_user,insert_date,update_user,update_date)
        VALUES('E', 'Birthday', 'Birthday','admin', GETDATE(), 'admin', GETDATE())
    END
    IF NOT EXISTS(SELECT id FROM LocaleStringResource WHERE LanguageId='C' AND ResourceName='Birthday')
    BEGIN
        INSERT INTO LocaleStringResource(LanguageId,ResourceName,ResourceValue,insert_user,insert_date,update_user,update_date)
        VALUES('C', 'Birthday', '生日','admin', GETDATE(), 'admin', GETDATE())
    END

脚本先判断了 LocaleStringResource 表中是否已经存在 Birthday 语言资源,如果不存在则插入一条。因为需要同事支持中文和英文,所以有两条语句需要插入。
这时运行项目,是不会看到 Birthday 变为“生日”两个字的。因为我们的站点在首次启动时,会将 LocaleStringResource 表中所有数据加载到内存中,以后再需要获取多语言的内容就不需要从数据库中查询了,而是到内存中查询——查询内存要比查询数据库快多了。因为这个缘故,当我们添加了语言资源后,如果是开发环境,需要将 IIS Express 退出;若是现场环境,则需要重启站点。

总结

  • 了解了 Fluent Validation、jQuery Validation 两款验证类库,它们与 ASP.NET MVC 验证相结合实现了前端验证和后端验证,前端验证用户体验更好,但后端验证必不可少
  • 当重复功能的代码遍布各处时,是时候进行一些封装和重构,让代码看起来更简洁了
  • 我们的项目中用到 POST 请求时,不要忘了添加防伪标记
  • 本地化在不同位置的实现方式,以及如何添加新的多语言资源

下一节中,我们将回到最开始时制作的列表页面,将它改造为带有查询功能,并且使用无刷新技术呈现查询结果的崭新的列表页面,敬请期待。

扩展阅读

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值