GVKun编程网logo

学习NetCore应用框架——返回json数据.(.net 返回json数据)

18

如果您对学习NetCore应用框架——返回json数据.感兴趣,那么本文将是一篇不错的选择,我们将为您详在本文中,您将会了解到关于学习NetCore应用框架——返回json数据.的详细内容,我们还将为

如果您对学习NetCore应用框架——返回json数据.感兴趣,那么本文将是一篇不错的选择,我们将为您详在本文中,您将会了解到关于学习NetCore应用框架——返回json数据.的详细内容,我们还将为您解答.net 返回json数据的相关问题,并且为您提供关于.Net Core3.0 WebApi 项目框架搭建 十四:自定义返回Json大小写格式、.NET Core应用框架AA介绍(二)、.Net Core应用框架Util介绍(三)、.Net Core应用框架Util介绍(五)的有价值信息。

本文目录一览:

学习NetCore应用框架——返回json数据.(.net 返回json数据)

学习NetCore应用框架——返回json数据.(.net 返回json数据)

此文档解决以下问题:

1.通过前端页面按钮执行控制器的方法

2.地址栏访问json数据

3.返回json数据(JavaScript的ajax方法)

4.返回json数据(jQuery的ajax方法).

附:ASP.NET Core 官方文档 地址:https://docs.microsoft.com/zh-cn/aspnet/core/?view=aspnetcore-2.2

 


 

 

1.通过前端页面按钮执行控制器的方法

1)AccountController.cs

public IActionResult Index3()
        {
            //View 帮助程序方法
            //用 return View("<ViewName>"); 显式返回
            //如果使用从应用根目录开始的绝对路径(可选择以“/”或“~/”开头),则须指定.cshtml或者.html 扩展名
            //此处介绍Views文件夹外的页面访问
            return View("/pages/demo/index3.html");
        }


        public IActionResult Index5()
        {
            //Redirect是让浏览器重定向到新的地址
            //建议创建在wwwroot项目文件下
            return Redirect("/pages/demo/index5.html");
        }

 

 

2) index5.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <script src="../../lib/jquery.js"></script>
</head>
<body>
    <h2>根目录下wwwroot项目文件夹中pages 中demo文件夹中的index5.html页面</h2>
    <input type="button" name="btn" id="btn" value="跳转(执行account控制器中的index3方法)" />
    <script>

        $(function () {
            $("#btn").click(function () {
                //执行account控制器中的index3方法
                window.location = "/account/index3";
            });
        });
    </script>
</body>
</html>

 

 

 

3)运行浏览  ,account控制器中的index5方法,页面结果如下,点击按钮

 

4) 按钮执行 account控制器中的index3方法,页面结果如下

 

 

2.地址栏访问json数据

1)AccountController.cs

public JsonResult GetJson()
        {
            // 返回Json数据
            //可在浏览器中通过 localhost:端口/[ControllerName]/[MethodName]执行
            //这里是   localhost:端口/Account/GetJson
            return Json(new { Code = 0, Msg = "Json数据测试" });
        }

 

 

 

2) 运行浏览,结果如下

 

 

3.返回json数据(JavaScript的ajax方法)

1)AccountController.cs

        public IActionResult Index6()
        {
            //Redirect是让浏览器重定向到新的地址
            //建议创建在wwwroot项目文件下
            return Redirect("/pages/demo/index6.html");
        }

 

 

2) index6.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>

    <h2>根目录下wwwroot项目文件夹中pages 中demo文件夹中的index6.html页面</h2>
    code:<input type="text" name="code" id="code" value="" /><br />
    msg:<input type="text" name="msg" id="msg" value="" /><br />

    <input type="button" name="btn1" id="btn1" value="返回json数据(JavaScript的ajax方法)" onclick="ajax()" /><br />
    <input type="button" name="btn2" id="btn2" value="返回json数据(jQuery的ajax方法)" /><br />
    <a id="xinxi"></a><br />

</body>
</html>

 

 

 

3) AccountController.cs,添加代码,如下

public IActionResult Index6()
        {
            //Redirect是让浏览器重定向到新的地址
            //建议创建在wwwroot项目文件下
            return Redirect("/pages/demo/index6.html");
        }

        public IActionResult GetJson2([FromBody]Model model)
        {
            //此方法传递模型数据类型的数据
            //可通过ajax来调用
            if (model != null)
            {
                int code = model.Code;
                string msg = model.Msg;
                return Json(new { Code = code, Msg = msg, result = "code=" + code + "*******msg=" + msg });
            }
            else
            {
                return Json(new { result = "Is Null" });
            }
        }
        public class Model
        {   //此类可以放在Models文件夹中
            public int Code { get; set; }
            public string Msg { get; set; }
        }

 

 

 

 

 

 

4) index.html ,添加代码,结果如下

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <script src="../../lib/jquery.js"></script>
</head>
<body>

    <h2>根目录下wwwroot项目文件夹中pages 中demo文件夹中的index6.html页面</h2>
    code:<input type="text" name="code" id="code" value="" /><br />
    msg:<input type="text" name="msg" id="msg" value="" /><br />

    <input type="button" name="btn1" id="btn1" value="返回json数据(JavaScript的ajax方法)" onclick="ajax()" /><br />
    <input type="button" name="btn2" id="btn2" value="返回json数据(jQuery的ajax方法)" /><br />
    <a id="xinxi"></a><br />

    <script>
        function ajax() {
            var code = document.getElementById("code").value;
            var msg = document.getElementById("msg").value;
            var xixn = JSON.stringify({
                code: code,
                msg: msg

            });
            var xhr = new XMLHttpRequest;//创建一个 XMLHttpRequest 对象,XMLHttpRequest是实现ajax的基础
            xhr.open("POST", "/Account/GetJson2", true)//请求方式为"Post","/Account/GetJson2"为服务器地址(在MVC这里就是控制器地址+方法名),true表示选择异步
            xhr.setRequestHeader("Content-type", "application/json")//设置请求参数类型
            xhr.send(xixn);

            xhr.onreadystatechange = function () {
                if (xhr.readyState == 4 && xhr.status == 200) {
                    var s = xhr.responseText;
                    document.getElementById("xinxi").innerHTML = JSON.parse(s).result;
                }

            }
        }
    </script>
</body>
</html>

 

 

5) 运行浏览,先执行accoun控制器中的index6方法

 

6) 输入数据,点击按钮

 

 

6) 在当前页返回数据,结果如下

 

 

4.返回json数据(jQuery的ajax方法)

1)AccountController.cs

public IActionResult Index6()
        {
            //Redirect是让浏览器重定向到新的地址
            //建议创建在wwwroot项目文件下
            return Redirect("/pages/demo/index6.html");
        }

        public IActionResult GetJson2([FromBody]Model model)
        {
            //此方法传递模型数据类型的数据
            //可通过ajax来调用
            if (model != null)
            {
                int code = model.Code;
                string msg = model.Msg;
                return Json(new { Code = code, Msg = msg, result = "code=" + code + "*******msg=" + msg });
            }
            else
            {
                return Json(new { result = "Is Null" });
            }
        }
        public class Model
        {   //此类可以放在Models文件夹中
            public int Code { get; set; }
            public string Msg { get; set; }
        }

 

2) index6.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <script src="../../lib/jquery.js"></script>
</head>
<body>

    <h2>根目录下wwwroot项目文件夹中pages 中demo文件夹中的index6.html页面</h2>
    code:<input type="text" name="code" id="code" value="" /><br />
    msg:<input type="text" name="msg" id="msg" value="" /><br />

    <input type="button" name="btn1" id="btn1" value="返回json数据(JavaScript的ajax方法)" /><br />
    <input type="button" name="btn2" id="btn2" value="返回json数据(jQuery的ajax方法)" /><br />
    <a id="xinxi"></a><br />

    <script>
        $(document).ready(function () {

            $("#btn2").click(function () {
                var code = $("#code").val();
                var msg = $("#msg").val();
                var data = JSON.stringify({
                    code: code,
                    msg: msg

                });
                $.ajax({
                    type: "post",
                    data: data,
                    url: "/account/GetJson2",
                    contentType: ''application/json;charset=utf-8'',//返回json数据,一定要设置contentType 为application/json,其他不需要
                    dataType: "json",
                    success: function (result) {
                        var n = result.result;
                        alert(n)
                    },
                    error: function () {
                        alert("获取数据失败!");
                    }
                });

            });

        });

    </script>

</body>
</html>

 

 

3) 运行浏览,先执行accoun控制器中的index6方法

 

4) 在当前页以弹出框形式返回数据,结果如下

 

   正文结束~~~  

.Net Core3.0 WebApi 项目框架搭建 十四:自定义返回Json大小写格式

.Net Core3.0 WebApi 项目框架搭建 十四:自定义返回Json大小写格式

默认格式

在.net core 3中,默认的json返回格式为驼峰命名法

 

 

 有的时候,我们希望返回的全小写或者全大写等。当然微软爸爸已经提供了拓展的接口。

设置返回Json名称全小写

新建JsonConv文件夹,新建LowercasePolicy.cs,继JsonNamingPolicy

/// <summary>
    /// 返回对象全小写
    /// </summary>
    public class LowercasePolicy : JsonNamingPolicy
    {
        public override string ConvertName(string name) =>
            name.ToLower();
    }

startup.cs的ConfigureServices方法新加AddJsonOptions

services.AddControllers(option =>
            {
                option.Filters.Add(typeof(GlobalExceptionsFilter));
            }).AddJsonOptions(option =>
            {
                //空的字段不返回
                option.JsonSerializerOptions.IgnoreNullValues = true;
                //返回json小写
                option.JsonSerializerOptions.PropertyNamingPolicy = new LowercasePolicy();


            });

继续测试刚才的方法,发现已经转为小写了

 

 格式化时间

JsonConv文件夹新建DateTimeConverter和DateTimeNullableConverter

public class DateTimeConverter : JsonConverter<DateTime>
    {
        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return DateTime.Parse(reader.GetString());
        }

        public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss"));
        }
    }
public class DateTimeNullableConverter : JsonConverter<DateTime?>
    {
        public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return string.IsNullOrEmpty(reader.GetString()) ? default(DateTime?) : DateTime.Parse(reader.GetString());
        }

        public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value?.ToString("yyyy-MM-dd HH:mm:ss"));
        }
    }

AddJsonOptions添加进去

//时间格式格式化
                option.JsonSerializerOptions.Converters.Add(new DateTimeConverter());
                option.JsonSerializerOptions.Converters.Add(new DateTimeNullableConverter());

这样,如果我们的实体是Datetime类型那么最后输出的就是yyyy-MM-dd HH:mm:ss这种格式

.NET Core应用框架AA介绍(二)

.NET Core应用框架AA介绍(二)

AA的开源地址

https://github.com/ChengLab/AAFrameWork 

AA框架是一个基础应用框架,是建立在众多大家熟知的流行工具之上并与之集成。比如:ASP.NET Core、Automapper、Dapper、Dapper-FluentMap、RabbitMQ、Redis、MassTransit、Log4net等等

大家可以很方便的去使用,学习成本很低,也易于扩展。目标能做成一个大家都能吼得住、可以自己改进的框架。

AA这个名字来源于AA制,一起贡献于社区才能从社区获取果实。

基于AA创建一个示例demo

示例demo 很简单,创建一个任务管理的模块,包含增删改查的功能。

示例项目架构图 

示例项目截图

准备工作

数据库脚本:

