Wellenlinien

Es ist ein wenig paradox mit uns Menschen: Umso besser es uns geht, umso mehr stören uns oft die paar wenigen verbleibenden Ärgernisse.

So ähnlich kam es mir vor kürzlich beim Editieren im Visual-Studio-Editor: Es ist ok, dass ein Compiler-Fehler mit einer Wellenlinie unterstrichen wird, und schön, dass der Text des Fehlers als Tooltip angezeigt wird, wenn man den Mauszeiger an diese Stelle positioniert. Es kann aber vorkommen, dass man danach einen anderen Tooltip bräuchte, um den Fehler beheben zu können, nämlich denjenigen, der IntelliSense bzw. Quick Info normalerweise anzeigt, der aber jetzt wegen dem Fehler nicht mehr kommt.

Klassisches Beispiel: Es fehlt ein Parameter bei einem Methoden-Aufruf oder einem Constructor-Aufruf, der ganze Aufruf kriegt eine Wellenlinie, welche den Tooltip mit der Info über die Parameter blockiert.

Also die Frage: Gibt es eine schnelle Möglichkeit, die Wellenlinie loszuwerden, oder andersherum eine Möglichkeit, trotzdem an den Quick Info Tooltip zu kommen?

Resultat meiner Analyse des Problems: Bei einem so grossen System wie Visual Studio kann man sich kaum je richtig sicher sein, ob es etwas nicht gibt, aber auf jeden Fall habe ich keinen Befehl bzw. kein Feature gefunden, das vorgesehen ist, um Fehler-Wellenlinien direkt wegzunehmen.

Eine relativ schnelle Umgehungsmöglichkeit, die ich gefunden habe: Zeile mit dem Fehler selektieren, ^C, ^V. Der für den Editor „neue“ Text trägt keine Wellenlinie mehr.

Die andere Möglichkeit ist, den Cursor in den Namen der Methode oder des Constructors zu positionieren und dann den IntelliSense-Befehl Quick Info explizit auszulösen, entweder per Menu oder per Tastaturkürzel ^KI.

Ein kurzer Versuch, den IntelliSense-Tooltip per Macro zur Anzeige zu bringen, mit Hilfe von DTE.ExecuteCommand(„Edit.QuickInfo“), war nicht erfolgreich: Der Tooltip blitzt zwar kurz auf, verschwindet aber gleich wieder.

Veröffentlicht in Keine Kategorie. Schlagwörter: . Leave a Comment »

Die Millionen-DataGridView

Es gibt ein paar Aufgaben in der Welt der Programmierung, die seltsamerweise seit vielen Jahren auf eine elegante Lösung warten, obwohl es sich um wichtige Aufgaben handelt und sich immer wieder sehr intelligente Leute um eine Lösung bemühen. Die effiziente interaktive Anzeige eines grossen Datenbestandes, auf den man per SQL zugreift, scheint mir eine solche Aufgabe zu sein.

Konkret: Nehmen wird an, man hat eine Tabelle, die nicht zwangsläufig gross ist, aber doch gross werden kann, und will sie in einer DataGridView anzeigen. Das Ganze soll auch dann noch ordentlich funktionieren, wenn die Tabelle mal 1 Million Rows umfasst.

Auf meiner Suche nach einer Lösung für dieses Problem war ich zunächst optimistisch, weil die DataGridView vor einiger Zeit ein Feature namens VirtualMode dazugelernt hat. Vor diesem Feature hätte die View bei einem Einsatz von „DataBinding“ mal eben die Million Rows aus der Tabelle gelesen, was natürlich keine gute Idee ist. Mit VirtualMode kann man selbst die Daten häppchenweise laden, weil die View per CellValueNeeded jeweils bekanntgibt, welche Daten für die Anzeige gerade benötigt werden.

Das Lesen und Liefern der Daten aus einer SQL-Datenquelle in „Häppchen“ ist zwar nicht trivial, wie einschlägiger Demo-Code zeigt, aber machbar.

