Thursday, March 19, 2009

Fixing a NullReferenceException with NAnt 0.86-beta1 and .Net 3.5

My attempts at building a .Net 3.5 application with NAnt 0.86-beta1 resulted in a NullReferenceException before it ran any targets. Debugging NAnt revealed the error. The NAnt.exe.config file in my NAnt installation had this readregistry task in the framework element for the "net-3.5" framework, near line 470:

<readregistry
    property="sdkInstallRoot"
    key="SOFTWARE\Microsoft\Microsoft SDKs\Windows\v6.0A\WinSDKNetFxTools\InstallationFolder"
    hive="LocalMachine"
    failonerror="false" />

The error is the key attribute, which I changed to:

SOFTWARE\Microsoft\Microsoft SDKs\Windows\v6.1\WinSDKNetFxTools\InstallationFolder

because I had installed the .Net 3.5 SDK after the release of .Net 3.5 SP1.

Sunday, March 15, 2009

Complications

Having set up a nice build process using NAnt, I didn't expect to have to pick up another build tool. However, in my endeavor to create a development environment without Visual Studio, I've run into stumbling block to creating WPF applications. It seems that Microsoft distributes no XAML compiler with the .Net Framework. Instead, they built that ability into MSBuild. So much for simplicity.

Saturday, March 14, 2009

NAnt: Getting Subversion Revision

I'm sure this may be old news for many of you, but it isn't for me. This NAnt include file extracts the revision number for the HEAD revision for the base project directory (the directory of the build file that was invoked) into a property called "svn.revision":

<project>
    <property name="svn.revision" value="0" />
    <target name="svn-revision">
        <exec
            program="svn"
            commandline="info ${project::get-base-directory()}@HEAD --xml"
            output="_svnrev.xml"
            failonerror="false" />
        <xmlpeek
            file="_svnrev.xml"
            xpath="info/entry/commit/@revision"
            property="svn.revision"
            failonerror="false" />
        <delete file="_svnrev.xml" />
        <echo message="SVN Revision: ${svn.revision}" />
    </target>
</project>

This script requires a Subversion client. Being a TortoiseSVN fan, I looked into its automation potential. Unfortunately, it wouldn't do anything without popping up window of some sort, and I sought a quieter approach. Hence, the command-line Subversion client.

The exec task invokes the SVN "info" subcommand, passing the option to output the results as XML. Then, it stores the XML in a temporary file. Finally, the xmlpeek task uses XPath to read the revision number from the temporary file.

Tuesday, July 22, 2008

Getting SQL Server 2005 Default Data and Log Locations... My Way

My current project uses backup devices to accomplish a few tasks. The scripts that I wrote to create these backup devices used hard-coded paths until the project required the backup devices to use the drive that the SQL Server 2005 instance points to for its default database file locations. Therefore, I needed a way to find that drive letter.

Immediate results for using T-SQL to get those default locations suggested using an undocumented system stored procedure, xp_instance_regread. This sproc works just fine, returning instance-specific registry values. Pretty slick, if you ask me.

However, I don't like using something that I don't understand. In this case, I didn't put together how you could expect a "DefaultLog" or "DefaultData" key to exist using a registry path like "Software\MSSQLServer\MSSQLServer" with SQL Server 2005. If you browse the registry, you won't find a "DefaultData" or "DefaultLog" key at that location. I sure didn't. In fact, I found very little at that location, which puzzled me, and I didn't find any explanation for this behavior anywhere on the Internet.

On the other hand, I understood how xp_regread works. It will read only keys that you can find by hand. So, I came up with these UDF's that do the same job as xp_instance_regread in a way that I can feel comfortable about:

create function udf_GetDefaultDataFilePath ()
returns nvarchar(260)
as
begin
    declare @SystemInstanceName nvarchar(200),
            @RegKey nvarchar(512),
            @Path nvarchar(260);
 
    set @SystemInstanceName = dbo.udf_GetSystemInstanceName();
 
    set @RegKey = N'Software\Microsoft\Microsoft SQL Server\' +
                    @SystemInstanceName + '\MSSQLServer';
 
    exec master.dbo.xp_regread
            N'HKEY_LOCAL_MACHINE',
            @RegKey,
            N'DefaultData',
            @Path output;
 
    if @Path is null
    begin
        set @RegKey = N'Software\Microsoft\Microsoft SQL Server\' +
                        @SystemInstanceName + '\Setup';
  
        exec master.dbo.xp_regread
            N'HKEY_LOCAL_MACHINE',
            @RegKey,
            N'SQLDataRoot',
            @Path output;
  
        set @Path = @Path + '\Data';
    end
 
    return @Path;
end

