Mittelstrenges XML-Schema

Ein zu scharf wachendes Schema, welches XML-Dateien validiert, verdirbt die Freude an der Entwicklung von XML-Strukturen. Das Schema soll so großzügig wie möglich sein, d.h. so viele Knoten und Attribute wie möglich akzeptieren. Andererseits soll das Schema gewährleisten, dass alle zum Betrieb der Anwendung nötigen Daten vorhanden und korrekt sind. Letzteres ist einfach zu erreichen, da XML-Schema von Natur aus restriktiv ist.
Um ein vorhandenes Schema aufzuweichen, dass es auch so großzügig wie möglich ist, habe ich folgende Lösungen gefunden:

1. xs:key statt xs:ID
xs:key stellt die Eindeutigkeit eines Attributes innerhalb eines mit XPath festgelegten Geltungsbereichs des Dokuments sicher. Es können z.B. die Eindeutigkeit von Hotelzimmernummern und von Gästenummern garantiert werden, auch wenn beide Nummernbereiche überlappen. xs:ID ist aber innerhalb des Dokuments global. Außerdem kann ein xs:key-Attribut beliebige Zeichen beinhalten.

2. xs:choice statt xs:sequence
Will man beliebige Kindknoten neben definierten Kindknoten zulassen, könnte man in xs:sequence bestimmte Kindknoten definieren und dann mit xs:any beliebige Knoten zuassen. xs:sequence drängt aber eine Reihenfolge auf. Steht ein Element an erster Stelle, erwartet das Schema auch diesen Typ an erster Stelle.
Definiert man das Inhaltsmodell eines Elements mit xs:choice, muss man alle möglichen Knoten kennen, um eine Auswahl von Kindknoten anzubieten. Andererseits ist dann die Reihenfolge der Kindknoten beliebig. Die Festlegung des XML-Autors auf eine bestimmte Reihenfolge ist aber strenger als die Einschränkung, mögliche Elemente erst dem Schema bekannt zu machen.

3. xs:anyAttribute
Die Reihenfolge von Attributen ist bei XML-Schema beliebig. Will man jedoch, dass ein Schema zusätzlich zu den im Schema definierten Attributen beliebige Attribute akzeptieren soll, erlaubt man diese mit xs:anyAttribute.

MVC – Managed M, Unmanaged VC

Ein neues Projekt beginnt, also habe ich eine neue Chance, endlich sauber nach MVC zu programmieren. Zusätzlich wollen wir das Modell managed in C# programmieren. Das Modell betrachten wir hiermit als das wertvollste Stück und erhalten die Möglichkeit es in neuen Programmgenerationen wieder verwenden zu können. Also die Abspaltung des Modells ist gut, die weitere Sonderstellung als verwaltete Komponente in einem unverwalteten System könnte weitere Vorteile bringen. Letzteres steht aber noch unter Beobachtung. Schließlich erfordert es weiteren Aufwand. Fast hätte ich es vergessen: Das Modell kann testgetrieben entwickelt werden. Das bringt Agilität in die Entwicklung einer komplexen Logik und macht diese einfach nachvollziehbar.

Idealerweise alles managed

Nach der reinen Lehre wird das MVC Entwurfsmuster sauber eingesetzt, indem das Modell das Observable-Interface und verschiedene Views das Observer-Interface implementieren. Der Controller greift einseitig verändernd auf das Modell zu. Wird das Modell geändert, werden alls horchenden Views, nachdem sie sich beim Modell registriert haben, benachrichtigt. Danach holen sie sich die nötigen Informationen vom Modell ab. Durch das Observer-Interface kennt das Modell von den Views nur die eine Methode notify

Eine Modellklasse Bauteil könnte etwa so aussehen:


public class Bauteil
{
	public String Beschreibung { get; set; }
	public event EventHandler BauteilChanged;
	private double _Betriebsspannung = 0;
	public double Betriebsspannung
	{
		get
		{
			return _Betriebsspannung;
		}

		set
		{
			_Betriebsspannung = value;
			EventHandler handler = BauteilChanged;
			if (handler != null)
			{
				handler(this, EventArgs.Empty);
			}
		}
	}
}

