Category Archives: Code Garage

A One Line CSV Parser

4
Filed under Code Garage, Regular Expressions, Utilities, VB Feng Shui

Parsing up Quote Comma delimited text is a pretty common thing to do, and it seems trivial enough till you realize all the little gotcha’s that come with the problem (like doubled quotes, commas in quotes, etc, etc). Then it becomes just another laborious exercise in boring coding.

I came across a regex some time ago that makes the process literally one line of code. I can no longer find the original author but the code I found (what little there was of it) was C# and actually split up into a few lines of code, so this is converted to the equivalent VB.net:

    Public Function QCSplit(ByVal Args As String) As String()

        Return (New System.Text.RegularExpressions.Regex(",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))")).Split(args)
    End Function

The regex used here doesn’t actually match on the contents, it matches on the commas that split things up, and it then uses the SPLIT function to actually split those things up.

I’ve used this a while now and it works great, but I’ve seen a few interesting alternatives since.

The most interesting thus far is this one by Daniel Einspanjer. Not interesting enough yet for me to switch to it, but the regexlib.com website is quite nice as a great repository of good regex recipes.

Regular Expression Tester

And speaking of Regular Expressions, the guys over at RAD Software, have a free Regular expression tester for .net style regular expressions that works fantastically. If you’re just starting out in regex’s (and seriously, who isn’t <G>), you owe it to yourself to pick up a decent regex test tool, and this one is as good as I’ve seen so far.

image

As far as I can tell, it can handle all the various options for regex’s, and dynamically shows match results, etc. Very handy for trying out expressions without actually running them in .net.

Add it to your External Tools menu in VS and it’ll be right there, good to go.

Cleaning up Messy DataContractSerializer XML

7
Filed under Code Garage, Software Architecture, VB Feng Shui, XML

I was working with XML serialization of objects recently and was using the good ol’ DataContractSerializer again.

One thing that I bumped into almost immediately is that the XML that it spits out isn’t exactly the neatest, tidiest of XML possible, to say the least.

So I set out on a little odyssey to see exactly how nice and clean I could make it.

(EDIT: I’ve added more information about how the Name property of the Field object is being serialized twice, which is another big reason for customizing the serialization here, and for specialized dictionary serialization in general).

First, the objects to serialize. I’ve constructed a very rudimentary object hierarchy that still illustrates the problem well.

In this case, I have a List of Record objects, called a Records list. Each Record object is a dictionary of Field objects. And each Field object contains two properties, Name and Value. The code for these (and a little extra code to make populating them easy) is as follows.

Public Class Records
    Inherits List(Of Record)


    Public Sub New()
        '---- default constructor
    End Sub

End Class


Public Class Record
    Inherits Dictionary(Of String, Field)


    Public Sub New()
        '---- default constructor
    End Sub


    Public Sub New(ByVal ParamArray Fields() As Field)
        For Each f In Fields
            Me.Add(f.Name, f)
        Next
    End Sub
End Class


Public Class Field

    Public Sub New()
        '---- default constructor
    End Sub


    Public Sub New(ByVal Name As String, ByVal Value As String)
        Me.Name = Name
        Me.Value = Value
    End Sub


    Public Property Name() As String
        Get
            Return _Name
        End Get
        Set(ByVal value As String)
            _Name = value
        End Set
    End Property
    Private _Name As String



    Public Property Value() As String
        Get
            Return _Value
        End Get
        Set(ByVal value As String)
            _Value = value
        End Set
    End Property
    Private _Value As String

End Class

Yes, I realize there are DataTables, KeyValuePair objects, etc that could do this, but that’s not the point, so just bear with me<g>.

To populate a Records object, you might have code that looks like this:

Dim Recs = New Records
Recs.Add(New Record(New Field("Name", "Darin"), New Field("City", "Arlington")))
Recs.Add(New Record(New Field("Name", "Gillian"), New Field("City", "Ft Worth")))
Recs.Add(New Record(New Field("Name", "Laura"), New Field("City", "Dallas")))

Ok, so far so good.

Now, lets serialize that with a simple serialization function using the DataContractSerializer:

    ''' <summary>
    ''' Serializes the data contract to a string (XML)
    ''' </summary>
    Public Function Serialize(Of T As Class)(ByVal SerializeWhat As T) As String
        Dim stream = New System.IO.StringWriter
        Dim writer = System.Xml.XmlWriter.Create(stream)

        Dim serializer = New System.Runtime.Serialization.DataContractSerializer(GetType(T))
        serializer.WriteObject(writer, SerializeWhat)
        writer.Flush()

        Return stream.ToString
    End Function

In the test application, I put together, I dump the resulting XML to a text box. Yikes!

image

So, what’re the problems here? <g>

  1. You’ve got that “http://www.w3.org/2001/XMLSchema-instance” namespace attribute amongst other
  2. lots of random letters
  3. no indenting
  4. You can’t really tell it from this shot, but the Record dictionary is serializing the name property twice, because I’m using it as the Key for the dictionary, but it’s also a property of the objects in the dictionary.

All this noise might be fine for computer to computer communication, but it’s pretty tough on human eyes<g>.

Ok, first thing to do is indent:

    ''' <summary>
    ''' Serializes the data contract to a string (XML)
    ''' </summary>
    Public Function Serialize(Of T As Class)(ByVal SerializeWhat As T) As String
        Dim stream = New System.IO.StringWriter
        Dim xmlsettings = New Xml.XmlWriterSettings
        xmlsettings.Indent = True
        Dim writer = System.Xml.XmlWriter.Create(stream, xmlsettings)

        Dim serializer = New System.Runtime.Serialization.DataContractSerializer(GetType(T))
        serializer.WriteObject(writer, SerializeWhat)
        writer.Flush()

        Return stream.ToString
    End Function

Notice that I added the use of the XMLWriterSettings object. This allows me to set the Indent property, and things are much more readable.

image

But that’s still a far cry from nice, simple, tidy XML. Notice all the “ArrayofArrayOf blah blah” names, and the randomized letter sequences? Plus, it’s much more obvious how the NAME jproperty is being serialized twice now. Yuck! Surely, we can do better than this!

Cleaning Up the Single Entity Field Object

The DataContractSerializer certainly works easily enough to serialize the Field object, but unfortunately, it decorates the serialized elements with a load of really nasty looking and completely unnecessary cruft.

My first thought was to simply decorate the class with <DataContract> attributes:

<DataContract(Name:="Field", Namespace:="")> _
Public Class Field

    Public Sub New()
        '---- default constructor
    End Sub


    Public Sub New(ByVal Name As String, ByVal Value As String)
        Me.Name = Name
        Me.Value = Value
    End Sub

    <DataMember()> _
    Public Property Name() As String
        Get
            Return _Name
        End Get
        Set(ByVal value As String)
            _Name = value
        End Set
    End Property
    Private _Name As String



    <DataMember()> _
    Public Property Value() As String
        Get
            Return _Value
        End Get
        Set(ByVal value As String)
            _Value = value
        End Set
    End Property
    Private _Value As String

End Class

But this yields:

image

So we have several problems:

  • Each field is rendered into a Value element of the Record’s field collection
  • The Key of the Record collection duplicates the Name of the individual Field objects
  • and we still have a noxious xmlns=”” attribute being rendered.

Unfortunately, this is where the DataContractSerializer’s simplicity is it’s downfall. There’s just no way to customize this any further, using ONLY the DataContractSerializer.

However, we can implement IXMLSerializable on our Field object to customize its serialization. All I need to do is remove the DataContract attribute, and add a simple implementation of IXMLSerializable to the class:

Public Class Field
    Implements System.Xml.Serialization.IXmlSerializable


    Public Sub New()
        '---- default constructor
    End Sub


    Public Sub New(ByVal Name As String, ByVal Value As String)
        Me.Name = Name
        Me.Value = Value
    End Sub

    Public Property Name() As String
        Get
            Return _Name
        End Get
        Set(ByVal value As String)
            _Name = value
        End Set
    End Property
    Private _Name As String


    Public Property Value() As String
        Get
            Return _Value
        End Get
        Set(ByVal value As String)
            _Value = value
        End Set
    End Property
    Private _Value As String


    Public Function GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema
        Return Nothing
    End Function


    Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml

    End Sub


    Public Sub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml
        writer.WriteElementString("Name", Me.Name)
        writer.WriteElementString("Value", Me.Value)
    End Sub
End Class

And that yields a serialization of:

image

Definitely better, but still not great.

Cleaning up a Generic Dictionary’s Serialization

The problem now is with the Record dictionary.

Public Class Record
    Inherits Dictionary(Of String, Field)
    Implements System.Xml.Serialization.IXmlSerializable


    Public Sub New()
        '---- default constructor
    End Sub


    Public Sub New(ByVal ParamArray Fields() As Field)
        For Each f In Fields
            Me.Add(f.Name, f)
        Next
    End Sub

    Public Function GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema
        Return Nothing
    End Function

    Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml

    End Sub

    Public Sub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml
        For Each f In Me.Values
            DirectCast(f, System.Xml.Serialization.IXmlSerializable).WriteXml(writer)
        Next
    End Sub
End Class

Adding an IXMLSerializable implementation to it as well yields the following XML:

image

Definitely much better! Especially notice that we’ve gotten rid of the duplicated “Name” key. It was duplicated before because we used the Name element of the Field object as the Key for the Record dictionary. This be play an important part in deserializing the Record’s dictionary of Field objects later.

Cleaning up the List of Records

Finally, the only thing really left to do is clean up how the generic list of Record objects is serialized.

But once again, the only way to alter the serialization is to implement IXMLSerializable on the class.

<Xml.Serialization.XmlRoot(Namespace:="")> _
Public Class Records
    Inherits List(Of Record)
    Implements System.Xml.Serialization.IXmlSerializable


    Public Sub New()
        '---- default constructor
    End Sub

    Public Function GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema
        Return Nothing
    End Function

    Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml

    End Sub

    Public Sub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml
        For Each r In Me
            DirectCast(r, System.Xml.Serialization.IXmlSerializable).WriteXml(writer)
        Next
    End Sub
End Class

Notice that I’ve implemented IXMLSerializable, but I also added the XmlRoot attribute with a blank Namespace parameter. This completely clears the Namespace declaration from the resulting output, which now looks like this:

image

And that is just about as clean as your going to get!

But That’s Not all there is To It

Unfortunately, it’s not quite this simple. The thing is, you very well may want to serialize each object independently, not just serialize the Records collection. Doing that as we have things defined right now won’t work. The Start and End elements won’t be generated in the XML properly.

Instead, we need to add XmlRoot attributes to all three classes, and adjust where the WriteStartElement and WriteEndElement calls are made. So we end up with this:

<Xml.Serialization.XmlRoot(Namespace:="")> _ Public Class Records Inherits List(Of Record) Implements System.Xml.Serialization.IXmlSerializable Public Sub New() '---- default constructor End Sub Public Function GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema Return Nothing End Function Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml End Sub Public Sub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml For Each r In Me writer.WriteStartElement("Record") DirectCast(r, System.Xml.Serialization.IXmlSerializable).WriteXml(writer) writer.WriteEndElement() Next End Sub End Class <Xml.Serialization.XmlRoot(ElementName:="Record", Namespace:="")> _ Public Class Record Inherits Dictionary(Of String, Field) Implements System.Xml.Serialization.IXmlSerializable Public Sub New() '---- default constructor End Sub Public Sub New(ByVal ParamArray Fields() As Field) For Each f In Fields Me.Add(f.Name, f) Next End Sub Public Function GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema Return Nothing End Function Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml End Sub Public Sub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml For Each f In Me.Values writer.WriteStartElement("Field") DirectCast(f, System.Xml.Serialization.IXmlSerializable).WriteXml(writer) writer.WriteEndElement() Next End Sub End Class <Xml.Serialization.XmlRoot(ElementName:="Field", Namespace:="")> _ Public Class Field Implements System.Xml.Serialization.IXmlSerializable Public Sub New() '---- default constructor End Sub Public Sub New(ByVal Name As String, ByVal Value As String) Me.Name = Name Me.Value = Value End Sub Public Property Name() As String Get Return _Name End Get Set(ByVal value As String) _Name = value End Set End Property Private _Name As String Public Property Value() As String Get Return _Value End Get Set(ByVal value As String) _Value = value End Set End Property Private _Value As String Public Function GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema Return Nothing End Function Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml End Sub Public Sub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml writer.WriteElementString("Name", Me.Name) writer.WriteElementString("Value", Me.Value) End Sub End Class

 

 

And Finally, Deserialization

Of course, all this would be for nought if we couldn’t actually deserialize the xml we’ve just spent all this effort to clean up.

Turns out that deserialization is pretty straightforward. I just needed to add code to the ReadXml member of the implemented IXMLSerializable interface. The full code for my testing form is below. Be sure to add a reference to System.Runtime.Serialization, though, or you’ll have type not defined errors.

Public Class frmSample

    Private Sub btnTest_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnTest.Click

        '---- populate the objects
        Dim Recs = New Records
        Recs.Add(New Record(New Field("Name", "Darin"), New Field("City", "Arlington")))
        Recs.Add(New Record(New Field("Name", "Gillian"), New Field("City", "Ft Worth")))
        Recs.Add(New Record(New Field("Name", "Laura"), New Field("City", "Dallas")))

        Dim t As String
        t = Serialize(Of Field)(Recs(0).Values(0))
        Dim fld = Deserialize(Of Field)(t)
        Debug.Print(fld.Name)
        Debug.Print(fld.Value)
        Debug.Print("--------------")

        t = Serialize(Of Record)(Recs(0))
        Dim rec = Deserialize(Of Record)(t)
        Debug.Print(rec.Values.Count)
        Debug.Print("--------------")

        t = Serialize(Of Records)(Recs)
        tbxOutput.Text = t

        Dim recs2 = Deserialize(Of Records)(t)
        Debug.Print(recs2.Count)
    End Sub
End Class


<Xml.Serialization.XmlRoot(Namespace:="")> _
Public Class Records
    Inherits List(Of Record)
    Implements System.Xml.Serialization.IXmlSerializable


    Public Sub New()
        '---- default constructor
    End Sub

    Public Function GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema
        Return Nothing
    End Function

    Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml
        reader.MoveToContent()
        reader.ReadStartElement("Records")
        reader.MoveToContent()
        Do While reader.NodeType <> Xml.XmlNodeType.EndElement
            Dim Rec = New Record
            DirectCast(Rec, System.Xml.Serialization.IXmlSerializable).ReadXml(reader)
            Me.Add(Rec)
            reader.MoveToContent()
        Loop
        reader.ReadEndElement()
    End Sub

    Public Sub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml
        For Each r In Me
            writer.WriteStartElement("Record")
            DirectCast(r, System.Xml.Serialization.IXmlSerializable).WriteXml(writer)
            writer.WriteEndElement()
        Next
    End Sub
End Class


<Xml.Serialization.XmlRoot(ElementName:="Record", Namespace:="")> _
Public Class Record
    Inherits Dictionary(Of String, Field)
    Implements System.Xml.Serialization.IXmlSerializable


    Public Sub New()
        '---- default constructor
    End Sub


    Public Sub New(ByVal ParamArray Fields() As Field)
        For Each f In Fields
            Me.Add(f.Name, f)
        Next
    End Sub

    Public Function GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema
        Return Nothing
    End Function

    Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml
        reader.MoveToContent()
        reader.ReadStartElement("Record")
        reader.MoveToContent()
        Do While reader.NodeType <> Xml.XmlNodeType.EndElement
            Dim fld = New Field
            DirectCast(fld, System.Xml.Serialization.IXmlSerializable).ReadXml(reader)
            Me.Add(fld.Name, fld)
            reader.MoveToContent()
        Loop
        reader.ReadEndElement()
    End Sub

    Public Sub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml
        For Each f In Me.Values
            writer.WriteStartElement("Field")
            DirectCast(f, System.Xml.Serialization.IXmlSerializable).WriteXml(writer)
            writer.WriteEndElement()
        Next
    End Sub
End Class


<Xml.Serialization.XmlRoot(ElementName:="Field", Namespace:="")> _
Public Class Field
    Implements System.Xml.Serialization.IXmlSerializable


    Public Sub New()
        '---- default constructor
    End Sub


    Public Sub New(ByVal Name As String, ByVal Value As String)
        Me.Name = Name
        Me.Value = Value
    End Sub

    Public Property Name() As String
        Get
            Return _Name
        End Get
        Set(ByVal value As String)
            _Name = value
        End Set
    End Property
    Private _Name As String


    Public Property Value() As String
        Get
            Return _Value
        End Get
        Set(ByVal value As String)
            _Value = value
        End Set
    End Property
    Private _Value As String


    Public Function GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema
        Return Nothing
    End Function


    Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml
        reader.MoveToContent()
        reader.ReadStartElement("Field")
        reader.MoveToContent()
        If reader.Name = "Name" Then Me.Name = reader.ReadElementContentAsString
        reader.MoveToContent()
        If reader.Name = "Value" Then Me.Value = reader.ReadElementContentAsString
        reader.MoveToContent()
        reader.ReadEndElement()
    End Sub


    Public Sub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml
        writer.WriteElementString("Name", Me.Name)
        writer.WriteElementString("Value", Me.Value)
    End Sub
End Class



Public Module Serialize
    ''' <summary>
    ''' Serializes the data contract to a string (XML)
    ''' </summary>
    Public Function Serialize(Of T As Class)(ByVal SerializeWhat As T) As String
        Dim stream = New System.IO.StringWriter
        Dim xmlsettings = New Xml.XmlWriterSettings
        xmlsettings.Indent = True
        Dim writer = System.Xml.XmlWriter.Create(stream, xmlsettings)

        Dim serializer = New System.Runtime.Serialization.DataContractSerializer(GetType(T))
        serializer.WriteObject(writer, SerializeWhat)
        writer.Flush()

        Return stream.ToString
    End Function


    ''' <summary>
    ''' Deserializes the data contract from xml.
    ''' </summary>
    Public Function Deserialize(Of T As Class)(ByVal xml As String) As T
        Using stream As New MemoryStream(UnicodeEncoding.Unicode.GetBytes(xml))
            Return DeserializeFromStream(Of T)(stream)
        End Using
    End Function


    ''' <summary>
    ''' Deserializes the data contract from a stream.
    ''' </summary>
    Public Function DeserializeFromStream(Of T As Class)(ByVal stream As Stream) As T
        Dim serializer As New DataContractSerializer(GetType(T))
        Return DirectCast(serializer.ReadObject(stream), T)
    End Function
End Module

Of particular note above is the ReadXML function of the Field object.

It checks the name of the node first and then places the value of the node into the appropriate property of that object. If I didn’t do that, the deserialization process would require the fields in the XML to be in a specific order. This is a minor drawback to the DataContractSerializer that this approach alleviates.

What’s Next?

The one unfortunate aspect of this is that it requires you to implement IXMLSerializable on each object that you want the XML cleaned up for.

Generally speaking, The DataContractSerializer will be perfectly fine for those cases where humans aren’t likely to ever have to see the XML you’re generating. And you get a performance boost for sacrificing that flexibility and “cleanliness”.

But for things like data file imports, custom configuration files, and the like, it may be desirable to  implement custom serialization like this so that your xml files can be almost as easy to read as those old school INI files!

Code Garage – Case Insensitive Dictionaries

0
Filed under Code Garage

Something that’s always bothered me a little about the generic dictionary support in .NET is that it’s, by default, case sensitive. I’d never really contemplated it much more than that until today, when I really needed  a dictionary that supported a fast, case insensitive lookup.

At first, I used a list, and the FirstOrDefault function along with a lambda expression. It worked, but I soon realized it was wretchedly slow.

I knew that surely, there was a way to get case insensitive lookups with a generic dictionary, but I’d never really gone looking for it. But a little searching later, and I’d found the answer.

    Public Sub Test()

        Dim d = New Dictionary(Of String, String)(StringComparer.CurrentCultureIgnoreCase)
        d.Add("John", "JohnTest")
        d.Add("Bob", "BobTest")
        d.Add("Bill", "BillTest")
        d.Add("Zack", "ZackTest")

        Debug.Print(d.Keys.Contains("ZACK"))
        Debug.Print(d("bill"))
    End Sub

You must specify an IEqualityComparer object as part of the constructor, and the object to use can be easily obtained from the StringComparer factory object., as show above.

Give it a shot with and without the (StringComparer.CurrentCultureIgnoreCase) clause.

Even better. If you’re defining your own strongly typed dictionary based on the generic dictionary, you can force the comparer in your constructor, so that instances of your dictionary will always use the right comparer; code that instantiates your dictionary won’t have to bother with (or remember to supply) the StringComparer object.

    Public Class StringDict
        Inherits Dictionary(Of String, String)

        Public Sub New()
            MyBase.New(StringComparer.CurrentCultureIgnoreCase)
        End Sub
    End Class


    Public Sub Test2()
        Dim d = New StringDict
        d.Add("John", "JohnTest")
        d.Add("Bob", "BobTest")
        d.Add("Bill", "BillTest")
        d.Add("Zack", "ZackTest")

        Debug.Print(d.Keys.Contains("ZACK"))
        Debug.Print(d("bill"))
    End Sub

It may not be new, but it’s new to me, and awfully nice to know!

The Code Garage – SplitQuoteComma

1
Filed under Code Garage

Today, I’m restarting my (well, actually I’ve seen something similar around the web) “Code Garage” concept. Basically, the idea is to post (hopefully regularly) some snippet of code that I’ve either come across, translated from some other language, or whatever and that I find myself using pretty consistently. I’ll tag them all with the “Code Garage” tag in my tag cloud.

Today, it’s a SplitQuoteComma function. VB has long had a split function, in one incarnation or another, but occassionally, you have to deal with quote/comma delimited text (i.e. text input that is comma delimited unless there’s a comma in the text, in which case the text is wrapped in quotes).

This kind of function is pretty trivial stuff, really, but I happened across a regex that makes it even trivialer (is that a word?):

    Public Function SplitQuoteComma(ByVal Args As String) As String()
        Dim r = New System.Text.RegularExpressions.Regex(",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))")
        Return r.Split(Args)
    End Function

Merging Assemblies, the Easy Way (part 1)

0
Filed under Code Garage, Utilities, VB Feng Shui

image Recently, I ran into a need to make use of the Mono.Cecil.dll library from a .net application I was working on. Cecil is a fantastic little project under the Mono project that allows you to interrogate an existing .net assembly (EXE or DLL file) and retrieve all kinds of information about the assembly, from the resources embedded within it to the classes and methods defined, and much more. You can even alter the contents of the assembly and write the new version back out to disk!

The thing is, Cecil comes prebuilt as a C# assembly (a DLL file), and I really did not want to have a second file required for this particular application (it’s just a little command line utility).

My first take was to use Oren Eini’s excellent concept of an assembly locator. My idea here was to embed an assembly as a binary resource, then, when requested, read that resource into a stream, load the assembly from the stream, and resolve references to it via the assembly locator.

I’m still working on that concept, because I think it’s a clever and convenient solution to the problem, but, in researching that, I happened to remember the even easier (as in, no code required at all!) solution of using ILMerge.

ILMerge

If you haven’t already discovered it, ILMerge is a Microsoft research project in the form of a single command line EXE utility, that can “merge” any number of .net assemblies with a “target” assembly, and produce either a .net DLL or EXE output file.

There’s a really good article on CodeProject describing the general use of the program. There’s even a GUI (Gilma) for it.

Automating ILMerge

What the article doesn’t go into is automating the ILMerge process. After all, you won’t want to manually run that command line utility every time you build your application!

The good thing is, it’s trivially easy to automate as long as you take care of one critical step.

  • First, download and install ILMerge from the link above.
  • Once it’s installed be sure to copy the ILMerge.exe utility to somewhere on your path (or add the folder it’s in to your path).
  • Then, grab the Mono.Cecil.dll file (or whatever DLL it is you want to merge into your main project EXE). Put it in the root folder of your project.
  • Go ahead and set a reference to that file, so you still get all the .net intellisense goodness, but be sure to set the Copy Local property for the reference to FALSE (after all, you don’t want to copy this DLL to the output folder along with the application if you don’t actually need it, right?)

    image

  • With that done, you MAY want to mark the DLL you’re embedding as “included in project” from the Solution Explorer.

image If you do this, however, make sure you also set the Copy to Output Folder to “Do not copy” (that’s the default though so it should already be set).

The Tricky Part

One limitation of ILMerge is it that it can’t alter the target assembly “in place”. This means that, for instance, if you want the final resulting executable file name to be called GenerateLineMap (my utility), you can’t have VS compile the assembly to that name. You’ll need to use some other name for the initially compiled assembly.

On the Application tab of the project properties you can change that name, like so:

image

I just added “-Interim” to the Assembly Name.

Now, go to the compile tab of the project properties and click Build Events

image

Insert this as the POST BUILD EVENT

ilmerge /target:winexe /out:”$(TargetDir)$(ProjectName)$(TargetExt)” “$(TargetPath)” “$(ProjectDir)Mono.Cecil.dll”

Notice that the OUT parameter specifies the output file name via the $(ProjectName) variable (which is still “GenerateLineMap”) while the first assembly to merge is specified using the $(TargetPath) variable (which is the full filename of the output assembly, including the “-Interim”).

image

EDIT: Important note: Notice the /target:winexe option. You’ll need to change that as appropriate depending on the type of exe you’re building. In my case, I had a console app where this needed to be set to just /target:exe. Setting it to /target:winexe caused all of the console output functions to just do nothing, which threw me for a bit!

Now, just rebuild the solution and you should see two resulting EXE files in your output folder, one with the “–Interim” (which is the version WITHOUT the embedded Mono.Cecil.DLL file or whatever DLL you’ve chosen) and one WITH the embedded file. As a final cleanup, you may want to add an additional Post build step to remove the “*-Interim” files.

To test, just make sure you DO NOT have the embedded DLL anywhere on the path or in the same folder as the exe and run the exe. If everything went right, your app should work exactly the same as if the embedded DLL was included externally with the app!

Now, your application is back to being a single EXE to distribute, and it was built completely automatically by Visual Studio.

Caveats

You knew there had to be some, right?

The first is that if the dll to embed contains licensing information, it might not work properly after being embedded. I don’t have any DLLs like that to test with, but I’ve read reports about the problem at various sites on the internet. Just something to be aware of.

Second, trying to replace the originally named exe with the newly built one results in VS not being able to debug the exe in the IDE (it throws a message about the assembly manifest being different from what was expected). I haven’t worked that issue out yet. What this means is that you might need to leave any references set to “Copy to Output Folder” while debugging, and not replace the original compiled assembly, just ILMerge it into a new assembly name.

Simplified VB.Net Configuration

1
Filed under .NET, Code Garage

image I really liked the idea of a built-in configuration management framework with .NET. That is, until I actually tried to use it.

I wrote about the configuration functions in .NET here. My specific words were:

If your sideline utility needs to save a few settings, use the .NET configuration…

Ugh, I’m sorry I said that…

What’s Wrong

The .NET configuration namespace is powerful, no doubt about that. But, that comes at a huge cost. The thing is enormous. And the available documentation and examples just aren’t that good.

Honestly, though. The docs don’t bother me that much. What bothers me is that even though the existing framework will probably do everything I want it to, discovering how is just too dang hard and non-intuitive.

For instance:

  • Why the hell should I have to create a new ExeConfigurationFileMap object just to change the path of where the system reads its config file? Sorry, but that’s about as intuitive as a car you accelerate by yodeling.
  • What on earth were they smoking when they came up with those ludicrous guid/hash/hex based folder names where your user level config files are stored by default? Facilitate upgrades? Yeah, maybe, but pretty much nothing else. That’s one that I guarantee will go down with the registry and DCOM as a bad idea.
  • Registering ConfigurationSection handlers? Huh? Why should I have to write the full class names of classes internal to my application into my configuration file so that .NET can read them?

What Would Be Nice

What I was looking for was a simple way to create a class like so:

Public Sub MySettingClass
   Public Setting1 as string = ""
   Public Setting2 as Integer = 0
End Class

No need to explicitly use properties unless you need them. No need for attributes. Heck, you shouldn’t even have to declare the class <Serializable>, though that’s a minor point.

In lieu of coexisting within the My.Settings space, it should be able to persist itself to a config file, and then de-persist itself back when asked, like this:

Settings = New MySettingsClass
Settings.Load
...
Settings.Save

Accessing your settings should be completely early bound, with all that sweet Intellisense goodness baked right in:

x = Settings.Setting1
y = Setting.Setting2

Further, I should be able to easily persist sub-objects or collections made accessible off this root settings object. For example, to save a form’s current position and size, and then restore it, should take code similar to:

Settings.FormPositions.Save(MyForm)
Settings.FormPositions.Restore(MyForm)
Debug.print "Form position is " & Settings.FormPositions(0).Position

And a few additional requirements.

  • First, having no initial configuration file shouldn’t be a problem. The entire collection of settings should easily default to some “built in” default values when no config file exists.
  • Second, I should be able to go from 0-60 in no time. In other words, I should be able to take the .VB file for a settings base class, drop it in my application, add a settings class with the properties I need to persist, as well as a .LOAD and a .SAVE at the appropriate points in my project, and be off. No “presetting” my config file, no tweaks to anything, no registering this or that, mucking with the GAC, etc, etc.

Research

The available Microsoft documentation on the configuration system was so confusing, I believe I knew less about it after I finished reading the docs than I did when I started.

I did turn up a very good article on CodeProject by Jon Rista called Cracking the Mysteries of .NET 2.0 Configuration. Definitely worth a read if you’re diving into this stuff.

While Jon gives several samples of code, nothing really illustrated exactly what I was looking for. However, there was more than enough info in the article to kick-start things for me.

Long story short, it turns out that the Configuration system in .NET is, in typical MS fashion, more than capable but ultra-overkill for many small-app type scenarios.

My Solution

While this is definitely still a work in progress, it’s proved quite useful so far, so I thought others might find it handy too.

The system consists of one file, SettingsBase.vb. It defines two classes, SettingsBase and SettingsBaseDictionary.

SettingsBaseDictionary is just a simple extension to the normal generic Dictionary class that allows it to be serialized. This is something I found on the web and is so handy with respect to settings that I just include it directly in the file.

SettingsBase is a MustInherit class, meaning it’s abstract. To use it, you must create your own settings class (call it whatever you like) that inherits from SettingsBase:

Public Class Settings
   Inherits SettingsBase

   Public Name As String = ""
   Public Phone As String = ""
End Class

When you want to load your settings, just instantiate your Settings object and invoke Settings.Load. To change settings, set the object’s properties as you normally would.

To save your settings, invoke Settings.Save.

Finally, you’ll need to add a reference to System.Configuration. Directly accessing the ConfigurationManager and EXEConfigurationFileMap objects requires it.

The sample project I’ve zipped up shows several examples of this, from ridiculously simple to moderately sophisticated. I even threw in a really simple example of DataBinding to a setting property (in this case, a Dictionary of contacts).

The Code

If you don’t want to download the sample, I’ve included the source to the SettingBase.VB file here. Note that this also includes the source to the SettingBaseDictionary, but if you don’t want it, you can simply delete it.

Imports System.Configuration
Imports System.IO
Imports System.Text
Imports System.Xml
Imports System.Xml.Serialization


''' <summary>
''' Base Class that will allow you to easily persist
''' a "settings" object via the .net configuration management framework
''' 
''' By Darin Higgins
''' Sept 2008
''' You are free to use this class in your own projects.
''' But please, keep the attributions as to the source.
''' </summary>
''' <remarks>
''' Be sure to add a reference to System.Configuration
''' </remarks>
''' <editHistory></editHistory>
Public MustInherit Class SettingsBase
#Region " Constants"
   '---- just some names of constants used in the class
   Private Const DEFAULTSETTINGFILENAME = "Settings.config"
   Private Const ROOTSECTION = "general"
   Private Const ROOTITEM = "settings"
#End Region


#Region " Properties"
   Private rFilename As String = DEFAULTSETTINGFILENAME
   ''' <summary>
   ''' The Settings filename (name only, no path)
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Public Property FileName() As String
      Get
         Return rFilename
      End Get
      Set(ByVal value As String)
         rFilename = value
      End Set
   End Property


   Private rCompanyName As String = My.Application.Info.CompanyName
   ''' <summary>
   ''' The CompanyName used when creating a path to the settings store
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Public Property CompanyName() As String
      Get
         Return rCompanyName
      End Get
      Set(ByVal value As String)
         rCompanyName = value
      End Set
   End Property


   Private rAppName As String = My.Application.Info.ProductName
   ''' <summary>
   ''' The AppName used when creating a path to the settings store
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Public Property AppName() As String
      Get
         Return rAppName
      End Get
      Set(ByVal value As String)
         rAppName = value
      End Set
   End Property


   ''' <summary>
   ''' Retrieves the full path and filename to the settings store
   ''' Normally \Docs and Settings\All Users\Application Data\CompanyName\AppName\Settings.config
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Private ReadOnly Property pAppConfigFilename() As String
      Get
         '---- Don't use My.Computer.FileSystem.SpecialDirectories 
         '     because the commonappdata folder returned will always have
         '     the version in it and it will automatically be created
         '     but I don't want that here
         Dim path = System.Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)

         '---- as is pretty standard practice, our app settings
         '     go in a CompanyName\Appname folder in the CommonAppData folder
         If Me.CompanyName.Length > 0 Then
            path = System.IO.Path.Combine(path, Me.CompanyName)
            If Not My.Computer.FileSystem.DirectoryExists(path) Then
               My.Computer.FileSystem.CreateDirectory(path)
            End If
         End If
         If Me.AppName.Length > 0 Then
            path = System.IO.Path.Combine(path, Me.AppName)
            If Not My.Computer.FileSystem.DirectoryExists(path) Then
               My.Computer.FileSystem.CreateDirectory(path)
            End If
         End If

         Dim Filename = System.IO.Path.Combine(path, Me.FileName)
         Return Filename
      End Get
   End Property


   ''' <summary>
   ''' Creates an ExeConfigurationFileMap object to properly
   ''' locate the config files we'll use
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Private ReadOnly Property pAppConfigMap() As ExeConfigurationFileMap
      Get
         Dim filemap = New ExeConfigurationFileMap
         filemap.ExeConfigFilename = Me.pAppConfigFilename
         Return filemap
      End Get
   End Property


   ''' <summary>
   ''' Creates a Configuration object mapped to 
   ''' the proper settings files
   ''' Sets up several internal setting elements and sections
   ''' so we can persist the host object
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Private ReadOnly Property pConfig() As Configuration
      Get
         Static cfg As Configuration

         '---- cache the config object so we can reuse it
         If cfg Is Nothing Then
            cfg = ConfigurationManager.OpenMappedExeConfiguration(Me.pAppConfigMap, ConfigurationUserLevel.None)
         End If
         If Not cfg.HasFile Then
            '---- force a file to be created
            '     This settings is just general purpose placeholder
            '     not really intended to be used
            cfg.AppSettings.Settings.Add("version", My.Application.Info.Version.ToString)
            cfg.Save()
         End If

         Dim bDirty As Boolean = False
         If cfg.HasFile Then
            '---- no need for groups in this case
            '     but this sample code illustrates how you'd create a ConfigGroup if necessary
            'If cfg.SectionGroups(ROOTSECTION) Is Nothing Then
            '   cfg.SectionGroups.Add(ROOTSECTION, New ConfigurationSectionGroup)
            '   bDirty = True
            'End If
            'If cfg.SectionGroups(ROOTSECTION).Sections("options") Is Nothing Then
            '   cfg.SectionGroups(ROOTSECTION).Sections.Add("options", New ClientSettingsSection)
            '   bDirty = True
            'End If
            Dim sect As ClientSettingsSection = cfg.Sections(ROOTSECTION)
            If cfg.Sections(ROOTSECTION) Is Nothing Then
               sect = cfg.Sections(ROOTSECTION)
               cfg.Sections.Add(ROOTSECTION, sect)
               bDirty = True
            End If
            Dim element = sect.Settings.Get(ROOTITEM)
            If element Is Nothing Then
               element = New SettingElement(ROOTITEM, SettingsSerializeAs.Xml)
               sect.Settings.Add(element)
               bDirty = True
            End If
            If element.Value.ValueXml Is Nothing Then
               '---- make sure element contains something
               element.Value.ValueXml = New Xml.XmlDocument().CreateElement("value")
               bDirty = True
            End If

            If bDirty Then cfg.Save()
         End If
         Return cfg
      End Get
   End Property


   ''' <summary>
   ''' If you would like to use the built in ConfigurationStringsSection
   ''' just add a readonly property that exposes this property
   ''' Doing it this way, you can expose this property anywhere you want
   ''' in your Setting object heirarchy.
   ''' 
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Protected ReadOnly Property ConnectionStrings() As ConnectionStringsSection
      Get
         Return pConfig.ConnectionStrings
      End Get
   End Property


   ''' <summary>
   ''' Since this is essentially a key/value pair collection
   ''' there's not much benefit to exposing it, but I've included it
   ''' for completeness.
   ''' 
   ''' These kinds of sections are particularly useful when config merging
   ''' is used heavily, but that's not the point of this base class
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Protected ReadOnly Property AppSettings() As AppSettingsSection
      Get
         Return pConfig.AppSettings
      End Get
   End Property
#End Region


#Region " Methods"
   ''' <summary>
   ''' Persist the host object to the configuration file
   ''' </summary>
   ''' <remarks></remarks>
   Public Sub Save()
      With Me.pConfig
         '---- not using configgroups right now, but keeping this for reference
         'Dim sect = TryCast(.SectionGroups(ROOTSECTION).Sections("options"), ClientSettingsSection)
         'If sect.Settings.Get(ROOTITEM) Is Nothing Then
         '   element = New SettingElement(ROOTITEM, SettingsSerializeAs.Xml)
         '   element.Value.ValueXml = New Xml.XmlDocument().CreateElement("value")
         '   sect.Settings.Add(element)
         'Else
         '   element = sect.Settings.Get(ROOTITEM)
         'End If
         Dim sect = TryCast(.Sections(ROOTSECTION), ClientSettingsSection)
         Dim element = sect.Settings.Get(ROOTITEM)

         '---- Create a serializer to serial our superclass
         Dim s = New XmlSerializer(Me.GetType)
         Using ms = New MemoryStream
            '---- serialize it
            s.Serialize(ms, Me)
            '---- rewind and convert the stream to a string
            ms.Seek(0, SeekOrigin.Begin)
            Dim myutf As UTF8Encoding = New UTF8Encoding()
            '---- load it up into an xml doc
            Dim xml = New XmlDocument
            xml.LoadXml(myutf.GetString(ms.GetBuffer()))
            '---- and push into the settings "value"
            '     stripping out the XML header stuff
            element.Value.ValueXml.InnerXml = xml.DocumentElement.OuterXml
         End Using

         '---- Force the save, because we won't otherwise trigger
         '     the dirty condition
         sect.SectionInformation.ForceSave = True
         .Save()
      End With
   End Sub


   ''' <summary>
   ''' Reload the superclass's properties from configuration
   ''' </summary>
   ''' <remarks></remarks>
   Public Sub Load()
      With Me.pConfig
         Dim sect = TryCast(.Sections(ROOTSECTION), ClientSettingsSection)
         Dim element As SettingElement = sect.Settings.Get(ROOTITEM)

         '---- if we've got the required element...
         If Len(element.Value.ValueXml.InnerXml) Then
            '---- deserialize the xml
            Dim s = New XmlSerializer(Me.GetType)
            Dim myutf As UTF8Encoding = New UTF8Encoding()
            Using ms = New MemoryStream(myutf.GetBytes(element.Value.ValueXml.InnerXml))
               '---- just get a generic object
               '     and we'll use reflection to 
               '     update the properties and fields of THIS object
               Dim o As Object = Nothing
               Try
                  o = s.Deserialize(ms)
               Catch ex As Exception
                  Debug.Print("Problem")
               End Try

               If o IsNot Nothing Then
                  '---- now need to refresh our values from 
                  '     this deserialized object
                  For Each Field In Me.GetType().GetFields
                     If Field.IsPublic Then
                        '---- BaseSettings has no fields, so 
                        '     we don't need to check if the field
                        '     is defined the the base object
                        Try
                           Field.SetValue(Me, Field.GetValue(o))
                        Catch
                        End Try
                     End If
                  Next

                  '---- now copy over any properties
                  For Each Prop In Me.GetType().GetProperties
                     '---- first, check that the property
                     '     isn't one of the BaseSettings properties
                     '     we don't want to depersist those!
                     Dim n = Prop.Name
                     Dim query As IEnumerable(Of System.Reflection.PropertyInfo) = Me.GetType.BaseType.GetProperties.Where(Function(Prop2) Prop2.Name = n)
                     If query.Count = 0 Then
                        '---- this is not a property on BaseSettings
                        If Prop.CanWrite Then
                           If Prop.GetIndexParameters.Count = 0 Then
                              '---- handle non-indexed properties
                              Try
                                 Prop.SetValue(Me, Prop.GetValue(o, Nothing), Nothing)
                              Catch
                              End Try
                           Else
                              '---- not handling indexed properties yet
                           End If
                        End If
                     End If
                  Next
               End If
            End Using
         End If
      End With
   End Sub
#End Region


#Region " SettingsBaseDictionary"
   ''' <summary>
   ''' A simple serializable dictionary class I pulled off the web.
   ''' 
   ''' Originally at http://www.playswithcomputers.com/SGDCollection.aspx
   ''' 
   ''' I've included it here because very often settings collections
   ''' need to be keyed for access, and lists/bindinglists (which will
   ''' persist just fine) don't make that especially straightforward
   ''' like a dictionary.
   ''' 
   ''' However, you can also use a generic List or BindingList for properties
   ''' and they seem to be persisted just fine, they just aren't quite 
   ''' as easy to perform keyed lookups on.
   ''' 
   ''' </summary>
   ''' <remarks></remarks>
   ''' <editHistory>
   ''' </editHistory>
   <XmlRoot("dictionary", IsNullable:=True)> _
   Public Class SettingsBaseDictionary(Of TKey, TValue)
      Inherits Generic.Dictionary(Of TKey, TValue)
      Implements IXmlSerializable

      Private Const ITEMNAME = "item"
      Private Const KEYNAME = "key"
      Private Const VALUENAME = "value"

      Public Function GetSchema() As System.Xml.Schema.XmlSchema Implements IXmlSerializable.GetSchema
         Return Nothing
      End Function


      Public Sub New()
         MyBase.New()
      End Sub
      Public Sub New(ByVal capacity As Integer)
         MyBase.New(capacity)
      End Sub
      Public Sub New(ByVal comparer As System.Collections.Generic.IEqualityComparer(Of TKey))
         MyBase.New(comparer)
      End Sub
      Public Sub New(ByVal capacity As Integer, ByVal comparer As System.Collections.Generic.IEqualityComparer(Of TKey))
         MyBase.New(capacity, comparer)
      End Sub
      Public Sub New(ByVal dictionary As Generic.IDictionary(Of TKey, TValue))
         MyBase.New(dictionary)
      End Sub
      Public Sub New(ByVal dictionary As Generic.IDictionary(Of TKey, TValue), ByVal comparer As System.Collections.Generic.IEqualityComparer(Of TKey))
         MyBase.New(dictionary, comparer)
      End Sub
      Public Sub New(ByVal info As System.Runtime.Serialization.SerializationInfo, ByVal context As System.Runtime.Serialization.StreamingContext)
         MyBase.New(info, context)
      End Sub


      ''' <summary>
      ''' Read a Serialized XML Dictionary of generic objects
      ''' </summary>
      ''' <param name="reader"></param>
      ''' <remarks></remarks>
      Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) Implements IXmlSerializable.ReadXml
         Dim keySerializer As XmlSerializer = New XmlSerializer(GetType(TKey))
         Dim valueSerializer As XmlSerializer = New XmlSerializer(GetType(TValue))
         Dim wasEmpty As Boolean = reader.IsEmptyElement
         reader.Read()
         If wasEmpty Then Return

         Do While (reader.NodeType <> System.Xml.XmlNodeType.EndElement)
            reader.ReadStartElement(ITEMNAME)
            reader.ReadStartElement(KEYNAME)

            Dim key As TKey = DirectCast(keySerializer.Deserialize(reader), TKey)
            reader.ReadEndElement()

            reader.ReadStartElement(VALUENAME)
            Dim value As TValue = DirectCast(valueSerializer.Deserialize(reader), TValue)
            reader.ReadEndElement()

            Me.Add(key, value)

            '---- finish reading this element and move to the next
            reader.ReadEndElement()
            reader.MoveToContent()
         Loop
         reader.ReadEndElement()
      End Sub


      ''' <summary>
      ''' Write the XML Serialization of a dictionary of generic objects
      ''' </summary>
      ''' <param name="writer"></param>
      ''' <remarks></remarks>
      Public Sub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements IXmlSerializable.WriteXml
         Dim keySerializer As XmlSerializer = New XmlSerializer(GetType(TKey))
         Dim valueSerializer As XmlSerializer = New XmlSerializer(GetType(TValue))

         For Each key As TKey In Me.Keys
            writer.WriteStartElement(ITEMNAME)

            writer.WriteStartElement(KEYNAME)
            keySerializer.Serialize(writer, key)
            writer.WriteEndElement()

            writer.WriteStartElement(VALUENAME)
            valueSerializer.Serialize(writer, DirectCast(Me(key), TValue))
            writer.WriteEndElement()
            writer.WriteEndElement()
         Next
      End Sub
   End Class