CREATE TABLE [dbo].[QuartzJobdetail](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [JobGroup] [varchar](100) NULL,
    [JobName] [varchar](100) NULL,
    [RunStatus] [int] NULL,
    [Cron] [varchar](100) NULL,
    [StartTime] [datetime] NULL,
    [EndTime] [datetime] NULL,
    [Description] [varchar](100) NULL,
    [GmtCreateTime] [datetime] NULL,
    [ApiUrl] [varchar](100) NULL,
    [Status] [int] NULL,
 CONSTRAINT [PK_QUARTZJOBDETAIL] PRIMARY KEY CLUSTERED
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

基础设施层

配置数据库连接 和指定数据库类型
public AADapperContent() : base(new NameValueCollection()
        {
            ["aa.dataSource.AaBase.connectionString"] = "Data Source =.; Initial Catalog = QuartzAA-Job;User ID = sa; Password = 123;",
            ["aa.dataSource.AaBase.provider"] = "SqlServer"
        })
        {
        }
配置领域实体和数据库表映射
public class QuartzJobdetailMap:DommelEntityMap<QuartzJobdetail>
    {
        public QuartzJobdetailMap()
        {
            ToTable("QuartzJobdetail");
            
            Map(x=>x.Id).IsKey().IsIdentity();
        }
    }
实现仓储层
public class QuartzJobdetailRepository:DapperRepository<QuartzJobdetail>, IQuartzJobdetailRepository
    {
        public IPage<QuartzJobdetailDto> GetListReturnOrder(GetListQuartzJobDetailInput input)
        {
            object sqlParam = null;
            var sql = new StringBuilder();
            sql.Append("select *  from QuartzJobdetail ");
            sql.Append(" where 1=1");
            var result = DapperContext.Current.DataBase.GetPage<QuartzJobdetailDto>(new PageRequest
            {
                PageIndex = input.PageIndex,
                PageSize = input.PageSize,
                SqlText = sql.ToString(),
                SqlParam = sqlParam,
                OrderFiled = " Id desc ",
            });
            return result;
        }
    }

DapperRepository 实现了IDapperRepository接口,IDapperRepository接口代码如下

public virtual bool Delete(TEntity entity);
        public virtual Task<bool> DeleteAsync(TEntity entity);
        public virtual bool DeleteMultiple(Expression<Func<TEntity, bool>> predicate);
        public virtual Task<bool> DeleteMultipleAsync(Expression<Func<TEntity, bool>> predicate);
        public virtual TEntity FirstOrDefault(Expression<Func<TEntity, bool>> predicate);
        public virtual Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate);
        public virtual TEntity Get(object id);
        public virtual IEnumerable<TEntity> GetAll();
        public virtual Task<IEnumerable<TEntity>> GetAllAsync();
        public virtual Task<TEntity> GetAsync(object id);
        public virtual object Insert(TEntity entity);
        public virtual Task<object> InsertAsync(TEntity entity);
        public virtual IEnumerable<TEntity> Select(Expression<Func<TEntity, bool>> predicate);
        public virtual Task<IEnumerable<TEntity>> SelectAsync(Expression<Func<TEntity, bool>> predicate);
        public virtual bool Update(TEntity entity);
        public virtual Task<bool> UpdateAsync(TEntity entity);

领域层

示例项目,比较简单,并没有完全按照DDD去实现领域服务,领域事件等等,这里主要演示如何基于AA基础框架创建一个项目。领域模型(贫血)代码如下

public class QuartzJobdetail
    {
        /// <summary>
        /// 编号
        /// </summary>
        public int Id
        {
            get;
            set;
        }
        /// <summary>
        /// 任务组
        /// </summary>
        public string JobGroup
        {
            get;
            set;
        }
        /// <summary>
        /// 任务名称
        /// </summary>
        public string JobName
        {
            get;
            set;
        }
        /// <summary>
        /// 运行状态
        /// </summary>
        public int RunStatus
        {
            get;
            set;
        }
        /// <summary>
        /// cron表达式
        /// </summary>
        public string Cron
        {
            get;
            set;
        }
        /// <summary>
        /// 开始日期
        /// </summary>
        public DateTime StartTime
        {
            get;
            set;
        }
        /// <summary>
        /// 结束日期
        /// </summary>
        public DateTime EndTime
        {
            get;
            set;
        }
        /// <summary>
        /// 描述
        /// </summary>
        public string Description
        {
            get;
            set;
        }
        /// <summary>
        /// 创建日期
        /// </summary>
        public DateTime GmtCreateTime
        {
            get;
            set;
        }
        /// <summary>
        /// api地址
        /// </summary>
        public string ApiUrl
        {
            get;
            set;
        }
        /// <summary>
        /// 状态
        /// </summary>
        public int Status
        {
            get;
            set;
        }
     }
仓储接口代码如下
public interface IQuartzJobdetailRepository: IDapperRepository<QuartzJobdetail>
    {
        IPage<QuartzJobdetailDto> GetListReturnOrder(GetListQuartzJobDetailInput input);
    }

应用层

服务接口和实现代码如下
public interface IQuartzJobdetailService
    {
        void Save(SaveQuartzJobdetailInput input);
        void Update(UpdateQuartzJobdetailInput input);
        void Remove(RemoveQuartzJobdetailInput input);
        QuartzJobdetailDto GetQuartzJobdetail(GetQuartzJobdetailInput input);
        PagedResultDto<QuartzJobdetailDto> GetList(GetListQuartzJobDetailInput input);
    }
public class QuartzJobdetailService : IQuartzJobdetailService
    {
        #region filed
        private readonly IQuartzJobdetailRepository _quartzJobdetailRepository;
        #endregion

        #region actor
        public QuartzJobdetailService()
        {
            var dapperContent = new AADapperContent();
            _quartzJobdetailRepository = new QuartzJobdetailRepository();
        }
        #endregion

        public void Save(SaveQuartzJobdetailInput input)
        {
            var obj = _quartzJobdetailRepository.Insert(new QuartzJobdetail
            {
                JobGroup = input.JobGroup,
                JobName = input.JobName,
                RunStatus = input.RunStatus,
                Cron = input.Cron,
                StartTime = input.StartTime,
                EndTime = input.EndTime,
                Description = input.Description,
                GmtCreateTime = DateTime.Now,
                ApiUrl = input.ApiUrl,
                Status = input.Status,
            });
        }

        public void Update(UpdateQuartzJobdetailInput input)
        {
            var model = _quartzJobdetailRepository.Get(input.Id);
            model.JobGroup = input.JobGroup;
            model.JobName = input.JobName;
            model.Cron = input.Cron;
            model.StartTime = input.StartTime;
            model.EndTime = input.EndTime;
            model.Description = input.Description;
            model.ApiUrl = input.ApiUrl;
            _quartzJobdetailRepository.Update(model);
        }
        public void Remove(RemoveQuartzJobdetailInput input)
        {
            var model = _quartzJobdetailRepository.Get(input.Id);
            _quartzJobdetailRepository.Delete(model);
        }
        public QuartzJobdetailDto GetQuartzJobdetail(GetQuartzJobdetailInput input)
        {
            var model = _quartzJobdetailRepository.FirstOrDefault(p => p.Description.Contains(input.Description));
            return new QuartzJobdetailDto()
            {

                JobGroup = model.JobGroup,
                JobName = model.JobName,
                RunStatus = model.RunStatus,
                Cron = model.Cron,
                StartTime = model.StartTime,
                EndTime = model.EndTime,
                Description = model.Description,
                ApiUrl = model.ApiUrl,

            };
        }
        public PagedResultDto<QuartzJobdetailDto> GetList(GetListQuartzJobDetailInput input)
        {
            var result = _quartzJobdetailRepository.GetListReturnOrder(input);
            return new PagedResultDto<QuartzJobdetailDto>
            {
                TotalCount = result.Count,
                Items = result.Data.ToList()
            };
        }

表现层

控制器的代码
public IActionResult Index()
        {
            return View();
        }
        /// <summary>
        /// job列表
        /// </summary>
        /// <param name="limit">每页显示条数</param>
        /// <param name="start"></param>
        /// <param name="page">页码</param>
        /// <param name="draw"></param>
        /// <returns></returns>
        public IActionResult GetListQuartzJobdetail(int limit, int start, int page, int draw)
        {
            var result = _quartzJobdetailService.GetList(new GetListQuartzJobDetailInput()
            {
                PageIndex = page,
                PageSize = limit,
            });

            var vm = new PageResponse<QuartzJobdetailViewModel>
            {
                draw = draw,
                recordsTotal = result.TotalCount,
                recordsFiltered = result.TotalCount,
                data = result.Items.MapTo<List<QuartzJobdetailViewModel>>()
            };
            return Json(vm);
        }
        /// <summary>
        /// 添加
        /// </summary>
        /// <param name="vm"></param>
        /// <returns></returns>
        public IActionResult Save(QuartzJobdetailVm vm)
        {
            _quartzJobdetailService.Save(vm.MapTo<SaveQuartzJobdetailInput>());
            return Json(Result.Success("操作成功"));
        }
        /// <summary>
        /// 编辑
        /// </summary>
        /// <param name="vm"></param>
        /// <returns></returns>
        public IActionResult Update(QuartzJobdetailVm vm)
        {
            _quartzJobdetailService.Update(vm.MapTo<UpdateQuartzJobdetailInput>());
            return Json(Result.Success("修改成功"));
        }

        /// <summary>
        /// 删除
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpPost]
        public IActionResult Remove(int id)
        {
            _quartzJobdetailService.Remove(new RemoveQuartzJobdetailInput { Id = id });
            return Json(Result.Success("删除成功"));
        }

最后运行,新增一条记录如图:

 文中展示了部分代码,整个示例项目的代码会上传到github上,这个示例项目不会是最终版本,还会继续。.NET Core 3.1这个长期版本快发布了,期待ing。。。。。

原文出处:https://www.cnblogs.com/chengtian/p/11987401.html

.Net Core应用框架Util介绍(三)

.Net Core应用框架Util介绍(三)

  上篇介绍了Util的开发环境,并让你把Demo运行起来。本文将介绍该Demo的前端Angular运行机制以及目录结构。

 

目录结构

    在VS上打开Util Demo,会看见如下的目录结构。

  

  现代前端通常采用VS Code开发,不过我们为了使用TagHelper,需要采用VS开发,这为你提供了更多的选择。 

  你可以将WebApi和Angular应用放在同一个项目中,就像现在看见的那样。也可以分别把WebApi和Angular应用放到不同项目中。 

  如果你已经习惯了VS Code开发,这同样没问题,不过你将放弃TagHelper带来的强类型代码提示和编译时检查特性。 

  对于Angular,它提供了ng cli命令行工具,你可以用ng cli来创建项目结构。

  前文已简要介绍了TagHelper,它是用来提升Angular视图页面开发效率的利器。为了使用TagHelper,不得不放弃ng cli,因为它不支持在Angular组件上配置服务端动态地址。

  下面介绍这个项目中包含的目录和文件。

 

Apis目录

  这个目录用来存放Web Api控制器。

  ApplicationController演示了普通CRUD操作,RoleController演示了树型层次的CRUD操作。

  你暂时不要关心Web Api CRUD操作,我会在后续介绍。

 

 Areas目录

   用过Asp.Net Mvc的同学可能知道,Areas就是区域,它的作用是提供模块化管理。我们把不同的模块用Areas的区域分隔开,这样在项目规模变大时,还能迅速找到相关页面。

  与传统Asp.Net Mvc应用不同,Util的Areas控制器并不进行任何操作,只是简单的返回视图页面,cshtml仅起到代码生成器的作用。

  一个更好的选择是使用RazorPage,它把控制器和页面合并了,将来会使用这种方式。 