Ich meine, Degaten und Events überträfen Interfaces in ihrer Eleganz. Deswegen entwerfe ich hier das Observer/Observable-Muster Modellseitig mit einem Event public event EventHandler BauteilChanged; und viewseitig kann in folgendem Observer die Methode void HandleEvent(Object^ sender, EventArgs^ args) als Delegat gekapselt und dem Event des Bauteils hinzugefügt werden:

ref class Observer
{
	public:
		Observer()
			:eventcount (0)
			, lastSender (NULL)
		{}

	void HandleEvent(Object^ sender, EventArgs^ args)
	{
		eventcount++;
		lastSender = sender;
	}

	int eventcount;
	Object^ lastSender;
};

Folgender Unittest beweist mir, dass das Zusammenspiel funktioniert:

[TestMethod]
void MultipleObjectsSendsUpdate()
{
	Bauteil^ bauteil = gcnew Bauteil();
	Observer^ observer = gcnew Observer();

	EventHandler^ handler;
	handler = gcnew EventHandler(observer, &Observer::HandleEvent);

	bauteil->BauteilChanged += handler;
	bauteil->Betriebsspannung = 50;

	Assert::AreEqual(true, observer->eventcount == 1);
	Assert::AreEqual(true, observer->lastSender == bauteil);

	Bauteil^ bauteil2 = gcnew Bauteil();
	bauteil2->BauteilChanged += handler;
	bauteil2->Betriebsspannung = 100;

	Assert::AreEqual(true, observer->eventcount == 2);
	Assert::AreEqual(true, observer->lastSender == bauteil2);
};

Eingeschränkte Komplexität

Über die managed/unmanaged-Grenze können die Informationen nicht so ungehindert wie oben beschrieben fließen. Würde man alles beherrschen wollen, müsste man das Design so fassen, dass Controller wie Views entweder managed oder unmanaged sein können. Auch müsste man die Zugriffe auf das verwaltete Modell von einem unverwalteten Controller kapseln und die Nachrichten von dem verwalteten Modell zu den unverwalteten Views ebenso. Hier kommt man nicht umhin, die Komplexität zu beschränken. Wir haben uns für unser Design entschieden, dass das Modell nur verwaltet ist und Controller wie Views nur unverwaltet sein können. Daraus resultiert, dass die Vermittlungsschicht, der ModelAgent, hauptsächlich das Design umsetzt. Der ModelAgent hat ein unverwaltetes Interface und eine verwaltete Implementierung, die auf das Modell zugreift. Er nimmt Anfragen des Controllers entgegen, registriert Views und entscheidet, ob diese benachrichtigt werden sollen.

XML-Validator über die managed/unmanaged-Grenze

