Friday, 26 November 2010

Bogus Windows Azure Tools Installer Error Message

Many have probably gone before me, and figured this out, but just thought I’d post this small tip.  (At least one person had a similar problem as shown here).

If you go here to get the Windows Azure tools installer, and you install on a machine or VM that happens to ONLY have Visual Studio 2010 on it, but not Visual Studio 2008, you get this error message:

BogusAzureSDKInstallMessage

Huh?  This seems alarming, especially since it says “it is recommended that you address these warnings before continuing.”

But there’s no cause for Alarm, because it’s actually Bogus, with a capital “B”.

Why should I care that it won’t install the tools for VS 2008, if I don’t have VS 2008 on my machine?  Couldn’t MS have made the installer smart enough to figure that out, and shouldn’t that be okay?  Or display a more intelligent message, like “you don’t appear to have Visual Studio 2008 installed, so only the tools for VS 2010 will be installed” ?

Clicking “Continue” (with a capital “C”) causes it to install the tools for VS 2010.  There you have it, the ABCs of this installer…

Alarmed?  Bogus.  Continue.

Thursday, 7 October 2010

Lambda expressions and LINQ to XML

You know you’ve become a true geek when you find yourself getting excited about Lambda expressions.  I know it’s old news to some, but I’m just posting a couple of links (no pun intended) here to remind myself of a couple of cool postings on MSDN about Functional Programming and Lambda expressions, etc.  If you’re a C# programmer and you haven’t used this feature of the C# language, you really need to take a look at it.  It makes for some very readable, very terse yet powerful code.  In my own coding I’m just scratching the surface of what’s possible here but the more I use these techniques the more I like them.

Eric White’s posting on Lambda Expressions

Eric White’s posting on “Query Composition using Functional Programming Techniques in C# 3.0

Friday, 1 October 2010

This application has failed to start because the application configuration is incorrect.

 

“Reinstalling the application may fix this problem.”

Yeah, right!

Here’s the scenario… you’ve written a simple console app.  It’s working fine on your test VM, which has the exact same version of Windows (e.g. Server 2003) as the customer’s test server.  You deploy it, try to run it, and you get the following cryptic message box (identifying information obfuscated here):

01_InitialErrorMessage 

WTF?  You figure since it’s the same app, running under the same OS, and all you did was copy it, it must be an environmental issue, right?  But the customer verifies the .Net framework is properly installed; you even try copying the app from the problem machine to another machine in their environment; same issue; now you’re thinking maybe it’s a permissions thing, or something similar that’s preventing your app from running in their environment.

You Google it (or Bing it, or whatever it is you do ;-), and, you get a million hits of hog slop, none of which helps you.

You want to find out more so you go to “Computer Management” and check the System Event Log, and lo and behold, an event was logged related to what just happened:

02_ComputerMgmt

You look at the event, read something about a “Manifest Parse Error,” and think, hang on, this is a simple console app, it doesn’t have a manifest.  WTF (x 2)?

03_EventProperties

You post something on a Microsoft forum and you receive the following advice:

quote:

I recommend you use a tool named Cordbg.exe, which helps tools vendors and application developers find and fix bugs in programs that target the .NET Framework common language runtime. This tool uses the runtime Debug API to provide debugging services.

Fore how to use the tool to debug:
http://msdn.microsoft.com/en-us/library/a6zb7c8d(VS.80).aspx

If you still can't solve it with the tool, could you create a dump file about the application and send to me, here is how to get dump file:
http://msdn.microsoft.com/en-us/library/d5zhxt22.aspx 

end quote.

You spin your wheels on that for awhile, thinking, gee, this is a lot to have to deal with just to figure out why I’m getting this cryptic error.  Finally you start to think, hang on.  Let’s take a step back.  You think, what’s different between my dev machine and the customer’s environment?  And what’s up with this bogus Manifest Parse error?  Finally the light bulb lights.

