Zum Inhalt springen

Archiv

Kategorie: C#

Vor ein paar Tagen kam ein Kollege auf mich zu und meinte, er habe beim Unit-Test schreiben einen Fehler in meinem Code entdeckt. Um mir den Fehler zu zeigen, hat er mir eigens einen Unit-Test geschrieben, welcher in etwa so aussah:

// MyTypeList : List<string>
MyTypeList list = new MyTypeList {"a", "b", "c"};

if(list.ElementAt(0) == null)
{
    throw new NullReferenceException();
}

if(list[0] == null)
{
    // Hier knallt er!
    throw new NullReferenceException();
}

if(list[1] == null)
{
    throw new NullReferenceException();
}

Er knallte immer, wenn man auf list[0] zugreifen wollte. Im Debugger zeigte er mir alle Elemente an, list[1] usw. funktionierte auch. Ich stand erstmal auf dem Schlau. Da ich den Fehler nicht sofort identifizieren konnte, baute ich mir eine kleine Testapplikation, um mit einem Minimum an Code das Problem zu identifizieren.

Letzendlich lag es an einem Indexer, welchen ich selber erstellt habe. Und zwar erwartete dieser als Übergabetyp ein Enum. Dieser überschreibt zwar nicht den von List<T> geerbten Indexer this[int index], jedoch scheint der Compiler hier leichte Probleme zu machen.

Schauen wir uns mal den kompilierten Code in dotPeek an.

      MyTypeList myTypeList1 = new MyTypeList();
      myTypeList1.Add("a");
      myTypeList1.Add("b");
      myTypeList1.Add("c");
      MyTypeList myTypeList2 = myTypeList1;
      if (Enumerable.ElementAt<string>((IEnumerable<string>) myTypeList2, 0) == null)
        throw new NullReferenceException();
      if (myTypeList2[MyEnum.Value1] == null)
        throw new NullReferenceException();

Den ersten Zugriff auf den numerischen Indexer wurde auf den Enum-Indexer geändert. Eine Erklärung dazu konnte ich nicht wirklich finden, bzw. keine die ich nachvollziehen konnte. Letztendlich hat es damit zu tun, dass ein Enum im Bauch eigentlich nur ein int-Typ ist. Dies passiert immer, wenn ich den numerischen Indexer mit 0 zugreife, wohlgemerkt, nur mit 0. Alle anderen Werte funktionieren problemlos.

Wie kann ich das Problem lösen / umgehen?

Dafür gibt es mehrere Wege.

Wird der Index-Wert woher in eine Variable (z.B. innerhalb einer for-Schleife) gesetzt, kann er den korrekten Indexer auflösen.

int index = 0;
if(list[index] == null) {
...
}

Ein Cast direkt auf int hingegen funktioniert nicht (er nutzt wieder den Enum-Indexer):

if(list[(int) 0] == null) {
..
}

Lustiger Weise lässt sich die Solution nicht mehr kompilieren, sobald ich einen weiteren Enum-Indexer hinzufüge:

The call is ambiguous between the following methods or properties: 'DumpSolution.MyTypeList.this[DumpSolution.MyEnum]' and 'DumpSolution.MyTypeList.this[DumpSolution.MyEnum2]'

Die simpelste Lösung ist, einfach den Standard-Indexer zu überschreiben:

    public class MyTypeList : List<string>
    {
        public string this[MyEnum myEnum]
        {
            get { return this.FirstOrDefault(item => item == Enum.GetName(myEnum.GetType(), myEnum)); }
        }

        public string this[MyEnum2 myEnum]
        {
            get { return this.FirstOrDefault(item => item == Enum.GetName(myEnum.GetType(), myEnum)); }
        }

        public new string this[int index]
        {
            get { return base[index]; }
            set { base[index] = value; }
        }
    }

Danach funktioniert der Code oben problemlos und korrekt. Ach ja, ein anpassen des Enums wie folgt brachte auch keinen Erfolg.

enum MyEnum {
   Value1 = 1,
   Value2 = 2
}

Ich kann mir vorstellen, dass es das Problem auch bei anderen Listen gibt, ich selbst habe es aber aktuell nur bei List<T> festgestellt. Im Netz findet man einige Einträge dazu, dazu auch eine Erklärung (welche mich aber nur bedingt zufrieden stellt)

Ich habe ein kleines Beispiel, welches ihr hier herunterladen könnt.

Update: Es gab noch einen kleinen Bug im Code, den ich noch mal eben behoben habe!


Ursprünglicher Beitrag:

Ich habe mal spaßeshalber ein MVC Projekt angefangen und dort ebenfalls das vor wenigen Tagen erschienene Bootstrap 3 implementiert. Nun wollte ich die bekannte ValidationSummary von MVC ein wenig mit Bootstrap stylen, jedoch scheint dies nicht so einfach.

