VB.NET Extension Methods primary
This articles intent is to provide some basics to writing extension methods with practical examples along with references at the end of the article. Please note that the “how-to” is emphasized here over actual code within the extensions since if you were to ask 10 programmers how to do task A there will more likely than not at least five different ways to accomplish the task.
One of the features I embraced when VS2008 arrived was extension method (I tend to call them language extensions) as my style of programming is declarative in nature so this was a natural progression for me.
When reading various documents from MSDN web site along with the sparse amount of information I did pick it up easily. Also realized that for some developers not in the game for very long may not see the benefits of using extensions along with others who might abuse extension or would shun them if the few limitations caused them discomfort.
From MSDN Visual Basic 2008 introduces extension methods, which enable developers to add custom functionality to data types that are already defined without creating a new derived type. Extension methods make it possible to write a method that can be called as if it were an instance method of the existing type.
http://msdn.microsoft.com/en-us/libr...36(VS.90).aspx
Okay my intent is to show the average developer useful ways to use extension methods in their every day task of developing solutions. If you want in-depth information, check out the links at the bottom of this article.
First steps: Do not write an extension simply because you can but instead because it serves a valid purpose over writing a convention function or procedure. Secondly, write the extension as a conventional function or sub until you get the hang of writing extension methods. Another consideration is how show exceptions be handled and finally in regards to starting out writing extensions think about their scope.
Re: VB.NET Extension Methods primary
A common operation is to check to see if a String has a value.
Code:
Dim SomeValue As String = ""
If String.IsNullOrEmpty(SomeValue) Then
Console.WriteLine("Empty")
Else
Console.WriteLine("Not empty")
End If
In the above example, the string is empty and the code worked as expected but is not very declarative plus this code logic if used a lot is not so easy to upgrade if a better method comes along. In this case under VS2010 we have been given IsNullOrWhiteSpace Method which means if you want to upgrade your code you will need to change every instance of one method to another method in your projects.
A better way is to create a centralized function so that change happens in one place rather in many places. Back to extension methods, for this example of checking if a string is empty what would be a good name for such an extension? Well the best thought process is to find another type which has a similar method name such as Nullable Integer which has .HasValue. So we create
Code:
Public Function HasValue(ByVal Value As String) As Boolean
Return Not String.IsNullOrEmpty(Value)
End Function
And test it to make sure it works as expected.
Code:
Dim SomeValue As String = Nothing
If HasValue(SomeValue) Then
Console.WriteLine("Not empty")
Else
Console.WriteLine("Empty")
End If
Next convert the function to an extension method by adding a Extension Attribute
Code:
<System.Runtime.CompilerServices.Extension()>
So now the function turned extension method looks like this.
Code:
<System.Runtime.CompilerServices.Extension()> _
Public Function HasValue(ByVal Value As String) As Boolean
Return Not String.IsNullOrEmpty(Value)
End Function
And to call the extension method
Code:
If SomeValue.HasValue Then
Console.WriteLine("Not empty")
Else
Console.WriteLine("Empty")
End If
And we can still call it as a regular function
Code:
If HasValue(SomeValue) Then
Console.WriteLine("Not empty")
Else
Console.WriteLine("Empty")
End If
There are some issues as in
Code:
If HasValue(SomeValue) Then
Console.WriteLine("Not empty")
Else
Console.WriteLine("Empty")
End If
<System.Runtime.CompilerServices.Extension()> _
Public Function HasValue(ByVal sender As String) As Boolean
Return Not String.IsNullOrEmpty(sender)
End Function
<Runtime.CompilerServices.Extension()> _
Public Function HasValue(ByVal sender() As String) As Boolean
Dim Result As Boolean = False
If sender IsNot Nothing Then
Result = sender.Count > 0
End If
Return Result
End Function
Re: VB.NET Extension Methods primary
Essentially, we have two functions that accept different types, which produce an ambiguous IDE error. The way around this if you want to call the function as a regular function instead of a extension method is to prefix the name space if different from the project name space or simple prefix the function call with the module name where the proper function to use is located in.
For this example the function is in StringMethods.vb
Code:
If StringMethods.HasValue(SomeValue) Then
Console.WriteLine("Not empty")
Else
Console.WriteLine("Empty")
End If
While discussing code modules, extensions must reside in code modules and not forms. When naming these modules I suggest giving them meaningful names like StringMethods.vb for string extensions and for arrays ArrayMethods.vb and so on.
Code:
Dim SomeValue As String = Nothing
If SomeValue.HasValue() Then
Console.WriteLine("SomeValue is Not empty")
Else
Console.WriteLine("SomeValue is Empty")
End If
SomeValue = "Got some text"
If SomeValue.HasValue Then
Console.WriteLine("SomeValue is Not empty")
Else
Console.WriteLine("SomeValue is Empty")
End If
Dim SomeNullableInteger As New Nullable(Of Integer)
If SomeNullableInteger.HasValue Then
Console.WriteLine("SomeNullableIneger has a value")
Else
Console.WriteLine("SomeNullableIneger does not has a value")
End If
SomeNullableInteger = 100
If SomeNullableInteger.HasValue Then
Console.WriteLine("SomeNullableIneger has a value")
Else
Console.WriteLine("SomeNullableIneger does not has a value")
End If
So now when checking to see if a string or Nullable type (an Integer in this example) is empty the same method name is used.
For a medium challenge, let’s take an array of strings where the goal is to get only integer elements. The following code would do the trick.
Code:
Dim Values() As String = {"1", "2", "-3", "x2", "300", "", "Hello"}
Dim Numbers_A = (From x In Values _
Where IsNumeric(x) _
Select CInt(x)).ToArray
If Numbers_A.Count > 0 Then
For Each Number In Numbers_A
Console.WriteLine("{0,15}", Number)
Next
End If
Well we can do a little better than using IsNumeric by using Integer.TypeParse
Code:
Dim Numbers_B = (From x In Values _
Where Integer.TryParse(x, Nothing) _
Select Convert.ToInt32(x)).ToArray
Or how about the following which is an extension method of type Integer.
Code:
Dim Numbers_C = (From x In Values Where x.IsNumeric Select CInt(x)).ToArray
<Runtime.CompilerServices.Extension()> _
Public Function IsNumeric(ByVal sender As String) As Boolean
Return Integer.TryParse(sender, Nothing)
End Function
Or better yet
Code:
<Runtime.CompilerServices.Extension()> _
Public Function IsInteger(ByVal sender As String) As Boolean
Return Integer.TryParse(sender, Nothing)
End Function
<Runtime.CompilerServices.Extension()> _
Public Function IsDouble(ByVal sender As String) As Boolean
Return Double.TryParse(sender, Nothing)
End Function
Please note that I am not saying you should replace IsNumeric but simply using a this for a simple example.
Another thought when developing extension methods is coupling them together, for instance the following extension method checks to see if a Integer value is between a low and high integer value.
Code:
<System.Runtime.CompilerServices.Extension()> _
Public Function IsBetween( _
ByRef sender As Integer, _
ByRef LowerLimit As Integer, _
ByRef UpperLimit As Integer) As Boolean
If sender < LowerLimit Or sender > UpperLimit Then Return False
Return True
End Function
Re: VB.NET Extension Methods primary
Implemented
Code:
Dim IntegerTestBetween As Integer = 100
Console.WriteLine("IntegerTestBetween {0}", IntegerTestBetween)
If IntegerTestBetween.IsBetween(101, 105) Then
Console.WriteLine("Value is between 101 and 105")
Else
Console.WriteLine("Value is not between 101 and 105")
End If
If IntegerTestBetween.IsBetween(50, 101) Then
Console.WriteLine("Value is between 50 and 101")
Else
Console.WriteLine("Value is not between 50 and 101")
End If
The IsBetween extension can be used to check if a character is between two characters.
Code:
<System.Runtime.CompilerServices.Extension()> _
Public Function IsBetween(ByVal sender As Char, _
ByVal LowerLimit As Char, ByVal UpperLimit As Char) As Boolean
Dim Value As Integer = Asc(sender)
Return Value.IsBetween(Asc(LowerLimit), Asc(UpperLimit))
End Function
In the IsBetween extension method for characters, the return line uses the IsBetween extension method for Integers.
Using extension methods with data routines is great too, like when you retrieve data from a database into a DataTable, which becomes the source of a DataGridView. Once the data is loaded the user wants to save the current view to an XML file so you might code this.
Code:
DirectCast(bsData.DataSource, DataTable).WriteXml("SampleData.xml")
If IO.File.Exists("SampleData.xml") Then
MsgBox("Data successfully exported")
Else
MsgBox("Failed to export data")
End If
We know casting the BindingSource DataSource is safe because we set the BindingSource DataSource from a DataTable. For me I would rather be declarative and not see the casting so we ended up with another extension method for casting the DataSource of any BindingSource called .DataTable.
Code:
bsData.DataTable.WriteXml("SampleData.xml")
If IO.File.Exists("SampleData.xml") Then
MsgBox("Data successfully exported")
Else
MsgBox("Failed to export data")
End If
<System.Runtime.CompilerServices.Extension()> _
Public Function DataTable(ByVal sender As BindingSource) As DataTable
Return DirectCast(sender.DataSource, DataTable)
End Function
If for some reason you did not load the data or did not have your morning coffee and the BindingSource DataSource is not a DataTable a run time exception will be thrown. We could handle it as follows;
Code:
<System.Diagnostics.DebuggerStepThrough()> _
<System.Runtime.CompilerServices.Extension()> _
Public Function DataSourceIsDataTable(ByVal sender As BindingSource) As Boolean
Return sender.DataSource.GetType.Equals(GetType(System.Data.DataTable))
End Function
If bsData.DataSourceIsDataTable Then
bsData.DataTable.WriteXml("SampleData.xml")
If IO.File.Exists("SampleData.xml") Then
MsgBox("Data successfully exported")
Else
MsgBox("Failed to export data")
End If
Else
' . . .
End If
If you test well then how about doing your check within the DataTable extension?
Code:
<System.Runtime.CompilerServices.Extension()> _
Public Function DataTable(ByVal sender As BindingSource) As DataTable
If sender.DataSourceIsDataTable Then
Return DirectCast(sender.DataSource, DataTable)
Else
Throw New Exception("DataSource is not a DataTable")
End If
End Function
An important thing to know is that using a standard code module the header is as follows
Where all the extensions within are public to code in the current project but not to other support projects. Example, you create a DLL with all your extension methods so they can be used in several projects. As the module sits no other projects within a solution will see these extensions unless you prefix Module with Public.
Code:
Public Module DateMethods
The mark of a professional will add a header to each extension so that when someone consumes your extension method they have information through intellisense. Of course if you give a good name to the extension method the information is not so important.
Code:
''' <summary>
''' Indicates if the current row is a new row
''' </summary>
''' <param name="sender"></param>
''' <returns></returns>
''' <remarks></remarks>
<System.Diagnostics.DebuggerStepThrough()> _
<System.Runtime.CompilerServices.Extension()> _
Public Function IsNewRow(ByVal sender As BindingSource) As Boolean
Return DirectCast(sender.Current, DataRowView).Row.RowState = DataRowState.Detached
End Function
So with that said the following information would be helpful so when the user gets an different size array back they know this ahead of time.
Code:
''' <summary>
''' Creates a new array of integer values and discards all other types
''' </summary>
''' <param name="sender"></param>
''' <returns></returns>
''' <remarks>
''' If the original array (sender) has 10 elements where seven are
''' valid integers the resulting array will have seven elements
''' </remarks>
<Runtime.CompilerServices.Extension()> _
Public Function ToIntArray(ByVal sender() As String) As Integer()
Return (From x In sender Where Integer.TryParse(x, Nothing) Select CInt(x)).ToArray
End Function
To be continued
Re: VB.NET Extension Methods primary
Re: VB.NET Extension Methods primary
A very nice write up. I will have to give these a try sometime. Thanks...
Database connection extension methods suggestions
Let’s say you have a database connection object that is opened/closed while the form or class is being used you would use logic similar to the following
Code:
If MyFormAppConnection.State = ConnectionState.Closed Then
MyFormAppConnection.Open()
End If
Using an extension method we can place the logic into the method and give it a easy to remember name such as the following
Code:
If MyFormAppConnection.IsClosed Then
MyFormAppConnection.Open()
End If
Another name for the method (not my cup of tea but simply show this is possible)
Code:
If MyFormAppConnection.NeedsToBeOpened Then
MyFormAppConnection.Open()
End If
The key is to give the method a name that indicates what it is doing for you without having to look into the method.
Now suppose when the connection was closed the code set the connection to nothing we need to check prior to opening the connection as follows.
Code:
If MyFormAppConnection Is Nothing Then
MyFormAppConnection = New OleDb.OleDbConnection(ConnectionString)
End If
Using a extension method
Code:
If MyFormAppConnection.IsNothing Then
MyFormAppConnection = New OleDb.OleDbConnection(ConnectionString)
End If
Where the beauty is with the examples above is when using multiple data providers in a project we can have the same extension methods for both providers. In the example below the IsNothing method is for both OleDb and IBM DB2.
Code:
<System.Diagnostics.DebuggerStepThrough()> _
<Runtime.CompilerServices.Extension()> _
Public Function IsNothing(ByVal sender As OleDb.OleDbConnection) As Boolean
Return sender Is Nothing
End Function
<System.Diagnostics.DebuggerStepThrough()> _
<Runtime.CompilerServices.Extension()> _
Public Function IsNothing(ByVal sender As DB2.iSeries.iDB2Connection) As Boolean
Return sender Is Nothing
End Function
The attached demonstration project was done in VS2008 using LINQ and embedded expressions in the connection string which was done because this is easier to read then how programmers generally do connection string as in concatenation of the string. Personally my connection strings are in an XML settings file so I never have to see them in my code.
Code:
Private ConnectionString As String = _
<Text>
Provider=Microsoft.Jet.OLEDB.4.0;
Data Source=<%= Application.StartupPath %>\;
Extended Properties='text;HDR=No'
</Text>.Value
How most developers do connection strings (when done cleanly)
Code:
Private ConnectionString As String = String.Format("Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};", MyFileName)
Re: VB.NET Extension Methods primary
And remember to pass variables ByVal unless they need to be ByRef to avoid confusion, use OrElse instead of Or, and
Code:
<Runtime.CompilerServices.Extension()> _
Public Function HasValue(ByVal sender() As String) As Boolean
Dim Result As Boolean = False
If sender IsNot Nothing Then
Result = sender.Count > 0
End If
Return Result
End Function
could be
Code:
<Runtime.CompilerServices.Extension()> _
Public Function HasValue(ByVal sender() As String) As Boolean
Return sender IsNot Nothing AndAlso sender.Count > 0
End Function
Because of the new (relative to VB 2003) AndAlso and OrElse operators.
Extension for apostrophe in SQL statements
One of my most used extension method s underlying logic is to circumvent an error from a data provider when an apostrophe is entered into a TextBox which is used in a WHERE statement of an SQL statement. The apostrophe bug where best seen in the SQL statement example below (figure 1) where the apostrophe after the “I” in “I’m” will be treat as the end of the SQL statement string and when the database engine runs into the last apostrophe an error is generated so we need to add logic to replace single apostrophes with double apostrophes (figure 2). Of course if you use parameterized statements you may find out that this problem is null and void but my guess is that many developers will forgo using parameterized SQL.
Note 1 that in this case an apostrophe is also known as an escape character.
Note 2 some databases do not have this problem when your SQL statement is put together using parameters.
Figure 1
Code:
INSERT INTO Customers (F1,F2,F3...) VALUES ('Hi folks, I'm a developer', ‘’, ‘’ . . .)
Figure 2
Code:
Dim QueryStatement As String = _
"SELECT * FROM Customers " & _
"WHERE CompanyName ='" & _
txtSearchForValue.Text.Replace("'", "''") & "'"
When writing and debugging complex SQL statements it might be helpful to have methods to show where there are issues like when creating an insert statement where the underlying table has constraints against empty fields. We could create extensions, which allow you to see them easily. Figure 3 is one example which when used as in figure 4 if a value is empty it will show up as shown in figure 5 where the first parameter value is empty because the user only entered a contact and not a company name. Of course there should be logic to prevent this from ever happening, instead this is an example for checking values in an insert statement, use your imagination.
Figure 3
Code:
Private Const NULL_INDICATOR As String = "NULL"
<System.Diagnostics.DebuggerStepThrough()> _
<Runtime.CompilerServices.Extension()> _
Public Function sqlTextNullDetection(ByVal sender As String) As String
Dim Result As String = sender
If Not sender.HasValue Then
Result = NULL_INDICATOR
End If
Return Result.SqlEncode
End Function
Figure 4
Code:
Dim Sample As String = _
<SQL>
INSERT INTO Company
(
Name,
Contact
)
VALUES
(
'<%= txtCompanyName.Text.sqlTextNullDetection %>',
'<%= txtCompanyContact.Text.sqlTextNullDetection %>'
)
</SQL>.Value
txtInsertQueryDebugging.Text = Sample.TrimEmbeddedQueryText
Figure 5
Code:
INSERT INTO Company (Name, Street ) VALUES ( 'NULL', 'John Doe' )
You might be thinking that in figure 4 we need to have an oversized text box since the example SQL statement spans multiple lines. This is handled by an extension method TrimEmbeddedQueryText which in short, flattens the SQL statement to one line shown in figure 6.
Figure 6
Code:
Imports System.Text.RegularExpressions
Module FixJaggedSQL_Method
<System.Diagnostics.DebuggerStepThrough()> _
<Runtime.CompilerServices.Extension()> _
Function TrimEmbeddedQueryText(ByVal value As String) As String
Return Regex.Replace(value.TrimStart, " {2,}", " ").Replace(Chr(10), "")
End Function
End Module
Notable
Using Parameters in ADO.NET http://jmcilhinney.blogspot.com/2009...in-adonet.html
SQL Server (2008 R2) QUOTENAME (Transact-SQL)
http://msdn.microsoft.com/en-us/library/ms176114.aspx
SQL-Server
SET QUOTED_IDENTIFIER = [On/Off]
http://msdn.microsoft.com/en-us/libr...v=sql.80).aspx
ADO.NET Configuring parameters
http://msdn.microsoft.com/en-us/library/yy6y35y8.aspx
DbParameter Class http://msdn.microsoft.com/en-us/libr...parameter.aspx