Sunday, June 01, 2008

image A recently posted article on CodeProject had this to say about Visual Studio's support for multi-lingual applications:

Conclusion: Visual Studio .NET does not offer any multilanguage support which is worth thinking about it.
You will only waste your time with Microsoft's approach!

Check the whole article/project out here.

I can't verify the author's credentials that would justify such a claim, but from the sound of it, he did at least start down the MS sanctioned path, and I have to agree with him.

Way back in the dark ages, pre-internet, around 1990 or so, I was managing development of a CRM system (customer relationship management software).

We'd picked up some resellers oversees and needed to get the product internationalized. This was really at the very beginning of Windows even (Win 3.1 and WFW, if you remember those!). Our internationalization efforts had to apply equally well to our DOS app and our Windows version.

Several of the developers and myself went to a conference, the precursor to VBits, (I don't remember exactly what it was called back then) and I got a chance to talk with one of the MS internationalization engineers directly.

I'd played with the whole "separate resources for each language" technique and found it workable, but so labor intensive, that I couldn't imagine anyone but the largest shops actually doing it that way.

The MS guy verified that suspicion. He said (and I'm paraphrasing), "The core team finishes up the project, and ships it, and then the whole project base is 'thrown over the wall' and each internationalization team then takes over and internationalized the project into their respective languages, re-tests, etc."

Ouch.

Now, I'm sure times have changed at MS, but if the comment from Elmue in the article on CodeProject is any indication, they haven't changed that much.

The internationalization functions I helped build way back then was based on 3 simple concepts:

  1. The original english strings had to remain in the code
  2. Those strings has to actually be used by the code
  3. Those strings had to be easily searchable (say, by a GREP or similar utility) for extraction and translation

Why those first two points?

Because if the english strings remain in the code, the code remained relatively easy to debug and maintain for the programmers.

Further, if the strings are actually used by the code, that meant that during dev and alpha/beta testing, we wouldn't have any disconnect issues with resources not matching what was needed in the code itself. This is akin to the age old concept of eating your own dog food. The idea of socking strings away in resources and just having a comment in the code as to what the string contained just scared the hell out of me.

Also, it also meant that if the translatable resources are lost for whatever reason, the program would still be able to run based on the "compiled in" English strings. Not ideal, but better than simply throwing errors or displaying blanks.

We accomplished all these goals by embedding all translatable strings (including those in dialogs, etc), into a function call. Something like:

MyString$ = TX$("This is the english version", stringcode, groupcode)

Where stringcode and groupcode were optional arguments that indicated, basically, the resource ID of the string and an arbitrary group ID of the string.

Originally, when you were writing or working on code, you'd never even bother entering the stringcode or groupcode args, so your call would look like:

MyString$ = TX$("This is the english version")

But, because it was trivially easy to scan for TX$(), when our scanner was run on the code, it could

  1. Extract the strings
  2. Give then ID's
  3. Rewrite the source code with the appropriate string and group codes as necessary.
  4. Generate a "translator's file" that contained the string, ID's and potentially developer comments that would indicate the context of the string and intention (for use by translators to assist with the translation).

Nowadays, with OO, extension methods, reflection, and all the other .NET goodies, seems like this whole process could be vastly more efficient than even what we did back then.

But in the end, we translated the product into 8 or more languages in just a few months with this technique, using no additional developers, and a few native speaker volunteer translators. And it didn't require any code rewrites and was just as efficient to debug as if you'd left the strings in the code and did nothing about translation at all.

Now, granted, there's a lot more to internationalizing an app that translating text. You have to worry about icons, input method (when applicable), date, time, and number formats, and even subtle things like color choices, but it was a huge timesaver for an otherwise arduous task.

Closing comment: Why TX$() you might ask\:\-\) ? Basically, it was because we didn't want a huge function name taking up tons of space in the code when it would be used as often as it was. That's all. As I recall, it is about the only two letter function I've ever authored in code. I was never a big fan of the BASICA 2 letter name restriction!

Any translation war stories out there? How have you translated applications?

posted on Sunday, June 01, 2008 12:04:15 PM (Central Standard Time, UTC-06:00)   •  # •  Comments [0] • 
Kick it •  Add to del.icio.us •  View blog reactions; 
 Thursday, May 29, 2008

I'm a space buff so I've been following the Phoenix mission for a while now. It finally arrived at Mars and successfully landed 5 days ago (Sunday).

That in and of itself is incredible, considering some of the recent failures NASA has had. This landing went picture perfect, though.

But what's even more amazing is that the Mars Orbiter, already in orbit around Mars, was able to snap a picture of Phoenix as it was parachuting to the surface!. Just contemplating the enormous number of tasks that had to be done just right to pull that off can make your head spin.

Here's the photo (from Space.com and NASA):

image

Granted, it's not a frame from "The Right Stuff" but still, you gotta give the guys at NASA props for pulling this off.

Here's hoping Phoenix performs even half as well as the other two scamps, Spirit and Opportunity, that have been scurrying around farther south for more than 4 years now.

posted on Thursday, May 29, 2008 9:06:24 PM (Central Standard Time, UTC-06:00)   •  # •  Comments [0] • 
Kick it •  Add to del.icio.us •  View blog reactions; 
 Tuesday, May 27, 2008

I'm constantly amazed by some of the goofy things that application installations do to my system without even giving me the option.

Obviously, there's the whole spyware/adware problem, but that's just blatant.

There's an entirely other, more subtle, level to the problem, where applications do little things that are difficult to undo, are annoying and that you aren't given the option of not "doing" in the first place.

Here's one example. Now, I like the CodeJock products, and I'm not really picking on them here, it's just that they give me a good example:

image

This is the "Right Click - New" menu that you get from Explorer when you right click in a folder and select "New".

Now, I'm a pretty busy developer, and I like a nice shortcut just as much as the next guy, but honestly, I can't imagine EVER creating so many "Command Bars Designer Documents" that I'd want an entry for it on my New Items menu like this. As you can see, there's already enough junk on this menu. I wonder how many people out there create a new contact, bitmap image or briefcase so regularly that it belongs on this menu. Hell, I've always created new Word docs by opening Word, starting my document and then "File-Save"-ing them wherever, never by the "New Office Word Document" menu, but then, maybe that's just me.

A few others:

  • Unrequested shortcuts on the QuickLaunch space or the Desktop.
  • Unrequested Tray Icons.
  • Adding Vista Sidebar gadgets without permission.
  • Stealing file extension associations without asking and not restoring them on uninstall.

Any other bits of application rudeness out there anyone has run into?

posted on Tuesday, May 27, 2008 8:21:51 PM (Central Standard Time, UTC-06:00)   •  # •  Comments [0] • 
Kick it •  Add to del.icio.us •  View blog reactions; 
 Sunday, May 04, 2008

One thing I've begun to notice about VB.NET is that there are a pile of options as to how you might implement a particular piece of functionality. I'm still not convinced that this is necessarily a good thing, but it is hard to argue with having options.

For instance, I recently needed a function to retrieve the number of elements defined in an enumeration. Now, I realize that enumerations can be sparse, and that creating an array to be indexed by the enumeration values will not work with all enumerations, but that's a topic for another post.

The first option I thought of was to implement it as a plain ol' function in a module:

Debug.print GetEnumItemCount(GetType(MyEnum))

    Public Function GetEnumItemCount(ByVal typ As Type) As Integer
        If typ.IsEnum Then
            Return System.Enum.GetNames(typ).Length
        Else
            Return 0
        End If
    End Function

Not terribly interesting and it's perfectly functional, but it just doesn't seem particularly .NET-ish. Also, I don't really care for the "double function" approach (ie calling GetType() to retrieve the enum type descriptor object to pass into the function).

So, any way to improve this?

One way is to get rid of the extra GetType function call. Unfortunately, the only way I've seen to do this so far is to pass in one of the enumeration values instead of the enumeration itself (or its type).

Debug.print GetEnumSiblingCount(MyEnum.Value1)

    Public Function GetEnumSiblingCount(ByVal e As [Enum]) As Integer
        Return System.Enum.GetNames(e.GetType).Length
    End Function

I've renamed the function GetEnumSiblingCount because that's exactly what this new function does, return the total number of siblings to the passed in enumeration element.

Unfortunately, this approach still seems a little a lot odd.

My next thought was, why not add an extension method to the enumeration itself, so you could do something like:

Debug.print MyEnum.Count

Unfortunately, since MyEnum is a type and not considered an actual object in any sense, you can't invoke a method on it, or even define one on it for that matter.

You can, however, get close, by defining an extension method on the Type object itself:

Debug.Print GetType(MyEnum).Count

Module EnumExtender
    <extension ()> _
    Public Function Count(ByVal typ As Type) As Integer
        If typ.IsEnum Then
            Return System.Enum.GetNames(typ).Length
        Else
            Return 0
        End If
    End Function
End Module

The downside here is that we're extending not just enum types but all types, and extending anything other than enums with this function doesn't make much sense.

Interestingly, there's yet another option; extend the enum elements themselves.

Debug.print MyEnum.Value1.SiblingCount

Module EnumExtender
    <extension ()> _
    Public Function SiblingCount(ByVal anEnum As [Enum]) As Integer
        Return System.Enum.GetNames(anEnum.GetType).Count
    End Function
End Module

However, this also has the same problem as a previous try in that it just doesn't feel right to ask an enumeration element for some property of it's containing type.

Ok, how about just changing up the definition of the enumeration itself:

    Public Enum MyEnum
        Value1 = 0
        Value2 = 1
        Value3 = 2
        Count = 3
    End enum

Short, sweet, no compiler tricks or reflection.

But. It doesn't work with existing, already defined Enumerations, and since the Count is part of the enumeration, the actual count of the enum is 4, not 3, which would seem to be confusing in the long run.

I'm sure there's many more possibilities as well, and, hopefully, I've missed a better one and someone out there will let me know it!

Long story short, my first inclination, the plain ol' function-in-a-module GetEnumItemCount(), seems the best solution for this particular piece of functionality.

In reality, the best solution for most of these sorts of generic, application-wide, stateless utility functions seems to be the plain ol' function-in-a-module.

posted on Sunday, May 04, 2008 7:35:22 PM (Central Standard Time, UTC-06:00)   •  # •  Comments [0] • 
Kick it •  Add to del.icio.us •  View blog reactions; 
 Thursday, April 24, 2008

For today's blog entry, I'm going to delve into a bit of Word esoterica that, if you're into this sort of thing, you might find fascinating, and if you're not, well, you have been warned\:\-\) .