Astute readers will have already figured this out.  If it was a snake it would’ve bit me, he says, smacking himself in the forehead.

What’s different?  What’s different is the config file that you deployed to their environment, which you then edited in their environment in Notepad, because they didn’t have Visual Studio in their environment, and you trashed it by entering some left or right bracket, thus corrupting your XML.  You didn’t know you did that, because unlike Visual Studio, Notepad doesn’t show you any nice red squiggly lines underneath your bad XML.

So you go grab the config file:

04_DirEntries

You copy its contents back into Visual Studio in your dev environment, and lo and behold, you see some red squiggly lines:

05_BadAppSetting

You correct the problem, copy the good XML back to the customer’s environment, and, whoopee!  Your app runs again, and all is well with the world.  A lengthy trial is held to determine whether you’re an idiot, or simply a human being who sometimes makes mistakes.  Your fingers are crossed; you may be let off the hook.

In my case the jury is still out!  ;-)

Bottom line:  if you get the error “the application has failed to start because the application configuration is incorrect,” check your app.config file (i.e. “yourApp.exe.config” after it’s been built) for bad XML, particularly the possibility of bad left and/or right angle brackets.

dB (doing my part to document MS error messages)

Wednesday, 4 August 2010

Using ReSharper with Log4Net

Blogging this to remind myself of certain steps, now that I’ve got it set up on a new project, and to share the love.  This is a great tip that I learned from Chris O’Brien when I worked with him on a project in 2009 at Parity Solutions (thanks again Chris!).  It takes a bit of setup, and there is a licensing cost for Resharper, but it’s a brilliant time saver for .Net developers.  The idea has several aspects, as follows:  a) logging is a critical part of any application, but entering the lines of logging code is tedious and repetetive; b) Log4Net is a good standardized way of implementing a logging infrastructure; and c) using Resharper in conjunction with Log4Net eliminates a lot of the tedium.  It’s like “code snippets on steroids.”

Setting up Log4Net

The goal here is to get the Log4Net.DLL referenced in your Visual Studio project.  Go to Log4Net and download.  There is lots of information about Log4Net on the web; use your favorite search engine to learn more (I don’t claim to be an expert, and I’m sure what I’m describing is a fairly rudimentary use of Log4Net, but it seems to work for me). 

Put the DLL in a subdirectory of your solution folder and add a reference to the Log4Net.DLL to your project (I like putting the DLL in a path like <SolutionFolder>/ThirdParty/Log4Net/Log4Net.DLL).

image

Here’s a screenshot of the structure of a test solution:

image

So I’ve got two projects.  First is a class library project that I call “Diagnostics” (and the other is a console app to test logging).  “Diagnostics” contains a reference to the Log4Net.DLL; and two classes.  One is a simple enum that corresponds to Log4Net’s different severity levels:

using System;

 

namespace SomeTestSolution.Diagnostics

{

    /// <summary>

    /// Severity of a log message.

    /// </summary>

    public enum LogLevel

    {

        Debug,

        Info,

        Warning,

        Error,

        FatalError

    }

 

}

 

The other class is the one that does the work.  It allows for two different types of logging messages; one where it’s just a simple string of text, and the other is one where you can create a string.Format (e.g. “Entering method {0} of class {1}”), then it also takes an array of objects which represent the parameters for the formatted string.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using log4net;

 

namespace SomeTestSolution.Diagnostics

{

    /// <summary>

    /// Provides log/trace functionality to configured log outputs (e.g. file, database, e-mail etc.)

    /// </summary>

    public class TraceHelper

    {

        /// <summary>

        /// Write trace output (currently using Log4Net), formatting the passed arguments into the Message string.

        /// </summary>

        /// <param name="Level">Severity level.</param>

        /// <param name="Log">Instance of logger from calling class.</param>

        /// <param name="Message">Message to write.</param>

        /// <param name="Args">Arguments to insert into message.</param>