Wir stützen uns mehr und mehr auf XML-Dokumente. Das hält die Typenvielfalt im Code geringer und die Datenvielfalt kann dort explodieren, wo sie hingehört. Und es ermöglicht dem Fachpersonal auch ohne C++ oder ähnlich komplizierte Sprachen zu ‘programmieren’. XML-Dokumente und XSD sind menschenlesbar und spiegeln genau die Struktur und den Stand der Daten wider. Daten können auch ebenso direkt wie einfach geändert werden, ohne qualifizierte Programmierer zu bemühen. Routinearbeit wie das Eingeben von Daten kann ausgelagert werden und der Programmierer kann sich wieder dem Wesentlichen wie dem Design der Anwendung widmen.
Ein XML-Schema in Form einer oder mehrerer XSD-Dateien hilft, die Struktur der XML-Daten zu spezifizieren und deren Gültigkeit zu überprüfen. Natürlich kann die Gültigkeit auch programmatisch verifiziert werden. Und warum ungültige Datein einlesen? Wenn man denn ein Schema besitzt, sollte man tunlichst die Daten dagegen validieren. Die Anwendung wird dadurch stabiler und die Fachkraft der Domäne kann informative Rückmeldungen seitens der Software bekommen.
Nun habe ich einige Zeit geforscht, wie man XML gegen ein XSD in C++ mit der msxml6.dll überprüft. Da gibt es auch einige Beispiele aber keins hat auf Anhieb funktioniert. Der Beispielcode, darunter Beispiele von Microsoft, mutet archaisch an und viele Datentypen, welche aus der C-Urzeit kommen, sind mir nicht bekannt. Zu kompliziert für meinen Geschmack und ich überlege mir, wieso eigentlich nicht Richtung .NET gehen? Auf meiner Suche nach C#-Code werde ich sofort fündig und als ich das abgetippte Progrämmchen ausprobiere, tut es genau das Erwartete. Also lege ich eine neue DLL für .NET-Auslagerungen an. Denn C# kann ich nicht in ein C++-Projekt hineinmischen. Ich kann C# auch nicht direkt in unmanaged C++ aufrufen. Dafür brauche ich eine Zwischenschicht in C++/CLI. Ich kann eine einzelne Datei in einem C++-Projekt managed kompilieren. Code darin kann von unmanaged heraus aufgerufen werden und seinerseits C#-Objekte instanziieren und benutzen. Ich denke an die Zukunft, in der ich vermehrt C#-Code mit der alten Welt verbinden möchte und überlege mir, ob es nicht besser ist, mein Ziel direkt in C++/CLI zu erreichen. Im Nachhinein widerum denke ich eher, dass C++/CLI doch als Vermittlungsschicht geeignet ist und dass man neue Zeilen bevorzugt in C# schreiben sollte. Aber sei es drum, ich werde dem Thema auf den Fersen bleiben und liste nachfolgend erst einmal den C++/CLI-Code auf.

namespace XMLUtilities
{
ref class Validator
{
public:
	String^ ErrorDescription;
	Validator(Stream^ xmlstream, XmlReader^ xsddocument)
	{
		XmlReaderSettings^ settings = gcnew XmlReaderSettings();
		settings->ValidationType = ValidationType::Schema;

		XmlSchemaSet^ schemas = gcnew XmlSchemaSet();
		settings->Schemas = schemas;
		
		try
		{
			schemas->Add("", xsddocument);
		}
		
		catch (Exception^ err)
		{
			ErrorDescription = err->ToString();
		}
		settings->ValidationEventHandler += gcnew ValidationEventHandler(this, &Validator::HandleError);
		XmlReader^ validator = XmlReader::Create(xmlstream, settings);

		try
		{
			while (validator->Read()) { }
		}
		catch (Exception^ err)
		{
			ErrorDescription = err->ToString();
		}
		finally
		{
			validator->Close();
		}

	}
private:
	void HandleError(Object^ sender, ValidationEventArgs^ args)
	{
		ErrorDescription = args->Message;
	}


};

_declspec(dllexport) CString ValidateXMLString(CString xmlstr, CString xsdstr  )
{
	array<Byte>^ byteArray = Encoding::UTF8->GetBytes(CStringToString(xmlstr));
	MemoryStream^ xmlstream = gcnew MemoryStream(byteArray);

	XmlReader^ xsddocument = XmlReader::Create(gcnew StringReader(CStringToString(xsdstr)));

	Validator v (xmlstream, xsddocument);
	return StringToCString(v.ErrorDescription);
}

_declspec(dllexport) CString ValidateXMLFile(CString xmlfile, CString xsdfile)
{
	FileStream^ xmlstream = gcnew FileStream(CStringToString(xmlfile),  FileMode::Open, FileAccess::Read);
	FileStream^ xsdstream = gcnew FileStream(CStringToString(xsdfile),  FileMode::Open, FileAccess::Read);

	XmlReader^ xsddocument = XmlReader::Create(xsdstream);

	Validator v (xmlstream, xsddocument);
	xmlstream->Close();
	xsdstream->Close();
	return StringToCString(v.ErrorDescription);
}
}