If a document is changed in Word, you'd expect to get a prompt when you close the document, wouldn't you?

Well, that's apparently just expecting too much, my friend.

And if you were to load a document programmatically and simply iterate through it, without changing anything, you'd expect that the document wouldn't actually change, wouldn't you?

Well, that's also… oh, you get the picture.

It appears to be possible in Word, without programming, to create a header that doesn't actually exist, or rather, that exists but is completely empty.

If you then iterate through the headers of the document, retrieving the Range of each, the act of simply "retrieving" the range, not actually modifying it, will cause the header to be modified, inserting a paragraph mark where there was none before.

This can effectively shift the entire document down, since, in the original document there was no "paragraph" in the header, but now there is.

Even more interesting, this modification isn't logged internally by Word, so if you were to close the document, you get no prompt that a change has been made, and from what I can tell, the change isn't actually saved, although it's very visible on-screen.

You can test this by creating a document with a null header, then, adding this VBA code and running it:

Sub Test()
   Dim sct As Section
   Dim r As Range
   Dim hdr As HeaderFooter
   
   For Each sct In Me.Sections
      For Each hdr In sct.Headers
         Set r = hdr.Range
      Next
   Next
End Sub

How do you create a document with a null header? To be perfectly honest, neither I nor a Microsoft Tech could figure that one out. Please let me know if you can.

