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有了一定的认识。当然还只是皮毛。
写这些文章的目的不是为了模板引擎的实现,只是想表达设计的思想,但是感觉文字的表达确实很脆弱,可惜本人也不太会做图,所以就这么着吧。
posted @   君之蘭  阅读(2334)  评论(13编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
点击右上角即可分享
微信分享提示