Configs目录

   你并不需要它,我在Demo中用来放测试配置,项目上我通常把Configs目录放在应用层类库。

Controllers目录

   Controllers目录是用来放置与首页相关的控制器。

Datas目录  

  Util引入了DDD经典架构,Datas位于基础设施层,一些人把它叫仓储层。

  Datas通常放在单独的类库,为了演示简单,我放在该WEB项目的目录中。

DbScripts目录

  这个目录提供了Sql Server建库脚本。

  一些人可能很惊讶,什么年代了,还在使用Db First开发。

  在多年的开发实战中,我摸索到一套以PowerDesigner数据建模配合CodeSmith代码生成的开发模式。对于CRUD,它具有快速高效的特点,同时你还能拥有清晰的数据字典以供未来查阅。

  对于具备面向对象编程能力的人,这种方式并不会降低代码质量和设计水平,在将代码生成出来以后,通过手工调整就可达到与Code First相同的代码水平。

  我会在未来某个合适的时候介绍这种开发模式。

 Domains目录

  DDD经典架构中领域层相关的目录,实际开发中将放到单独的类库。

Services目录

  DDD经典架构中应用层相关的目录,实际开发中将放到单独的类库。

Typings目录

   Angular相关的所有东西都在这里。

 

  app目录用来存放与业务相关的项目资源,比如Angular组件,指令,服务等。

  值得注意的是,该目录包含组件对应的.html文件,这些.html文件和.cshtml文件是怎样的关系?

 

 

  如果你从未运行过Util Demo项目,打开app目录,并未找到任何.html文件。

  你可能已经猜到了,.html文件是由.cshtml文件生成的

  你永远都不应该手工编辑这些.html文件,因为在调试运行时将被覆盖。 

  test目录包含Ts单元测试,我仅对极少数Helper进行单元测试。通过下面的npm命令把测试运行起来。

npm test

 

  util目录包含对Angular常用API和Angular Material组件的封装。

  

 

  Angular组件由视图和控制器两部分构成。视图即模板页,包含html标签。控制器用来编写逻辑,包含Ts代码。换句话说,Angular应用开发主要是编写html和ts(当然还有css,暂时不要管它)。

  TagHelper并不是Util封装Angular的唯一手段,对于Angular控制器,Util采用链式封装手法,将Angular常用Api封装得更加简单易用,使你对Angular Api只要有一个模糊的印象就可以开发了。

  对于Angular视图页面,并不能直接采用TagHelper简单包装,这样会导致TagHelper过于复杂,另外很多功能需要在运行时进行判断,TagHelper只在开发调试阶段存在,所以采用两层封装会更加省力。

  首先采用Angular组件或指令对Material组件进行封装,然后采用TagHelper提供强类型提示。  

  对于希望采用VS Code开发的同学,Typings/util目录中封装的代码同样可以使用,它跟TagHelper没有什么关系,你可以把它Copy到你的项目,我尚未把它发布到npm。 

Views目录

  Views目录包含首页。

appsettings.json文件

  它是一个配置文件,数据库连接字符串在这里。

nlog.config文件

  它是NLog日志组件的配置文件,Util 采用NLog输出开发调试和错误日志,默认位置是c:\log目录。

package.json文件

  它是npm包管理器的配置文件。

Program.cs文件

  它是Asp.Net Core程序入口点文件。

Startup.cs文件

  它是Asp.Net Core启动文件,在这里配置依赖注入和中间件请求管道。

tsconfig.json文件

  它是Typescript语言配置文件。

webpack.config.js文件

  它是Webpack自动化构建工具的配置文件。

  还有两个配置文件隐藏在webpack.config.js下,它们对util和第三方Js框架进行处理。

 

运行机制

  对于没有前端基础的同学,可能很难理解这个Demo是如何运行起来的,下面为你介绍这个Demo的运行机制,我们从npm包还原开始。 

 npm还原

  当你输入yarncnpm install node-sass,它会找到package.json文件的dependencies节,然后把需要的文件下载到node_modules目录中。

 执行Webpack构建

  然后输入npm run dev,这里发生了什么?

  npm run是npm的一个命令,它会查找package.jsonscripts定义的命令。

 

  • npm run dev 

  dev就是npm run要查找的命令名,它是一个约定俗成的名称,代表开发阶段配置,即develop,当然你不一定用这个名字,叫abc也可以。

  npm run dev查找到package.json文件scripts节定义的dev命令,它的内容是npm run vendor && npm run app,这个命令是由两个npm run命令组成的。

  •  npm run vendor

  npm run vendor的内容是webpack --config webpack.config.vendor.js,这将对webpack.config.vendor.js执行构建操作。

  webpack命令默认查找webpack.config.js文件,现在要查找的是webpack.config.vendor.js,所以需要添加参数—config。

  我们来看看webpack.config.vendor.js包含什么内容。 

  1 const pathPlugin = require(''path'');
  2 const webpack = require(''webpack'');
  3 var Extract = require("extract-text-webpack-plugin");
  4 
  5 //第三方Js库
  6 const jsModules = [
  7     ''reflect-metadata'',
  8     ''zone.js'',
  9     ''moment'',
 10     ''@angular/animations'',
 11     ''@angular/common'',
 12     ''@angular/common/http'',
 13     ''@angular/compiler'',
 14     ''@angular/core'',
 15     ''@angular/forms'',
 16     ''@angular/elements'',
 17     ''@angular/platform-browser'',
 18     ''@angular/platform-browser/animations'',
 19     ''@angular/platform-browser-dynamic'',
 20     ''@angular/router'',
 21     ''@angular/cdk/esm5/collections.es5'',
 22     ''@angular/flex-layout'',
 23     ''@angular/material'',
 24     ''primeng/primeng'',
 25     ''lodash'',
 26     "echarts-ng2"
 27 ];
 28 
 29 //第三方Css库
 30 const cssModules = [
 31     ''@angular/material/prebuilt-themes/indigo-pink.css'',
 32     ''material-design-icons/iconfont/material-icons.css'',
 33     ''font-awesome/css/font-awesome.css'',
 34     ''primeicons/primeicons.css'',
 35     ''primeng/resources/themes/omega/theme.css'',
 36     ''primeng/resources/primeng.min.css''
 37 ];
 38 
 39 module.exports = (env) => {
 40     //是否开发环境
 41     const isDev = !(env && env.prod);
 42     const mode = isDev ? "development" : "production";
 43 
 44     //将css提取到单独文件中
 45     const extractCss = new Extract("vendor.css");
 46 
 47     //获取路径
 48     function getPath(path) {
 49         return pathPlugin.join(__dirname, path);
 50     }
 51 
 52     //打包第三方Js库
 53     let vendorJs = {
 54         mode: mode,
 55         entry: { vendor: jsModules },
 56         output: {
 57             publicPath: ''dist/'',
 58             path: getPath("wwwroot/dist"),
 59             filename: "[name].js",
 60             library: ''[name]''
 61         },
 62         resolve: {
 63             extensions: [''.js'']
 64         },
 65         devtool: "source-map",
 66         plugins: [
 67             new webpack.DllPlugin({
 68                 path: getPath("wwwroot/dist/[name]-manifest.json"),
 69                 name: "[name]"
 70             }),
 71             new webpack.ContextReplacementPlugin(/\@angular\b.*\b(bundles|linker)/, getPath(''./Typings'')),
 72             new webpack.ContextReplacementPlugin(/angular(\\|\/)core(\\|\/)@angular/, getPath(''./Typings'')),
 73             new webpack.IgnorePlugin(/^vertx$/)
 74         ]
 75     }
 76 
 77     //打包css
 78     let vendorCss = {
 79         mode: mode,
 80         entry: { vendor: cssModules },
 81         output: {
 82             publicPath: ''./'',
 83             path: getPath("wwwroot/dist"),
 84             filename: "[name].css"
 85         },
 86         devtool: "source-map",
 87         module: {
 88             rules: [
 89                 { test: /\.css$/, use: extractCss.extract({ use: isDev ? ''css-loader'' : ''css-loader?minimize'' }) },
 90                 {
 91                     test: /\.(png|jpg|gif|woff|woff2|eot|ttf|svg)(\?|$)/, use: {
 92                         loader: ''url-loader'',
 93                         options: {
 94                             limit: 20000,
 95                             name: "[name].[ext]",
 96                             outputPath: "images/"
 97                         }
 98                     }
 99                 }
100             ]
101         },
102         plugins: [
103             extractCss
104         ]
105     }
106     return isDev ? [ vendorJs, vendorCss] : [vendorCss];
107 }
webpack.config.vendor.js

  vendorJs 对象用于配置将哪些第三方Js框架文件进行打包,vendorCss 对象用于配置需要打包的第三方框架提供的Css文件。 

  entry属性指定了需要打包的入口文件,output属性则指定输出的位置和文件名。 

  当webpack.config.vendor.js执行完毕,会在Util.Samples.Webs项目的wwwroot目录创建一个dist子目录,并生成vendor.jsvendor.css两个文件。

  注意vendor.js仅在开发调试阶段使用,所以并没有对它进行压缩,正式发布并不需要执行vendorJs对象。

  该脚本的最后一行证明了这一点。

return isDev ? [ vendorJs, vendorCss] : [vendorCss];
  •  npm run app 

  npm run app又包含两个命令,用于执行webpack.config.util.jswebpack.config.js

