如果您对学习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数据)
- .Net Core3.0 WebApi 项目框架搭建 十四:自定义返回Json大小写格式
- .NET Core应用框架AA介绍(二)
- .Net Core应用框架Util介绍(三)
- .Net Core应用框架Util介绍(五)
学习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 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介绍(二)
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介绍(三)
上篇介绍了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还原
当你输入yarn和cnpm install node-sass,它会找到package.json文件的dependencies节,然后把需要的文件下载到node_modules目录中。
执行Webpack构建
然后输入npm run dev,这里发生了什么?
npm run是npm的一个命令,它会查找package.json中scripts定义的命令。
- 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 }
vendorJs 对象用于配置将哪些第三方Js框架文件进行打包,vendorCss 对象用于配置需要打包的第三方框架提供的Css文件。
entry属性指定了需要打包的入口文件,output属性则指定输出的位置和文件名。
当webpack.config.vendor.js执行完毕,会在Util.Samples.Webs项目的wwwroot目录创建一个dist子目录,并生成vendor.js和vendor.css两个文件。
注意:vendor.js仅在开发调试阶段使用,所以并没有对它进行压缩,正式发布并不需要执行vendorJs对象。
该脚本的最后一行证明了这一点。
return isDev ? [ vendorJs, vendorCss] : [vendorCss];
- npm run app
npm run app又包含两个命令,用于执行webpack.config.util.js和webpack.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 }
它将查找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查找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路由配置中设置的。
控制器ApplicationController的Index方法将返回视图。
Angular JIT编译会在系统启动时请求服务端URL,在Chrome浏览器F12调出开发者工具,刷新页面,会观察到页面请求了Areas中的控制器。
所以你在开发阶段运行项目会感觉比较慢,在正式发布后就没这些开销了。
小结
本文简要介绍了Util Demo的目录结构和运行机制,如果你没有Angular基础,估计还是很难看懂,建议你阅读Angular中文网https://angular.cn。
未完待续,下一篇将对Util Demo的Angular封装进行介绍,本来是准备这篇介绍的,不过限于篇幅,放到下篇,我知道,太长的文章既难写更难读。
写文需要动力,请大家多多支持,点下推荐,Github点下星星。
Util应用框架交流一群: 24791014
.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>$ </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 }
客户端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 }
下面来演示一下用法。
先把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组件库,它的树型表格功能非常弱,我花了数天时间来修改它的源码,以满足我的基本需求。
由于树型包含同步加载,异步加载,上移,下移,单选,多选等操作,封装树型表格比普通表格要复杂得多。
服务端提供了PrimeTreeControllerBase和PrimeTreeNode等对象类型来实现与客户端通信,不过它们都还相当具体化,待后续封装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介绍(五)等更多相关知识的信息可以在本站进行查询。
本文标签: