One of the biggest niceties about a Tivo is that you don’t have to fumble with media. I used to have a stack of VHS tapes that I’d put in rotation to records shows I’d want to watch. I’d usually record over them once I’d watched the show, but dealing with all the media was a big headache (forget about actually programming the VCR).
Fast forward, and now, instead of tapes, we have DVDs, CDs, and BluRay discs. Same basic problem though.
Fortunately, Tivo to the rescue! I’ve written before about the easiest free way to move a DVD movie over to the Tivo, so I won’t rehash that here.
However, recently, I ended up with a subtitled foreign language film that I wanted to move to my Tivo. I moved it just fine, but I ended up with an MPG file that contained the foreign language film and NO SUBTITLES! Ack!
So, I need amend my previous post with additional information about how to handle subtitled movies, still completely free. It’s a little more involved but not much so.
Step 1
First, you’ll need to copy the movie off the DVD using DVDFab. This is the same as before. However, you’ll want to be sure to copy the “Full Disc” this time, since you want to make sure you have the subtitle files available for later. Grab a copy of the free “DVDFab Decrypter” here. Install it then run it and choose the “Full Disc” option, as highlighted below.
Choose a target folder on your harddrive somewhere and kick it off. We go through this first step for 2 reasons:
if the DVD has been encrypted, this removes that, so that the following utilities will work on the movie.
all the remaining processing is MUCH faster if the source files are on your harddrive and not the DVD.
Step 2
Now that you’ve got all the files from the DVD to your harddrive, you need to “render” the movie with the appropriate subtitles. You see, subtitles are embedded in the VOB files on the DVD as literally bitmapped images of the text that need to be overlaid by the DVD player when you select to show subtitles (if you were to play the movie on an actual DVD player). Since we’ll be ending up with an MPG file, none of that will apply. Tivo doesn’t have clue one about subtitles and whatnot, so the separate subtitle images are useless to it.
Instead, what you need is a copy of the movie with the subtitles “burned onto” the actual frames of the movie. This way, the Tivo can simply “play” the movie, the subtitles will be just a part of the image frames in the movie. This does mean you won’t be able to “turn off” the subtitles, but, at least for my purposes, that’s a pretty minor issue.
In order to burn the subtitles onto the movie, you’ll need the free program AutoGordianKnot. Yeah, weird name, but it does exactly what it says it will do. Essentially, it converts the DVD VOB fileset into a DIVX AVI file, but it can render the subtitles into the output avi file easily.
In the above screenshot:
Select the input file (choose the VTS_01_0.IFO file, that will almost always be the proper root file to pick). Select an output folder for the resulting AVI file.
Pick your audio track (if english is listed, you shouldn’t need to be doing any of this!).
Select the subtitles you want rendered into the output avi file. In this case, you’ll likely want English, but there might be more than one choice here
I set the target quality to 100. This is because we’re going to have to “reconvert” the video again and the higher the quality here, the better the end result will be.
And finally, click the Add Job and then the Start buttons.
Note that in my case, the very first time I ran this, I had a few dialogs pop up that I had to click OK on. If you don’t wait for them, eventually the program will timeout and the conversion will fail. The good thing is that, at least for me, all the dialogs displayed within the first 5 minutes or so of the process, so you shouldn’t have to watch the entire process. It can take a while!
Step 3
Once that’s done, you should end up with an AVI file that is playable and that contains the foreign language audio track, along with the english subtitles. Yeah!
However, there’s one problem. The Tivo doesn’t know how to play these files. Basically, Tivos can ONLY play MPG files, and AutoGK can ONLY render DivX AVI files.
With AVITompg, the easiest option is to download their “Portable” version. It’s the exe and nothing more. Just download it and run it!
Click the “Add Video” button, select the AVI file you generated in Step 2 above, then be sure to select the DVD Compatible MPEG2 format in the Output Format box.
The other MPG formats should work as well, but I haven’t tested them. Leave the other settings “Auto”. I found no need to change any of them.
Click OK, then click the Convert button and let it go! When it finishes, you should have an MPG file in the output folder, that contains the foreign language audio track and the English subtitles.
Done!
The MPG file is the only one you still need. You can delete all the VOB files from Step 1 and the AVI file from Step 2.
Now, just move that MPG file wherever it needs to go so that you can get to it from your Tivo and you’ll be able to watch that movie, pause, rewind etc. No DVD to hassle with anymore, either!
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.
PublicClass Records
Inherits List(Of Record)
PublicSubNew()
'---- default constructorEndSubEndClassPublicClass Record
Inherits Dictionary(Of String, Field)
PublicSubNew()
'---- default constructorEndSubPublicSubNew(ByValParamArray Fields() As Field)
ForEach f In Fields
Me.Add(f.Name, f)
NextEndSubEndClassPublicClass Field
PublicSubNew()
'---- default constructorEndSubPublicSubNew(ByVal Name AsString, ByVal Value AsString)
Me.Name = Name
Me.Value = Value
EndSubPublicProperty Name() AsStringGetReturn _Name
EndGetSet(ByVal value AsString)
_Name = value
EndSetEndPropertyPrivate _Name AsStringPublicProperty Value() AsStringGetReturn _Value
EndGetSet(ByVal value AsString)
_Value = value
EndSetEndPropertyPrivate _Value AsStringEndClass
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>PublicFunction Serialize(Of T AsClass)(ByVal SerializeWhat As T) AsStringDim 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
EndFunction
In the test application, I put together, I dump the resulting XML to a text box. Yikes!
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>PublicFunction Serialize(Of T AsClass)(ByVal SerializeWhat As T) AsStringDim stream = New System.IO.StringWriter
Dim xmlsettings = New Xml.XmlWriterSettings
xmlsettings.Indent = TrueDim 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
EndFunction
Notice that I added the use of the XMLWriterSettings object. This allows me to set the Indent property, and things are much more readable.
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:="")> _
PublicClass Field
PublicSubNew()
'---- default constructorEndSubPublicSubNew(ByVal Name AsString, ByVal Value AsString)
Me.Name = Name
Me.Value = Value
EndSub
<DataMember()> _
PublicProperty Name() AsStringGetReturn _Name
EndGetSet(ByVal value AsString)
_Name = value
EndSetEndPropertyPrivate _Name AsString
<DataMember()> _
PublicProperty Value() AsStringGetReturn _Value
EndGetSet(ByVal value AsString)
_Value = value
EndSetEndPropertyPrivate _Value AsStringEnd Class
But this yields:
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:
PublicClass Field
Implements System.Xml.Serialization.IXmlSerializable
PublicSubNew()
'---- default constructorEndSubPublicSubNew(ByVal Name AsString, ByVal Value AsString)
Me.Name = Name
Me.Value = Value
EndSubPublicProperty Name() AsStringGetReturn _Name
EndGetSet(ByVal value AsString)
_Name = value
EndSetEndPropertyPrivate _Name AsStringPublicProperty Value() AsStringGetReturn _Value
EndGetSet(ByVal value AsString)
_Value = value
EndSetEndPropertyPrivate _Value AsStringPublicFunction GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema
ReturnNothingEndFunctionPublicSub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml
EndSubPublicSub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml
writer.WriteElementString("Name", Me.Name)
writer.WriteElementString("Value", Me.Value)
EndSubEnd Class
And that yields a serialization of:
Definitely better, but still not great.
Cleaning up a Generic Dictionary’s Serialization
The problem now is with the Record dictionary.
PublicClass Record
Inherits Dictionary(Of String, Field)
Implements System.Xml.Serialization.IXmlSerializable
PublicSubNew()
'---- default constructorEndSubPublicSubNew(ByValParamArray Fields() As Field)
ForEach f In Fields
Me.Add(f.Name, f)
NextEndSubPublicFunction GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema
ReturnNothingEndFunctionPublicSub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml
EndSubPublicSub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml
ForEach f InMe.Values
DirectCast(f, System.Xml.Serialization.IXmlSerializable).WriteXml(writer)
NextEndSubEndClass
Adding an IXMLSerializable implementation to it as well yields the following XML:
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:="")> _
PublicClass Records
Inherits List(Of Record)
Implements System.Xml.Serialization.IXmlSerializable
PublicSubNew()
'---- default constructorEndSubPublicFunction GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema
ReturnNothingEndFunctionPublicSub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml
EndSubPublicSub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml
ForEach r InMeDirectCast(r, System.Xml.Serialization.IXmlSerializable).WriteXml(writer)
NextEndSubEndClass
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:
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:="")> _PublicClass Records
Inherits List(Of Record)
Implements System.Xml.Serialization.IXmlSerializable
PublicSubNew()
'---- default constructorEndSubPublicFunction GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema
ReturnNothingEndFunctionPublicSub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml
EndSubPublicSub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml
ForEach r InMewriter.WriteStartElement("Record")DirectCast(r, System.Xml.Serialization.IXmlSerializable).WriteXml(writer)
writer.WriteEndElement()NextEndSubEndClass<Xml.Serialization.XmlRoot(ElementName:="Record", Namespace:="")> _PublicClass Record
Inherits Dictionary(Of String, Field)
Implements System.Xml.Serialization.IXmlSerializable
PublicSubNew()
'---- default constructorEndSubPublicSubNew(ByValParamArray Fields() As Field)
ForEach f In Fields
Me.Add(f.Name, f)
NextEndSubPublicFunction GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema
ReturnNothingEndFunctionPublicSub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml
EndSubPublicSub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml
ForEach f InMe.Values
writer.WriteStartElement("Field")DirectCast(f, System.Xml.Serialization.IXmlSerializable).WriteXml(writer)
writer.WriteEndElement()NextEndSubEndClass<Xml.Serialization.XmlRoot(ElementName:="Field", Namespace:="")> _PublicClass Field
Implements System.Xml.Serialization.IXmlSerializable
PublicSubNew()
'---- default constructorEndSubPublicSubNew(ByVal Name AsString, ByVal Value AsString)
Me.Name = Name
Me.Value = Value
EndSubPublicProperty Name() AsStringGetReturn _Name
EndGetSet(ByVal value AsString)
_Name = value
EndSetEndPropertyPrivate _Name AsStringPublicProperty Value() AsStringGetReturn _Value
EndGetSet(ByVal value AsString)
_Value = value
EndSetEndPropertyPrivate _Value AsStringPublicFunction GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema
ReturnNothingEndFunctionPublicSub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml
EndSubPublicSub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml
writer.WriteElementString("Name", Me.Name)
writer.WriteElementString("Value", Me.Value)
EndSubEndClass
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.
PublicClass frmSample
PrivateSub btnTest_Click(ByVal sender AsObject, ByVal e As System.EventArgs) Handles btnTest.Click
'---- populate the objectsDim 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 AsString
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)
EndSubEndClass
<Xml.Serialization.XmlRoot(Namespace:="")> _
PublicClass Records
Inherits List(Of Record)
Implements System.Xml.Serialization.IXmlSerializable
PublicSubNew()
'---- default constructorEndSubPublicFunction GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema
ReturnNothingEndFunctionPublicSub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml
reader.MoveToContent()
reader.ReadStartElement("Records")
reader.MoveToContent()
DoWhile 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()
EndSubPublicSub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml
ForEach r InMe
writer.WriteStartElement("Record")
DirectCast(r, System.Xml.Serialization.IXmlSerializable).WriteXml(writer)
writer.WriteEndElement()
NextEndSubEndClass
<Xml.Serialization.XmlRoot(ElementName:="Record", Namespace:="")> _
PublicClass Record
Inherits Dictionary(Of String, Field)
Implements System.Xml.Serialization.IXmlSerializable
PublicSubNew()
'---- default constructorEndSubPublicSubNew(ByValParamArray Fields() As Field)
ForEach f In Fields
Me.Add(f.Name, f)
NextEndSubPublicFunction GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema
ReturnNothingEndFunctionPublicSub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml
reader.MoveToContent()
reader.ReadStartElement("Record")
reader.MoveToContent()
DoWhile 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()
EndSubPublicSub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml
ForEach f InMe.Values
writer.WriteStartElement("Field")
DirectCast(f, System.Xml.Serialization.IXmlSerializable).WriteXml(writer)
writer.WriteEndElement()
NextEndSubEndClass
<Xml.Serialization.XmlRoot(ElementName:="Field", Namespace:="")> _
PublicClass Field
Implements System.Xml.Serialization.IXmlSerializable
PublicSubNew()
'---- default constructorEndSubPublicSubNew(ByVal Name AsString, ByVal Value AsString)
Me.Name = Name
Me.Value = Value
EndSubPublicProperty Name() AsStringGetReturn _Name
EndGetSet(ByVal value AsString)
_Name = value
EndSetEndPropertyPrivate _Name AsStringPublicProperty Value() AsStringGetReturn _Value
EndGetSet(ByVal value AsString)
_Value = value
EndSetEndPropertyPrivate _Value AsStringPublicFunction GetSchema() As System.Xml.Schema.XmlSchema Implements System.Xml.Serialization.IXmlSerializable.GetSchema
ReturnNothingEndFunctionPublicSub ReadXml(ByVal reader As System.Xml.XmlReader) Implements System.Xml.Serialization.IXmlSerializable.ReadXml
reader.MoveToContent()
reader.ReadStartElement("Field")
reader.MoveToContent()
If reader.Name = "Name"ThenMe.Name = reader.ReadElementContentAsString
reader.MoveToContent()
If reader.Name = "Value"ThenMe.Value = reader.ReadElementContentAsString
reader.MoveToContent()
reader.ReadEndElement()
EndSubPublicSub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements System.Xml.Serialization.IXmlSerializable.WriteXml
writer.WriteElementString("Name", Me.Name)
writer.WriteElementString("Value", Me.Value)
EndSubEndClassPublicModule Serialize
''' <summary>''' Serializes the data contract to a string (XML)''' </summary>PublicFunction Serialize(Of T AsClass)(ByVal SerializeWhat As T) AsStringDim stream = New System.IO.StringWriter
Dim xmlsettings = New Xml.XmlWriterSettings
xmlsettings.Indent = TrueDim 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
EndFunction''' <summary>''' Deserializes the data contract from xml.''' </summary>PublicFunction Deserialize(Of T AsClass)(ByVal xml AsString) As T
Using stream AsNew MemoryStream(UnicodeEncoding.Unicode.GetBytes(xml))
Return DeserializeFromStream(Of T)(stream)
End Using
EndFunction''' <summary>''' Deserializes the data contract from a stream.''' </summary>PublicFunction DeserializeFromStream(Of T AsClass)(ByVal stream As Stream) As T
Dim serializer AsNew DataContractSerializer(GetType(T))
ReturnDirectCast(serializer.ReadObject(stream), T)
EndFunctionEnd 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!
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.
PublicSub 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"))
EndSub
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.
PublicClass StringDict
Inherits Dictionary(Of String, String)
PublicSubNew()
MyBase.New(StringComparer.CurrentCultureIgnoreCase)
EndSubEndClassPublicSub 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"))
EndSub
It may not be new, but it’s new to me, and awfully nice to know!
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?):
PublicFunction SplitQuoteComma(ByVal Args AsString) AsString()
Dim r = New System.Text.RegularExpressions.Regex(",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))")
Return r.Split(Args)
End Function
If you’ve ever had to use the Word Object Model’s Find object, you probably know it can be a bit… well… finicky. It has guids that conflict with Excel 95 typelibs which causes problems, it uses a veritable forest of optional parameters, most of which are declared as OBJECT so intellisense doesn’t work well with it, it’s a real pain to use with C# because of C#’s lack of optional parameter handling, etc, etc.
And I have a new one to add to the list.
I was recently working on a kind of super advanced mail merge facility for Word documents. Word’s MailMerge capabilities are certainly good, but they couldn’t cut the mustard for this particular batch of requirements.
However, since the MAILMERGE field (and all the various other fields in Word) have a lot of capability, I wanted to leverage that as much as possible.
Essentially, the templates to be used would embed normal mail merge fields, but Word’s Merge ability would never get used. Instead, I’d have a VB.NET assembly that would navigate through each field in the document, analyze its code, and replace it with the appropriate result. The code looks something like this:
Dim Code = Field.Code.Text
'....parse the code for the field here' determine the final field contents, put it in Content
Field.Result.Text = Content
Field.Unlink
Pretty simply stuff, really.
But, the content of the field might have embedded style information. Sort of like a stripped down HTML code system. For instance, <b> might be “Turn bold on”, and </b> would turn bold off.
First thought
Initially, I thought I could just detect the existence of the opening tag, remove all the tags, and just format the entire field consistently. It’s simple and fast. But unfortunately, some field values had tags embedded within the field, meaning that only a portion of the field’s data should be formatted.
Strike 1.
RTF??? In 2010?
Then, I figured I could just swap out the formatting code with the equivalent RTF codes, and use PasteSpecial to paste the RTF formatted data directly in the field.
While this did work, it presented a bad side effect. Since the RTF text coming off the clipboard represented specific and complete formatting information, it overrode all default formatting already applied to the field. So, say the entire document was formatted in Calibri Font. When I pasted the RTF text “{\b My Text Here}”, the end result was a field formatted in bold Times New Roman, not Calibri. So even though I hadn’t specifically set the font for the text, the paste special was assuming a default of Times.
Strike 2
That ol’ Word FIND Object
I finally realized that I was going to HAVE to use the FIND object in Word to locate those formatting marks, and then apply the applicable styles to the found text.
Performing the Find is pretty straightforward, although there’s a few things to watch for.
The first is the aforementioned Excel 95 Guid clash. What it means is that you need to access the FIND object via late binding. So obtain a FIND object from the (Field.Result range) and store it in an OBJECT variable. That will force VB.net to make latebound calls to it.
The next problem is how to actually find the marks. You’ll need to use the Find object’s wildcard support for that. The code looks something like this:
Dim Find asObject = Field.Result.Find
With Find
.ClearFormatting
If .Execute(FindText:="\<b\>*\</b\>", MatchCase:= false, MatchWildcards:= True, Forward:= True) then'the text was foundElse'The text was not foundEndIfEndWith
Again, pretty simple stuff.
But there was two problems.
The first was, the above would find the text alright, but I still needed to remove those embedded formatting marks. After a false starts, I realized that the REPLACE functionality was could do this very easily. Just mark the * (i.e. the found text that you want to keep) by enclosing it in “()”, then Replace with “\1” (the first marked substring).
Dim Find asObject = Field.Result.Find
With Find
.ClearFormatting
If .Execute(FindText:="\<b\>(*)\</b\>", MatchCase:= false, MatchWildcards:= True, Forward:= True, Replace:=wdReplace.wdReplaceAll, ReplaceWith:="\1") then'the text was foundElse'The text was not foundEndIfEndWith
The second problem was much more insidious.
It worked perfectly on fields embedded in the body of the document, but completely failed to find anything in field embedded in cells in a table. Needless to say, I was scratching my head over that one for a while.
It turns out that Find just doesn’t work right if you attempt to use it in the Result range of a Field object when that object has been embedded within a table.
The trick, then is to Unlink the field object before you attempt a find inside of it.
Dim Rng = Field.Result
Field.Unlink
Dim Find = Rng.Find
With Find
'.... continue as before
With the range no longer within the field, the Find works just like it should, regardless of whether the field is in the body of the document or embedded within a table.
So there’s my terrifically arcane Office object model trivia for the day!
So, I was working on parsing a WordPerfect Merge DAT file today…
Yep, you read that right, WordPerfect! Seriously old school stuff.
Anyway, this file format is a peculiar binary format with a bunch of binary headers surrounding all the nice juicy ASCII merge text fields. Dealing with such files in VB6 was easy, but it seems that the few times I’ve had to work with this type of thing in .NET, I always end up stubbing my toe on encoding.
You see, all strings in .NET are UNICODE, and reading binary data like this into a string involves an encoding or decoding process. In order for things to work the way you’d (or at least, I’d) expect them to, you have to be SURE that the strings will round trip properly. And boy, they weren’t for me.
I wrestled with it all day, finally knocking off to go home and unwind.
But it was still bugging me.
So I whipped up this little sample (why didn’t I think of this at noon today<sigh>).
PrivateSub TestEncode()
Dim b(255) AsByteFor x = 0 To 255 : b(x) = x : NextDim buf AsString = System.Text.Encoding.Unicode.GetString(b)
Dim c(255) AsByte
c = System.Text.Encoding.Unicode.GetBytes(buf)
For x = 0 To 255
If c(x) <> x ThenStopNextStopEndSub
If the code stops at that stop in the last FOR loop, something didn’t round trip properly and you’re pretty much guaranteed a headache.
And sure enough, the UNICODE encoding object failed to round trip. But so does the ASCIIENCODING, UTF8, etc etc.
On a whim, I tried the “default” object, SYSTEM.TEXT.ENCODING.DEFAULT.
And it worked!
A quick check revealed that on my system, DEFAULT is actually the encoding object for the codepage 1252, which is the Windows ANSI ASCII encoding. Read more about it here. But 1252 is the codepage you want to use if you want EVERY SINGLE binary value from 0-255 to map to the exact same unicode character when you read the file into a string.
Long story short, if you’re used to looking at binary files via a hex editor, and you want to manipulate those files in VB, you have two choices.
Read the file into a BYTE() array as raw data, then operate on the bytes directly.
Read the data into a string, but be SURE to use the proper encoder, like so:
Dim Buf asString= My.Computer.FileSystem.ReadAllText(MyFileName, System.Text.Encoding.GetEncoding(1252))
Option 1 is great if you need to work on the data as, more or less, strictly byte type info. But it’s a real pain if much of the data is string type stuff.
Option 2 is MUCH easier to work with for mostly string data (you can use INSTR, MID, LEFT, RIGHT, cutting and chopping much more easily than with byte arrays), BUT you have to have read the data in via the right encoder or it will be “altered” during the loading process and won’t contain the same bytes that were actually in the source file.
Doing this won’t work:
Dim Buf asString= My.Computer.FileSystem.ReadAllText(MyFileName)
Because the ReadAllText routine uses, as its default, the UTF8 encoder.
Hopefully, putting all this down in writing now will keep me from forgetting about it the next time I’m mucking with funky file formats!
A while back, I wrote up a short post about calling C function pointer style interfaces with VB.net. When I started into that, what I was really wanting was a pure VB.net solution for calling the amBX API interfaces. There was already a library out to do the job, but that would have required including a separate dll, and, well, it’s not VB! It was C++ and C#.
But what’s amBX? Well, the basic system looks like this:
That box in the middle is actually called a “wall washer.” It contains several banks of lights that shine up onto the wall behind the unit. The speaking looking things are exactly that: LED lights that look similar to speakers.
You can also get an extension pack:
which consists of 2 fans, and a rumble bar (basically just a bar with two vibration motors in it).
In action, it looks like this:
Anyway, flash forward to now and my VB version of that library is ready for an initial release.
I call it amBXLibrary (catchy eh?)<g>.
amBXLibrary
The whole thing is in one ambx.vb source code file, and there’s no external references necessary, so, literally all you do is drop that source file into your project and you’ve got support for the amBX device set. Yes, it could have been broken up into a separate file for each class, and, normally, that’s how I approach things. But in this case, knowing I was planning on making this code publicly available like this, I opted for the simplest distribution form possible. If you must have it split up, feel free!
One more important note: You will need to have the ambxrt.dll file somewhere accessible, either in the same folder as your application or on the path. This file is installed with the amBX system, but won’t normally be on the path.
General Usage
All the internal classes are part of the amBXLibrary namespace. You’ll need to add an imports for that or set it in the project properties (or remove the Namespace line from the source) before you can use it.
The root class is amBX. To start up amBX and connect to its drivers, you’ll need to call the Connect method:
amBX.Connect(1, 0, "My Application", "1.0.0")
The first 1 and 0 are the required major and minor version of the amBX library. Since there’s only a version 1.0, that’s the only possible value to those arguments at this point. The next args are the string name of your application and a string version of the version number of your application. As far as I can tell, these values can be anything you want.
When you’re finished with amBX, be sure to call the Disconnect method (although this is not strictly necessary).
amBX.Disconnect()
The amBX class is a static class, which means you can’t instantiate instances of it. All its methods are Shared. It exposes several collections, into which you can add new amBX objects. Those include:
amBX.Lights
amBX.Fans
amBX.Rumbles
amBX.Events
amBX.Movies
Create a new amBX light like this:
Dim LeftLight = amBX.Lights.Add("Left", amBXLibrary.Locations.East, amBXLibrary.Heights.AnyHeight)
Technically speaking, since the newly created light is automatically added to the Lights collection, you don’t absolutely need to keep a local reference to it. But doing so makes it easier to work with your objects. Further, generally speaking, in amBX applications, you create the objects you need up front, when your application loads, and you use them until the application quits, so it tends to be more convenient to keep local references.
The first argument, the name, is simply an arbitrary descriptor of the object you’re creating. That is not part of the underlying amBX library. I added it more for convenience than anything. That said, you don’t have to give names to your objects. You can also create a light like this:
Dim RightLight = New amBX.Light(amBXLibrary.Locations.West, amBXLibrary.Heights.AnyHeight)
Notice, no name, and I just created the object directly with New. Note however that the new Light object is still added to the Lights collection and you can still reference it my index or via an enumeration.
Threading
amBX has several different options for multi-threading. This library only uses one. When you initially Connect, a background thread is started internally which calls the amBX.Update method 20 times per second. You can alter the timing however you want, and even turn it off by settings UpdatesPerSecond to 0. If you do turn it off, you’ll be responsible for calling the amBX.Update method periodically yourself.
Changing Objects
All the amBX objects are fairly simple. The lights have a Color; the fans, an Intensity; and the rumbles, an Intensity, Waveform, and speed. You simply set the properties however you want. The next time the amBX.Update method is called, the changes will be sent to the amBX drivers and the hardware updated.
Events and Movies
Event and Movie objects operate a little differently from the other amBX objects. Events are usually for short event sequences that can’t be stopped or paused. Movies are for longer sequences where you may want to stop or pause the sequence arbitrarily.
With Events, you generally create them during your application Load process and then simply call the Play method anytime you need the event sequence to play out.
Since Movies tend to be much longer sequences, you’ll generally create them right before you need to play them, Play the movie, and then dispose of the object.
Disposing of Objects
All of the effects objects in the library implement the IDisposable interface, so it’s usually best to explicitly Dispose the object once you’re done with it. Usually, this is easiest to do by using a Using Block:
Using Evt = New amBX.Event("c:\SomeEffectFile.ambx_bn")
Evt.Play()
End Using
Enabling and Disabling
You’ll notice that several of the Enable/Enabled/Disable members are seemingly arbitraryly properties or methods. For instance, Light.Enabled is a boolean property, whereas Fan.Enable is a method, and fan.Enabled is a readonly property. This is because the different objects deal with enabling in different ways. This is the nature of the amBX drivers, not an arbitrary decision of mine!
In the case of a fan, the Enabled property will return which state the can is currently in, which can include Enabled, Disabled, Enabling or Disabling. However, in the case of a Light, the light can only be Enabled or Disabled.
Also note that although the amBX documentation indicates that disabling an object will “cause it to retract itself from the user experience”, I’ve found that doesn’t appear to be the case. When I Disable objects, it simply means that they no longer react to any property changes to the them.
For instance, if you set a fan intensity to .5, then disable the fan, it continues to spin. But, setting the intensity to 0 will no longer alter the fan speed. You must Enable the fan again in order to change its intensity again.
Rumble objects are particularly troublesome in this respect, since there doesn’t appear to be a way to turn one off once it’s turned on. I’ve found that releasing the object and then recreating it will turn the rumbler off, but that causes more problems. So, as currently implemented, you MAY have problems with rumble effects (at least, problems getting them to stop!).
From what I’ve seen online, this is described as a known bug in the amBX drivers, but I’m not completely sure.
The Tricky Parts
I’ve really already covered (in the post referenced at this top of this post) what was the most difficult aspect of getting this library operational. All of the amBX interfaces are C style function pointer structures. These are easy to deal with in C, but more difficult to work with in VB.net. However, pleasantly, they are, in fact completely interoperable with VB.net (or C# for that matter). You just have to use the right combination of IntPtr, and Marshalling to convert those C function pointers to .net Delegates that canbe called from managed code with no problems at all.
If you want to know more, check out my previous post here, and pay special attention to the Generate methods in the source code. For instance, here’s the one for the Event object:
It uses the GetDelegateForFunctionPointer method of the Marshal class to create the necessary delegate from the function pointers passed back by amBX. Then, it’s just a matter of retrieving those function pointers correctly, managing the whole mess efficiently and calling the delegates when you need them.
The Caveats
This is a very “in flux” library at this point. In reality, I don’t even have any amBX hardware to test on. Everything I’ve done so far has been using the Virtual amBX Test Device, a program that simulates all the various amBX hardware that’s out there at this point.
I’ve implemented all the various methods and properties of the C amBX interfaces, but I have not tested them all out at this point (specifically, the RunThread and StopThread methods of the amBX object).
I also have noticed the above mentioned problem with disabling rumble devices (it just doesn’t seem to work right), and the only workaround seems to completely kill the rumbler, such that you can’t turn it back on at all without disconnecting from amBX and reconnecting.
However, I’ve noticed similar issues when working with the “amBX Test Tool” that comes with the amBX SDK, so I’m not completely sure whether it’s something in my code, or with the drivers themselves.
That said, the lights, fans, events, and movies all seem to work spot on, and I’m not much concerned about the rumble effects myself anyway<g>.
My Intentions…
I’ve got some very specific things I intend to use amBX for, but mainly, once I find a set for a reasonable price, I’ll be disassembling it and mounting the various pieces in a purpose built cabinet. My hope is to use all the elements (lights, fans and rumblers) but I’m not completely sure that’ll make sense.
I’m also working on a general purpose “screen color tracker” that will drive the amBX lights based on whatever happens to be onscreen, not use amBX enabled DirectX windows. But that’s a whole other project in and of itself!
The Code
I’ve made this project Open Source via CodePlex. Check out the project page at ambx.codeplex.com.
Alternately, you can download the test project (with the ambx.vb library source) here.
And Finally…
Let me know what you think! Have suggestions? Ways to improve the library? Things you would have done differently? Fixes?
By all means let me know. I’ll do what I can to get improvements and suggestions implemented.
I’ve been playing recently with the amBX ambient effects library and devices.
Essentially, it’s an effects platform that allows you to easily create light, wind and rumble effects to coordinate with what’s going on on-screen, in a game, in media players, or what-have-you. The lighting effects are nothing short of fantastic. The rumble and fan effects…. meh. They’re interesting, but I’m not sure where that’ll go.
Regardless, the API for amBX is all C style objects, which means essentially an array of function pointers to the methods of the object; not really an API that VB.net likes to consume. Be sure to grab the amBX developer kit here. You’ll also need to core amBX software available from the main website. Finally, play around with the “Virtual amBX Test Tool”. It’ll allow you to experiment with all the amBX features and functions without having to buy anything at all. Of course, eventually you’ll want to get at least the starter kit, with the lights. But it’s not necessary to begin experimenting.
Hats off to Philips for making this possible!
Here’s an example of the structure definition from the amBX.h header file that comes with the API (I’ve clipped out comments and other bits):
As you can see, each element in the structure is made up of a pointer to a function. The various functions all take, as their first argument, a pointer back to this structure. Under the covers, this is how virtually all object oriented languages function, it’s just the the compiler usually hides all this nasty plumbing so you don’t have to deal with it regularly.
But this presents a problem. VB.net is “managed” code, and as such, it likes things to be wrapped up in nice “managed” bits. There are plenty of good reasons for this, but these function pointers are decidedly not nice and tidy managed bits! So, what to do?
One solution is the approach Robert Grazz took here. It’s a solid solution, no doubt, and I learned a lot from looking at his approach, but, this being VBFengShui, I wanted that same functionality in native VB.net with no additional dll files hanging around!
Delegates to the Rescue!
A delegate in .net is essentially a managed function pointer. In VB.net, they’re most commonly used to handle events, but you wouldn’t really know it, because VB’s compiler does a good job of hiding all that plumbing from you. However, unlike VB6 and earlier, in VB.net, all the plumbing can, if you’re willing, be “brought into the daylight” and used however you want.
There are many, many discussions about delegates for VB.net out on the web. A really good Introduction to the concepts by Malli_S is on codeproject, so I won’t rehash the basics here.
The problem is, delegates in VB.net tend to be generated by using the AddressOf operator, and I already had the function addresses (they’re in that C structure). I just needed to get a delegate that would allow me to call it.
Some Googling turned up several good posts that provided pointers, including this one on StackOverflow. But it wasn’t exactly all the steps necessary.
Getting the C Structure
The first step toward getting something working with amBX is to retrieve the main amBX object. You do this through a standard Windows DLL call.
<DllImport("ambxrt.dll", ExactSpelling:=True, CharSet:=CharSet.Auto)> _
PublicSharedFunction amBXCreateInterface(ByRef IamBXPtr As IntPtr, ByVal Major As UInt32, ByVal Minor As UInt32, ByVal AppName AsString, ByVal AppVer AsString, ByVal Memptr AsInteger, ByVal UsingThreads AsBoolean) AsIntegerEnd Function
Assuming you have the ambxrt.dll file somewhere on your path or in the current dir, the call will succeed, but what exactly does that mean?
Essentially, you pass in the Major and minor version numbers of the Version of amBX you need, and two strings indicating the name of your app and the version of your app. No problems with any of that. But that &EngineHandle is the trick.
It means that this call will return a 4 byte pointer to a block of memory that contains the amBX object structure, which is, itself, an array of 4 byte function pointers to the various methods of the amBX object. Therefore, in VB.net, that argument is declared ByRef IamBXPtr as IntPtr.
Now, we need a place to store that “structure of pointers.” Going through the ambx.h file, I ended up with a structure that looks like this:
<StructLayout(LayoutKind.Sequential)> _
PrivateStructure IamBX
Public ReleasePtr As IntPtr
Public CreateLightPtr As IntPtr
Public CreateFanPtr As IntPtr
Public CreateRumblePtr As IntPtr
Public CreateMoviePtr As IntPtr
Public CreateEventPtr As IntPtr
Public SetAllEnabledPtr As IntPtr
Public UpdatePtr As IntPtr
Public GetVersionInfoPtr As IntPtr
Public RunThreadPtr As IntPtr
Public StopThreadPtr As IntPtr
EndStructure
That StructLayout attribute is particularly important. It tells the compiler that the structure should be layed out in memory JUST AS it’s declared in source code. Otherwise, the compiler could possibly rearrange elements on us. With Managed Code, that’d be no problem, but when working with unmanaged C functions, that would not be a good thing!
And finally, we need a way to retrieve that function pointer array and move it into the structure above, so that it’s easier to work with from VB.net. That’s where the System.Runtime.InteropServices.Marshal.PtrToStructure function comes in. This routine will copy a block of unmanaged memory directly into a managed structure.
Private _IamBX As IamBX
Private _IamBXPtr As IntPtr
...
amBXCreateInterface(_IamBXPtr, 1, 0, “MyAppName”, “1.0”, 0, False)
_IamBX = Marshal.PtrToStructure(_IamBXPtr, GetType(IamBX))
And presto, you should now have the _IamBX structure filled in with the function pointers of all the methods of this C “Object”.
Calling the C Function Pointer
At this point, we’ve got everything necessary to call the function pointer. Take the CreateLight function. The C prototype for this function is shown at the top of this page. As arguments, it takes a pointer back to the amBX structure, 2 32bit integers describing the location and height of the light source, and it returns are pointer to another structure, in this case, an amBX_Light structure, which contains another set of function pointers, just like the amBX structure described above.
First, we need to declare a delegate that matches the calling signature of the CreateLight C function we’ll be calling:
<UnmanagedFunctionPointer(CallingConvention.Cdecl)> _
PrivateDelegateFunction CreateLightDelegate(
ByVal IamBXPtr As IntPtr, _
ByVal Location As Locations, _
ByVal Height As Heights, _
ByRef IamBXLightPtr As IntPtr) As amBX_RESULT
The key points here are:
Make sure you have the CallingConvention attribute set right. Most C libraries will be CDECL, but some libraries are STDCALL.
Make sure your parameter types match, especially in their sizes. Not doing this will lead to corrupted called or system crashes.
Now, create a variable for the delegate and use the Marshal.GetDelegateFromFunctionPointer function to create a new instance of the delegate, based on the applicable function pointer. Since the function pointer for the CreateLight function is stored in the CreateLightPtr field of the _IamBX structure, we pass that in as the pointer argument.
IMPORTANT NOTE: There are 2 overloads for the Marshal.GetDelegateFromFunctionPointer function. Be sure to use the one what requires a second argument of the TYPE of the delegate to create. Using the other one will fail at runtime because it won’t be able to dynamically determine the type of delegate to create.
Next, call the function via the delegate, just as if it was a function itself.
Finally, use Marshal.PtrToStructure again to copy the array of function pointers returned into another structure, this one formatted to contain the function pointers of the Light object that just got created.
Dim d As CreateLightDelegate = Marshal.GetDelegateForFunctionPointer(_IamBX.CreateLightPtr, GetType(CreateLightDelegate))
Dim r = d(_IamBXPtr, Location, Height, _IamBXLightPtr)
_IamBXLight = Marshal.PtrToStructure(_IamBXLightPtr, GetType(IamBX_Light))
The amBX library is much, much larger than just this little bit I’ve shown here. Once I get the entire thing coded up, I’ll be presenting it here as well.
But in the meantime, if you have a need to interface with C style function pointer objects, don’t let anyone tell you it can’t be done in VB.net
Final Note
As you may or may not have guessed, amBX is a 32bit library and its interfaces, functions and arguments all live in 32bit land. If you’re working with VS2008, BE SURE to set your project to the x86 platform, and NOT “Any CPU” or “x64”! The default, for whatever reason is “Any CPU”, which means things will work properly when run under 32bit windows, but things won’t work well at all if run under a 64bit OS.
At one point a while back (while I was still working for my previous company), we had “support for x64” handled down as a request for an interim version.
I thought, “No sweat”. Grab a few alternative prerequisite MSIs, determine the bitness of the processor you’re on in the install (InstallShield has the functionality built in), and just fire off the appropriate preqs. Our actual application is 32bit, and we had no intention or need to compile as 64bit.
Well. A few clicks later and the install was built and deployed to a test machine.
Crash.
Initial sample database didn’t get deployed properly.
Ok, why not. Well. Long story short, the application couldn’t find a registry key setting. But I hadn’t changed anything about the registry.
Lots of digging through verbose install logs later, and I discovered that the installer was, on a 64bit OS creating registry keys under the key HKEY_CURRENT_USER\Software\Wow6432Node whereas when my application when to get the registry key, it was looking in the normal HKEY_CURRENT_USER\Software\ key.
Huh?!
First, what the hell was this Wow6332Node? Obviously it’s some kind of “compatibility” bit for running 32bit apps under a 64bit OS. And that’s exactly what it is.
Ok, my Installer is InstallShield, and it’s a 32bit app (even if it can install 64bit packages), so that explains why it was writing the key to that Wow6432Node, but why wasn’t my application reading it from there?
A little digging later, and I found this option buried in the “Advanced Compiler Settings” panel of the “Compile Options” tab of the Project’s properties.
See that AnyCPU setting? When set to AnyCPU, the JIT compiler will dynamically compile your .net dll as either a 32bit or 64bit dll, depending on the process that loaded it.
Obviously, on a 32bit OS, all the processes will be 32bit, so everything’s 32bit.
BUT, on a 64bit OS, things get nastier.
If the DLL runs under a native code 32bit process, it’ll get compiled as 32bit x86 code and run under the Wow32 (Windows On Windows) layer. This explains why the DLL that acts as an AddIn to Word worked just fine. Word is a native 32bit process, so the DLL was ending up executing as 32bit too.
However, the little utility app that creates our initial database was a .net application, and it was set to AnyCPU.
That meant that when IT loaded our DLL, the DLL ended up JITed to 64bit code (because the utility app was set to AnyCPU, so it was JITed to 64bit code because it was a base process and was running under a 64bit OS.
Case closed.
Short version of the story. Since tracing all this down, I’ve learned that for VS2008, the .net team decided to change the default of that option from x86 to AnyCPU, and then, in general, that has been regarded as a bad thing to have done.
The default will be changed BACK to x86 in 2010 apparently, but until then, unless you REALLY need to compile code for AnyCPU, be sure to switch this option back to x86!