Sergio and the sigil

It's obvious, use TimeSpans to measure time

Posted by Sergio on 2008-07-24

I know, this is probably not news to anyone. TimeSpan is the native .NET type to represent time intervals. But answer quickly, SqlCommand.CommandTimeout represents the interval in milliseconds, seconds, or minutes? What about System.Timers.Timer.Interval ?

To circumvent this situation we see all sorts of API design contortions, like trying to standardize all time units to the same unit, which sometimes is not viable, or using unnatural affixes when naming the class members, for example.

class CalendarEvent
{
	public string Name { get; set; }
	public int DurationMinutes { get; set; }
}
//or..
class EquipmentError
{
	public int ErrorCode { get; set; }
	public double MillisecondsTotal { get; set; }
}
//or...
class ProjectPhase
{
	public int ProjectID { get; set; }
	public string PhaseName { get; set; }
	public int PhaseWeeksDuration { get; set; }
}

I think this stinks. Why do we constantly ignore the TimeSpan structure? I know it's kind of the bastard child of the System namespace. It lacks for example string formats. It's larger than a simple Int32. But the complete annihilation of any doubt as to what time unit we are using is worth all that.

The previous examples could be made clearer with TimeSpans.

class CalendarEvent
{
	public string Name { get; set; }
	public TimeSpan Duration { get; set; }
}
//or..
class EquipmentError
{
	public int ErrorCode { get; set; }
	public TimeSpan TotalTime { get; set; }
}
//or...
class ProjectPhase
{
	public int ProjectID { get; set; }
	public string PhaseName { get; set; }
	public TimeSpan PhaseDuration { get; set; }
}

But let's not stop there. We can simplify our lives by, for example, creating some extension methods to deal with time interval tasks. I don't show below, but we could very well write an extension method to fix the lack of a TimeSpan.ToString(string format) method.

using System;
namespace Utils.Extensions.Time
{
	public static class TimespanExt
	{
		public static TimeSpan Minutes(this int interval)
		{
			return Minutes((double)interval);
		}
		
		public static TimeSpan Minutes(this double interval)
		{
			return TimeSpan.FromMinutes(interval);
		}

		public static TimeSpan Seconds(this int interval)
		{
			return Seconds((double)interval);
		}

		public static TimeSpan Seconds(this double interval)
		{
			return TimeSpan.FromSeconds(interval);
		}

		//.. a metric ton more utility methods like these...
	}
}

With these extension methods, we get some handy syntax to ease the creation of TimeSpans.

TimeSpan interval = 5.Seconds();
TimeSpan elapsedTime = 0.7.Minutes();

And don't forget to use the Nulalble<TimeSpan>, a.k.a. TimeSpan? when the interval is optional. I think the nullable is clearer than using TimeSpan.Zero (or TimeSpan.MinValue — argh!!!) to represent unknown or missing values.

UdtTypeName and .NET data types in SQL

Posted by Sergio on 2008-06-11

We are working on SQL 2008 for a new project with the intent to leverage the new data types, in this case especially the HIERARCHYID data type. One of the stored procedures we created had a parameter of type HIERARCHYID.

The code that calls the stored procedure was something like this.

SqlHierarchyId nodeValue = GetNodeValueFromSomewhere();
var cmd =new SqlCommand("usp_EnableNode");
cmd.CommandType = CommandType.StoredProcedure;
var  nodeParam = myCommand.Parameters.Add("@Node", SqlDbType.SqlHierarchyId);
nodeParam.Value = nodeValue; 
var enableParam = myCommand.Parameters.Add("@Enable",  SqlDbType.SqlBit);
enableParam.Value = true;
cmd.ExecuteNonQuery();

When running this code we were getting the error "UdtTypeName property must be set for UDT parameters." Since the HIERARCHYID data type is a .NET type in SQL 2008 and we had not played with .NET types in SQL 2005 yet, we scratched our heads for a few minutes.

We tried both:

nodeParam.UdtTypeName = typeof(SqlHierarchyId).Name; 

