Das SALUT-Vorgehen

Es ist kein Geheimnis dass ich ein großer Standard-SQL Fan bin. Diese Sprache ist nicht nur ungemein mächtig ohne dabei aufgebläht zu sein sondern auch minimalistisch in der Syntax. Zwei Eigenschaften die ich als Kriterium dafür werte dass SQL eine ästhetische Sprache ist – wenn man sie denn richtig nutzt.

Eine Sache allerdings hat mich immer gestört und hat auch oft dazu geführt dass vermeintlich einfache Wege unnötig holprig wurden. Ich nenne es mal das SELECT INTO-Problem oder genauer das Problem der Unflexibilität des erwähnten Statements. Was ich damit genau meine ist, dass dieses Statement gerade in Bezug auf Client-Server Applikationen eine große Performanceeinsparung realisieren kann aber diesen Vorteil bei der Flexibilität wieder verliert. Denn in den meisten Fällen braucht man doch keine aggregierte Teilmenge sondern eine erweiterte Ergebnismenge. Also eher sowas wie
Teilmenge + LastUpdated/ SessionId statt Teilmenge aus zwei Tabellen.

In besten Fall wünsche ich mir da sowas wie SELECT 0 AS [Konstanter Wert], * INTO … Weil es nicht nur praktisch sondern auch schnell wäre. Das gibt es so aber leider nicht und damit ist der kleinste Umweg gefragt. Meine präfererierte Lösung ist bsw für ADO in VBA aber auch in Stored Procedures das „SALUT“-Vorgehen:
• SELECT INTO
• ALTER
• UPDATE Table
Also ein SELECT INTO mit einer (Teil-)Menge von aggregierten Daten in eine Ergebnistabelle, dann eine Tabellenerweiterung mit ALTER und einem zufügen von Spalten für die neu hinzuzufügenden Informationen. Beispielsweise einer SessionId. Danach noch ein Update auf die komplette Tabelle in der diese SessionId gesetzt wird und voila haben wir die Anforderung erfüllt.

Das ganze ist performant und auch mit recht wenig Zeilen Code machbar.

Änderungshistory in Microsoft SQL Datenbanken. Audit Trails mit Triggern. Nachtrag

Kürzlich wurde ich darauf hingewiesen, dass in der vorgestellten Lösung zwar Datenänderungen erfasst werden, jedoch keine Daten die gelöscht oder neu hinzugefügt werden.
Um aber auch NULL- oder Leer-Werte bei Hinzufügen oder Löschen in einem Feld mit in der Audittable auswerten und anzeigen zu können, braucht es nur noch eine explizite Typisierung.

D.h., wenn man in dem dynamischen SQL-Statement den Vergleich in der WHERE-Bedingung noch von NULL-Werten befreit und explizit als NVARCHAR typisiert, werden auch die gelöschten und hinzugefügten Werte in Feldern angezeigt:

SET @SQL = @SQL + 'NOT #Inserted.[' + @ColumnName + '] = #Deleted.[' + @ColumnName + ']

wird dann zu

SET @SQL = @SQL + 'NOT ISNULL(CONVERT(NVARCHAR(255), #Inserted.[' + @ColumnName + ']), '''') = ISNULL(CONVERT(NVARCHAR(255), #Deleted.[' + @ColumnName + ']), '''')'

Änderungshistory in Microsoft SQL Datenbanken. Audit Trails mit Triggern.

Einleitung
Zur Überwachung von Änderungen an Datensätzen in Microsoft SQL Datenbanken sind Trigger unersetzlich. Mit dem Zugriff auf die speziellen Tabellen „inserted“ für in einer Operation eingefügte Datensätze und „deleted“ für gelöschte hat man darüber hinaus alle Möglichkeiten offen um Datensatzveränderungen lokalisieren und loggen zu können.