But what I did discover was that the situation yielded something you could detect.

Essentially, the problem is this:

Each SECTION object in Word has a PAGESETUP object associated with it.

And each PAGESETUP object has an associated HEADERDISTANCE and TOPMARGIN property. It also has FOOTERDISTANCE and BOTTOMMARGIN properties, but I'll only deal with headers here. Footers are handled in exactly the same way.

In a document's section, if the HEADERDISTANCE is ever >= the TOPMARGIN, any changes in the size of the header have the potential for "spilling out" over the limit of the margin and pushing the content of the section body down.

I say potential, because if the header doesn't contain anything, or if what it contains is not enough to push the bottom of the header past the TOPMARGIN, everything will stay put and all will be well.

In any case, right before you save the document, you simply need to iterate through all the headers in all the sections of the document and test if they're empty. If they are, you force the HeaderDistance back to half of the TopMargin.

Section.PageSetup.HeaderDistance = Section.PageSetup.TopMargin / 2

Why half? It essentially doesn't matter what it is, as long as it's less than the TopMargin.

But what makes up an "empty" header?

Once you get the Range of the header

set Range = Header.Range

Make sure it's not a linked header

If Range.LinkToPrevious then {it's linked so don't bother with it}

and then check the character and word counts

If Range.Characters.Count > 1 and Range.Words.Count > 0 and Range.Characters.Count > Range.Words.Count Then
   {this is NOT an empty header}