And

nodeParam.UdtTypeName = typeof(SqlHierarchyId).FullName; 

But the solution was much simpler:

nodeParam.UdtTypeName = "HierarchyId";

This should be needed for the new GEOMETRY and GEOGRAPHY data types as well. Maybe this helps someone else.

Excel is the business format, automate it

Posted by Sergio on 2008-06-09

One thing I realized when working in large companies is that Excel is the true data exchange file format. That what the business types exchange among themselves and how they like to persist any kind of lists or anything that needs to be formatted like a table (be it tabular data or not.)

All too often one of this documents is forwarded to me containing some form of business data that will become all sorts of things: lookup data, configuration values, updates to an existing table, etc.

I dread these conversion tasks when I have to do them manually so I tend to automate the task as soon as it smells like a candidate for recurrence. That way I'll take a hit on the first time and reap the benefits from then on.

For some reason I like writing these automation scripts in Ruby. I could probably almost as easily write them in VBScript, C#, PowerShell or even VBA but my experience with Ruby is that it tends to be shorter (as in more concise) and easier to develop. Of course that varies with your level of familiarity with the Ruby libraries, but the learning curve isn't bad at all.

Consider the following hypothetical layout for an Excel file dropped in your inbox for import.

DescriptionPriceCategory
1DVD Player76.49Electronics
2Rain Coat35.10Men's apparel
3Code Complete49.99Books
............
12024" Monitor499.99Computer Accessories

My typical script to process such file would be the obvious line-by-line read, maybe processing each cell before ultimately converting the row to its final format (or skipping that row.) The skeleton of that kind of script is shown below.

require 'win32ole'

begin 
  excel_app = WIN32OLE.new('Excel.Application')
  book = excel_app.Workbooks.Open("c:\\project\\data\\datafile.xls")
  sheet = book.Worksheets(1)
  #first row contains only the headers
  row = 2 #start at the 2nd line (excel is 1-based)

  #stop when we find an empty row
  while sheet.Cells(row, 1).Value
    data = []
    (1..4).each {|c| data[c] = sheet.Cells(row, c).Value }
    #at this point the 'data' array contains the entire row
    #use it as desired *********
    
    row += 1
  end
  
rescue 
  #oops...
  puts "There was an error: #{$!}"
ensure
  #cleanup
  excel_app.DisplayAlerts = 0
  book.Close if book
  excel_app.Quit
end

But, no matter if you use Ruby or any other language, the value in automating these tasks can be tremendous. Taking the time to learn the basics of Excel automation is, in my opinion, very important and will let your users continue to use the tool that they like.

In a kind of reverse example, I have had occasions when one of the business users was repeatedly requesting for some of the data in Excel format during the application development (while we didn't have a web page to show that data yet) so, after he asked for a updated report for the second time, I created a script to read the data from the database, create an Excel file, and send the file attached in an email message every Monday morning. Easy way to gain a happy user.

Update: As noted in the comments, one nice reference for automating with Ruby is Ruby on Windows

Coding style per project

Posted by Sergio on 2008-06-01

A few months ago, when I installed Visual Studio 2008 on my system, I opened one of my projects, did the usual project conversion, edited some files, created a few others, saved, tested, and checked in source control. Another developer working on the same project, also with VS2008, later edited some of the same files I had edited.

Then I went to do a diff to verify the changes and I saw what I always hate to see. He hadn't changed a lot of code, but he was using different style settings in VS and all the extra spaces around ifs, function arguments, etc were making it harder to find the important changes. To be fair, I was the one that forgot to adjust my default settings to be more compatible with our current coding style.

The situation above is not all that critical, but is certainly a big annoyance. Some diff programs will allow you to ignore white space differences but it is still sickening to see your source code with mixed code formatting style in the same file, sometimes in the same method.

That kept me thinking: why aren't code format style settings part of the project? Imagine if you work for two different clients, with different standards, or that you are a contributor to Open Source projects that also have different opinions about code format. It's too easy to forget to adjust your settings every time you switch projects, even if you export and save the appropriate VS settings to import as needed. Any IDE with so many options as VS should offer an option to save any settings that deal with the actual code be part of the project. A developer should be able to explicitly ignore or change the project settings but not by default.

With that problem in mind, I thought it would be useful to just adopt the practice of exporting the VS settings that are relevant to the project coding style and checking that in source control too. Here's what I did.

Fire up the the Import and Export settings wizard and choose only the settings that relate to code formatting and for the languages the project uses, for example C#, HTML, and XML. In this case we will just do the C# settings.

 

Right here we hit a limitation in VS. There's no way to select only the code formatting settings for C#, so we will export all of them anyway. Once we do that, save the file to the project directory (like MyProject.vssettings) and add it to the project or solution. Open it in VS (it's just XML) an stare at the plethora of settings that are saved.

