Search

Wednesday, February 27, 2008

12 Essential Free Tools for .NET Developers

Over my course of .NET development, I have compiled a list of essential free tools for .NET applications. This is not a the alpha or the omega of tools, just a short list that I feel is essential.

  1. SharpDevelop

    http://www.icsharpcode.net/OpenSource/SD/Download
    An open source IDE for .NET. Check out the full feature tour.
  2. Visual Web Developer Express Edition

    http://www.microsoft.com/express/vwd
    Stripped down version of Visual Studio that allows you to write .NET web applications.
  3. TortoiseSVN

    http://tortoisesvn.net/downloads
    There is no better Subversion client for Windows. You need this if you are going to use VisualSVN with Visual Studio.
  4. NAnt

    http://nant.sourceforge.net
    .NET based automation tool that has many built in tasks but could be extended with custom code written in any .NET language.
  5. Snippet Compiler

    http://www.sliver.com/dotnet/SnippetCompiler
    Snippet compiler is a small tool to write and execute small chunks of .NET code without creating a Visual Studio project.
  6. .NET Reflector

    http://www.aisto.com/roeder/dotnet
    Reflector is the class browser, explorer, analyzer and documentation viewer for .NET. Reflector allows to easily view, navigate, search, decompile and analyze .NET assemblies in C#, Visual Basic and IL.
  7. Microsoft SQL Server Management Studio Express

    http://www.microsoft.com/downloads/details.aspx?FamilyID=c243a5ae-4bd1-4e3d-94b8-5a0f62bf7796&displaylang=en
    Tool for database administration and development from Microsoft
  8. Quest Comparison Suite for SQL Server

    http://www.quest.com/Comparison-Suite-for-SQL-Server
    Compare and synchronize database schema and data
  9. XYPlorer

    http://www.xyplorer.com
    Awesome file manager. The older version is completely free.
  10. Convert C# to VB.NET

    http://labs.developerfusion.co.uk/convert/csharp-to-vb.aspx
  11. NAnt Add-In

    http://www.netlogics.ch/devcenter/display/NLC/NAntAddin
    Visual Studio add-in for NAnt integration
  12. NUnit Add-In

    http://www.netlogics.ch/devcenter/display/NLC/NUnitAddin
    Visual Studio add-in for NUnit integration

More Visual Studio add-ins can be found at 15+ Free Visual Studio Add-Ins and 15+ FREE Visual Studio Add-ins - Part 2

Thursday, February 21, 2008

Tools for Authoring NAnt Build Files

NAnt is a great tool for automating many computer tasks. For a quick introduction, check my previous articles Automating Your Computer Tasks with NAnt and Use NAnt and WinRar to Create a Self Extracting Archive. Authoring NAnt build files can be difficult without a good editor and editing XML is not my favorite thing to do anyway. Having to know all the tags and parameters for each NAnt task, makes it even less desirable. Looking at the current options, you can find several tools for authoring NAnt build files:

While the tools above will do the job, it would be nice if you could edit your build files in Visual Studio and have at least the basic intellisense that the IDE provides. It turns out that editing NAnt build files with VS and having code insight is a pretty easy thing to setup. Here is how it is done:

  1. Install the NAnt schema by copying the file "nant.xsd" form the NAnt distribution to "C:\Program Files\Microsoft Visual Studio 8\Xml\Schemas". Update: for VS 2008, the directory is "C:\Program Files\Microsoft Visual Studio 9.0\Xml\Schemas"
  2. Associate NAnt build files (.build) with the Visual Studio XML editor. This can be done in one of two ways:
    • Create a registry merge file with the following contents and merge it in your registry:
      Windows Registry Editor Version 5.00
      
      [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\8.0\Editors\{412B8852-4F21-413B-9B47-0C9751D3EBFB}\Extensions]
      "build"=dword:00000029
      
    • Make the association through Visual Studio:
      • Open any Visual Studio solution
      • Add a NAnt build file to it
      • Right click on the .build file and choose "Open With"
      • Select "XML Editor" and click on "Set as Default"
  3. Almost there. The last thing is to add the "xmlns" (namespace) attribute to the "project" tag of your build file. If you have copied the NAnt schema file (nant.xsd) to the right place, adding the "xmlns" attribute should list the NAnt schema as one of the available choices. The end result should look like:
    <project
     name="testProject"
     default="buildSetup"
     basedir="."
     xmlns="http://nant.sf.net/release/0.85/nant.xsd"
    >
    
  4. And Voila! Start typing some XML and you should see a list of NAnt tasks and attributes.

