上篇简要介绍了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(,,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#代码如下。

    /// <summary>
/// 民族
/// </summary>
public enum Nation {
/// <summary>
/// 汉族
/// </summary>
[Description( "汉族" )]
Hz = ,
/// <summary>
/// 蒙古族
/// </summary>
[Description( "蒙古族" )]
Mgz = ,
/// <summary>
/// 回族
/// </summary>
[Description( "回族" )]
HuiZ = ,
/// <summary>
/// 藏族
/// </summary>
[Description( "藏族" )]
Zz = ,
/// <summary>
/// 维吾尔族
/// </summary>
[Description( "维吾尔族" )]
Wwez = ,
/// <summary>
/// 苗族
/// </summary>
[Description( "苗族" )]
Mz = ,
/// <summary>
/// 彝族
/// </summary>
[Description( "彝族" )]
Yz = ,
/// <summary>
/// 壮族
/// </summary>
[Description( "壮族" )]
ZhuangZ = ,
/// <summary>
/// 布依族
/// </summary>
[Description( "布依族" )]
Byz = ,
/// <summary>
/// 朝鲜族
/// </summary>
[Description( "朝鲜族" )]
Cxz = ,
/// <summary>
/// 满族
/// </summary>
[Description( "满族" )]
ManZ = ,
/// <summary>
/// 侗族
/// </summary>
[Description( "侗族" )]
Tz = ,
/// <summary>
/// 瑶族
/// </summary>
[Description( "瑶族" )]
YaoZ = ,
/// <summary>
/// 白族
/// </summary>
[Description( "白族" )]
Bz = ,//baizu
/// <summary>
/// 土家族
/// </summary>
[Description( "土家族" )]
Tjz = ,
/// <summary>
/// 哈尼族
/// </summary>
[Description( "哈尼族" )]
Hnz = ,
/// <summary>
/// 哈萨克族
/// </summary>
[Description( "哈萨克族" )]
Hskz = ,
/// <summary>
/// 傣族
/// </summary>
[Description( "傣族" )]
Dz = ,
/// <summary>
/// 黎族
/// </summary>
[Description( "黎族" )]
Lz = ,
/// <summary>
/// 傈僳族
/// </summary>
[Description( "傈僳族" )]
Lsz = ,
/// <summary>
/// 佤族
/// </summary>
[Description( "佤族" )]
Wz = ,
/// <summary>
/// 畲族
/// </summary>
[Description( "畲族" )]
Sz = ,
/// <summary>
/// 高山族
/// </summary>
[Description( "高山族" )]
Gsz = ,
/// <summary>
/// 拉祜族
/// </summary>
[Description( "拉祜族" )]
Lhz = ,
/// <summary>
/// 水族
/// </summary>
[Description( "水族" )]
ShuiZ = ,
/// <summary>
/// 东乡族
/// </summary>
[Description( "东乡族" )]
Dxz = ,
/// <summary>
/// 纳西族
/// </summary>
[Description( "纳西族" )]
Nxz = ,
/// <summary>
/// 景颇族
/// </summary>
[Description( "景颇族" )]
Jpz = ,
/// <summary>
/// 柯尔克孜族
/// </summary>
[Description( "柯尔克孜族" )]
Kekzz = ,
/// <summary>
/// 土族
/// </summary>
[Description( "土族" )]
TuZ = ,
/// <summary>
/// 达斡尔族
/// </summary>
[Description( "达斡尔族" )]
Dwez = ,
/// <summary>
/// 仫佬族
/// </summary>
[Description( "仫佬族" )]
Mlz = ,
/// <summary>
/// 羌族
/// </summary>
[Description( "羌族" )]
Qz = ,
/// <summary>
/// 布朗族
/// </summary>
[Description( "布朗族" )]
Blz = ,
/// <summary>
/// 撒拉族
/// </summary>
[Description( "撒拉族" )]
Slz = ,
/// <summary>
/// 毛南族
/// </summary>
[Description( "毛南族" )]
Mnz = ,
/// <summary>
/// 仡佬族
/// </summary>
[Description( "仡佬族" )]
Ylz = ,
/// <summary>
/// 锡伯族
/// </summary>
[Description( "锡伯族" )]
Xbz = ,
/// <summary>
/// 阿昌族
/// </summary>
[Description( "阿昌族" )]
Acz = ,
/// <summary>
/// 普米族
/// </summary>
[Description( "普米族" )]
Pmz = ,
/// <summary>
/// 塔吉克族
/// </summary>
[Description( "塔吉克族" )]
Tjkz = ,
/// <summary>
/// 怒族
/// </summary>
[Description( "怒族" )]
Nz = ,
/// <summary>
/// 乌孜别克族
/// </summary>
[Description( "乌孜别克族" )]
Wzbkz = ,
/// <summary>
/// 俄罗斯族
/// </summary>
[Description( "俄罗斯族" )]
Elsz = ,
/// <summary>
/// 鄂温克族
/// </summary>
[Description( "鄂温克族" )]
Ewkz = ,
/// <summary>
/// 德昂族
/// </summary>
[Description( "德昂族" )]
Daz = ,
/// <summary>
/// 保安族
/// </summary>
[Description( "保安族" )]
Baz = ,
/// <summary>
/// 裕固族
/// </summary>
[Description( "裕固族" )]
Ygz = ,
/// <summary>
/// 京族
/// </summary>
[Description( "京族" )]
Jz = ,
/// <summary>
/// 塔塔尔族
/// </summary>
[Description( "塔塔尔族" )]
Ttrz = ,
/// <summary>
/// 独龙族
/// </summary>
[Description( "独龙族" )]
Dlz = ,
/// <summary>
/// 鄂伦春族
/// </summary>
[Description( "鄂伦春族" )]
Elcz = ,
/// <summary>
/// 赫哲族
/// </summary>
[Description( "赫哲族" )]
Hzz = ,
/// <summary>
/// 门巴族
/// </summary>
[Description( "门巴族" )]
Mbz = ,
/// <summary>
/// 珞巴族
/// </summary>
[Description( "珞巴族" )]
Lbz = ,
/// <summary>
/// 基诺族
/// </summary>
[Description( "基诺族" )]
Jnz =
}

民族枚举

        /// <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来完成的。

 using System;
using Newtonsoft.Json; namespace Util {
/// <summary>
/// 列表项
/// </summary>
public class Item : IComparable<Item> {
/// <summary>
/// 初始化
/// </summary>
/// <param name="text">文本</param>
/// <param name="value">值</param>
/// <param name="sortId">排序号</param>
/// <param name="group">组</param>
/// <param name="disabled">禁用</param>
public Item( string text, object value, int? sortId = null, string group = null, bool? disabled = null ) {
Text = text;
Value = value;
SortId = sortId;
Group = group;
Disabled = disabled;
} /// <summary>
/// 文本
/// </summary>
[JsonProperty( "text", NullValueHandling = NullValueHandling.Ignore )]
public string Text { get; } /// <summary>
/// 值
/// </summary>
[JsonProperty( "value", NullValueHandling = NullValueHandling.Ignore )]
public object Value { get; } /// <summary>
/// 排序号
/// </summary>
[JsonProperty( "sortId", NullValueHandling = NullValueHandling.Ignore )]
public int? SortId { get; } /// <summary>
/// 组
/// </summary>
[JsonProperty( "group", NullValueHandling = NullValueHandling.Ignore )]
public string Group { get; } /// <summary>
/// 禁用
/// </summary>
[JsonProperty( "disabled", NullValueHandling = NullValueHandling.Ignore )]
public bool? Disabled { get; } /// <summary>
/// 比较
/// </summary>
/// <param name="other">其它列表项</param>
public int CompareTo( Item other ) {
return string.Compare( Text, other.Text, StringComparison.CurrentCulture );
}
}
}

Util.Item

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

 //============== 列表=============================
//Copyright 2018 何镇汐
//Licensed under the MIT license
//================================================
import { ISort, sort } from '../core/sort';
import { util } from '../index'; /**
* 列表
*/
export class Select {
/**
* 初始化列表
* @param items 列表项集合
*/
constructor(private items: SelectItem[]) {
} /**
* 转换为下拉列表项集合
*/
toOptions(): SelectOption[] {
return this.getSortedItems().map(value => new SelectOption(value));
} /**
* 获取已排序的列表项集合
*/
private getSortedItems() {
return sort(this.items);
} /**
* 转换为下拉列表组集合
*/
toGroups(): SelectOptionGroup[] {
let result: SelectOptionGroup[] = new Array<SelectOptionGroup>();
let groups = util.helper.groupBy(this.getSortedItems(), t => t.group);
groups.forEach((items, key) => {
result.push(new SelectOptionGroup(key, items.map(item => new SelectOption(item)), false));
});
return result;
} /**
* 是否列表组
*/
isGroup(): boolean {
return this.items.every(value => !!value.group);
}
} /**
* 列表项
*/
export class SelectItem implements ISort {
/**
* 文本
*/
text: string;
/**
* 值
*/
value;
/**
* 禁用
*/
disabled?: boolean;
/**
* 排序号
*/
sortId?: number;
/**
* 组
*/
group?: string;
} /**
* 下拉列表项
*/
export class SelectOption {
/**
* 文本
*/
text: string;
/**
* 值
*/
value;
/**
* 禁用
*/
disabled?: boolean; /**
* 初始化下拉列表项
* @param item 列表项
*/
constructor(item: SelectItem) {
this.text = item.text;
this.value = item.value;
this.disabled = item.disabled;
}
} /**
* 下拉列表组
*/
export class SelectOptionGroup {
/**
* 初始化下拉列表组
* @param text 文本
* @param value 值
* @param disabled 禁用
*/
constructor(public text: string, public value: SelectOption[], public disabled?: boolean) {
}
}

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( , ) ) )
.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 = ,
/// <summary>
/// 男
/// </summary>
[Description( "先生" )]
Male =
}
        /// <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代码。

 <util-table id="tableApplication" query-param="queryParam" base-url="application"