Was mir allerdings an dieser Lösung nicht gefallen wollte, war die Tatsache, dass die View auch im VirtualMode alle Zeilen und Zellen erzeugt, also im vorliegenden Beispiel 1 Million Zeilen. Es werden nur die Daten „virtualisiert“, das Grid ist aber immer in seiner ganzen monumentalen Grösse da und frisst Speicher.

Ich habe darum für die neue Programmierumgebung Triton der Megos, die gerade entsteht, und deren View auf grosse Tabellen einen Ansatz implementiert, den ich seltsamerweise so noch nirgendwie sonst gefunden habe. Ich will nicht behaupten, der Ansatz stelle die vielgesuchte Lösung für das hier diskutierte hartnäckige Problem dar, denn er hat auch seine Nachteile, aber ich denke, das Resultat ist ganz ordentlich:

Ich verwende eine DataGridView als reines Anzeige-Instrument: Kein DataBinding, kein VirtualMode, einfach eine Anzahl Zeilen, sagen wir als Beispiel einmal 30, die alle ganz sichtbar sind, so dass die View findet, vertikal gäbe es nichts zu rollen. Die View hat sozusagen keine Ahnung von den Daten, die sie anzeigt.

Ich lese Rows selbst aus der Tabelle mit Hilfe eines SqlDataReader. Um nach dem erstmaligen Öffnen und Anzeigen der View ihre 30 Zeilen mit den ersten 30 Rows aus den Daten zu füllen, führe ich ein SELECT * FROM Tabelle aus, hole mit Hilfe des Readers die ersten 30 Rows ab und fülle deren Daten in die entsprechenden Zellen.

Die SELECT-Anweisung ist schon richtig: Sie geht über die ganze Tabelle. Also kein SELECT TOP n oder ähnliches. Sorgfältige Tests haben gezeigt, dass eine solche Anweisung völlig harmlos ist, wenn die angegebene ORDER durch einen Index unterstützt wird. Weder wird der Datenbank-Server über Gebühr belastet, weil er versucht, die ganze Tabelle im Voraus zu lesen, noch werden Unmengen von Daten vorauseilend über das Netz gesendet.

Positioniert nun der Anwender die Selektion auf die letzte angezeigte Row und drückt Cursor Down, schiebe ich zunächst selbst die View „nach oben“, indem ich die Inhalte der Zellen umkopiere: Die Werte aus der 2. Zeile kommen in die 1. Zeile, die Werte aus der 3. Zeile kommen in die 2. Zeile, usw. Das mag Ihnen auf den ersten Blick vielleicht sehr seltsam vorkommen, denn natürlich schiebt im Normalfall die View völlig selbständig, aber hier kann sie es nicht, weil es eben keine 31. Zeile gibt, welche die View von sich aus in den sichtbaren Bereich hineinschieben könnte. Man muss es selber tun, was aber nicht weiter kompliziert ist.

Die Daten für die quasi „neu erscheinende“ Zeile beschaffe ich mir, indem ich per Reader noch eine Row mehr abhole. Der SqlDataReader hat ja – wie gesagt ohne schädliche Nebenwirkungen – brav auf mich gewartet, in einem „offenen“ Zustand, positioniert hinter Row 30.

Solange es also in der View abwärts geht, hole ich einfach weiter Rows ab mit Hilfe des Readers und schiebe Zelleninhalte selber nach oben.

Die Sache wird nur moderat komplizierter, wenn der Anwender etwas verlangt, was zu einer Repositionierung des Daten-Ausschnitts oder zumindest zu einem Richtungswechsel bei der Anzeige führt. Dann muss ich den Reader freigeben und einen neuen aufsetzen, in der Richtung, in der es bei der Anzeige gehen soll, rückwärts also mit Hilfe von DESC. Ich muss mir zudem die Schlüssel aller 30 in der View angezeigten Zeilen merken, damit ich bei einem Richtungswechsel die Schlüsselwerte für eine WHERE x < <key> bzw. WHERE x > <key>-Klausel zur Verfügung habe beim Öffnen des neuen Readers.