End If

If it's empty, adjust the HeaderDistance as noted above.

Fun stuff.

posted on Thursday, April 24, 2008 9:43:19 PM (Central Standard Time, UTC-06:00)   •  # •  Comments [0] • 
Kick it •  Add to del.icio.us •  View blog reactions; 

I talked about fixed the IsNullOrEmpty method on the string object here.

But another aspect of the string object that has irritated me for a while is the Length property.

Obviously, the Length property returns the length of the current instance of a string variable.

However, because it's an instance property, it can only be invoked against variables that have actually already been initialized to some value.

If you try and run this code, you'll get an Object Not Set error when you execute the second line:

dim s as string
debug.print s.Length

Now, anybody that's coded in VB intuitively knows that an unitialized string has a length of 0, and such a request should certainly  not result in throwing an error. It doesn't matter one whit whether this is technically incorrect or not. I my book, those issues are for the compiler to work out, NOT the programmer.

Since the string class is an intrinsic type and we really can't inherit from it or extend it in any traditional way, is there any way to correct this gaff?

With VB 2008, there is.

I blogged a bit about extension methods in the previous mentioned post and that's what I'll use to fix the Length property as well.

Unfortunately, as I also mentioned in my previous post, Length is a property  and not a method, so you can't create an extension method for it directly, which is a shame.

However, long time VB programmers don't think to use "Length" anyway, you always retrieved the length of  string via LEN(stringname), and LEN is not a member of the string object.

So, just add the following to a module to your application:

Imports System.Runtime.CompilerServices

