Thursday, January 26, 2012

Binding UI controls using Fluent API

Hi guys,

I've been working on ASP.NET Web Forms application for a while and have been constantly writing Data Binding on all kinds of containers - repeaters, list views, grid views and etc.

Let's imagine a very simple scenario. We have a Contact Class and Address Class. You get the Address of the Contact, using the Contact Id. In most cases, these objects will come from the databases, but for the purposes of this post, we don't need any complications. Therefore here are my classes:
namespace FluentBindingSample.Data
{
public class Address
{
public int ContactId { get; set; }
public string Country { get; set; }
public string City { get; set; }
public int PostalCode { get; set; }

public Address()
{
}
}
}
and
namespace FluentBindingSample.Data
{
public class Contact
{
public int ContactId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }

public Contact()
{
}
}
}

Now, here is a sample data provider, which will help us retrieve Contacts and retrieve data (currently, hardcoded):
using System;
using System.Collections.Generic;
using System.Linq;
using FluentBindingSample.Data;

namespace FluentBindingSample.Model
{
public class SampleDataProvider
{
public static IEnumerable< Contact > GetSampleContacts()
{
yield return new Contact { ContactId = 1, FirstName="Leo", LastName="Messi" };
yield return new Contact { ContactId = 2, FirstName = "Wayne", LastName = "Rooney" };
yield return new Contact { ContactId = 3, FirstName = "Christiano", LastName = "Ronaldo" };
}

public static Address GetAddressByContactId(int contactId)
{
switch (contactId)
{
case 1:
return new Address { Country = "Argentina" };

case 2:
return new Address { Country = "England" };

case 3:
return new Address { Country = "Portugal" };

default:
throw new NotSupportedException();
}
}
}
}
We're ready with our sample Business Layer. Now, let's bind this data to a Repeater:
<asp:Repeater ID="rptContats" runat="server">
<HeaderTemplate>
<table>
<thead>
<tr>
<th>
Contact Id
</th>
<th>
First Name
</th>
<th>
Last Name
</th>
<th>
Country
</th>
</tr>
</thead>
<tbody>
</HeaderTemplate>
<ItemTemplate>
<tr>
<td>
<asp:Literal ID="ltrContactId" runat="server" />
</td>
<td>
<asp:Literal ID="ltrFirstName" runat="server" />
</td>
<td>
<asp:Literal ID="ltrLastName" runat="server" />
</td>
<td>
<asp:Literal ID="ltrCountry" runat="server" />
</td>
</tr>
</ItemTemplate>
<FooterTemplate>
</tbody>
</table>
</FooterTemplate>
</asp:Repeater>
We've finally set up our sample - we've reached the main point of this article. What I have always found boring is implementing an ItemDataBound event, getting all controls, casting and initiating them. I think this is a common problem and the code looks like this:
private void rptContats_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
{
Contact contact = e.Item.DataItem as Contact;

Literal ltrContactId = e.Item.FindControl("ltrContactId") as Literal;
Literal ltrFirstName = e.Item.FindControl("ltrFirstName") as Literal;
Literal ltrLastName = e.Item.FindControl("ltrLastName") as Literal;
Literal ltrCountry = e.Item.FindControl("ltrCountry") as Literal;

ltrContactId.Text = contact.ContactId.ToString();
ltrFirstName.Text = contact.FirstName;
ltrLastName.Text = contact.LastName;

Address address = SampleDataProvider.GetAddressByContactId(contact.ContactId);
ltrCountry.Text = address.Country;
}
}

You see, we get all controls. Cast them, initialize them. Get some other things, related to the object and bind them too.
What are the problems of this approach - we do not have any code reusability. In case I want to show some Contact details in another Template Control I'll have to copy paste my whole code. The code is bloated - we say few things with much lines.
How can we improve the current situation? What I've come to useful in my projects is to implement a Fluent API. Let us see, how can Fluent API improve our code.
We first have to create the Interface of the API, itself. For our Contact and Address classes, this can look like this:
using System;
using System.Linq;
using System.Web.UI;

namespace FluentBindingSample.Model.Binders
{
public interface IContactBinder
{
IContactBinder WithTextControl(Control control, string controlName);

IContactBinder BindFirstName();

IContactBinder BindLastName();

IContactBinder BindContactId();
}
}

and
using System.Web.UI;

namespace FluentBindingSample.Model.Binders
{
public interface IAddressBinder
{
IAddressBinder WithTextControl(Control control, string controlName);

IAddressBinder BindCountry();
}
}

The implementation of this Fluent APIs is quite straight forward, too:
using System;
using System.Linq;
using System.Web.UI;
using FluentBindingSample.Data;

namespace FluentBindingSample.Model.Binders
{
public class ContactBinder : IContactBinder
{
private readonly Contact contact;
private ITextControl textControl;

private ContactBinder()
{

}

public ContactBinder(Contact contact)
{
if (contact == null)
{
throw new ArgumentNullException();
}

this.contact = contact;
}

public IContactBinder WithTextControl(Control control, string controlName)
{
ITextControl textControl = control.FindControl(controlName) as ITextControl;

this.textControl = textControl;

return this;
}

public IContactBinder BindFirstName()
{
textControl.Text = contact.FirstName;

return this;
}

public IContactBinder BindLastName()
{
textControl.Text = contact.LastName;

return this;
}

public IContactBinder BindContactId()
{
textControl.Text = contact.FirstName;

return this;
}
}
}