        public static void WriteLineFormat(LogLevel Level, ILog Log, string Message, params object[] Args)

        {

            string message = null;

            if (Args != null)

                message = string.Format(Message, Args);

            else

                message = Message;

 

            WriteLine(Level, Log, message);

        }

 

        /// <summary>

        /// Write trace output (currently using Log4Net).

        /// </summary>

        /// <param name="Level">Severity level.</param>

        /// <param name="Log">Instance of logger (log4net ILog) from calling class.</param>

        /// <param name="Message">Message to write.</param>

        public static void WriteLine(LogLevel Level, ILog Log, string Message)

        {

            switch (Level)

            {

                case LogLevel.Debug:

                    if (Log.IsDebugEnabled)

                    {

                        Log.DebugFormat(Message);

                    }

                    break;

                case LogLevel.Info:

                    if (Log.IsInfoEnabled)

                    {

                        Log.InfoFormat(Message);

                    }

                    break;

                case LogLevel.Warning:

                    if (Log.IsWarnEnabled)

                    {

                        Log.WarnFormat(Message);

                    }

                    break;

                case LogLevel.Error:

                    if (Log.IsErrorEnabled)

                    {

                        Log.ErrorFormat(Message);

                    }

                    break;

                case LogLevel.FatalError:

                    if (Log.IsFatalEnabled)

                    {

                        Log.FatalFormat(Message);

                    }

                    break;

            }

        }

    }

}

 

Now let’s turn our attention to the other project in our solution called “LoggingTestApp”.  This is a console application with a single program as follows:

using System;

using log4net;

using SomeTestSolution.Diagnostics;

 

 

namespace LoggingTestApp

{

    class Program

    {

        // this line is required by log4net, in any class where logging is desired.

        // the parameter of "typeof" will be the name of the class.

        private static readonly ILog logger = LogManager.GetLogger(typeof(Program));

 

        static void Main(string[] args)

        {

            TraceHelper.WriteLineFormat(LogLevel.Debug, logger, "Entering method {0} of class {1}.", "Main","Program");

 

            Console.ReadLine();

 

        }

    }

}

Note the required “private static readonly ILog” line at the top.  This enabled the Log4Net functionality in this class.

Also note our (handwritten) line of code used to test the logging, the “TraceHelper.WriteLineFormat” line.  This is an example of the type of code we will automate later when I get into talking about Resharper.  (Note also the fact that the log level is set to “Debug”, which is a threshold you can configure in the App.config file.  You can set it to a higher severity level such as “Warn” or “Error” for production apps).

There are a couple more things we need in place for Log4Net to work.  First, we need to update the AssemblyInfo.cs file of our console application with a reference to Log4Net’s XmlConfigurator( ):

using System.Reflection;

using System.Runtime.CompilerServices;

using System.Runtime.InteropServices;

 

// General Information about an assembly is controlled through the following

// set of attributes. Change these attribute values to modify the information

// associated with an assembly.

[assembly: AssemblyTitle("LoggingTestApp")]

[assembly: AssemblyDescription("")]

[assembly: AssemblyConfiguration("")]

[assembly: AssemblyCompany("YourCompany")]

[assembly: AssemblyProduct("LoggingTestApp")]

[assembly: AssemblyCopyright("Copyright © YourCompany")]

[assembly: AssemblyTrademark("")]

[assembly: AssemblyCulture("")]

 

// this line is required to initiate log4net functionality

[assembly: log4net.Config.XmlConfigurator()]

(It should be noted you also need a reference to the Log4Net.DLL in the TestLoggingApp console application project).

Now we also need to set up our “appenders” for Log4Net in our App.config file (this is a fundamental Log4Net concept; it’s how we tell Log4Net where we want its logging output to go).  In this example I’ve got 3 appenders:  one sends output to the console; one is a “RollingFileAppender” which can be configured to roll to a new log file when it reaches specified boundaries of date and/or size; and one to send output to a trace listener.  For testing purposes I’ve got the size set to a very small size for the RollingFileAppender.

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

  <configSections>

    <!-- reference to log4net section -->

    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net, Version=1.2.10.0, Culture=neutral, PublicKeyToken=1b44e1d426115821" />

  </configSections>

 

  <!-- Configuration for log4net -->

  <log4net>

    <root>

      <!-- this is the default logging behaviour if no matching logger element is found below -->

      <level value="DEBUG" />

      <appender-ref ref="ConsoleAppender" />

      <appender-ref ref="RollingFileAppender" />

      <appender-ref ref="TraceAppender" />

    </root>

 

    <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">

      <layout type="log4net.Layout.PatternLayout">

        <conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" />

      </layout>

    </appender>

 

    <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">

      <file type="log4net.Util.PatternString" value="C:\YourAppDir\Logs\" />

      <AppendToFile value="true" />

      <DatePattern value="yyyyMMdd_HHmm'_trace.log'" />

      <CountDirection value="1" />

      <Threshold value="DEBUG" />

      <LockingModel type="log4net.Appender.FileAppender+MinimalLock" />

      <MaximumFileSize value="5KB" />

      <MaxSizeRollBackups value="-1" />

      <RollingStyle value="Composite" />

      <StaticLogFileName value="false" />

      <layout type="log4net.Layout.PatternLayout">

        <conversionPattern value="%date [%-5level] %logger - %message%newline" />

      </layout>

    </appender>

 

    <appender name="TraceAppender" type="log4net.Appender.TraceAppender">

      <threshold value="DEBUG" />

      <layout type="log4net.Layout.PatternLayout">

        <conversionPattern value="%date %-5level %logger - %message%newline" />

      </layout>

    </appender>

 

  </log4net>

</configuration>

Now, when we run our test app, lo and behold, we get a logging message to the console, and a log file:

image

Setting up Resharper

So that’s all well and good, but it would be tedious to have to write those logging lines by hand every time we want to log the fact that we’re entering a method, or leaving a method, or that we want to log an exception, etc.  Enter Resharper.

First, download and install Resharper.  You can use an evaluation copy if you’re not ready to pay the licensing fee.  It will integrate itself with your copy of Visual Studio, and you’ll see a new “Resharper” menu when you go into VS.  The main power of this whole idea comes from using what are called “Live Templates” in Resharper terminology.  Under the “ReSharper” menu go to “Live Templates…”

image

Click the “New Template” button in the “Templates Explorer”…

image

This will bring up the template editing window, as shown below.  In this example I’ve created a shortcut which will be triggered when the developer enters “lem” while editing a program.  Note how, by entering text surrounded by dollar signs, we can create a variable that will then let you select how ReSharper interprets that variable (see the menu on the right).  It lets you select things like “containing type member name,” which is what you want if you want to indicate the method in which you’re entering the line of code.  Once you’ve finished editing, you save the template.

 image

Now when we go to a program and type “lem” ReSharper displays a little menu of shortcuts.  We can then hit the Tab key and it will automatically enter the line, intelligently figuring out what method and class we’re in:

image

It also displays the “variable” data as entry fields, so we can Tab through them and change them if desired:

image

Once I tab through, the line is now entered as we would expect:

image

So in this case, I just had to type “lem, Tab, Tab, Tab” to enter a line of code that you might enter hundreds of times while writing an application.  Not bad!

Good practice suggests strings should not be hard-coded, but should exist in a Constants class (or a Resources file, or an XML file, or however your good practices manifest themselves).  In this case for the “EnteringMethod” string, I’m using a constant that looks like this:

image

Well, anyway, you get the idea.

Happy coding!

Friday, 21 May 2010

Using jQuery to enable keyboard navigation of a survey form that uses radio buttons in ASP.NET

I had been meaning to blog about this for awhile but haven’t had a chance until now. I did some work for my previous employer where the client had a survey form written in ASP.NET, using lots of radio buttons. The radio buttons were generated by a Repeater control.

The problem was that these surveys could get quite long and run over many pages. This meant the user would be doing a lot of mouse clicking and could end up getting carpal-tunnel syndrome by the time they were done, which could cause future legal problems! ;-). So the request was to enable some sort of keyboard navigation. As it was a purely client-side issue, it seemed like a perfect opportunity to use jQuery in conjunction with ASP.NET to enable the desired functionality.

I figured the most intuitive behavior would be as follows:

  • Give the user a visual clue as to what the “current” radio button is. Use CSS to style the button to put a box around it.
  • Use left, right, up and down arrow keys to go from the “current” button to the next appropriate button.
  • When you get to the end, wrap back around. So if you are at the top row and the user hits the up arrow key, the next “current” button would be at the bottom row. If you’re already at the far right, and you hit the right arrow key, the “current” button would flip back to the beginning of the row (i.e. the leftmost button).
  • When the user wants to “select” the current button (i.e., have the page behave as if the user had physically clicked that button with their mouse), let them use the Space bar or the Enter key to indicate selection.

The idea is simply to use the fact that the repeater control was generating radio button control names that sort of leant themselves to a kind of X,Y coordinate system whereby, if one knew the naming convention, one could figure out which row and column the current and desired target radio buttons. (Theoretically this could work with any technology, not just ASP.NET, assuming the buttons had a consistent naming convention that identified their X,Y coordinate in a grid of radio buttons).

I used a jQuery plug-in called jQuery hotkeys to handle the keyboard events. This worked beautifully. At the time I was using version 0.7.9 but it seems there have been some updates since then.

So apart from putting the jQuery library and jQuery hotkeys into your app, basically there are a few components:

1. The page source HTML that gets generated by ASP.NET, which includes the repeater control that renders radio buttons using some naming convention like this:

ctl00_mainContent_Repeater1_ctl00_AnswerControl_AnswerButtonList_0

a. The main pertinent bits that exist in the ASPX file are a ScriptManagerProxy reference which wraps the ScriptReference objects (I also had a div called “jQueryTestOutput” where I could use jQuery to write things while testing):

<div>

<asp:ScriptManagerProxy ID="ScriptManagerProxy1" runat="server">

<Scripts>

<asp:ScriptReference Path="~/js/jquery.hotkeys-0.7.9.js" />

<asp:ScriptReference Path="~/js/rvwAnswerKeyNav.js" />

</Scripts>

</asp:ScriptManagerProxy>

<div id="jQueryTestOutput">

</div>

</div>

b. There is also the reference to the repeater object (in this case I didn’t change what existed before, which was to format the output using HTML tables; you could use this same idea with different formatting, but in my case, if you look at my jQuery code below, “setPreviousAndCurrentCellStyles”, you can see I used jQuery to make a .css call that would re-style the TD that was wrapped around the the radio button) .

<table class="AnswerTable">

<tr>

<asp:Repeater ID="AnswerColumnRepeater" runat="server">

<ItemTemplate>

<td align="left">

<asp:Label ID="AnswerHeading" runat="server">

<%# ((SomeClassLibraryReference)Container.DataItem).Description %>

</asp:Label>

</td>

</ItemTemplate>

</asp:Repeater>

</tr>

</table>

2. Finally there is the jQuery code itself, which in this case is a file called “rvwAnswerKeyNav.js”. I’m not going to explain everything as it’s pretty well commented and should be fairly self-explanatory. Essentially I’m keeping track of the current button, and figuring out the target button based on which key the user hit. Then I re-style the old (button the user just left) and the new (target button the user wants to go to). If the user hits Space or Enter, I get the string representing the current button, and using that as the jQuery selector, I execute the .click method.

