Das Megos .NET-Weblog

31. Oktober 2009

Visual Studio Gallery

Gespeichert unter: Keine Kategorie — Schlagworte: — René Brunner @ 15:09

Unter dem Titel Visual Studio Gallery betreibt Microsoft ein Repository für verschiedenste Tools, Controls und Templates, mit deren Hilfe man Visual Studio erweitern kann. Vielleicht ja kalter Kaffee für Sie, aber wenn Sie sich da noch nie umgeschaut haben, könnte es sich lohnen, das nachzuholen.

Die Vielfalt der angebotenen Dinge täuscht ein wenig: Wenn man links in der Sektion Visual Studio Versions diejenige Version anklickt, die man selbst konkret in Betrieb hat und die Anzeige damit filtert, fällt einiges weg. Und wenn man gleich darunter in der Sektion Cost Categories mit der Wahl von Free alles verschwinden lässt, was Geld kostet oder eine limitierte Probeversion darstellt, bleiben z.B. von total fast 500 Tools nur noch etwa 160 freie Tools für VS2008 übrig. Aber das ist allemal noch eine interessante Menge.

Als ich selbst heute diese Liste der kostenlosen Tools für VS2008 durchging, bin ich beim HyperAddin hängengeblieben. Damit ist es unter anderem möglich, in Kommentaren Links zu schreiben, die zu irgendeinem Symbol des Projekts führen, denen man folgen kann wie den „eingebauten“ URL-Links mit Hilfe von Ctrl + Mausklick.

Konkret sieht das so aus:

  public class HyperAddinDemo {
    public void Method1() {
    }

    public void Method2() {
      // Hinweis: macht nicht dasselbe wie code:HyperAddinDemo.Method1
    }
  }

Links zu beliebig setzbaren Ankerpunkten in irgendeinem File des Projekts statt zu Symbolen sind ebenso möglich.

Ich denke, ich werde das in Betrieb nehmen und schauen, ob solche Links wirklich so nützlich sind, wie ich nach ersten Tests vermute.

Eine Frage steht bei solchen Erweiterungen natürlich immer im Raum: Was passiert, wenn ich auf eine neue Version von Visual Studio wechsle? Wie die Visual Studio Gallery selbst demonstriert, können Addins durchaus spezifisch sein für bestimmte Versionen von Visual Studio; damit besteht die Gefahr, dass ich z.B. beim bevorstehenden Schritt auf VS2010 das gerade liebgewonnene HyperAddin bereits wieder verliere, falls der Autor nicht mitzieht.

Es muss wohl jeder selbst für sich entscheiden, wie er mit diesem Risiko umgehen will und wie er Nutzen jetzt gegen möglichen Verlust bei einem Wechsel von Visual Studio in der Zukunft gegeneinander verrechnet. Konkret bei diesem Addin hat mir in dieser Hinsicht gefallen, dass der Sourcecode zur Verfügung steht und ich mich damit wahrscheinlich selbst durchschlagen könnte bei einem Problem auf einer neuen Plattform.

27. Juni 2009

Der Historical Debugger in VS2010

Gespeichert unter: Keine Kategorie — Schlagworte: , — René Brunner @ 13:21

Schon vor einiger Zeit hat Microsoft angekündigt, dass Visual Studio 2010 einen neuen, speziellen Debugger namens Historical Debugger enthalten wird. Man findet z.B. hier eine Ankündigung aus dem Oktober 2008:

Are you tired of constantly setting breakpoints to hone in on a pesky bug? How would you like to be able to step „back in time“ through your debugger? The Historical Debugger in Visual Studio Team System 2010 promises to revolutionize your debugging experience.

Ich sagte mir, na das ist doch mal was Interessantes: Irgendwie rückwärts durch ein Programm debuggen? Weil ich für Revolutionen immer zu haben bin und als „Systemprogrammierer“ einfach wissen muss, wie man einen Debugger implementiert, der so etwas leistet, bin ich der Sache heute auf den Grund gegangen und kann nun berichten, worum es geht.

Zunächst musste ich eine Hürde aus dem Weg räumen: Der Historical Debugger ist tatsächlich nur im Visual Studio Team System 2010 verfügbar, das „normale“ Visual Studio ohne TS, das ich mir zuerst testweise installiert hatte, enthält ihn nicht. Man findet die VS2010-TS-Preview als ISO-Download hier.

Der Witz an der Sache ist der folgende: Der Historical Debugger schreibt selbständig zu bestimmten Zeitpunkten während der Programmausführung ein Log mit Informationen über den Zustand des Programms zum jeweiligen Zeitpunkt. Der angepriesene „Schritt zurück in der Zeit“ besteht darin, nach einem Stopp des Programms aus der Debug History, d.h. der Liste der Zeitpunkte, einen Eintrag auszuwählen und sich im Debugger den Zustand des Programms anzuschauen, wie er damals war.

Das kann natürlich – im Prinzip – ganz nützlich sein: Hat man z.B. einen Breakpoint erreicht und stellt fest, dass eine Variable einen falschen Wert aufweist, muss man nun nicht mehr mit dem Debugging von vorne beginnen und sich vorsichtig an den Zeitpunkt heranpirschen, wo der falsche Wert zum ersten Mal auftaucht, sondern kann bequem und entspannt die Debug History verwenden, um den Zeitpunkt zu finden oder mindestens stark einzugrenzen, wo die Sache aus dem Ruder läuft.

Man kann in 2 Stufen wählen, wie die Zeitpunkte bestimmt werden sollen: Log schreiben entweder bei „wichtigen Systemereignissen“ oder bei solchen Ereignissen plus bei jedem Methoden-Aufruf. Man kann hier erahnen, dass die Sache Potential hat auszuufern, denn Methoden-Aufrufe können ja sehr zahlreich sein.

Ins Log geschrieben werden der Call Stack zum Zeitpunkt und Variablen-Werte zum Zeitpunkt, wobei man bei Objekten nur noch das Resultat von ToString sieht, aber keinen Zugriff mehr bekommt auf die einzelnen Komponenten.

