Scott Hanselman

Hanselminutes.com: Rewriting a 5 year old VB.NET WebForms application as an ASP.NET Web Pages application with Razor

September 27, '11 Comments [24] Posted in ASP.NET | VS2010 | WebMatrix
Sponsored By

I'm planning on updating the Hanselminutes.com website to look more modern and look more like this newly redesigned blog. Until then, one of the things I wanted to try was to update the application with some simpler code. The Hanselminutes podcast website is running the same VB.NET WebForms application it was on the day I started the show in January of 2006 (almost 300 episodes ago!)

Sure, the app runs fine and there's no real reason to update it, but I wanted to see how quickly I could do it and if the code felt cleaner. I'll probably do a post where I update this .NET 1.1 WebForms app to .NET 4 and show how to maintain it. In this case, I'm updating it to Razor just because it sounded like fun. No other reason.

There's a few requirements I need to make sure I don't miss:

  • I'd like a new URL scheme. Currently showid is the database, not the logical "show id." Rather than
  • I can't break any existing links, so I want to HTTP 301 redirect from the old style to the new style.
  • I'll keep the look and feel now, but update it after.
  • Folks shouldn't notice the difference.
  • I need to use the existing wacky 2004 database, it's tables and stored procedures because all of Carl's existing backend administration systems feed into it.

But, it's such a basic application, it's basically a bunch of for loops. So, here's what I did tonight.

UPDATE: Check out the updates below as I was using inline SQL when there was no reason to! Very cool stuff in the Massive micro-ORM that I wasn't taking advantage of. Rob Conery has done a post as a result of my foolishness.

Importing the layout

I started in WebMatrix, although I could have just as easily used Visual Studio. I created a new "Starter Site." I took the existing ASP.NET WebForms application's Hanselminutes.master and copy/pasted it into the new ASP.NET Web Pages _SiteLayout.cshtml.

I then added a RenderBody() call where my WebForms application's master page had a ContentPlaceholder:

<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
</asp:ContentPlaceHolder>

I added in the existing CSS and Scripts and that got me here. A "hello world" equivalent:

Hanselminutes - Step 0

Then, I copied the Default.aspx ASP.NET WebForms code into Default.cshtml. This ASPX file has lots of server side WebForms controls like this:

<asp:HyperLink ID="hPermalink" Font-Bold="True" Text="Permalink" runat="server"/> 

But since Web Pages doesn't process any of those, they are literally ignored and sent across as if they were HTML markup. They aren't valid tags so the browser ignores them. That gets me here. More is laid out but there's no data behind it, of course.

 Hanselminutes - Step 1

Next, I need to get to the data.

Getting at the Data

I can get at the data in a number of ways, but I like lightweight and easy so I'm using Rob Conery's "Massive" micro-orm. It's 500 lines of code, so that's nice.

UPDATE: Rob Conery pointed out that everyone thinks that Massive and micro-ORMs like it require inline SQL. That they just turn table rows into objects. Turns out I was using Massive in an OK way, but not the easiest way. Note below as I've added before and after details.

There's a good discussion of WebMatrix.Data, WebMatrix.Data.StronglyTyped, Massive, and Simple.Data over at Mikesdotnetting. They are all very similar. Use the one that makes you happy. Also look at Dapper from Sam Saffron and friends and check out my Massive vs. Dapper Podcast.

The easiest page to do first is the Archives page. It's a for loop of links, so a good place to get started.

In WebForms the archives uses a DataGrid like this:

<asp:DataGrid ID="dgArchive" ForeColor="Black" GridLines="None" CellPadding="2" BackColor="#EFEFDA"
BorderWidth="1px" BorderColor="Khaki" Width="100%" runat="server" PagerStyle-Visible="false"
AutoGenerateColumns="False" ShowFooter="false" DataKeyField="ShowID" ShowHeader="False"
PageSize="999">
<FooterStyle BackColor="Tan"></FooterStyle>
<AlternatingItemStyle BackColor="#F4F7E0"></AlternatingItemStyle>
<Columns>
<asp:BoundColumn DataField="ShowNumber" Visible="True"></asp:BoundColumn>
<asp:HyperLinkColumn ItemStyle-ForeColor="Black" DataNavigateUrlField="ShowID" DataNavigateUrlFormatString="default.aspx?showID={0}" DataTextField="ShowTitle"/>
<asp:BoundColumn DataField="ShowID" Visible="False"></asp:BoundColumn>
<asp:BoundColumn DataField="DatePublished" HeaderText="Date" DataFormatString="{0:yyyy-MMM-dd}">
</asp:BoundColumn>
</Columns>
</asp:DataGrid>

There's some color alternating going on, hard-coded colors, kind of cheesy, and some columns for the data. It's a half-decade old. I didn't write it. But as developers we inherit old code all the time.

The Massive ORM needs a connection string in the web.config, so I'll put one there.

<connectionStrings>
<add name="hanselminutes"
connectionString="Data Source=yada yada yada"
providerName="System.Data.SqlClient" />
</connectionStrings>

We've got a table called "Shows," so I need to let Massive know about it.

using System;
using System.Collections.Generic;
using System.Web;

public class Shows : DynamicModel
{
//you don't have to specify the connection - Massive will use the first one it finds in your config
public Shows():base("hanselminutes")
{
PrimaryKeyField = "ShowID";
}
}

Next, I'll reuse the same SQL Query used in 2006, except now with Massive. I'll just see if I can get the titles first with inline SQL.