#End Region

End Class

Points of Interest

  • Your settings file will, by default, be written to the CommonApplicationData folder, and in there, in a folder named the same as the CompanyName in your assembly information screen, and in there, in a folder named the same as the Application Name in the assembly information screen.

image 

So, for the above app, under Windows XP, you’ll find the default setting file in

c:\Documents and Settings\All Users\Application Data\One Nifty Company\SettingsTest\Settings.config

and under Vista

c:\Program Data\One Nifty Company\SettingsTest\Settings.config

This is pretty standard practice for config files, but if you want to change it, you’ve got the source<g>.

  • You can change the setting filename, the Company Name and the Product Name used by simply changing the associated  properties of your setting object (these properties are all inherited from the SettingsBase object).
  • Although you can use fields in your base Settings object, you won’t be able to use any DataBinding support with them. This is a limitation of the .NET Databinding support, and not with the Settings class. Quite frankly, it sucks. Sometimes full blown properties make sense, but in this case, properties are just a lot more work for the same net effect.
    If you want to use Databinding with your settings class, you’re probably best off declaring all persistent elements of your settings class as properties to begin with, and just avoid using fields at all.
  • The SettingsBase object doesn’t even try to provide access to configuration “sections” or “section groups”. You can easily break settings down into groups or sections by using “sub objects” off your main settings object (the one that inherits from SettingsBase). I illustrate this is the sample app.
  • There’s no merging (at least not intentionally anyway). I’m still not completely sure of how that works, and I haven’t needed it yet. 
  • As I indicated earlier, much of this could be accomplished by creating a custom ConfigurationSection class, and registering it in your config file, but from what I can tell so far, that mean you have to manually add gunk to your config file. To me, that’s got a code smell akin to that guy in the next cube that burns patchouli all day. It doesn’t stink, per se, but it sure makes your eyes water if you’re around it long enough.

<configSections>
    <section name="sampleSection"
    type="System.Configuration.MySectionHandler" />
</configSections>

 

What’s Next

  • Really, this should be wrappable in a custom ConfigurationSection handler. My only gripes with that approach is that it doesn’t resolve the folder naming problem, and it requires the goofy registration of the section handler. I’m betting there’s ways around both those issues, though. I just haven’t found them yet.
  • In the same vein, I’d really love to figure a way to accommodate what I’m looking for, AND still use the My.Settings namespace as it’s currently automatically defined by Visual Studio. Again, I’m working on that, but this simple approach works for the short and sweet utility apps I’ve needed to turn out recently.

Check it out and let me know what you think!

Tooltips for Disabled Controls

5
Filed under .NET, Code Garage, VB Feng Shui

We recently got into a discussion where I work about the best way to explain to a user why a control is disabled.

One person argued that it made sense to leave the “disabled” controls enabled, and, when a user tried to click/use the control, pop up a messagebox explaining why it won’t work in this instance.

To me, that seemed to go against everything I’d ever learned about UI design and controls, namely, if a control is disabled, it ought to look disabled on screen and it shouldn’t do anything if you poke at it.

Still, I’d had plenty of experience myself with apps where controls were disabled and I had no idea why they were disabled, much less how to go about getting that functionality enabled. That can certainly make for a frustrating time.

Then someone suggested a tooltip or a status bar message. If you tried to click the disabled control, or hovered your mouse over it, you’d get a little, innocuous message somewhere telling you why that control was disabled.

Awesome idea!

Only one problem.

Tooltips don’t work for disabled controls. Actually, they do for menus, toolbars and likely a few others, but that’s another story.

And you don’t get MouseOver events on disabled controls.

Sigh.

Well, I couldn’t just walk away from this.

A quick google turned up a few bits:

  • There was this from Roy Auchterlounie, but it’s MFC.
  • But then there was this nugget from a post by Linda Lui, apparently with MS Support. It’s in C#, but it’s relatively easy to translate to VB.

The only thing was, Lui’s solution was not exactly what I’d call an encapsulated solution.  As Scott Hanselman would say, dropping snippets of code like this all over my forms just doesn’t have a wonderfully fragrant code smell.

I’d messed around with Control Extenders under ASP.NET some time ago, and this seemed like the perfect excuse to try it out on a good ol’ WinForms app.

A little refactoring later, and I’ve ended up with the “Disabled Tool Tip” Extender Control. It directly inherits from the out-of-the-box tooltip in VS2008. As a result, there’s not a lot of code here. Also, it should work with VS2005, but I’m not guaranteeing as much.

Add this class to your project, recompile, then drop one onto a form.

Zip! Boom! Pow! Every control on your form should now have a “ToolTip on DisabledToolTip” property. You simply set this new property to the tooltip you want to show when the control is disabled and you’re done.

image

(my test rig, she is much fine, no?<g>)

A few notes about this class.

One significant issue I ran into immediately, was how do you retrieve a reference to the containing form if you’re a component sited on that form. All the obvious stuff didn’t work. There’s gotta be a more straightforward way to do it, but I failed to find it, at least with respect to a Component type control (one of those that isn’t actually sited ON the form, but rather in that little area at the bottom of the designer).

I ended up caching an instance of some control during the SetToolTip method, since this method is called during the form initialization by any control on the form that had a tooltip set for it via the designer.

   Public Shadows Sub SetToolTip(ByVal control As Control, ByVal caption As String)
      MyBase.SetToolTip(control, caption)

      '---- if we don't have the parent form yet...
      If rParentForm Is Nothing Then
         '---- attempt to get it from the control
         rParentForm = control.FindForm
         '---- if that doesn't work
         If rParentForm Is Nothing Then
            '---- cache the control for use a little later
            rControl = control
         End If
      End If
   End Sub

But, you can’t use the FindForm method here, necessarily, because if the form is still being initialized, you’ll get back nothing.

So, I ended up implementing the ISupportInitialize interface, and, during the EndInit method, if I have a cached control reference, I use it at this point to retrieve the parent form via FindForm.

   Public Sub EndInit() Implements ISupportInitialize.EndInit
      '---- if we weren't able to retrieve the form from the control
      '     before, we should be able to now
      If rControl IsNot Nothing Then
         rParentForm = rControl.FindForm
      End If
   End Sub

Roundabout, yes, but it seems to work in a very stable way, and it means I don’t have to resort to MFC style subclassing and the like. I can just monitor events on the parent form via a simple WithEvents variable reference.

Anyway, the full code for the class is here. It’s short enough that I’m not going to bother with a ZIP file at this point.

And finally, as with any code you pick up off the net, I’m making no guarantees of any sort. If it works for you, great. If not. Well, I’ll certainly do my best to help if you let me know. It works for me, but it is necessarily full on, battle tested, bulletproof stuff? Uh. No.

Enjoy! If you see any improvements to be made, please share!

And please, if you post it elsewhere, give me (and Linda Lui) proper credit!

Imports System.ComponentModel

