|
Introduction
Values retrieved from DataSets, ViewState, Session State, or any number of other storage mechanisms can cause errors if they comes back as DBNull or Nothing. As an example, let's say you want to retrieve a String from a DataSet. Typically, you begin by checking if the value is DBNull.Value, then casting the value to a String (if you code with Option Strict On), then checking if it's Nothing, then trimming it. A similar process applies when retrieving a String from ViewState and Session State: Dim firstName as string
If ds.Tables(0).Rows("FirstName") is DBNull.Value Then
firstName = String.Empty
Else
firstName = CStr(ds.Tables(0).Rows("FirstName"))
If Not IsNothing(firstName) Then
firstName = firstName.Trim
End If
End If
To overcome this cumbersome and repetitive process, I've created several "NullSafe" functions that handle the casting, trimming, and safety-checking of values.
Making Strings NullSafe
The first function is called NullSafeString. Notice that if the arg is DBNull, Nothing or String.Empty, the function defaults to returning an empty string, but you can override this behavior by passing a String of your choice as the second parameter:
Public Shared Function NullSafeString(ByVal arg As Object, _
Optional ByVal returnIfEmpty As String = "") As String
Dim returnValue As String
If (arg Is DBNull.Value) OrElse (arg Is Nothing) _
OrElse (arg Is String.Empty) Then
returnValue = returnIfEmpty
Else
Try
returnValue = CStr(arg).Trim
Catch
returnValue = returnIfEmpty
End Try
End If
Return returnValue
End Function
Using the above function which resides in my Utility class, the code to pull FirstName from the DataSet becomes: Dim firstName as string = _
Utility.NullSafeString(ds.Tables(0).Rows("FirstName"), "Not Available")
This function guarantees that the value of firstName is safe to work with. In other words, method calls like firstName.Length, firstName.Replace will not throw exceptions.
Integers, Doubles, and Booleans
To ensure a value is strongly-typed upon its return from the NullSafe function, I create a new method for each data type. On most projects, I have four NullSafe methods, but on a few, I've had to extend to six to encompass Longs and Shorts. Below are my NullSafeInteger, NullSafeDouble, and NullSafeBoolean methods. You'll notice the default return value for the numeric functions is WILDCARD_ID, a constant I create with a value of -1. In the calling code, I can then check for this constant value to determine if the value is "valid."
Public Shared Function NullSafeInteger(ByVal arg As Object, _
Optional ByVal returnIfEmpty As Integer = Constants.WILDCARD_ID) As Integer
Dim returnValue As Integer
If (arg Is DBNull.Value) OrElse (arg Is Nothing) _
OrElse (arg Is String.Empty) Then
returnValue = returnIfEmpty
Else
Try
returnValue = CInt(arg)
Catch
returnValue = returnIfEmpty
End Try
End If
Return returnValue
End Function
Public Shared Function NullSafeDouble(ByVal arg As Object, _
Optional ByVal returnIfEmpty As Integer = Constants.WILDCARD_ID) As Double
Dim returnValue As Double
If (arg Is DBNull.Value) OrElse (arg Is Nothing) _
OrElse (arg Is String.Empty) Then
returnValue = returnIfEmpty
Else
Try
returnValue = CDbl(arg)
Catch
returnValue = returnIfEmpty
End Try
End If
Return returnValue
End Function
Public Shared Function NullSafeBoolean(ByVal arg As Object) As Boolean
Dim returnValue As Boolean
If (arg Is DBNull.Value) OrElse (arg Is Nothing) _
OrElse (arg Is String.Empty) Then
returnValue = False
Else
Try
returnValue = CBool(arg)
Catch
returnValue = False
End Try
End If
Return returnValue
End Function
Conclusion
There is nothing magical about these functions; they simply encapsulate repetitive and cumbersome code into easy to use functions. Best of luck putting them to use!
| You must Sign In to use this message board. |
|
| | Msgs 1 to 14 of 14 (Total in Forum: 14) (Refresh) | FirstPrevNext |
|
|
 |