@{
dynamic tbl = new Shows();
var shows = tbl.Query(@"SELECT ShowID, DatePublished, ShowTitle, Description, Enabled, ShowNumber
FROM Shows
WHERE Enabled = 1
ORDER BY DatePublished DESC");

foreach(var show in shows) {
@show.ShowTitle<br/>
}
}

Cool, that works. But I've got inline SQL here and it's, well, inline SQL. Rob points out that Massive will let you use the Jon Skeet-designed named parameter syntax such that I can just do this and the same result as the inline SQL! I'm such a goof for forgetting this.

show = tbl.Find(Enabled: 1, orderby: "DatePublished DESC");

Ok, just to be clear, here is the whole thing again, except with smarter use of Massive.

@{    
dynamic tbl = new Shows();
show = tbl.Find(Enabled: 1, orderby: "DatePublished DESC");
forch(var show in shows)
{
@show.ShowTitle<br/>
}
}

Is nice, yes? I'll flesh out the table. Yes, a <table/>. Cause, it's, like, a table.

<table id="archives">
@foreach(var show in shows) {
<tr>
<td>@show.ShowID</td>
<td><a href="/@show.ShowNumber">@show.ShowTitle</a></td>
<td>@show.DatePublished.ToString("yyyy-MMM-dd")</td>
</tr>
}
</table>

Notice the href="" for the show is the ShowNumber, not the ShowID. It doesn't point anywhere yet, but I'll figure it out later.

Before they were doing some server side alternating color stuff. I need to update the CSS at some point, but since I'm redesigning soon, I'll just do it with jQuery on the client side, it's easier then the server-side if and gets the same result. Again, I'll change the template when I do the redesign.

<script type="text/javascript">
$(function() {
$('#archives tr:odd').css('backgroundColor','#EFEFDA');
$('#archives tr:even').css('backgroundColor','#F4F7E0');
});
</script>

Now I've got this page done, without the links hooked up. Nice and easy.

The complete Hanselminutes podcast archive - step 2

Here is the whole Archives page so far:

@{  
Layout = "~/_SiteLayout.cshtml";
Page.Title = "The complete Hanselminutes podcast archive";

dynamic tbl = new Shows();
var shows = shows = tbl.Find(Enabled: 1, orderby: "DatePublished DESC");
}

<table id="archiveTable" width="100%">
@foreach(var show in shows) {
<tr>
<td>@show.ShowNumber</td>
<td><a href="/@show.ShowNumber">@show.ShowTitle</a></td>
<td>@show.DatePublished.ToString("yyyy-MMM-dd")</td>
</tr>
}
</table>

<script type="text/javascript">
$(document).ready(function() {
$('#archiveTable tr:odd').css('backgroundColor','#EFEFDA');
$('#archiveTable tr:even').css('backgroundColor','#F4F7E0');
});
</script>

I can refactor the data access into a helper class and generally tidy up, but you get the idea.

On the home page I want to get the most recent show first using using inline SQL, that is, the LAST show:

lastShow = tbl.Single(@"SELECT Top 1 ShowID, DatePublished, ShowTitle, Description, Enabled, ShowNumber 
FROM Shows
WHERE Enabled = 1
ORDER BY DatePublished DESC").First();

And then again using Massive's syntax:

lastShow = tbl.Single(Enabled: 1, orderby: "DatePublished DESC");

At this point, it's easy to setup the home page by just sprinkling some Razor could around the original HTML template. I'm replacing <asp:Label> and <asp:Literal> with @lastShow.ShowID and things like this. Here I'm calling an existing Stored Proc and getting the filename and location then building an HTML5 <audio> element for all the MP3s.

@{
var files = tbl.Query("GetFilesByShowID @0", lastShowID);
foreach(var file in files) {
var filename = file.WebFilePath + file.FileName;
if (filename.EndsWith(".mp3")) {
<audio width='200' height='30' controls='controls' preload='auto'>
<source type='audio/mp3' src='@filename' />
<!-- Flash fallback for non-HTML5 browsers without JavaScript -->
<object width='320' height='240' type='application/x-shockwave-flash' data='flashmediaelement.swf'>
<param name='movie' value='flashmediaelement.swf' />
<param name='flashvars' value='controls=true&file=@filename' />
</object>
</audio>
}
}
}

Then, just 20 minutes of changing a bunch of asp:Controls to @lastShow.Something syntax and I was on my way. It's not pretty, but it's identical in functionality to the existing WebForms version.

Hanselminutes - Windows Internet Explorer (82)

Changing URLs

The only thing left is to make it so my URLs look like http://hanselminutes.com/1 instead of http://www.hanselminutes.com/default.aspx?showID=2.

There's lots of ways to do this, but the simplest is to take advantage of the URL rewriting that's built into IIS7. While i'm in there, I'll add a total of three rules.

Tiny URLs with just Number

This rule will take /123 and rewrite (not redirect) to /?showNumber=123. Because I'm redirecting such a "greedy" URL, it's important that I used the constraint of \d+ to just get numbers.

<rule name="number">
<match url="(\d+)" />
<action type="Rewrite" url="/?showNumber={R:0}" appendQueryString="false" />
<conditions logicalGrouping="MatchAny">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
</conditions>
</rule>

Default Document to /

This rule will take /default.aspx and make it /.

<rule name="Default Document" stopProcessing="true"> 
<match url="(.*?)/?Default\.aspx$" />
<action type="Redirect" url="{R:1}/" />
</rule>

Remove .ASPX extensions

This rule will change things like /archives.aspx to just /archives.

<rule name="RewriteASPX">
<match url="(.*).aspx" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Redirect" url="{R:1}" />
</rule>

But what if someone visits a link with a database ID, NOT a ShowID, like

http://hanselminutes.com/default.aspx?showID=300

Given a URL like this, I'll look up the show by showID, get the correct show number and then just redirect to /280 (the right number, given an old database ID of 300):

Response.RedirectPermanent("/" + lastShow.ShowNumber);

With these small changes in the web.config, plus one line of code, I'll have the same URL structure as before with 301 Permanent Redirects to the new structure that the rest of the site uses.

I'll also add one last rule to remove www. from the front of the URL which will make tweeting shows by ID nicer:

Remove www. and canonicalize domain name

<rule name="Cannonical Hostname"> 
<match url="(.*)" />
<conditions logicalGrouping="MatchAll" trackAllCaptures="false">
<add input="{HTTP_HOST}" pattern="^hanselminutes\.com$" negate="true" />
</conditions>
<action type="Redirect" url="http://hanselminutes.com/{R:1}" />
</rule>

So when this is up, the current (as of this writing) show is http://hanselminutes.com/285, nice and clean.

I haven't put the new site live yet, but I'll do some more testing and do it this week. It took about two hours while watching Breaking Bad on NetFlix to convert this five page VB.NET ASP.NET WebForms application to ASP.NET Web Pages, and I'm well positioned now to make database changes as well as change the template for a redesign. More importantly, I had fun.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web
Tuesday, September 27, 2011 11:15:25 AM UTC
This is probably a bigger change, but wouldn't it make sense to include the title of the episode in the URL? E.g. http://hanselminutes.com/285/startup-series-gibraltar-software-with-kendall-miller/ or something similar. This might help SEO, but more importantly, the link would then give some information to the reader on what the episode contains.
Tuesday, September 27, 2011 11:30:23 AM UTC
Scott,

I think you're going to run into trouble with with sql


lastShow = tbl.Query(@"SELECT ShowID, DatePublished, ShowTitle, Description, Enabled, ShowNumber
FROM Shows
WHERE Enabled = 1 and ShowID=IDENT_CURRENT('Shows')
ORDER BY DatePublished DESC").First()


If the last show to be inserted is not Enabled (not published yet?) this query won't return anything. I think you're better to filter by Enabled=1 and order desc by DatePublished and take the top 1
Tuesday, September 27, 2011 12:01:22 PM UTC
Scott,

With regards the following query:

lastShow = tbl.Query(@"SELECT ShowID, DatePublished, ShowTitle, Description, Enabled, ShowNumber FROM Shows WHERE Enabled = 1 and ShowID=IDENT_CURRENT('Shows') ORDER BY DatePublished DESC").First();

Would you not be better selecting the top 1 in the query. Eg:

lastShow = tbl.Query(@"SELECT TOP (1) ShowID, DatePublished, ShowTitle, Description, Enabled, ShowNumber FROM Shows WHERE Enabled = 1 and ShowID=IDENT_CURRENT('Shows') ORDER BY DatePublished DESC").First();

Keep up the good work
Tuesday, September 27, 2011 12:53:25 PM UTC
Thanks Scott, nice write-up about refactoring an old site in place.
Tuesday, September 27, 2011 1:58:32 PM UTC
Thanks for the article.
Is there going to be a HTML5 version ;).
Redgie F
Tuesday, September 27, 2011 2:35:29 PM UTC
I get that this is example-fodder, but really? SQL in the UI?
Tuesday, September 27, 2011 4:05:00 PM UTC
I agree with the first comment. By any chance do you have an old asp application using SQL 2000.
Bobby
Tuesday, September 27, 2011 4:13:41 PM UTC
Reminds me of Coldfusion but I like it!
Mike
Tuesday, September 27, 2011 5:25:29 PM UTC
Good idea about the URL. It would look better to, although I would still use the short one when tweeting.

Also, great point on the SQL. If there was a disabled show that was newer that WOULD be a problem. I need the LAST show that is also ENABLED.

Steve - Sure, as I point out I can easily pull that into a model class. The point is that now I'm positioned to move this into MVC if I like. Perhaps I'll also see if I can take the same application from WebForms 1.1 and move it to HTML5 and WebForms 4.x.





Tuesday, September 27, 2011 5:52:11 PM UTC
Great example of a simple, yet very useful post. It shows your thought process for how you would approach this type of migration. Also easy to follow even for people unfamiliar with Razor. I gotta try out a couple of those lightweight ORM packages.
Wednesday, September 28, 2011 1:26:03 PM UTC
Hi Scott.

How many rows do you expect to return from this?

tbl.Query(@"SELECT Top 1 ShowID, DatePublished, ShowTitle, Description, Enabled, ShowNumber
FROM Shows
WHERE Enabled = 1
ORDER BY DatePublished DESC")

Shoudln't you have written it like this?

tbl.Query(@"SELECT Top 1 ShowID, DatePublished, ShowTitle, Description, Enabled, ShowNumber
FROM Shows
WHERE Enabled = 1
ORDER BY DatePublished DESC").Single()
Wednesday, September 28, 2011 5:20:20 PM UTC
I noticed you didn't mention how you would handle invalid showIDs. Sure enough, visiting http://hanselminutes.com/?showID=3000 generates a runtime error (although the detail is of course hidden).

I'm guessing you would prefer to redirect to a home page or some other 404 in this case.
Thursday, September 29, 2011 3:02:27 AM UTC
will your also choose html5, css3? or asp.net Razor?
Thursday, September 29, 2011 11:02:01 AM UTC
Would be a good feature if you can pop the transcript status in the main archives index too :) At present I have to click through to check each show's status.
Jonathan
Friday, September 30, 2011 6:23:59 AM UTC
Morgado - Great point. I've changed that to a Single(). Thanks for that. An important point for folks to remember. Don't return more than you need to!

Marcel - Also a good point. I now check the single show access for a valid show and redirect home if none is found.

Jonathan - I'll do that. What should it look like?
Sunday, October 02, 2011 5:26:01 AM UTC
Hey Scott,

"?showID=XXX" appears to be broken currently. Doesn't matter what number I put in there, it always redirects to the home page.
Sunday, October 02, 2011 6:55:25 AM UTC
hemp - Thanks, fixed!
Monday, October 03, 2011 4:02:04 PM UTC
Is there any way to use Massive as strongly typed?

Thanks.

Duder
Monday, October 03, 2011 4:43:12 PM UTC
Hi Scott,

I feel the update pain also. I'm a consultant recently assigned to maintain and update (as possible) a clients .Net Web App that was built 10 years ago using 1.something. It was built by a classic asp developer who was brand new to .Net. Imagine my surprise when I looked at the Onload procedure for the first page in the app and found 3000 lines of code surrounded by one Try...Catch. That was it, all the code was in the Onload. Ouch!

Randy
Randy Schmidt
Wednesday, November 09, 2011 3:58:49 PM UTC
I need to use the existing wacky 2004 database, it's tables and stored procedures...


OK, English nit-picking time: Did you really mean to say "it is tables and stored procedures"?
Or were you trying to say "its tables and stored procedures"? :)
Tuesday, December 13, 2011 2:59:08 AM UTC
Scott,

It looks to me like the canonical hostname rewrite rule will remove any and all domain prefixes (i.e. not just www.mydomain.com but mobile.mydomain.com, ws.mydomain.com, etc.). But since I have not spent much hands-on time with IIS rewriting, I'm not certain of that. Can you confirm one way or the other?

Thanks!
Tuesday, December 13, 2011 3:30:27 AM UTC
For my site, yes, that was my intent. You'll want to change that to taste.
Friday, June 01, 2012 10:43:52 AM UTC
i'm a beginner in learning ASP.NET using visual basic.i hav a question,please help me with a answer for this. the question is how to hide long comments if its too long and can display it when we click read more or see more? how do we do that? plez help me...as soon as possible,will b very greatful...reply me in my email address given above...thank u sir :) hope sir will help me...
jangchu
Saturday, December 01, 2012 4:57:15 AM UTC
Well the lesson here is that if you have good deepvoelrs then be very very nice to them. The skill gap is widening between good engineers and bad ones and this is mostly I believe do to the enviroment for the first few IT jobs. we have to grow and nuture our junior staff into the lead deepvoelrs of the future. I know of at least one major consultantcy that does not hire juniors at all and cherry picks the best they can find.
Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.