CMS系统模板引擎设计(6):Field的类的设计
Field的意思是字段,我们在展示一条数据的时候总是要展示具体的某些字段,有时候是一条数据,有时候是个循环。
一条数据的时候很好处理,我们把数据准备好,然后替换相关的[field]标签就行了。当循环输出的时候,其实只需要调用显示一条数据的方法就行了。
Field的标签格式大概是这样:[field:PostTime length=10 dateFormat="yyyy-MM-dd HH:mm"/] 可以看出,Field也有自己的一些属性,就像一个Label一样。
我们在展示Field的时候有时不可能完全按照数据库的字段名来展示,比如 发布时间PostTime字段,我们可能需要展示为“多久以前”,比如“刚刚”,“5分钟前”等字样,这时候就需要调用一个方法(Render)来处理。所以Field类需要一个展示的方法成员,并且有一个存储初始值的Value字段。
为了实现代码的一致性,我们统一调用Render方法,但Render方法具体做什么,天知道。我们可以设计一个FieldBase,默认返回初始值Value。
类的大概设计是这样子:
public class FieldBase { public string Name { get ; set ;} public string Value { get ; set ;} public string Html { get ; set ;} //html用来存放Field的完整标签 public virtual string Render() { return Value; } } |
那么,这么一来只要我们稍有不一致的Render都要实现一个具体的Field类吗?那岂不是传说中的类爆炸。而且,我们还要考虑如何去通过简单的field标签实例化这些不同Render的子类,想想就头大。显然用继承的方式来设计是有问题的。
所以,我们得把Render改下:
//... public event Func<FieldBase,String,String> Render; public virtual string GetRender() { //if(SetValue!=null) // SetValue(this,RowData) if (Render!= null ) return Render( this ,Value); //这句表明了一个顺序问题:在调用GetRender之前,假如需要Field.Value的值的话必须先赋值,后面我们会提到。 else return Value; } //public Object RowData {get;set;} public Func<FieldBase,Object> SetValue; //... |
是的,我们只是增加了一个事件,并且修改了下名字,这样就想外部公布了一个自定义的方法,但内部实现调用机制。而且GetRender仍然是可重写的,比如一些非常特殊的Field标签,可能就需要重写FieldBase。
这个GetRender写的非常简单,实际使用情况是复杂的,你有没有注意掉我们忽略掉了field的Parameters(那些length formate之类的),其实调用情况和Label的Parameter类似。
如何使用这些Field?
之前的Label类设计中,Label有个Init方法,
/// <summary> /// 初始化Label /// </summary> public virtual void Init() { if (PreInit != null ) { PreInit( this ); } //初始化所有参数 Parameters = new ParameterCollection(ParameterString); //初始化所有字段 Fields = new FieldCollection(TemplateString); } |
一个标签在实例化后就需要初始化所包含的Field。有了这些Fields,我们在执行一行数据替换的时候就很容易了。
public string GetItemRenderHtml<T>( string template,T itemData) { foreach ( var field in Fields) { //这个是判断被替换的模板是否包含这个Field,这里有个弊端就是标签里field的ield必须小写了。之所以加这个判断是避免不需要的耗时的field在这里被执行。比如AlterTemplate可能会两行的模板不一致。 if (!template.Contains( "ield:" +field.Name)) continue ; //itemData显然是整行的数据,我们要从这里拿出该field对应的值,具体怎么拿,这要看field的特性,所以Field的render事件负责拆解这个itemData, //所以Feild还需要公开一个SetValue的方法接口,否则Value是没法赋值了。 //之所以说是方法接口(委托),而不是用事件,就是因为Field不具备itemData这个属性,除非再给Field增加一个RowData属性,否则Field内部无法实现事件的执行。当然,增加这个RowData也没什么,使用事件更具备封闭性。 //因此,Field上又多个SetValue方法,至于设计的是否合理,兄弟们可以自己琢磨,必须SetValue的具体实现还是在field所在的Label里比较合适。 field.SetValue(field,itemData); template = template.Replace(field.Template,field.GetRender()); } } |
假如我们要实现一个评论列表的调用,标签大致如下:
{Comment:List ArticleId=Url(id)} < div >[field:Index/]楼 [field:UserName/] 发表于[field:PostTime dataformat="yyyy-MM-dd HH:mm"/]</ div > < div >[field:Content/]</ div > {/Comment:List} |
显然Index字段在数据库中是不存在的,那么我们的Comment.List这个Label里的数据中需要为这个字段整理从数据库中获取的数据。
假如我们又声明了一个List通用的数据实体
public class ListItem<T> { public int Index{ get ; set ;} public T ItemData{ get ; set ;} } |
那么,在调用Label.Init()的时候,我们要对特熟的Field做好SetValue工作。
public override Init() { base .Init(); var fieldIndex = Fields[ "Index" ]; //获取Index这个字段 if (fieldIndex!= null ) { //给Index这个字段定义获取Value的方法。 field.SetValue = (me,itemData) => { me.Value = itemData.GetPropertityValue( "Index" ); //这是一个扩展方法,通过反射获取成员值。你可能会说,kao,反射多伤身,其实如果用hash表类型获取是比较快速,但是一个页面没多少数据,所以反射影响性能不大,何况还有页面缓存。 }; } } |
那么,整个Comment.List的GetRenderHtml的伪代码大致是这样的。
public override string GetRenderHtml() { var comments = CommentHelper.GetList().ToList<ListItem>(); //获取数据源,并整理成需要的数据格式 var html = string .Empty; foreach ( var comment in comments) { html += GetItemRenderHtml(ItemTemplate,comment); } return html; } |
举一个实际种的例子,比如分页这个Field,他所需要的数据就是 RecordCount PageSize PageIndex,但他需要很多定制化的参数,所以这种特熟的Field直接写一个Field子类,在Label初始化的时候实例化给Filds["Page"]就行了。
在实际应用中要比我写的这些例子复杂的多,我个人觉得难度还是在于代码的编写做到可读、可维护、可扩展。
编写这个模板引擎系统给我带来最大的好处就是对OOP有了一定的认识。当然还只是皮毛。
写这些文章的目的不是为了模板引擎的实现,只是想表达设计的思想,但是感觉文字的表达确实很脆弱,可惜本人也不太会做图,所以就这么着吧。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?