Tuesday, February 19, 2008

Microsoft Giving Away Development Software to Students

Dreamspark is the name of the Microsoft initiative to give students free developer tools so "you can chase your dreams and create the next big breakthrough in technology - or just get a head start on your career."

As of right now, this program is only avaiable to students from the US, UK, Canada, China, Germany, France, Finaldn, Spain, Sweeden, Switzerland and Belguim.

So if you are a student in one of those countries, it's your lucky day.

To get started, go to https://downloads.channel8.msdn.com/

Universal Package Tracking in ColdFusion

A while back, I was in need of a universal package tracking tool that is not specific to the carrier. Almost every carrier provides some kind of an API but it can be a pain to set up each one separately. Fortunately, I found a nifty tracking tool which is universal and can be leveraged through RSS. It works with UPS, FedEx, USPS, or DHL/AirBorne without having to specify which carrier you need. Instead, it determines the carrier from the tracking number.

You can check it out at http://isnoop.net/tracking and the some details can be found at http://isnoop.net/blog/?p=19.

While this might not be good commercial solution, it is still usable for personal or a small site. The code below shows how to leverage this with ColdFusion. The Ajax/JavaScript implementation relies on Prototype.js. You can see it in action at http://blog.tech-cats.net/examples/universalPackageTracking.cfm and download it from http://blog.tech-cats.net/examples/universalPackageTracking.txt

The code is well documented and should be easy to read/understand:



<cfsetting enablecfoutputonly="yes">
<!--- Setup default parameters and constants --->

<!--- Is this call to the page from javascript (Ajax) --->
<cfparam name="url.isAjaxCall" default="false" />

<!--- The message to display while loading --->
<cfset loadingMessage = "Loading..." />

<!--- Default tracking number as a url variable (trackingNumber) --->
<cfparam name="url.trackingNumber" default="" />
<!---
 Tracking page url: very nice free tracking for all carriers that you can
 call to get an rss feed generated based on your tracking number as in
 'http://isnoop.net/tracking/index.php?t=85642012466&rss=1'.

 If you call it without setting the 'rss' variable as in:
 'http://isnoop.net/tracking/index.php?t=85642012466' you can see a nice
 google map of where in route your package is.
 
 This service works for UPS, FedEx, USPS, or DHL/AirBorne without having
 to specify the carrier as it determins it from the tracking number.
 --->
<cfparam name="trackingPageUrl" default="http://isnoop.net/tracking/index.php" />

<!---
Function:  parseRss

Arguments:

rssData  string (The string of rss xml retrieved with cfhttp)
debugMode boolean

Return Value:
An array of structures containing the parsed rss feed. Example:
array[1]
 link - the link from the rss item
 title - the title from the rss feed
 description - the description from the rss feed

Description:
Parses the RSS feed passed in --->
<cffunction name="parseRss" returntype="array" output="true" hint="Parses the RSS feed passed in">
 <cfargument name="rssData" type="string" required="true">
 <cfargument name="debugMode" type="string" required="false">

 <!--- Set default variables --->
 <cfset var xmlData = "">
 <cfset var result = arrayNew(1)>
 <cfset var x = "">
 <cfset var items = "">
 <cfset var xPath = "">
 <cfset var node = "">

 <cftry>
  <!--- Parse the data as xml --->
  <cfset xmlData = xmlParse(arguments.rssData)>

  <!--- Create xpath search string based on the xml root name --->
  <cfif xmlData.xmlRoot.xmlName is "rss">
   <cfset xPath = "//item">
  <cfelse>
   <cfset xPath = "//:item">
  </cfif>

  <!--- Get all the xml nodes matching the xpath search string --->
  <cfset items = xmlSearch(xmlData, xPath)>

  <!--- Loop through the found xml nodes and build an array of structures --->
  <cfloop index="i" from="1" to="#arrayLen(items)#">
   <cfset node = structNew()>
   <cfset node.link = items[i].link.xmlText>
   <cfset node.title = items[i].title.xmlText>
   <cfset node.description = items[i].description.xmlText>

   <cfset result[arrayLen(result) + 1] = duplicate(node)>
  </cfloop>
  <cfcatch>
  </cfcatch>
 </cftry>

 <cfreturn result>