/*

jQuery code to handle arrow key navigation on Review/ViewReviewQuestions.aspx

This script works based on the fact that the radio buttons are named like this:

ctl00_mainContent_Repeater1_ctl00_AnswerControl_AnswerButtonList_0

where the "00" in the middle (substring 31, 2) will represent the row coordinate,

and the "0" at the end (substring 65, 2) will represent the column coordinate.

This makes it possible to figure out, when the user hits an arrow key,

which is the next radio button that should get the focus.

The illusion of "focus" is created with CSS styling of the TD element containing the button.

Then the Enter key or the space bar will cause the button to be clicked.

*/

// document ready handler

$(function() {

// an object to store global settings

var settings =

{

rowCeiling: 0,

columnCeiling: 0,

currentRow: 0,

currentColumn: 0,

previousCurrentRow: 0,

previousCurrentColumn: 0

}

$("form").data("globalSettings", settings);

// set row/column ceiling values by processing radio button set

$("input[type=radio]").SetRowAndColumnCeilings();

// keypress handling utilizes jQuery plug-in: js/jquery.hotkeys-0.7.9.js

// bind an event handler to each radio button to capture keypress events

$(document).bind('keydown', 'left', $.HandleLeftArrowKeypress);

$(document).bind('keydown', 'right', $.HandleRightArrowKeypress);

$(document).bind('keydown', 'up', $.HandleUpArrowKeypress);

$(document).bind('keydown', 'down', $.HandleDownArrowKeypress);

$(document).bind('keydown', 'space', $.HandleSpaceBarKeypress);

$(document).bind('keydown', 'return', $.HandleEnterKeypress);

// select the first radio button

$.SetCurrentCellStyle();

});

// utility and wrapper functions

