Search code examples
c#asp.netdata-bindingdynamicproxy-classes

Framework for ASP.NET data-binding wrapper classes


Apparently ASP.NET doesn't allow data-binding to dynamic objects. Major bummer, because I could see a syntax like this being really useful:

public class User
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

...

// No this doesn't exist, I just wish it did!
MyGrid.DataSource = GetAllUsers()
    .AsDynamic()
        .WithProperty("FullName", user => user.FirstName + " " + user.LastName)
    .ToEnumerable(); // returns IEnumerable<dynamic>
MyGrid.DataBind()

...

<asp:BoundField DataField="FirstName" HeaderText="First Name" />
<asp:BoundField DataField="LastName" HeaderText="Last Name" />
<asp:BoundField DataField="FullName" HeaderText="Full Name" />

In this example, AsDynamic() would return a class that would configure the dynamic objects that would be returned by .ToEnumerable() later (because you can't implement IEnumerable<dynamic>) effectively adding properties to the wrapped data object. The requests for FirstName and LastName would be "served" by the real object, and the request for FullName would be routed to a delegate or expression to be evaluated dynamically.

This is a trivial example, because in most cases you could easily add a FullName property to the User object, and you could easily pull this off with a TemplatedField.

But what if the added property was way too difficult to implement in a TemplatedField without several lines of databinding codebehind? And what if you didn't control the source code for the User class? Or what if you can't add the property to User because its calculation is dependent on an assembly which itself depends on User's assembly? (circular reference problem)

For this reason it would be great to have a very easy-to-apply data binding wrapper such as this, where you don't have to generate a brand new class every single time.

So what am I really after?

Are there any frameworks or techniques out there that allow this kind of thing? The exact syntax above isn't really important, just the ability to dynamically add stuff to classes and use those proxies in data-binding, without a bunch of manual plumbing code.


Solution

  • I find three ways to solve (some) of your problems using C# and a way to extend some of these approaches using Visual Studio tools.

    Anonymous types

    ASP.NET can data bind to anonymous types:

    DataGrid.DataSource = GetAllUsers().
      .AsQueryable()
      .Select(u => new { User = u, FullName = GetFullName(u) });
    DataGrid.DataBind()
    

    The anonymous type can still give easy access to the original type (in this example through the User property). This will make data binding relatively easy (using <asp:TemplateField>), and you have moved the complex logic to a separate method that operates on a User object.

    <%# Eval("User.FirstName") %>
    <%# Eval("User.LastName") %>
    <%# Eval("FullName") %>
    

    The data binding syntax should be placed inside the ItemTemplate of the <asp:TemplateField>, but I have omitted that code for brevity. Of course the last property can also be displayed using the <asp:BoundField>:

    <asp:BoundField DataField="FullName" />
    

    Notice that you don't have to map each property of the original type in the anonymous type, you can just map one property to the original object. The (only?) drawback is that you can no longer use <asp:BoundField> for those properties but you must use <asp:TemplateField>.

    Extension methods

    To complement this approach, you could use extension methods to 'attach' methods to a class even when you don't have access to the class' source:

    public static class UserExtensions
    {
      public static string GetFullName(this User user)
      {
          return user.FirstName + " " + user.LastName;
      }
    }
    

    For data binding we must use <asp:TemplateField>:

    <%# Eval("User.FirstName") %>
    <%# Eval("User.LastName") %>
    <%# (Container.DataItem as User).GetFullName() %>
    

    Partial classes

    Another option, available since C# 2.0, is to write a partial class, but only if the original class is also declared partial and is declared in your project (part of the same module). This approach is useful if the User class is generated with a tool, for instance if you use some kind of automatic database mapper tool in your project.

    public partial class User
    {
        public string FullName
        {
            get { return this.FirstName + " " + this.LastName; }
        }
    }
    

    For data binding we are now back to using '':

    <asp:BoundField DataField="FirstName" />
    <asp:BoundField DataField="LastName" />
    <asp:BoundField DataField="FullName" />
    

    These are all possibilities of the C# compiler and the .NET runtime, so they fall in the category of techniques instead of frameworks. Of course, basic inheritance could also be used, but it may not be applicable in your situation?

    T4 Text Templates

    If you have very specific needs about how the data bound class should look like but can't use any of the approaches above, you can always look into T4 templates in Visual Studio. (They work in ASP.NET Web Application projects but not in ASP.NET Web Site projects.)

    With these templates you can generate code at design time, for instance to create a shallow, partial class UserViewModel that maps all properties to an internal User object transparently. Then, using the partial class approach, you can add extra properties and methods to this type using another partial class declaration in a .cs file and simply data bind against your UserViewModel:

    DataGrid.DataSource = GetAllUsers().
      .AsQueryable()
      .Select(u => new UserViewModel(u));
    DataGrid.DataBind()
    

    Data bind becomes straight-forward again using <asp:BoundField>:

    <asp:BoundField DataField="FirstName" />
    <asp:BoundField DataField="LastName" />
    <asp:BoundField DataField="FullName" />
    

    Using T4 templates you could generate these custom view model classes automatically for all your domain types. When using reflection in T4 there are caveats: