The CodeExpressionBuilder
UPDATE: THIS POST HAS BEEN MOVED TO MY NEW BLOG! Please redirect your bookmarks here:
http://weblogs.asp.net/infinitiesloop/archive/2006/08/09/The-CodeExpressionBuilder.aspx
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 ResourcesExpressionBuilder hard at work. Nice!
There's also a ConnectionStringsExpressionBuilder 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 %>"/>
Wow.
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="lbl1" 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 for purposes of this example, lets assume that doesn't work for you. 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, I even eluded to it in my previous blog post. That will work. 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!
It seems like it should be a common need... all you want is to assign the default value for a control to something a little dynamic. Something that can't be represented with a literal. Something with some logic behind it. 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?
Here's a better example. You want, for some reason, 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). It doesn't matter why exactly. 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 suck the serialized string down the pipe, then making them push it back down 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, good luck getting that to work. It won't.
The CodeExpressionBuilder comes to the rescue! Another great thing about ExpressionBuilders is that you can create 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. It seems like such a useful thing to have, I don't know why it isn't included as a built in feature.
The trick is to use the CodeDom's CodeSnippetExpression to convert the given string into a CodeExpression. Here's the entire class:
[ExpressionPrefix("Code")]
class CodeExpressionBuilder : ExpressionBuilder
{
public override object ParseExpression(string expression,
Type propertyType, ExpressionBuilderContext context)
{
return expression;
}
public override CodeExpression GetCodeExpression(BoundPropertyEntry entry,
object parsedData, ExpressionBuilderContext context)
{
return new CodeSnippetExpression(parsedData.ToString());
}
}
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". Here it is:
<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!
You can use any code expression want:
<%$ 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? I just figured out this thing tonight and I'm all happy about it. I'm curious why this isn't a built in expression builder, its so simple yet so useful. Perhaps there's a good reason I haven't thought of. It would be awesome if an asp.net team member could provide their insight :) One of these days I hope to lure one of them into this site...
HAPPY CODING.
4 Comments:
A simple form with a CheckBox and Button produces a ViewState of 108 chars:
<asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" />
<asp:CheckBox id="chk1" runat="server" Text="design" />
Adding a method for Button1_Click event produces a ViewState of 152 chars on postback:
protected void Button1_Click(object sender, EventArgs e)
{
this.chk1.Text = "button";
}
Similarly, dynamically modifying the text by overriding the OnLoad method produces a ViewState of 152 chars as expected;
protected override void OnLoad(EventArgs e)
{
this.chk1.Text = "onload";
base.OnLoad(e);
}
Testing the concepts introduced in this article, I removed the OnLoad procedure and added a new class to the App_Code directory implementing ExpressionBuilder;
namespace Test
{
public class CodeExpressionBuilder : ExpressionBuilder
{
public override object ParseExpression(string expression, Type propertyType, ExpressionBuilderContext context)
{
return expression;
}
public override CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
{
return new CodeSnippetExpression(parsedData.ToString());
}
}
}
The class was registered in Web.Comfig:
<compilation debug="true">
<expressionBuilders>
<add expressionPrefix="Code" type="Test.CodeExpressionBuilder"/>
</expressionBuilders>
</compilation>
And the checkbox was modified to use the Expression Builder, <%$ ... %>.
<asp:CheckBox id="CheckBox1" runat="server" Text='<%$ Code: "expbld" %>' />
The resulting ViewState is 116 chars, suggesting this technique adds something to the ViewState, but is more efficient than traditional Dynamic property assignment.
Any comment on what may be happening here?
I don't get the same results. I get the exact same viewstate size and even the content of it is identical. I think you have a difference because the ID of the checkbox is different when you are using the code expression builder. Run your example again (with no onload or button_click) and simply change the text attribute back and forth between literal and expression builder, keeping the ID the same.
Apparently the checkbox is saving something into viewstate, but it isn't the text, because you can increase the size of that text all you want and viewstate is unaffected. So checkbox is saving something else in viewstate (whatever that is), and the viewstate is bigger in your example because the ID is longer.
I'm currently working on a mechanism that will replace the need of databinding expressions which as you describe need the call to the DataBind method. I'm using expression builders to make it work one way i.e.: it is currently readonly. You can bind a property from a page to a property on a control. I have an idea on how to make it work both ways but it currently requires some code behind which I don't like. I will post the solution once it is ready.
The class you added -- is the name of the type spelled exactly like that, including the casing? Is it within a namespace? Are there any other compilation errors?
Post a Comment
<< Home