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 @ 2011-03-24 14:19  君之蘭  阅读(2326)  评论(13编辑  收藏  举报