<UserSettings>
	  <ApplicationIdentity version="9.0"/>
	  <ToolsOptions>
	    <ToolsOptionsCategory name="TextEditor"
	    RegisteredName="TextEditor">
	      <ToolsOptionsSubCategory name="CSharp"
	    RegisteredName="CSharp" PackageName="Text Management Package">
	        <PropertyValue name="TabSize">4</PropertyValue>
	        <PropertyValue name="AutoListMembers">true</PropertyValue>
	        <!-- ...(snip)... -->
	        <PropertyValue name="AutoListParams">true</PropertyValue>
	      </ToolsOptionsSubCategory>
	      <ToolsOptionsSubCategory name="CSharp-Specific"
	    RegisteredName="CSharp-Specific"
	    PackageName="Visual C# Language Service Package">
	        <PropertyValue name="NewLines_QueryExpression_EachClause">1</PropertyValue>
	        <PropertyValue name="Space_Normalize">0</PropertyValue>
	        <PropertyValue name="WarnWhenMembersCauseCompilerGeneratedReferences">1</PropertyValue>
	        <PropertyValue name="CollapseInactiveBlocksOnOpen">1</PropertyValue>
	        <PropertyValue name="Watson_MaxExceptionsToReport">1</PropertyValue>
	        <PropertyValue name="EditAndContinueReportEnterBreakStateFailure">1</PropertyValue>
	        <!-- ...(snip)... -->
	        <PropertyValue name="RemoveUnusedUsings">1</PropertyValue>
	        <PropertyValue name="Rename_Overloads">0</PropertyValue>
	        <PropertyValue name="EncapsulateField_SearchInComments">0</PropertyValue>
	        <PropertyValue name="ProgressDialogDelaySeconds">2</PropertyValue>
	      </ToolsOptionsSubCategory>
	    </ToolsOptionsCategory>
	  </ToolsOptions>
	</UserSettings>

Upon closer inspection, we can more or less recognize each settings by their names. Let's remove the ones that don't seem to be related to code formatting. This is more or less a value judgment on the importance of some of the settings. My choice was to keep TabSize, IndentStyle, and InsertTabs in the first subcategory and, for the second subcategory, I'm keeping any of the setting whose name starts with NewLines_, Indent_, or Space_ and also the item named SortUsings.(I should probably just write a macro to do that at some point)

After all this sanitizing, my settings file is reduced to the following:

<UserSettings>
	  <ApplicationIdentity version="9.0"/>
	  <ToolsOptions>
	    <ToolsOptionsCategory name="TextEditor"
	    RegisteredName="TextEditor">
	      <ToolsOptionsSubCategory name="CSharp"
	    RegisteredName="CSharp" PackageName="Text Management Package">
	        <PropertyValue name="TabSize">4</PropertyValue>
	        <PropertyValue name="IndentStyle">2</PropertyValue>
	        <PropertyValue name="InsertTabs">true</PropertyValue>
	      </ToolsOptionsSubCategory>
	      <ToolsOptionsSubCategory name="CSharp-Specific"
	    RegisteredName="CSharp-Specific"
	    PackageName="Visual C# Language Service Package">
	        <PropertyValue name="NewLines_QueryExpression_EachClause">1</PropertyValue>
	        <PropertyValue name="Space_Normalize">0</PropertyValue>
	        <PropertyValue name="Space_AroundBinaryOperator">1</PropertyValue>
	        <PropertyValue name="NewLines_Braces_Method">1</PropertyValue>
	        <PropertyValue name="Indent_CaseLabels">1</PropertyValue>
	        <PropertyValue name="NewLines_Braces_ControlFlow">1</PropertyValue>
	        <PropertyValue name="NewLines_Braces_AnonymousMethod">1</PropertyValue>
	        <PropertyValue name="Space_WithinOtherParentheses">0</PropertyValue>
	        <PropertyValue name="Space_AfterBasesColon">1</PropertyValue>
	        <PropertyValue name="Indent_Braces">0</PropertyValue>
	        <PropertyValue name="Space_WithinMethodCallParentheses">0</PropertyValue>
	        <PropertyValue name="Space_AfterCast">0</PropertyValue>
	        <PropertyValue name="NewLines_Braces_CollectionInitializer">0</PropertyValue>
	        <PropertyValue name="NewLines_AnonymousTypeInitializer_EachMember">1</PropertyValue>
	        <PropertyValue name="NewLines_Keywords_Catch">1</PropertyValue>
	        <PropertyValue name="NewLines_Braces_ObjectInitializer">1</PropertyValue>
	        <PropertyValue name="NewLines_Braces_ArrayInitializer">0</PropertyValue>
	        <PropertyValue name="Space_WithinExpressionParentheses">0</PropertyValue>
	        <PropertyValue name="Space_InControlFlowConstruct">0</PropertyValue>
	        <PropertyValue name="Space_BetweenEmptyMethodDeclarationParentheses">0</PropertyValue>
	        <PropertyValue name="Indent_UnindentLabels">1</PropertyValue>
	        <PropertyValue name="SortUsings">1</PropertyValue>
	        <PropertyValue name="NewLines_ObjectInitializer_EachMember">1</PropertyValue>
	        <PropertyValue name="Space_WithinMethodDeclarationParentheses">0</PropertyValue>
	        <PropertyValue name="Space_BetweenEmptyMethodCallParentheses">0</PropertyValue>
	        <PropertyValue name="Space_BeforeSemicolonsInForStatement">0</PropertyValue>
	        <PropertyValue name="Space_BeforeComma">0</PropertyValue>
	        <PropertyValue name="Space_AfterMethodCallName">0</PropertyValue>
	        <PropertyValue name="Space_AfterComma">1</PropertyValue>
	        <PropertyValue name="Space_BeforeBasesColon">1</PropertyValue>
	        <PropertyValue name="Space_AfterMethodDeclarationName">0</PropertyValue>
	        <PropertyValue name="Space_AfterDot">0</PropertyValue>
	        <PropertyValue name="NewLines_Braces_Type">1</PropertyValue>
	        <PropertyValue name="Space_AfterLambdaArrow">1</PropertyValue>
	        <PropertyValue name="NewLines_Braces_LambdaExpressionBody">1</PropertyValue>
	        <PropertyValue name="Space_WithinSquares">0</PropertyValue>
	        <PropertyValue name="Space_BeforeLambdaArrow">1</PropertyValue>
	        <PropertyValue name="NewLines_Braces_AnonymousTypeInitializer">1</PropertyValue>
	        <PropertyValue name="Space_WithinCastParentheses">0</PropertyValue>
	        <PropertyValue name="Space_AfterSemicolonsInForStatement">1</PropertyValue>
	        <PropertyValue name="Indent_CaseContents">1</PropertyValue>
	        <PropertyValue name="Indent_FlushLabelsLeft">0</PropertyValue>
	        <PropertyValue name="Space_BetweenEmptySquares">0</PropertyValue>
	        <PropertyValue name="Space_BeforeOpenSquare">0</PropertyValue>
	        <PropertyValue name="Space_BeforeDot">0</PropertyValue>
	        <PropertyValue name="Indent_BlockContents">1</PropertyValue>
	      </ToolsOptionsSubCategory>
	    </ToolsOptionsCategory>
	  </ToolsOptions>
	</UserSettings>

