Anyone who has ever developed with ASP.NET coming from classic ASP or some other embedded/inline based web development platform knows that ASP.NET is quite different. ASP.NET, along with other Microsoft-brewed development platforms like WPF are attempting to make it easier (or at least more intuitive) to separate the Presentation Layer from the logic layer or data access layer.

This is all good and fun, but sometimes when you are developing a website where you need to do something that would be quite simple and mindless in something like PHP or classic ASP, in ASP.NET it ends up being rather nontrivial.

One such issue is when you start dynamically changing the presentation of data, based on logic applied to the data itself. Often times, ASP.NET works rather well for this type of thing – inline Item Templates in the markup with databound controls such as the ListView, Repeater, etc., are a wonderfully intuitive way to accomplish this. The issue comes when this isn’t just simple logic anymore, but rather complex logic. Even more, what if you don’t know what your template is going to look like? What if the actual template is being created dynamically?

In instances like this, you must abandon the ItemTemplate pattern in the markup, and you must implement the ITemplate interface or the IBindableTemplate interface. These interfaces are brilliant creations (for the most part) on Microsoft’s part, but it then becomes extremely difficult (or at least verbose) to introduce standard HTML objects into the template.

This was a problem that particularly bothered me in a recent project I was working on, so I wrote up a little implementation of the IBindableTemplate interface which took care of this problem rather simply for me.

public class LiteralItemTemplate : IBindableTemplate
{
    private string formatString;
    public delegate string LambdaExpr(object container);
    public Dictionary<string,> LambdaDictionary = new Dictionary<string,>();

    public LiteralItemTemplate(string FormatString)
    {
        formatString = FormatString;
    }

    public void InstantiateIn(Control container)
    {
        LiteralControl ctl = new LiteralControl();
        ctl.DataBinding += new EventHandler(OnDataBinding);
        container.Controls.Add(ctl);
    }

    public void OnDataBinding(object sender, EventArgs e)
    {
        LiteralControl target = (LiteralControl)sender;
        Control item = target.BindingContainer;
        target.Text = Regex.Replace(formatString, @"(\{.+?\})",
                m =>
                {
                    string word = m.Groups[1].Value;
                    int ind = word.IndexOf(':');
                    return string.Format(
                        string.Format("{{0{0}}}",
                            (ind > 0) ? word.Substring(
                               ind, word.Length - ind - 1) : string.Empty
                            ),
                        evaluateKey(item.DataItem, word.Substring(
                                  1, ((ind>0)?(ind-1):(word.Length - 2))))
                        );
                });
    }

    private object evaluateKey(object container, string key)
    {
        return (LambdaDictionary.ContainsKey(key))?
          LambdaDictionary[key](container):DataBinder.Eval(container, key);
    }

    public System.Collections.Specialized.IOrderedDictionary ExtractValues(Control container)
    {
        System.Collections.Specialized.OrderedDictionary _table = 
           new System.Collections.Specialized.OrderedDictionary();
        return _table;
    }
}

This class is pretty simple. I have implemented 2 of the 3 necessary members of IBindableTemplate. The third, ExtractValues, is only necessary for two-way databinding, which I did not need at the time.

For instance, we can replace the simple ItemTemplate created in the markup below:

<asp:Repeater ID="myDataBoundControl" runat="server">
    <ItemTemplate>
        <tr>
            <td>
                <a href='<%# Eval("WebsiteUrl") %>'>
                <%# Eval("CompanyName") %>
                </a>
            </td>
            <td><%# Eval("IndustrySector") %></td>
            <td><%# string.Format("{0:#,0}",Eval("MarketCap") %></td>
        </tr>
    </ItemTemplate>
</asp:Repeater>

and replace it with the following:

protected void Page_Load(object sender, EventArgs e)
{
    Repeater myDataBoundControl = new Repeater();

    myDataBoundControl.ItemTemplate = new LiteralItemTemplate("<tr><td>" + 
      "<a href=\"{WebsiteUrl}\">{CompanyName}</a></td>" + 
      "<td>{IndustrySector}</td><td>{MarketCap:#,0}</td></tr>");

    myDataBoundControl.DataSource = GetDataSource();
    myDataBoundControl.DataBind();
}

In this case, you use syntax similar to the string.Format() syntax, but slightly different. If I would like to apply DataBinder.Eval(“FieldName”), I would pass a string with {FieldName} inside the constructor.

Further, you can use a format string preceded by a colon just like in string.Format. For instance, string.Format(“{0:#,0}”,DataBinder.Eval(“FieldName”)) is equivalent to using the format string {FieldName:#,0} inside the constructor parameter.

Further, if you would like to apply a certain logic to the incoming data, rather than directly display it, you can do this very easily using lambda expressions.

For example:

protected void Page_Load(object sender, EventArgs e)
{
    Repeater myDataBoundControl = new Repeater();

    LiteralItemTemplate t = 
       new LiteralItemTemplate("<tr class=\"{LambdaCssClass}\"><td>" + 
       "<a href=\"{WebsiteUrl}\">{CompanyName}</a></td>" + 
       "<td>{IndustrySector}</td><td>{LambdaField}</td></tr>");

    t.LambdaDictionary.Add("LambdaField", 
      c => BusinessLogicToString(DataBinder.Eval(c, "CompanySize")));
    t.LambdaDictionary.Add("LambdaCssClass", 
      c => CompanyIDToCssClass(DataBinder.Eval(c, "CompanyID")));

    myDataBoundControl.ItemTemplate = t;

    myDataBoundControl.DataSource = GetDataSource();
    myDataBoundControl.DataBind();
}

The lambda expressions accept the contract:

public delegate string LambdaExpr(object container);

Here, container is the DataBinding container, which can then be used inside the lambda expression with the DataBinder.Eval function to retrieve any necessary contextual data.

I would love to hear some potential improvements anyone has to offer. I have not tested the performance, but it would also be interesting to see how these templates stack up to the inline templates in the markup.

Some known issues

  • Getting the right Databinding container is sometimes a problem.
  • The ExtractValues method needs to be implemented to allow for two-way data binding.
推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"