webpack --config webpack.config.util.js && webpack

  先来看看webpack.config.util.js

 1 const pathPlugin = require(''path'');
 2 const webpack = require(''webpack'');
 3 
 4 module.exports = (env) => {
 5     //是否开发环境
 6     const isDev = !(env && env.prod);
 7     const mode = isDev ? "development" : "production";
 8 
 9     //获取路径
10     function getPath(path) {
11         return pathPlugin.join(__dirname, path);
12     }
13 
14     //打包util脚本库
15     return {
16         mode: mode,
17         entry: { util: [getPath("Typings/util/index.ts")] },
18         output: {
19             publicPath: ''dist/'',
20             path: getPath("wwwroot/dist"),
21             filename: "[name].js",
22             library: ''[name]''
23         },
24         resolve: {
25             extensions: [''.js'', ''.ts'']
26         },
27         devtool: "source-map",
28         module: {
29             rules: [
30                 { test: /\.ts$/, use: [''awesome-typescript-loader?silent=true''] }
31             ]
32         },
33         plugins: [
34             new webpack.DllReferencePlugin({
35                 manifest: require(''./wwwroot/dist/vendor-manifest.json'')
36             }),
37             new webpack.DllPlugin({
38                 path: getPath("wwwroot/dist/[name]-manifest.json"),
39                 name: "[name]"
40             })
41         ]
42     }
43 }
webpack.config.util.js

  它将查找Util.Samples.Webs项目下Typings/util/index.ts文件,这是util默认导出文件,所有在外部需要访问的类型都会从这里导出。

  当webpack.config.util.js执行完毕,会在dist目录创建util.js文件。

  同样的,util.js文件仅用于开发调试阶段 

  下面看webpack.config.js

 1 const pathPlugin = require(''path'');
 2 const webpack = require(''webpack'');
 3 var Extract = require("extract-text-webpack-plugin");
 4 const AngularCompilerPlugin = require(''@ngtools/webpack'').AngularCompilerPlugin;
 5 
 6 module.exports = (env) => {
 7     //是否开发环境
 8     const isDev = !(env && env.prod);
 9     const mode = isDev ? "development" : "production";
10 
11     //将css提取到单独文件中
12     const extractCss = new Extract("app.css");
13 
14     //获取路径
15     function getPath(path) {
16         return pathPlugin.join(__dirname, path);
17     }
18 
19     //打包js
20     let jsConfig = {
21         mode: mode,
22         entry: { app: getPath("Typings/main.ts") },
23         output: {
24             publicPath: ''dist/'',
25             path: getPath("wwwroot/dist"),
26             filename: "[name].js",
27             chunkFilename: ''[id].chunk.js''
28         },
29         resolve: {
30             extensions: [''.js'', ''.ts'']
31         },
32         devtool: "source-map",
33         module: {
34             rules: [
35                 { test: /\.ts$/, use: isDev ? [''awesome-typescript-loader?silent=true'', ''angular-router-loader''] : [''@ngtools/webpack''] },
36                 { test: /\.js$/, loader: ''@angular-devkit/build-optimizer/webpack-loader'', options: { sourceMap: false } },
37                 { test: /\.html$/, use: ''html-loader?minimize=false'' }
38             ]
39         },
40         plugins: [
41             new webpack.DefinePlugin({
42                 ''process.env'': { NODE_ENV: isDev ? JSON.stringify("dev") : JSON.stringify("prod") }
43             })
44         ].concat(isDev ? [
45             new webpack.DllReferencePlugin({
46                 manifest: require(''./wwwroot/dist/vendor-manifest.json'')
47             }),
48             new webpack.DllReferencePlugin({
49                 manifest: require(''./wwwroot/dist/util-manifest.json'')
50             })
51         ] : [
52                 new AngularCompilerPlugin({
53                     tsConfigPath: ''tsconfig.json'',
54                     entryModule: "Typings/app/app.module#AppModule"
55                 })
56             ])
57     }
58 
59     //打包css
60     let cssConfig = {
61         mode: mode,
62         entry: { app: getPath("wwwroot/css/main.scss") },
63         output: {
64             publicPath: ''./'',
65             path: getPath("wwwroot/dist"),
66             filename: "[name].css"
67         },
68         resolve: {
69             modules: [''wwwroot'']
70         },
71         devtool: "source-map",
72         module: {
73             rules: [
74                 {
75                     test: /\.scss$/, use: extractCss.extract({
76                         use: isDev ? [''css-loader'', { loader: ''postcss-loader'', options: { plugins: [require(''autoprefixer'')] } }, ''sass-loader'']
77                             : [''css-loader?minimize'', { loader: ''postcss-loader'', options: { plugins: [require(''autoprefixer'')] } }, ''sass-loader'']
78                     })
79                 },
80                 {
81                     test: /\.(png|jpg|gif|woff|woff2|eot|ttf|svg)(\?|$)/, use: {
82                         loader: ''url-loader'',
83                         options: {
84                             limit: 20000,
85                             name: "[name].[ext]",
86                             outputPath: "images/"
87                         }
88                     }
89                 }
90             ]
91         },
92         plugins: [
93             extractCss
94         ]
95     }
96     return [jsConfig, cssConfig];
97 }
webpack.config.js

  webpack.config.js查找Typings目录下的main.ts,main.ts是angular项目的入口文件。

  webpack通过递归依赖查找main.ts,将除了util.js和vendor.js以外所有引用到的ts或js文件打包到dist/app.js文件中。

  注意,正式发布时,app.js将采用angular官方提供的webpack编译插件@ngtools/webpack进行AOT编译并打包生成。

 

  现在dist目录生成了如下文件。

  0.chunk.js是由angular子模块生成的js文件,当路由配置对子模块启用了延迟加载,每个子模块都会生成一个独立的js文件。

 

  loadChildren以延迟加载的方式来配置SystemModule子模块。

 运行机制

  现在运行angular应用的js文件已经就绪,让我们把它运行起来,在VS上F5启动项目。 

  注意:你应该使用Google Chrome来打开它,IE浏览器,可以通过启用polyfill来勉强支持,不过由于效果不佳,我已经把它扔掉了。 

  当浏览器打开首页http://localhost:5200,Asp.Net Core启动文件Startup.cs中配置的默认路由将被激活,从而将请求发送到HomeController控制器的Index方法。

   Index方法直接返回了Views目录下Index.cshtml首页。

 

  environment标签是一个环境判断条件,用于设置开发及上线等不同阶段的内容。

  <environment include="Development">用于开发阶段,<environment exclude="Development">用于发布阶段,可以看出,在发布后并不需要vendor.js和util.js文件,因为app.js会包含它们。 

  好,现在浏览器加载了Index首页,Angular应用是如何运行起来的呢?

  • Angular的引导过程

   还记得Angular应用入口文件main.ts吗,来看看它包含什么内容。

 

  platformBrowserDynamic是为浏览器平台提供的JIT动态编译服务,它将引导AppModule根模块的启动。

  AppModule是Angular应用的根模块,它的主要任务之一就是启动AppComponent根组件。

  AppComponent是整个Angular应用的根组件,所有其它组件都将被加载到根组件中。

 

  selector用于指定组件的自定义标签,这里将根组件标签定义为<app></app>,你发现它已经被放置在Index.cshtml中。 

  AppComponent根组件准备启动了,由于是JIT编译,所以它需要获取视图 

  组件的视图由templateUrl属性指定。

templateUrl: env.prod() ? ''./app.component.html'' : ''/home/main''

  我们希望开发阶段通过访问服务端控制器来获取视图,这样在编辑TagHelper时就能更方便,只需刷新页面就能看见效果。 

  env是一个环境检测对象,prod方法如果返回true表明当前为正式环境,将从app.component.html静态文件获取视图,如果是开发调试环境,则访问服务端HomeController控制器的Main方法获取视图。 

  Main方法上的Html特性,是用来帮助.cshtml生成.html静态文件的辅助工具。 

  一般情况下,你并不需要手工设置Html特性来生成html文件,Util提供了ViewControllerBase控制器基类,当你的视图控制器继承它,所有html文件就会生成到约定的目录中。

 

  由Template属性设置的路径可知,Typings/app中的项目结构也采用模块化组织,与区域模块相对应。

   现在来看根组件的视图。

   这是你第一次看见Util封装的TagHelper标签,以<util-打头的标签都是Util TagHelper,它们以粗体显示,这是由于安装了Resharper的原因。

  TagHelper在运行时会把html输出到页面,它们把弱类型的html封装成了具有强类型提示的标签。

  如何知道某个TagHelper到底输出了什么html呢?

  一种办法是打开它生成的.html文件来查找,不过当页面很复杂时,这种办法有点吃力。

  另一种办法是查看日志,Util TagHelper的每个组件都提供了write-log属性,当设置为true,就会在C盘log目录生成日志。

 

  main.cshtml视图中最关键的部分就是<router-outlet></router-outlet>标签。

  router-outlet是Angular路由的占位符,当根模块AppModule中配置的路由激活时,相关的Angular组件就会被放进这个占位符中。

  根模块中的路由配置被拆分到一个单独的模块AppRoutingModule中,路由配置如下。

 

  通过路由配置可以发现,当打开首页时,命中路由第二项path:’’,会跳转到/systems/application路径,systems是一个子模块,我们来查看它的路由配置。

 

 

  /systems/application将激活ApplicationIndexComponent组件,并把它加载到根组件的<router-outlet></router-outlet>中。

  ApplicationIndexComponent组件请求服务端地址/view/systems/application获取视图。

 

   /view打头的地址将匹配到Areas区域控制器,这是在MVC路由配置中设置的。

 

  控制器ApplicationControllerIndex方法将返回视图。

 

  Angular JIT编译会在系统启动时请求服务端URL,在Chrome浏览器F12调出开发者工具,刷新页面,会观察到页面请求了Areas中的控制器。

  

  所以你在开发阶段运行项目会感觉比较慢,在正式发布后就没这些开销了。

小结  

  本文简要介绍了Util Demo的目录结构和运行机制,如果你没有Angular基础,估计还是很难看懂,建议你阅读Angular中文网https://angular.cn。  

  未完待续,下一篇将对Util Demo的Angular封装进行介绍,本来是准备这篇介绍的,不过限于篇幅,放到下篇,我知道,太长的文章既难写更难读。

  写文需要动力,请大家多多支持,点下推荐,Github点下星星。

  Util应用框架交流一群: 24791014

.Net Core应用框架Util介绍(五)

.Net Core应用框架Util介绍(五)

  上篇简要介绍了Util在Angular Ts方面的封装情况,本文介绍Angular封装的另一个部分,即Html的封装。 

标准组件与业务组件 

  对于管理后台这样的表单系统,你通常会使用Angular Material或Ng-Zorro这样的UI组件库,它们提供了标准化的UI组件 

  标准组件将Ts封装起来,以特定标签和属性的方式提供使用。 

  业务组件使用标准组件拼凑页面,并从服务端API获取数据绑定到页面上。 

  可以看出,标准组件是业务开发的基础,我们必须将标准组件的开发效率提升到极致。 

使用标准组件的问题 

  直接使用原生标准组件有什么问题呢? 

复杂的Html结构 

  现代流行的UI组件库,为了构造美观大气的视觉效果及增强组件的功能特性,一个组件需要组装多个Html元素来表达。 

  在带来美观视觉体验的同时,也导致了Html结构变得很复杂。

  Angular Material是Google以Material设计风格开发的UI组件库。 

  我们来看一个Angular Material文本框的例子。 

<mat-form-field>
    <input matInput placeholder="Favorite food" value="Sushi">
</mat-form-field>

  你看到了Angular Material文本框并不是一个input标签,input标签嵌套在mat-form-field标签内。

  这看上去并不算复杂,不过它只是最简单的情况,让我们增加两个特性。 

<mat-form-field>
    <input matInput placeholder="测试一下" [(ngModel)]="value" >
    <mat-hint>哈哈</mat-hint>
    <button mat-button *ngIf="value" matSuffix mat-icon-button (click)="value=''''">
        <mat-icon>close</mat-icon>
    </button>
</mat-form-field>

  我们在文本框的下方添加了提示文本,并在文本框右侧加了个按钮,你可以点击这个按钮清空文本框的内容。

 

  你应该观察到Html结构变得稍微复杂了,让我们再添加两个特性。

<mat-form-field>
    <input matInput  #testControl="ngModel" name="test" placeholder="金额" [(ngModel)]="value" required max="10">
    <span matPrefix>$ &nbsp;</span>
    <span matSuffix></span>
    <mat-hint>充值金额</mat-hint>
    <button mat-button *ngIf="value" matSuffix mat-icon-button (click)="value=''''">
        <mat-icon>close</mat-icon>
    </button>
    <mat-error *ngIf="testControl.hasError(''max'') && !testControl.hasError(''required'')">
        最大金额不能超过10元
    </mat-error>
    <mat-error *ngIf="testControl.hasError(''required'')">
        这是一个必填项
    </mat-error>
</mat-form-field>

  现在在文本框的左侧加了一个美元符号,在文本框右侧添加了后缀“元”,另外添加了必填和最大值验证。

  这还只是一个不太复杂的文本框,Html居然这么长。  

  组件标签结构成为前端业务开发的第一个关注点

繁琐的数据绑定

  如果要绑定一些可选项到下拉列表,一种办法是硬编码。

<mat-form-field>
    <mat-select placeholder="请选一个吧">
        <mat-option value="1">A</mat-option>
        <mat-option value="2">B</mat-option>
        <mat-option value="3">C</mat-option>
    </mat-select>