''' <summary>
''' Custom ToolTip Component that is based on a normal tooltip component but tracks tips 
''' for disabled controls
''' Note that the because this is a separate extender, all the controls on a form
''' can have an "Enabled" tip (as normal) AND a "disabled" tip.
''' 
''' By Darin Higgins 2008
''' Based on a code example by Linda Lui (MSFT)
'''
''' </summary>
''' <remarks></remarks>
''' <editHistory></editHistory>
Public Class DisabledToolTip
   Inherits ToolTip
   Implements ISupportInitialize

   '---- hold onto a reference to the host form
   '     to monitor the mousemove
   Private WithEvents rParentForm As System.Windows.Forms.Form

   Private _rbActive As Boolean = True
   ''' <summary>
   ''' Active for the Disabled ToolTip has a slightly different meaning
   ''' than "Active" for a regular tooltip
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   <DefaultValue(True)> _
   Public Shadows Property Active() As Boolean
      Get
         Return _rbActive
      End Get
      Set(ByVal value As Boolean)
         If _rbActive <> value Then
            _rbActive = value
         End If
      End Set
   End Property



   '---- hold on to a control temporarily while we wait for things to 
   '     settle
   Private rControl As Control

   ''' <summary>
   ''' Shadow the settooltip function so we can intercept and save a control
   ''' reference. NOTE: the form MIGHT not be setup yet, so the control
   ''' might not know what it's parent is yet, so we cache the the first control
   ''' we get, and use it later, if necessary
   ''' </summary>
   ''' <param name="control"></param>
   ''' <param name="caption"></param>
   ''' <remarks></remarks>
   Public Shadows Sub SetToolTip(ByVal control As Control, ByVal caption As String)
      MyBase.SetToolTip(control, caption)

      '---- if we don't have the parent form yet...
      If rParentForm Is Nothing Then
         '---- attempt to get it from the control
         rParentForm = control.FindForm
         '---- if that doesn't work
         If rParentForm Is Nothing Then
            '---- cache the control for use a little later
            rControl = control
         End If
      End If
   End Sub


   Public Sub BeginInit() Implements ISupportInitialize.BeginInit
      '---- Our base tooltip is disabled by default
      '     because we don't want to show disabled tooltips when
      '     a control is NOT disabled!
      MyBase.Active = False
   End Sub


   ''' <summary>
   ''' Supports end of initialization phase tasks for this control
   ''' </summary>
   ''' <remarks></remarks>
   Public Sub EndInit() Implements ISupportInitialize.EndInit
      '---- if we weren't able to retrieve the form from the control
      '     before, we should be able to now
      If rControl IsNot Nothing Then
         rParentForm = rControl.FindForm
      End If
   End Sub


   Public Sub New(ByVal IContainer As IContainer)
      MyBase.New(IContainer)
   End Sub


   ''' <summary>
   ''' Monitor the MouseMove event on the host form
   ''' If we see it move over a disabled control
   ''' Check for a tooltip and show it
   ''' If the cursor moved off the control we're displaying
   ''' a tip for, hide the tip.
   ''' </summary>
   ''' <param name="sender"></param>
   ''' <param name="e"></param>
   ''' <remarks></remarks>
   Private Sub rParentForm_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles rParentForm.MouseMove
      Static ctrlWithToolTip As Control = Nothing

      Dim ctrl = rParentForm.GetChildAtPoint(e.Location)

      If ctrl IsNot Nothing Then
         If Not ctrl.Enabled Then
            If ctrlWithToolTip IsNot Nothing Then
               If ctrl IsNot ctrlWithToolTip Then
                  '---- if we're not over the control we last showed
                  '     a tip for, close down the tip
                  Me.Hide(ctrlWithToolTip)
                  ctrlWithToolTip = Nothing
                  MyBase.Active = False
               End If
            End If
            If ctrlWithToolTip Is Nothing Then
               Dim tipstring = Me.GetToolTip(ctrl)
               If Len(tipstring) And Me.Active Then
                  '---- only enable the base tooltip if we're going to show one
                  MyBase.Active = True
                  Me.Show(tipstring, ctrl, ctrl.Width / 2, ctrl.Height / 2)
                  ctrlWithToolTip = ctrl
               End If
            End If

         ElseIf ctrlWithToolTip IsNot Nothing Then
            '---- if we're over an enabled control
            '     the tip doesn't apply anymore
            Me.Hide(ctrlWithToolTip)
            ctrlWithToolTip = Nothing
            MyBase.Active = False
         End If
      ElseIf ctrlWithToolTip IsNot Nothing Then
         '---- if we're not over a control at all, but we've got a
         '     tip showing, hide it, it's no longer applicable
         Me.Hide(ctrlWithToolTip)
         ctrlWithToolTip = Nothing
         MyBase.Active = False
      End If
   End Sub
End Class

Reimagining MsgBox

0
Filed under Code Garage, Software Architecture, VB Feng Shui

Here’s a little trick I’m not sure many people know about, but that I’ve used for so long, I’d basically forgotten about it.

I wrote about overriding the ERR object in VB here, and also wrote a far more involved article about it.

But did you know you can also override the MSGBOX function?

Just create a function with the following signature in a BAS module of your choice:

Public Function MsgBox(ByVal Prompt$, Optional ByVal Buttons As VbMsgBoxStyle = vbOKOnly, Optional ByVal Title$ = "", Optional ByVal HelpFile$ = "", Optional ByVal Context As Long = 0) As VbMsgBoxResult

Put the appropriate code in it, and presto, custom msgbox.

Why, you might ask?

  • Personally, I like the option of logging every msgbox that’s displayed by my app. Errs are handier, but msgbox’s can be useful.
  • In addition, you have the option of calling the MessageBox API call directly yourself, which gives you access to additional flags that you can’t use with the VB version alone.
  • You might also decide that the MSGBOX proper just doesn’t look good, and craft up a modal, nicely skinned form to stand in its place.
  • And finally, in a particular app I worked on, I needed to call a function BEFORE and a different function AFTER displaying every msgbox. I certainly didn’t want to alter every call to MSGBOX and add those extra lines. Overriding it made that a trivial task.

Please let me know if you come up with any good uses for this trick.

True Or False

3
Filed under Code Garage, VB Feng Shui

It seems like such a simple task; check a variable for whether it contains a true value or a false value.

No problem, right? Well, if you’re dealing with numbers, maybe, but when input comes from config files or databases, the truth <ahem> may not be so obviously out there.

As part of my code garage, I thought I’d post two functions I’ve used for ages to do just that; convert a variable, virtually any variable, to a boolean result.

They are especially useful in configuration handling, where you might want to support multiple values that might mean “true” or “false”, like on/off, yes/no, etc. I’ve also found that they can make code clearer by specifying the “assumed” default value if the variable can’t be concretely identified one way or another (is “Bob Thomas” true or false?).

Public Function IsTrue(VarToTest As Variant, Optional ByVal Default As Boolean = True) As Boolean
   '---- Resolve an input variable to a boolean
   '     but convert common "true"/"false" phrases as well
   '     Also, this allows for an easy way to indicate a
   '     "default" value in cases of an undetermined (ie blank)
   '     value
   Dim t$
   Dim s

   Select Case VarType(VarToTest)
      Case vbArray
         Err.Raise 5, "IsTrue", "Can't test an array for true"
      Case vbObject
         IsTrue = ObjPtr(VarToTest) <> 0
      Case vbString
         If Len(VarToTest) = 0 Then
            '---- is true assumes blanks are true
            IsTrue = Default
         Else
            On Error Resume Next
            '---- strip to first space
            t$ = Trim$(Left$(VarToTest, 15))
            s = InStr(t$, " ")
            If s > 0 Then t$ = Left$(t$, s - 1)
            '---- clean out any tabs
            t$ = Replace(t$, Chr$(9), vbNullString)
            '---- accept some synonyms (any other good ones?)
            If InStr(1, t$, "YES", vbTextCompare) = 1 Then
               IsTrue = True
            ElseIf InStr(1, t$, "NO", vbTextCompare) = 1 Then
               IsTrue = False
            ElseIf InStr(1, t$, "ON", vbTextCompare) = 1 Then
               IsTrue = True
            ElseIf InStr(1, t$, "OFF", vbTextCompare) = 1 Then
               IsTrue = False
            ElseIf InStr(1, t$, "TRUE", vbTextCompare) = 1 Then
               IsTrue = True
            ElseIf InStr(1, t$, "FALSE", vbTextCompare) = 1 Then
               IsTrue = False
            Else
               IsTrue = CBool(VarToTest)
               If Err Then
                  IsTrue = Val(VarToTest) <> 0
               End If
               On Error GoTo 0
            End If
         End If
      Case Else
         If IsEmpty(VarToTest) Then
            IsTrue = Default
         Else
            On Error Resume Next
            IsTrue = CBool(VarToTest)
            If Err Then
               IsTrue = Default
            End If
            On Error GoTo 0
         End If
   End Select
End Function


Public Function IsFalse(VarToTest As Variant, Optional ByVal Default As Boolean = False) As Boolean
   '---- basically, the inverse of IsTrue above
   '     mainly for convenience

   IsFalse = Not IsTrue(VarToTest, Default)
End Function

Hey, CBOOL is undoubtedly faster, but:

   If IsFalse(SettingValue) Then 
      '---- handle the negative condition here
   End If

just seems so much clearer.