sort="CreationTime" sort-direction="Desc" max-height="500">
<util-table-column type="Checkbox"></util-table-column>
<util-table-column type="LineNumber"></util-table-column>
<util-table-column for="Code" sort="true"></util-table-column>
<util-table-column for="Name" sort="true"></util-table-column>
<util-table-column for="Enabled" sort="true"></util-table-column>
<util-table-column for="RegisterEnabled" sort="true"></util-table-column>
<util-table-column for="CreationTime" sort="true"></util-table-column>
<util-table-column title="操作" column="operation">
<util-table-cell>
<util-a styles="Icon" tooltip="编辑" bind-link="['update',row.id]">
<util-icon material-icon="Edit"></util-icon>
</util-a>
<util-button styles="Icon" menu-id="menu">
<util-icon material-icon="More_Vert"></util-icon>
<util-menu id="menu">
<util-menu-item label="删除" material-icon="Delete" on-click="delete(row.id)"></util-menu-item>
<util-menu-item label="查看详细" material-icon="Visibility" bind-link="['detail',row.id]"></util-menu-item>
</util-menu>
</util-button>
</util-table-cell>
</util-table-column>
</util-table>

  生成的Html如下。

 <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">
<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>
<ng-container matColumnDef="lineNumber"><mat-header-cell *matHeaderCellDef="">ID</mat-header-cell><mat-cell *matCellDef="let row">{{ row.lineNumber }}</mat-cell></ng-container>
<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>
<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>
<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>
<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>
<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>
<ng-container matColumnDef="operation"><mat-header-cell *matHeaderCellDef="">操作</mat-header-cell>
<mat-cell *matCellDef="let row">
<a mat-icon-button="" matTooltip="编辑" [routerLink]="['update',row.id]">
<mat-icon>edit</mat-icon>
</a>
<button mat-icon-button="" type="button" [matMenuTriggerFor]="menu">
<mat-icon>more_vert</mat-icon>
<mat-menu #menu="matMenu"><ng-template matMenuContent="">
<button (click)="delete(row.id)" mat-menu-item=""><mat-icon>delete</mat-icon><span>删除</span></button>
<button mat-menu-item="" [routerLink]="['detail',row.id]"><mat-icon>visibility</mat-icon><span>查看详细</span></button>
</ng-template></mat-menu>
</button>
</mat-cell>
</ng-container>
<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

