Files via lokale HTML-Links öffnen

Visual Studio ist – unter vielem anderen – ein ganz passabler HTML-Editor, wenn man sich damit anfreunden kann, direkt in HTML zu arbeiten. Es muss ja nicht immer gleich eine Riesending wie Microsoft Expression Web oder Dreamweaver sein für Modifikationen an ein paar Seiten.

Eine Funktion habe ich allerdings so vermisst, dass ich sie per Makro nachgerüstet habe: eine Funktion, um einem lokalen HTML-Link zu folgen und das referenzierte File zu öffnen. Hat man z.B. im HTML-Text das folgende:

<a href=“sample.html“>

sollte es auf ganz einfache Weise möglich sein, dem Link zu folgen und das File sample.html in Visual Studio zu öffnen, auch wenn man kein Projekt hat, das dieses File enthält, weil man einfach mit einer Directory voller HTML-Files arbeitet.

Mit dem folgenden Makro braucht man nur den Cursor in den Filenamen zu positionieren und dann das Makro zur Ausführung zu bringen. (Am besten konfiguriert man natürlich eine Tastenkombination oder mindestens ein Icon in einer Toolbar hierzu.)

  Public Sub OpenFileAtCursor()
    Dim activeDoc As Document
    Dim sel As TextSelection
    Dim line As String
    Dim fileName As String
    Dim basePath As String
    Dim cursorColumn As Integer
    Dim startPos As Integer
    Dim endPos As Integer
    activeDoc = DTE.ActiveDocument
    sel = activeDoc.Selection
    cursorColumn = sel.AnchorPoint.LineCharOffset
    sel.SelectLine()
    line = sel.Text
    sel.Collapse()
    startPos = InStrRev(line, """", cursorColumn)
    endPos = InStr(cursorColumn, line, """")
    fileName = Mid(line, startPos + 1, endPos - startPos - 1)
    fileName = Replace(fileName, "/", "\")
    If Left(fileName, 1)  "\" Then
      basePath = Path.GetDirectoryName(activeDoc.FullName)
      fileName = basePath + "\" + fileName
    End If
    If File.Exists(fileName) Then
      DTE.ExecuteCommand("File.OpenFile", fileName)
    Else
      DTE.StatusBar.Text = "File """ + fileName + """ not found"
    End If
  End Sub
Advertisements
Veröffentlicht in Keine Kategorie. Schlagwörter: , , . Leave a Comment »

Eine Art neues Grid für den Designer

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);
    }
  }
Veröffentlicht in Keine Kategorie. Schlagwörter: , , , . Leave a 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 »

Flexible Delegates

In den meisten C#-Programmen dürften Delegates nur vorkommen, um sie einem Event anzuhängen. Es könnte deshalb der Eindruck entstehen, Delegates seien unwichtig und ausser im Zusammenhang mit Events für sich alleine nicht zu gebrauchen. Einige Tests, die ich kürzlich durchgeführt habe, zeigen aber ein anderes Bild.

Anlass war folgende Frage: Hängen an einem Event mehrere Handler, hat man so gut wie keine Kontrolle darüber, in welcher Reihenfolge diese aufgerufen werden. Was tut man, wenn das ein Problem darstellt, d.h. wenn man die Kontrolle über die Aufruf-Reihenfolge benötigt?

Eine mögliche Antwort ist die folgende: Man lässt Events links liegen und arbeitet direkt mit Delegates.

Delegates haben gegenüber anderen .NET-Objekten eine „magische“ Eigenschaft: Man kann sie aufrufen, d.h. man kann mit ihrer Hilfe Methoden bestimmter Objekte ausführen. Sie sind aber trotzdem ganz normale Objekte, und man kann mit ihnen alles machen, was man mit anderen Objekten auch machen kann, inklusive Verwendung für Template-basierte Klassen wie List<T>. Wie untenstehender Demo-Code zeigt, ist es deshalb kein Problem, eine Art Event in Form einer Liste selbst mit sehr wenig Aufwand nachzubauen, einer Liste, wo man dann eben selbst beliebig bestimmen kann, in welcher Reihenfolge welcher Delegate aufgerufen werden soll.

Delegates sind übrigens Objekte der Klasse Delegate. Objekte dieser Klasse haben eine öffentliche Eigenschaft Target – das Objekt, dessen Methode sie aufrufen. Wenn man will, kann man also sogar noch weiter gehen als einfach die Reihenfolge des Aufrufs kontrollieren und z.B. einen Aufruf nur durchführen, wenn sich das entsprechende Objekt in einem bestimmten Zustand befindet.

using System.Collections.Generic;

namespace DelegateDemo {
  delegate void Method(int firstArg, int secondArg);

  class TestClass {
    public void TestMethod(int x, int y) {
    }
  }

  class Program {
    static void Main(string[] args) {
      TestClass a = new TestClass();
      TestClass b = new TestClass();
      TestClass c = new TestClass();

      List<Method> methods=new List<Method>();
      methods.Add(a.TestMethod);
      methods.Add(b.TestMethod);
      methods.Add(c.TestMethod);

      methods[0](0, 1);
      methods[1](2, 3);
      methods[2](4, 5);
    }
  }
}
Veröffentlicht in Keine Kategorie. Schlagwörter: , , . Leave a Comment »

Forcierte Updates für den Forms-Designer

Es kann vorkommen, dass während der Arbeit im Windows-Forms-Designer an einem Control so etwas ändert, dass es der Designer nicht mitbekommt. Dies ist speziell dann der Fall, wenn man wie in einem früheren Blog-Eintrag hier beschrieben quasi „hinterrücks“ Eigenschaften eines Controls ändert, ohne dass der Designer direkt beteiligt ist, nämlich via Code beim Control selbst.

Konkret hatte ich das Problem, dass ich den Text eines Label-Controls änderte, aber die Markierung rund um das Control nicht an die neue visuelle Länge angepasst wurde.

Es hat sich zum Glück herausgestellt, dass es relativ einfach ist, an die Komponente des Designers heranzukommen, welche für Reaktionen auf solche Änderungen zuständig ist. Konkret in Code:

 protected override void OnMouseClick(MouseEventArgs e) {
   base.OnMouseClick(e);
   if (DesignMode) {
     // gewünschte DesignTime-Verarbeitung 
     IComponentChangeService changeService = (IComponentChangeService)Site.GetService(typeof(IComponentChangeService));
     if (changeService != null) {
       changeService.OnComponentChanged(this, null, "", "");
     }
   }
 }
Veröffentlicht in Keine Kategorie. Schlagwörter: , , . Leave a Comment »

Bestehende Klassen erweitern

Die meisten Programmierer haben sich wohl schon irgendeinmal gewünscht, im einem System eine bestehende Klasse auf einfache Weise um ein paar Methoden erweitern zu können, die ganz einfach fehlen zum vollkommenen Glück.

Die C#-Klasse string im .NET-Framework ist wohl so ein Kandidat: Im Standard-Angebot ist da nur 1 Methode Substring, aber viele praktische Methoden wie z.B. Right, die es in VB.NET gibt, fehlen.

Seit C# 3.0 gibt es mit Hilfe sogenannter Extension Methods tatsächlich die Möglichkeit, selbst eine Standard-Klasse wie string auf einfache und elegante Weise zu erweitern. Das sieht dann bei der erwähnten Right-Methode konkret so aus:

public static class Strings {
public static string Right(this string str, int len) {
if (str.Length <= len) { return str; } else { return str.Substring(str.Length - len); } } } [/sourcecode] Der wichtigste Punkt bei dieser Sache ist wohl der, von irgendwoher zu wissen, dass das geht, z.B. eben aus diesem Blog-Eintrag hier. Der Rest ist einfach und lässt sich in der Microsoft-Dokumentation nachlesen, z.B. hier.

Man kann sich natürlich fragen, ob es eine gute Idee ist, Standard-Klassen auf diese Weise zu erweitern, wie z.B. hier in einem Forum-Thread leidenschaftlich diskutiert wurde. Ich denke, es ist wie fast bei allen Dingen: Richtig angewendet, sind extension methods eine nützliche Sache, aber ja, man kann sie missbrauchen, aber nein, die Versuchung zum Missbrauch ist nicht so gross, dass man die Sache per se pauschal zum Teufel schicken müsste.

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

Klicks auf Objekte zur Design-Zeit

Der im Visual Studio enthaltene Designer für Windows Forms ist ein grosser Fortschritt gegenüber früheren Programmen, z.B. dem Designer in Visual Basic 6. Und doch: Wenn es darum geht, Forms zu entwerfen, die eine Menge Controls haben, würde man sich viele Dinge einfacher wünschen, mit weniger Eingaben, und mit weniger Mausklicks.

Als ich kürzlich eben eine solche komplexe Form entwerfen musste, kam mir das Label-Control als besonders schlechtes Beispiel in dieser Beziehung vor: Jede Menge Eigenschaften, so viele, dass es häufig ein paar Klicks braucht, bis man sich bis zur einzigen Eigenschaft „durchgekämpft“ hat, die man wirklich routinemässig ändern möchte: den Text des Labels.

Mit der Zeit sagte ich mir, wie wunderbar es doch wäre, einfach ein Label im Designer anklicken zu können, um eine Dialogbox hervorzuholen mit einem Eingabefeld für das Ändern des Label-Textes!

Es gibt da nur ein kleines, aber entscheidendes Problem: Wie schafft man es, zur Design-Zeit an diesen Klick heranzukommen, um eigenen Code für die Anzeige besagter Eingabebox anhängen zu können? Es ist ja so, dass das Ereignis OnMouseClick eines Labels normalerweise im Designer nicht feuert, denn damit wäre ja das normale Funktionieren des Designers nicht mehr gewährleistet.

Hierzu ein verwandtes Beispiel, das noch viel klarer mögliche Probleme mit solchen Ereignissen illustriert: Stellen Sie sich vor, Sie haben für ein Control Drag-und-Drop-Logik implementiert, die plötzlich triggert, wenn Sie das Control im Designer verschieben…

Aber es geht trotzdem, und ein bisschen Suchen via Google und Ausprobieren hat folgende Lösung zu Tage gebracht: Man leitet eine eigene Klasse ab von Windows.Forms.Label und stattet diese mit einem sogenannten Designer aus, der nur gerade 1 Methode GetHitTest zu implementieren braucht. Damit kann man erreichen, dass selbst zur Design-Zeit unter bestimmten Umständen, die man frei selbst bestimmen kann, die Methode OnMouseClick des Labels aufgerufen wird.

In diese Methode baut man dann z.B. die Anzeige einer Dialogbox für die Abfrage eines neuen Textes ein, natürlich noch geschützt durch eine Abfrage von DesignMode, damit man nicht umgekehrt zur Laufzeit des Programms Ärger bekommt, wenn jemand den Label anklicken sollte.

Ich habe die Sache experimentell mal so geregelt, dass ein Klick auf die linke Hälfte des Labels wie gehabt an den Designer geht und ein Klick in die rechte Hälfte des Labels selbst zum Ändern des Textes. Das hat sich bisher als recht brauchbar erwiesen.

Im Code sieht das ganze so aus:

  class TritonLabelDesigner : System.Windows.Forms.Design.ControlDesigner {

    protected override bool GetHitTest(Point point) {
      TritonLabel hitLabel = (TritonLabel)Component;
      Point localPoint = hitLabel.PointToClient(point);
      if (localPoint.X > (hitLabel.Width / 2)) {
        return true;
      }
      else {
        return false;
      }
    }
  }

  [DesignerAttribute(typeof(TritonLabelDesigner))]
  public partial class TritonLabel : Label {
    public TritonLabel() {
      InitializeComponent();
    }

    protected override void OnMouseClick(MouseEventArgs e) {
      base.OnMouseClick(e);
      if (DesignMode) {
        // gewünschte DesignTime-Verarbeitung
      }
    }
  }
Veröffentlicht in Keine Kategorie. Schlagwörter: , , , . 1 Comment »