Ein paar Screenshots hierzu findet man im Blog Dariusz quatscht hier.

Meiner Meinung nach ist das ganz hübsch, aber ob es für eine Revolution reicht, ist fraglich. Insbesondere die doch sehr eingeschränkte historische Sicht auf Variablen-Werte enttäuscht. Mir ist schon klar, dass es schlicht unmöglich ist, zu Hunderten von Zeitpunkten den gesamten Heap ins Log zu schreiben, damit ich auch nachträglich noch Objekte mit all ihren Details anschauen kann, aber hey, es hätte ja sein können, dass jemand mit Hilfe einer genialen Idee ein Log implementiert hat, das viele Details mit praktikablem Aufwand speichern kann, z.B. mit einer Heap-Differenzen-Methode…

Was haben andere so gemacht in Sachen Debugger mit Historie? Hier ist von einem experimentellen Java-Debugger die Rede, der vor ein paar Jahren implementiert wurde und der offenbar alle Variablen-Zuweisungen während eines Programmlaufs protokolliert, indem er sich in die Pseudocode-Ausführung der JVM einklinkt. Er nennt sich denn auch treffend Omniscient Debugger („allwissender Debugger“) und erlaubt offenbar die volle Sicht zu einem beliebigen Zeitpunkt.

In einer PDF-Datei, welche den Debugger genauer beschreibt, findet sich die Information, dass Apache Ant etwa 24 Millionen Variablen-Zuweisungen macht, während das Tool seine eigene Kompilation steuert. Das sind eine ganze Menge, aber nicht so viele, dass sie den Ansatz an sich gleich zu Fall bringen.

VMWare hat etwas im Angebot, das die Firma Replay Debugging nennt, beschrieben auf der Website hier. Unterstützt wird Debugging für C/C++, wie man im Artikel hier nachlesen kann. Das ganze funktioniert mit Hilfe der Virtual Machine, wo man natürlich wie bei der JVM eine zentrale Stelle hat, wo man sich einklinken kann, um zu protokollieren, was läuft. Das Debugging, sprich der Blick auf das Log findet statt in einem Visual Studio, das um ein VMWare-Plugin ergänzt wurde.

Anhand der Beschreibung vermute ich, dass komplett protokolliert wird und man so in einzelnen Schritten vorwärts und sogar rückwärts durch das Programm gehen kann. Die Suche nach dem Punkt im Programm, wo eine Datenstruktur „zerstört“ wird, finde man so: Zeitpunkt finden, wo sie falsch ist, dann Programm „rückwärts laufen lassen“ mit Daten-Breakpoint auf der Struktur und einfach auf den Break durch Veränderung warten.

Hier meldet sich natürlich wieder meine Neugier, die mich veranlasst hat, überhaupt erst den Historical Debugger von Microsoft anzuschauen und die sich nun fragt: Wie macht VMWare das? Ich vermute, dass die komplette Abfolge aller CPU-Befehle, welche das Programm ausführte, der zentrale Bestandteil des Logs ist, und dass das „Debugging“ darin besteht, mit Hilfe der Virtual Machine das Programm irgendwie entlang dieser Abfolge zu führen, vielleicht gestützt auf Komplett-Speicherabzüge in gewissen Zeitabständen.

Und ich vermute ebenfalls, dass die Sache derart auf die Ausführung direkter CPU-Anweisungen zentriert ist, dass die Ausdehnung des Prinzips auf C# mit seinem JIT-Compiler eine schwierige bis unmögliche Sache werden dürfte.

2. Mai 2009

Ein etwas nostalgischer Blick zurück

Gespeichert unter: Keine Kategorie — Schlagworte: , , — René Brunner @ 13:50

Als ich vor einiger Zeit ernsthaft mit Visual Studio und .NET/C# zu arbeiten begann, verglich ich diese Programmierumgebung ganz automatisch mit dem vor über 10 Jahren erschienenen VB6, mit dem ich vor allem durch eine privat damit gebaute Shareware sehr gut vertraut war.

Bei diesen beiden Systemen sind nur wenige Dinge exakt gleich, schon alleine wegen C# anstelle von BASIC als Sprache. Aber trotzdem hatte ich immer das Gefühl einer grossen Vertrautheit, den Eindruck einer weitgehenden Übereinstimmung bezüglich der grundlegenden Philosophie: Überall sah ich quasi den „Geist“ von VB6 durchschimmern.

Was mich aber noch viel mehr beeindruckte, und auch immer stärker, je genauer ich Visual Studio und .NET kennenlernte, war die folgende Geschichte: Im Gegensatz zum Nachfolger ist VB6 geradezu ein Winzling, weist aber trotz seiner bescheidenen Grösse (und trotz seines Alters) eine erstaunliche Menge der wichtigsten technologischen „Errungenschaften“ bereits auf.

Dinge wie Garbage Collection, IntelliSense im Editor, grafischer Form-Designer, hervorragendes Laufzeit-Debugging inklusive Watches und Immediate-Fenster, Code-Änderungen während das Programm läuft, mächtige String-Klasse, Kompilieren zu Native Code, Klassen inklusive Late Binding beim Aufruf von Objekt-Methoden, alles schon drin in diesem System von 1998, in einem Laufzeitsystem von nicht mal 3 MB und einer IDE von absolut lächerlichen 15 MB.

Natürlich könnte ich problemlos eine noch längere Liste von Eigenschaften aufzählen, die VB6 nicht hatte, ohne die man aber im Jahre 2009 zum Entwickeln von Software gar nicht anzutreten braucht.

Es geht mir um etwas Anderes: Der Vergleich VB6 versus Visual Studio 2010, C# und .NET 3.5 ist für mich mittlerweile eines der besten Beispiele, die ich persönlich kenne, um die 80%/20%-Regel zu illustrieren: Ja, das moderne System kann mehr als das gute alte VB6, aber man sehe sich mal an, wieviele der wirklich wichtigen, zentralen Eigenschaften VB6 bereits aufweist (die „80%“ in besagter Regel), erreicht mit vergleichsweise bescheidenem Aufwand (den „20%“). Oder umgekehrt: Ja, das moderne System ist überlegen, aber man sehe sich mal an, mit welch gigantischen Aufwand gewisse Verbesserungen erkauft werden mussten.