</mat-form-field>

  这是具有三个选项的下拉列表。

 

  如果我们要绑定56个民族,就需要硬编码56个选项,这确实可行,不过一个下拉框就60几行,占地太广,复制粘贴也不方便。

  另外,下拉选项可能是动态的,这些可选值存储在数据库中。

  数据绑定大多从服务端获取数据,绑定到组件上。

  Angular提倡将数据访问与组件分离,这个设计理念被Angular Material这些标准组件库所遵循。

  为了绑定数据,你首先需要发送一个Http请求,从服务端获取Json数据,转换为Ts对象,然后通过Angular提供的循环语法绑定上去。

<mat-form-field>
    <mat-select placeholder="Favorite food">
        <mat-option *ngFor="let food of foods" [value]="food.value">
            {{food.viewValue}}
        </mat-option>
    </mat-select>
</mat-form-field>

  Angular Material下拉列表能够分组,它与普通下拉列表的Html结构不同,如果服务端返回的数据格式不太友好,绑定起来将更加困难。

<mat-form-field>
    <mat-select placeholder="Pokemon" [formControl]="pokemonControl">
        <mat-option>-- None --</mat-option>
        <mat-optgroup *ngFor="let group of pokemonGroups" [label]="group.name"
                        [disabled]="group.disabled">
            <mat-option *ngFor="let pokemon of group.pokemon" [value]="pokemon.value">
                {{pokemon.viewValue}}
            </mat-option>
        </mat-optgroup>
    </mat-select>
</mat-form-field>

 

  下拉列表并不是唯一需要数据绑定的组件,还有一些组件也需要,且它们更加复杂,比如树型控件,表格控件,树型表格控件等。

  数据绑定成为前端业务开发的第二个关注点 

低效的验证

  验证是业务健壮性的基本保障,Angular Material表单组件提供了基本的验证方法。 

<mat-form-field>
    <input matInput name="test" [(ngModel)]="value" required>
</mat-form-field>

  上面演示了设置必填项的方法,它相当简单,只要把required加到input标签上就好了。

  遗憾的是,文本框虽然得到了验证,但却没有显示出任何错误提示消息。

  通过添加一个mat-error标签,可以显示指定错误提示。

<mat-form-field>
    <input matInput  #control="ngModel" name="test" [(ngModel)]="value" required>
    <mat-error *ngIf="control.hasError(''required'')">
        这是一个必填项
    </mat-error>
</mat-form-field>

  如果组件上有两个验证条件,你需要添加两个mat-error标签。

<mat-form-field>
    <input matInput  #control="ngModel" name="test" [(ngModel)]="value" required max="10">
    <mat-error *ngIf="control.hasError(''max'') && !control.hasError(''required'')">
        最大值不能超过10
    </mat-error>
    <mat-error *ngIf="control.hasError(''required'')">
        这是一个必填项
    </mat-error>
</mat-form-field>

  注意,为了让提示消息只在特定验证条件失败时才显示,你需要在mat-error标签上进行验证状态判断。

  如果现在组件包含5个验证条件,mat-error和它上面的判断条件将变得相当复杂。

  另一方面,客户端脚本验证只是为了提升用户体验,用户可以绕过界面直接请求你的服务端,所以真正的验证必须在服务端完成。

  这样一来,验证需要在客户端和服务端编写两次,这造成了双倍的工作量。

  当需求发生变动,服务端和客户端的验证很难同步更新,维护变得更加困难。

  验证成为前端业务开发的第三个关注点 

解决方案 

  如果开发的时候,既不用关心Html的结构,又不用关注数据怎么绑定,验证还能自动完成,甚至连标签和它上面的属性也不用记忆,这就最理想不过了,该如何实现呢? 

用Angular组件包装标准组件 

  首先我们需要用Angular组件对标准组件进行包装,以方便功能扩展,这个自定义组件称为包装器 

  • 封装Html复杂结构

  我们把标准组件的Html标签包装起来,以属性的形式提供访问。

<mat-form-field>
    <input matInput placeholder="测试一下" [(ngModel)]="value" >
    <mat-hint>哈哈</mat-hint>
    <button mat-button *ngIf="value" matSuffix mat-icon-button (click)="value=''''">
        <mat-icon>close</mat-icon>
    </button>
</mat-form-field>

  把上面标签包装后变成这样。 

<mat-textbox-wrapperplaceholder="测试一下" [(ngModel)]="value" hint="哈哈" showClearButton="true"></mat-textbox-wrapper>

  mat-hint标签现在被转换为hint属性,通过showClearButton属性来控制是否显示清空按钮,大幅提升了组件的易用性。 

  • 约定前后端数据格式

  不论下拉列表,还是表格,甚至树型控件这些需要数据绑定的组件,都有一定规律可循。

  当你一遍又一遍的复制粘贴,仔细观察这个机械乏味的绑定过程,不难抽取出公共元素,形成前后端数据绑定的通用数据格式。

  一旦抽取出前后端通用数据格式,你只需将业务数据转换为通用格式,发送到客户端就自动绑定完成。

  • 将数据操作内置到包装器

  如果你曾经使用过EasyUi这样的组件库,定会发现它的数据绑定功能十分强大,这是因为它把数据操作内置到了标准组件中。

  基于Angular低耦合设计原则,Angular Material标准组件并不会直接请求服务端,任何数据绑定工作都需要你手工完成,不过我们可以将数据操作内置到包装器。 

  一旦封装完成,数据绑定变得非常简单,比如设置一个url属性即可,服务端返回约定的数据格式。

<mat-select-wrapper url="/api/test"></mat-select-wrapper>

用TagHelper封装Angular组件 

  Angular包装器组件,大幅简化了标准组件的使用,但它提供的依然是Html,而自定义Html标签和属性没有什么提示,这意味着你如果记不住这些API,就需要随时欣赏API文档。

  TagHelper终于闪亮登场。

  • 强类型代码提示和编译时检查 

  一旦把Html标签封装成TagHelper,就可以跟API文档拜拜了,把代码提示点出来,慢慢选,只要你知道该组件确实有这功能,哪怕印象有点模糊也没关系。

  Html标签和属性的拼写错误也将与你无缘,VS大哥会为你把关,代码健壮性将大幅提升。

 

  • Lambda表达式元数据解析

  很多人已经认识到HtmlHelper或TagHelper的好处是强类型提示,不过这个认识还很肤浅。

  TagHelper真正的威力来自Lambda表达式元数据解析,它提供了一个统一的抽象方式,自动设置表单组件的常规属性、验证,甚至数据绑定。

  对于Angular Material表单组件,通常需要设置以下常规属性:

    •   控件名称 name
    •   占位文本 placeholder
    •   双向绑定 ngModel

  常规验证:

    •   必填项验证 required
    •   Email验证 email
    •   最小长度验证 minlength
    •   最大长度验证 maxlength
    •   最小值验证 min
    •   最大值验证 max
    •   正则验证 pattern

  几乎所有表单组件都需要设置这三个常规属性,而文本框更需要进行多种验证,虽然这些操作并不复杂,但由于一个表单界面包含很多组件,每个组件都要挨个设置,既浪费时间又枯燥乏味。 

  如果能够自动化设置这些常规属性和验证属性,虽然从单个组件看并不起眼,但从整个项目的角度,能大幅提升生产力。 

  Lambda表达式元数据解析,通过读取C#属性的类型信息以及相关的特性,能够自动化设置三大常规属性,以及对文本框实施多种验证,还解决了客户端与服务端验证无法同步的难题。 

  一旦用上Lambda表达式,界面标签将变得干净整洁,你的关注点将迅速转移到业务上。

 

  上面的TagHelper标签生成的结果Html如下。 

<mat-textbox-wrapper name="code" placeholder="应用程序编码" requiredMessage="应用程序编码不能为空" [(model)]="model&&model.code" [maxLength]="60" [required]="true"></mat-textbox-wrapper>

  for指向了ApplicationDto对象的Code属性,下面是Code属性的定义。

 

  从Code属性定义可以解析出该组件需要设置的常规属性和验证属性。

 

  上面演示了文本框组件,对于单选框,多选框,下拉框等表单组件,都可以使用相同的方式,一个for属性,基础工作已经完成。

封装的弊端

  看了前面的解决方案,你知道经过几层高强度封装后,组件将变得简单易用,不过在将这些方法应用到你的项目之前,你需要对这些方法有更深的了解。

  任何事物都有其两面性,所谓此消彼长,在组件变得更加简单易用的同时,它的灵活性也在降低

  包装器组件将Html结构封装起来,这会导致组件不再支持模板化,如果某个功能在你的包装器中未实现,那么不能通过在包装器标签内嵌套HTML的方式组合出新的功能。

  封装包装器组件有相当多的讲究,特别是Angular Material这样的组件库在功能上几乎无法与EasyUi或Ext等企业级UI库相提并论,你必须在易用性和灵活性间进行平衡,对于像表格这样的重量级组件,很难封装到完全满足业务需求,这种情况下,你必须为其保留模板化能力。

  另一方面,封装后的傻瓜式TagHelper,很容易把程序员惯坏,开发常用功能风升水起,一碰到超出框架范围的需求就变得束手无策,因为他们从来没有学习过原生的知识。

  你团队的主力开发人员必须对原生技术有系统了解。

  一旦功能超出框架范围,你必须有能力扩展框架,在必要的时候,直接使用原生Html进行开发,这时候你更能体会到TagHelper与Html混合编程的好处,既提升了常规功能的开发效率,又满足了复杂功能对操作体验的需求。

Util组件介绍

  下面简要介绍Util中封装的几个常用组件,它们来自Angular Material或PrimeNg组件库。 

文本框

  前面已经展示过文本框的用法,除了常规属性设置和验证以外,for指向属性的数据类型会影响生成的文本框类型,比如属性为日期类型,文本框会变成一个日期选择控件。  

/// <summary>
        /// 创建时间
        /// </summary>
        [Display( Name = "创建时间" )]
        public DateTime? CreationTime { get; set; }
<util-textbox for="CreationTime"></util-textbox>

  生成的Html结果如下。 

<mat-datepicker-wrapper name="creationTime" placeholder="创建时间" [(model)]="model&&model.creationTime"></mat-datepicker-wrapper>

  下面再演示一下数值类型,添加了最大和最小值验证,并设置前缀文本和后缀图标。 当属性为数值类型时,文本框只能输入数字。

/// <summary>
        /// 金额
        /// </summary>
        [Required( ErrorMessage = "必须填写金额" )]
        [Range(10,50,ErrorMessage = "有效金额在10到50之间")]
        [Display( Name = "金额" )]
        public decimal Money { get; set; }

  生成的Html结果如下。 

<mat-textbox-wrapper type="number" name="money" placeholder="金额" [(model)]="model&&model.money"   
        startHint="a" endHint="b" prefixText="$"   suffixFontAwesomeIcon="fa-apple"
        [required]="true" requiredMessage="必须填写金额"
        [min]="10" [max]="50"  minMessage="有效金额在10到50之间" maxMessage="有效金额在10到50之间">
    </mat-textbox-wrapper>

  来看看执行效果。

 