</cffunction>

<cfoutput>
<!---
If this is an ajax call, get the tracking results trackingPageUrl specified above --->
<cfif url.isAjaxCall and url.trackingNumber neq ''>
 <cfhttp method="get" url="#trackingPageUrl#" result="test" charset="windows-1252">
  <!--- Set the 'rss' url variable --->
  <cfhttpparam name="rss" type="url" value="1" />
  <!--- Set the tracking url variable --->
  <cfhttpparam name="t" type="url" value="#url.trackingNumber#" />
 </cfhttp>

 <!--- Parse the rss feed from the contents returned by cfhttp --->
 <cfset rssFeed = parseRss(test.filecontent) />

 <cfif arraylen(rssFeed) gt 0>
  <div id="rssItem">
   <span id="description">Tracking data for tracking number
   '<span id="trackingNumber">#url.trackingNumber#</span>'</span>
  </div>
  <br />
  <!--- Loop through the contents of the rss feed and display them --->
  <cfloop index="i" from="1" to="#arrayLen(rssFeed)#">
  <div id="rssItem">
   <span id="description">#rssFeed[i].description.replaceall("Package update on ", "")#</span>
   
  </div>
  </cfloop>
 <cfelse>
  <div id="rssItem">
   <span id="description">No tracking data found for tracking number
   '<span id="trackingNumber">#url.trackingNumber#</span>'</span>
  </div>
 </cfif>
<cfelse>
 <!--- This is not an ajax call, so display a form for the user to enter a tracking number --->
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
 <script type="text/javascript" src="/js/prototype.js"></script>
 <style type="text/css">
 span##trackingNumber { font-weight: 700; }
 div##statusContainer { position:absolute;left:0px;top:0px;width:100%;height:10px; }
 div##statusMessageContainer { position:absolute;background-color:##000000;color:white;width:70px;font-family:Arial, Helvetica, sans-serif;padding:2px;left:0px; }
 </style>
 </head>
 <body>
 Browser Bookmarklet:
 <a href="javascript:(function(){var url='http://#cgi.http_host##cgi.script_name#';searchString=prompt('Enter your tracking number:','');searchString=((searchString==null)?'':searchString.replace(/^(\s+)?(.*?)(\s+)?$/gi,'$2'));var params='?isAjaxCall=true&t=';if(searchString!=''){if(location.href.indexOf(url)==-1){var win=window.open(url+params+escape(searchString));}else{location.href=url+params+escape(searchString);}}})();">
 Track Packages
 </a>
 <br /><br />
 <form id="trackingForm" name="trackingForm" method="get" action="#cgi.script_name#" class="ajaxForm">
 <input type="hidden" id="isAjaxCall" name="isAjaxCall" value="true" />
 <input type="text" id="trackingNumber" name="trackingNumber" value="1Z04WF350314328154" />
 <input type="submit" id="getTrackingResults" name="getTrackingResults" value="Track" />
 </form>
 <br />
 <!-- Results container that will be updated with the results of the request --->
 <div id="resultsContainer" class="ajaxContent"></div>
 <br />
 <!-- Status container that will be display durring processing --->
 <div id="statusContainer" style="display: none;" class="ajaxStatus">
  <div id="statusMessageContainer">Loading...</div>
 </div>
 <script language="javascript" type="text/javascript">
 var trackingForm = Class.create();
 trackingForm.prototype = {
  ajaxContainerElement: 'div',
  formID: '',
  ajaxUrl: '',
  resultsContainer: '',
  /*
  Function: initialize
  Description: Performs various intiliazion tasks for the form
  */
  initialize: function() {
   var ajaxFormsList = $$('form.ajaxForm');
   var ajaxContainersList = $$(this.ajaxContainerElement + '.ajaxContent');

   if (ajaxFormsList.length > 0 && ajaxContainersList.length > 0) {
    this.formID = ajaxFormsList[0].id;
    this.ajaxUrl = $(this.formID).action;
    this.resultsContainer = ajaxContainersList[0].id;

    // Tie the submit event to the submitForm function
    $(this.formID).observe('submit', this.submitForm.bind(this));

    // Reset the form
    $(this.formID).reset();

    // Activate the first element on the form
    $(this.formID).findFirstElement().activate();
   }
  },
  /*
  Function: submitForm
  Description: Submits the form
  */
  submitForm: function(event) {
   // Serialize the form parameters to pass them along as part of the form submission
   var params = $(this.formID).serialize(true);

   // Check if the tracking number is empty
   if (!params.trackingNumber.empty()) {
    // Disable the form
    $(this.formID).disable();

    // Make an ajax request passing it the serialized form    
    new Ajax.Updater(
     $(this.resultsContainer),
     this.ajaxUrl,
     {
     method: 'get',
     parameters: params,
     onFailure: this.reportError.bindAsEventListener(this),
     onSuccess: this.processResults.bindAsEventListener(this),
     evalScripts: true
     }
    );
   }

   // Prevent the form from being submitted
   Event.stop(event);
  },
  /*
  Function: processResults
  Description: Processes the server results
  */
  processResults: function() {
   // Enable the form
   $(this.formID).enable();
 
   // Reset the form
   $(this.formID).reset();

   // Activate the first element on the form
   $(this.formID).findFirstElement().activate();
  },
  reportError: function(request){}
 };

 Event.observe(window, 'load', function() {
  var ajaxStatusContainersList = $$('div.ajaxStatus');
  var ajaxStatusContainer = '';

  // Create an instance of the form object defined above
  trackingFormInstance = new trackingForm();

  if (ajaxStatusContainersList.length > 0) {
   ajaxStatusContainer = $$('div.ajaxStatus')[0].id;

   Ajax.Responders.register({
    onCreate: function() {
     $(ajaxStatusContainer).show();
    },
    onComplete: function() {
     $(ajaxStatusContainer).hide();
    }
   });
  }
 });
 </script>
 </body>
 </html>
</cfif>
</cfoutput>

Monday, February 18, 2008

Get All Form Fields in ModelGlue the Elegant Way

If you are using the ModelGlue framework for your ColdFusion development, you know how to get variable values from the form/url scope. Assuming you are inside a controller method that takes the argument "event" of type "ModelGlue.Core.Event", ModelGlue makes this as simple as
arguments.event.getValue("eventVariableName")
where "eventVariableName" is the name of the variable in the form/url scope. Simple enough. However, what happens if you have a form with 10 fields. You could recreate the above with 10 lines with a line for each variable in the form/url scope. Not nice and a bit of a pain. An alternative method would be to great a model cfc containing your getters/setters for each form field. Then using ModelGlue, you would call the "makeEventBean" function passing it the instance of your model and then use model.getName() to get the value of the "name" field.
<cfset formModelCFC = component.createObject("component", "path.to.formModelCFC") />
<cfset arguments.event.makeEventBean(formModelCFC) />
<cfset name = formModelCFC.getName() />
If you are interested in traveling that route, the method is described in more detail on Dan Willson's blog under So you want to create a ModelGlue:Unity application? (Part 3)
This method can also be a pain since you have to update your bean every time you add a new field to the form. After a short dicussion with Ray Camden, there is a even easier way using a built in Model-Glue method (that I had initially overlooked). The code is pretty self-explanatory.
<cfset var eventValues = structnew() />

<!--- Get the form/url values --->
<cfset eventValues = arguments.event.getAllValues() />

Thursday, February 07, 2008

Get Business Days and Working Hours in a Month with SQL Server

In business applications, there often is a need to know the number of business days and/or hours in a month/year. There is a dozen ways to do this depending on your language but here is a SQL Server implementation. The function to get the number of working hours in a month, simply uses the first function - dbo.fnGetBusinessDaysInMonth and by default multiplies the number of business days by 8 hours a day.

To use these custom functions, you would call them as follows:
-- Get the number of business days in the month using the current date as a base
select dbo.fnGetBusinessDaysInMonth(getdate())
-- Outputs 21

-- Get the number of business days for January, the date can be any January date
select dbo.fnGetBusinessDaysInMonth('01/01/2008')
-- Outputs 23

-- Get the number of working hours for the current month with the default (8) number of hours per day
select dbo.fnGetWorkingHoursInMonth(getdate(), null)
-- Outputs 168