Was es auch noch braucht, ist eine vertikale Scrollbar, denn die View zeigt ja wie gesagt von sich aus keine solche an, weil alle ihre 30 Zeilen stets ganz sichtbar sind, und von mehr weiss sie nichts. Man muss sich also wiederum wie beim Umkopieren der Zellen-Werte um etwas kümmern, was die View sonst immer vollständig alleine macht, und alle 7 Scroll-Funktionen (Anfang, Ende, Zeile auf, Zeile ab, Seite auf, Seite ab, Positionieren) nachbauen.

Und zu guter Letzt muss man sogar beim Rollen die Selektion selber umpositionieren, denn vom Standpunkt der View aus wird nichts gerollt, weshalb sie auch die Selektion nicht bewegt.

Die resultierende Kombination DataGridView / VScrollBar / SqlDataReader braucht ein paar Seiten Code, um zu funktioneren, aber der Code ist über weite Strecken relativ einfach und durchsichtig. Auf irgendwelche komplizierten „Paging“-Konstrukte, stored procedures usw. kann man verzichten, und die Datenmenge in der Tabelle ist mehr oder weniger egal: Es dürfen auch 100 Millionen Records sein, wenn es sein muss.

Veröffentlicht in Keine Kategorie. Schlagwörter: , . 1 Comment »

Von Fehler zu Fehler

Ich bin diesbezüglich vielleicht ein bisschen eigen, aber ich mag das Fenster Error List in Visual Studio nicht besonders: Die Art und Weise, wie da während dem Tippen von Code andauernd die Fehler auftauchen und wieder verschwinden, stört mich irgendwie und lenkt mich ab.

Ich habe mir deshalb das folgende Makro programmiert, mit dessen Hilfe ich den Cursor von Fehler zu Fehler navigieren kann, wobei der Fehlertext gleich im Statusbereich angezeigt wird. Das ist nicht nur bequemer, als einen Fehler um den anderen in der Error List anzuklicken, sondern ermöglicht mir eben auch, die Error List im Normalfall geschlossen zu halten, womit sich das Problem mit der Ablenkung erledigt hat.

Ich habe hierzu ein Icon in Visual Studio konfiguriert, welches das Makro GoToNextError aufruft.

Eine kleine Schwierigkeit, die sich beim Implementieren des Makros ergab und die noch auf eine elegantere Lösung wartet als jetzt programmiert: Das Makro muss irgendwie merken, dass neu kompiliert wurde, um mit dem Durchgehen der Fehler wieder von vorne beginnen zu können, gesteuert durch die Variable currentError. (Natürlich könnte man ein kleines zweites Makro mit einem zweiten Icon bauen, um von vorne zu beginnen, aber wenn es automatisch geht, ist das bequemer.)

Hierzu wird eine Art „Hashcode“ über die Liste aller Fehler gebildet, und wenn diese ändert, nimmt man an, dass neu compiliert wurde.

Ich war usprünglich skeptisch, ob sich die Einarbeitung in die Visual-Studio-Makro-Programmierung wirklich lohnt, und war auch ein bisschen abgeschreckt durch Visual Basic statt C# als Makro-Sprache und die seltsamen „DTE“-Objekte, aber diese Skepsis hat sich gelegt. Und schliesslich gibt es auch bei einem so ausgereiften Werkzeug wie der Visual-Studio-IDE immer mal wieder etwas zu verbessern, vor allem, wenn die eigene Arbeitsweise etwas ausserhalb des „Mainstream“ liegt…

Option Strict Off
Option Explicit On
Imports System
Imports EnvDTE
Imports EnvDTE80
Imports EnvDTE90
Imports System.Diagnostics

