CMS系统模板引擎设计(5):Label应用初探

话说上节听的很郁闷,讲的也郁闷,整个系列没有详细的Code,所以都有点懵。其实仔细看之前的几节应该还是能够理解整个思路的。 我在这里再整理一遍: 用户访问URL后 ->根据所有Page的URL规则(urlpattern)获取当前Page,然后再根据page的Template,Find出所有的Label(通过反射),然后遍历这些Label,把得到的数据的Html替换掉Label本身的标签代码。最后整个Template就是要生成的HTML了。
所以我们要明白Label是如何获取工作才能获取数据库的数据的。一个Label可以理解成一个控件,那么控件必然会支持一些属性(Parameter)和一些内容(Field)。 我们上节就是在讲怎么来解析parameter,因为有些特殊的parameter,所以设计的时候设计了parameter的基类,特殊的则是子类。
同样,field是具体的要展现在HTML代码中的字段,比如中的[field:Title/],这就是一个字段,我们的模版引擎的工作就是把他替换掉应该展现的标题,而如何才能工作?我们就得设计Field的整个逻辑。在替换循环的过程中使用field类。
但是,我今天不讲Field了,因为这样讲大家还是糊涂依旧。今天我们就来设计一个Article:List的初级版。我觉得或许从实例讲解大家更容易理解设计的理念。OK,那就开始设计一个Article.List,我们最熟悉的文章列表。
//简单的循环列表
{Article:List Top="10" CategoryId="5"}
<a href ="/details/[field:FileName/]" target="_blank">[field:Title/]</a>

{/Article:List} 

想象一下Repeater,有个ItemTemplate,那么对于List这个Label来说,他的ItemTemplate显然就是Template属性。那么如果我们获取到数据源后直接foreach替换掉所有的field即可。代码大概如下:
/// <summary>
/// 获取要展示的HTML代码
/// </summary>
/// <returns></returns>
public override string GetRenderHtml()
{
    var html = TemplateString;
    foreach (var article in GetDataSource())
    {
        foreach (var field in Fields)
        {
            html = html.Replace(field.Html, field.GetValue(article));
        }
    }
    return html;
}

  从上面的方法中,我们可以看到替换的机制是每一行数据都要执行一次所有字段的替换(所以之前有提过在构造嵌套的时候为了防止Field混乱要处理TemplateString),最后返回html。我们还能看到一些未知的方法和字段:GetDataSource(),Field.Html,Field.GetValue(),这些已经暴露了我们的Field设计的部分内容。我们先看GetDataSource()是什么?

    /// <summary>
    /// 获取Article列表
    /// </summary>
    /// <returns></returns>
    private IEnumerable<Article> GetDataSource()
    {
        var parameter = new ArticleQueryParameter();
        //构造查询的参数
 
        return ArticleDataHelper.GetList(parameter);
    }
/// <summary>
/// 查询参数基类
/// </summary>
public class QueryParameter
{
    public int PageSize { get; set; }
    public int PageIndex { get; set; }
    public int RecordCount { get; set; }
    public string SearchKey { get; set; }
}
/// <summary>
/// 文章查询类
/// </summary>
public class ArticleQueryParameter
{
    public QueryParameter PageParameter { get; set; }
    public DateTime PostTime { get; set; }
    public int CategoryId { get; set; }
    public int Top { get; set; }
}
/// <summary>
/// 文章获取数据类
/// </summary>
public class ArticleDataHelper
{
    public static IEnumerable<Article> GetList(ArticleQueryParameter parameter)
    {
        return null;
    }
}

其实就是获取ArticleList的数据源,具体的实现方式大家可能都不一样,但Article.List最终需要这么一个数据获取的方法,然而这个方法都需要接受一些查询条件的参数,这些参数都来自Parameters!!现在我们来填充GetDataSource()的参数构造部分。

private IEnumerable<Article> GetDataSource()
  {
      var parameter = new ArticleQueryParameter();
      //构造查询的参数
      parameter.CategoryId = Parameters["CategoryId"].ConvertToInt(0);
      parameter.Top = Parameters["Top"].ConvertToInt(Parameters["PageSize"].ConvertToInt(0));
      var pageIndex = Parameters["PageIndex"].ConvertToInt(1);
      if (pageIndex > 1)
      {
          parameter.PageParameter = new QueryParameter
          {
              PageIndex = pageIndex,
              PageSize = parameter.Top
          };
      }
      return ArticleDataHelper.GetList(parameter);
  }