create function udf_GetDefaultLogFilePath ()
returns nvarchar(260)
as
begin
    declare @SystemInstanceName nvarchar(200),
            @RegKey nvarchar(512),
            @Path nvarchar(260);
 
    set @SystemInstanceName = dbo.udf_GetSystemInstanceName();
 
    set @RegKey = N'Software\Microsoft\Microsoft SQL Server\' +
                    @SystemInstanceName + '\MSSQLServer';
 
    exec master.dbo.xp_regread
            N'HKEY_LOCAL_MACHINE',
            @RegKey,
            N'DefaultLog',
            @Path output;
 
    if @Path is null
    begin
        set @RegKey = N'Software\Microsoft\Microsoft SQL Server\' +
                        @SystemInstanceName + '\Setup';
  
        exec master.dbo.xp_regread
            N'HKEY_LOCAL_MACHINE',
            @RegKey,
            N'SQLDataRoot',
            @Path output;
  
        set @Path = @Path + '\Data';
    end
 
    return @Path;
end

create function udf_GetSystemInstanceName ()
returns nvarchar(10)
as
begin
    declare @InstanceName nvarchar(200),
            @SystemInstanceName nvarchar(10);

    set @InstanceName = convert(nvarchar(20), serverproperty('InstanceName'));

    if @InstanceName is null
        set @InstanceName = 'MSSQLSERVER';

    exec master.dbo.xp_regread
            N'HKEY_LOCAL_MACHINE',
            N'Software\Microsoft\Microsoft SQL Server\Instance Names\SQL',
            @InstanceName,
            @SystemInstanceName output;

    return @SystemInstanceName
end

The registry keys reside in an instance-specific location, so udf_GetSystemInstanceName() figures out the underlying name for the instance. (The first instance is "MSSQL.1", the second is "MSSQL.2", and so on.) The other two use this value to compose the registry path where the "DefaultData" and "DefaultLog" keys reside, respectively. If the key does not exist, each function returns the default default location, the typical "<installdir>\<instance>\MSSQL\Data".

Tuesday, March 14, 2006

Getting Around the ClickOnce MSB3113 Problem

ClickOnce is great technology. Publish your application to a web site or network share, and users click a link in a web page to install your application in Windows as if they were installing a browser plug-in. Publish a new version of the application, and your application will tell the users about it. Then, they can opt to install the new version or skip it. If they install a new version and don't like it, they can use the "Add/Remove Programs" control panel to roll back the application to the previous version. To fellow employees at my company—my users—installation and updates of my application have worked without incident, and everyone likes the process.

However, a complication occurred on my end. After publishing my project to a network share, Visual Studio 2005 began showing an error during the build process. Perhaps you know the error, the one described in knowledge base article 907757:

Error MSB3113: Could not find file 'Microsoft.Windows.CommonLanguageRuntime, Version=2.0.50727.0'.

The article says that this is a bug in VS, and the workaround is to use a file reference instead of a project reference to your Windows Forms project. However, I could not accept using a file reference. I'd have to change it whenever I changed solution configurations because the file reference could only point to one configuration's output. Plus, I did not find a way to implement conditional references. Fortunately, I found my own workaround that allows me to continue using a project reference.

According to the article, project references to a Windows Forms project cause the error, and in my solution, two class library projects each had a reference to a Windows Forms project. The class libraries used the business classes I had added to the Windows Forms project, so I moved the business logic into a separate project. Then, to the other three projects I added a reference to the new project. As a result, I have business logic seperate from UI logic, and my build no longer has the error.

Friday, February 10, 2006

RowEntryFixture for .Net FitNesse

Many acceptance tests need test data to work with, and RowEntryFixture is a simple fixture made for just that. You create a class that inherits from RowEntryFixture and override its EnterRow method to add an item of data from the corresponding table in your acceptance test.

When I started using FitNesse for my .Net projects, however, I discovered that the FitNesse developers didn't add RowEntryFixture to the .Net part of FitNesse. So I ported the Java version to .Net, and I built it into my .Net FitNesse replacement bundle. Here's the code for anyone wanting to build FitNesse with it:

using System;
using System.IO;

using fit;

namespace fitnesse.fixtures
{
    public abstract class RowEntryFixture : ColumnFixture
    {
        public abstract void EnterRow();

        public const String ERROR_INDICATOR = "Unable to enter last row: ";
        public const String RIGHT_STYLE = "pass";
        public const String WRONG_STYLE = "fail";

        public override void DoRow(Parse row)
        {
            if(row.Parts.Body.IndexOf(ERROR_INDICATOR) != -1)
                return;

            base.DoRow(row);
            try
            {
                EnterRow();
                Right(AppendCell(row, "entered"));
            }
            catch(Exception e)
            {
                Wrong(AppendCell(row, "skipped"));
                ReportError(row, e);
            }
        }