下拉列表

  下拉列表的封装,重点在于数据绑定

  • 绑定枚举 

  下面演示如何把民族枚举绑定到下拉列表。 

  C#代码如下。 

  1    /// <summary>
  2     /// 民族
  3     /// </summary>
  4     public enum Nation {
  5         /// <summary>
  6         /// 汉族        
  7         /// </summary>
  8         [Description( "汉族" )]
  9         Hz = 0,
 10         /// <summary>
 11         /// 蒙古族        
 12         /// </summary>
 13         [Description( "蒙古族" )]
 14         Mgz = 1,
 15         /// <summary>
 16         /// 回族        
 17         /// </summary>
 18         [Description( "回族" )]
 19         HuiZ = 2,
 20         /// <summary>
 21         /// 藏族        
 22         /// </summary>
 23         [Description( "藏族" )]
 24         Zz = 3,
 25         /// <summary>
 26         /// 维吾尔族        
 27         /// </summary>
 28         [Description( "维吾尔族" )]
 29         Wwez = 4,
 30         /// <summary>
 31         /// 苗族        
 32         /// </summary>
 33         [Description( "苗族" )]
 34         Mz = 5,
 35         /// <summary>
 36         /// 彝族        
 37         /// </summary>
 38         [Description( "彝族" )]
 39         Yz = 6,
 40         /// <summary>
 41         /// 壮族        
 42         /// </summary>
 43         [Description( "壮族" )]
 44         ZhuangZ = 7,
 45         /// <summary>
 46         /// 布依族        
 47         /// </summary>
 48         [Description( "布依族" )]
 49         Byz = 8,
 50         /// <summary>
 51         /// 朝鲜族        
 52         /// </summary>
 53         [Description( "朝鲜族" )]
 54         Cxz = 9,
 55         /// <summary>
 56         /// 满族        
 57         /// </summary>
 58         [Description( "满族" )]
 59         ManZ = 10,
 60         /// <summary>
 61         /// 侗族        
 62         /// </summary>
 63         [Description( "侗族" )]
 64         Tz = 11,
 65         /// <summary>
 66         /// 瑶族        
 67         /// </summary>
 68         [Description( "瑶族" )]
 69         YaoZ = 12,
 70         /// <summary>
 71         /// 白族        
 72         /// </summary>
 73         [Description( "白族" )]
 74         Bz = 13,//baizu
 75         /// <summary>
 76         /// 土家族        
 77         /// </summary>
 78         [Description( "土家族" )]
 79         Tjz = 14,
 80         /// <summary>
 81         /// 哈尼族        
 82         /// </summary>
 83         [Description( "哈尼族" )]
 84         Hnz = 15,
 85         /// <summary>
 86         /// 哈萨克族        
 87         /// </summary>
 88         [Description( "哈萨克族" )]
 89         Hskz = 16,
 90         /// <summary>
 91         /// 傣族        
 92         /// </summary>
 93         [Description( "傣族" )]
 94         Dz = 17,
 95         /// <summary>
 96         /// 黎族        
 97         /// </summary>
 98         [Description( "黎族" )]
 99         Lz = 18,
100         /// <summary>
101         /// 傈僳族        
102         /// </summary>
103         [Description( "傈僳族" )]
104         Lsz = 19,
105         /// <summary>
106         /// 佤族        
107         /// </summary>
108         [Description( "佤族" )]
109         Wz = 20,
110         /// <summary>
111         /// 畲族        
112         /// </summary>
113         [Description( "畲族" )]
114         Sz = 21,
115         /// <summary>
116         /// 高山族        
117         /// </summary>
118         [Description( "高山族" )]
119         Gsz = 22,
120         /// <summary>
121         /// 拉祜族        
122         /// </summary>
123         [Description( "拉祜族" )]
124         Lhz = 23,
125         /// <summary>
126         /// 水族        
127         /// </summary>
128         [Description( "水族" )]
129         ShuiZ = 24,
130         /// <summary>
131         /// 东乡族        
132         /// </summary>
133         [Description( "东乡族" )]
134         Dxz = 25,
135         /// <summary>
136         /// 纳西族        
137         /// </summary>
138         [Description( "纳西族" )]
139         Nxz = 26,
140         /// <summary>
141         /// 景颇族        
142         /// </summary>
143         [Description( "景颇族" )]
144         Jpz = 27,
145         /// <summary>
146         /// 柯尔克孜族        
147         /// </summary>
148         [Description( "柯尔克孜族" )]
149         Kekzz = 28,
150         /// <summary>
151         /// 土族        
152         /// </summary>
153         [Description( "土族" )]
154         TuZ = 29,
155         /// <summary>
156         /// 达斡尔族        
157         /// </summary>
158         [Description( "达斡尔族" )]
159         Dwez = 30,
160         /// <summary>
161         /// 仫佬族        
162         /// </summary>
163         [Description( "仫佬族" )]
164         Mlz = 31,
165         /// <summary>
166         /// 羌族        
167         /// </summary>
168         [Description( "羌族" )]
169         Qz = 32,
170         /// <summary>
171         /// 布朗族        
172         /// </summary>
173         [Description( "布朗族" )]
174         Blz = 33,
175         /// <summary>
176         /// 撒拉族        
177         /// </summary>
178         [Description( "撒拉族" )]
179         Slz = 34,
180         /// <summary>
181         /// 毛南族        
182         /// </summary>
183         [Description( "毛南族" )]
184         Mnz = 35,
185         /// <summary>
186         /// 仡佬族        
187         /// </summary>
188         [Description( "仡佬族" )]
189         Ylz = 36,
190         /// <summary>
191         /// 锡伯族        
192         /// </summary>
193         [Description( "锡伯族" )]
194         Xbz = 37,
195         /// <summary>
196         /// 阿昌族        
197         /// </summary>
198         [Description( "阿昌族" )]
199         Acz = 38,
200         /// <summary>
201         /// 普米族        
202         /// </summary>
203         [Description( "普米族" )]
204         Pmz = 39,
205         /// <summary>
206         /// 塔吉克族        
207         /// </summary>
208         [Description( "塔吉克族" )]
209         Tjkz = 40,
210         /// <summary>
211         /// 怒族        
212         /// </summary>
213         [Description( "怒族" )]
214         Nz = 41,
215         /// <summary>
216         /// 乌孜别克族        
217         /// </summary>
218         [Description( "乌孜别克族" )]
219         Wzbkz = 42,
220         /// <summary>
221         /// 俄罗斯族        
222         /// </summary>
223         [Description( "俄罗斯族" )]
224         Elsz = 43,
225         /// <summary>
226         /// 鄂温克族        
227         /// </summary>
228         [Description( "鄂温克族" )]
229         Ewkz = 44,
230         /// <summary>
231         /// 德昂族        
232         /// </summary>
233         [Description( "德昂族" )]
234         Daz = 45,
235         /// <summary>
236         /// 保安族        
237         /// </summary>
238         [Description( "保安族" )]
239         Baz = 46,
240         /// <summary>
241         /// 裕固族        
242         /// </summary>
243         [Description( "裕固族" )]
244         Ygz = 47,
245         /// <summary>
246         /// 京族        
247         /// </summary>
248         [Description( "京族" )]
249         Jz = 48,
250         /// <summary>
251         /// 塔塔尔族        
252         /// </summary>
253         [Description( "塔塔尔族" )]
254         Ttrz = 49,
255         /// <summary>
256         /// 独龙族        
257         /// </summary>
258         [Description( "独龙族" )]
259         Dlz = 50,
260         /// <summary>
261         /// 鄂伦春族        
262         /// </summary>
263         [Description( "鄂伦春族" )]
264         Elcz = 51,
265         /// <summary>
266         /// 赫哲族        
267         /// </summary>
268         [Description( "赫哲族" )]
269         Hzz = 52,
270         /// <summary>
271         /// 门巴族        
272         /// </summary>
273         [Description( "门巴族" )]
274         Mbz = 53,
275         /// <summary>
276         /// 珞巴族        
277         /// </summary>
278         [Description( "珞巴族" )]
279         Lbz = 54,
280         /// <summary>
281         /// 基诺族        
282         /// </summary>
283         [Description( "基诺族" )]
284         Jnz = 55
285     }
民族枚举 
/// <summary>
        /// 民族
        /// </summary>
        [Required( ErrorMessage = "必须选择一个民族" )]
        [Display( Name = "民族" )]
        [DataMember]
        public Nation Nation { get; set; }

  TagHelper代码如下。

<util-select for="Nation"></util-select>

  生成的Html如下,可以看出,民族可选项被硬编码到Html标签中。