.Net Core应用框架Util介绍(五)的更多相关文章

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

    Util的开源地址 https://github.com/dotnetcore/util Util的开源协议 Util以MIT协议开源,这是目前最宽松的开源协议,你不仅可以用于商业项目,还能把Util ...

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

    距离上次发文,已经过去了三年半,这几年技术更新节奏异常迅猛,.Net进入了跨平台时代,前端也被革命性的颠覆. 回顾 2015年,正当我还沉迷于JQuery + EasyUi的封装时,突然意识到技术已经 ...

  3. .Net Core应用框架Util介绍(二) 转

    Util的开源地址 https://github.com/dotnetcore/util Util的开源协议 Util以MIT协议开源,这是目前最宽松的开源协议,你不仅可以用于商业项目,还能把Util ...

  4. .Net Core应用框架Util介绍(一)转

    回顾 2015年,正当我还沉迷于JQuery + EasyUi的封装时,突然意识到技术已经过时. JQuery在面对更加复杂的UI需求时显得力不从心,EasyUi虽然组件比较完善,但界面风格老旧,响应 ...

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

    前面介绍了Util是如何封装以降低Angular应用的开发成本. 现在把关注点移到服务端,本文将介绍分层架构各构造块及基类,并对不同层次的开发人员应如何进行业务开发提供一些建议. Util分层架构介绍 ...

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

    上篇介绍了Util Angular Demo的目录结构和运行机制,本文介绍Util封装Angular的基本手法及背后的动机. Angular应用由Ts和Html两部分构成,本文介绍第一部分. Angu ...

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

    上篇介绍了Util的开发环境,并让你把Demo运行起来.本文将介绍该Demo的前端Angular运行机制以及目录结构. 目录结构 在VS上打开Util Demo,会看见如下的目录结构. 现代前端通常采 ...

  8. python nose测试框架全面介绍五--attr介绍

    之前写了一系列nose框架的,这篇介绍下attr tag 在nose框架中attr用来标识用例,使得在运行时可以通过标识来执行用例,之前在nose测试框架全面介绍四中有说明,但没有说明清楚,这里再总结 ...

  9. Magicodes.Admin.Core开源框架总体介绍

    框架说明 Magicodes.Admin.Core框架在ABP以及ASP.NET ZERO的基础上进行了封装和完善,目前基于.NET Core 2.0+(Framework版本),由于部分组件在.NE ...