Als einfachsten Weg um Datensatzänderungen nachvollziehbar zu machen können sogenannte Shadow-Tables angesehen werden. Diese Tabellen sind einfache 1:1-Kopien der Original-Tabellen, die um ein paar Informations-Spalten erweitert sind. Solche Spalten könnten zum Beispiel informative Werte wie „Änderungsdatum“ oder Ähnliches sein.

Eine 1:1-Kopie mit den Daten vor und nach der Änderung zu befüllen ist auch ohne Mühe machbar. Hier reicht ja einfach ein INSERT-Statement a la

INSERT INTO dbo.tbl_Test_Shadow 
SELECT 
*, GETDATE() AS AuditDate 
FROM 
inserted

Hier muss man vielleicht noch ein wenig erweitern um mit eventuellen Autowerten klarzukommen. Ein wenig Anpassung ist ja in den meisten Fällen nicht nur nötig sondern auch hilfreich.

Der Nachteil ist, dass bei dieser Alternative der Änderungshistorie schnell mal ein größeres INSERT-Statement zustande kommen kann und vor allem hat es das gravierende Problem, dass eine Auswertung nach geänderten Werten schwierig ist beziehungsweise nur mit einem hohen Gewicht realisierbar ist, da hier Datensätze miteinander verglichen werden müssten um herauszufinden welcher explizite Wert sich denn nun geändert hat.

Die Ánforderung
Sinnvoll wäre es also, wenn man diesen Trigger dahin ausbauen könnte, dass man direkt mitschreibt welcher Wert oder welche Werte sich bei einer Operation verändert haben oder betroffen waren.
Das ist mit viel Zeilen natürlich machbar mit direkten Vergleichen, aber das wird schon dann wieder komplizierter, wenn man bedenkt dass wir in SQL ja Mengenorientiert unterwegs sind und damit kann in der inserted oder der deleted-Table ja auch mehr als ein Datensatz stehen. Da wird ein Vergleich dann schon mal unübersichtlicher. Vor allem macht das spätestens dann keinen Spass mehr, wenn man so einen Trigger für mehr als fünf Tabellen einrichten muss, denn das ist viel Schreibarbeit und weit entfernt von einer optimalen Lösung für faule Entwickler.

Die Idee
Der im Folgenden vorgestellte Ansatz geht deswegen einen anderen – dynamischeren – Weg. Es soll eine Funktionalität entstehen, die Tabellenunabhängig ist, aber dennoch die Tabellenänderungen im Einzelnen per Trigger verfolgt. Da wir hier nun nicht mehr alles an Daten mitloggen sondern nur noch die Daten die sich tatsächlich geändert haben, spreche ich im Folgenden auch nicht mehr von Shadow-Tables sondern von Audit-Tables, da ein echter Changelog ensteht.
Ob man dabei den Weg wählt für jede zu überwachende Tabelle eine eigene Audit-Tabelle anzulegen oder ob man eine globale Audit-Tabelle nimmt, sei dem Leser dabei selber überlassen. Vorgestellt wird die Variante mit je einer Audit-Tabelle pro zu überwachender Tabelle.
Zudem werde ich hier nur den AFTER UPDATE-Trigger vorstellen da dieser der komplexeste Trigger ist und AFTER INSERT und AFTER DELETE sich hiervon ableiten.

Die Voraussetzungen
Unsere Voraussetzung ist, das jede zu überwachende Tabelle eine rowguid besitzt. Das ist aber kein Muss, wie wir unten sehen werden. Zudem hat – da wir jede Tabelle einzeln auditieren – jede Tabelle eine Audit-Table die immer die Form hat.

CREATE TABLE dbo.tbl_Test_Audit
(
	id INT PRIMARY KEY IDENTITY(1,1) NOT NULL,
	Operation NVARCHAR(20) NOT NULL,
	SourceUser NVARCHAR(255) NOT NULL,
	AuditDate DATETIME NOT NULL
	rowguid uniqueidentifier NOT NULL
	ColumnName NVARCHAR(255) NOT NULL,
	DeletedValue NVARCHAR(2000) NOT NULL,
	InsertedValue NVARCHAR(2000) NOT NULL
)