Bei stackoverflow (wo auch sonst) bin ich auf eine eigene Implementierung der ValidationSummary-Ausgabe speziell für Bootstrap gestoßen. Diese ist jedoch nur für die Server-seitige Validierung geeignet, und da ich mich sowieso noch nicht entschlossen hatte, welche Validierung ich nutzen möchte, habe ich kurzerhand die Client-seitige deaktiviert.

Neben der bootstrap-gestylten ValidationSummary zeigte „PeteGo“ noch, wie er seine Eingabefelder für den Benutzer fehlerhaft markiert. Dies ist mir allerdings zu aufwendig, da man natürlich mit jedem neuen Feld auch auch an die Prüfung denken muss. Ja ich weiß, ich kann ja Editor-Templates nutzen, will ich aber nicht 😉

Da ich die Erweiterung von PeteGo sowieso anfassen musste, sie war nur für Bootstrap 2 geeignet, habe ich die einzelnen Fehlermeldungen auch noch einem data--Attribute versehen und markiere per jQuery die Felder als fehlerhaft.

Die Erweiterung:

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace System.Web.Mvc
{
    public static class HtmlExtensionMethods
    {
        /// <summary>
        /// Returns an error alert that lists each model error, much like the standard ValidationSummary only with
        /// altered markup for the Twitter bootstrap styles.
        /// </summary>
        public static MvcHtmlString ValidationSummaryBootstrap(this HtmlHelper helper, bool closeable)
        {
            string errorMessage = "Please fix the errors listed below and try again.";
            string validationTitle = "Validation error";

            ModelStateDictionary modelErrors = helper.ViewContext.ViewData.ModelState;

            if (!modelErrors.Any(error => error.Value.Errors.Any()))
            {
                return new MvcHtmlString(string.Empty);
            }

            StringBuilder divContent = new StringBuilder();
            
            if (closeable)
            {
                TagBuilder button = new TagBuilder("button");
                button.AddCssClass("close");
                button.MergeAttribute("type", "button");
                button.MergeAttribute("data-dismiss", "alert");
                button.SetInnerText("x");
                divContent.Append(button);
            }

            TagBuilder validationTitleTag = new TagBuilder("strong");
            validationTitleTag.SetInnerText(validationTitle);
            divContent.Append(validationTitleTag);

            divContent.Append(" ");
            divContent.Append(errorMessage);

            StringBuilder ulContent = new StringBuilder();
            
            foreach (KeyValuePair<string, ModelState> modelError in modelErrors)
            {
                string id = modelError.Key;
                foreach(ModelError item in modelError.Value.Errors)
                {
                    TagBuilder li = new TagBuilder("li");
                    li.SetInnerText(item.ErrorMessage);
                    li.MergeAttribute("data-bootstrap-has-error", id);
                    ulContent.Append(li);
                }
            }
            TagBuilder ul = new TagBuilder("ul") {InnerHtml = ulContent.ToString()};
            divContent.Append(ul);

            TagBuilder div = new TagBuilder("div");
            div.AddCssClass("alert");
            div.AddCssClass("alert-error");
            div.AddCssClass("alert-block");
            div.InnerHtml = divContent.ToString();

            return new MvcHtmlString(div.ToString());
        }

        /// <summary>
        /// Overload allowing no arguments.
        /// </summary>
        public static MvcHtmlString ValidationSummaryBootstrap(this HtmlHelper helper)
        {
            return ValidationSummaryBootstrap(helper, true);
        }
    }
}

Und nun die paar Zeilen Javascript, welche dann die Felder entsprechend markieren:

$(function () {
    $('div.alert').each(function () {
        $(this).find('li[data-bootstrap-has-error]').each(
            function () {
                var id = $(this).attr('data-bootstrap-has-error');
                $('input[id=' + id + '],select[id=' + id + ']').parent().addClass('has-error');
            });
    });
});

Vielleicht kann es ja jemand gebrauchen 🙂

Ach ja, falls jemand ein paar Pros / Contras bezüglich Client-/Server-Validierung hat, kann er diese gerne in den Kommentaren hinterlassen.

Vor einer ganzen Weile habe ich mal die StringWriter-Klasse empfohlen.
Die StringWriter-Klasse nutzt intern UTF-16, wodurch zum Beispiel bei der Xml-Serialisierung von Klassen auch das Ergebnis in UTF-16 vorliegt. Dies ist nicht zwangsläufig gewünscht, bei mir hat dadurch ein Backend-Service gestreikt.
Um trotzdem den StringWriter weiter zu verwenden, muss man sich eine eigene Klasse schreiben, welche von der StringWriter ableitet und das Encoding-Property überschreibt:

using System;
using System.IO;
using System.Text;

namespace Utilities.IO
{

	/// <summary>
	/// 	A simple class derived from StringWriter, but which allows
	/// 	the user to select which Encoding is used. This is most
	/// 	likely to be used with XmlTextWriter, which uses the Encoding
	/// 	property to determine which encoding to specify in the XML.
	/// </summary>
	public class StringWriterWithEncoding : StringWriter
	{
		private Encoding _encoding;

		/// <summary>
		/// 	Initializes a new instance of the StringWriterWithEncoding class
		/// 	with the specified encoding.
		/// </summary>
		/// <param name = "encoding">The encoding to report.</param>
		public StringWriterWithEncoding(Encoding encoding)
			: base()
		{
			this._encoding = encoding;
		}

		/// <summary>
		/// 	Initializes a new instance of the StringWriter class with the 
		/// 	specified format control and encoding.
		/// </summary>
		/// <param name = "encoding">The encoding to report.</param>
		/// <param name = "formatProvider">An IFormatProvider object that controls formatting.</param>
		public StringWriterWithEncoding(Encoding encoding, IFormatProvider formatProvider)
			: base(formatProvider)
		{
			this._encoding = encoding;
		}

		/// <summary>
		/// 	Initializes a new instance of the StringWriter class that writes to the
		/// 	specified StringBuilder, and reports the specified encoding.
		/// </summary>
		/// <param name = "encoding">The encoding to report.</param>
		/// <param name = "sb">The StringBuilder to write to. </param>
		public StringWriterWithEncoding(Encoding encoding, StringBuilder sb)
			: base(sb)
		{
			this._encoding = encoding;
		}

		/// <summary>
		/// 	Initializes a new instance of the StringWriter class that writes to the specified 
		/// 	StringBuilder, has the specified format provider, and reports the specified encoding.
		/// </summary>
		/// <param name = "encoding">The encoding to report.</param>
		/// <param name = "sb">The StringBuilder to write to. </param>
		/// <param name = "formatProvider">An IFormatProvider object that controls formatting.</param>
		public StringWriterWithEncoding(Encoding encoding, StringBuilder sb, IFormatProvider formatProvider)
			: base(sb, formatProvider)
		{
			this._encoding = encoding;
		}

		/// <summary>
		/// 	Gets the Encoding in which the output is written.
		/// </summary>
		public override Encoding Encoding
		{
			get { return this._encoding; }
		}
	}
}

Update:
Habe die Klasse mal um Kommentare erweitert.

Ich verstehe zwar nicht, warum es standardmäßig nicht im .NET Framework implementiert ist, allerdings ist es ohne Hilfsmittel nicht möglich, eine Klasse mit einem Dictionary mittels XmlSerializer zu serialisieren! Mit folgender Hilfsklasse funktioniert es dahin anstandslos, vorausgesetzt sowohl TKey und TValue sind Xml-serialisierbar.

using System.Collections.Generic;
using System.Xml.Serialization;

namespace Utilities.Generic
{
	[XmlRoot("dictionary")]
	public class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
	{
		#region Constructors

		public SerializableDictionary():base() {}

		public SerializableDictionary(IDictionary<TKey, TValue> dictionary) : base(dictionary) { }

		public SerializableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer) : base(dictionary, comparer) {}

		public SerializableDictionary(int capacity, IEqualityComparer<TKey> comparer) : base(capacity, comparer) { }

		public SerializableDictionary(IEqualityComparer<TKey> comparer) : base(comparer) { }

		public SerializableDictionary(int capacity) : base(capacity) { }

		#endregion

		private const string ItemTagName = "item";
		private const string KeyTagName = "key";
		private const string ValueTagName = "value";

		/// <summary>
		/// Diese Methode ist reserviert und sollte nicht verwendet werden. Wenn Sie die IXmlSerializable-Schnittstelle implementieren, sollten Sie null (Nothing in Visual Basic) von der Methode zurückgeben und stattdessen das <see cref="T:System.Xml.Serialization.XmlSchemaProviderAttribute"/> auf die Klasse anwenden, wenn ein benutzerdefiniertes Schema erforderlich ist.
		/// </summary>
		/// <returns>
		/// Ein <see cref="T:System.Xml.Schema.XmlSchema"/> zur Beschreibung der XML-Darstellung des Objekts, das von der <see cref="M:System.Xml.Serialization.IXmlSerializable.WriteXml(System.Xml.XmlWriter)"/>-Methode erstellt und von der <see cref="M:System.Xml.Serialization.IXmlSerializable.ReadXml(System.Xml.XmlReader)"/>-Methode verwendet wird.
		/// </returns>
		public System.Xml.Schema.XmlSchema GetSchema()
		{
			return null;
		}