|
|
With overload "magic", you can simply write :
Public Shared Function NVL(ByVal value As DBNull, ByVal emptyValue As Object) As Object Return emptyValue End Function
Public Shared Function NVL(ByVal value As DBNull, ByVal emptyValue As ValueType) As ValueType Return emptyValue End Function
Public Shared Function NVL(ByVal value As Object, ByVal emptyValue As Object) As Object Return value End Function
Public Shared Function NVL(ByVal value As ValueType, ByVal emptyValue As ValueType) As ValueType Return value End Function
#If DEBUG Then Private Sub TestNVL() Debug.Write("NVL values : ") Debug.Write(NVL(DBNull.Value, "test1")) Debug.Write(", ") Debug.Write(NVL(DBNull.Value, 1)) Debug.Write(", ") Debug.Write(NVL(Nothing, "test2")) Debug.Write(", ") Debug.Write(NVL("val", "test3")) Debug.Write(", ") Debug.Write(NVL(1234, 3)) Debug.Write(", ") Debug.WriteLine(NVL(New Decimal(123.345), 4)) Debug.WriteLine("Correct values : test1, 1, test2, val, 1234, 123.345") End Sub #End If
If you wonder where NVL comes from, it's how that function is named in Oracle.
Sébastien
Intelligence shared is intelligence squared.
Homepage : http://sebastienlorion.com
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
|
I ran through your code and it works as expected. This is a clever solution to this issue. However, there are a couple of tasks the NVL functions don't appear to accomplish:
1. They don't cast values. For instance, if I pass dataTable.Rows(0).Item("FirstName") to your NVL functions I get back an Object (not a String). If I pass this same value to NullSafeString I receive a String, pass it to NullSafeInteger I receive an integer, etc... You can solve #1 by adding an appropriate CType call around each call to NVL, but one of the beauties of the NullSafe functions is they eliminate the need for these calls.
2. If you do make the calls to CType, then let's say the value in dt.Rows(0).Item("ShouldBeBoolean") is "Tru"; an exception will be thrown when you pass this to NVL and use CBool (see code below).
If you pass the same value to NullSafeBoolean you receive the correct return value. The same goes with invalid dates, invalid integers, etc... Of course, you could add Try/Catch blocks around all of these casts, but then you're almost back to the first block of code in the article.
Please post again if you have any ideas, I am interested in finding a way around these issues!
#If DEBUG Then Private Sub TestNVL()
Dim dt As New DataTable("Test") dt.Columns.Add("ShouldBeBoolean") dt.Rows.Add(New Object() {"Tru"})
Debug.Write("NVL value : ") Debug.Write(CBool(NVL(dt.Rows(0).Item("ShouldBeBoolean"), True)))
Debug.WriteLine("Correct value : False") End Sub #End If
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
1.
To call the value type version of NVL, the internal type of the value argument must be a value type (ie the boxed type) AND the type of the emptyValue argument must also be a value type.
Dim o As Object
o = 1 NVL(o, 1) 'Call to value type version
o = "asdf" NVL(o, 1) 'Call to object version
NVL(DBNull.Value, 1) 'Call to value type version NVL(DBNull.Value, "asdf") 'Call to object value
If you want a strongly typed value, simply wrap the call to NVL with the casting as DirectCast(NVL(...), GetType(String)) or CType(NVL(...), GetType(Integer)). Please note that DirectCast is much faster than CType and that CBool, CInt, etc. are from the VisualBasic namespace which you might want to avoid.
2.
As for your example, I would expect this code to fail and not return False. If you have "Tru" instead of "True" in your DB, that clearly shows some logic error somewhere and I think it is really a bad practice to let it go like you suggest. If you really want to do it that way, then one straighforward way :
Public Shared Function NVL(ByVal value As Object, ByVal emptyValue As Boolean) As Boolean Try Return Convert.ToBoolean(value) Catch ex As InvalidCastException Return emptyValue Catch ex As System.FormatException Return emptyValue End Try End Function
'repeat for each value type and any other type you want to manually handle like String
'Generic version Public Shared Function NVL(ByVal value As Object, ByVal emptyValue As Object) As Object Try Return Convert.ChangeType(value, emptyValue.GetType()) Catch ex As System.InvalidCastException Return emptyValue Catch ex As System.FormatException Return emptyValue End Try End Function
There might be a way of doing this without raising exceptions which are very expensive, probably using type converters (System.ComponentModel.XXXTypeConverter), but I leave that as an exercise for the reader
Sébastien
Intelligence shared is intelligence squared.
Homepage : http://sebastienlorion.com
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
While I agree with your approach, I prefer to use the NullSafe functions I've presented for a couple of reasons:
1. They eliminate somewhat verbose DirectCast calls throughout my code, which make it less readable.
2. Your approach doesn't allow code the option to not submit the emptyValue argument. Since my methods are strongly typed I can take advantage of optional parameters to provide a default emptyValue argument, thereby slimming the calling code even more.
Since these functions are called in literally hundreds of places in a medium-sized application, and possibly thousands of places in a large application, my goal is to minimize complexity of the calling code for better readability and maintainability.
In the end it's up to each developer to make his or her choice. I certainly appreciate Sébastien's feedback as it provides a more complete picture of the options.
Rob
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
|
 |
