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
      }
    }
  }
Advertisements
Veröffentlicht in Keine Kategorie. Schlagwörter: , , , . 1 Comment »

Die Ursprünge von C#

Wenn Sie mal eine Pause einlegen wollen von all der anstrengenden C#-Programmierarbeit, kann es vielleicht interessant sein, sich anzuschauen, wo denn C# eigentlich herkommt.

Wenn man ein fertiges Sprach-Design vor sich hat, und noch viel mehr, wenn man eine Weile mit der jeweiligen Programmiersprache gearbeitet hat, fällt es sehr schwer sich vorzustellen, dass jemand dieses Design irgendwann in der Vergangenheit erarbeiten musste, und dass dies in den meisten Fällen ein hartes Stück Arbeit war: Einmal geschaffen, haben es solche Designs so an sich, irgenwie selbstverständlich oder sogar trivial auszusehen, aber sie sind alles andere als das!

Im Falle von C# ist ein gewisser Anders Hejlsberg der Mann, der als Architekt für einen grossen Teil des Sprach-Designs verantwortlich ist. Microsoft machte 1996 einen guten Fang, als es der Firma gelang, diesen Mann bei Borland abzuwerben, und ihn zum Chef-Designer im Team machte, das die C#-Sprache entwickelte.

Zu erstem Ruhm gelangte Hejlsberg bereits 1983, als sein Pascal-Compiler Turbo Pascal auf den Markt kam. Dieser Compiler deklassierte mit seiner Geschwindigkeit alles bisher Dagewesene komplett.

Wenn Sie neugierig geworden sind, ein paar interessante weiterführende Links:
Ein Interview mit Anders Hejlsberg über das Design von C#
Wikipedia über Anders Hejlsberg
Wikipedia über Turbo Pascal

Und zu guter letzt noch etwas für Nostalgiker, z.B. für Leute wie mich, die damals 1983 die Ankunft von Turbo Pascal selbst miterlebt haben und kaum glauben konnten, was sie da sahen: Borland hat den Compiler in die Public Domain gegeben; man kann Version 1.0 hier herunterladen, und das Ding läuft sogar in der DOS-Box von Windows XP!

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

Reflektionen über die Reflection

Woran merkt man, dass jemand schon etwas länger programmiert? Z.B. daran, dass er oder sie bei der ersten Begegnung mit der Reflection im .NET-Framework sich darüber Gedanken macht, ob es wirklich eine gute Idee ist, dieses Feature zu verwenden.

Ich habe viele Jahre lang mit Compiler-basierten Sprachen wie Modula-2 und C++ gearbeitet, wo der Compiler so ziemlich jede Information über das Programm selbst vernichtet und dieses zur Laufzeit über keinerlei „Selbstbewusstsein“ mehr verfügt. Und just als ich unbewusst irgendwie fast das Gefühl bekommen hatte, dass müsse so sein, und ich die fehlende oder mangelhafte Umsetzung von Standards wie RTTI in den gängigen Compilern für C++ mit Gedanken quittierte wie z.B. „konnte ja nicht klappen“, dann kommt das .NET-Framework daher, wo ein Programm so ziemlich alles über sich selbst zur Laufzeit abfragen kann, obwohl natürlich mit Compilern gearbeitet wird.

Meine spontane Reaktion: Zuerst Erstaunen. Dann Skepsis: Hat das wirklich keine Nachteile? Geht nicht die Performance meines Programms in den Keller, wenn ich Reflection einsetze? Blasen all die Meta-Informationen, die ja irgendwo gespeichert sein müssen, nicht die Assemblies zu unanständigen Grössen auf? Besteht nicht sogar eine gewisse Gefahr, dass irgendwann einmal Microsoft hingeht, mir dieses schöne neue Spielzeug namens Reflection wieder wegnimmt und ich dann mit meinem Programm, das ohne nicht mehr auskommt, ganz bös im Regen stehe?

Ich kann vermelden, dass sich meine Skepsis gelegt hat. Der Hauptgrund dafür ist, dass ich mittlerweile einen Überblick habe, wo überall im .NET-Framework selbst Reflection eingesetzt wird. Die Nutzung dieses Mechanismus durchdringt das Framework mehr oder weniger von oben bis unten. Wenn da irgendwelche Haken wären, hätte man das schon längst gemerkt bzw. Microsoft wäre gezwungen gewesen, Probleme schleunigst auszuräumen.

Und wenn man sieht, dass schon beim einfachsten Data Binding eines DataGridView und dem automatischen Generieren von Kolonnen Reflection zum Einsatz kommt, weiss man, dass man die Anwesenheit der Reflection als Selbstverständlichkeit nehmen und ohne Bedenken darauf bauen kann.

Man kann wirklich einige faszinierende Dinge machen mit Reflection, und ich empfehle jedem, der ernsthaft mit dem .NET-Framework arbeiten will, sich die Sache mindestens einmal anzusehen.

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

Dynamische ComboBox

Bei der ComboBox ist auffällig, dass ihre Fähigkeiten im „Grundzustand“ seit vielen Jahren unverändert geblieben sind, und auch die ComboBox des neuesten .NET-Frameworks passt die Grösse des DropDown-Bereichs weder in der Höhe noch in der Breite dynamisch an den anzuzeigenden Datenbestand an.