<mat-select-wrapper name="nation" placeholder="民族" requiredMessage="必须选择一个民族" [(model)]="model&&model.nation"
        [dataSource]="[{''text'':''汉族'',''value'':0,''sortId'':0},{''text'':''蒙古族'',''value'':1,''sortId'':1},{''text'':''回族'',''value'':2,''sortId'':2},{''text'':''藏族'',''value'':3,''sortId'':3},{''text'':''维吾尔族'',''value'':4,''sortId'':4},{''text'':''苗族'',''value'':5,''sortId'':5},{''text'':''彝族'',''value'':6,''sortId'':6},{''text'':''壮族'',''value'':7,''sortId'':7},{''text'':''布依族'',''value'':8,''sortId'':8},{''text'':''朝鲜族'',''value'':9,''sortId'':9},{''text'':''满族'',''value'':10,''sortId'':10},{''text'':''侗族'',''value'':11,''sortId'':11},{''text'':''瑶族'',''value'':12,''sortId'':12},{''text'':''白族'',''value'':13,''sortId'':13},{''text'':''土家族'',''value'':14,''sortId'':14},{''text'':''哈尼族'',''value'':15,''sortId'':15},{''text'':''哈萨克族'',''value'':16,''sortId'':16},{''text'':''傣族'',''value'':17,''sortId'':17},{''text'':''黎族'',''value'':18,''sortId'':18},{''text'':''傈僳族'',''value'':19,''sortId'':19},{''text'':''佤族'',''value'':20,''sortId'':20},{''text'':''畲族'',''value'':21,''sortId'':21},{''text'':''高山族'',''value'':22,''sortId'':22},{''text'':''拉祜族'',''value'':23,''sortId'':23},{''text'':''水族'',''value'':24,''sortId'':24},{''text'':''东乡族'',''value'':25,''sortId'':25},{''text'':''纳西族'',''value'':26,''sortId'':26},{''text'':''景颇族'',''value'':27,''sortId'':27},{''text'':''柯尔克孜族'',''value'':28,''sortId'':28},{''text'':''土族'',''value'':29,''sortId'':29},{''text'':''达斡尔族'',''value'':30,''sortId'':30},{''text'':''仫佬族'',''value'':31,''sortId'':31},{''text'':''羌族'',''value'':32,''sortId'':32},{''text'':''布朗族'',''value'':33,''sortId'':33},{''text'':''撒拉族'',''value'':34,''sortId'':34},{''text'':''毛南族'',''value'':35,''sortId'':35},{''text'':''仡佬族'',''value'':36,''sortId'':36},{''text'':''锡伯族'',''value'':37,''sortId'':37},{''text'':''阿昌族'',''value'':38,''sortId'':38},{''text'':''普米族'',''value'':39,''sortId'':39},{''text'':''塔吉克族'',''value'':40,''sortId'':40},{''text'':''怒族'',''value'':41,''sortId'':41},{''text'':''乌孜别克族'',''value'':42,''sortId'':42},{''text'':''俄罗斯族'',''value'':43,''sortId'':43},{''text'':''鄂温克族'',''value'':44,''sortId'':44},{''text'':''德昂族'',''value'':45,''sortId'':45},{''text'':''保安族'',''value'':46,''sortId'':46},{''text'':''裕固族'',''value'':47,''sortId'':47},{''text'':''京族'',''value'':48,''sortId'':48},{''text'':''塔塔尔族'',''value'':49,''sortId'':49},{''text'':''独龙族'',''value'':50,''sortId'':50},{''text'':''鄂伦春族'',''value'':51,''sortId'':51},{''text'':''赫哲族'',''value'':52,''sortId'':52},{''text'':''门巴族'',''value'':53,''sortId'':53},{''text'':''珞巴族'',''value'':54,''sortId'':54},{''text'':''基诺族'',''value'':55,''sortId'':55}]"
        [required]="true">
    </mat-select-wrapper>

  执行效果如下。


  • 绑定服务端数据

  为了绑定服务端数据,必须约定通用数据格式,对于下拉列表,服务端C#是由Util.Item来完成的。

 1 using System;
 2 using Newtonsoft.Json;
 3 
 4 namespace Util {
 5     /// <summary>
 6     /// 列表项
 7     /// </summary>
 8     public class Item : IComparable<Item> {
 9         /// <summary>
10         /// 初始化
11         /// </summary>
12         /// <param name="text">文本</param>
13         /// <param name="value"></param>
14         /// <param name="sortId">排序号</param>
15         /// <param name="group"></param>
16         /// <param name="disabled">禁用</param>
17         public Item( string text, object value, int? sortId = null, string group = null, bool? disabled = null ) {
18             Text = text;
19             Value = value;
20             SortId = sortId;
21             Group = group;
22             Disabled = disabled;
23         }
24 
25         /// <summary>
26         /// 文本
27         /// </summary>
28         [JsonProperty( "text", NullValueHandling = NullValueHandling.Ignore )]
29         public string Text { get; }
30 
31         /// <summary>
32         ///33         /// </summary>
34         [JsonProperty( "value", NullValueHandling = NullValueHandling.Ignore )]
35         public object Value { get; }
36 
37         /// <summary>
38         /// 排序号
39         /// </summary>
40         [JsonProperty( "sortId", NullValueHandling = NullValueHandling.Ignore )]
41         public int? SortId { get; }
42 
43         /// <summary>
44         ///45         /// </summary>
46         [JsonProperty( "group", NullValueHandling = NullValueHandling.Ignore )]
47         public string Group { get; }
48 
49         /// <summary>
50         /// 禁用
51         /// </summary>
52         [JsonProperty( "disabled", NullValueHandling = NullValueHandling.Ignore )]
53         public bool? Disabled { get; }
54 
55         /// <summary>
56         /// 比较
57         /// </summary>
58         /// <param name="other">其它列表项</param>
59         public int CompareTo( Item other ) {
60             return string.Compare( Text, other.Text, StringComparison.CurrentCulture );
61         }
62     }
63 }
Util.Item

  客户端Typescript定义了对应的结构。

  1 //============== 列表=============================
  2 //Copyright 2018 何镇汐
  3 //Licensed under the MIT license
  4 //================================================
  5 import { ISort, sort } from ''../core/sort'';
  6 import { util } from ''../index'';
  7 
  8 /**
  9  * 列表
 10  */
 11 export class Select {
 12     /**
 13      * 初始化列表
 14      * @param items 列表项集合
 15      */
 16     constructor(private items: SelectItem[]) {
 17     }
 18 
 19     /**
 20      * 转换为下拉列表项集合
 21      */
 22     toOptions(): SelectOption[] {
 23         return this.getSortedItems().map(value => new SelectOption(value));
 24     }
 25 
 26     /**
 27      * 获取已排序的列表项集合
 28      */
 29     private getSortedItems() {
 30         return sort(this.items);
 31     }
 32 
 33     /**
 34      * 转换为下拉列表组集合
 35      */
 36     toGroups(): SelectOptionGroup[] {
 37         let result: SelectOptionGroup[] = new Array<SelectOptionGroup>();
 38         let groups = util.helper.groupBy(this.getSortedItems(), t => t.group);
 39         groups.forEach((items, key) => {
 40             result.push(new SelectOptionGroup(key, items.map(item => new SelectOption(item)), false));
 41         });
 42         return result;
 43     }
 44 
 45     /**
 46      * 是否列表组
 47      */
 48     isGroup(): boolean {
 49         return this.items.every(value => !!value.group);
 50     }
 51 }
 52 
 53 /**
 54  * 列表项
 55  */
 56 export class SelectItem implements ISort {
 57     /**
 58      * 文本
 59      */
 60     text: string;
 61     /**
 62      * 值
 63      */
 64     value;
 65     /**
 66      * 禁用
 67      */
 68     disabled?: boolean;
 69     /**
 70      * 排序号
 71      */
 72     sortId?: number;
 73     /**
 74      * 组
 75      */
 76     group?: string;
 77 }
 78 
 79 /**
 80  * 下拉列表项
 81  */
 82 export class SelectOption {
 83     /**
 84      * 文本
 85      */
 86     text: string;
 87     /**
 88      * 值
 89      */
 90     value;
 91     /**
 92      * 禁用
 93      */
 94     disabled?: boolean;
 95 
 96     /**
 97      * 初始化下拉列表项
 98      * @param item 列表项
 99      */
100     constructor(item: SelectItem) {
101         this.text = item.text;
102         this.value = item.value;
103         this.disabled = item.disabled;
104     }
105 }
106 
107 /**
108  * 下拉列表组
109  */
110 export class SelectOptionGroup {
111     /**
112      * 初始化下拉列表组
113      * @param text 文本
114      * @param value 值
115      * @param disabled 禁用
116      */
117     constructor(public text: string, public value: SelectOption[], public disabled?: boolean) {
118     }
119 }
SelectItem

  下面来演示一下用法。 

  先把Nation属性的类型改成int。

/// <summary>
        /// 民族
        /// </summary>
        [Required( ErrorMessage = "必须选择一个民族" )]
        [Display( Name = "民族" )]
        [DataMember]
        public int Nation { get; set; }

  在WebApi控制器中,添加一个方法,用来获取民族枚举可选项。

  通过Util.Helpers.Enum.GetItems方法可以提取出枚举项列表,返回值为List<Item>,这正是我们约定的标准格式,如果返回的是业务类型列表,应转换为List<Item>。

  Success方法用来将List<Item>转换为前后端约定的标准结果类型Result。 

/// <summary>
        /// 获取民族可选项列表
        /// </summary>
        [HttpGet( "nationItems" )]
        public IActionResult GetNationItems() {
            List<Item> items = Util.Helpers.Enum.GetItems<Util.Biz.Enums.Nation>();
            return Success( items );
        }

  再来看TagHelper标签,for属性承包了常规的机械工作,你将注意力集中在业务上,通过手工设置url属性来加载远程数据。 

<util-select for="Nation" url="/api/test/nationItems"></util-select>

  效果跟直接绑定枚举一样,不过生成的Html简单很多。 

<mat-select-wrapper name="nation" placeholder="民族" requiredMessage="必须选择一个民族" url="/api/application/nationItems" [(model)]="model&&model.nation" [required]="true"></mat-select-wrapper>

  上面演示的下拉列表并未分组,我们来改造一下,让它以分组显示。 

/// <summary>
        /// 获取民族可选项列表
        /// </summary>
        [HttpGet( "nationItems" )]
        public IActionResult GetNationItems() {
            var result = Util.Helpers.Enum.GetItems<Util.Biz.Enums.Nation>()
                .GroupBy( t => Util.Helpers.String.PinYin( t.Text.Substring( 0,1 ) ) )
                .SelectMany( t => t.ToList().Select( item => new Item( item.Text, item.Value, item.SortId, t.Key ) ) );
            return Success( result );
        }

  Util包含大量有用的Helper,Util.Helpers.String.PinYin方法能够将汉字转换为拼音首字母缩写,使用GroupBy方法将民族拼音首字母进行分组,并转换为Item标准格式。

  执行效果如下。

 

  TagHelper没有任何变化,Angular Material下拉列表是否分组,其原生Html格式完全不同,但封装以后,你根本感觉不到它们的区别,你不需要编写任何一行Ts代码,就完成了分组下拉列表的绑定,你应该已经体会到封装的强大之处。 

单选按钮

  单选按钮和下拉列表类似,下面演示一下枚举绑定。

  C#代码如下。

/// <summary>
    /// 性别
    /// </summary>
    public enum Gender {
        /// <summary>
        ////// </summary>        
        [Description( "女士" )]
        Female = 1,
        /// <summary>
        ////// </summary>
        [Description( "先生" )]
        Male = 2
    }
/// <summary>
        /// 性别
        /// </summary>
        [Display( Name = "性别" )]
        public Gender Gender { get; set; }

  TagHelper标签如下。

<util-radio for="Gender"></util-radio>

  生成的Html标签如下。

<mat-radio-wrapper label="性别" name="gender" [(model)]="model&&model.gender" [dataSource]="[{''text'':''女士'',''value'':1,''sortId'':1},{''text'':''先生'',''value'':2,''sortId'':2}]">
    </mat-radio-wrapper>

  执行效果如下。

 

复选框

  复选框用于操作布尔类型。

  C#代码如下。

/// <summary>
        /// 启用
        /// </summary>
        [Display( Name = "启用" )]
        [DataMember]
        public bool? Enabled { get; set; }

  TagHelper标签如下。

<util-checkbox for="Enabled"></util-checkbox>

  生成的Html标签如下。

<mat-checkbox name="gender" [(ngModel)]="model&&model.gender">性别</mat-checkbox>

  执行效果如下。

 

 

滑动开关

  滑动开关与复选框功能相同,但长像更具现代化气质。

  TagHelper标签如下。

<util-slide-toggle for="Enabled"></util-slide-toggle>

  生成的Html标签如下。

<mat-slide-toggle name="enabled" [(ngModel)]="model&&model.enabled">启用</mat-slide-toggle>

  执行效果如下。

 

 

表格 

  Angular Material表格提供了一套模板化机制,你需要任何功能,往表格标签中添加元素就好了。

  像序号,多选,分页等常规功能都没有内置到Angular Material表格中,Angular Material官网以Demo的形式提供了参考样例,如果你直接使用它来进行业务开发,将导致十分低效的开发效率。

  Util将自动生成序号,多选,分页,排序等常见功能以及数据绑定能力封装到表格包装器组件中,同时,Util保留了Angular Material表格的模板化能力,你依然可以通过往表格标签中添加元素的方式扩展功能。

  由于大多表格都需要分页,约定的后台数据格式由PagerList承载,Ts也定义了类似的分页列表对象。

  下面演示一个简单的表格示例。

  服务端已经封装了通用的查询方法,留待下篇介绍。

  先看看TagHelper代码。 

 1 <util-table id="tableApplication" query-param="queryParam" base-url="application"
 2             sort="CreationTime" sort-direction="Desc" max-height="500">
 3     <util-table-column type="Checkbox"></util-table-column>
 4     <util-table-column type="LineNumber"></util-table-column>
 5     <util-table-column for="Code" sort="true"></util-table-column>
 6     <util-table-column for="Name" sort="true"></util-table-column>
 7     <util-table-column for="Enabled" sort="true"></util-table-column>
 8     <util-table-column for="RegisterEnabled" sort="true"></util-table-column>
 9     <util-table-column for="CreationTime" sort="true"></util-table-column>
