Einlesen von “delimited with” Daten – Leicht gemacht mit Hilfe von Custom Attribut

So lange es EDV gibt und wohl auch, so lange es EDV geben wird ist der Austausch von Daten immer wieder ein Thema.

Um Daten zwischen Verschiedenen Systemen auszutauschen sind die Möglichkeiten fast unerschöpflich.

Eine der Möglichkeiten besteht darin Daten in eine Zeichenkennte zu schreiben und die Daten mit einem sogenannten Trennzeichen (Delimiter) zwischen den einzelnen Feldinformationen zu trennen (Auch noch im Zeitalter von Serialisierung und anderen tollen Dingen).

Die meisten Programme können Daten in diesem Format exportieren.

Viel interessanter wird es, wenn man diese Daten in seine eigenen Programmen verarbeiten möchte.

Ich möchte hier eine einfache Möglichkeit zeigen, wie man sich mit ein paar Zeilen Code eine schicke kleine Funktion und ein Custom Attribut schreiben kann die einem dabei behilflich sind, solche Daten einzulesen und diese Daten in Eigenschaften seiner eigenen Klasse zu schreiben.

Hier zuerst das Custom Attribute.  Das bietet schon ein wenig mehr Möglichkeiten als es hier in diesem Beispiel zum Einsatz kommt, aber der Beitrag soll auch nicht eine fertige Lösung darstellen, sondern nur behilflich sein, einen Weg zu zeigen:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class ImportFieldAttribute : Attribute
{
	/// <summary>
	/// Feldtype der zu importierenden Daten
	/// </summary>
	private readonly ImportFieldType fieldType;

	/// <summary>
	/// Position im Segment
	/// </summary>
	private readonly int position;

	/// <summary>
	/// Initializes a new instance of the <see cref="ImportFieldAttribute"/> class.
	/// Für Import von Sätzen mit Trennzeichen
	/// </summary>
	/// <param name="fieldType">Type of the field.</param>
	/// <param name="position">The position.</param>
	/// <param name="length">The length.</param>
	public ImportFieldAttribute(ImportFieldType fieldType, int position)
	{
		this.fieldType = fieldType;
		this.position = position;
	}

	/// <summary>
	/// Feldtype der zu importierenden Daten
	/// </summary>
	public ImportFieldType FieldType
	{
		get
		{
			return this.fieldType;
		}
	}

	/// <summary>
	/// Position im Segment
	/// </summary>
	public int Position
	{
		get
		{
			return this.position;
		}
	}
}

Schauen wir uns nun den Code an der sich mit Hilfe des Custom Attribute an die Daten ran macht.

public static void ImportClassFieldsFrom(object target, string buf, char delimiter)
{
	var fields = buf.Split(delimiter);

	const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Public;
	var targetProperties = target.GetType().GetProperties(Flags);
	try
	{
		foreach (PropertyInfo propertyInfo in targetProperties)
		{
			var attribs =
				(ImportFieldAttribute[])propertyInfo.GetCustomAttributes(typeof(ImportFieldAttribute), true);

			if (attribs.Length > 0)
			{
				ImportFieldAttribute attrib = attribs[0];
				if (attrib.Position <= fields.Length)
				{
					var temp = fields[attrib.Position];
					propertyInfo.SetValue(target, temp.TrimEnd(), null);
				}
			}
		}
	}
	catch (Exception ex)
	{
		Exceptions.Log.LogError(buf, ex);
	}

}

Und so verwendet man diese Methode und das Custom Attribute:

Bei der Definition der Klasse, welche die Informationen aus dem “Delimited width” Zeichenkette aufnehmen soll, wende ich nun das Custom Attribute wie folgt an:

public class Palettes
{
	/// <summary>
	/// Gets or sets GrossWeight.
	/// </summary>
	[ImportField(ImportFieldType.Char, 1)]
	public string GrossWeight { get; set; }

	/// <summary>
	/// Gets or sets Height.
	/// </summary>
	[ImportField(ImportFieldType.Char, 2)]
	public string Height { get; set; }

	/// <summary>
	/// Gets or sets Lenght.
	/// </summary>
	[ImportField(ImportFieldType.Char, 3)]
	public string Lenght { get; set; }
}

Um die Klasseneigenschaften meiner Klasse mit den Werten aus der “Delimited width” Zeichenkette zu bekommen wird folgender Methodenaufruf durchgeführt:

var palette = new Palettes());
AttributHelper.ImportClassFieldsFrom(palette, "12,00~15,01~23,00", '~');

Fragen, Anregungen und Kritik wie immer, einfach als Kommentar

ASCII Schnittstellen mit Hilfe von Custom Attributes komfortabel erstellen – C#

Viele kennen dass Problem, dass Daten zwischen verschiedenen Welten auch Heute noch immer durch Übergabe von Schnittstellendateien, seien es EDI oder auch ASCII Dateien, die einen festen Satzaufbau haben, übergeben werden.

Die Definitionen sehen oft wie folgt aus:

Wobei die nähere Definition etwas komplexer sein kann:

Art: A alphanumerisches Feld N numerisches Feld Länge L Gesamtlänge des Datenfeldes Nachkomma K davon Nachkommastellen Position von von Stelle ... im Datensatz bis bis Stelle ... im Datensatz M/O M Muss-Feld O Kann-Feld (optional) M/O abhängig von anderen Angaben handelt es sich wahlweise um ein Muss- oder ein Kann-Feld

Der Output, also die Übertragungsdatei enthält dann Daten die ähnlich der nachfolgenden Abbildung aussehen können.

image

Eine durchaus übliche Vorgehensweise solche Daten erstellen zu können ist es, sich Klassen zu erstellen, die dem eigentlichen benötigten Satzaufbau entsprechen, diese in einer Füll Routine mit Daten füllt und anschließend die Daten der Klasseneigenschaften mit string.Format Stück für Stück ausgibt.

Wäre es nicht wünschenswert, bereits bei der Definition der Klasse festlegen zu können, welche Ausgabeeigenschaften (Ausgabeformat wie führende Nullen, Links oder Rechtsbündig, Anzahl Nachkommastellen usw.) die jeweilige Eigenschaft besitzt und an welcher Position die Eigenschaft ausgegeben werden soll, anzugeben, damit man anschließend die Daten einfach mit einer einzelnen Methode im richtigen Format ausgeben kann.

Und genau da setze ich mit meiner Lösung an:

Man kann mit sogenannten Custom Attributes beliebig zusätzliche Informationen an jedes beliebige Klassenelement anheften. Hierzu muss man eine Klasse erstellen die von System.Attributes abgeleitet wird.

Siehe Nachfolgendes Beispiel, welches ziemlich selbsterklärend sein sollte.

using System;
    using System.Reflection;

    public enum ExportFieldType
    {
        /// <summary>
        /// Alphanumeric Field
        /// </summary>
        Alpha,

        /// <summary>
        /// Numeric Field
        /// </summary>
        Numeric
    }

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class ExportFieldAttribute : Attribute
    {
        private readonly int index;

        private readonly int length;

        private readonly ExportFieldType fieldType;

        private readonly int precision;

        private readonly bool optionalField;

        private readonly int fromPos;

        private readonly int toPos;

        /// <summary>
        /// Initializes a new instance of the <see cref="ExportFieldAttribute"/> class.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <param name="fieldType">Type of the field.</param>
        /// <param name="length">The length.</param>
        /// <param name="precision">The precision.</param>
        /// <param name="optionalField">if set to <c>true</c> [optional field].</param>
        /// <param name="fromPos">From pos.</param>
        /// <param name="toPos">To pos.</param>
        public ExportFieldAttribute(int index, ExportFieldType fieldType, int length, int precision, bool optionalField = false, int fromPos = -1, int toPos = -1)
        {
            this.index = index;
            this.fieldType = fieldType;
            this.length = length;
            this.precision = precision;
            this.optionalField = optionalField;
            this.fromPos = fromPos;
            this.toPos = toPos;
        }

        public int Index
        {
            get
            {
                return this.index;
            }
        }

        public ExportFieldType FieldType
        {
            get
            {
                return this.fieldType;
            }
        }

        public int Length
        {
            get
            {
                return this.length;
            }
        }

        public int Precision
        {
            get
            {
                return this.precision;
            }
        }

        public bool OptionalField
        {
            get
            {
                return this.optionalField;
            }
        }

        public int FromPos
        {
            get
            {
                return this.fromPos;
            }
        }

        public int ToPos
        {
            get
            {
                return this.toPos;
            }
        }
    }