Module StringExtensions

    ''' <summary>
    ''' Returns length of the string or 0 if the string is empty or has never been assigned (is nothing)
    ''' </summary>
    ''' <param name="aString"></param>
    ''' <returns></returns>
    <Extension()> _
    Public Function Len(ByVal aString As String) As Integer
        Return Microsoft.VisualBasic.String.Len(aString)
    End Function
End Module

and you'll have a LEN function available directly from any string variable that will work regardless of whether the variable is initialized or not.

Also, note the <summary> comment. This will be displayed by Intellisense in the IDE, which can be quite handy.

image

And finally, you can't use Return Len(aString) in the function, because doing so would be treated as a recursive call to the Len Extension method itself! Thus, I fully qualify the call to the Microsoft.VisualBasic.Strings LEN function.

All of this begets the obvious next question, "Why not just use Len(stringvar)"?

To which, I have no good answer.

Maybe it's just a .NET thing\:\-\)

posted on Thursday, April 24, 2008 7:04:59 AM (Central Standard Time, UTC-06:00)   •  # •  Comments [0] • 
Kick it •  Add to del.icio.us •  View blog reactions; 
 Tuesday, April 22, 2008

First, I should say I'm a pretty ardent supporter of VB, even VB.net. Heck, that should be obvious from the name of this blog\:\-\)

But, as I'm getting more and more into the ".net" of VB, I'm finding more and more things to dislike about it.

There's plenty already written on this subject and maybe I'm a tad late to the party, but I haven't seen these particular things mentioned before, so here's my additions to the topic.

  • Sluggish IDE. Good god. I've got a dual core Core2 rig with ultrafast video and 4gb of ram, and the VS2008 IDE feels like trudging through a Valdez-soaked beach compared to working with VB6. I understand this isn't specifically a VB problem. The IDE applies to all .NET languages. But that doesn't change the fact that if you program in VB, you use the IDE, and it's a dog.
  • Intellisense. I like Intellisense, the concept, but there's just something about the VB.NET implementation that is so much more "in the way" that VB6's. It feels a bit like that brother-in-law that you can't get off the couch soon enough...
  • MDI style IDE. While there are definitely some improvements to the UI of the IDE, and just maybe I'm a relic, but I MUCH prefer the VB6 (and before) "SDI" floating windows style IDE to this monolithic, MDI style environment. To make good use of it, you have to maximize it on-screen, which hides everything else onscreen. Granted, a nice dual or triple monitor setup minimizes that impact, but still, I just find it much more cumbersome and "in the way" than the old environment.
  • Uninitialized strings variables. This one just floors me. Take this code:
        dim s as string
        debug.print len(s)
        debug.print s.length
    The len(s) line works like you'd expect (you get 0), but the s.length line fails with an error! What the hell? An uninitialized object? This is like a car manufacturer coming out with the new year model and swapping the gas and brake pedals. I don't care whether they told me about it before or not, it's just plain stupid. Why on earth have two functions that do exactly the same thing but one works as any longtime VB programmer would expect and one does not? And no, "That's just the CLR/it's necessary for C#/Blah Blah" just doesn't cut it.

As I read through many of the posts, I have to admit I've started to wonder myself as to whether this is all some low-level ultra-subtle manipulations on the part of MS to nudge people further and further away from VB. Yes, it starts to sound like conspiracy theory, but surely, all this couldn't be completely unintentional, could it?

Even so, there are ways around some of this. And I still much prefer VB, the language, to anything even resembling C and it's ilk.

Still, if no one complains, things won't change (or they'll get worse<sigh>).

posted on Tuesday, April 22, 2008 7:35:59 AM (Central Standard Time, UTC-06:00)   •  # •  Comments [0] • 
Kick it •  Add to del.icio.us •  View blog reactions; 
 Sunday, April 20, 2008

String manipulation is a very different beast in VB.NET than in VB6. In fact, I'd wager that it's the one area that trips up programmers new to VB.NET more than any other aspect of the language.

I've talked about some annoying aspects of System.String before, but this go round, I'm going to concentrate on shared methods.

Shared Methods are methods that are invoked via the class type itself, and not via an instance of the class.

For instance, with strings, you have a nicely named IsNullOrEmpty  function that you'd expect would return a true/false indicating whether the string in question is null or empty. Unfortunately, you'd be only half right.

Bring up the Object browser and find the String class, then highlight the IsNullOrEmpty method and you'll see this:

image

Notice the Shared keyword. That indicates that this method IS NOT an instance method, thus you can't invoke it via a specific instance of a string; rather, you must invoke it via the String class directly.

So, you can't do this:

Dim s as string
If s.IsNullOrEmpty Then

But you can do this:

Dim s as string
If String.IsNullOrEmpty(s) Then

Now, it makes perfect sense, from a purely technical perspective, that invoking an instance method on an uninitialized object wouldn't work anyway, so a method like IsNullOrEmpty wouldn't make sense to be an instance method, since attempting to invoke it via a variable that hadn't actually already been initialized would result in an "Object not Set" error.

However, this is VB, not some ivory tower exercise in theoretical language design. If I'm going to invoke a method like IsNullOrEmpty, I’m expect to be able to do so against an instance. Having to invoke it via the String class is just so utterly unintuitive, it defies all reason.

Oddly, the very argument that I note above in favor of using a shared method for IsNullOrEmpty is violated by another string property, Length. Here's a property that is definitely an instance property, but causes VB code to fail (with the Object Not Set error) when invoked on a variable that hasn't actually been set to value yet.

Is this just an arbitrary oversight, a design flaw, or an intentional "feature" of the language? I can't answer that.

But, realistically speaking, I can say that it's utterly frustrating to have elements of a language, such as these, behave so drastically different from one version to another. It doesn't matter that the syntax is different (x.Length vs Len(x), for instance), there is an expectation there that simply is no longer met and does nothing but confuse.

Fortunately, with VB 2008, there is a relatively trivial way to correct these problems, and likely a host of other similar issues.

It's called "extension methods".

To create an IsNullOrEmpty that works like any reasonable person would expect it too, just put this in a Utility module somewhere in your project:

Imports System.Runtime.CompilerServices

Module StringExtensions

    ''' <summary>
    ''' Returns True if the current string instance is nothing or a null string
    ''' </summary>
    ''' <param name="aString"></param>
    ''' <returns></returns>
    <extension ()> _
    Public Function IsNullOrEmpty(ByVal aString As String) As Boolean
        Return String.IsNullOrEmpty(aString)
    End Function
