| |
Creating a Merge Replication
Custom Conflict Resolver Using VB.NET
(Article by Doug Arterburn)
Introduction
Microsoft SQL Server 2000 includes a DLL called
the Microsoft SQL Replication Conflict Resolver Library, replrec.dll.
This library of methods enables your application to respond as changes are
applied during synchronization. A very nice article written by Andrea Fox from
Microsoft titled Creating Merge Replication Custom Conflict Resolvers Using
Visual Basic, documents these methods as well as the Merge Replication
Custom Conflict Resolver VB6 sample, SampleResolver, included on the SQL
Server 2000 Resource Kit. Andrea's MSDN article can be found
here.
This current article attempts to go one step
further and provide a complete code example written in VB.NET that readers can
adapt to their own business needs. I will be
covering just the steps required to create a VB.NET custom conflict resolver
DLL. I will not be covering the steps necessary to set up merge replication nor
how to register the custom DLL to SQL Server. The latter is partially covered in Andrea's
article and you may also find it useful to read up on the .NET utilities, tlbimp, ildasm, and ilasm; and the .NET Marshal class.
The Challenge
The .NET type library importer incorrectly
defines the buffer parameter of methods GetSourceColumnValue,
GetDestinationColumnValue, and SetColumn to be the address of an Object. For
COM interfaces, which is what replrec.dll appears to be, that implies a
COM-VARIANT parameter passing mechanism, however this is not what replrec.dll
passes back. Replrec.dll is simply passing back the address of a buffer
containing the column value. Furthermore, I was unable to find any
documentation on the format of the data being passed back. The simple data
types are straight forward, but more complex data types required some detective
work. DATETIME, for example, is being passed back as an 8 element array of 16
bit integers: year, month, day, hour, minute, second, ?, ? (I'm still not sure
what the last two integers represent - tenths of seconds?, hundredths of
seconds?, timezone?). Herein lies the challenge.
The Solution - Summary
The solution is to create a type library for .NET
that will force a parameter passing protocol that we can live with for those
routines which pass column values via a buffer. I chose the simplest parameter
passing mechanism, 32-bit integer by value, so I can be assured that I
know how .NET will pass the parameter. (I could have done something much more
interesting, perhaps created a custom COM wrapper, but I always say the most
effective programmers are the laziest :)) I suspect that some of you are
already thinking to yourself, "He must mean by reference, not by value!"
but I will be using the .NET Marshal class to allocate a buffer. The Marshal
class will give me the address of a buffer which I will store in a 32-bit
integer variable (side note for VB6 programmers: .NET integer type is 32 bits,
long type is 64 bits). So, if I pass this integer variable that contains an
address by value, I am, in effect, passing the buffer by reference.
Makes me dizzy just thinking about it! The final step is to create procedures
to parse each SQL data type that I am interested in. For this example, I have
created a procedure to parse string values and a procedure to parse date values
(see Appendix A for complete source listing).
The Solution - Nitty-Gritty Details
- Open a Visual Studio .NET 2003 Command Prompt
- tlbimp "c:\Program Files\Microsoft SQL
Server\80\COM\replrec.dll" /OUT:SQLResolver_import.dll
- ildasm "SQLResolver_import.dll" /OUT=SQLResolver.il
- Change line "[out] object& marshal( struct)
pvBuffer" for the methods GetSourceColumnValue, GetDestinationColumnValue, and
SetColumn to "[out] int32 pvBuffer"
- ilasm SQLResolver.il /OUT=SQLResolver.dll /dll
- Create a new .NET Windows Control Library
project
- Remove the wizard generated control
- Use "Add New Item..." to add a new COM class
- Add reference to SQLResolver.dll created in
step #5
About the Sample
This sample is relatively simple. At Precision
Development, Inc., we routinely maintain ModifyUser and ModifyDate columns to
track who made the last change to the record and when the last change was made.
For every column conflict, the columns from the row with the latest ModifyDate
wins. The conflict is also logged with a message to indicate whose changes were
lost, the ModifyUser of the losing row.
About the Author
Doug Arterburn has been programming for 29 years
and married for 29 years; and has yet to determine whether these two milestones
are in any way related. His most challenging project is nearing completion
after 26 years, that of advancing a daughter and son from birth to college
graduation. He still writes programs furiously under the guise of Senior
Consultant at Precision Development, Inc.,
www.precisdev.com, a company specializing in creating technology systems
customized to meet the needs of small and medium-sized businesses across the
central United States.
Appendix A - The Source Code
Imports System.Text
Imports SQLResolver
Imports System.Runtime.InteropServices
Imports System.Runtime.InteropServices.MarshalAsAttribute
Imports SQLResolver.REPOLE_CHANGE_TYPE
Imports SQLResolver.REPOLE_COLSTATUS_TYPE
<ComClass(ComClass1.ClassId, ComClass1.InterfaceId,
ComClass1.EventsId)> _
Public Class ComClass1
Implements SQLResolver.IVBCustomResolver
Private Const MAX_BUFFER_SIZE As Integer = 1048576
Private Const MAX_NAME_LENGTH As Integer = 128
#Region "COM GUIDs"
' These GUIDs provide the COM identity for this class
' and its COM interfaces. If you change them, existing
' clients will no longer be able to access the class.
Public Const ClassId As String = "825818F7-3531-4524-8B07-72343EFDC8AB"
Public Const InterfaceId As String = "CECFBB8F-584F-4733-9373-B69AFA6F117F"
Public Const EventsId As String = "5D55BA40-A438-4FD4-BA8B-05095DC89948"
#End Region
' A creatable COM class must have a Public Sub New()
' with no parameters, otherwise, the class will not be
' registered in the COM registry and cannot be created
' via CreateObject.
Public Sub New()
MyBase.New()
End Sub
Public Sub GetHandledStates(ByRef ResolverBm As Integer)
Implements IVBCustomResolver.GetHandledStates
ResolverBm = REPOLEUpdateConflicts
End Sub
Public Sub Reconcile(ByVal pRowChange As IReplRowChange,
ByVal dwFlags As Integer, ByVal pvReserved As IReplRowChange) Implements
IVBCustomResolver.Reconcile
Dim cntColumns As Integer
Dim intColumn As Integer
Dim strColumnName As String
Dim strLogMessage As String
Dim WinningData As Object
Dim blnSourceIsWinner As Boolean
Dim ColStatus As SQLResolver.REPOLE_COLSTATUS_TYPE
Dim intBufferLenActual As Integer
Dim intBufferLen As Integer
Dim strDestinationDateTime As String
Dim strSourceDateTime As String
Dim strDestinationUser As String
Dim strSourceUser As String
Dim myBuffer As IntPtr = Marshal.AllocHGlobal(MAX_BUFFER_SIZE)
Dim strMsg As String
Call pRowChange.GetNumColumns(cntColumns)
For intColumn = 1 To cntColumns
strColumnName = " ".PadRight(MAX_NAME_LENGTH)
' strColumnName.PadRight(OSQL_SYSNAME_SET, Chr(vbNull))
Call pRowChange.GetColumnName(intColumn, strColumnName, MAX_NAME_LENGTH)
' strColumnName.TrimEnd(Chr(vbNull))
strColumnName = strColumnName.TrimEnd()
If (String.Compare(strColumnName, "ModifyDate", True) = 0) Then
pRowChange.GetDestinationColumnValue(intColumn, myBuffer.ToInt32,
MAX_BUFFER_SIZE, intBufferLenActual)
strDestinationDateTime = ConvertBufferToDateString(myBuffer)
pRowChange.GetSourceColumnValue(intColumn, myBuffer.ToInt32,
MAX_BUFFER_SIZE, intBufferLenActual)
strSourceDateTime = ConvertBufferToDateString(myBuffer)
If strSourceDateTime > strDestinationDateTime Then
blnSourceIsWinner = True
Else
blnSourceIsWinner = False
End If
End If
If (String.Compare(strColumnName, "ModifyUser", True) = 0) Then
pRowChange.GetDestinationColumnValue(intColumn, myBuffer.ToInt32,
MAX_BUFFER_SIZE, intBufferLenActual)
strDestinationUser = ConvertBufferToString(myBuffer, intBufferLenActual)
pRowChange.GetSourceColumnValue(intColumn, myBuffer.ToInt32,
MAX_BUFFER_SIZE, intBufferLenActual)
strSourceUser = ConvertBufferToString(myBuffer, intBufferLenActual)
End If
Next intColumn
For intColumn = 1 To cntColumns
'Get the column status of each column
pRowChange.GetColumnStatus(intColumn, ColStatus)
' If the column has been updated at both the Publisher and Subscriber
If (ColStatus = REPOLEColumn_UpdatedWithConflict) Then
If blnSourceIsWinner Then
pRowChange.CopyColumnFromSource(intColumn)
End If
' For columns that have been updated without any changes, copy column values
from source
ElseIf (ColStatus = REPOLEColumn_UpdatedNoConflict) Then
pRowChange.CopyColumnFromSource(intColumn)
' For columns that have not been updated - do nothing.
ElseIf (ColStatus = REPOLEColumn_NotUpdated) Then
End If
Next intColumn
' Log conflict and call the UpdateRow method to commit all the column value
changes.
'
If strDestinationDateTime.Length > 0 And strDestinationUser.Length > 0 Then
If blnSourceIsWinner Then
strMsg = "Losing update(s) made by " & strDestinationUser
Else
strMsg = "Losing update(s) made by " & strSourceUser
End If
End If
pRowChange.LogConflict(REPOLE_BOOL.REPOLEBool_TRUE,
REPOLE_CONFLICT_TYPE.REPOLEConflict_ColumnUpdateConflict,
REPOLE_BOOL.REPOLEBool_FALSE, strMsg, REPOLE_BOOL.REPOLEBool_FALSE)
Call pRowChange.UpdateRow()
Marshal.FreeHGlobal(myBuffer)
End Sub
Private Function ConvertBufferToDateString(ByVal p As
IntPtr) As String
Dim s As String = String.Empty
Dim i(7) As Short
Dim j As Integer
Marshal.Copy(p, i, 0, i.Length)
s = i(0).ToString '4 digit Year
For j = 1 To i.GetUpperBound(0)
s &= i(j).ToString.PadLeft(2, "0"c)
Next
ConvertBufferToDateString = s
End Function
Private Function ConvertBufferToString(ByVal p As IntPtr,
ByVal intBufferLenActual As Integer) As String
Dim i As Integer
Dim s As String = String.Empty
For i = 0 To intBufferLenActual - 1
s &= Chr(Marshal.ReadByte(p, i))
Next
ConvertBufferToString = s
End Function
End Class
|
|