Public Module BrMacros
  Private hashCode As Integer
  Private errList As ErrorList
  Private currentError As Integer

  Private Function ErrorListHashCode() As Integer
    ' Detektiere heuristischerweise eine NEUE Error-Liste nach Kompilation an Hand einer Art Hashcode
    Dim h As Integer
    Dim i As Long
    Dim errItem As ErrorItem
    For i = 1 To errList.ErrorItems.Count
      errItem = errList.ErrorItems.Item(i)
      If Not (errItem.FileName Is Nothing) Then
        h = h + errItem.Line + errItem.Column
      End If
    Next
    ErrorListHashCode = h
  End Function

  Private Function ErrorCount() As Integer
    Dim n As Integer
    Dim i As Long
    Dim errItem As ErrorItem
    n = 0
    For i = 1 To errList.ErrorItems.Count
      errItem = errList.ErrorItems.Item(i)
      If errItem.ErrorLevel = vsBuildErrorLevel.vsBuildErrorLevelHigh Then
        n = n + 1
      End If
    Next
    ErrorCount = n
  End Function

  Public Sub GoToNextError()
    errList = DTE.ToolWindows.ErrorList
    If ErrorCount() = 0 Then
      Exit Sub
    End If
    Dim h As Integer
    h = ErrorListHashCode()
    If h <> hashCode Then
      hashCode = h
      currentError = 1
    End If
    Dim numErrors = errList.ErrorItems.Count
    Dim errItem As ErrorItem
    Do
      If currentError > numErrors Then
        currentError = 1   ' Cycle
      End If
      errItem = errList.ErrorItems.Item(currentError)
      currentError = currentError + 1
      If errItem.ErrorLevel = vsBuildErrorLevel.vsBuildErrorLevelHigh Then
        errItem.Navigate()
        DTE.StatusBar.Text = errItem.Description
        Exit Sub
      End If
    Loop
  End Sub

End Module
Veröffentlicht in Keine Kategorie. Schlagwörter: , , , . Leave a Comment »

PowerShell

Die Windows PowerShell ist eine von Microsoft entwickelte Alternative zum Windows-Kommandozeilenprogramm cmd.exe und zum Windows Script Host. Eine Übersicht und Links zu Download-Seiten gibt z.B. folgender Wikipedia-Artikel.

Das Besondere an der PowerShell – und der Grund, warum ein Eintrag darüber durchaus in den Megos .NET-Weblog passt – ist die enge Integration mit dem .NET-Framework.

Die Resultate eines Kommandos in der PowerShell bestehen nicht wie in anderen Shells einfach aus dem Text, der im Konsolenfenster angezeigt oder mit Hilfe einer „Pipe“ an ein nächstes Kommando weitergereicht wird, sondern aus Objekten des .NET-Frameworks.

Da diese Objekte meist doch wieder zu Text verwandelt werden, da sich der Anwender ja keine Objekte direkt ansehen kann, fällt dies zunächst kaum auf, aber es ergeben sich erstaunliche Möglichkeiten bei der Formulierung von Kommandos.

Nur ein ganz kleines Beispiel: Folgendes Kommando

$env:path

liefert den Wert der Umgebungsvariablen path, aber eben nicht einfach als Text, sondern in Form eines Objekts vom Typ System.String. Das hat zur Folge, dass man den Wert mit allen Methoden dieser Klasse weiterverarbeiten kann. So liefert folgendes Kommando

$env:path.Split(";")

eine übersichtliche Liste mit 1 Directory-Namen aus dem Pfad pro Zeile. Und so geht es weiter: Diese Liste ist wiederum kein Text, sondern eben das, was String.Split zurückgibt, nämlich ein String-Array. Darum finden Sie mit

$env:path.Split(";").Count

heraus, wieviele Directories Ihr Suchpfad listet.

Das soll jetzt kein Wettbewerb werden im Stile von „Meine Shell ist besser als Deine Shell“, und natürlich formuliert ein alter Linux-Hase ohne Probleme ein Shell-Kommando, das mit Hilfe von sed und wc (oder was auch immer) dasselbe macht. Aber wenn Sie das hier lesen, kennen Sie wahrscheinlich schon das .NET-Framework und seine Klassen, und es könnte sein, dass Sie sich darum nach dem Nehmen einer ersten Hürde schnell in der PowerShell zurechtfinden und Sie plötzlich .NET nutzen können an einem Ort, wo Sie das bisher nicht konnten, nämlich auf einer Windows-Kommandozeile.

Veröffentlicht in Keine Kategorie. Leave a Comment »