        protected Parse AppendCell(Parse row, String text)
        {
            Parse lastCell = new Parse("td", text, null, null);
            row.Parts.Last.More = lastCell;
            return lastCell;
        }

        public void ReportError(Parse row, Exception e)
        {
            Parse errorCell = MakeMessageCell(e);
            InsertRowAfter(row, new Parse("tr", null, errorCell, null));
        }

        public Parse MakeMessageCell(Exception e)
        {
            Parse errorCell = new Parse("td", "", null, null);
            StringWriter buffer = new StringWriter();

            buffer.Write(e.StackTrace);
            errorCell.AddToTag(" colspan=\"" + (ColumnBindings.Length + 1) + "\"");
            errorCell.AddToBody("<i>" + ERROR_INDICATOR + e.Message + "</i>");
            errorCell.AddToBody("<pre>" + (buffer.ToString()) + "</pre>");
            Wrong(errorCell);

            return errorCell;
        }

        public void InsertRowAfter(Parse currentRow, Parse rowToAdd)
        {
            Parse nextRow = currentRow.More;
            currentRow.More = rowToAdd;
            rowToAdd.More = nextRow;
        }

    }
}

Building FitNesse and FitLibrary for .Net 2.0

Here's how I built both FitNesse and FitLibrary for .Net 2.0. (If you don't have fifteen minutes for this, help yourself to my .Net FitNesse replacement bundle.)

UPDATE 25OCT2006: These instructions for building FitNesse may not work. Around May 2006, the developers of FitNesse merged their .Net FitServer with FitLibrary.NET into a new SourceForge project. (See the .Net page at FitNesse for more information.) I will try to investigate this, but I make no promise of updating these instructions.

First, let's tackle FitNesse:

  1. Download the latest source bundle for FitNesse. As of today, the latest version is 20060209.
  2. Create a directory in your "Visual Studio 2005\Projects" directory called "FitNesse".
  3. From the FitNesse source bundle, extract the contents of the "dotnet" directory (but not the directory itself) into the folder you just created.
  4. Open the "fitnesse.sln" solution file. This will start VS 2005 and invoke the Conversion Wizard.
  5. Click the "Finish" button, and the wizard converts the solution into one compatible with VS 2005.
  6. Click the "Close" button. If VS displays the conversion summary, then it encountered errors. Scan for the errors and resolve them. If the conversion had no errors, then VS will not display this summary.
  7. If you're using the same FitNesse version as I did, then you will see two errors in the summary regarding "TableFixture.cs" and "TableFixtureTest.cs". The errors occurred because, for some reason, the two files did not make it into the source bundle. Grab them from the latest release source bundle, and put them into the "fitnesse" folder in the "fit" project.
  8. In each of those files, add a using fit; line and change the namespace from fit to fitnesse.fixtures.
  9. For each project:
    1. Right click the project, and choose "Properties" from the context menu.
    2. In the properties page that appears, click the "Build Events" tab.
    3. Delete the pre-build event command line.
    4. Change the post-build event command line to
      copy "$(TargetPath)" "$(SolutionDir)"
      copy "$(TargetPath)" "full path to FitNesse 'dotnet' directory"
      including the double quotes.
  10. Build the solution. (Press F6.) You should see no errors, but you may get warnings. I addressed one of the two that appeared for me. I changed a method call from GetByHostName to GetHostEntry.

And now for FitLibrary:

  1. Download the latest bundle of the .Net version of FitLibrary. As of today, the latest version is 20060117.
  2. Extract the contents of the bundle to your "Visual Studio 2005\Projects" directory. This will add a "FitLibrary.NET" project folder.
  3. Open the "fitlibrary.sln" solution file, and VS 2005 will start its Conversion Wizard.
  4. Click "Finish", and click "Close" when the wizard finishes. If you get errors, address them now. (I did not get any errors.)
  5. For the "FitLibrary" project:
    1. Right click the project, and choose "Properties" from the context menu.
    2. In the properties page that appears, click the "Build Events" tab.
    3. Delete the pre-build event command line. No longer necessary (25OCT2006)
    4. Change the post-build event command line to
      copy "$(TargetPath)" "full path to FitNesse 'dotnet' directory"
      including the double quotes. (Updated 25OCT2006)
  6. In the "FitLibrary" project, remove the reference to "fit", and add a reference to the "fit.dll" in your FitNesse installation's "dotnet" directory.
  7. Build the solution. (Press F6.) It should succeed.

Now you have a .Net 2.0 compatible version of FitNesse and FitLibrary ready for you in your FitNesse installation. Happy Testing!