Natürlich kann ich als reiner Anwender von Visual Studio und .NET das Resultat geniessen, denn Microsoft trieb ja quasi für mich den grossen Aufwand, nicht ich. Und einen modernen Rechner bringen auch grosse IDEs und Laufzeitsysteme nicht ins Schwitzen.

Aber wenn ich selber etwas baue, tue ich gut daran, immer mal wieder an die 80%/20%-Regel zu denken und diese zu beherzigen. Mir persönlich fällt das nicht schwer, mit einem etwas nostalgischen Blick zurück zu VB6 dann und wann, zu dem ich z.B. Gelegenheit habe, wenn mein PC die Visual-Studio-IDE mit viel Festplatten-Aktivität in den Speicher wuchtet und diese riesige Maschine anwirft…

31. Januar 2009

IntelliSense und Erweiterungen von Klassen

Gespeichert unter: Keine Kategorie — Schlagworte: , — René Brunner @ 14:44

IntelliSense in Visual Studio ist eine wunderbare Sache, die das Programmieren ungemein erleichtert. Eigentlich sollte heutzutage niemand mehr in einer Umgebung programmieren müssen, wo kein in etwa gleichwertiges Feature zur Verfügung steht. Trotzdem sahen wir ein ernstzunehmendes Problem damit, als wir hier in der Megos ernsthaft damit begannen, unsere neues Programmierwerkzeug Triton in C# zu implementieren.

IntelliSense scheint seine Listen immer stur alphabetisch zu sortieren und immer alles aufzulisten, ohne Filtermöglichkeiten. Das kann dan zu einem Problem werden, wenn man Klassen erweitert, die bereits eine Menge Members haben, und dann in der IntelliSense-Liste seine „eigenen“ Members (d.h. diejenigen der Erweiterung) kaum mehr findet. Je nach Art der Klassen benötigt man beim Implementieren bevorzugt eben diese eigenen Members, was das Ganze dann etwas mühsam macht.

Unsere Überlegungen zeigten uns, dass sich die Sache zuspitzt, wenn man Klassen für den Datenbank-Zugriff generiert, wo für jede Kolonne in einer Tabelle eine Property definiert wird, die sich dann zu all den Members der Zugriffs-Basisklasse gesellt und unter diesen irgendwie unterzugehen droht, obwohl sie doch zum eigentlichen „Witz“ der generierten Klasse gehört.

Man kann das bereits bei Visual-Studio-eigenen typed datasets beobachten: Eine Tabellen-Zugriffs-Klasse wird von System.Data.TypedTableBase abgeleitet, einer Klasse mit geschätzten etwa 100 Members. Eine Kolonne TitelKurs wird zu einer Property namens TitelKursColumn und wird von IntelliSense unter T eingeordnet; eine Kolonne WährungsKurs wird zu WährungsKursColumn und taucht dann in der Liste unter W und damit an einem ganz anderen Ort auf. Dieses Verhalten verunmöglicht es praktisch, sich per IntelliSense einen schnellen Überblick über alle Kolonnen zu verschaffen – aber wo ausser da soll denn dieser Überblick herkommen?

Es war schnell klar, dass es bei Visual Studio und IntelliSense, so wie jetzt implementiert, nur ein einziges „Rädchen“ gibt, an dem man überhaupt drehen kann, wenn man dieses Problem irgendwie lösen will: Will man Members zusammen sehen in einer IntelliSense-Liste, so müssen sie eben entsprechend heissen, d.h. alle mit irgendeinem Präfix beginnen.

Sie werden als geneigter Leser vielleicht schon ahnen, dass dieser Blog-Eintrag hier geradewegs auf ein Dilemma zumarschiert: Von Namenspräfixen wird in .NET und C# eigentlich abgeraten; verwendet man sie trotzdem, stecken einen gewisse Leute schnell einmal in die Schublade „Ewiggestrige“, die reserviert ist für Leute, die von ihren alten Gewohnheiten nicht lassen können und auch nicht verstanden haben, dass man heutzutage nur noch „natürliche“ Namen vergibt, weil man – eben gerade durch Features moderner IDEs wie z.B. IntelliSense – nicht mehr darauf angewiesen ist, vom Namen eines Dings irgendwelche Meta-Information ablesen zu können.

Aber wie gesagt, hier geht es nicht darum, sondern um eine Lösung für ein kleines, aber sehr lästiges Problem mit IntelliSense.

Wir sahen das Ganze als Wahl zwischen zwei Übeln: 1) einen Teil der Namen mit Hilfe von Präfixen verunstalten, aber damit den Nutzen von IntelliSense aufwerten, oder 2) den Empfehlungen und der Mehrheit der C#-Programmierer folgen, aber sich mit Situationen abfinden, in denen IntelliSense wenig hilft, obwohl es gerade da sehr nützlich wäre.

Nach einigem Hin und Her entschieden wir uns für das Übel 1). Wir wählten z.B. für die Properties unserer eigenen Klassen in Triton den Einbuchstaben-Präfix z, weil eine Abklärung ergab, dass nur wenige .NET-Klassen bereits Properties haben, mit mit Z beginnen, und damit diese Properties nicht nur zusammen angezeigt werden in einer IntelliSense-Liste, sondern auch noch herausstechen.

Seit dieser Entscheidung haben wir eine rechte Menge Code gemäss diesen Präfix-Konventionen erzeugt, und es ist eine erste Beurteilung des Erreichten möglich. Der gewünschte Effekt bei IntelliSense hat sich eingestellt: Man behält so tatsächlich ohne Probleme den Überblick über die eigenen Dinge im Gegensatz zu den „fremden“ Dingen des Frameworks. Aber hübsch sieht es irgendwie nicht aus, und eine richtige Gewöhnung an unsere mit Präfixen dekorierten Namen will sich eigentlich auch nicht einstellen.