-- Get the number of working hours for the current month with 4 of hours per day
select dbo.fnGetWorkingHoursInMonth(getdate(), 4)
-- Outputs 84

And here are the function definitions:
/*
Type:		Function
Name:		dbo.fnGetBusinessDaysInMonth
Author:		Boyan Kostadinov
Created:	02.05.2007
Dependencies:	None
Usage:		select dbo.fnGetBusinessDaysInMonth(getdate())
		select dbo.fnGetBusinessDaysInMonth('01/01/2008')
Parameters:	@currentDate(datetime)
		- The date to use as a starting point
Description:

Gets the number of business days in a month
*/
alter function dbo.fnGetBusinessDaysInMonth(
	@currentDate datetime
)
returns int
as
begin

declare @dateRange int
declare @beginningOfMonthDate datetime, @endOfMonthDate datetime

-- Get the beginning of the month
set @beginningOfMonthDate = dateadd(month, -1, dateadd(day, -1, dateadd(month, datediff(month, 0, @currentDate) + 1, 1)))

-- Get the the beginning date of the next month
set @endOfMonthDate = dateadd(day, -1, dateadd(month, datediff(month, 0, @currentDate) + 1, 1))

-- Get the date range between the beginning and the end of the month
set @dateRange = datediff(day, @beginningOfMonthDate, @endOfMonthDate)

return
(
	-- Get the number of business days by getting the number
	-- of full weeks * 5 days a week plus the number days remaining
	-- minus any days from the remaining days that are a weekend day
	select	@dateRange / 7 * 5 + @dateRange % 7 -  
	(
	        select	count(*)
		from
	        (
	            select 1 as d
	            union
	            select 2
	            union
	            select 3
	            union
	            select 4
	            union
	            select 5
	            union
	            select 6
	            union
	            select 7
	        ) weekdays
	        where	d <= @dateRange % 7
		        and
			datename(weekday, dateadd(day, -1, @endOfMonthDate) - d)
		        in ('Saturday', 'Sunday')
	)
)

end
To get the number of working hours in a month, we can use the dbo.fnGetWorkingHoursInMonth function defined as:
/*
Type:		Function
Name:		dbo.fnGetWorkingHoursInMonth
Author:		Boyan Kostadinov
Created:	02.05.2007
Dependencies:	None
Usage:		select dbo.fnGetWorkingHoursInMonth(getdate(), null)
		select dbo.fnGetWorkingHoursInMonth('01/01/2008', 4)
Parameters:	@currentDate(datetime)
		- The date to use as a starting point

		@workingHoursInADay(int) - Optional, default is 8
		- The number of working hours in a day
Description:

Gets the number of business days in a month
*/
create function dbo.fnGetWorkingHoursInMonth(
	@currentDate datetime,
	@workingHoursInADay int = null
)
returns int
as

begin

if @workingHoursInADay is null
	set @workingHoursInADay = 8

return dbo.fnGetBusinessDaysInMonth(@currentDate) * @workingHoursInADay

end

How to Impress with Your VB.NET Skills