Parameters是Label的ParameterCollection,他可以通过索引直接访问具体的parameter。ConvertTo<T>(T defaultValue)是可以将parameter的value转成T类型。 这就是Parameter所用到的地方之一。另外可以看到Field具体Html属性和GetValue方法,而且GetValue接受了当前Article实体作为参数(不接受参数的话,我们怎么得到某个字段的值呢:)。

整个List流程应该比较清楚了吧,获取数据源,然后循环数据,每行再去替换所有的Field,最后把拼接好的HTML返回。当然这是List,如果是其他的标签可能就是另外一个处理办法。比如System.Include标签,他的工作就是嵌入一个用户控件(PartialTemplate),那么他的处理逻辑和List就完全不一样(他是先根据templateid参数的值获取template,然后再把自己所有的Parameters传递给这个template里的所有标签,最后再把这个template替换后的结果作为自己的结果返回,他没有循环)。所以我们的具体控件逻辑都是大相径庭的,但最终都是要返回替换后的HTML,但所有的List却都是差别多的,无非就是不同的数据源进行循环。所以对于List我们应该进行抽象,把公共部分提取出来,尽量让每个具体的Label更明确职责。如何抽象呢? 那就看看有没有可提取的公共部分。

所有的List都可能会有分页,所以ListBase应该有PageParameter,所有的List都会去循环DataSoruce,所以ListBase默认实现了DataSource循环,但是增加了一个方法那就是GetDataSource。这个方法是抽象的,所有的List必须实现。

/// <summary>
/// 循环标签基类
/// </summary>
public abstract class ListBase : Label
{
    public QueryParameter PageParameter { get; set; }
 
    public abstract IEnumerable<dynamic> GetDataSource();
 
    public override string GetRenderHtml()
    {
        var dataSource = GetDataSource();
        if (dataSource == null) return string.Empty;
        var html = TemplateString;
        foreach (var dataItem in dataSource)
        {
            foreach (var field in Fields)
            {
                field.Data = dataItem;
                html = html.Replace(field.Html, field.GetValue());
            }
        }
 
        return html;
    }
}
 

foreach里我也做了点细微的调整,就是把Field的GetValue的参数拿掉了,换成了成员,这样更明白些。你可能会有一些疑点:

为什么设计为抽象而不是虚方法或接口?

所有子类的实现方法都不一致,没有可提取部分,所以虚函数没有意义,如果单独抽象成接口,则所有子类必须继承此接口,因为GetRenderHtml和该方法紧密结合,foreach里需要显式转换为接口才能调用,完全没有意义。

为什么是GetDataSource方法,而不是公开一个DataSource成员? 如果需要Set呢?还要增加一个SetDataSource?

其实这个我考虑过,很少有Set的情况,因为标签都是自动生成的没有外部去干扰(Set),但不能否认以后完全没有,如果设为成员,则必须有一个可get的地方,要么是abstract,那样也会把set abstract,要么就在Init里给set先,那也得有一个抽象的set方法。所以考虑现状还是使用一个方法最为合适。

另外一点就是为什么用了dynamic,而不是T。

首先不能是T,如果是T,则GetRenderHtml调用时也需要指明T,则整个ListBase就要变成泛型类ListBase<T>,除非base不执行GetDataSource调用。为什么不能用ListBase<T>?因为有些GetDataSource会用linq返回匿名类型集合,子类无法确定返回的具体类型名称,所以就不能继承ListBase<T>。但我们可以用dynamic,动态类型,到真正执行时可以确定T就行,这个不用我们操心,然而object显然略逊一筹了。

这样一来,Article的List只需要实现GetDataSource就行了。

这只是最简单的List雏形,假如说我还需要像Repeater控件那样,有headtemplate itemtemplate foottemplate altertemplate spacetemplate怎么办?

这个就需要定义子标签类了。这里我就不多说了,其实很简单,就是再定义几个Label,他们又各自的获取Html的方法,我们最后组合起来就行。自需要注意List的Template和Field已经没了,都属于子标签了。而且像交替执行的(Item和Alter)需要再循环里给他们隔行赋值。下面是我以前写的代码,虽然比较难看,不太OO,但能说明实现的逻辑:

代码

今天就讲到这了,不知道还有朋友有兴趣没有,目前还没有演示,或许某天我会放出个demo源码。 下次讲Field的设计吧,这也算是最后一个设计了。

posted @   君之蘭  阅读(3723)  评论(14编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
点击右上角即可分享
微信分享提示