Code the Global Change - Part 1
Previous: Retrieve the TimeZoneEntry
In this section, I will provide a brief explanation of the CLR code that implements the bulk of the business logic along with the USR_USP_ADDRESSCOORDINATETIMEZONE_GCPROCESS_UPDATE stored procedure which will be covered in a following section.
Tip: The best way to walk through this code is to debug the global change within Visual Studio. For details on debugging a global change, see Debugging.
The code will accomplish the following:
-
Obtain the parameter values for the global change instance.
-
Use parameter values to retrieve a set of address coordinate values to process.
-
For each address coordinate, call to the Google Time Zone API and retrieve the name of time zone.
-
Use the name of the time zone to obtain the Time Zone GUID from the TIMEZONEENTRY table.
-
Build an XML message representing the rows of time zones to add/update.
-
Pass the XML message to the USR_USP_ADDRESSCOORDINATETIMEZONE_GCPROCESS_UPDATE stored procedure to add/edit the time zones to the USR_ADDRESSCOORDINATETIMEZONE.
-
Return the result including numbers of rows inserted and updated.
Tip: The source code includes a Visual Studio 2010 solution and server side assembly project containing the XML specs/catalog items and VB class file. The source code is included with this example within a file named AddressCoordinateTimeZone.GlobalChange.vb. Grab the source from the repo in GitHub.
Obtain the Parameter Values for the Global Change Instance
Step 1 - Open the CLR code file and add the code below.
The IDSetRegisterID, OnlyPrimary, OnlyChanged variables are declared as PUBLIC. In this way the system can pass the parameter values into the code at runtime.
Note how we inheriting AppGlobalChangeProcess and overriding ProcessGlobalChange() function. The system will call this function when the instance is started. The ProcessGlobalChange() function must return an object of type AppGlobalChangeResult, which will contain the number of rows inserted, updated, and deleted.
Imports Blackbaud.AppFx.Server
Imports System.Xml
Imports System.Data.SqlClient
Imports Blackbaud.AppFx.XmlTypes
Public NotInheritable Class AddressCoordinateTimeZoneGlobalChange
Inherits AppCatalog.AppGlobalChangeProcess
Public IDSetRegisterID As Guid
Public OnlyPrimary As Boolean?
Public OnlyChanged As Boolean?
Private AsOf As DateTime
Private _expectedExceptions As ExpectedDBExceptions
Public Overrides Function ProcessGlobalChange() As AppCatalog.AppGlobalChangeResult
Dim changeAgentID As Guid = RequestContext.GetChangeAgentID()
Dim numberUpdated As Integer = 0
Dim numberInserted As Integer = 0
Dim numberExceptions As Integer = 0
AsOf = MyBase.RequestArgs.LastRunOn
Use Parameter Values to Retrieve a Set of Address Coordinate Values to Process
Grab a connection to the Infinity database within the connection context of the business process.
Closing Connections
Sometimes your code requires an unmanaged resource, such as a SQL connection. If the SqlConnection goes out of scope, it won't be closed. Therefore, you must explicitly close the connection by calling Close or Dispose. You may also consider a Using block which guarantees the disposal of one or more such resources when your code is finished with them. For more information see http://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnection.close.aspx and http://msdn.microsoft.com/en-us/library/htd05whh.aspx.
For example, the following code would require the explicit closing of the connection:
Dim conn As SqlConnection = Me.RequestContext.OpenAppDBConnection
‘code to perform work with the sql connection
conn.Close
Whereas utilizing a Using block guarantees the disposal of the SQLConnection:
using conn As SqlConnection = Me.RequestContext.OpenAppDBConnection
‘code to perform work with the sql connection
end using
Build a dynamical T-SQL SELECT statement that returns rows from the AddressCoordinates table. The code below utilizes the parameter values to determine the portions of the SELECT statement that restricts the amount of rows returned. You can review the entire, completed code sample at the bottom of this content within the section named Completed CLR Class.
Dim baseTableName As String = "dbo.CONSTITUENT"
Dim withClause As String = BuildRecordSecurityWithClause("CONSTITUENT", "Constituent", "ID")
Dim FilterOutUnmodified As Boolean = OnlyChanged.HasValue AndAlso OnlyChanged.Value
If Len(withClause) > 0 Then
baseTableName = "CONSTITUENT_RACS"
End If
Dim commandText As String = withClause & _
"SELECT " & _
" ADDRESSCOORDINATES.ID " & _
" , ADDRESSCOORDINATES.LATITUDE " & _
" , ADDRESSCOORDINATES.LONGITUDE " & _
"FROM " & baseTableName & " as C " & _
"INNER JOIN dbo.ADDRESS on ADDRESS.CONSTITUENTID = C.ID " & _
"INNER JOIN dbo.ADDRESSCOORDINATES on ADDRESSCOORDINATES.ADDRESSID = ADDRESS.ID "
If IDSetRegisterID <> Guid.Empty Then
Dim idSetReader As New Blackbaud.AppFx.Server.IDSetReader(IDSetRegisterID, RequestContext)
commandText &= String.Format("inner join {0} SELECTION on SELECTION.{1} = C.ID ", idSetReader.GetResultsTableOrFunctionName(), idSetReader.IDColumnName)
End If
If FilterOutUnmodified Then
commandText &= "LEFT JOIN dbo.USR_ADDRESSCOORDINATETIMEZONE on USR_ADDRESSCOORDINATETIMEZONE.ID = ADDRESSCOORDINATES.ID "
End If
If FilterOutUnmodified Then
commandText &= "where (USR_ADDRESSCOORDINATETIMEZONE.ID is null or USR_ADDRESSCOORDINATETIMEZONE.DATECHANGED < ADDRESSCOORDINATES.DATECHANGED) "
End If
If OnlyPrimary Then
If FilterOutUnmodified Then
commandText &= "and ADDRESS.ISPRIMARY = 1 "
Else
commandText &= "where ADDRESS.ISPRIMARY = 1 "
End If
End If
commandText &= "order by ADDRESSCOORDINATES.DATEADDED"
command = theConn.CreateCommand()
command.CommandText = commandText
command.CommandTimeout = 20
command.CommandType = CommandType.Text
For Each Address Coordinate, Call to the Google Time Zone API and Retrieve the Name of Time Zone
Loop through the rows and begin building an XML string. Pass the latitude and logiture to GetTimeZone() to retrieve time zone name via the API. Within GetTimeZone(), call GetTimeZoneEntryID() to grab the GUID for the time zone from the database. Use the TimeZoneResponse class to returned the time zone information. Report any issues via an ENUM named "Status." Build an XML message representing the rows of time zones to add/update. Call the SP named USR_USP_ADDRESSCOORDINATETIMEZONE_GCPROCESS_UPDATE to insert and/or update the rows in the database. Return the result this database call including numbers of rows inserted and updated.
See the section named Completed CLR Class at the bottom of this topic for details on GetTimeZone(), GetTimeZoneEntryID(), and the TimeZoneResponse class.
Dim mergeXMLMsg As String
mergeXMLMsg = "<ROOT>"
Try
Dim reader As SqlDataReader = command.ExecuteReader()
While reader.Read()
Dim ADDRESSCOORDINATESID As Guid = reader.GetGuid(0)
Dim ADDRESSCOORDINATESLATITUDE As Decimal = reader.GetDecimal(1)
Dim ADDRESSCOORDINATESLONGITUDE As Decimal = reader.GetDecimal(2)
' Make call to GetTimeZone function which uses an experimental Google maps time zone api
' The use of this service within this global change code is for educational purposes only and
' is not meant for production.
' This service has usage limits.
'https://developers.google.com/maps/documentation/timezone/
Dim TimeZoneSvcResponse As TimeZoneResponse = GetTimeZone(ADDRESSCOORDINATESLATITUDE.ToString, ADDRESSCOORDINATESLONGITUDE.ToString)
'Using result of calls to service and the lat and longitude, build up an xml message and pass
'to stored procedude which will update the custom USR_ADDRESSCOORDINATETIMEZONE table
' Example xml message passed to stored procedure.
'<ROOT>
'<TimeZoneResponse ADDRESSCOORDINATESID="AEC02AB8-430B-48B8-A32C-348F98C04C2B" StatusCode="1" StatusDesc="The request was successful." TimeZoneEntryID="B6CE5E5E-236F-437E-9A42-DFDB945B5286"/>
'<TimeZoneResponse ADDRESSCOORDINATESID="E457774D-97E4-4F0C-A35B-5020347C2EAD" StatusCode="1" StatusDesc="The request was successful." TimeZoneEntryID="B6CE5E5E-236F-437E-9A42-DFDB945B5286"/>
'<TimeZoneResponse ADDRESSCOORDINATESID="41BDE2D6-C9F7-4AB6-86DC-129A01F98920" StatusCode="1" StatusDesc="The request was successful." TimeZoneEntryID="B6CE5E5E-236F-437E-9A42-DFDB945B5286"/>
'<TimeZoneResponse ADDRESSCOORDINATESID="B9E1C919-811E-4B88-A39E-D8203FF08BAD" StatusCode="1" StatusDesc="The request was successful." TimeZoneEntryID="B6CE5E5E-236F-437E-9A42-DFDB945B5286"/>
'</ROOT>
mergeXMLMsg &= "<TimeZoneResponse ADDRESSCOORDINATESID=""" & ADDRESSCOORDINATESID.ToString & """ StatusCode=""" & TimeZoneSvcResponse.ResponseStatus & """ StatusDesc=""" & TimeZoneSvcResponse.ResponseStatusDesc.ToString & """ TimeZoneEntryID=""" & TimeZoneSvcResponse.TimeZoneEntryID.ToString & """/>"
End While
mergeXMLMsg &= "</ROOT>"
reader.Close()
Dim DBCommand As SqlCommand = New SqlCommand("USR_USP_ADDRESSCOORDINATETIMEZONE_GCPROCESS_UPDATE", theConn)
DBCommand.CommandType = CommandType.StoredProcedure
Dim xmlDocParm As SqlParameter = DBCommand.Parameters.Add("@doc", SqlDbType.NVarChar, -1)
xmlDocParm.Value = mergeXMLMsg
Dim changeAgentParm As SqlParameter = DBCommand.Parameters.Add("@CHANGEAGENTID", SqlDbType.UniqueIdentifier)
changeAgentParm.Value = changeAgentID
Dim myReader As SqlDataReader = DBCommand.ExecuteReader()
Dim changeType As String
Do While (myReader.Read())
changeType = myReader.GetString(0)
If changeType = "UPDATE" Then
numberUpdated = myReader.GetInt32(1)
ElseIf changeType = "INSERT" Then
numberInserted = myReader.GetInt32(1)
End If
Loop
myReader.Close()
theConn.Close()
Catch sqlEx As Exception
'Build the collection of expected exceptions
BuildExceptions("SQL Exception", sqlEx.Message)
MyBase.HandleSQLException(sqlEx, _expectedExceptions)
End Try
Dim result As New AppCatalog.AppGlobalChangeResult(numberInserted, numberUpdated, 0)
Return result
End Function
Step 2 - Review the CLR class
Review the completed class below. We will deploy the class at the end of this exercise.
Completed CLR Class
Imports Blackbaud.AppFx.Server
Imports System.Xml
Imports System.Data.SqlClient
Imports Blackbaud.AppFx.XmlTypes
Public NotInheritable Class AddressCoordinateTimeZoneGlobalChange
Inherits AppCatalog.AppGlobalChangeProcess
Public IDSetRegisterID As Guid
Public OnlyPrimary As Boolean?
Public OnlyChanged As Boolean?
Private AsOf As DateTime
Private _expectedExceptions As ExpectedDBExceptions
Public Overrides Function ProcessGlobalChange() As AppCatalog.AppGlobalChangeResult
Dim changeAgentID As Guid = RequestContext.GetChangeAgentID()
Dim numberUpdated As Integer = 0
Dim numberInserted As Integer = 0
Dim numberExceptions As Integer = 0
If Not MyBase.RequestArgs.LastRunOn Is Nothing Then
AsOf = MyBase.RequestArgs.LastRunOn
End If
_expectedExceptions = New ExpectedDBExceptions
Dim theConn As SqlConnection = RequestContext.OpenAppDBConnection(RequestContext.ConnectionContext.BusinessProcess)
Dim command As SqlCommand
Dim baseTableName As String = "dbo.CONSTITUENT"
Dim withClause As String = BuildRecordSecurityWithClause("CONSTITUENT", "Constituent", "ID")
Dim FilterOutUnmodified As Boolean = OnlyChanged.HasValue AndAlso OnlyChanged.Value
If Len(withClause) > 0 Then
baseTableName = "CONSTITUENT_RACS"
End If
Dim commandText As String = withClause & _
"SELECT " & _
" ADDRESSCOORDINATES.ID " & _
" , ADDRESSCOORDINATES.LATITUDE " & _
" , ADDRESSCOORDINATES.LONGITUDE " & _
"FROM " & baseTableName & " as C " & _
"INNER JOIN dbo.ADDRESS on ADDRESS.CONSTITUENTID = C.ID " & _
"INNER JOIN dbo.ADDRESSCOORDINATES on ADDRESSCOORDINATES.ADDRESSID = ADDRESS.ID "
If IDSetRegisterID <> Guid.Empty Then
Dim idSetReader As New Blackbaud.AppFx.Server.IDSetReader(IDSetRegisterID, RequestContext)
commandText &= String.Format("inner join {0} SELECTION on SELECTION.{1} = C.ID ", idSetReader.GetResultsTableOrFunctionName(), idSetReader.IDColumnName)
End If
If FilterOutUnmodified Then
commandText &= "LEFT JOIN dbo.USR_ADDRESSCOORDINATETIMEZONE on USR_ADDRESSCOORDINATETIMEZONE.ID = ADDRESSCOORDINATES.ID "
End If
If FilterOutUnmodified Then
commandText &= "where (USR_ADDRESSCOORDINATETIMEZONE.ID is null or USR_ADDRESSCOORDINATETIMEZONE.DATECHANGED < ADDRESSCOORDINATES.DATECHANGED) "
End If
If OnlyPrimary Then
If FilterOutUnmodified Then
commandText &= "and ADDRESS.ISPRIMARY = 1 "
Else
commandText &= "where ADDRESS.ISPRIMARY = 1 "
End If
End If
commandText &= "order by ADDRESSCOORDINATES.DATEADDED"
command = theConn.CreateCommand()
command.CommandText = commandText
command.CommandTimeout = 20
command.CommandType = CommandType.Text
Dim mergeXMLMsg As String
mergeXMLMsg = "<ROOT>"
Try
Dim reader As SqlDataReader = command.ExecuteReader()
While reader.Read()
Dim ADDRESSCOORDINATESID As Guid = reader.GetGuid(0)
Dim ADDRESSCOORDINATESLATITUDE As Decimal = reader.GetDecimal(1)
Dim ADDRESSCOORDINATESLONGITUDE As Decimal = reader.GetDecimal(2)
' Make call to GetTimeZone function which uses an experimental Google maps time zone api
' The use of this service within this global change code is for educational purposes only and
' is not meant for production.
' This service has usage limits.
'https://developers.google.com/maps/documentation/timezone/
Dim TimeZoneSvcResponse As TimeZoneResponse = GetTimeZone(ADDRESSCOORDINATESLATITUDE.ToString, ADDRESSCOORDINATESLONGITUDE.ToString)
'Using result of calls to service and the lat and longitude, build up an xml message and pass
'to stored procedude which will update the custom USR_ADDRESSCOORDINATETIMEZONE table
' Example xml message passed to stored procedure.
'<ROOT>
'<TimeZoneResponse ADDRESSCOORDINATESID="AEC02AB8-430B-48B8-A32C-348F98C04C2B" StatusCode="1" StatusDesc="The request was successful." TimeZoneEntryID="B6CE5E5E-236F-437E-9A42-DFDB945B5286"/>
'<TimeZoneResponse ADDRESSCOORDINATESID="E457774D-97E4-4F0C-A35B-5020347C2EAD" StatusCode="1" StatusDesc="The request was successful." TimeZoneEntryID="B6CE5E5E-236F-437E-9A42-DFDB945B5286"/>
'<TimeZoneResponse ADDRESSCOORDINATESID="41BDE2D6-C9F7-4AB6-86DC-129A01F98920" StatusCode="1" StatusDesc="The request was successful." TimeZoneEntryID="B6CE5E5E-236F-437E-9A42-DFDB945B5286"/>
'<TimeZoneResponse ADDRESSCOORDINATESID="B9E1C919-811E-4B88-A39E-D8203FF08BAD" StatusCode="1" StatusDesc="The request was successful." TimeZoneEntryID="B6CE5E5E-236F-437E-9A42-DFDB945B5286"/>
'</ROOT>
mergeXMLMsg &= "<TimeZoneResponse ADDRESSCOORDINATESID=""" & ADDRESSCOORDINATESID.ToString & """ StatusCode=""" & TimeZoneSvcResponse.ResponseStatus & """ StatusDesc=""" & TimeZoneSvcResponse.ResponseStatusDesc.ToString & """ TimeZoneEntryID=""" & TimeZoneSvcResponse.TimeZoneEntryID.ToString & """/>"
End While
mergeXMLMsg &= "</ROOT>"
reader.Close()
command.Dispose()
Dim DBCommand As SqlCommand = New SqlCommand("USR_USP_ADDRESSCOORDINATETIMEZONE_GCPROCESS_UPDATE", theConn)
DBCommand.CommandType = CommandType.StoredProcedure
Dim xmlDocParm As SqlParameter = DBCommand.Parameters.Add("@doc", SqlDbType.NVarChar, -1)
xmlDocParm.Value = mergeXMLMsg
Dim changeAgentParm As SqlParameter = DBCommand.Parameters.Add("@CHANGEAGENTID", SqlDbType.UniqueIdentifier)
changeAgentParm.Value = changeAgentID
Dim myReader As SqlDataReader = DBCommand.ExecuteReader()
Dim changeType As String
Do While (myReader.Read())
changeType = myReader.GetString(0)
If changeType = "UPDATE" Then
numberUpdated = myReader.GetInt32(1)
ElseIf changeType = "INSERT" Then
numberInserted = myReader.GetInt32(1)
End If
Loop
myReader.Close()
DBCommand.Dispose()
theConn.Close()
Catch sqlEx As Exception
'Build the collection of expected exceptions
BuildExceptions("SQL Exception", sqlEx.Message)
MyBase.HandleSQLException(sqlEx, _expectedExceptions)
End Try
Dim result As New AppCatalog.AppGlobalChangeResult(numberInserted, numberUpdated, 0)
Return result
End Function
Private Function GetTimeZone(Latitude As String, Longitude As String) As TimeZoneResponse
Dim doc As XmlDocument
Dim mainnode As XmlNode
Dim Location As String = Latitude.Trim & "," & Longitude.Trim
Dim RequestStatus As String
' Create a new XmlDocument
doc = New XmlDocument()
'Make call to experimental Google maps time zone api
' The use of this service within this global change code is for educational purposes only and
' is not meant for production.
' This service has usage limits.
'See https://developers.google.com/maps/documentation/timezone/
' Load data
doc.Load("https://maps.googleapis.com/maps/api/timezone/xml?location=" & Location & "×tamp=0&sensor=false")
mainnode = doc.SelectSingleNode("TimeZoneResponse")
RequestStatus = mainnode.SelectSingleNode("status").InnerText
If mainnode Is Nothing Then
Return Nothing
ElseIf RequestStatus = "OK" Then
Dim timezonename As String = mainnode.SelectSingleNode("time_zone_name").InnerText
Dim TimeZoneGuid As System.Guid = GetTimeZoneEntryID(timezonename)
If TimeZoneGuid = System.Guid.Empty Then
Return New TimeZoneResponse(TimeZoneResponse.Status.TIMEZONEENTRY_FK_NOTFOUND, "indicates no row found in TIMEZONEENTRY table in DB for '" & timezonename & "'.", "", System.Guid.Empty)
Else
Return New TimeZoneResponse(TimeZoneResponse.Status.OK, "The request was successful.", timezonename, TimeZoneGuid)
End If
ElseIf RequestStatus = "INVALID_REQUEST" Then
Return New TimeZoneResponse(TimeZoneResponse.Status.INVALID_REQUEST, "The API request was malformed.", "", System.Guid.Empty)
ElseIf RequestStatus = "OVER_QUERY_LIMIT" Then
Return New TimeZoneResponse(TimeZoneResponse.Status.OVER_QUERY_LIMIT, "The requestor has exceeded API quota.", "", System.Guid.Empty)
ElseIf RequestStatus = "REQUEST_DENIED" Then
Return New TimeZoneResponse(TimeZoneResponse.Status.REQUEST_DENIED, "The API did not complete the request. Confirm that the request was sent over http instead of https.", "", System.Guid.Empty)
ElseIf RequestStatus = "UNKNOWN_ERROR" Then
Return New TimeZoneResponse(TimeZoneResponse.Status.UNKNOWN_ERROR, "indicates an unknown API error.", "", System.Guid.Empty)
ElseIf RequestStatus = "ZERO_RESULTS" Then
Return New TimeZoneResponse(TimeZoneResponse.Status.ZERO_RESULTS, "indicates that no time zone API data could be found for the specified position or time. Confirm that the request is for a location on land, and not over water.", "", System.Guid.Empty)
Else
Return New TimeZoneResponse(TimeZoneResponse.Status.GENERAL_ERROR, "indicates a general error.", "", System.Guid.Empty)
End If
End Function
Private Function GetTimeZoneEntryID(TimeZoneEntry As String) As System.Guid
Dim DBConn As SqlConnection = RequestContext.OpenAppDBConnection(RequestContext.ConnectionContext.BusinessProcess)
Dim DBCommand As SqlCommand = New SqlCommand("USR_USP_TIMEZONEENTRY_GETBYNAME", DBConn)
DBCommand.CommandType = CommandType.StoredProcedure
Dim myParm As SqlParameter = DBCommand.Parameters.Add("@TimeZoneName", SqlDbType.NVarChar, 400)
myParm.Value = TimeZoneEntry
Dim myReader As SqlDataReader
Dim retval As System.Guid
Try
myReader = DBCommand.ExecuteReader()
'EXEC(USR_USP_TIMEZONEENTRY_GETBYNAME) 'Eastern Standard Time'
Do While (myReader.Read())
If myReader.IsDBNull(0) Then
retval = System.Guid.Empty
Else
Console.WriteLine("{0}", myReader.GetGuid(0))
retval = myReader.GetGuid(0)
End If
Loop
myReader.Close()
Catch ex As Exception
Throw ex
Finally
DBCommand.Dispose()
DBConn.Close()
End Try
Return retval
End Function
Protected Class TimeZoneResponse
Public Enum Status As Integer
OK = 1
INVALID_REQUEST = 2
OVER_QUERY_LIMIT = 3
REQUEST_DENIED = 4
UNKNOWN_ERROR = 5
ZERO_RESULTS = 6
GENERAL_ERROR = 7
TIMEZONEENTRY_FK_NOTFOUND = 8
End Enum
Private _responseStatus As Status
Private _responseStatusDesc As String
Private _timeZone As String
Private _timeZoneEntryID As Guid
Sub New(ByVal responseStatus As Status, ByVal responseStatusDesc As String, ByVal timeZone As String, timeZoneEntryID As Guid)
_responseStatus = responseStatus
_responseStatusDesc = responseStatusDesc
_timeZone = timeZone
_timeZoneEntryID = timeZoneEntryID
End Sub
Public ReadOnly Property ResponseStatus As Status
Get
Return _responseStatus
End Get
End Property
Public ReadOnly Property ResponseStatusDesc As String
Get
Return _responseStatusDesc
End Get
End Property
Public ReadOnly Property TimeZone As String
Get
Return _timeZone
End Get
End Property
Public ReadOnly Property TimeZoneEntryID As Guid
Get
Return _timeZoneEntryID
End Get
End Property
End Class
Private Sub BuildExceptions(ByVal SearchText As String, ByVal CustomErrorMsg As String)
_expectedExceptions = New ExpectedDBExceptions
ReDim _expectedExceptions.CustomExceptions(0)
Dim invalidSelection As New CustomExceptionDescriptor
invalidSelection.SearchText = SearchText
invalidSelection.CustomErrorMsg = CustomErrorMsg
_expectedExceptions.CustomExceptions(0) = invalidSelection
End Sub
End Class