Wie die zu überwachenden Tabellen aussehen, ist uns erstmal relativ egal.

Die Umsetzung
Wir werden den Trigger hier Abschnittsweise durchgehen, da der Ablauf ziemlich Straight-Forward ist. Ein kompletter Download des Codes ist am Ende des Artikels möglich.

CREATE TRIGGER dbo.udt_Test_Audit_Update ON dbo_scm.tbl_Test
AFTER UPDATE
AS

Wir wollen dass unser Trigger für alle Tabellen mit möglichst wenig Anpassungen funktioniert, also holen wir uns aus den sysobjects/ syscolumns die Tabellendefinition der zu überwachenden Tabelle um diese dann Spalte für Spalte durchzugehen, zu vergleichen und im Falle von Ungleichheiten diese in der Audit-Table abzuspeichern.
Ich habe mich in dem Ansatz dafür entschieden die Spaltennamen zuerst aus der sysobjects/ syscolumns semikolon-separiert in einem String abzulegen um diese später Schritt für Schritt durchzugehen. Gegenüber anderen Alternativen spart man sich damit auf jeden Fall einen CURSOR, den ich bei Triggern prinzipiell vermeiden würde.
Hierfür brauchen wir die Variablen ColumnName, für die aktuelle Spalte und ColumnList für die semikolon-separierte Liste.

Zudem geben wir in diesem Beispiel den Tabellennamen der zu überwachenden Tabelle in der Variablen TableName explizit an.

	--alle columns von inserted durchgehen
	DECLARE @TableName AS NVARCHAR(255)
	SET @TableName = 'tbl_Test'

	DECLARE @ColumnName AS NVARCHAR(255)
	DECLARE @Hostname AS NVARCHAR(255)
	DECLARE @ColumnList AS NVARCHAR(2000)
	DECLARE @Column AS NVARCHAR(255)
	DECLARE @SQL AS NVARCHAR(2000)

Mit HOST_NAME() erhalten wir den Namen der SQL-Instanz die die Operation „zu verantworten“ hat. Das ist inbesondere in Replikationsszenarien interessant, da hier dann die Information verfügbar ist, welches Replikationsteilnehmer, also welcher Datenbank-User, die Änderung vorgenommen hat. Damit sind alle Änderungen und Datenbankoperationen direkt auf einen User zurückzuführen.
Der Grund warum ich hier den Hostnamen vorinitialisiere statt die HOST_NAME()-Function weiter unten einfach auzurufen ist, dass mit der unten definierten dynamischen SQL-Ausführung der Benutzerkontext wechselt und damit ist unter Umständen der HOST_NAME() innerhalb des Triggers ein anderer als im sp_executesql-Kontext. Zudem ist es nur guter Programmierstil wenn eine Funktion, die in einem Scope mehrmals aufgerufen wird über eine Variable gesteuert wird.

	SET @ColumnName = ''
	SET @Hostname = HOST_NAME()
	SET @ColumnList = ''
	SET @Column = ''

Wir überführen die Tabellen inserted und deleted in Temporäre Tabellen, da diese speziellen Tabellen dynamischem SQL nicht zur Verfügung stehen. Das hat – wie oben schon erwähnt – damit zu tun, das mit Aufruf von dynamischem SQL – sp_executesql – ein neuer Scope beginnt und damit der Aufruf aus einem neuen Kontext heraus geschieht, in dem diese Tabellen dann (nachvollziehbarerweise) nicht mehr erreichbar sind. Um das zu vermeiden und die Daten dennoch in einem dynamisch generierten SQL-Statement vorliegen zu haben, nutzen wir temporäre Tabellen für diese beiden Tabellen, die nur für die Dauer der Ausführung dieses Triggers existieren.

	SELECT * INTO #Inserted FROM inserted
	SELECT * INTO #Deleted FROM deleted