End Module

The Imports System.Runtime.CompilerServices is only used during compilation. You can actually continue to target the .NET runtime v2.0, even if you use this code (however, you still have to compile the code from VS2008, it won't work in VS2005).

You tag the new version of IsNullOrEmpty with the <extension()> attribute to  mark it as an extension method.

The first parameter of an extension method is required and is an argument of the type of class that you're extending, in this case the String class.

You can have additional arguments if necessary, but you don't need any for this method.

This trick takes advantage of the fact that even though the String class already has a method named IsNullOrEmpty, the function signature is not the same as this one (since ours has the implicit first argument). This is effectively an overload and it allows VB to know to call the new method if invoked against an instance, and the old one if invoked against the String class directly (which is exactly what's being done within the method itself!).

There are several other "shared" methods on the string class that can similarly be extended to more intuitive instance methods in this way, for instance:

  • Compare
  • Concat
  • Format
  • Join

Length could also be added to this list but you can't quite treat it the same, since it's a property, and the Extension attribute can't be applied to properties.

Finally, extension methods can be unbelievably useful for associating functionality with specific classes that you can't extend in any other way, but, as always, you need to be careful with how much you extend a  class.

For example, it might be tempting to extend the String class with all sorts of path/file/folder manipulation and parsing logic, so that you could do something like this:

dim s as string = "c:\myFolder"
debug.print s.DriveLetter

but doing so could quickly clutter the String object and Intellisense.

As usual with these sorts of techniques, use judiciously.

posted on Sunday, April 20, 2008 3:24:11 PM (Central Standard Time, UTC-06:00)   •  # •  Comments [0] • 
Kick it •  Add to del.icio.us •  View blog reactions;