Meine Abklärungen heute haben aber gezeigt, dass sich die ComboBox sehr wohl verbessert hat, was die Methoden und Properties betrifft, welche sie dem Programmierer zur Verfügung stellt. Ein „dynamischer“ DropDown-Bereich war in VB6 nur möglich mit ziemlich abenteuerlichem Einsatz von Window Handles und WIN32-API-Aufrufen; jetzt, im .NET-Framework, ist die ganze Sache zum Glück wesentlich einfacher.

Man kann mit Hilfe der Property DropDownHeight bequem die Höhe des DropDown-Bereichs programmiert auf eine bestimmte Anzahl Pixel setzen. Analog hat man via DropDownWidth Einfluss auf die Breite. Man muss nur IntegralHeight nach dem Setzen der Höhe wieder auf true setzen, wenn man angeschnittene Einträge nicht mag.

Das Anpassen der Höhe des DropDown-Bereichs einer ComboBox bis zu einer vernünftigen Obergrenze von hier 30 Zeilen ist mit zwei kleinen Zeilen Code bereits erledigt:

comboBox.DropDownHeight = Math.Min(30, comboBox.Items.Count) * comboBox.ItemHeight;
comboBox.IntegralHeight = true;

Eine dynamische Anpassung der Breite an den längsten Eintrag ist ebenfalls mit wenig Aufwand möglich, wie man z.B. hier nachlesen kann.

Den DropDown-Bereich resizable zu machen, so dass der Anwender selbst die Grösse ändern kann, wenn er sie anders haben will, dürfte allerdings nicht so einfach sein, denn hierfür habe ich in der ComboBox-Klasse keine Unterstützung gefunden. Ja, es ist mir im Moment nicht einmal klar, wie man hier vorgehen müsste, denn ich ich habe nicht herausgefunden, wo der DropDown-Bereich eigentlich „steckt“. Ein Child Control des Textfeld-Teils der ComboBox scheint er jedenfalls nicht zu sein.

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

IBindingList

Im Rahmen der Erstellung eines Prototyps hatte ich es mit einem DataGridView zu tun, der mit DataBinding arbeiten sollte, aber nicht auf einer Datenbank, sondern auf Daten im Speicher.

Das klappte zunächst ganz gut, dank der grossen Flexibilität des DataGridView-Controls, das unter anderem auf allem aufsetzen kann, was das IList-Interface aufweist. Dann aber tauchte die Frage auf: Wie merkt der DataGridView, wenn an den zugrundeliegenden Daten etwas ändert, d.h. in der Liste Elemente dazukommen oder gelöscht werden, damit das Nachführen auf dem Bildschirm klappt?

Schnell einmal war klar, dass es nicht „einfach so“ klappt, durch irgendetwas, was ein Objekt tut, welches das IList-Interface implementiert, oder dadurch, dass sich der DataGridView bei der Liste irgendwie einklinkt.

Oft braucht es nur einen Hinweis auf die richtige Spur, und wer sich mit dem .NET-Framework einigermassen auskennt, findet den Weg dann alleine. Deshalb keine grosse Ausschweifungen, sondern nur ein Stichwort für Leser, die vor derselben Frage stehen: IBindingList heisst das Lösungswort.

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

Formatierung von Kommentaren im C#

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&#91;pos&#93;) != -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&#91;pos&#93; == ' ')) 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&#91;contentIndent&#93; == ' ')) 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&#91;&#93; lineData;
 		int result;

			line = null;
 		lineData = new LINEDATA&#91;1&#93;;
 		result = text.GetLineData(lineNum, lineData, null);
 		if (result != VSConstants.S_OK) return false;
 		line = Marshal.PtrToStringAuto(lineData&#91;0&#93;.pszText, lineData&#91;0&#93;.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…(;-)

C#-Code in Blog-Einträgen

Heute war eine Abklärung dran, wie man denn in ein Blog C#-Sourcecode so einfügt, dass er gut aussieht, möglichst mit Syntax Coloring erhalten, und so dass man ihn per Cut-and-Paste aus dem Blog ins Visual Studio übernehmen kann, ohne lästige Nebeneffekte wie z.B. eine zusätzliche Leerzeile pro Codezeile.

Eine Abklärung per Google förderte ein hübsches Visual Studio AddIn namens CopySourceAsHtml zu Tage, das selektierten Code zu HTML-Code umwandelt, inklusive Farben und Einrückungen, und diesen ins Clipboard ablegt, von wo man ihn dann ins Weblog kopieren kann. Das ganze funktioniert bequem über einen neuen Context-Menu-Eintrag im Visual-Studio-Editor Copy to HTML…:

Ursprüngliche Version von CopySourceAsHtml für VS2005
Portierung von CopySourceAsHtml auf VS2008

Wie sich herausstellte, hat die Sache nur einen Haken: Der WordPress-Editor für das Erfassen von Einträgen ist mit dem HTML etwas überfordert.

Zum Glück haben die Leute von WordPress an uns Programmierer gedacht und etwas für Sourcecode in Form spezieller Markierungen vorgesehen (siehe FAQ: How do I post source code?).

Das sieht dann zwar nicht ganz so hübsch aus wie das, was eine 1-zu-1-Übertragung aus dem Visual-Studio-Editor bringt, ist aber trotzdem recht brauchbar, wie man hier sieht:

public override bool Equals(object obj) {
  if ((obj == null) || (obj.GetType() != GetType())) return false;
  return (CompareResult)CompareTo((DataVector)obj) == CompareResult.Equal;
}
Veröffentlicht in Keine Kategorie. Schlagwörter: , , , . Leave a Comment »