Für jemanden, der sich im .NET-Framework wirklich gut auskennt, gäbe es vielleicht eine Wahlmöglichkeit 3), welche das Dilemma umgeht: ein Add-In programmieren, welches neben der IntelliSense-Liste eine zweite, entsprechend eingeschränkte Liste implementiert. Aber ganz soweit sind wir in der Megos noch nicht…

10. November 2008

Forms in Visual Studio Macros

Gespeichert unter: Keine Kategorie — Schlagworte: , , — René Brunner @ 13:43

Kann man in Macros für Visual Studio Forms verwenden, oder sind diese damit überfordert? Wenn ein Macro eine Liste anzeigen will, kann es dazu eine System.Windows.Form mit einer ListBox oder sogar DataGridView verwenden, statt nur Zeilen von Text mit Hilfe einer MsgBox auszugeben oder das Output-Fenster zu verwenden?

Als ich mir kürzlich diese Frage stellte, brachte mich Google nicht weiter – ich konnte weder eine Bestätigung noch ein Dementi finden. Ich begann deshalb selbst zu experimentieren.

Erste Versuche waren nicht sehr hoffnungsvoll: Einen Designer für Forms konnte ich in der Macros IDE nicht finden. System.Windows.Forms zu importieren klappte, aber beim Aufruf der Methode Show für eine Form verhakte sich das System irgendwie. Zudem schienen alle Events in der Liste zu fehlen, die IntelliSense anzeigte für Form.

Bei einem zweiten Anlauf klärte sich allerdings einiges: ShowDialog anstelle von Show funktioniert problemlos, und betreffend Events wusste ich einfach zu wenig darüber, wie man diese genau in Visual Basic deklariert – irgendwie erstaunlich anders als in C#.

Einen Designer konnte ich auch im zweiten Anlauf nicht finden, aber das ist schliesslich kein grosses Hindernis, weil man die paar Controls, die man brauchen wird für eine Form in einem Macro, natürlich per Code produzieren kann.

Also kann ich bestätigen: Forms funktionieren in Macros für Visual Studio.

20. Oktober 2008

Bookmarks in Visual Studio

Gespeichert unter: Keine Kategorie — Schlagworte: — René Brunner @ 09:14

Es muss irgendwie so sein, dass die Anzahl Zeilen meiner C#-Source-Files eine entscheidende Grösse überschritten hat, denn ich begann mich vor kurzem dafür zu interessieren, ob man in Visual Studio Textstellen markieren und dann auf einfache Weise zu diesen Stellen zurückkehren kann. Die Antwort: Ja, natürlich kann man, mit Hilfe von Bookmarks.

Wie man in der Microsoft-Original-Doku zu diesem Thema hier nachlesen kann, gibt es ein eigenes Fenster für das Verwalten von Bookmarks, die Möglichkeit Bookmarks einen Namen zu geben und sogar einen Mechanismus, sie zu Gruppen zusammenzufassen. Eine Kurz-Anleitung inklusive Tastenkürzel findet man z.B. hier.

Ich versuchte daraufhin, eine Arbeitsweise mit Hilfe von Bookmarks zu unterstützen, die wir in der Megos seit vielen Jahren verwenden: Unsere Source-Files enthalten spezielle textuelle Markierungen, die jeweils am Beginn bestimmter Sektionen in den Files stehen, und unser eigener Programm-Editor kann bei Bedarf automatisch Bookmarks auf diese Markierungen setzen.

Auf diese Weise löst man – elegant, wie ich meine – das Problem, dass Bookmarks eine recht flüchtige Sache sind und zudem schlecht von einem Rechner zum nächsten transportiert werden können.

Ich dachte, etwas Ähnliches sei in Visual Studio sicher mit Hilfe eines recht einfachen Macros zu implementieren: Text von vorne bis hinten nach den fraglichen Markierungen absuchen, für jede gefundene Markierung ein Bookmark mit dem in der Markierung enthaltenen Namen definieren, fertig. Es sieht allerdings so aus, als könne man den Namen eines Bookmarks nicht „von aussen“ setzen, weil die entsprechende COM-Schnittstelle (EnvDTE.TextSelection) dies leider nicht anbietet. Das wartet irgendwie noch auf eine gute Umgehungslösung…

Zwei Dinge noch, die vielleicht interessant sind, wenn Sie Bookmarks ernsthafter einsetzen wollen: Gespeichert werden sie in .suo-Files (Solution User Options), die normalerweise nicht sichtbar sind, da sie das hidden-Attribut tragen. (Diese Files sind nicht geeignet, sie per Source Control zu verwalten, was die von mir beschriebene Bookmark-Auto-Regenerier-Funktion umso dringender macht.) Und hier wird beschrieben, wie man die Farbe der markierten Zeile ändern kann, so dass das Bookmark besser sichtbar wird – mit der Nebenwirkung zwar, dass man Breakpoints nicht mehr so einfach setzen kann.

6. Oktober 2008

Des Programmierers liebste Schrift

Gespeichert unter: Keine Kategorie — Schlagworte: — René Brunner @ 14:05

Wenn man als Programmierer mehrere Stunden pro Tag auf Source-Code blickt, spielt es natürlich eine grosse Rolle, in welchem Font dieser Code angezeigt wird, speziell dann, wenn man eine relativ kleine Schrift bevorzugt, um viel Code auf einmal sichtbar zu haben.

Die Megos hat vor über 15 Jahren sogar zwei Windows-Bitmap-Fonts namens EMBASSY Proportional und EMBASSY Monospaced selbst erstellt, weil damals für kleine Punkt-Grössen einfach nichts zu finden war, was wir als wirklich brauchbar bezeichnet hätten. Es ist denn auch EMBASSY Monospaced 8 Punkt, was ich heute für die C#-Programmierung im Visual Studio verwende.

Als ich kürzlich ausprobieren wollte, ob es nicht mit 9 Punkt noch etwas besser aussehen würde, und ich feststellen musste, dass die nächste mögliche Grösse 10 Punkt ist, habe ich mich im Internet etwas umgeschaut, was andere Leute heutzutage denn so verwenden und empfehlen.