It sucks that I still have to remember to load the settings for each project before making changes in that project, but at least I don't have to remember which individual settings to use for each project.

How do you handle this situation? Do you just live with the inconsistencies or is there an alternative way to deal with this issue? Are you using StyleCop (a.k.a. MS Source Analysis) or something like it to enforce some or all of the rules?

Unescape my Strings

Posted by Sergio on 2008-05-07

Since I started using Visual Studio 2005 and now 2008 it bugs me that strings in the debug windows (Immediate, Watch, Locals, Autos, etc) are automatically escaped in C# syntax (well, at least in C# projects.)

Let's assume we are writing some code that produces HTML (or XML, or SQL, or JSON, etc) where the text identation is important for understanding the contents. If we find ourselves in the middle of debugging that code and check the value of a variable containing such type of text, here's what we would see.

System.Diagnostics.Debug.WriteLine(html);
Console.WriteLine(html); // BREAKPOINT HERE
Console.ReadLine();

The Immediate window becomes less useful. The line breaks below do not exist in the Immediate window, I added them here for clarity.

?html
"<html>\r\n\t<head>\r\n\t\t<title>Monthly Report</title>\r\n\t\t<style>
\r\n\t\t\t.results td {color:#999;}\r\n\t\t\t/*etc*/\r\n\t\t</style>\r
\n\t</head>\r\n\t<body>\r\n\t\t<h1>Report for April 2008</h1>\r\n\t\t<d
iv id=\"content\">\r\n\t\t\t<p>\r\n\t\t\t\ttext goes here...\r\n\t\t\t<p
>\r\n\t\t</div>\r\n\t</body>\r\n</html>"

Same problem in the Locals or Autos windows, but there at least you can use the visualizers.

Too many clicks for me, though. Besides, the visualizers are modal windows and that makes it harder when you want to compare things.

If you happen to know how to revert to the old behavior of keeping the text format, please let me know. Until I find the right way (or at least a better way) I'm using a little macro to output any selected escaped text, unescaped to a new output pane.

To create the macro:

  • View » Other Windows » Macro Explorer
  • Right-click "My Macros" » New Module
  • Call the new module Unescape
  • Add the following sub
Public Module Unescape
  Public Sub UnescapeText()
    Dim winName As String = "Unescaped"
    Dim win As Window
    Dim output As OutputWindow
    Dim pane As OutputWindowPane = Nothing

    If DTE.ActiveWindow IsNot Nothing AndAlso _
      DTE.ActiveWindow.Selection IsNot Nothing Then

      Dim text As String = DTE.ActiveWindow.Selection.Text

      If Not String.IsNullOrEmpty(text) Then

        text = text.Replace("\t", vbTab) _
          .Replace("\r\n", vbCrLf) _
          .Replace("\n", vbCrLf) _
          .Replace("\r", vbCrLf) _
          .Replace("\""", """")

        win = DTE.Windows.Item(EnvDTE.Constants.vsWindowKindOutput)
        output = win.Object

        For Each p As OutputWindowPane In output.OutputWindowPanes
          If p.Name = winName Then
            pane = p
            Exit For
          End If
        Next
        If pane Is Nothing Then _
          pane = output.OutputWindowPanes.Add(winName)

        win.Activate()
        pane.Activate()
        pane.OutputString(text)
      End If
    End If
  End Sub
End Module

I made a shortcul for this macro:

  • Tools » Options » Environment » Keyboard
  • Show commands containing: unescape
  • Select the macro when it gets listed
  • Add a Global shortcut, I used Ctrl+Alt+U

Now if I select the escaped text in the Immediate window and press the macro shortcut, I get the following:

If I keep selecting other escaped texts and unescaping them, they'll just be appended to that output, making it possible to do some visual inspection or comparisons.