It is no secret that people who write in VB.NET are often looked down upon. Sometimes that can be without merit but sometimes there is a good reason as folks do not bother to learn and improve. That being said, here are some quick rules to impress people while still programming with VB.NET.

  1. Know the .NET Framework
    Seems like a no-brainer but it is mind boggling how many people switch from VB6 to VB.NET and never bother to learn the new functionality available in .NET. So here are some quick tips:
    1. Use System.IO.File.ReadAllText() for reading a complete text file instead of reading it line by line.
    2. If you need to process a file line by line, use #1 but split the text file with String.Split() with Environment.NewLine as the delimiter and then loop over the array.
    3. When you need to find something inside a string, indexOf() is not the only choice. Regular expressions are much better and more powerful.
    4. Related to #3, avoid using VB specific functions like Left(), Mid(), or Right() - use String.StartsWith(), String.EndsWith(), String.Substring() or regular expressions instead.
    5. Expanding on #4, avoid VB constructs left over from VB6. A prime example is using "vbCrLf" for inserting new lines instead of Environment.NewLine.
  2. Strive to program better
    Some of the rules below apply not to just VB.NET but to programming in general. Some of course apply only to VB.NET as for example you cannot get by compiling your program in other languages unless your variables are strongly typed (strongly typed simply means that each variable used has an assigned type at compile time such as integer, float or string).
    1. Strongly type your variables. "dim myVariable" might work for you but how do I know what data type it is. "dim myVariable as String" is much more clear.
    2. Initialize your variables. While for string this might not be needed, for complex object (such as DataTables), it is almost a must. You should simply do
      dim myData as New DataTable

      or
      dim myStringData as String = String.Empty
    3. Settle on a naming convention that works for you. I name my private member variables starting with "m_" while my public properties have the same name without the "m_". Methods and functions are named in with camel case as in "doSomething()". Whatever you decide is great as long as you are consitant and you stick to it.
    4. Do not explicetly check a boolean value for "False". Instead of
      If Directory.Exists(tempPath) = false Then
      use
      If Not Directory.Exists(tempPath) Then
    5. Use Path.Combine() to combine file system paths intead of concatinating strings with "&" or "+"
    6. Use String.Format() when outputting strings instead of simple concatenation:
      String.Format("{0}, {1}, {2}", row.Item("videoID"), row.Item("videoTitle"), row.Item("videoDate"))
    7. Do not assume the data type. A data table row may contain a string value but still use Convert.ToString() to get the value as
      Convert.ToString(row.Item("categoryName"))
    8. Related to #3, enable "Option Explicit" and "Option Strict" in your VB.NET projects. This will catch many potential errors at compile time and not let you build your program unless you correct the issues.
  3. Do not reinvent the wheel
    This point goes hand in hand with knowing the .NET framework but I feel it is worth mentioning on it's own. .NET has so many methods and functions you can use to write your application. Furthermore, there are plenty of open source projects written for all kinds of things:
    1. Log4Net for file, event, or trace logging.
    2. NAnt for task automation through XML build files and inline scripting
    3. SubSonic, NHibernate or dozen other projects for database access
    4. Check out .NET Zone, CodePlex, CodeProject or even Google before writing your own implementation. If you need something that can be useful in more than one occasion, it is very likely that somebody else has already written it. You can easily convert code from C# to VB.NET with a dozen of free tools such as the one found at http://labs.developerfusion.co.uk/convert/csharp-to-vb.aspx

Wednesday, February 06, 2008

Quick Tip on Including JavaScript files in Your Model-Glue Views

While programming with Model-Glue, I find that I have to often include JavaScript files in each individual view. I most often do this for form validation but it could be for whatever other reason. To keep things consistent, my project layout is based on the standard Model-Glue project layout with the editions of "css", "js" and "images":
  • trunk
    • config
    • controller
    • css
    • images
    • js
    • model
    • views

As you might have guessed, all my JavaScript files reside under "js". So until recently, I would include a JavaScript file as follows:

<script language="javascript" type="text/javascript">
<cfinclude template="../js/frmScanBadge.js" />
</script>

While this works, I do not like to have the hard coded path of the file. So instead, I settled on a solution that will get the name of the current view, look for a corresponding JavaScript file under the "js" directory and if finds one, it will include it:

<!--- If a JavaScript file exists with the name of the current template under the /js directory, include it --->
<cfif fileExists(expandPath("js/" & getFileFromPath(getCurrentTemplatePath()).replaceAll(".cfm", ".js")))>
<script language="javascript" type="text/javascript">
<cfinclude template="#'../js/' & getFileFromPath(getCurrentTemplatePath()).replaceAll('.cfm', '.js')#" />
</script>
</cfif>
This solution allows me to keep the name of the view and the corresponding JavaScript file the same. For new views, I can just drop a JavaScript file with the correct name in the "js" directory and it will be included in my view. Not insanely useful but nice non the less.

Tuesday, February 05, 2008

Capture Combination Keys in Your Windows Forms Application

