The CodeExpressionBuilder
A very exciting new feature in ASP.NET 2.0 is Expression Builders. Expression builders allow for some pretty interesting interaction with the ASP.NET compilation model.
For example, new to ASP.NET 2.0 is the ability to reference appSettings declaratively. Lets say you wanted the text of a button to be based on a value in the appSettings section of your web.config. Piece of cake:
<asp:Button id="cmdSubmit" runat="server" Text="<%$ appSettings: ButtonText %>" />
This is made possible by the built in AppSettingsExpressionBuilder. Lets say you wanted to display a localizable string, which is stored as a resource. In ASP.NET 1.1 it was sort of a pain. No longer:
<%$ resources: ResourceKey %>
That's the ResourceExpressionBuilder hard at work. Nice!
There's also a ConnectionStringExpressionBuilder so you can refer to Connection Strings defined in the new connection strings section in the web.config. That is extremely helpful when working with the new declarative data controls:
<asp:SqlDataSource id="data1" runat="server" ConnectionString="<%$ ConnectionStrings: MyConnectionString %>"/>
Pretty useful.
Now lets take it one step further. Have you ever seen this exception?
The dreaded "Server tags cannot contain constructs" exception.
That's because you probably tried to do something like this:
<asp:Label id="lbl1" runat="server" Text=<%= CurrentUserName %> />
Perhaps you tried to fix it by putting it in quotes:
<asp:Label id="Label1" runat="server" Text="<%= CurrentUserName %>" />
... Only to be thwarted once again, as the literal text, including the <%= %> construct, ended up in the page:
Putting <%= %> in quotes doesn't help much.
If all you want to do is show the result of this code as a string, you could quite simply just get rid of the label:
<%= CurrentUserName %>
That would work. But perhaps you need the label server control for some other reason, or perhaps you need to set a string property of some other type of server control in this way.
The problem is you have incorrectly (although intuitively) tried to assign the property of a server control using the <%= %> construct. Unfortunately that is simply not supported by ASP.NET, 1.1 nor 2.0. If you ask around about your problem, someone may tell you that you will have to convert to using the <%# %> databinding construct instead. That is advice that I have given myself. But it requires that you are calling DataBind() on the control, AND, it will cause you to BLOAT VIEWSTATE... and you KNOW how much I hate bloating viewstate!
You can just go ahead and assign the value in your code-behind, say.. in the OnInit method.. but that too will bloat viewstate. I'm afraid there's no super simple solution unless you don't mind disabling viewstate on that control. That may work in some scenarios, but sometimes you really need the ViewState enabled! What's a web developer to do?
Let me back up a little and give a more concrete example. You want the text value of a CheckBox to be the current date and time. For whatever reason, you can't disable viewstate on the CheckBox (say, because you need the CheckChanged event, which doesn't work without viewstate). How on Earth are you going to get the current date and time into the Text property on the CheckBox, WITHOUT BLOATING VIEWSTATE? By "Bloating ViewState", I mean causing data to become serialized in the __VIEWSTATE hidden form field when it isn't necessary to begin with. There's no reason to put the current Date and Time into serialized viewstate, is there? It's going to be reassigned on the next request. You'd just be making ASP.NET serialize it, then making the user's browser pull the serialized string down the pipe, then making them push it back up the pipe to the server on a postback, then making ASP.NET deserialize the value -- ONLY FOR IT TO BE REASSIGNED? How rude! How wasteful and inefficient. For a single control its not a big deal, what's a few bytes? But that is not a path you want to start down my friend... with that kind of mantra, your web forms will quickly grow a viewstate tumor the size of a Borg cube! Ideally, you want the functional equivalence of this:
<asp:CheckBox id="chk1" runat="server" Text="<%= DateTime.Now %>"/>
That is ideal, because the ViewState StateBag is not tracking changes when ASP.NET assigns declared attributes, so our beloved ViewState remains optimized. And, we didn't even have to disable ViewState. AND we can do it declaratively! Woohoo! Right. Well, it won't work.
The CodeExpressionBuilder comes to the rescue! Another great thing about ExpressionBuilders is that you can roll your own. So, I created a CodeExpressionBuilder, one that allows you to use raw code to assign values to control properties. Using the CodeExpressionBuilder you can do this:
<asp:CheckBox id="chk1" runat="server" Text="<%$ Code: DateTime.Now %>"/>
Now that is nice. And to think the darn thing is only a few lines of code! The trick is to use the CodeDom's CodeSnippetExpression to convert the given string into a CodeExpression. Here's the entire class:
[ExpressionPrefix("Code")]
public class CodeExpressionBuilder : ExpressionBuilder {
public override CodeExpression GetCodeExpression(BoundPropertyEntry entry,
object parsedData, ExpressionBuilderContext context) {
return new CodeSnippetExpression(entry.Expression);
}
}
To use it, or any custom ExpressionBuilder for that matter, you must register it in the web.config expressionBuilders section. Now... how you do this part sort of depends on how your project is setup. If you have a standard ASP.NET Web Site project, then you will be defining the CodeExpressionBuilder class in the app_code directory, and the "type" will just be "CodeExpressionBuilder". However, if you are creating a Web Application Project (read about it here), then the CodeExpressionBuilder is just another class in your project, with its own namespace. For that you will need to define the whole type string (or, if you define it in a reusable library, you'll need the fully qualified type and assembly name). In my case that is "Infinity.Web.Compilation.CodeExpressionBuilder". Like this:
<compilation debug="true">
<expressionBuilders>
<add expressionPrefix="Code" type="Infinity.Web.Compilation.CodeExpressionBuilder"/>
</expressionBuilders>
</compilation>
And to see it in action:
<asp:CheckBox id="chk1" runat="server" Text="<%$ Code: DateTime.Now %>" />
ExpressionBuilders are truly a thing of beauty! Use any expression you want!
Also try:
<%$ Code: DateTime.Now.AddDays(1) %>
<%$ Code: "Hello World, " + CurrentUserName %>
<%$ Code: CurrentUserName.ToUpper() %>
<%$ Code: "Page compiled as: " + this.GetType().AssemblyQualifiedName %>
Just be careful what combination of quotes you use. If you have literal strings in your code expression like in two of the above examples, you will need to use single quotes (') to surround the entire <%$ %> expression if it is within a server control declaration. If you don't you will get the "Server tag is not well formed" error.
In the beginning I said ExpressionBuilders were an interesting way to plug into the ASP.NET compilation model. Well this really illustrates that... put a break point in the expression builder, and debug the page. You will hit the break point once, and only once, even after you refresh the page several times. The Date and Time will continue to update, but the expression builder breakpoint will only activate the first time you hit the page. The reason is because the ExpressionBuilder is used when ASP.NET compiles the page. Once the page is compiled, that's it. That's the reason why ExpressionBuilder returns a CodeExpression, and not an actual object. In essence, the builder tells ASP.NET what code it needs to run to get the value, instead of giving it the actual value. It's the old adage, teach a man to fish, and he eats forever. How geeky is that? Too cool.
PS: The one thing this ExpressionBuilder doesn't do... it won't work in "No-Compile" pages. Seems like a reasonable limitation.
Questions? Comments? Random odd facts?
HAPPY (dynamic) CODING!
UPDATE 09/05/2006: Thanks to the tip from Kelly, made the code even shorter. Also fixed a typo in the web.config snippet. The "Type" attribute should be lower case, "type".