Sunday, June 29, 2008
Tuesday, June 17, 2008
Perfect ASP.NET application, Part 7
Url rewriting.
Nowdays, every web developer bows to Google and Yahoo. And that is right. Who wants/needs to spend it's time and get the perfect application out there and no visitors. And the only "free" way to get visitors is to get them from search engines. But for that you must make your website easy to spider. You can say anything you want about Google and Yahoo is being able to spider urls with query parameters but show me the popular search phrase where URLs with ? symbols shows up on a first page. Sorry, I did not see that.
So now we want to make our pages to look pretty. So instead of http://www.mspiercing.com/Product.aspx?id=1526 we want something like http://www.mspiercing.com/Product/belly-button-ring-web-ball-1526.aspx
Note: With the current version of IIS 6.0 in order to have a pure .NET UrlRewriting solution you need to have .aspx extension. That is how IIS knows that this request needs to be routed through ASP.NET engine. IIS 7.0 will change that but as of now it's in beta. So it will be hard to rewrite url like that http://www.mspiercing.com/belly-rings simply because there is no .aspx extension. You might use ISAPI dll to do that. But there are no pure "good" .NET solution. Some people use custom 404 page that has .aspx extension to trick IIS. So when request is made for http://www.mspiercing.com/belly-rings IIS trying to serve custom 404.aspx page and that is when .NET kicks in. Not sure that it's good solution because that 404.aspx will be called for everything, even images.
HttpContext.RewritePath makes it really easy in .NET to rewrite the path, but unfortunately it's not that simple. Problem is in the <form runat=server> tag which is a must in .NET
Let say you have rewritten url so when browser hits page http://www.mspiercing.com/Product/belly-button-ring-web-ball-1526.aspx it actually goes to page http://www.mspiercing.com/product.aspx?id=1526. Unfortunately the <form> tag in the HTML will look like following <form method="POST" action="http://www.mspiercing.com/product.aspx?id=1526" ...> and if you have single button on the page when user clicks it the browser going to hit not rewritten url with method POST. And users obviously will see it in their address bar of the browsers. Not good.
I've seem solution that uses custom written object derived from HtmlForm which suppresses rendering of the "action" property. Unfortunately it does not work well with validators and who knows what else. I found a nice easy solution (on internet) . All you need to do is rewrite URL back before Render method called.
So here is a skeleton of application that supports URL rewriting.
Global.asax
-----------------
void Application_BeginRequest(Object sender, EventArgs e){
HttpContext ctx = HttpContext.Current;
string sPath = ctx.Request.Path.ToLower().Substring(clsGlobal._sRoot.Length);
int iIndex = sPath.LastIndexOf(".aspx");
if (iIndex == -1)
return;
ctx.Items["OriginalPath"] = sPath;
......do rewriting here....
}
------------------------------
clsStandardPage.cs (remember from previous post that every page is derived form this object)
public class clsStandardPage : System.Web.UI.Page{
protected override void OnPreInit(EventArgs e)
{
//rewrite URL back
string sPath = (string)Context.Items["OriginalPath"];
Context.RewritePath(sPath, null, null);
....
}
}
Note: We need to check that we have request for .aspx file first. We do not want to rewrite request for famous WebResource.axd. Cause it will be routed through ASP.NET since .axd extension assigned to ASP.NET and it will trigger the BeginRequest event.
That solves all problems....
Note: Just discovered that if you use button with PostBackUrl set then it breaks UrlRewriting all together. Do not know exactly why. It has something to do with actual url not matching requested url. But i do not use PostBackUrl in my projects. So far did not have a need. So i am good :)
Tuesday, June 3, 2008
Perfect ASP.NET application, Part 6
Objects, objects everywhere
ASP.NET is a great framework. It turns HTML into objects and it's so much easier to work with objects. I love working with objects. But sometimes it kills me that to do small thing you would need to create an object. Let say you need to show current date/time on a bottom of each page. You have 2 options.
- Create asp:Label control, drop it on master page (I hope you use one in each of your project). In your OnLoad even you write following line lblDate.Text = clsGlobal.GetDateTime(DateTime.Now);
- Another option is simply write in a bottom of your master page <%=clsGlobal.GetDateTime(DateTime.Now)%>
Which option would you choose?
First of all why would you want to show current Date/Time on a page. It's simple. We live in a world where Google and Yahoo are kings. Every site owner is worried about how often spiders spider their precious websites. Look at this page. It's a google's cache of the www.mspiercing.com. Scroll to the bottom and you will see a date there. Now you know when Google's robot visited this page last time.
Now I hope you made up your mind. I would chose #2. Simply because I hate to create small objects without any intelligence in it. It's simply a waste of memory and CPU resource.
Another problem, a lot of people want to do website branding or use themes. Meaning that they want to show for one person everything blue, for another person everything white, or change the logo of the site depending on who is logged in. How can we do it easily?
I offer relatively simple solution. What if we could write our HTML using following construction. <img src="@ImageFolder@/logo.gif> and had a component that would analyze our output stream and substitute @ImageFolder@ appropriately. I offer you a "Replacer".
Response.Filter allows us to write such thing easily. First let's make a CConfig class which is a global Hashtable for our website and then CReplacer which is a class that being created for each request.
So idea would be that when our application starts we populate CConfig class and put it in clsGlobal
public class clsGlobal
{
......
public CConfig replacer = new CConfig();
public static void Init()
{
.......
cfg.AddToken("SITENAME",
"My Site");
}
}
Change our clsStandardPage a little:
public class clsStandardPage : System.Web.UI.Page
{
......
Replacer.CReplacer rpl = clsGlobal.replacer.GetReplacer(Response.Filter);
Response.Filter = rpl;
rpl.AddToken("DATE", DateTime.Now.ToString());
rpl.AddToken("ImageFolder", "/brand1/images/");
}
And we done. Now look at this page
<HTML>
<BODY>
Welcome to @SITENAME@
Current date is @DATE@ and our logo is <img src="@ImageFolder@/logo.gif">
</BODY>
</HTML>
No objects created. The code does not break our HTML. The person who has no idea about programming languages can now look at it and understand.
There are many uses for "Replacer". The most common one is in CMS (Content Management System). Look at this page http://www.mspiercing.com/Product/razor-navel-ring-1370.aspx. Notice page Title matches the item's name, Item has a description, picture...... All that, I did with a "Replacer". The code determines Title, Description, Image, BigImage, price.... and adds those as tokens to "Replacer". The SEO (search engine optimizator) can now move staff around at will. If he dumps on a page <img src="@Bigimage@"> user will see big image instead of regular image. He wants to make price red.... I do not care. He edits HTML so it becomes <font color=red>@PRICE@</font>
I hope you got an idea.
Note: You will be able to download Replacer from my last post. It will
be included in a sample application.
Note: To improve runtime I made "Replacer" to run in unsafe
mode. Meaning that you need to compile your .NET application with /unsafe flag.
Easy to do. Dump following line in web.confiig
<system.codedom>
<compilers>
<compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CSharp.CSharpCodeProvider, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" compilerOptions="/unsafe" warningLevel="1"/>
</compilers>
</system.codedom>
If you are on shared hosting environment most likely you will not be able to use /unsafe switch. Then you need to get your hands dirty and make small modifications.
Note: To avoid problems with emails "Replacer" only counts 20 characters after it encounters first @ symbol. And if no second @ symbol it leaves the HTML intact. So HTML like this "my email is email@email.com send me all spam in a world" will not be modified.
Of course Replacer scans the whole HTML output. So you might think there is a performance hit. CPU is designed to do scans and comparing to creating and initializing several Label controls on your page it would be nothing. Also do you know that your IIS supports so called "HTML includes". It's constructions like this in HTML <!--#include virtual="insertthisfile.html" -->. How do you think it's working. The same scanning looking for <!-- #include.... And I do not remember anyone complaining.... Of course if you work in google you might have to come up with something else.
Perfect ASP.NET application, Part 5
Benefits of clsStandardPage
Note: You must see previous post to understand what I am talking about here.
Let's talk about our project and what we have already.
First we have clsBrowser:
public class clsBrowser
{
public string _sFirstName = '';
}
Then we have our clsStandardPage all pages derived from
public class clsStandardPage : System.Web.UI.Page
{
public clsBrowser _objBs;
protected override void OnPreInit(EventArgs e)
{
//no caching
Response.CacheControl =
"no-cache";
Response.Expires = -1;
//Get Browser object
_objBs = (clsBrowser)Session["browser"];
if (_objBs == null)
Session["browser"] = _objBs = new clsBrowser();
base.OnInit(e);
}
}
So now it's time to see why we went through all the trouble and did all that.
Our customer comes to us and says that he want people to be able to login before
seeing content of the web site. So here are our steps.
Note: I will skip some code that you can see in previous posts.
public class clsBrowser
{
public int _iUserId = -1; //add iUserId
for users
....
}
public class clsStandardPage : System.Web.UI.Page
{
public clsBrowser _objBs;
private bool _bRequiresLogin = false;
public bool RequiresLogin
{
get {return _bRequiresLogin;}
set { _bRequiresLogin = value;}
}
protected override void OnPreInit(EventArgs e)
{
..............get _objBs.......
//Check
if user is not logged in then redirect to login.aspx
if ( _bRequiresLogin
&& ( _objBs.iUserId == -1 ) )
Response.Redirect("~/login.aspx");
base.OnInit(e);
}
}
------Login.aspx--------
<%@ Page RequiresLogin = "false" %>
.....code to do actual login and set our _objBs._iUserId to valid UserId
Note: We added public property RequiresLogin to avoid infinite
loop. Our Login.aspx is derived from clsStandardPage and we do not want to
redirect from it on itself.
Note: If we not using separate .cs file we can declaratively set it to RequiresLogin
to false (like i did). Unfortunately if you separate aspx page and .cs file
you will not be able to do that. Visual Studio throws a compiler error. (Not
sure about VS 2008). In this case you would have to set RequiresLogin to
false in constructor.
Note: You need to set RequiresLogin only on Login.aspx since by
default we set it to true.
Another reason to do it that way.... I had a client who came back to me after site was done and said "We want to do bait and switch. Meaning that we let users to see 10 pages of our web site without having him to create an account/login. Then when he sees what we offer he would more easily give us his email address."
Good luck doing that with your standard way.....
I did it with practically 3 lines of code.
public class clsBrowser
{
public int _iUserId = -1; //add iUserId
for users
public int _iCounter = 0; //page view counter
....
}
public class clsStandardPage : System.Web.UI.Page
{
........
protected override void OnPreInit(EventArgs e)
{
..............get _objBs.......
//Check
if user is not logged in then redirect to login.aspx
if ( _bRequiresLogin
&& ( _objBs.iUserId == -1 ) )
{
if( _objBs._iCounter > 10)
Response.Redirect("~/login.aspx");
else
_objBs._iCounter ++;
}
base.OnInit(e);
}
}
Note: We only count pages that requires login. And it makes sense. We do not want to make person "pay" (in terms of views) if poor guy refreshed login page 10 times. Or was looking at our "Privacy Policy". Only real content matters.
I hope by now you starting to like the way I do it.....
Perfect ASP.NET application, Part 4
clsStandardPage and clsBrowser
Note: You must see previous post to understand what i am talking about here.
Look at your ASP.NET application. It's full of references to Session object. It's everywhere. In one spot you have Session["FirstName"] = 'George' in another you have Response.Write((string)Session["FirstName"]. What if you make a misspell. And write .....Session["FirstNane"] (notice nane not name). Who is going to know... only when you populate your order database with empty names you will see it. And hopefully QA department will see it.
Plus every time you write Session["..."] it's a look up in hashtable.
So here is an idea. Let's make a class
public class clsBrowser
{
public string _sFirstName = '';
}
and put it in out clsStandardPage. Then since every our page is derived from clsStandardPage it will have access to _objBs. So if we need to get user's first name we simply write _objBs._sFirstName.
Benefits:
- Faster, no hashtable look-ups.
- Type safe. We do not need to do conversion to string or Int32 every time we use it.
- Misspell proof. Compiler will throw an error if you try to write _sFirsNane.
Perfect ASP.NET application, Part 3
clsStandardPage
Our goal here is to write easy maintainable application. So nowadays in any of my projects I define following class (we will discuss later what is clsBrowser and why i need it.)
public class clsStandardPage : System.Web.UI.Page
{
public clsBrowser _objBs;
protected override void OnPreInit(EventArgs e)
{
//no caching
Response.CacheControl = "no-cache";
Response.Expires = -1;
//Get Browser object
_objBs = (clsBrowser)Session["browser"];
if (_objBs == null)
Session["browser"] = _objBs = new clsBrowser();
base.OnPreInit(e);
}
}
In web.config we do a small change
<system.web>
<pages pageBaseType="clsStandardPage" />
</system.web>
And now all our pages derived from clsStandardPage.
Benefits: All our pages now derived from clsStandardPage and we can change the way they behave in one place (You will see how later).
Notes: You can have more than one clsStandardPage, but then on a page you will need to specify (if you do not separate code and html)
<%@ Page Inherits="clsStandardPage1"%>
to overwrite the web.config setting. And if you do code separation (VS default) you would need to modify your .cs class. Something like
public partial class Default2 : clsStandardPage
Read next post to find out what is clsBrowser and why it's so much better to have clsStandardPage
PS: If you are working on your home page you are welcome to do anything you want. If you working on a real project even small one use MasterPage. First of all small projects tend to become medium projects with time. And trust me it's much easier to modify a navigation, menu or simply change company's logo in one place than 10s aspx pages.
Perfect ASP.NET application, Part 2
clsGlobal and it's benefits.
We now have a static class clsGlobal. Let fill it with our little helpers that simplify our
lives.
The first thing that comes in mind methods to work with database
public static DataTable GetData(string sSql)
{
SqlConnection con = new SqlConnection(sConnection);
SqlDataAdapter ad = new SqlDataAdapter(sSql, con);
DataTable dt = new DataTable();
try
{
con.Open();
ad.Fill(dt);
return dt;
}
finally
{
ad.Dispose();
con.Close();
}
}
public static void ExecuteSql(string sSql)
{
SqlConnection con = new SqlConnection(sConnection);
SqlCommand com = new SqlCommand(sSql, con);
try
{
con.Open();
com.ExecuteNonQuery();
}
finally
{
com.Dispose();
con.Close();
}
}
public static object ExecuteScalar(string sSql)
{
SqlConnection con = new SqlConnection(sConnection);
SqlCommand com = new SqlCommand(sSql, con);
try
{
con.Open();
object objTmp = com.ExecuteScalar();
return objTmp;
}
finally
{
com.Dispose();
con.Close();
}
return null;
}
Now we want to be consistent everywhere in our web site so let's introduce
couple of methods
public static string GetDateTime(DateTime dt)
{
return dt.ToString("MM/dd/yy hh:mm tt");
}
public static string GetMoneyHtml(object objVal)
{
return ((decimal)objVal).ToString("$0.00");
}
Benefits:
- Convenient and save methods to get DataTable or run SQL statement. You will not forget to close .NET connection.
- Unified look of our website. Everywhere where we need to show DateTime we use <%=clsGlobal.GetDateTime(DateTime.Now) %> or like this in a table <%# clsGlobal.GetMoneyHtml(Eval("amount"))%>
I hope you got an idea and will put bunch of helpers in this class. Just do not forget to make them static.