and
using System;
using System.Linq;
using System.Web.UI;
using FluentBindingSample.Data;

namespace FluentBindingSample.Model.Binders
{
public class AddressBinder : IAddressBinder
{
private readonly Address address;
private ITextControl textControl;

private AddressBinder()
{

}

public AddressBinder(Address address)
{
if (address == null)
{
throw new ArgumentNullException();
}

this.address = address;
}

public IAddressBinder WithTextControl(Control control, string controlName)
{
ITextControl textControl = control.FindControl(controlName) as ITextControl;

this.textControl = textControl;

return this;
}

public IAddressBinder BindCountry()
{
textControl.Text = address.Country;

return this;
}
}
}

Now we've come to the sweet part. Integrating the Fluent API in the Item Data Bound event:
private void rptContats_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
{
Contact contact = e.Item.DataItem as Contact;

IContactBinder contactBinder = new ContactBinder(contact);
contactBinder.WithTextControl(e.Item, "ltrContactId").BindContactId();
contactBinder.WithTextControl(e.Item, "ltrFirstName").BindFirstName();
contactBinder.WithTextControl(e.Item, "ltrLastName").BindLastName();

Address address = SampleDataProvider.GetAddressByContactId(contact.ContactId);
IAddressBinder addressBinder = new AddressBinder(address);
addressBinder.WithTextControl(e.Item, "ltrCountry").BindCountry();
}
}

Now - this is much shorter, more readable and reusable. If I need to bind such information elsewhere, I have the functionality to do it and can just reuse it.
When can we use such approach?
- If we want to reuse code.
- If we want to encapsulate some logic - for instance formatting of the text, default values and etc.
What's the cost? We have to create Fluent API for the objects - this means little more developer effort in the beginning, which can be paid off later. It all depends on the case. If you have complex objects and functionality with formatting, default values and etc, I think such approach will armor you better than the simpler way.

The solution we outlined in this blog post can be downloaded from here:
Fluent Binding API Sample
Hope you found some ideas for yourselves in this post.

Happy Coding!

2 comments:

Alexander Vakrilov said...

Hi there. Kudos for the great blog ;)
I like your approach but in this case I would use a simpler solution with an extension method. Here is how it goes:

public static void BindChildTextControl(this Control parent, string childControlName, string data)
{
ITextControl textControl = parent.FindControl(childControlName) as ITextControl;
textControl.Text = data;
}

and the code in the DataBound event will go like this:

Contact contact = e.Item.DataItem as Contact;

e.Item.BindChildTextControl("ltrContactId", contact.ContactId.ToString());
e.Item.BindChildTextControl("ltrFirstName", contact.FirstName);
e.Item.BindChildTextControl("ltrLastName", contact.LastName);

Address address = SampleDataProvider.GetAddressByContactId(contact.ContactId);
e.Item.BindChildTextControl("ltrCountry", address.Country);

So we have some code reuse, but still we have to specify the names of the child controls. I don't see a way to get rid of that, except if there is some kind of naming convention for the template.

cypressx said...

Hi man,

you're more than correct. The sample I've provided doesn't actually show the benefits of the approach. It's too complex, for few columns of direct property binding.
The Fluent API itself, doesn't have much benefits from extending the class with computed properties, but I wouldn't like to have UI stuff in the Data objects (even in a different assembly).
These are some of the cases, I would consider using the Fluent API:
1. It's testable - if you are a TDD freak, you can test exactly what you'll be outputting. (not talking about plain string properties, but properties applied formatting, localization and etc.)

2. You have default values:

if(!string.IsNullOrEmpty(contact.AvatarUrl))
{
imgAvatar.ImageUrl = contact.AvatarUrl;
}
else
{
imgAvatar.ImageUrl = Constants.AvatarUrl;
}
In this case, with the fluent API, you'll have something as simple as:
e.Item.WithImage(e.Item, "imgAvatar").BindAvatar();
You'll be able to reuse it everywhere you need to show the Users Avatar.

3. Now this case is uglier. Let's suppose you've bought a product and received support with it. You have some "days left" from your support. When you show it to the user, you have to state exactly what does he posses.

string supportLeft = string.Empty;
// in the Order object, we have DaysLeft property
int daysLeft = myOrder.DaysLeft;
if(daysLeft == int.MaxValue)
{
supportLeft = "unlimited support";
}
else
{
if(daysLeft <= 0)
{
supportLeft = "Your support has expired";
}
else
{
supportLeft = string.Format("You have {0} days support left", daysLeft);
}
}
Now in this case, I'd prefer using the Fluent API - everywhere I need to show the user how much support he has, I'd use the Fluent Method and don't worry, if tomorrow someone wants to change the "unlimited support" to "forever", because the developer will have to change it in one place only.

Sorry for the long comment:) As you know, all these things can be accomplished without Fluent API, it's up to you to decide which way you prefer.

Thanks :)