October 2009 - Posts

TIP: How To Generate a Fully Qualified URL in ASP.NET (E.g., http://www.yourserver.com/folder/file.aspx)
26 October 09 03:59 PM | Scott Mitchell

Imagine that you've got an ASP.NET page that is generating an email message that needs to include links back to the website. Perhaps you're writing the next greatest online message board application and when someone replies to a thread you want to send out emails to the other thread participants indicating that a new message has been posted along with a link to view the just-posted message. You know that the URL to view a particular thread is, say, ~/Threads/View.aspx?ThreadId=threadId. But how do you turn that relative URL into an absolute URL like http://www.yourserver.com/Threads/View.aspx?ThreadId=threadId?

The simplest answer is to hard code the domain name into the URL when generating the email's content, but that approach is naive and wouldn't work if you were building an application that others could buy and install on their servers. It also would cause a headache if you change your domain name. Finally, it's a headache because oftentimes an ASP.NET application has different URLs depending on the environment. From the development environment the URL might be http://localhost:somePortNumber/MyMessageboard/Threads/View.aspx?ThreadId=threadId, whereas on the staging environment it might be http://stagingserver/MB/Threads/View.aspx?ThreadId=threadId. Ideally, the email generated from a particular environment would contain the appropriate URL back to said environment.

The good news is that generating a fully qualified URL in ASP.NET is quite simple. The following code will pick up the correct HTTP or HTTPS setting, the correct server name - be it localhost, localhost:portNumber, serverInIntranet, an IP address, or a domain name (www.example.com) - and the applicaiton path, if any. Here's the code in both C# and VB:

// C#
string relativeUrl = "~/Threads/View.aspx?ThreadId=" + threadId;
string fullyQualifiedUrl = Request.Url.GetLeftPart(UriPartial.Authority) + Page.ResolveUrl(relativeUrl);
' VB
Dim relativeUrl As String = "~/Threads/View.aspx?ThreadId=" + threadId
Dim fullyQualifiedUrl As String = Request.Url.GetLeftPart(UriPartial.Authority) & Page.ResolveUrl(relativeUrl)

That's it! Pretty easy.

The Request.Url property returns the fully qualified URL of the currently requesting page. The GetLeftPart(UriPartial.Authority) method returns just the protocol and domain name, which is then concatenated with the resolved relative URL to give us the fully qualified URL for the relative URL.

For more on Request.Url, along with a look at the many other path-related properties available of the HttpRequest class, see Rick Strahl's blog entry, Making Sense of ASP.NET Paths.

Filed under:
Rich Tooltips With jQuery
23 October 09 03:17 PM | Scott Mitchell

I was recently working on an application and needed the ability to present the user with a list of links when they hovered over a particular line of text. HTML elements include a title attribute that, when set, displays the attribute's value in a small yellow window when the user hovers over the element. Problem is, the tooltip is a text-only interface. There's no way to embed a hyperlink or image or other rich markup into the tooltip.

Since we are using jQuery in this project I spent some time exploring various techniques for implementing rich tooltips in jQuery. Specifically, I needed a solution that met the following criteria:

  • The tooltip would appear near or next to the text that the user was hovering over, and not on some fixed, predefined spot on the screen.
  • I could, from server-side code, specify what elements would display tooltips when hovered over.
  • I could, from server-side code, customize the tooltip message.
  • The tooltip could include any sort of HTML markup I wanted to throw into it - links, images, etc. - and the user could interact with this markup (click on a link, for instance).
  • The tooltip would remain displayed for a specified amount of time (say, 500 milliseconds) after the user moused off of the element that displayed the tooltip.

Unfortunately, I was unable to find a pre-built solution that would meet our needs, so I created my own, which I'd like to share now.

Please keep in mind that I know just enough JavaScript to shoot myself in the foot. Also, my experience with jQuery is limited, to be kind.

In order to display rich tooltips using my approach you need to do three things.

First, add the following CSS to those pages that use the rich tooltips:

.skmTooltipHost
{
cursor: help;
border-bottom: dotted 1px brown;
}

.skmTooltipContainer
{
padding-left: 10px;
padding-right: 10px;
padding-top: 3px;
padding-bottom: 3px;
display: none;
position: absolute;
background-color: #ff9;
border: solid 1px #333;
z-index: 999;
}

Those elements that should display a tooltip when hovered over need to use the skmTooltipHost class. The skmTooltipContainer class defines the styles for the tooltip itself.

Next, add the following $(document).ready function to your page (and add a script reference to the appropriate jquery.js file, if you haven't already):

<script type="text/javascript">
$(document).ready(function() {
$(".skmTooltipHost").hover(
function() {
$(this).append('<div class="skmTooltipContainer">' + $(this).attr('tooltip') + '</div>');

$(this).find('.skmTooltipContainer').css("left", $(this).position().left + 20);
$(this).find('.skmTooltipContainer').css("top", $(this).position().top + $(this).height());
$(".skmTooltipContainer").fadeIn(500);
},

function() {
$(".skmTooltipContainer").fadeTo(500, 1.0, function() { $(this).remove(); });
}
);
});
</script>

Finally, decorate those HTML elements that you want to display a tooltip with the skmTooltipHost class and add an attribute named tooltip that contains the content to display in the tooltip. For example, imagine I had a document with the following markup:

<p>ASP.NET simplifies the process of creating dynamic web applications!</p>

I could turn add a tooltip to the text “ASP.NET” by adding the additional markup:

<p><span class="skmTooltipHost" tooltip="ASP.NET was created by Microsoft in 2001.">ASP.NET</span> simplifies the process of creating dynamic web applications!</p>

To get rich markup in the tooltip attribute you'll need to escape the <, >, and " characters with &lt;, &gt;, and &quot;, respectively. For instance, we could add a hyperlink in the above tooltip like so:

<p><span class="skmTooltipHost" tooltip="ASP.NET was created by Microsoft in 2001. Learn more at &lt;a target="_blank" href=&quot;http://www.4guysfromrolla.com/&quot;&gt;4GuysFromRolla.com&lt;/a&gt;!">ASP.NET</span> simplifies the process of creating dynamic web applications!</p>

You can see these rich tooltips in action here - http://scottonwriting.net/sowBlog/CodeProjectFiles/JQueryTooltipDemo.htm

And lastly, to do this programmatically from server-side code, you would add a Label Web control to your page named, say, lblASPNET. You'd set its CssClass property to skmTooltipHost. To set its tooltip attribute add the following (untested) code to the Page_Load event handler:

lblASPNET.Attributes.Add("tooltip", Server.HtmlEncode("ASP.NET was created by Microsoft in 2001. Learn more at <a target="_blank" href="http://www.4guysfromrolla.com/">4GuysFromRolla.com</a>!")

Filed under:
SOLUTION: JSLint.VS Add-In Always Reports "No Errors" Even For Invalid JavaScript Files
15 October 09 03:49 PM | Scott Mitchell

JSLint is a free JavaScript code quality tool created by Douglas Crockford. At the JSLint.com website you can paste in a block of JavaScript code and JSLint will examine the code and warn you when it encounters any script that violates its list of rules. Many of the rules JSLint checks against are configurable, and include checks for usage of undefined variables, use of the eval function, statements not terminated with semicolons, and other 'trouble waiting for a place to happen' coding patterns.

JSLint.VS is a free, open source Visual Studio Add-In created by Predrag Tomasevic that brings JSLint to the Visual Studio IDE.

Today I installed JSLint.VS for the first time on a machine. The installation went smoothly, but anytime I used the Add-In it reported No Errors, even though the JSLint.com website was finding errors with the same block of JavaScript code. The good news is that I was able to identify the problem and fix it.

Just the Workaround, Please
For JSLint.VS to work the .js extension must be associated with the JScript WSH engine. Chances are, you have the JScript WSH engine already installed, but another program has claimed the association. In my case, the .js extension was associated with UltraEdit. Once I reassociated it with the JScript engine, the JSLint.VS Add-In worked as expected.

To see what program the .js extension is associated with, drop to the command line and enter:

assoc .js

The output should be: .js=JScript. If it's not, enter:

assoc .js=JScript

Now try JSLint.VS again.

For Those Who Care... The Why...
When you launch JSLint.VS from within the IDE it creates three files in the Application Data folder (My Documents\User\Application Data):

  • wsh.js - contains the JSLint JavaScript code, which you can download from http://www.jslint.com/fulljslint.js
  • temp.js - contains the JavaScript code to check with JSLint
  • csh.cmd - a batch file that executes the wsh.js file, passing in the temp.js contents as input.

Specifically, JSLint.VS executes the following command:

csh.cmd wsh.js < temp.js

If the .js extension is not mapped to the JScript WSH engine the above command will result in the following error message: “There is no script engine for file extension .js”

In the face of that error, JSLint.VS simply returns, “No Errors.” Once you associate the .js extension with the JScript WSH engine you should be good to go!

October's Toolbox Column Now Online
02 October 09 10:10 AM | Scott Mitchell

My Toolbox column in the October 2009 issue of MSDN Magazine is available online and includes the following reviews:

  • nukeationMachine - nukeationMachine is a Visual Studio Add In that streamlines implementing WPF, WinForms, and ASP.NET user interfaces. nukeationMachine includes 1,600 UI bits, which are common groupings of user interface elements, such as Ok, Cancel, Retry buttons. You can add any UI bit to a design surface with a click of the mouse and in a fraction of the time it would take to add and position each user interface element separately.
  • Podcasts of Note: .NET Rocks - each week hosts Carl Franklin and Richard Campbell spend about an hour exploring a particular technology, product, or person of interest to the .NET community. Franklin and Campbell are both natural interviewers and do a great job picking the brains of their guests while maintaining a smooth and natural flow to the discussion. Listening to a .NET Rocks podcast is a lot like overhearing a conversation among experienced developers at a user group meeting or conference - you're bound to learn something new, hear an interesting anecdote or two, and discover how other knowledgeable developers are using .NET and related technologies in their daily jobs.
  • Fluent NHibernate - NHibernate is a popular ORM for .NET whose relational to domain mapping is typically specified via XML. Fluent NHibernate is an open source library that enables developers to specify these mappings using a fluent interface, an API design style that aims at maximizing readability through the use of descriptive names and method chaining.

This issue reviewed The Nomadic Developer, by Aaron Erickson. An exerpt from my review follows:

If you currently work for a technology consulting company, or are weighing the pros and cons of doing so, then Aaron Erickson's book "The Nomadic Developer" is for you. ... [It] should be required reading for anyone currently seeking a job at a consulting company, especially those who have not previously worked as consultants. The book's second chapter, "The Seven Deadly Firms," describes seven dysfunctional traits that, if present, will negatively impact your time at the company. For each dysfunction, Erickson supplies a detailed description, explains what life is like at a consulting firm when that trait is present, and provides tips for spotting the dysfunction during a job interview. There are also chapters on the top 10 traits a technology consulting firm is looking for in applicants, and another chapter that suggests questions applicants should ask during the interview process.

Enjoy! - http://msdn.microsoft.com/en-us/magazine/ee309513.aspx

As always, if you have any suggestions for products, blogs, or books to review for the Toolbox column, please send them to toolsmm@microsoft.com.

Filed under:
Deleting All Records In a Table EXCEPT For the N Most Recently Added Records
01 October 09 08:50 AM | Scott Mitchell

I recently ran into a situation where I needed to delete all records from a table except for the 1,000 most recently added records. Specifically, I was working on a site that used Error Logging Modules And Handlers (ELMAH), a free, open source error logging library for ASP.NET applications. ELMAH doesn't automatically prune its error log. If you are using the database as a log source and if your web hosting provider has a database quota in effect, it is possible to have ELMAH's error log grow so large that the quota is surpassed and the database is taken offline. To help mitigate this problem, it is a good idea to take steps to keep ELAMH's error log size in check:

As with any sort of logging service is is important that ELMAH's log be periodically pruned. If you let ELMAH's log grow unchecked it can reduce performance when querying the log and suck up disk space, which is especially important in hosted environments where there are typically hard disk quote limits for each user on the database server. The good news is that there are a number of techniques you can employ to help ensure that your ELMAH error log stays a reasonable size.

  • Use error filtering.
  • Setup a weekly job in SQL Server to delete entries older than, say, three months.
  • Update the ELMAH_LogError stored procedure to delete old log entries.

Keep in mind that trimming the error log brings with it a tradeoff: you are removing error log entries that might be important for analysis later in time. If this is the case, if you think you might need to review that error log from more than 90 days in the past, then before deleting records from the error log you should archive them somewhere.

While these approaches are certainly good measures to implement, they don't handle the case where there's a flood of errors in a short amount of time. If ELMAH's error log is being inundated with errors from the past 24 hours, a script that deletes error log entries older than three months is going to have no effect.

An alternative approach is to delete all entries in ELMAH other than the most recently added N error log entries. This ensures that the error log never exceeds a maximum number of records (N), while keeping the N most recent records. The following query will do the trick:

DELETE FROM ELMAH_Error
WHERE ErrorId NOT IN
        (SELECT TOP 1000 ErrorId
         FROM ELAMH_Error
         ORDER BY TimeUtc DESC)

The ErrorId column is the primary key in the ELMAH_Error table, and TimeUtc stores the UTC date/time the error was recorded. The subquery gets the first 1,000 ErrorId values ordered by the time the error was logged, from the most recently logged to the oldest. The query deletes all error records whose ErrorId values are not in that set of the 1,000 most recently added errors.

If you are using SQL Server 2005 or beyond, you can use the ROW_NUMBER() keyword in a query like so:

DELETE FROM ELMAH_Error
WHERE ErrorId IN (
      SELECT ErrorId
      FROM
        (
           SELECT ErrorId, ROW_NUMBER() OVER(ORDER BY TimeUtc DESC) AS RowIndex
           FROM ELMAH_Error
        ) AS ErrorsWithRowNumbers
      WHERE RowIndex > 1000
)

The innermost query uses the ROW_NUMBER() keyword to assign a row number to each record from ELMAH_Error, numbering the most recently logged error 1, the next one 2, and so forth, such that the oldest error will have the largest row number assigned. This innermost query is used as a derived table. The query that uses it gets the ErrorId values whose row number is greater than 1,000, which returns those ErrorId values for errors that are “ranked” 1,001 or higher. In other words, it returns those ErrorId values for errors that are not one of the 1,000 most recently added errors. Finally, this set of ErrorIds are deleted from the table.

I ended up using the second query (the one with ROW_NUMBER()), placing it in the ELMAH_LogError stored procedure (along with a DELETE statement that removes any error log entries older than 3 weeks). If you use either one of these queries, consider adding an index on the TimeUtc column sorted in descending order.

Happy Programming!

Filed under: ,
More Posts

Archives

My Books

  • Teach Yourself ASP.NET 4 in 24 Hours
  • Teach Yourself ASP.NET 3.5 in 24 Hours
  • Teach Yourself ASP.NET 2.0 in 24 Hours
  • ASP.NET Data Web Controls Kick Start
  • ASP.NET: Tips, Tutorials, and Code
  • Designing Active Server Pages
  • Teach Yourself Active Server Pages 3.0 in 21 Days

I am a Microsoft MVP for ASP.NET.

I am an ASPInsider.