Nun kann ich in der Klassendefinition in der ich die Datensatzstruktur der Exportdatei abbilde, dieses Attribut zusätzlich zu den Eigenschaften der Klasse verwenden um die für den korrekten Export benötigen Informationen anzuheften.

Die Klasse könnte so aussehen:

    public class Ksta
    {
        [ExportField(0, ExportFieldType.Alpha, 5, 0)]
        public string Satza
        {
            get
            {
                return "KSTA_";
            }
        }

        [ExportField(1, ExportFieldType.Numeric, 2, 0)]
        public int Gsber
        {
            get
            {
                return 1;
            }
        }

        [ExportField(2, ExportFieldType.Numeric, 8, 0)]
        public int Kvkda { get; set; }

        [ExportField(3, ExportFieldType.Numeric, 8, 0)]
        public int Dtkda { get; set; }

        [ExportField(4, ExportFieldType.Numeric, 5, 0)]
        public int Kvkvn { get; set; }

        [ExportField(5, ExportFieldType.Numeric, 5, 0)]
        public int Ssbpa { get; set; }

        [ExportField(6, ExportFieldType.Numeric, 8, 3)]
        public int Sskda { get; set; }
    }

Wie aber können wir diese zusätzlichen Attribute verwenden?

Ich möchte dies an einem einfachen Beispiel demonstrieren. Hierzu erweitere ich die Klasse ExportFieldAttribute um folgende Methode:

public static string ExportFieldToString(object obj, bool sorted = true)
{
	string buffer = string.Empty;
	PropertyInfo[] pi = obj.GetType().GetProperties();
	if (sorted)
	{
		var propertyInfoSorted = new PropertyInfo[pi.Length];
		foreach (var propertyInfo in pi)
		{
			var attribs = (ExportFieldAttribute[])propertyInfo.GetCustomAttributes(typeof(ExportFieldAttribute), true);
			if (attribs.Length > 0)
			{
				propertyInfoSorted[attribs[0].Index] = propertyInfo;
			}
		}

		pi = propertyInfoSorted;
	}

	foreach (var propertyInfo in pi)
	{
		if (propertyInfo == null)
		{
			continue;
		}

		var attribs = (ExportFieldAttribute[])propertyInfo.GetCustomAttributes(typeof(ExportFieldAttribute), true);

		if (attribs.Length > 0)
		{
			ExportFieldAttribute attrib = attribs[0];
			var o = propertyInfo.GetValue(obj, null);
			if (o == null)
			{
				o = string.Empty;
			}

			if (attrib.FieldType == ExportFieldType.Alpha)
			{
				string t = string.Format(o.ToString().PadRight(attrib.Length));
				buffer += t;
			}

			if (attrib.FieldType == ExportFieldType.Numeric)
			{
				if (attrib.Precision > 0)
				{
					string concat;
					var buf = o.ToString().Split(',');
					if (buf.Length > 1)
					{
						buf[1] = buf[1].PadRight(attrib.Precision, '0').Substring(0, attrib.Precision);
						concat = buf[0] + buf[1];
					}
					else
					{
						concat = buf[0] + string.Empty.PadRight(attrib.Precision, '0');
					}
					string t = string.Format(concat.PadLeft(attrib.Length, '0'));
					buffer += t;
				}
				else
				{
					string t = string.Format(o.ToString().PadLeft(attrib.Length, '0'));
					buffer += t;
				}
			}
		}
	}
	return buffer;
}

Ich möchte an dieser Stelle nun auch keine aufwändige Erklärung der Methode vornehmen, ich denke dass derjenige der sich mit diesem Problem beschäftigt erkennen wird was darin geschieht.

Und um diese Methode zu verwenden, also um den Satzaufbau der Daten so zu erhalten wie ich ihn mit den Attributen definiert habe, überschreibe ich einfach die ToString() Methode der Klasse in welcher ich die Datenstruktur abgebildet habe.

Das sieht dann so aus (In der Klasse Ksta):

        public override string ToString()
        {
            return ExportFieldAttribute.ExportFieldToString(this);
        }

Wenn ich nun eine Instanz der Klasse Ksta erstelle, diese dann mit Daten fülle und anschließen die ToString Methode aufrufe erhalten ich die Daten genau in der definierten Struktur.

Wenn jemand noch Fragen zu dem Thema hat, oder doch noch weiterführende Erläuterungen benötigt, dann einfach per Kommentar die Fragen oder Anregungen stellen.