Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write a WinMerge plugin in C# (or VB.NET)

I would like to write a WinMerge plugin to translate SQLite databases to text, so I can use WinMerge to compare databases.

I have written code in C# to do the conversion, but I can't seem to make it appear as a WinMerge plugin. But I'm not very familiar with writing COM-visible .NET objects.

I figured I must not have put in the right COM attributes (I just put ComVisible(true) on the class). However, I think VB.Net is supposed to do all that stuff for you, so I rewrote the class in VB.Net, using Project/Add New/COM class. However, it still does not appear in WinMerge as a loaded plugin.

In desperation, I tried looking at the VB DLL using DLL Export Viewer, but it did not show any exported functions. I'm obviously doing something wrong.

Here is the code in full:

<ComClass(WinMergeScript.ClassId, WinMergeScript.InterfaceId, WinMergeScript.EventsId)> _
Public Class WinMergeScript

#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 = "9b9bbe1c-7b20-4826-b12e-9062fc4549a0"
    Public Const InterfaceId As String = "b0f2aa59-b9d0-454a-8148-9715c83dbb71"
    Public Const EventsId As String = "8f4f9c82-6ba3-4c22-8814-995ca1050de6"
#End Region

    Dim _connection As SQLite.SQLiteConnection
    Dim _output As IO.TextWriter
    Dim _error As Long
    Dim _errordesc As String

    ' 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 ReadOnly Property PluginEvent() As String
        Get
            PluginEvent = "FILE_PACK_UNPACK"
        End Get
    End Property

    Public ReadOnly Property PluginDescription() As String
        Get
            PluginDescription = "Display Sqlite Databases in tab-delimited format"
        End Get
    End Property

    Public ReadOnly Property PluginFileFilters() As String
        Get
            PluginFileFilters = "\.db$"
        End Get
    End Property

    Public ReadOnly Property LastErrorNumber() As Long
        Get
            LastErrorNumber = _error
        End Get
    End Property

    Public ReadOnly Property LastErrorString() As String
        Get
            LastErrorString = _errordesc
        End Get
    End Property

    Public Function UnpackFile(ByVal fileSrc As String, ByVal fileDst As String, ByRef bChanged As Boolean, ByRef subcode As Long) As Boolean
        On Error GoTo CleanUp
        subcode = 1
        _error = 0
        _errordesc = ""
        Using connection As New SQLite.SQLiteConnection("Data Source=" + fileSrc + ";Version=3;DateTimeFormat=ISO8601;")
            _connection = connection
            Using output As New IO.StreamWriter(fileDst)
                _output = output
                For Each table As DataRow In Query("Select name from sqlite_master where type = 'table' order by name")
                    Dump(table(0).ToString())
                Next
            End Using
        End Using
        bChanged = True
        UnpackFile = True
        Exit Function
CleanUp:
        _error = Err().Number
        _errordesc = Err().Description
        bChanged = False
        UnpackFile = False
    End Function

    Sub Dump(ByVal tablename As String)
        Dim reader As IDataReader
        Using cmd As New SQLite.SQLiteCommand(_connection)
            cmd.CommandText = "Select * from """ + tablename + """"
            cmd.CommandType = CommandType.Text
            reader = cmd.ExecuteReader()
            Using reader
                _output.WriteLine("==== " + tablename + " ====")
                Dim data(reader.FieldCount) As String
                For i As Integer = 0 To reader.FieldCount - 1
                    data(i) = reader.GetName(i)
                Next
                Dump(data)
                While reader.Read()
                    For i As Integer = 0 To reader.FieldCount - 1
                        data(i) = reader.GetValue(i).ToString()
                    Next
                    Dump(data)
                End While
            End Using
        End Using
    End Sub

    Sub Dump(ByVal data() As String)
        _output.WriteLine(String.Join(vbTab, data))
    End Sub

    Function Query(ByVal sql As String) As DataRowCollection
        Dim cmd As SQLite.SQLiteCommand
        cmd = _connection.CreateCommand()
        Using cmd
            cmd.CommandText = sql
            Using da As New SQLite.SQLiteDataAdapter(cmd)
                Dim dt As New DataTable()
                da.Fill(dt)
                Query = dt.Rows
            End Using
        End Using
    End Function
End Class
like image 594
Nikki Locke Avatar asked Nov 12 '22 19:11

Nikki Locke


1 Answers

I've just now reviewed the actual code of WinMerge 2.14.0, and I think it only works for real COM DLLs (and OCXs), mainly because it works without expecting the COM objects being registered. It would need to be expanded to at least refer to .TLBs or adjusted to allow no path to be specified, but a more explicit ID than "WinMergeScript".


In a working VB.NET COM object I explicitly mention <ComVisible(True)> _ and the project has "Register for COM" checked in the Compile Properties.

I just created a HelloWorld function using your code, compiled it with vbc /t:library and registered it with regasm /codebase. This was successfully called using this simple vbscript (using wscript):

Option Explicit
Dim so
Set so = CreateObject("SO13035027.WinMergeScript")
MsgBox so.HelloWorld

And the whole code (I didn't reconfigure my system to use a later vbc so this was actually compiled to .NET 1.1):

Option Explicit On
Option Strict On
'Option Infer On

Imports Microsoft.VisualBasic

Namespace SO13035027
<ComClass(WinMergeScript.ClassId, WinMergeScript.InterfaceId, WinMergeScript.EventsId)> _
Public Class WinMergeScript

#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 =     "9b9bbe1c-7b20-4826-b12e-9062fc4549a2"
Public Const InterfaceId As String = "b0f2aa59-b9d0-454a-8148-9715c83dbb72"
Public Const EventsId As String =    "8f4f9c82-6ba3-4c22-8814-995ca1050de2"
#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 Function HelloWorld() As String
 Return "Hello, world!"
End Function

End Class
End Namespace
like image 144
Mark Hurd Avatar answered Nov 15 '22 08:11

Mark Hurd