Jetzt brauchen wir noch die Tabellendefinition der überwachten Tabelle. Hierfür gehen wir die syscolumns (in Verbindung mit der sysobjects um auf eine bestimmte Tabelle zu filtern) durch und schreiben die Spaltennamen semikolon-separiert in eine Variable. Um die gleiche Spaltenordnung wie in den Tabellen zu erhalten, sortieren wir auch noch nach colid.

	--generate list of columns
	SELECT 
		@ColumnList = @ColumnList + c.name +';' 
	FROM 
		syscolumns c 
		INNER JOIN sysobjects o ON o.id = c.id 
	WHERE 
		o.name = @TableName
	ORDER BY 
		colid

Das letzte „;“ wird abgeschnitten.

	SET @ColumnList = SUBSTRING(@ColumnList, 1, DATALENGTH(@ColumnList)-1)

Dann gehen wir diese Liste Element für Element durch. Solange wie noch irgendwas in der Liste drin steht.

	--step through list of columns
	WHILE DATALENGTH(@ColumnList) > 0
		BEGIN

Wir extrahieren einen Spaltennamen, also den Eintrag in der ColumnList von Zeichen 1 bis zum ersten „;“. Weiter unten dann werden wir dieses Element aus der Liste löschen und so die Liste Schritt für Schritt durchgehen.

			SET @ColumnName = SUBSTRING(@ColumnList, 1, CHARINDEX(';', @ColumnList) - 1)

Dann bauen wir ein dynamischen SQL-String zusammen, der eventuelle Änderungen in die Audit-Tabel schreibt. Ich habe hier drei zusätzliche Informationen mitgeschrieben:
1. Operation: enthält Informationen darüber was für eine Operation einer Änderung zugrunde liegt, also „Update“, „Insert“ oder „Delete“
2. SourceUser: enthält den Usernamen wie oben erklärt, also faktisch HOST_NAME()
3. AuditDate ist einfach eine Zeitstempel der mit GETDATE() erfasst wird.

In dem aktuellen Fall war es so, dass die zu überwachenden Tabellen mit einer rowguid ausgestattet waren, was ich hierbei auch mal als Voraussetzung definiere, denn hierdurch lässt sich ein Vergleich einfacher und übersichtlicher erstellen. Man kann statt einer für alle Tabellen existenten rowguid natürlich auch id nehmen oder einen beliebigen anderen in allen Tabellen vorhandene Spaltennamen mit einer eindeutigen Kennung der Datensätze. Auch kann man die obige Abfrage der Tabellendefinition dahingehend erweitern, dass man sich die Primary-Keys ausgeben lässt. Dann kann man auch für alle Tabellen unterschiedliche Primärschlüsselspalten in das dynamische SQL „fummeln“.

Nun werden wir noch die temporären Tabellen #Inserted und #Deleted über die rowguid (oder einen anderen eindeutigen Schlüssel) miteinander JOINen und mit der WHERE-Bedingung nur die Datensätze rausfiltern in der die Inhalte der aktuell untersuchten Spalte unterschiedlich sind. Da wir ja alle Spalten der Ausgangstabelle durchgehen, untersuchen wir mit dieser Loop die komplette Tabelle Spaltenweise nach Änderungen ab. Wir sind mit dem in dem dynamischen SQL generierten Statement auch Mengenorientiert, das heißt dass wir selbstverständlich auch Änderungen korrekt mitschreiben die im Batch ausgeführt wurden und demzufolge mehrere Änderungen gleichzeitig pro Datensatz oder auch an mehreren Datensätzen enthalten.

			SET @SQL = ''
			SET @SQL = @SQL + 'INSERT INTO dbo_scm.' + @TableName + '_Audit '
			SET @SQL = @SQL + '( '
			SET @SQL = @SQL + 'Operation, '
			SET @SQL = @SQL + 'SourceUser, '
			SET @SQL = @SQL + 'AuditDate, '
			SET @SQL = @SQL + 'rowguid, '
			SET @SQL = @SQL + 'ColumnName, '
			SET @SQL = @SQL + 'DeletedValue, '
			SET @SQL = @SQL + 'InsertedValue '
			SET @SQL = @SQL + ') '
			SET @SQL = @SQL + 'SELECT '
			SET @SQL = @SQL + '''Update'', '
			SET @SQL = @SQL + '''' + @Hostname + ''', '
			SET @SQL = @SQL + 'GETDATE(), '
			SET @SQL = @SQL + '#Inserted.rowguid, '