Often while writing a Windows application, it can be helpful to capture a combination of keys such as Ctrl-F1 or Alt-F2. This can be a requirement when showing/hiding some special option that you would not want available by default. So here is how to do just that. To accomplish this task, the "ProcessDialogKey" function needs to be overridden as follows:
protected override bool ProcessDialogKey(Keys keyData)
{
    // Declare a variable to keep track of the key modifiers (Ctrl,Alt,Shift,etc.)
    string modifiers = "";

    // Get the actual integer value of the keystroke
    int keyCode = (int)keyData;

    // Check to see if the control key is on
    if ((keyCode & (int)Keys.Control) != 0)
    {
        modifiers += "";
    }

    // Check to see if the alt key is on
    if ((keyCode & (int)Keys.Alt) != 0)
    {
        modifiers += "";
    }

    // Check to see if the shift key is on
    if ((keyCode & (int)Keys.Shift) != 0)
    {
        modifiers += "";
    }

    // Strip off the modifier keys
    keyCode = keyCode & 0xFFFF;

    // Attempt to convert the remaining bits to the enum name
    Keys key = (Keys)keyCode;

    if ((key != Keys.Menu) && (key != Keys.ControlKey) && (key != Keys.ShiftKey))
    {
        // If the user pressed the Ctrl-F1 combination
        if (key == Keys.F1 && modifiers == "")
        {
            // Do something special
        }
    }

    // Return control to the base function
    return base.ProcessDialogKey(keyData);
}

Friday, February 01, 2008

Create a Paragraph Preview with Prototype.js

Creating a paragraph with expandable text can be quite useful if you have a lot of text on your page but do not want all of it to show in the same time. Something to the effect of:

This is a short preview of the paragraph (More)


so when the user clicks on "More" the full paragraph will be displayed like so:

This is a short preview of the paragraph and here is the more text you wanted to see.(Less)

You can see the simple example in action at http://tech-cats.net/blog/examples/createParagraphPreview.html

Here is how to create this with some simple HTML and JavaScript. The JavaScript relies on Prototype.js but it can be easily re-implemented with a different JavaScript framework.

<div>
	This is a short preview of the paragraph
	<div id="moreText" style="display: none">
	and here is the more text you wanted to see.
	</div>

	<a class="showMore" href="#" rel="moreText">
		<span id="do_more_less">More</span>
	</a>
</div>

And the JavaScript to do the magic:
<script type="text/javascript">
// For each link element that has the class name "showMore",
// tie the 'click' event to the 'showHide' function
Event.observe(window, 'load',
function() {
	$$('a.showMore').invoke('observe', 'click', showHide);
});

function showHide(event)
{
  // Use the event to get the id of the element that invoked the function
  var currentElement = Event.element(event);

  // Get the parent node of that element and the value of it's "rel" attribute
  var toggleContainer = $(currentElement).parentNode.getAttribute('rel');

  // Get the toggle caption based on the html of the clicked element
  var toggleCaption = ($(currentElement).innerHTML.toLowerCase() == "more") ? "Less" : "More";

  // Update the current element with the new caption value
  $(currentElement).update(toggleCaption);

  // If the container to be toggled exists, toggle it's visibility
  if ($(toggleContainer)) {
    $(toggleContainer).toggle();
  }

  // Return false so the link does not go anywhere
  return false;
}
</script>

Something to keep in mind is that you cannot simply copy the JavaScript in the head of the HTML page. That is because the line "$$('a.showMore').invoke('observe', 'click', showHide);", needs to fire after the HTML has been rendered. To resolve this, we can one of two things:

  1. Put the script in the head tag but wrap it inside a window load observer or dom:loaded event (if you are using Prototype v1.6) like so:
    Event.observe(window, 'load',
    function() {
    	$$('a.showMore').invoke('observe', 'click', showHide);
    });
    
    or
    document.observe("dom:loaded",
    function() {
    	$$('a.showMore').invoke('observe', 'click', showHide);
    });
    
  2. Put the script as the last thing on the page, after the html.

The code can also be downloaded at http://tech-cats.net/blog/examples/createParagraphPreview.txt

DZone Launches DZone Communities

Developer web site DZone has launched several new web sites each with specific focus - .NET, ColdFusion, Ajax, PHP, etc. Each web site or "zone" (as DZone folks are referencing the web sites) has zone leaders to write articles and contribute content. Their some famous names among the zone leaders such as John Resig (jQuery), Rey Bango, Brian Rinaldi and Dan Wilson.

I volunteered to write for the .NET, ColdFusion, Ajax and SQL zones. So from now on content that appears on my blog will also be posted on each appropriate zone web site.

// //]]>