(function($) {

// for testing purposes, a function to display output to the page

// (depends on the presence of a "jQueryTestOutput" element on ViewReviewQuestions.aspx)

$.say = function(text) {

$('#jQueryTestOutput').append('<div>' + text + '</div>');

}

// These "get position" functions assume the radio buttons are named like this:

// ctl00_mainContent_Repeater1_ctl00_AnswerControl_AnswerButtonList_0

// note this obscure JavaScript fact: if you try this:

// var myInt = parseInt('08') you will get zero as a result.

// why? because it treats '08' as an octal. Solution

// is to use parseInt('08',10) to explicitly indicate base 10.

// see http://blog.vishalon.net/index.php/be-careful-when-you-are-using-javascript-parseint-function

$.GetRowPositionFromID = function(text) {

// use base 10 radix to ensure correct results

return parseInt(text.substr(31, 2), 10);

}

$.GetColumnPositionFromID = function(text) {

// use base 10 radix to ensure correct results

return parseInt(text.substr(65, 2), 10);

}

$.DisplayRowColumnSettings = function() {

// for testing purposes

$.say("old row: " + $("form").data("globalSettings").previousCurrentRow +

", old col: " + $("form").data("globalSettings").previousCurrentColumn +

", new row: " + $("form").data("globalSettings").currentRow +

", new col: " + $("form").data("globalSettings").currentColumn);

}

$.HandleLeftArrowKeypress = function() {

/*

We're going left; that means the row will remain unchanged.

So figure out the target column. Rules:

1. if we're on column zero, the new column is the ceiling (i.e. the rightmost column).

2. if we're on any other column besides zero, the new column is minus 1.

*/

var targetColumn = 0;

var globalSettings = $("form").data("globalSettings");

var oldCurrentColumn = globalSettings.currentColumn;

var currentRow = globalSettings.currentRow;

if (oldCurrentColumn == 0) {

targetColumn = globalSettings.columnCeiling;

}

else {

targetColumn = oldCurrentColumn - 1;

}

// set the current column to the new value,

// and reset the "previous" values so the selected style can be cleared

globalSettings.currentColumn = targetColumn;

globalSettings.previousCurrentRow = currentRow;

globalSettings.previousCurrentColumn = oldCurrentColumn;

$.SetPreviousAndCurrentCellStyles();

// for testing:

//$.DisplayRowColumnSettings();

}

$.HandleRightArrowKeypress = function() {

/*

We're going right; that means the row will remain unchanged.

So figure out the target column. Rules:

1. if we're at the rightmost column, the new column is zero (i.e. the leftmost column).

2. if we're on any other column besides the rightmost, the new column is plus 1.

*/

var targetColumn = 0;

var globalSettings = $("form").data("globalSettings");

var oldCurrentColumn = globalSettings.currentColumn;

var currentRow = globalSettings.currentRow;

var columnCeiling = globalSettings.columnCeiling;

if (oldCurrentColumn == columnCeiling) {

targetColumn = 0;

}

else {

targetColumn = oldCurrentColumn + 1;

}

// set the current column to the new value,

// and reset the "previous" values so the selected style can be cleared

globalSettings.currentColumn = targetColumn;

globalSettings.previousCurrentRow = currentRow;

globalSettings.previousCurrentColumn = oldCurrentColumn;

$.SetPreviousAndCurrentCellStyles();

// for testing:

//$.DisplayRowColumnSettings();

}

$.HandleUpArrowKeypress = function(e) {

/*

We're going up; that means the column will remain unchanged.

So figure out the target row. Rules:

1. if we're at the first (top) row (row zero), the new row will be the row ceiling

(i.e. the highest numbered row, i.e the bottom row).

2. if we're on any other row besides the topmost, the new row is minus 1.

*/

var targetRow = 0;

var globalSettings = $("form").data("globalSettings");

var oldCurrentRow = globalSettings.currentRow;

var currentColumn = globalSettings.currentColumn;

var rowCeiling = globalSettings.rowCeiling;

if (oldCurrentRow == 0) {

targetRow = rowCeiling;

}

else {

targetRow = oldCurrentRow - 1;

}

// set the current row to the new value,

// and reset the "previous" values so the selected style can be cleared

globalSettings.currentRow = targetRow;

globalSettings.previousCurrentRow = oldCurrentRow;

globalSettings.previousCurrentColumn = currentColumn;

$.SetPreviousAndCurrentCellStyles();

// for testing:

//$.DisplayRowColumnSettings();

// suppress the default scrolling behavior caused by the up/down arrows.

// the arrows on the review page will only be used to navigate radio buttons,

// not scroll up and down. Some people hate this idea but it makes sense here,

// because the default scrolling behavior (in my opinion) is confusing. See:

// http: //stackoverflow.com/questions/910724/is-it-possible-to-prevent-document-scrolling-when-arrow-keys-are-pressed

e.preventDefault();

return false;

}

$.HandleDownArrowKeypress = function(e) {

/*

We're going down; that means the column will remain unchanged.

So figure out the target row. Rules:

1. if we're at the last (bottom) row (the row ceiling), the new row will be row zero (top row).

2. if we're on any other row besides the bottom, the new row is plus 1.

*/

var targetRow = 0;

var globalSettings = $("form").data("globalSettings");

var oldCurrentRow = globalSettings.currentRow;

var currentColumn = globalSettings.currentColumn;

var rowCeiling = globalSettings.rowCeiling;

if (oldCurrentRow == rowCeiling) {

targetRow = 0;

}

else {

targetRow = oldCurrentRow + 1;

}

// set the current row to the new value,

// and reset the "previous" values so the selected style can be cleared

globalSettings.currentRow = targetRow;

globalSettings.previousCurrentRow = oldCurrentRow;

globalSettings.previousCurrentColumn = currentColumn;

$.SetPreviousAndCurrentCellStyles();

// for testing:

//$.DisplayRowColumnSettings();

// suppress the default scrolling behavior caused by the up/down arrows.

// the arrows on the review page will only be used to navigate radio buttons,

// not scroll up and down. Some people hate this idea but it makes sense here,

// because the default scrolling behavior (in my opinion) is confusing. See:

// http: //stackoverflow.com/questions/910724/is-it-possible-to-prevent-document-scrolling-when-arrow-keys-are-pressed

e.preventDefault();

return false;

}

$.HandleSpaceBarKeypress = function() {

$.ClickCurrentButton();

}

$.HandleEnterKeypress = function() {

$.ClickCurrentButton();

}

$.ClickCurrentButton = function() {

var currentButtonSelector = $.GetCurrentButtonSelectorString();

$(currentButtonSelector).click();

}

$.GetCurrentButtonSelectorString = function() {

var globalSettings = $("form").data("globalSettings");

var currentRow = globalSettings.currentRow;

var currentColumn = globalSettings.currentColumn;

var strCurrentRow = $.toFixedWidth(currentRow, 2, '0');

return "input[id=ctl00_mainContent_Repeater1_ctl" +

strCurrentRow + "_AnswerControl_AnswerButtonList_" + currentColumn + "]"

}

$.GetPreviousButtonSelectorString = function() {

var globalSettings = $("form").data("globalSettings");

var previousRow = globalSettings.previousCurrentRow;

var previousColumn = globalSettings.previousCurrentColumn;

var strPreviousRow = $.toFixedWidth(previousRow, 2, '0');

return "input[id=ctl00_mainContent_Repeater1_ctl" +

strPreviousRow + "_AnswerControl_AnswerButtonList_" + previousColumn + "]"

}

$.SetCurrentCellStyle = function() {

// called directly when the page is first loaded,

// because at that point we don't want to set the "previous" style

var currentButtonCellSelector = "td > " + $.GetCurrentButtonSelectorString();

var currentCell = $(currentButtonCellSelector);

currentCell.css('border-style', 'solid').css('border-width', '1px');

}

$.SetPreviousAndCurrentCellStyles = function() {

// clear the "previous" cell's border styles,

// and set the new "current" cell's border styles,

// to indicate that cell contains the "current" button

var previousButtonCellSelector = "td > " + $.GetPreviousButtonSelectorString();

var previousCell = $(previousButtonCellSelector);

previousCell.css('border-style', 'none');

// factored out so it can be called here or called directly in ready handler

$.SetCurrentCellStyle();

}

$.toFixedWidth = function(value, length, fill) {

// utility function from the book "jQuery in Action"

if (!fill) fill = '0';

var result = value.toString();

var padding = length - result.length;

if (padding < 0) {

result = result.substr(-padding);

}

else {

for (var n = 0; n < padding; n++) result = fill + result;

}

return result;

};

$.fn.SetRowAndColumnCeilings = function() {

return this.each(function() {

// sets up row/column ceilings based on values

// derived from the names of the radio buttons

var rowPos = $.GetRowPositionFromID(this.id);

var colPos = $.GetColumnPositionFromID(this.id);

var globalSettings = $("form").data("globalSettings");

/*

Dynamically set the global values for row and column ceiling

based on the row/column coordinates of the radio buttons being processed;

when the user is navigating with arrow keys, we need to know if

they've reached the right-most column, or the bottom row;

thus we store those values globally. This logic allows for the

possibility that rows or columns may be added in the future.

*/

// reset the row ceiling if necessary, based on the current row value

var currentRowCeiling = globalSettings.rowCeiling;

if (rowPos > currentRowCeiling) {

globalSettings.rowCeiling = rowPos;

}

// reset the column ceiling if necessary, based on the current column value

var currentColumnCeiling = globalSettings.columnCeiling;

if (colPos > currentColumnCeiling) {

globalSettings.columnCeiling = colPos;

}

});

}

})(jQuery)

Apologies for not putting up screenshots; I don’t have access to that site anymore, but if everything is configured correctly, this works. Maybe some variation of this idea might work in your situation. I hope you may find it useful.

Regards, –Dave