10     <util-table-column title="操作" column="operation">
11         <util-table-cell>
12             <util-a styles="Icon" tooltip="编辑" bind-link="[''update'',row.id]">
13                 <util-icon material-icon="Edit"></util-icon>
14             </util-a>
15             <util-button styles="Icon" menu-id="menu">
16                 <util-icon material-icon="More_Vert"></util-icon>
17                 <util-menu id="menu">
18                     <util-menu-item label="删除" material-icon="Delete" on-click="delete(row.id)"></util-menu-item>
19                     <util-menu-item label="查看详细" material-icon="Visibility" bind-link="[''detail'',row.id]"></util-menu-item>
20                 </util-menu>
21             </util-button>
22         </util-table-cell>
23     </util-table-column>
24 </util-table>

  生成的Html如下。 

 1 <mat-table-wrapper #tableApplication="" baseUrl="application" key="application" maxHeight="500" [(queryParam)]="queryParam"><mat-table matSort="" matSortActive="CreationTime" matSortDirection="desc" matSortDisableClear="" [dataSource]="tableApplication.dataSource" [style.max-height]="tableApplication.maxHeight?tableApplication.maxHeight+''px'':null" [style.min-height]="tableApplication.minHeight?tableApplication.minHeight+''px'':null">
 2     <ng-container matColumnDef="selectCheckbox"><mat-header-cell *matHeaderCellDef=""><mat-checkbox (change)="$event?tableApplication.masterToggle():null" [checked]="tableApplication.isMasterChecked()" [disabled]="!tableApplication.dataSource.data.length" [indeterminate]="tableApplication.isMasterIndeterminate()"></mat-checkbox></mat-header-cell><mat-cell *matCellDef="let row"><mat-checkbox (change)="$event?tableApplication.checkedSelection.toggle(row):null" (click)="$event.stopPropagation()" [checked]="tableApplication.checkedSelection.isSelected(row)"></mat-checkbox></mat-cell></ng-container>
 3     <ng-container matColumnDef="lineNumber"><mat-header-cell *matHeaderCellDef="">ID</mat-header-cell><mat-cell *matCellDef="let row">{{ row.lineNumber }}</mat-cell></ng-container>
 4     <ng-container matColumnDef="code"><mat-header-cell *matHeaderCellDef="" mat-sort-header="">应用程序编码</mat-header-cell><mat-cell *matCellDef="let row">{{ row.code }}</mat-cell></ng-container>
 5     <ng-container matColumnDef="name"><mat-header-cell *matHeaderCellDef="" mat-sort-header="">应用程序名称</mat-header-cell><mat-cell *matCellDef="let row">{{ row.name }}</mat-cell></ng-container>
 6     <ng-container matColumnDef="enabled"><mat-header-cell *matHeaderCellDef="" mat-sort-header="">启用</mat-header-cell><mat-cell *matCellDef="let row"><mat-icon *ngIf="row.enabled">check</mat-icon><mat-icon *ngIf="!row.enabled">clear</mat-icon></mat-cell></ng-container>
 7     <ng-container matColumnDef="registerEnabled"><mat-header-cell *matHeaderCellDef="" mat-sort-header="">启用注册</mat-header-cell><mat-cell *matCellDef="let row"><mat-icon *ngIf="row.registerEnabled">check</mat-icon><mat-icon *ngIf="!row.registerEnabled">clear</mat-icon></mat-cell></ng-container>
 8     <ng-container matColumnDef="creationTime"><mat-header-cell *matHeaderCellDef="" mat-sort-header="">创建时间</mat-header-cell><mat-cell *matCellDef="let row">{{ row.creationTime | date:"yyyy-MM-dd" }}</mat-cell></ng-container>
 9     <ng-container matColumnDef="operation"><mat-header-cell *matHeaderCellDef="">操作</mat-header-cell>
10         <mat-cell *matCellDef="let row">
11             <a mat-icon-button="" matTooltip="编辑" [routerLink]="[''update'',row.id]">
12                 <mat-icon>edit</mat-icon>
13             </a>
14             <button mat-icon-button="" type="button" [matMenuTriggerFor]="menu">
15                 <mat-icon>more_vert</mat-icon>
16                 <mat-menu #menu="matMenu"><ng-template matMenuContent="">
17                     <button (click)="delete(row.id)" mat-menu-item=""><mat-icon>delete</mat-icon><span>删除</span></button>
18                     <button mat-menu-item="" [routerLink]="[''detail'',row.id]"><mat-icon>visibility</mat-icon><span>查看详细</span></button>
19                 </ng-template></mat-menu>
20             </button>
21         </mat-cell>
22     </ng-container>
23 <mat-header-row *matHeaderRowDef="[''selectCheckbox'',''lineNumber'',''code'',''name'',''enabled'',''registerEnabled'',''creationTime'',''operation''];sticky:true"></mat-header-row><mat-row (click)="tableApplication.selectedSelection.select(row)" *matRowDef="let row;columns:[''selectCheckbox'',''lineNumber'',''code'',''name'',''enabled'',''registerEnabled'',''creationTime'',''operation'']" class="mat-row-hover" [class.selected]="tableApplication.selectedSelection.isSelected(row)"></mat-row></mat-table></mat-table-wrapper>

  可以看见,Html比TagHelper代码要复杂得多,这还是封装过后的情况,如果完全没有封装,折腾一个表格将会耗费你大量精力,且Bug遍地,难以维护。 

  Ts代码几乎看不见,你只需设置base-url属性,数据绑定就完成了。 

  base-url是一个基地址,根据约定创建服务端请求地址/api/baseUrl,如果你的请求地址不同,可以改为设置url属性。 

  执行效果如下。

树型表格

  树型层次关系是业务常见操作之一。

  Util Angular Material的封装主要是在Angular Material 5.x之前完成的,Angular Material 6.x才提供了树型控件,所以Util尚未封装树型控件,不过为了解决编辑树型层次困难的局面,我从PrimeNg组件库Copy了一个树型表格过来。

  PrimeNg是另一个开源的Angular组件库,它的树型表格功能非常弱,我花了数天时间来修改它的源码,以满足我的基本需求。

  由于树型包含同步加载,异步加载,上移,下移,单选,多选等操作,封装树型表格比普通表格要复杂得多。

  服务端提供了PrimeTreeControllerBasePrimeTreeNode等对象类型来实现与客户端通信,不过它们都还相当具体化,待后续封装Ng-Zorro树型组件时再来重构。

  一旦封装完成,它用起来就跟Angular Material表格几乎没什么区别。

  来看个示例。

  TagHelper代码如下。 

<util-tree-table id="treeTable_role" base-url="role" query-param="queryParam" selection-mode="Multiple" key="treeTable_role" >
    <util-tree-table-column for="Name"></util-tree-table-column>
    <util-tree-table-column for="Code"></util-tree-table-column>
    <util-tree-table-column for="Enabled"></util-tree-table-column>
    <util-tree-table-column for="SortId"></util-tree-table-column>
    <util-tree-table-column title="操作">
        <util-a styles="Icon" tooltip="添加下级角色" link="create" query-params="{id:row.data.id}">
            <util-icon material-icon="Add"></util-icon>
        </util-a>
        <util-button id="btnMoveUp" styles="Icon" tooltip="上移" ng-if="!isFirst(row)" on-click="moveUp(row,btnMoveUp,$event)">
            <util-icon material-icon="Arrow_Upward"></util-icon>
        </util-button>
        <util-button id="btnMoveDown" styles="Icon" tooltip="下移" ng-if="!isLast(row)" on-click="moveDown(row, btnMoveDown,$event)">
            <util-icon material-icon="Arrow_Downward"></util-icon>
        </util-button>
        <util-button styles="Icon" menu-id="menu" on-click="selectRow(row,$event)">
            <util-icon material-icon="More_Vert"></util-icon>
            <util-menu id="menu">
                <util-menu-item label="编辑" material-icon="Edit" bind-link="[''update'',row.data.id]"></util-menu-item>
                <util-menu-item label="禁用" material-icon="Lock" on-click="disable(row)"></util-menu-item>
                <util-menu-item label="启用" material-icon="Lock_Open" on-click="enable(row)"></util-menu-item>
                <util-menu-item label="删除" material-icon="Delete" on-click="delete(row)"></util-menu-item>
                <util-menu-item label="详细" material-icon="Visibility" bind-link="[''detail'',row.data.id]"></util-menu-item>
            </util-menu>
        </util-button>
    </util-tree-table-column>
</util-tree-table>

  生成的Html如下。 

<p-tree-table #treeTable_role="" baseUrl="role" key="treeTable_role" selectionMode="checkbox" [(queryParam)]="queryParam">
    <p-column field="name" header="角色名称"></p-column>
    <p-column field="code" header="角色编码"></p-column>
    <p-column field="enabled" header="启用"><ng-template let-first="first" let-i="index" let-last="last" let-row="rowData"><mat-icon *ngIf="row.data.enabled">check</mat-icon><mat-icon *ngIf="!row.data.enabled">clear</mat-icon></ng-template></p-column>
    <p-column field="sortId" header="排序号"></p-column>
    <p-column header="操作"><ng-template let-first="first" let-i="index" let-last="last" let-row="rowData">
        <a mat-icon-button="" matTooltip="添加下级角色" routerLink="create" [queryParams]="{id:row.data.id}">
            <mat-icon>add</mat-icon>
        </a>
        <mat-button-wrapper #btnMoveUp="" (onClick)="moveUp(row,btnMoveUp,$event)" *ngIf="!isFirst(row)" style="mat-icon-button" tooltip="上移"><ng-template>
            <mat-icon>arrow_upward</mat-icon>
        </ng-template></mat-button-wrapper>
        <mat-button-wrapper #btnMoveDown="" (onClick)="moveDown(row, btnMoveDown,$event)" *ngIf="!isLast(row)" style="mat-icon-button" tooltip="下移"><ng-template>
            <mat-icon>arrow_downward</mat-icon>
        </ng-template></mat-button-wrapper>
        <button (click)="selectRow(row,$event)" mat-icon-button="" type="button" [matMenuTriggerFor]="menu">
            <mat-icon>more_vert</mat-icon>
            <mat-menu #menu="matMenu"><ng-template matMenuContent="">
                <button mat-menu-item="" [routerLink]="[''update'',row.data.id]"><mat-icon>edit</mat-icon><span>编辑</span></button>
                <button (click)="disable(row)" mat-menu-item=""><mat-icon>lock</mat-icon><span>禁用</span></button>
                <button (click)="enable(row)" mat-menu-item=""><mat-icon>lock_open</mat-icon><span>启用</span></button>
                <button (click)="delete(row)" mat-menu-item=""><mat-icon>delete</mat-icon><span>删除</span></button>
                <button mat-menu-item="" [routerLink]="[''detail'',row.data.id]"><mat-icon>visibility</mat-icon><span>详细</span></button>
            </ng-template></mat-menu>
        </button>
    </ng-template></p-column>
</p-tree-table>

  看上去Html比TagHelper没有复杂多少,那是因为已经将功能内置到树型表格组件内部,不得不承认,有时候修改源码比在外围扩展要省很多力气。 

  执行效果如下。

 

  在完成了异步加载,多选,上移,下移,搜索,删除行,刷新,分页等一系列功能后,一行Ts都没有,是否感觉到很清爽呢。

其它组件 

  Util还封装了颜色拾取器,菜单,侧边栏等组件,限于篇幅,就不一一介绍。 

小结

  本文简要介绍了Angular标准组件的封装手法,它能够大幅提升业务开发的生产力,同时也提醒你,必须系统学习原生技术,否则碰上稍微复杂点的问题就无法解决。

  本文更多的是介绍封装思路,而封装思想与具体UI技术无关,一旦你了解了封装背后的动机和技巧,不论Angular还是Vue,或者Android组件,甚至小程序都可以通过封装来提升开发效率。

  未完待续,C#服务端CRUD的封装将在下篇介绍。

  写文需要动力,请大家多多支持,点下推荐,Github点下星星

  Util应用框架交流一群: 24791014(已满)

  Util应用框架交流二群: 184097033

  Util应用框架地址:https://github.com/dotnetcore/util

今天关于学习NetCore应用框架——返回json数据..net 返回json数据的介绍到此结束,谢谢您的阅读,有关.Net Core3.0 WebApi 项目框架搭建 十四:自定义返回Json大小写格式、.NET Core应用框架AA介绍(二)、.Net Core应用框架Util介绍(三)、.Net Core应用框架Util介绍(五)等更多相关知识的信息可以在本站进行查询。

本文标签: