Thursday, May 27, 2010

Setting up a custom Team Build project

Our company has Team Server 2008 and Team Build agents all set up. Except that no one really knows what to do with it now. We don't have unit tests and we don't do code analysis and our deployment process is quite manual. And we haven't really established what the Microsoft compatible method is for organizing our version control repository.

Microsoft products are very "in the box". By that I mean, if you are going to do what Microsoft envisioned you would do everything is very easy (if you can figure out what it is they envisioned). As soon as step outside the box you're in for a world of pain.

As with many Microsoft tools it seems that in order to understand the tool have to to first understand the tool. So, with this post, I hope to help some other newbies get started.

So, to help ease the pain (but will realizing this is the blind leading the blind) here are some get started tips.

Our scenario was to simply pull the latest version a website from TFS and deploy it to a test web server. We wanted this to happen automatically when a check in occurred. As simple as this sounds it is not "in the box".

The box is this:

  1. Optionally get the latest code from TFS (with options for incremental get, cleaning, overwritting, etc).
  2. Optionally clean the build
  3. Optionally modify the default Drop location format
  4. Build
  5. Optionally Test
  6. Optional Code Analysis
  7. Semi-optionally drop the build to a network share
I am assuming you've already set up a TFS server and a TFS Build Agent on the same or a different server. I didn't have to do that and don't anything about it.

All the configuration is handled inside Visual Studio remotely from the Build Server. If you are using TFS 2008 then you must use Visual Studio 2008 (with the Team Explorer component installed). If you are using Visual Studio 2010 with TFS 2008 you will not be able to "Manage Build Agents" which is a required piece.

Step 1: Set up the Build Agent (this all happens in Visual Studio)
  • Using the Build menu select Manage Build Agents
  • Give any display name and description
  • Enter the computer name where you installed the TFS Build Agent service
  • Enter the port (it'll probably be the default)
  • The working directory is the location out on the computer were the TFS Build Agent is installed where all the temporary files will be placed (source code, logs, build output, etc). This includes a TFS MSBuild variable thingy called $(BuildDefinitionPath). Leave this in your path so the files in this build stay separated from other builds. This variable is assigned the name of the Build Definition that you create later.
  • They give you a bunch of warnings about have sufficient disk space (since in their scenarios builds take hours and a failed build is the worse thing possible)
  • Set the Agent to Enabled.
One Build Agent can only serve a single TFS server but can build lots of different projects from that TFS server.

Step 2: Build Definition

The build definition links together the source control path (so it knows what files to get), the build agent and the network where where to drop the output files. It also helps you create a basic TFSBuild.proj project file (it's just XML) that can help you customize what happens in the automated build.

In our environment we pretty much only use the Source Control portion of TFS (we don't use Work Items, Reports or Documentation). So I have a mental mask when I connect to the TFS server in Visual Studio (Team Explorer) and always immediately open the Source Control window without ever looking at the other items. One of those items is Builds. This is where you define your Build Definitions.

Right click on Builds and choose New Build Definition from the context menu. This gives you the Build Definition Dialog:
  • Give the Build Definition a name
  • Defined a workspace. My initial workspace was full of unrelated items. Delete everything you don't need as part of the build and add only those you need. Or, you can use the Copy Existing Workspace to pull in one that you are already using in Visual Studio
  • Next create a project file. Project files are stored and executed from within TFS. So create a place in TFS to store the project files. We store ours in a separate location from our projects (a separate TeamBuild folder with a subfolder for each Build Definition). Use the Create... button to get a default TFSBuild.proj file created for you. Here you choose which solution to build. Choose your tests and code analysis. Our project didn't use any of these so you're one your own. Once you've finished the Create... wizard new TFSBuild.proj and TFSBuild.rsp files will be created in the location you specified. This TFSBuild.proj file is where you customize your build.
  • Next choose your retention policy. The output from each is kept. During your initial setup you might want to keep everything so you can review the logs. You can go back and revisit what you want to keep after everything is finished. In the end you'll probably only want to keep Failed or Partially Succeeded builds so you can review the logs.
  • Next choose the Build Agent you setup in the last step. You are also required to choose a network share where the build will be "dropped" (a place where the result will be copied to). For some reason it MUST be a network share. Also, the user you configured the Team Build Service to execute as must have Full Permission access to that share. Also, the build agent will automatically create a drop folder on the share you specify (so you can use one share for lots of different Build Definitions).
  • Choose your desired trigger. During your initial setup you probably want to select "Check-ins do not trigger a new build". The amounts to "Manual Build". This way you can tweak your automated build and manually execute it rather than being tied to some other build trigger. Later you can return and select the most appropriate option.
Now you've got a build definition. And if you just want to Build, Test, etc. you are probably done. In our case we don't want to build at all. We want to get the latest from source control and simply copy files out to the Drop location.

Step 3: Customizing the TFSBuild.proj

In order to make the Build Server do something other than the default we need to break open the TFSBuild.proj file. Without some kind of guidance this can be a nebulous void of XML. However, the project file is what is executed and little is hidden (even if nothing is obvious).

Here are some tips for dealing with this file:
  • There are some DO NOT EDIT things there and, probably, you can just ignore that.
  • There are some Backwards Compatibility lines in there. And unless you are using old TFS stuff then you can delete that (so it's not in the way). Most of that legacy stuff is now defined in the Build Definition rather than the TFSBuild.proj file.
  • Think of this file like a class file that is inheriting from another class. That parent class is Microsoft's build instructions (get from TFS, build, clean, test, drop, etc). You get all the default functionality and can override any part of it to do what you want
  • Remember that when you override something you must still accept the same inputs and provide the proper outputs if you want everything to work right in the overall system (for example, there is a specific action that you have to take if you want to indicate that the build process was successful. If you don't it will report failed even if you didn't have any errors).
I had trouble finding a list of all the things that I could override and what variables and things were available (MSDN has a lot of this info). Fortunately you can look at the "parent class" source. It is just another XML MSBuild project file. And your TFSBuild project file includes it right near the top.

It's path is: $(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets

This translates to c:\Program Files\MSBuild\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets in our environment.

Everything Team Build does is (pretty much) in that file. If you don't overwrite anything in your TFSBuild.proj file then you're looking at the code that will be executed.

There are still a lot of non-obvious things going on here. You'll see a lot of references to DesktopBuild. That is just there to throw you off. Also, since you're looking at XML and not a procedural language the execution order is not defined by the order items appear in the file.

A couple of items the differ in a TFS build vs a regular build are:
  • Entry Points
  • Variables
TFS starts by calling the Target named "EndToEndIteration". This target has several dependencies defined on it so those are executed first. In fact, EndToEndIteration doesn't actually have any code to execute. It just ensures that all its dependencies are executed in the proper order. The dependencies are listed above the Target definition (I think this is only by convention). These are the names of the several Targets that should be executed before executing the current Target.

You can override EndToEndIteration to make your own fully customized build process but since you are working in a TFS build environment you'll probably still want to execute several of the existing dependecies. InitializeBuildProperties, for example, is important as it imports a bunch of TFS settings (like paths, TFS Source Control URLS, etc) into the build so you can work from those.

Also, some activities that you will want to execute are already available. So get familiar with what is there (e.g., the "get" target pulls down the files from source control).

Defining/Overriding variables

There are two kinds of variables in these build files. The most common are defined inside PropertyGroup tags. The other kinds are called Items and I'm not sure what the difference is.

You can define your own variables like so:
<propertygroup>
<myvariable>The Value</myvariable>
<myvariable2>The Value of Var #2</myvariable2>
</propertygroup>

Later you can dereference the variables using the $() syntax:

<propertygroup>
<myvariable3>$(MyVariable) of Var #3</myvariable3>
</propertygroup>

Defining/Overriding Targets

You define (or override existing) targets using the Target tag. Your own Targets need a unique name. Override an existing target by using the existing target name. Microsoft's pre-defined targets include several that are intended to be overridden.

<target name="MyTarget">
<message text="This is my target">
</target>

A Target can hold more variables (PropertyGroup) and call other actions. Above I call the Message action. There are lots of actions available. See MSDN or Google to get some lists.

In MS' default configuration most Targets have a before and after Target you can override to do whatever you need.

Greater Customization

In our project we didn't want to build anything. We just wanted some files copied out to a development server whenever someone checked in a change. This was too outside the box to do with the default configuration.

So I ended up with the following override of EndToEndIteration to do what I wanted:

<propertygroup>
<endtoenditerationdependson>
CheckSettingsForEndToEndIteration;
InitializeBuildProperties;
InitializeEndToEndIteration;
InitializeWorkspace;
Get;
DeployWebFiles;
Messages;
</endtoenditerationdependson>
</propertygroup>
<!-- Entry point: this target is invoked on the build machine by the build agent -->
<Target Name="EndToEndIteration"
DependsOnTargets="$(EndToEndIterationDependsOn)" />


You can see that it includes several of the initialization Targets but then skips to Get and then to my own Targets.

My targets look like this:

<propertygroup>
<deploywebfilesdependson>
</propertygroup>
<Target Name="DeployWebFiles"
DependsOnTargets="$(DeployWebFilesDependsOn)">

<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Name="DeployWebFiles"
Message="Deploying Web Files">
<output taskparameter="Id" propertyname="DeployWebFilesBuildStepID">
</buildstep>

<itemgroup>
<filestocopy include="$(SolutionRoot)\NET 1.1\Trunk\Source\Web\**\*">
</itemgroup>
<Copy
SourceFiles="@(FilesToCopy)"
DestinationFiles="@(FilesToCopy ->'$(DropLocation)\%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true"
ContinueOnError="false" />

<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Id="$(DeployWebFilesBuildStepID)"
Status="Succeeded"
/>

<SetBuildProperties
TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
CompilationStatus="Succeeded"
TestStatus="Succeeded" />

<onerror executetargets="PartialSuccess">

</target>

<target name="PartialSuccess">
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Id="$(DeployWebFilesBuildStepID)"
Status="Failed"
/>

<SetBuildProperties
TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
CompilationStatus="Failed"
TestStatus="Failed" />
</target>


I found examples of copying files through Google. I had to experiment a little to discover the file structure. You can browser the folders on your Build Agent system (remember step 1?).

Setting the right outputs

Visual Studio/TFS needs certain outputs in order to understand what is going on inside your custom targets.

I used the BuildStep action to display more steps that I can see inside Visual Studio's Build Explorer so I can watch the live progress of my build. TFS automatically does some of its own build steps but you can add as many as you want. You first create a new build step and accept its output of a Build Step ID (which you save in a variable). Then you can update that Build Step's status by passing the ID (using the $() syntax) in again.

The last tricky item is getting the build to report as successful. I finally got everything working with no errors or warnings but Visual Studio still reported the build as failed.

It looks like when you allow some of MS' default targets to run it somehow updates the build status (even though I can't find where). But, to handle this manually by calling the SetBuildProperties action.

We have to set the CompilationStatus and TestStatus to the string "Succeeded" in order to get TFS to believe that the build was successful. Again, here we are outside the box and even though we aren't doing a compilation or a test we have to report them as successful.

Drop Location

The last hiccup we had was with the Drop Location. All of MS' code turns the drop location from the one we specified in the Build Definition and adds a build path to it:

//myserver/myshare/BuildDefinitionName, Date.Number/

In our case we didn't want any of that. This meant we could either overwrite the DropBuild target or we could role our own Target. Since DropBuild is meant to deploy the OUTPUT of a build and not the source files I decided to role my own. Probably in the future we will use the DropBuild target and I didn't want to confuse other team members as to what the DropBuild functionality really is supposed to be.

The final hiccup that we haven't been able to resolve is that somewhere in the whole build process the Build folder is still generated in the Drop Location and the log file is placed inside it. I'm not sure if this is a function of the TFS Build service or one of the actions called by MS' build XML code. But it is really minor and we probably won't worry about it.

The End

I hope this helps get someone started. Unless you find a good book on the subject this initial hurdle is pretty difficult to get over. Most of the documentation/books I found were much more advanced and followed the original rule that in order to understand TFS Builds you have to first understand TFS builds.

Team Build Server (2008)

The Backstory
We have grand schemes of checking in code, having it build, test, and deploy all magically. Then we can remove developer access from the web servers and prevent out of band changes.

Unfortunately, we are missing one piece: the guy; We need the guy who can make it all happen.

It should be simple. We are a Microsoft shop with Visual Studio 2010 and Team Found Server 2008. That supports the Team Build Server and it's all designed right in. But, since we don't have the guy we've made little progress in that direction.

But, we've found another reason to get team builds in place. Our content designers (who are in a separate department) also have access to the web servers and have become our "analog hole". We've implemented change control processes and they have not. So we are working to get them on board. The biggest problem is: they use Macs and don't use Visual Studio.

We were pleased when Microsoft announced Team Foundation Server 2010 and its acquiring of the Teamprise software. Our software license would now include our Mac users and they could begin checking in to TFS. This turns out to be a real pain for them.

They use their own test web server where they build their content. When it's ready they copy the files out to the production web server. Now, in the TFS process, they have to 1) check out the files they will change, 2) copy them out to their test web server, 3) make their changes, 4) copy them back to their TFS workspace, 5) check the files back in, 6) ask our deployment guy to deploy it to Pre-production, 7) make sure it looks good in Pre 8) ask our deployment guy to deploy it to Production.

So, although we are now all synchronized and have regained control over the changes made to production we've totally given the content design team the shaft. They need to be much more dynamic that what the new process allows for.

So, the compromise is to set up the build server to automatically deploy to Pre-production when they check in content changes. They will worry about streamlining the rest of their process.

So, I became the guy who gets to set up Team Build. See the next post for details of the nightmare.

Friday, January 29, 2010

What's really wrong with the iPad

Apple has announced their iPad. Besides the stupid name and sophomoric jokes most reviews don't seem to care for the device.

What I don't get is why they don't get what the iPad is. They review it wishing the whole time it was Netbook. They want it both ways and just don't seem to get it.

You can't have the extreme ease of the iPhone and the total flexibility of a general purpose computer.

What the iPad (iPhone, iPod Touch) has going for it is:

  • Full size screen
  • Multi-touch
  • No file management (this is the bane of computer illiterates)
  • Simple application management
  • No booting
  • One piece
  • Multimedia
"Yeah, but a Netbook can do that and so much more!". Well, yeah, but a Desktop can do that and so much more. The argument doesn't hold water.

You don't need the Flash Player to experience the web. I can't believe they even argue this. The next generation browsers will be powerful enough to eliminate the need for the Flash Player in 90% of cases anyways, and mobile Safari is already on track there. It's not that far away. Any complaints along this vain really mean, "I can't watch Netflix or Hulu on the iPad." The XBox, PS3, and Wii don't have the Flash Player (at least not one worth speaking of) but they stream Netflix. If there is a market for it the iPhone/iPad will get it.

You don't need a physical keyboard to browse the web or to read eBooks. The iPhone has shown you don't need one to play games. Leaving out the keyboard keeps the form factor nice. And with multi-touch the virtual keyboard is usable.

You don't have to use eInk to be usable out doors. Besides, how much time do these reviewers really spend in full bright sunlight. You just need to turn up the brightness (more on this later).

You don't need a camera. This isn't a mobile device. It's a cordless, not a cell (phone). Perhaps a future version will have a forward facing web cam but I don't think that will fly with AT&T who seems to want you to pay for that data plan but not to use it.

You don't need a file system. This device is accessible to everyone. Mom won't lose her files because she saved them in Program Files instead of My Documents; She won't be downloading spyware or viruses; She'll always know where all her images are; She'll always know where her music and audiobooks are; She'll get the Mood Ring app and install it and run it and she'll have done it all by herself.

What the iPad missed:

Inter-application data sharing isn't limited to copy/paste. The iPad doesn't need a file system but it does need a way to email attachments from a variety of apps without each app having build out it's own "Email This" functionality. Any mp3 player I install should work with any music I've sync'd. Any ebook reader should be able to find any ebooks from any other ebook reader (among compatible formats). The Palm Pilot managed to do this more than 10 years ago. I think the iPad could swing it.

We want to print. I know the home printer world hasn't quite gone networking yet but, you're Apple, figure it out.

We want to share. I have an iPad, my wife likes her Kindle and the kid has an Android device. Make the ePub ebook format really open and let us read the books on any of our devices. Let iTunes sync with other devices even if it's only in a limited way. As much as you'd like it to be it's not an Apple world.

We need multi-tasking. Even if it's not what multi-tasking is on a Desktop. I need enough RAM (I'm looking at you iPod Touch 1g) and a UI for switching apps without exiting. Even if background apps are suspended. I want to switch between my eBook reader, email, web-browser and contact list without losing my place or waiting for the app to exit and start up again. If we can get it we want to full multitasking where that web page can keep loading while I switch over and finish composing that email.

We need a faster way to change the settings. If I do go out in the sunlight I need to be able to quickly adjust my brightness without exiting my eBook reader and going through 10 menus in the Settings app. Same goes for volume, networking and many of the other settings.

I want a mouse. The iPad supports a keyboard; We want some other peripherals. Let mice, headphones and whatever else work. This would be a boon for games and many other apps. In fact throw a CD/DVD burning device in there. And while we're at it where is the expandable storage. And internal microSD/SD card reader is a must. The Nintendo's Wii manages this and seems to handle piracy okay. You should follow suite.

I can handle the price. What I can't handle is that Amazon managed to get me free internet access for life and you didn't. I don't need another monthly bill. Convince AT&T to share my existing data plan with both my phone and iPad contributing toward my monthly limit. (Having the iPad is like having a separate electricity bill for each appliance in my house.) Give me some small amount for free and let me upgrade to the $20 or $30 plans for higher limits.


I hope the iPad finds it's niche. A lot of computing could go this closed route and I think it would be good for a lot of home users. Power users and businesses are always going to need the more open, powerful, and flexible systems we have today. But mom just can't handle it on her own. She needs an iPad (almost).

Monday, June 08, 2009

Vista becomes self aware


Hoho haha. Yeah. Having never used Windows ME, Vista x64 has been the most disappointing operating system from Microsoft that I have used. If only the 64 bit XP edition had turned out better.

Here's to hoping Windows 7 will be the new XP!

Thursday, May 14, 2009

Email Casualty

Back when Gmail was still invite only and pretty new I managed to get an invitation and get my name without any numbers or symbols or abbreviations. It was great. The kind of email address you always hope for.

Now, years later I'm starting to regret it. There seem to be lots of Jakes out there and they keep using my email address.

It's been manageable up until today when I was forwarded some dirty cell phone pics obviously meant for someone else's Jake. I'm ready for a jake123abc@obscure.example.com so I don't get drive by emailed any more.

Here are some other examples of emails I've received:

  • Some random dudes vacation photos, on two separate occasions.
  • Paypal Signup - I was only momentarily tempted to make a purchase. I stopped getting these so I suppose the account owner realized their email mistake.
  • Adult Friend Finder - I had to go deactivate that one, I didn't want any more of those.
  • YouMail - a voice mail service, BTW, Jake...your coach left you a message. I logged in and changed the email address on that one, it accepted nomail@example.com. Don't you love it when they send your temporary password in an email!
  • Wine Dog - I'm just filtering this one until I decide to become an alcoholic.
  • And finally, I, a guy without a cellphone, got sexted. I guess you could say that's the nipple that broke the camel's back.
So, next time you're signing up for an email address consider it a blessing when your first choice isn't available and you have to pick xyz123abc2009@crazymail.example.com.

P.S. Teenagers -- If you don't want your dirty pictures posted all over the Internet for the whole world to see DON'T TAKE THEM!



HTML Controls in ASP.Net

The more I use ASP.Net the more I move toward (almost) plain old HTML.

For example:

<asp:Panel id="myPanel" runat="server">
I am in a Panel
</asp:Panel>
<div id="myDiv" runat="server">
I am in a div
</div>

I used to use Panels to conveniently show and hide blocks but I found that I've slowly been moving to using DIV tags with the runat="server" attribute. What's the difference? Well, with a div I get to see and control my markup. With the Panel I don't really know what is going to be rendered. I also get to apply CSS in a way that is consistent with the way I'm applying it to the rest of the page. It can be tricky to apply CSS to ASP.Net controls since I don't know what HTML they might render.

I've found that I don't need to be shy about using runat="server" on regular everyday HTML tags. I use it on TR tags for easy show/hide or server-side modification of attributes (like class or style).

I've also found that using runat="server" on input controls is often much more desirable than using the ASP.Net equivalents.

<asp:checkbox id="myCheckbox" Text="Check Me Out!" runat="server" />
<input type="checkbox" id="myInputCheckbox1" value="10" runat="server" /><asp:Label AssociatedControlID="myInputCheckbox1" runat="server">Check Me Out!</asp:Label>
<input type="checkbox" id="myInputCheckbox2" value="10" runat="server" /><label for="<%=myInputCheckbox2.ClientID%>">Check Me Out!</label>

Why use one over the other? I guess it just depends on what I want to do. The Checkbox control doesn't have a Value property and the HtmlInputCheckBox does. However, the HTML version doesn't have a Text property and I have to provide my own label. HtmlInputCheckBox gives my control over the markup but doesn't provide AutoPostBack.

This is also why I prefer the Repeater over controls like the DataList or GridView. With the Repeater I have control over the markup. And with some help libraries getting things done on the ItemDataBound event is pretty easy.

Just remember that you have options and that even in ASP.Net your run of the mill HTML tags can be empowered as well.

AJAX.Net

In the past (and in the present) I have used a custom built Ajax library for making my Ajax calls and handling the responses. It's simple, supports plain text, XML and JSON, and handles multiple requests, cancellations and errors properly.

In the less distant past I began using ASP.Net Update Panels and AJAX.NET libraries. In some cases they are convenient and easy. But I'm finding in most cases they are buggy, extremely limited and implementation doesn't really fit well with my style.

Now...most likely all my AJAX.Net difficulties arise from my limited experience with the library and my failure to familiarize myself properly with all the ins and outs of implementing it in my sites.

Anyhow, given my experience I recommend against using AJAX.Net and update panels and recommend sticking with a client side library like those found in jQuery or MooTools.

Here is a list of problems that make implementing and using basic AJAX.Net Update Panels difficult:

  • Updates are too heavy. The view state has to go back and forth. Even just to update one word on the page.
  • Inflexible when you move outside the box. For example, I only want to trigger an update panel update if there are 4 or more characters typed in a box. Now I have to implement my own client side event handlers and trick AJAX.Net in to making the update. If I have to do all that anyways what savings is AJAX.Net giving me?
  • Out of order responses. I often see out of order responses. Faster executing updates return before slower executing updates regardless of the order I called them in. Most often I see this when the application decides it has to restart and the longer executing first call often returns after the second call.
  • Poor error handling. I haven't found a way to gracefully handle errors. I can catch some but it doesn't seem to catch them all.
  • Dies after 1 error. If I have 1 AJAX.Net error all AJAX.Net stops responding and no panels will update until the page is refreshed.
  • Updates die for no good reason. I have a text field that triggers an update after some keystrokes. But if the user presses tab to move to the next field between the time when the update starts and when it returns then the update hangs. Monitoring the application shows that it does process the request but the client side just ignores it. I guess it has ADD.
  • Loss of state. Since an update panel nukes the contents of a div and replaces with what it just got back from the server I lose some UI state. If I've done any client side tweaking that dissapears but most often I just lose focus on the control I just changed. This is a nuisance for those that navigate the form with the tab key.
So, needless to say, I've gone back to my custom Ajax JavaScript library. I never have any of the problems described above. Some things are a little more work but the reliability is worth it.

To be fair here are the issues with using my own Ajax library:
  • Everything is difficult (not that difficult) because even simple updates require full programming unlike UpdatePanels which just need the markup and event handler.
  • Manual management. I have to remember to cancel outstanding requests so things don't get out of order and I have to handle updating the page with the response. It's just more work.
  • Non-standard. Good luck future developer who gets to maintain my project. I've coded it well but it's all custom so your experience with standard libraries won't help you here.
  • Full post backs. I'm a little lazy and so I just create a separate ajax.aspx page that handles my ajax requests and makes the expected response. It's not very ASP.Net-y and is probably pretty heavy since I'm doing a complete page request. In fact, this method takes me outside all of ASP.Net's help. I don't have access to controls or events. I have to pull data from Request.Form process it and respond. I sometimes end up with duplicate code and duplicate markup (especially if I'm being lazy).

Even given those issues I still like my library because of the fine grained control it gives me.