Functional Construction for ASP.NET Web Forms
System.Xml.Linq (a.k.a. LINQ to XML) introduces a nifty approach to creating XML elements called functional construction.
I’m not entirely sure why they call it functional given that
constructing an object graph is a decidedly non-functional task in the
traditional sense of the word, but I digress.
Functional construction has three key features:
- Constructors accept arguments of various types, handling them appropriately.
- Constructors accept a
params
array of typeObject
to enable creation of complex objects. - If an argument implements
IEnumerable
, the objects within the sequence are added.
If you haven’t seen it in action, I encourage you to take a look at
the examples on MSDN and elsewhere—it really is pretty slick. This post
will show how a similar technique can be used to build control trees in
ASP.NET web forms (and probably WinForms with minimal adjustment).
Basic functional construction can be implemented using two relatively simple extension methods:
public static void Add(this ControlCollection @this, object content)
{
if (content is Control)
@this.Add((Control)content);
else if (content is IEnumerable)
foreach (object c in (IEnumerable)content)
@this.Add(c);
else if (content != null)
@this.Add(new LiteralControl(content.ToString()));
}
public static void Add(this ControlCollection @this, params object[] args)
{
@this.Add((IEnumerable)args);
}
We handle four cases:
- Control? Add it.
- Sequence? Add each.
- Other value? Add literal.
- Null? Ignore.
And our params
overload just calls its arguments a sequence and defers to the other.
In the time-honored tradition of contrived examples:
Controls.Add(
new Label() { Text = "Nums:" },
" ",
from i in Enumerable.Range(1, 6)
group i by i % 2
);
This would render “Nums: 135246”. Note that the result of that LINQ
expression is a sequence of sequences, which is flattened automatically
and converted into literals. For comparison, here’s an equivalent set
of statements:
Controls.Add(new Label() { Text = "Nums:" });
Controls.Add(new LiteralControl(" "));
foreach (var g in from i in Enumerable.Range(1, 6)
group i by i % 2)
foreach (var i in g)
Controls.Add(new LiteralControl(i.ToString()));
Hopefully seeing them side by side makes it clear why this new method of construction might have merit. But we’re not done yet.
Expressions, Expressions, Expressions
Many language features introduced in C# 3.0 and Visual Basic 9 make
expressions increasingly important. By expressions I mean a single
“line” of code that returns a value. For example, an object initializer
is a single expression…
var tb = new TextBox()
{
ID = "textBox1",
Text = "Text"
};
… that represents several statements …
var tb = new TextBox()
tb.ID = "textBox1";
tb.Text = "Text";
That single TextBox expression can then be used in a number of
places that its statement equivalent can’t: in another object
initializer, in a collection initializer, as a parameter to a method,
in a .NET 3.5 expression tree, the list goes on. Unfortunately, many
older APIs simply aren’t built to work in an expression-based world. In
particular, initializing subcollections is a considerable pain.
However, we can extend the API to handle this nicely:
public static T WithControls<T>(this T @this, params object[] content) where T : Control
{
if(@this != null)
@this.Controls.Add(content);
return @this;
}
The key is the return value: Control in, Control out. We can now
construct and populate a container control with a single expression.
For example, we could build a dictionary list (remember those?) from
our groups:
Controls.Add(
new HtmlGenericControl("dl")
.WithControls(
from i in Enumerable.Range(1, 6)
group i by i % 2 into g
select new [] {
new HtmlGenericControl("dt")
{ InnerText = g.Key == 0 ? "Even" : "Odd" },
new HtmlGenericControl("dd")
.WithControls(g)
}
)
);
Which would render this:
- Odd
- 135
- Even
- 246
Without the ability to add controls within an expression, this
result would require nested loops with local variables to store
references to the containers. The actual code produced by the compiler
would be nearly identical, but I find the expressions much easier to
work with. Similarly, we can easily populate tables. Let’s build a cell
per number:
Controls.Add(
new Table().WithControls(
from i in Enumerable.Range(1, 6)
group i by i % 2 into g
select new TableRow().WithControls(
new TableCell()
{ Text = g.Key == 0 ? "Even" : "Odd" },
g.Select(n => new TableCell().WithControls(n))
)
)
);
In a future post I’ll look at some other extensions we can use to
streamline the construction and initialization of control hierarchies.