Da ich meinen Code sehe, erkenne ich, dass ich den Validator nicht als ref class schreiben müsste. Eine ref class kann ich von C# aus benutzen. Diesen Typ definiert man für Verbindungen aus der abstrakteren .NET-Welt hin zu der schnellen nativen C++-Welt. Ich will aber hier in die andere Richtung gehen. Natürlich habe ich meinen Code testgetrieben entwickelt. Gerade, wenn man mit vielen neuen Dingen einer Programmwelt ausgesetzt ist, bieten sich Tests an, um programmatisch Herr der Lage zu werden. Für die Tests möchte ich dem Validator manuell geschriebene Daten geben, der Produktivcode möchte konkrete Dateien validiert bekommen. Ich abstrahiere also meinen Validator, dass er keine Dateien kennt und für beide Fälle jeweils mit den gleichen Argumenten initialisiert wird. Dann schreibe ich für Tests und Produktivcode zwei Funktionen, die aus Strings bzw. Dateien jeweils die gleichen Übergabewerte für den Validator erzeugen (_declspec(dllexport) deshalb, weil ich die Funktionen aus der DLL exportiere).

Gerne hätte ich aus dem Validator heraus die verschiedenen Exceptions geworfen, doch in C++ Coding Standards lese ich, dass man so etwas nicht über Modulgrenzen hinaus tun sollte (Keine Steine in des Nachbarn Garten werfen). Nachfolgend werden die Unittests aufgelistet:

[TestInitialize()]
void MyTestInitialize() 
{
	XSDString = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
		"<xsd:schema xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" >" 

		"<xsd:simpleType name=\"Status\">"
		"<xsd:restriction base=\"xsd:string\">"
		"<xsd:enumeration value=\"Angestellter\"/>"
		"<xsd:enumeration value=\"Freiberufler\"/>"
		"<xsd:enumeration value=\"Rentner\"/>"
		"</xsd:restriction>"
		"</xsd:simpleType>"


		"<xsd:element name=\"Person\" >"
		"<xsd:complexType>"
		"<xsd:attribute name=\"Status\" type=\"Status\"></xsd:attribute>"
		"</xsd:complexType>"
		"</xsd:element>"



		"<xsd:element name =\"Root\">"
			"<xsd:complexType>"
				"<xsd:sequence>"
					"<xsd:element ref =\"Person\" minOccurs=\"1\" maxOccurs=\"unbounded\"/>"
				"</xsd:sequence>"
			"</xsd:complexType>"

		"</xsd:element>"

		"</xsd:schema>";


	XMLString = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
		"<Root >"
		"<Person Status=\"Angestellter\" >"
		"</Person>"
		"</Root>";

};

[TestMethod]
void CPPXMLStringValidatorWorks()
{
	CString error = XMLUtilities::ValidateXMLString(XMLString, XSDString);
	Assert::AreEqual (error.IsEmpty(), true);
};

[TestMethod]
void ValidatorXMLSchemaException()
{
	XSDString = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
		"<xsd:schema xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" >"
		"<xsd:element name =\"Kontakt\" minOccurs=\"1\" maxOccurs=\"Sonnenschein\">"
		"<xsd:complexType>"
		"<xsd:sequence/>"
		"</xsd:complexType>"
		"</xsd:element>";

	CString error = XMLUtilities::ValidateXMLString("", XSDString);
	Assert::AreEqual (!error.IsEmpty(), true);


};

[TestMethod]
void CPPValidatorXMLException()
{

	XMLString = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
		"<Root >"
		"<Datenblaetter >"
		"<Datenblatt Description=\"Steckdosen\" ID=\"St1\" LongDescription=\"Steckdosen lang\">"

		/*"</Datenblatt>"*/
		"</Datenblaetter>"
		"</Root>";

	CString error = XMLUtilities::ValidateXMLString("", XSDString);
	Assert::AreEqual (!error.IsEmpty(), true);

};