Dabei war ich ziemlich überrascht über die Fülle von Artikeln, Blog-Einträgen und Forum-Postings zu diesem Thema. Auch heute scheinen einige Leute noch nicht den Font gefunden zu haben, den sie sich für ihren Code-Editor – auf welcher Plattform auch immer – eigentlich wünschen würden.

Das hat mich bewogen, hier etwas Werbung zu machen für unsere EMBASSY-Fonts, welche wir nach wie vor als „konkurrenzfähig“ ansehen, die wir als Freeware zur Verfügung stellen und die vielleicht etwas mehr Aufmerksamkeit als bisher durchaus vertragen könnten. Man findet sie auf der Megos-Website hier zum Download.

Und hier noch ein Ausschnitt eines Screenshots, EMBASSY Monospaced in 8 Punkt in Visual Studio:

30. August 2008

Eine Art neues Grid für den Designer

Gespeichert unter: Keine Kategorie — Schlagworte: , , , — René Brunner @ 15:08

Es ist im Designer des Visual Studio 2008 sehr viel einfacher geworden als früher etwa mit dem VB6-Designer, gutaussehende Forms zu produzieren, vor allem weil dank der Snaplines das Positionieren von Controls sehr viel einfacher geworden ist.

Dies ist auch gut so, wenn die Megos ihre Moneysoft-Applikationen, bisher entwickelt in einem einem eigenen System namens EMBASSY, in den nächsten Jahren in .NET neu schreiben will, denn wie man an folgendem, schon etwas älteren Screenshot sehen kann, wird es da oft Forms geben mit Dutzenden von Controls:

Valor Transaktionen

(Die vielen Controls in dieser Form sind nicht etwa schlechtes Design, sondern es gibt schlicht und einfach zu sogenannten Transaktionen in unserer Wertschriftenbuchhaltung Valor eine Menge Angaben, und die wollen alle erfasst werden. Für Kontrollen ist zudem ein schneller Überblick nötig, ohne dass man x Tabs wechseln muss.)

Als ich einmal eine solche Form testweise mit dem Visual-Studio-Designer nachbaute, merkte ich allerdings schnell, dass selbst die Unterstützung durch Snaplines noch nicht das ist, was ich eigentlich gerne hätte, um wirklich produktiv arbeiten zu können. Die Crux ist die: Forms, die praktisch nur aus vielen Labels, TextBoxes und GroupBoxes bestehen, sehen am besten aus (und die Controls haben am besten Platz), wenn man die Controls so positioniert und ihre Grössen so setzt, als würde man mit einem sehr speziellen Grid arbeiten – einem Grid, das mit einer „Zeilenhöhe“ und einer „Durchschnittszeichenbreite“ funktioniert.

Der Visual-Studio-Designer ist sehr modular aufgebaut und bietet eine Menge Möglichkeiten, Design-Prozesse zu beeinflussen, etwa mit Hilfe von ControlDesigner-Klassen, aber eine Möglichkeit, das Verhalten des Grids zu steuern oder zu übersteuern, scheint es nicht zu geben. Will man trotzdem mit einem speziellen Grid wie dem soeben beschriebenen arbeiten, muss man also zu einem Trick greifen.

Mein Trick ist der folgende: Controls haben eine protected virtual, also überdefinierbare Methode namens SetBoundsCore, die für das Setzen der Position und der Grösse eines Controls zuständig ist. Hier greife ich ein und runde Koordinaten auf mein Spezial-Grid, bevor ich sie an die Basis-Methode übergebe.

Das ist eine zwar drastische, aber einfache und effektive Methode, um x und y auf bestimmte Werte zu zwingen. Nichts und niemand im ganzen System kommt um die von mir etablierte Rundung der Koordinaten herum, auch der Designer nicht. Der Effekt im Designer ist der, dass man zwar ein solches Control nach wie vor mit der Maus frei herumziehen kann, aber sobald man es „absetzt“, kommt die geänderte SetBoundsCore-Methode zum Zug, und das Control hüpft sozusagen von sich aus auf die „richtige“ Position gemäss Grid. Das ist natürlich ungewohnt und am Anfang auch etwas gewöhnungsbedürftig, fühlte sich aber für mich schon nach kurzer Zeit sehr angenehm an.

Folgendes kleine Code-Fragment zeigt ein Label-Control, welches man nur gemäss einem Grid positionieren kann, dessen Zellen 30 Pixel breit und 15 Pixel hoch sind (vielleicht nicht unbedingt nützlich, zeigt aber den Effekt im Designer sehr schön):

  public class GriddedLabel : Label {
    protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified) {
      x = (x / 30) * 30;
      y = (y / 15) * 15;
      base.SetBoundsCore(x, y, width, height, specified);
    }
  }

25. Juli 2008

Wellenlinien

Gespeichert unter: Keine Kategorie — Schlagworte: — René Brunner @ 13:52

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.

2. April 2008

Formatierung von Kommentaren im C#

Gespeichert unter: Keine Kategorie — Schlagworte: , , , — Andres Rohr @ 10:34

Visual Studio 2008 bringt ja von Haus aus schon sehr viele Annehmlichkeiten für einen Software-Entwickler mit sich. Zum Beispiel liebe ich das Refactoring mit all seinen Möglichkeiten, vom Umbenennen von Identifiern bis zum automatischen Implementieren von get-set-Properties. Auch die eher unauffällige Funktion des Macro-Recording (Ctrl-Shift-R) hilft mir häufig dabei, stupide Umstrukturierungsarbeiten etwas angenehmer zu gestalten. Bisher konnte das Visual Studio fast alle produktionssteigernden Mittel anbieten, die wir auch in unserem selbstgebauten Vorgängersystem EMBASSY hatten. Und ein paar wenige Funktionen fehlten mir früher, die nun im Studio vorhanden sind. Beispielsweise die Auswahl von vertikalen Zeichenblöcken mithilfe von Alt-Markieren. Das kann manchmal dabei helfen, wenn man eine Reihe Identifier von Klein- auf Grossschrift ändern will.

