December just flew by.
I last wrote at Thanksgiving. Here it is January, a new year, and I'm still missing Kami and wondering why she's not visible.
I'm sure she's ignoring me, wherever she is. I just miss watching her do it. I guess we'll just have to get on with it. And maybe I'll take the occasion to discuss a topic that has often plagued me in integration work, not just with regard to cats.
A question of empty-ness and absence
In working with Linda a couple of months ago, to figure out how to stop data from appearing when you don't want it, I found I needed to have just such a discussion: How does the evaluating code know when you want to do this? Usually it comes down to figuring out some sort of negative condition. When the users haven't provided parameter values yet, the code should realize this and not do any unnecessary work. Reporting Services has a default way to deal with this; you omit a default value for a parameter, and you don't allow nulls for this parameter. The user is required to fill in that value before the work continues. However, the default UI for this is not pleasing to many people. In this scenario, and in many integration scenarios that have nothing to do with reporting, you can be moving between environments that don't understand "null", "nil", "nothing", "blank", or even a straightforward boolean "false" exactly the same way. You have to find some way that the receiver can understand a boolean false, a blank, or an absence of viable content, as provided by the sender. |
Just now, I started to write that this is the most annoying type casting and massaging scenario I know about — but. Don't get me started on dates and date times. Let's stick to one annoyance at a time.
So…
What's an appropriate strategy?
Usually I find two things have to be considered if I want my code to work with multiple calling environments:
1- The receiving environment has to define the values it considers to be a boolean false and/or a boolean true for a string according to the senders it expects. And it has to define these generously, and widely, not according to its own ideas (so, no, the default behavior of Boolean.TryParse isn't good enough, thank you very much).
2 – The receiving environment has to figure out what it considers blank and what it will do with values that evaluate to null-or-empty. And here, as well, the receiving environment can publish its limitations and stick to its guns, but if it wants to have a usable and popular interface, it should bend over backwards to consider other people's ideas of proper input values, and offer some reasonable amount of choice (so, no, the default behavior of String.IsNullOrEmpty isn't good enough, either, thank you very much).
How do you carry it out?
Below is an example, in VB.Net, of how you can allow the caller to specify what "empty" content should look like, in various ways:
- Do spaces "count", are they significant to the value being evaluated? and, as a result of this,
- Does a string of spaces "count" as empty or not?
- Should "emptiness", by default, result in an assumed True or an assumed False?
You can also see that I've set up a relatively wide set of values that will evaluate as a Boolean True (including an Xbase version, .T.), as appropriate for the potential callers of this code.
I don't say that the code is perfect, or that it fits every purpose. I'm just saying that you really have to do this and that the concept can be "translated" into any environment.
If you don't like my particular way of organizing the logic in the VB code, I should probably add that I've chosen this "contains" strategy because it translates particularly well to XSLT, another environment in which I code frequently.
I'll add another example below, slightly different, but you'll see how the same ideas are preserved. I've tried to make this obvious by naming the constants at the top of the VB snippet the same way as I've named the parameters at the top of the XSLT snippet, in these examples.
In the case of the specific XSLT template I'm showing, I'm also rendering some HTML content in either a readonly or editing form for the calling application; that's why you see two branches that send back either a straight value or a checkbox with the current boolean result filled in. Not relevant to the current discussion, but you might find it useful. Also, I haven't handled the "trimming" issues because I know that my callers have already applied xslt's normalize-space to what they're sending. If I want to include a blank as a "true" value, I can do so by including a single blank in the strings evaluating to true ("| |").
From each according to its abilities, to each according to its needs. Which brings me to one more point…
Be gracious to your less-capable partner technologies
While I personally like to set the rule that the server should bend to benefit the caller, and provide the most flexibility possible, sometimes you don't have this option. Sometimes the caller has to do the work.
Here's what I mean: the code you see below does translate really well to multiple environments. That includes creating an expression, such as Code!EvaluateStringToBoolean(…), for use in an RDL.
But you don't always have the luxury of including the logic in that function at exactly the reporting-point where you need it most. Consider the scarcity of usable functions when you're writing code to create a calculated column in a Semantic Model:
The somewhat ridiculous expression you see above takes care of considering a Percentage value that may be null. Null, not 0; zero is no problem.
Believe it or not, casting the Percentage to a text value and then checking to see if its length is 0 is the best way I've found of saying the equivallent of IS NULL, IsBlank(), IsNothing, ==null, nil, or what-have-you, in the limited syntax allowed.
If you have a better idea, let me know. Meanwhile, I'll use what I can, whatever it is.
And now for those examples…
Const TheseStringsEvaluateToTrue = _
"|YES|TRUE|ON|1|.T.|Y|RIGHT|CORRECT|YESSIREEBOB|"
Const TrueEvaluationsDelimiter = "|"
Public Shared Function EvalStringToBoolean( _
ByVal tSource As String, _
Optional ByVal tDefaultTarget As Boolean = False, _
Optional ByVal tTrimString As Boolean = True) As Boolean
Dim result As Boolean
If String.IsNullOrEmpty(tSource) Then
result = tDefaultTarget
Else
If tTrimString Then
Dim source As String = _
TrueEvaluationsDelimiter & _
tSource.Trim().ToUpper() & _
TrueEvaluationsDelimiter
result = TheseStringsEvaluateToTrue.Contains(source)
ElseIf tDefaultTarget AndAlso _
Len(tSource) > 0 AndAlso _
Len(tSource.Trim()) = 0 Then
result = tDefaultTarget
' return default if we have a string of spaces
' and we're not trimming.
' According to this library's assumptions
' (your mileage may vary),
' this condition wouldn't
' be handled properly by our set of "True" strings
' if the default is True.
Else
Dim source As String = _
TrueEvaluationsDelimiter & _
tSource.ToUpper() & _
TrueEvaluationsDelimiter
result = TheseStringsEvaluateToTrue.Contains(source)
End If
End If
Return result
End Function
Here's the XSLT version…
<xsl:param name="StringsEvaluateToTrue" select="'|1|true|True|T|Y|y|.T.|.t.|'"/>
<xsl:param name="TrueEvaluationsDelimiter" select="'|'"/>
<xsl:template name="Boolean">
<xsl:param name="condition" select="false()"/>
<xsl:param name="trueRepresentation" select="'Y'"/>
<xsl:param name="falseRepresentation" select="'&nbsp;'"/>
<xsl:param name="editname" select="''"/>
<xsl:param name="editable" select="false()"/>
<xsl:choose>
<xsl:when test="string-length($editname) > 0">
<input type="checkbox" name="{$editname}" value="ON">
<xsl:if test="contains($StringsEvaluateToTrue,
concat($TrueEvaluationsDelimiter,
string($condition),
$TrueEvaluationsDelimiter))">
<xsl:attribute name="checked">ON</xsl:attribute>
</xsl:if>
<xsl:if test="$editable=false()">
<xsl:attribute name="disabled">yes</xsl:attribute>
</xsl:if>
</input>
</xsl:when>
<xsl:when test="contains($StringsEvaluateToTrue,
concat($TrueEvaluationsDelimiter,
string($condition),
$TrueEvaluationsDelimiter))">
<xsl:value-of select="$trueRepresentation"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$falseRepresentation"
disable-output-escaping="yes"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>