Vielleicht fragen Sie sich, warum der ursprüngliche Wert und der neue Wert als NVARCHAR konvertiert wird. Wir legen die Änderungen als reine Strings ab und beachten in diesem Beispiel keine Datentypen. Das hat damit zu tun, dass eine Audit-Table Informationen bereitstellt und somit in diesem Fall keine explizite Typisierung braucht. Auch wenn das schnell herstellbar ist (über die sys-Tabellen kommt man auch an die Datentypen) habe ich darauf verzichtet. Die Konvertierung zu NVARCHAR wird daher gemacht um keine Typisierungsfehler heraufzubeschwören. Spätestens bei Typ money würde dieses Statement ohne die Konvertierung aussteigen.

			SET @SQL = @SQL + '''' + @ColumnName + ''', '
			SET @SQL = @SQL + 'CONVERT(NVARCHAR(255), #Deleted.[' + @ColumnName + ']), '
			SET @SQL = @SQL + 'CONVERT(NVARCHAR(255), #Inserted.[' + @ColumnName + ']) '
			SET @SQL = @SQL + 'FROM '
			SET @SQL = @SQL + '#Inserted '
			SET @SQL = @SQL + 'INNER JOIN #Deleted ON #Inserted.rowguid = #Deleted.rowguid '
			SET @SQL = @SQL + 'WHERE '
			SET @SQL = @SQL + 'NOT #Inserted.[' + @ColumnName + '] = #Deleted.[' + @ColumnName + ']'

Statement ausführen.

			EXEC sp_executesql @SQL

Und Spalte aus der Liste entfernen.

			SET @ColumnList = SUBSTRING(@ColumnList, CHARINDEX(';', @ColumnList) + 1, DATALENGTH(@ColumnList))
		END

Fertig. Ich finde es eine schöne Lösung und vor allem gibt es einem alle Informationen die man für einen Audit von Tabellen braucht. Es lässt sich natürlich auch beliebig erweitern. Das Grundgerüst ist aber meines Erachtens schon recht mächtig und damit sicherlich auch als Ausgangspunkt für weitere Anpassungen bestens geeignet.

Auswertung
Übrigens, auch hinsichtlich einer Auswertung oder Darstellung der Datensätze hat diese Art der Audit-Tabellen einen Vorteil. Wenn man bsw. ein Access-Frontend über eine ADP realisiert, dann kann man die Audit-Tabellen direkt und ohne viel Umschweife als Continuous Form anzeigen lassen und Access bringt schon alle Bordmittel mit um sämtliche Audit-Trails nachzugehen: Filtern nach User, Operation, Datum, Tabellen, Spalten, Werten uns so weiter.

Hier geht’s zum Download des Scripts.

SqlGenerator: Sql-Insert Statements generieren #1

Vor einiger Zeit habe ich ein kleines Script geschrieben, welches mir aus einer Tabelle einer Microsoft SQL Datenbank alle Datensätze als INSERT-Statements zurückliefert. Da der Microsoft SQL Server das nicht von alleine herstellt und man – alleine schon als Datenbank- und Softwareentwickler – recht häufig diese Funktionalität vermisst, musste ein kurzer und schmerzfreier Weg her.
Die Suche im Internet (man ist ja faul :)) brachte nicht das gewünschte Ergebnis. Zwar gibt es vergleichbare Tools aber eine schlanke, schnelle Lösung boten Sie nicht. Ich verstehe persönlich ja nicht, warum bei solchen kleinen Helferlein direkt eine Installation dabeisein muss und warum mir neue DLLs Windows verlangsamen müssen und Registry-Einträge die OS-Datenbank vollspammen müssen.
Also wollte ich ein kleines Programm welches man einfach starten kann, ohne Kosten, ohne Suchen, ohne Setup und ohne Pipapo. Sowas fand ich aber nicht. Also schnell selbergeschrieben.