The optional Default argument allow you to specify what value to return if the value to test can’t be resolved satisfactorily one way or the other. Practically, this allows you to easily specify whether a blank value or a “non-boolean” value should translate to true or false. This is especially important when you’re reading config options from a file or the registry where the option may not exist at all.

It allows to do something like so: 

   If IsTrue(SettingValue, False) Then 
      '---- handle positive conditions here, but if the SettingValue is blank, we default to False
   End If

Simple, but handy.

Comparing MS SQL Database Structures and Data

3
Filed under Code Garage, SQL

If you’ve messed with SQL much, invariably, you get into a situation where you have to ask “What has changed in the database from the last version to this version?”

There’s some good tools out there for this, to be sure.

AdeptSQL is my favorite, but RedGate SQL Compare is very good. And DBGhost is highly regarded for synchronizing db schemas (and data to a degree, I believe).

But sometimes you want (or need) to “roll your own”.

So, how would you do that?

First, the criteria:

  • I need to be able to compare both schema and some “seed data” in certain tables (for instance, pre-populated lookup tables, etc)
  • I’d like to see any significant changes, but everyone’s definition of significant can be different.
  • I’d like the changes to be “easy” to read. They need to pop out if possible

Next, the display.

Well, there’s plenty of file comparison utilities out there, and my favorite is Araxis, so why not just use it to do the “delta-generation” heavy lifting.

I’ll need two text files to compare, the “before” version and the after.

I need them grouped by schema object type (stored-procs in one section, table definitions in another, data values in another, etc).

And I need them ordered consistently so that any changes are easy to spot.

I ended up a with a stored proc that, at least right now, does most of what I need.

The basic idea is that you call DBOutput, and it then calls itself recursively to resolve and output all the various elements of the database that you might be interested in comparing. Why a recursive procedure instead of several separate procs? No earth-shattering reasons. It’s just easier to deal with one proc for this sort of thing that several separate ones.

The routine itself pretty simply, and it doesn’t yet have to resort to directly reading the system tables for any information. It makes heavy use of the INFORMATION_SCHEMA views to retrieve most information. It should run under MSSQL 2000, MSDE, 2005 Express and 2005, but I haven’t tested it under the 2000 variants. 

Probably most notable is the use of sp_helptext to retrieve the actual full definition text of stored procs and views.

You’ll have to excuse the formatting, pasting into HTML seems to have messed up my nice tabs.

ALTER PROCEDURE [dbo].[DBOutput] (
    @TableName sysname = null,
	@SPName sysname = null,
	@ViewName sysname = null
    )

AS

DECLARE @Rows as int

SET NOCOUNT ON

if	len(IsNull(@TableName, '')) = 0 and 
	Len(IsNull(@SPName,'')) = 0 and 
	Len(IsNull(@ViewName,'')) = 0 BEGIN

    -- do all tables
    declare @Table sysname
	declare @Routine sysname
	declare @View sysname
    declare @i int
    
    DECLARE cCursor CURSOR FOR
        select lower(Table_Name) from Information_Schema.Tables
        where table_type = 'base table' order by Table_Name
    
    OPEN cCursor
    FETCH NEXT FROM cCursor INTO @Table
    WHILE @@FETCH_STATUS = 0 BEGIN
        -- check for tables we don't care about
        if @Table in ('sysdiagrams'
                     )
            -- do nothing
            set @i = 0
        else if left(@Table,2) in ('ms', 'xx')
            set @i = 0
        else if left(@Table,3) in ('sys')
            set @i = 0
        else if left(@Table,6) in ('dtprop')
            set @i = 0
        else BEGIN
            exec DBOutput @Table, ''
            END
    
        FETCH NEXT FROM cCursor INTO @Table
        END

    CLOSE cCursor
    DEALLOCATE cCursor


	-- now Scan all the Stored Procs and functions
    DECLARE rCursor CURSOR FOR
        select lower(ROUTINE_NAME) from Information_Schema.Routines order by ROUTINE_NAME
    
    OPEN rCursor
    FETCH NEXT FROM rCursor INTO @Routine
    WHILE @@FETCH_STATUS = 0 BEGIN
        -- check for Stored Procs and functions we don't care about
        if @Routine in (''
                     )
            -- do nothing
            set @i = 0
        else if left(@Routine, 3) in ('sp_', 'fn_')
            set @i = 0
        else BEGIN
            exec DBOutput '', @Routine
            END
    
        FETCH NEXT FROM rCursor INTO @Routine
        END

    CLOSE rCursor
    DEALLOCATE rCursor


	-- now Scan all the Stored Procs and functions
    DECLARE vCursor CURSOR FOR
        select lower(TABLE_NAME) from Information_Schema.Views order By TABLE_NAME
    
    OPEN vCursor
    FETCH NEXT FROM vCursor INTO @View
    WHILE @@FETCH_STATUS = 0 BEGIN
        -- check for Stored Procs and functions we don't care about
        if @View in (''
                     )
            -- do nothing
            set @i = 0
        else if left(@View, 3) in ('sp_', 'fn_')
            set @i = 0
        else BEGIN
            exec DBOutput '', '', @View
            END
    
        FETCH NEXT FROM vCursor INTO @View
        END

    CLOSE vCursor
    DEALLOCATE vCursor

    RETURN
    END


-- handle a single table recursively
IF Len(@TableName) > 0 BEGIN
	DECLARE @PrimaryKeyColumn nvarchar(64)
	DECLARE @sql nvarchar(1000)
	DECLARE @Parms nvarchar(1000)
	DECLARE @Temp sysname

	-- Primary key info (we're not using yet)
	SELECT @PrimaryKeyColumn = [COLUMN_NAME] FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
	   WHERE [TABLE_NAME] = @TableName

	-- dump a list of the columns, with pertinent info
	PRINT replicate('*', 60)
	PRINT replicate('*', 60)
	PRINT 'TABLE: ' + @TableName 
	EXEC ('SELECT ORDINAL_POSITION as ColNum, left(COLUMN_NAME, 20) as ColName, left(DATA_TYPE, 20) as DataType, Left(COLUMN_DEFAULT, 20) as DefaultValue, CHARACTER_MAXIMUM_LENGTH as Len, IS_NULLABLE from INFORMATION_SCHEMA.COLUMNS WHERE Table_name = ''' + @TableName + ''' order by ColNum')

	-- dump a list of the indexes, with pertinent info
	-- also includes RI information associated with the keys (if any)
	EXEC ('SELECT	Keys.ORDINAL_POSITION as KeyNum, 
					left(Keys.CONSTRAINT_NAME,30) as IDXName, 
					left(Keys.COLUMN_NAME, 20) as ColumnName, 
					Left(RI.UNIQUE_CONSTRAINT_NAME, 20) as PrimaryKey, 
					RI.Match_Option, 
					RI.Update_Rule, 
					RI.Delete_Rule,
					CHK.Check_Clause
			FROM information_schema.Key_Column_Usage as Keys 
					left outer join information_schema.referential_constraints as ri on keys.Constraint_Name = ri.Constraint_Name 
					left outer join information_schema.check_constraints as chk on keys.Constraint_name = chk.Constraint_Name
			WHERE Table_name = ''' + @TableName + ''' 
			order by KeyNum, IDXName')

	-- check if there's any data in the table. If so, dump it all
	set @SQL = 'SELECT @Cnt = Count(*) FROM ' + @TableName
	set @parms = '@Cnt int OUTPUT'
	EXEC sp_executesql @SQL, @Parms, @Cnt = @Rows OUTPUT
	IF @Rows > 0 BEGIN
		PRINT replicate('~', 60)
		PRINT 'TABLE DATA FOR:' + @TableName
		EXEC ('SELECT * FROM ' + @TableName)
		END
	PRINT ''
	PRINT ''
	END


-- Handle all stored procs here
IF LEN(IsNull(@SPName, '')) > 0 BEGIN
	PRINT replicate('*', 60)
	PRINT replicate('*', 60)
	PRINT 'STORED PROCEDURE/FUNCTION: ' + @SPName
 
	-- pull the entire text of the proc into a variable to let us print it nicely
	EXEC sp_helptext @SPName
	PRINT ''
	PRINT ''
	END


-- Handle all Views
IF LEN(IsNull(@ViewName, '')) > 0 BEGIN
	PRINT replicate('*', 60)
	PRINT replicate('*', 60)
	PRINT 'VIEW: ' + @ViewName
 
	-- pull the entire text of the proc into a variable to let us print it nicely
	EXEC sp_helptext @ViewName
	PRINT ''
	PRINT ''
	END


SET NOCOUNT OFF

Basically, you just open up Enterprise Manager, and exec DBOutput. It’s best to put the results window in TEXT mode as opposed to grid mode. Then you can simply save the output to a text file and use your favorite file compare to get a delta. All the PRINTs are there just to make things a little more legible in a typical “text editor” view.

Let me know what you think, or if there’s additional bits to report that might be useful.

Also, I plan to update this script over time, and I’ll post the updates here.