Nur eines fing mich langsam an zu stören: Die häufigen Anpassungen von Kommentaren innerhalb der <summary>- und <remarks>-Tags führten zu einem ständigen Neu-Umbrechen der Absätze. Zuerst suchte ich auf dem Internet nach einem automatischen Umbrechen, fand aber nichts. Schon erstaunlich, dachte ich mir, bin ich der einzige, der das vermisst? Ich machte mich also dran, in unserem firmeneigenen VS-Package eine Funktion zu erstellen, die das Umbrechen automatisiert. Als Vorlage hatte ich ja bereits die sehr ausgefeilte Methode zur Verfügung, welche wir im EMBASSY-Editor selbst programmiert hatten und auch tagtäglich brauchten. Das Resultat ist im folgenden C#-Stück zu sehen:

	/// C#-Kommentare ausrichten
	/// Copyright (c) Megos AG 2008
	///
	/// Auszug aus dem Visual Studio Package 'Triton-IDE', welches die Megos für die Software-Entwicklung einsetzt.
	/// Die Lizenz ist Freeware. Jedermann darf den Sourcode verwenden, aber er darf nicht zu
	/// kommerziellen Zwecken vertrieben werden.
	///
 	///
 	/// Hole die grundlegenden Model-Viewer-Elemente und die aktuelle Caretposition des aktuellen Textdokuments.
 	/// Falls etwas schiefgeht, ist eines oder alle Resultatobjekte 'null'.
 	///
 	private void GetTextAndView(out IVsTextLines text, out IVsTextView view, out int caretLineNum, out int caretOffset) {
 		IVsUIShell uiShell;
 		IVsWindowFrame ppWindowFrame;
 		string pbstrData;
 		Object ppunk;
 		IVsTextManager textManager;

			text = null; view = null; caretLineNum = -1; caretOffset = -1;
 		uiShell = (IVsUIShell)GetService(typeof(SVsUIShell));
 		uiShell.GetCurrentBFNavigationItem(out ppWindowFrame, out pbstrData, out ppunk);
 		if (ppWindowFrame == null) return;
 		textManager = GetService(typeof(SVsTextManager)) as IVsTextManager;
 		if (textManager == null) return;

			// Mehrere Views können auftreten, wenn das Fenster gesplittet wäre
 		textManager.GetActiveView(1, null, out view);
 		view.GetBuffer(out text);
 		// Entgegen der Doku liefert 'GetCaretPos' hier den Offset ohne expandierte Tabs
 		view.GetCaretPos(out caretLineNum, out caretOffset);
 	}

		public const int ParaWidth = 119;  // Zeichen. Achtung: Kolonnenangabe im Studio ist 1-basiert
 	int tabSize = -1;

		///
 	/// Trenne eine Zeile auf in die Teile [Einrückung + Kommentarsymbol + Leerschläge + Inhalt].
 	/// Leerschläge am Endes des Textinhaltes sind abgeschnitten.
 	/// 'comment' gibt an, welches Kommentarsymbol ("///" oder "//") in 'indent' vorkommt falls nicht 'null'.
 	///
 	private void SplitLine(string line, out string indent, out string comment,
 												 out int contentIndent, out string content) {
 		int pos, contentStart;
 		string rest;

			indent = null; comment = null; content = null;
 		pos = 0;
 		while ((pos < line.Length) && ("\t ".IndexOf(line[pos]) != -1)) {
 			pos++;
 		}
 		indent = line.Substring(0, pos);
 		rest = line.Substring(pos);
 		if (rest.StartsWith("///")) {
 			comment = "///"; pos += 3;
 		} else if (rest.StartsWith("//")) {
 			comment = "//"; pos += 2;
 		}
 		contentStart = pos;
 		while ((pos < line.Length) && (line[pos] == ' ')) pos++;
 		content = line.Substring(pos).TrimEnd();
 		contentIndent = pos - contentStart;
 	}

		///
 	/// Stelle fest, ob die Zeile 'indent'/'comment'/'content' eine Trennzeile darstellt, dh. den auszurichtenden
 	/// Absatz begrenzt. 'paraComment' gibt an, welches Kommentarsymbol die Absatzzeilen haben müssen.
 	///
 	private bool IsSeparatorLine(string indent, string comment, string content, string paraComment) {
 		return (content == "") || (content == "/*") || content == "*/" ||
 						content.StartsWith("{") || content.StartsWith("}") ||
 						((comment == "///") && content.StartsWith("</")) ||
 						content.StartsWith("---") || content.StartsWith("===") ||
 						content.StartsWith("***") || content.StartsWith("*/") ||
 						((paraComment != null) && (comment != paraComment));
 	}

		///
 	/// Stelle fest, ob 'content' die Anfangszeile eines auszurichtenden Block begrenzt. Falls ja, gibt
 	/// 'startTag' an, welches Anfangstag vorliegt (XML oder auch "/*") und 'contentIndent' gibt die
 	/// Einrückung der Inhaltszeilen mit Leerschlägen relativ zum Starttag an.
 	///
 	private bool IsParaStart(string content, out string startTag, out int contentIndent) {
 		int i;

			startTag = ""; contentIndent = 0;
 		if (content.StartsWith("/*")) {
 			startTag = "/*";
 			contentIndent += 2;
 		} else if (content.StartsWith("- ")) {
 			contentIndent += 2;
 		} else if ((content == "") || content == "") {
 			startTag = content;
 			return true;
 		} else if ((content.Length > 0) && (Char.IsDigit(content[0]))) {
 			if ((content.Length > 1) && (Char.IsDigit(content[1]))) {
 				i = 2;
 			} else {
 				i = 1;
 			}
 			if ((content.Length > (i + 1)) && ((content[i] == ')') || (content[i] == '.')) && (content[i + 1] == ' ')) {
 				contentIndent += (i + 2);
 			} else {
 				return false;
 			}
 		} else if ((content.Length > 2) && Char.IsLetter(content[0]) && (content[1] == ')') && (content[2] == ' ')) {
 			contentIndent += 3;
 		} else {
 			return false;
 		}
 		while ((contentIndent < content.Length) && (content[contentIndent] == ' ')) contentIndent++;
 		return true;
 	}

		///
 	/// Hole die Zeile mit einer bestimmten Zeilennummer aus dem Text in Form eines normalen Strings
 	///
 	private bool GetLine(IVsTextLines text, int lineNum, out string line) {
 		LINEDATA[] lineData;
 		int result;

			line = null;
 		lineData = new LINEDATA[1];
 		result = text.GetLineData(lineNum, lineData, null);
 		if (result != VSConstants.S_OK) return false;
 		line = Marshal.PtrToStringAuto(lineData[0].pszText, lineData[0].iLength);
 		return true;
 	}

		///
 	/// Ermittle die Zeilenlänge, so wie sie im Viewer dargestellt wird. Tabulatoren setzen auf die Kolonne
 	/// mit Nummer, die ein Vielfaches von 2 ist.
 	///
 	private int ViewerLineLength(string line) {
 		int len;

			Debug.Assert(tabSize > 0, "TabSize muss eingestellt sein!");
 		len = 0;
 		foreach (char ch in line) {
 			if (ch == '\t') {
 				len = ((len / tabSize) + 1) * tabSize;
 			} else {
 				len++;
 			}
 		}
 		return len;
 	}

		///
    /// This function is the callback used to execute a command when the a menu item is clicked.
    /// See the Initialize method to see how the menu item is associated to this function using
    /// the OleMenuCommandService service and the MenuCommand class.
    ///
 	///
 	/// Richtet den Textinhalt unter dem Absatz des Carets linksbündig aus und bricht an Wortgrenzen um
 	///
    private void JustifyRemark(object sender, EventArgs e) {
 		DTE2 dte2;
 		IVsTextLines text;
 		IVsTextView view;
 		int caretLineNum, caretOffset, lineNum, paraIndent, contentIndent, nextIndent;
 		int firstLineNum, lastLineNum, lastLineLength;  // von Original
 		int secondIndent;  // Einrückung des Inhalts der Zeilen nach der ersten
 		string line, firstIndent, paraComment, comment, content, indent;
 		StringBuilder buf, addLine, output;
 		string startTag, nextTag, bufStr, testLine;
 		ArrayList breaks;
 		bool caretAtEnd, prevBroken, found0, found1, breakPosFound, partAppended;
 		int pos, startPos, pos0, pos1, partLen, prevLen;
 		int newLastLineNum, newLastLineLength;  // von neuem Absatz
 		IntPtr pText;

			dte2 = (DTE2)GetService(typeof(DTE));
 		tabSize = dte2.ActiveDocument.TabSize;

			GetTextAndView(out text, out view, out caretLineNum, out caretOffset);
 		if ((text == null) || (view == null)) return;

			// Zeile, auf dem der Befehl ausgelöst wird analysieren
 		if (!GetLine(text, caretLineNum, out line)) return;
 		SplitLine(line, out firstIndent, out paraComment, out paraIndent, out content);
 		if (IsSeparatorLine(firstIndent, paraComment, content, paraComment)) return;   // direkt auf Separator ausgelöst mache nichts

			// Anfang des Absatzes und Einrückung des Inhalts bestimmen
 		lineNum = caretLineNum;
 		for (; ; ) {
 			if (IsParaStart(content, out startTag, out secondIndent)) break;
 			if (!GetLine(text, lineNum - 1, out line)) break;
 			SplitLine(line, out indent, out comment, out contentIndent, out content);
 			if (IsSeparatorLine(indent, comment, content, paraComment)) break;
 			firstIndent = indent;
 			lineNum--;
 		}
 		if (startTag.StartsWith("<")) lineNum++;  // HTML-Tag
 		firstLineNum = lineNum;  // erste Zeile mit echtem Inhalt

			// Gesamten Text zu einem langen String zusammenhängen
 		buf = new StringBuilder(); breaks = new ArrayList();
 		lastLineNum = 0; lastLineLength = 0;
 		for (; ; ) {
 			if (!GetLine(text, lineNum, out line)) break;
 			SplitLine(line, out indent, out comment, out contentIndent, out content);

				// Auf einen einzigen Leerschlag reduzieren
 			do {
 				prevLen = content.Length;
 				content = content.Replace('\t', ' ').Replace("  ", " ");
 			} while (content.Length  firstLineNum) {
 				if (IsSeparatorLine(indent, comment, content, paraComment)) break;
 				if (IsParaStart(content, out nextTag, out nextIndent)) break;
 				if (buf.Length >= 1) {
 					if ((buf[buf.Length - 1] == '-') &&
 						 !(content.StartsWith("und ") || content.StartsWith("oder "))) {
 						// Klein-Klein zusammenhängen falls hinten nicht "und/oder"
 						if ((buf.Length >= 2) && Char.IsLower(buf[buf.Length - 2]) &&
 								(content.Length > 0) && Char.IsLower(content[0])) {
 							buf.Remove(buf.Length - 1, 1);
 							breaks.Add(buf.Length);
 						}
 					} else {
 						buf.Append(' ');
 					}
 				}
 			}

				// Eigenheit des Studio rückgängig: Innerhalb eines "/*"-Kommentars wird bei einem ENTER automatisch "* " eingefügt
 			if ((startTag == "/*") && content.StartsWith("* ")) {
 				content = content.Substring(2);
 			}

				buf.Append(content);
 			lastLineNum = lineNum;
 			lastLineLength = line.Length;

				lineNum++;
 			if ((startTag == "/*") && content.EndsWith("*/")) break;
 		}
 		caretAtEnd = (caretLineNum + 1 == lineNum) && (caretOffset >= lastLineLength);
 		bufStr = buf.ToString();

			// Generiere nun den Inhalt mit neuer Breite. Dazu jeweils ein weiteres
 		// unzerbrechliches Stück zur Zeile hinzu bis die Breite überschritten wird
 		output = new StringBuilder();
 		addLine = new StringBuilder(firstIndent + paraComment + new String(' ', paraIndent));
 		lineNum = 0; startPos = 0; prevBroken = false; partAppended = false;
 		newLastLineNum = 0; newLastLineLength = 0;
 		while (startPos < bufStr.Length) {
 			// Nächste Umbruchsposition ist ' ', '-' oder ein rausgenommenes '-'
 			pos0 = bufStr.IndexOf(' ', startPos);
 			found0 = pos0 != -1;
 			if (!found0) pos0 = bufStr.Length;

				pos1 = startPos;
 			for (; ; ) {
 				pos1 = bufStr.IndexOf('-', pos1);
 				found1 = pos1 != -1;
 				if (!found1) {
 					pos1 = bufStr.Length; break;
 				}
 				// Symbole "" nicht trennen
 				if ((pos1 > 0) && (bufStr[pos1-1] == '<')) {
 					// "<-"
 				} else if (((pos1+1) ')) {
 					// "->"
 				} else {
 					break;
 				}
 				pos1++;
 			}

				pos = Math.Min(pos0, pos1);
 			if (found0 || found1) pos++;

				// Schauen, ob es noch ein eliminiertes '-' davor gibt
 			breakPosFound = false;
 			foreach (int breakPos in breaks) {
 				if (breakPos >= pos) break;
 				if (breakPos > startPos) {
 					pos = breakPos; breakPosFound = true;
 					break;
 				}
 			}

				// Unzerbrechliches Stück bis und mit nächstem Trennzeichen. Achtung: Es könnte im Part wiederum
 			// Tabulatoren haben! Daher muss man jedes Mal die Viewerlänge von 0 her neu berechnen.
 			partLen = pos - startPos;
 			if ((partLen > 0) && (bufStr[startPos + partLen-1] == ' ')) {
 				// Blank zählt nicht für Test, ob am Ende der Zeile
 				testLine = addLine.ToString() + bufStr.Substring(startPos, partLen-1);
 			} else if (breakPosFound) {
 				// Trennstrich für Test dazurechnen
 				testLine = addLine.ToString() + bufStr.Substring(startPos, partLen) + "-";
 			} else {
 				testLine = addLine.ToString() + bufStr.Substring(startPos, partLen);
 			}
 			if ((ViewerLineLength(testLine) > ParaWidth) && (addLine.Length > 0)) {
 				// Stück geht nicht mehr auf Zeile, Zeilenumbruch generieren
 				if (prevBroken) addLine.Append('-');
 				output.Append(addLine.ToString().TrimEnd() + "\r\n");
 				addLine.Remove(0, addLine.Length);
 				addLine.Append(firstIndent + paraComment + new String(' ', paraIndent + secondIndent));
 				newLastLineNum = lineNum;
 				lineNum++;
 			}
 			addLine.Append(bufStr.Substring(startPos, partLen));
 			startPos = pos; prevBroken = breakPosFound; partAppended = true;
 			newLastLineLength = addLine.Length;
 		}
 		// gecachte Zeile noch raus
 		if (partAppended) {
 			output.Append(addLine.ToString() + "\r\n");
 			newLastLineNum = lineNum;
 		}

			// Ersetze den alten Inhalt durch den neuen
 		pText = Marshal.StringToCoTaskMemAuto(output.ToString());  // unmanaged text
 		try {
 			text.ReplaceLines(firstLineNum, 0, lastLineNum+1, 0, pText, output.Length, null);
 		} finally {
 			// in jedem Fall freigeben!
 			Marshal.FreeCoTaskMem(pText);
 		}

			// Caret am Ende soll wieder ans Ende, auch wenn Ende nach unten rechts geht, damit man
 		// direkt weiterschreiben kann
 		newLastLineNum += firstLineNum;
 		if (caretAtEnd) {
 			caretLineNum = newLastLineNum; caretOffset = newLastLineLength;
 		} else {
 			// Caret auf Absatz zurückholen
 			if (caretLineNum > newLastLineNum) {
 				caretLineNum = newLastLineNum;
 				if (caretOffset > newLastLineLength) {
 					caretOffset = newLastLineLength;
 				}
 			} else if (caretOffset > ParaWidth) {
 				caretOffset = ParaWidth;
 			}
 		}
 		view.SetSelection(caretLineNum, caretOffset, caretLineNum, caretOffset);
 		view.CenterColumns(caretLineNum, 0, 1);
 	}

Zur Funktionsweise: Die Funktion erkennt automatisch, um welche Art von Kommentar es sich handelt. Sie kann für fast jede Art von Kommentar im Sourcecode eingesetzt werden. Auch freistehende Absätze ohne Anfangs- und Endmarkierung werden erkannt. Der Umbruch erfolgt bei einem Leerschlag oder Minuszeichen. Minuszeichen, die durch eine andere Umbruchsposition überflüssig geworden sind, werden automatisch wieder entfernt, wobei auf spezielle Wörter wie „und“ und „oder“ geachtet wird. Prinzipiell sollte man einfach mal vertrauen haben in die Funktion und sie benutzen. Sollte ausnahmsweise mal was nicht korrekt umgewandelt werden, kann man die gesamte Umwandlung mit Ctrl-Z rückgängig machen (die gesamte Operation wird vom Studio als ein einziger Veränderungsschritt angeschaut).

Die Funktion fügt man am besten in ein VS Package ein und weist ihr ein Tastaturkürzel zu. Beispielsweise eignen sich F1 bis F12 oder Ctrl-0 bis Ctrl-9 gut, da sie leicht zu erreichen sind.

Leider stellt die hier verwendete Blog-Software den Code nicht exakt so dar, wie er ursprünglich war. Beispielsweise werden die summary-Tags einfach unterschlagen. Lustigerweise geht es in diesem Blog-Beitrag (meinem ersten), aber gerade um das Ausrichten von solchen Bemerkungen. Das trifft sich ja wieder mal gut…(;-)

Bloggen Sie auf WordPress.com.