Die Anwendung selber kann in der derzeit aktuellen (Beta) Version auf www.pracma.de unter “Download” heruntergeladen werden. Der komplette Quellcode der Anwendung inklusive der dazu nötigen Erklärungen gibts hier.

Das Prinzip ist – wie immer – eigentlich recht einfach. Man startet das Programm und bei Programmstart prüft die Anwendung nach auffindbaren SQL Instanzen. Diese werden gelistet und man kann sich per Login einwählen. Ein paar grundsätzliche Infos zu dem angemeldeten Server werden abgerufen und angezeigt (Version etc.) und die zur Verfügung stehenden Datenbank – natürlich abhängig vom angemeldeten User – aufgelistet. Nach Auswahl einer Datenbank kann man eine darin befindliche Tabelle auswählen und darin die zu skriptenden Felder. Hieraus wird dann ein SQL-INSERT ím Batch erzeugt. Das ganze funktioniert natürlich nicht nur mit lokalen sondern auch entfernten SQL-Servern.

1. SQL Server Instanzen abfragen
Die Abfrage von verfügbaren (also direkt erreichbaren) SQL Instanzen ist per .Net mit recht wenig Code machbar:

        Dim oSqlInstance As SqlDataSourceEnumerator = SqlDataSourceEnumerator.Instance
        Dim oDataTable As System.Data.DataTable = oSqlInstance.GetDataSources()
        Dim oRow As DataRow

        Dim oInstances As New ArrayList
        Dim sInstance As String
        oInstances = New ArrayList

        For Each oRow In oDataTable.Rows
            sInstance = oRow.Item("ServerName").ToString
            If Len(oRow.Item("InstanceName").ToString) > 0 Then
                sInstance &= "\" & oRow.Item("InstanceName").ToString
            End If

            oInstances.Add(sInstance)
        Next

was hier passiert ist, dass eine ArrayList definiert wird, die alle verfügbaren SQL-Instanzen aufnimmt. Diese Collection ist nicht nur einfach zu handeln sondern kann auch einfach als Datasource an bsw eine DropdownList gebunden werden.

        Me.cboSqlInstances.DataSource = oInstances

Gründe genug sich immer wieder für diese Art der internen Datenhaltung zu entscheiden.
Es wird im ersten Schritt eine Enumeration über alle SQL-Instanzen initialisiert, dann diese in einer DataTable gebunden und schließlich nacheinander durchgegangen um die Instanz-Namen in die ArrayList zu extrahieren.

2. Die Anmeldung
Nach der Eingabe von einem Login und Passwort (per Textfelder und mit der Unterscheidung ob man diese überhaupt ausfüllen muss [SQL Authentifizierung] oder nicht [Windows-Authentifizierung]) müssen wir per Code die Anmeldung ausführen. Dazu bedienen wir uns einem ConnectionStringBuilder und übergeben dem – abhängig von SQL-Authentifizierung oder Windows-Anmeldung – die Anmeldedaten:

        private _oSqlConnection  As SqlConnection
        Dim oConnectionStringBuilder As SqlConnectionStringBuilder

        oConnectionStringBuilder = New SqlConnectionStringBuilder

        With oConnectionStringBuilder
            .DataSource = Me.cboSqlInstances.Text
            If Me.cbIntegratedSecurity.Checked Then
                .IntegratedSecurity = True
            Else
                .IntegratedSecurity = False
                .UserID = Me.txtUser.Text
                .Password = Me.txtPassword.Text
            End If
        End With

        _oSqlConnection = New SqlConnection(oConnectionStringBuilder.ConnectionString)
        Try
            _oSqlConnection.Open()
        Catch ex As Exception
            MsgBox("Could not connect to SQL Server. Please review the login parameters.")
            Exit Sub
        End Try