随机推荐

  1. 支持异步写入的日志类,支持Framework2.0

    因为工作需要需要在XP上运行一个C#编写的Winform插件,我就用Framework2.0,因为存在接口交互所以想保留交易过程的入参出参. 考虑到插件本身实施的因素,就没有使用Log4.NLog等成 ...

  2. python之错误调试

    无论谁写的程序,必定会存在bug,解决bug需要我们去调试程序.于是乎,在Python中,就会好几种调试手段,如print.assert.logging.pdb.pdb.set_trace() 一.使 ...

  3. Linux基础命令第一天

    一.命令行bash基本操作 1,shell 用户不能直接操作内核,所以用户操作通过shell传递给内核,Linux下叫shell,就相当于Windows下的cmd shell分为两种: GUI:图形界 ...

  4. 如何使用Dubbo 2.7.0和Spring boot实现FAT测试(Feature Acceptance Test)

    在一个调用链非常长的功能中,如果想修改其中的一个特性,并进行测试,而又不影响该环境的其他用户使用现有功能.特性,例如: 1. A.B.C.D之间通过Dubbo实现远程调用 2. 这些模块可能有一个或者 ...

  5. ejs常用语法

    nodejs的模板引擎有很多, ejs是比较简单和容易上手的.常用的一些语法: 用<%...%>包含js代码 用<%=...%>输出变量 变量若包含 '<' '>' ...

  6. Laravel5中通过SimpleQrCode扩展包生成二维码实例

    Simple Qrcode是基于强大的Bacon/BaconQrCode库开发的针对Laravel框架的封装版本,用于在Laravel中为生成二维码提供接口. 安装SimpleQrCode扩展包 在项 ...

  7. Belgrade Azure 2019-2-11活动感悟

    这是<国外线下技术俱乐部建设>系列文章之一.   活动网址:https://www.meetup.com/Azure-UG-Srbija/events/258673179/ 活动内容:Az ...

  8. WGS84地理坐标系下,进行坐标运算

    经纬度坐标本身是不能直接运算的.原因是:经纬度坐标并非是直角坐标系.纬线圈间隔均匀,经线圈越靠近两级越密,如下图: 现在有个需求,已知两点和两点处射线斜率,求交点坐标. 虽然地球整体是个圆,但是局部地 ...

  9. <4>Python切片功能剖析

    引用文章:https://mp.weixin.qq.com/s/NZ371nKs_WXdYPCPiryocw 切片基础法则: (1)公式,禁止0. (2)i, n同号:从序列的第i位索引起,向右取n- ...

  10. WPF:间接支持虚拟化的ListBox

    /// <summary> /// 间接实现了虚拟化的ListBox /// 子项必须实现IVisible接口 /// 你可以在IsVisible发生改变时实现一系列自定义动作 /// 比 ...