		/// <summary>
		/// Generiert ein Objekt aus seiner XML-Darstellung.
		/// </summary>
		/// <param name="reader">Der <see cref="T:System.Xml.XmlReader"/>-Stream, aus dem das Objekt deserialisiert wird.</param>
		public void ReadXml(System.Xml.XmlReader reader)
		{
			XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
			XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));

			bool wasEmpty = reader.IsEmptyElement;
			reader.Read();

			if (wasEmpty)
			{
				return;
			}

			while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
			{
				reader.ReadStartElement(ItemTagName);

				reader.ReadStartElement(KeyTagName);
				TKey key = (TKey)keySerializer.Deserialize(reader);
				reader.ReadEndElement();

				reader.ReadStartElement(ValueTagName);
				TValue value = (TValue)valueSerializer.Deserialize(reader);
				reader.ReadEndElement();

				this.Add(key, value);

				reader.ReadEndElement();
				reader.MoveToContent();
			}
			reader.ReadEndElement();
		}

		/// <summary>
		/// Konvertiert ein Objekt in seine XML-Darstellung.
		/// </summary>
		/// <param name="writer">Der <see cref="T:System.Xml.XmlWriter"/>-Stream, in den das Objekt serialisiert wird.</param>
		public void WriteXml(System.Xml.XmlWriter writer)
		{
			XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
			XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));

			foreach (TKey key in this.Keys)
			{
				writer.WriteStartElement(ItemTagName);

				writer.WriteStartElement(KeyTagName);
				keySerializer.Serialize(writer, key);
				writer.WriteEndElement();

				writer.WriteStartElement(ValueTagName);
				TValue value = this[key];
				valueSerializer.Serialize(writer, value);
				writer.WriteEndElement();

				writer.WriteEndElement();
			}
		}
	}
}

Den ursprünglichen Code habe ich hier her.

Update:
Habe die Klasse mal um die Standardkonstruktoren der Basis-Klasse erweitert um ein bestehendes Dictionary einfach umwandeln zu können.

In einer kleinen Applikation mit NotifyIcon in der Taskbar wollte ich unterschiedliche ContextMenues öffnen, je nachdem, ob man mit der linken oder rechten Maustaste auf das Icon klickt.

Die erste Hürde, welche es zu nehmen gilt, ist es, dass ContextMenü auch bei einem Links-Klick zu öffnen. Man kann zwar eine entsprechende Methode nutzen, jedoch gibt es damit Probleme. Daher sollte man hier Reflection nutzen:

// constructor..
this._trayIcon.MouseDown += this.TrayIconMouseDown;

private void TrayIconMouseDown(object sender, MouseEventArgs e)
{
	if (e.Button == MouseButtons.Left)
	{
		MethodInfo mi = typeof (NotifyIcon).GetMethod("ShowContextMenu", BindingFlags.Instance | BindingFlags.NonPublic);
		mi.Invoke(this._trayIcon, null);
	}
}

Nun öffnet sich schon mal das Kontextmenü auch beim Linksklick. Und hier kann man ansetzen, um das Menü entweder zu manipulieren oder zu ersetzen. Ich selbst habe mir einfach zwei Objekte vorbereitet, welche ich je nach Mausklick austausche:

private void TrayIconMouseDown(object sender, MouseEventArgs e)
{
	if (e.Button == MouseButtons.Left)
	{
		this._trayIcon.ContextMenu = this._leftClickMenu;
		MethodInfo mi = typeof (NotifyIcon).GetMethod("ShowContextMenu", BindingFlags.Instance | BindingFlags.NonPublic);
		mi.Invoke(this._trayIcon, null);
	}
	else if (e.Button == MouseButtons.Right)
	{
		this._trayIcon.ContextMenu = this._rightClickMenu;
		MethodInfo mi = typeof(NotifyIcon).GetMethod("ShowContextMenu", BindingFlags.Instance | BindingFlags.NonPublic);
		mi.Invoke(this._trayIcon, null);
	}
}

Beim Testen ist mir aufgefallen, dass es mehrere Events gibt, an die man sich hängen könnte, etwa „MouseUp“, „MouseClick“ usw. Beim Testen hat bei mir allerdings nur „MouseDown“ korrekt funktioniert, bei den anderen hatte ich das Problem, dass zum Teil das Menü erst angezeigt wurde und dann der Eventhandler durchlaufen wurde (ich glaube sogar immer, wenn ich mit der linken Maustaste geklickt habe). Erklären konnte ich es mir allerdings nicht.