Erhalten wir in dieser Routine keinen Fehler, dann sind wir bei dem Server angemeldet.

3. Grundlegende Informationen zu der Instanz abfragen
Wollen wir ein paar Infos darüber erhalten was das für ein Server ist und mit welcher Version wir es zu tun haben, lässt sich das nach Anmeldung mit dem Server per SQL gut erledigen. Als erstes brauchen wir die Version um die weiteren Schritte – abhängig davon – auszuführen.

Die Version des MSSQL Servers erhalten wir mit

SELECT SERVERPROPERTY('productversion') AS ProductVersion, SERVERPROPERTY ('productlevel') AS ProductLevel, SERVERPROPERTY ('edition') AS ProductEdition

Hieraus erhalten wir drei verschiedenen Informationen:
a) die erste Stelle des Resultstring von SERVERPROPERTY(‘productversion’) AS ProductVersion verrät uns die Hauptversion des Servers. Hier steht bsw. “8” für einen MSSQL Server 2000, eine “9” für 2005 und so weiter. Diese Property verrät uns den kompletten Versionsstand.
b) SERVERPROPERTY(‘productlevel’) AS ProductVersion, also Hinweise auf das installierte ServicePack
c) SERVERPROPERTY(‘edition’) AS ProductVersion verrät uns die Edition, also bsw eine “Developer Edition” oder ähnliches

Damit schließe ich mal den heutigen Part. In den folgenden Teilen zeigen wir alle verfügbaren Datenbanken an und wählen per Listbox eine aus um die Tabellen zu erhalten um daraus die Felder auszuwählen die wir als INSERT scripten wollen.
Und ganz zum Schluß gibts vielleicht auch eine Solution zum download…

ListView und der Doppelclick

Es mag trivial erscheinen – wenn man weiß wie es geht. Aber das ist ja bei vielen Dingen so.

Eine ListView in Visual Studio/ .Net zu benutzen ist eine recht alltägliche Sache. Grund genug dass man hier nicht mit unnötigen Wegen an das Ziel kommt, welches man vor Augen hat.
Will man in einem ListView per Doppelklick auf ein Item rausfinden, welches Item man denn da gerade geklickt hat, kann man in vielen Tutorials teils ziemlich komplizierte Lösungen finden. Da gibt es grundsätzlich zwei verschiedene Ansatzpunkte.
Zum einen guckt man welches Item denn gerade unter dem Cursor ist, wenn man Doppelklickt, oder aber man geht alle Items in der ListView durch und fragt nach Selected ab.
Beides sind Wege die nicht nur Code produzieren, sondern vor allem schwer gegen die persönliche Einschätzung stehen “dass das doch irgendwie einfacher gehen muss”.
Tut es auch.

Wir sind in Visual Studio 2008 und haben eine ListView im Formular. Mit Event ListView.DoubleClick erhalten wie den Funktionsrumpf

    Private Sub MyListView_DoubleClick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyListView.DoubleClick
        
    End Sub

um nun das entsprechende Item zu extrahieren reicht der Auftruf

    Private Sub CustomerFilesListView_DoubleClick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CustomerFilesListView.DoubleClick
        Debug.Print(CType(sender, ListView).SelectedItems(0).Text.ToString)
    End Sub

Will man nun beispielsweise noch eine Id zurückliefern, die hinter dem eigentlichen Anzeigetext steht, dann geht das am einfachsten mit einem Tag. Also einfach beim Anlegen der Items den Item.Tag mit der id (oder einem anderen Key) füllen und dann statt “Text” einfach den “Tag” abfragen.

    Private Sub CustomerFilesListView_DoubleClick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CustomerFilesListView.DoubleClick
        Debug.Print(CType(sender, ListView).SelectedItems(0).Tag.ToString)
    End Sub

Et Voila.

A Programmer’s Blog about VB, MSSQL and slightly different subjects