|
|
Sadly point 1 isn't correct:
Sébastien Lorion wrote: 1.
To call the value type version of NVL, the internal type of the value argument must be a value type (ie the boxed type) AND the type of the emptyValue argument must also be a value type.
Dim o As Object
o = 1 NVL(o, 1) 'Call to value type version
o = "asdf" NVL(o, 1) 'Call to object version
NVL(DBNull.Value, 1) 'Call to value type version NVL(DBNull.Value, "asdf") 'Call to object value
The overload resolution is done at compile type and uses the type as declared, not the type of the object that is currently assigned to that variable.
You can see this if you change the test function to:
Private Sub TestNVL() Dim o As Object = DBNull.Value Debug.Write("NVL values : ") Debug.Write(NVL(o, "test1")) Debug.Write(", ") Debug.Write(NVL(o, 1)) Debug.Write(", ") o = Nothing Debug.Write(NVL(o, "test2")) Debug.Write(", ") o = "val" Debug.Write(NVL(o, "test3")) Debug.Write(", ") o = 1234 Debug.Write(NVL(o, 3)) Debug.Write(", ") o = New Decimal(123.345) Debug.WriteLine(NVL(o, 4)) Debug.WriteLine("Correct values : test1, 1, test2, val, 1234, 123.345") End Sub
Which produces this instead of the expected values:
NVL values : , , , val, 1234, 123.345 More importantly if you attempt to cast the result instead of calling .ToString on it (which Debug.WriteLine does) an exception will be raised as the following proves:
Private Sub TestNVL() Dim o As Object = DBNull.Value Debug.Write("NVL values : ") Dim s As String = DirectCast(NVL(o, "test"), String) Debug.Write(NVL(s, "test")) Debug.WriteLine("Correct value : test") End Sub
So the NVL function will only work as expected if the input values have already been cast to the correct type - which defeats its purpose!
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Simpler still!!!
If you have created a Typed Dataset, edit the default property of each field.
Typically, I set strings to String.Empty(), or a value like "N/A", numbers can be 0 (or anything else)
the code is automatically included in the Typed Dataset code. For Example:
Public Property I_CITY As String Get If Me.IsI_CITYNull Then Return String.Empty Else Return CType(Me(Me.tableMembers.I_CITYColumn),String) End If End Get Set Me(Me.tableMembers.I_CITYColumn) = value End Set End Property
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
If I am not mistaken, the point of the article was to provide a generic way of handling this part of your example :
If Me.IsI_CITYNull Then Return String.Empty Else Return CType(Me(Me.tableMembers.I_CITYColumn),String) End If
Otherwise, you will have to repeat that kind of code everytime it is needed instead of centralizing its use and behavior.
Sébastien
Intelligence shared is intelligence squared.
Homepage : http://sebastienlorion.com
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
No,
The point I was trying to make is that you don't have to code anything.
By setting the NullValue property for the elements of the XSD Schema, in the schema editor, the code to substitute the value is automatically generated in the Typed Dataset by the code generator built into Visual Studio (or the XSD.exe tool if you don't have VS).
VS has many features that allow you to create complex data access components without writing ANY code.
Hint: start off with a COMPONENT instead of a CLASS. Then you can drag and drop DataAdapters and use the Wizards to configure the SQL, mappings, and Typed Dataset creation.
Matthew
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
... but then you discover that typed datasets are such big fat monsters that it's better staying away from them.
Also, one-way generated code will always have the drawback that you cannot modify it because your changes will be lost the next time you regenerate the code.
Sébastien
Intelligence shared is intelligence squared.
Homepage : http://sebastienlorion.com
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
If you need alternate functionality, Inherit or wrap the Typed Dataset in another class. That way you separate the changed code from the generated code.
Antoher approach is to use something like CodeSmith (www.ericjsmith.com/codesmith) to create typed datasets of your design.
Remeber, every line of code you don't have to write, is one you didn't make a mistake on. Even better, you still get paid for it. 
I typically will change the XSD file many times during development (I use Test Driven methodologies) and just keep using the regenerated Dataset. create/modify a test
Add new tables, new fields, change types, modify SQL, generate the Dataset fix the code to use the new/renamed/deleted properties and methods run the test
Repeat as required
You get to work on the business value, not the infrastructure.
Matthew
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
CodeSmith is quite useful, but even a barebone dataset is a PITA. Just try to serialize it in something else than XML in a generic way. After, try to do intelligent caching, like having certain DataTable static while others are loaded everytime, while maintaining relations amongst those tables.
Everywhere you look including MSDN, there is one constant in what is said : DataSet are heavyweigth objects which should be avoided if performance is an issue.
Finally, business value is directly related to good design, ie a good infrastructure.
Sébastien
Intelligence shared is intelligence squared.
Homepage : http://sebastienlorion.com
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
General News Question Answer Joke Rant Admin
|