Fachbereich Elektrotechnik und Informatik
Bachelorarbeit
Entwicklung einer erweiterbaren, serviceorientierten Anwendung unter .NET
Denis Mazuev
Betreuer/Prüfer Prof. Dr. rer. nat. Nikolaus Wulff
II
Eidesstattliche Erklärung
Hiermit versichere ich die vorliegende Arbeit selbstständig verfasst und unter
ausschließlicher Verwendung der angegebenen Quellen und Hilfsmittel erstellt zu
haben.
Münster, den 29. August 2012
_________________________
Denis Mazuev
III
Danksagung
Mein erster Dank gilt meinen Eltern, meinem Bruder und meiner Frau, auf die ich
mich immer verlassen kann und die mich im Laufe der Erstellung dieser Arbeit
motiviert und unterstützt haben.
Des Weiteren bedanke ich mich ganz herzlich bei Herrn Prof. Dr. Nikolaus Wulff für
seine Unterstützung und Betreuung meiner Arbeit.
Weiterhin geht mein Dank an Alexander Serowy, meinen Betreuer in der Firma Betex,
der mir mit fachlicher Kritik zur Seite stand.
Darüber hinaus bedanke ich mich bei allen anderen Personen, die mich bei der
Anfertigung dieser Arbeit in unterschiedlicher Weise unterstützt haben.
IV
Inhaltsverzeichnis
1 EINLEITUNG ............................................................................................................................................. 1
1.1 UNTERNEHMENSPROFIL ................................................................................................................ 1 1.2 HINTERGRUND UND MOTIVATION .................................................................................................. 1 1.3 ZIELSETZUNG ............................................................................................................................... 1 1.4 GLIEDERUNG DER ARBEIT .............................................................................................................. 2
2 GRUNDLAGEN .......................................................................................................................................... 3
2.1 C# ............................................................................................................................................. 3 2.1.1 Eigenschaftsmethoden .................................................................................................... 3 2.1.2 Partielle Klassen ............................................................................................................... 4 2.1.3 Language Integrated Query ............................................................................................ 4
2.2 ENTITY FRAMEWORK .................................................................................................................... 5 2.2.1 Vorgehensweisen ............................................................................................................. 5 2.2.2 Abfragen und Kontext ...................................................................................................... 6
2.3 WINDOWS COMMUNICATION FOUNDATION .................................................................................... 7 2.3.1 Vertrag über ein Interface ............................................................................................... 7 2.3.2 Datenverträge ................................................................................................................. 8
2.4 MANAGED EXTENSIBILITY FRAMEWORK ........................................................................................... 9 2.4.1 Importe und Exporte ........................................................................................................ 9 2.4.2 Kompositionscontainer und Kataloge............................................................................ 10
2.5 WINDOWS PRESENTATION FOUNDATION ....................................................................................... 11 2.5.1 Geschichtliche Entwicklung der GUI-Frameworks für Windows.................................... 11 2.5.2 Wesentliche Merkmale .................................................................................................. 12 2.5.3 XAML ............................................................................................................................. 12
2.6 DAS MODEL-VIEW-VIEWMODEL-PATTERN .................................................................................... 13 2.6.1 Konzept .......................................................................................................................... 13 2.6.2 Verknüpfen der View mit ViewModel ............................................................................ 15 2.6.3 Data Binding und Commands ........................................................................................ 16 2.6.4 Die INotifyPropertyChanged-Schnittstelle ..................................................................... 17
2.7 SERVER..................................................................................................................................... 18 2.7.1 Active Directory ............................................................................................................. 18 2.7.2 Internet Information Services ........................................................................................ 20
3 ANFORDERUNGEN ................................................................................................................................. 21
3.1 ANALYSE ................................................................................................................................... 21 3.2 GROBKONZEPT .......................................................................................................................... 23
4 ENTWURF .............................................................................................................................................. 24
4.1 PLUG-IN-ARCHITEKTUR ............................................................................................................... 24 4.1.1 Laufzeitumgebung und die Module ............................................................................... 24 4.1.2 Projektstruktur und der Vertrag .................................................................................... 25 4.1.3 Benutzeroberfläche der Laufzeitumgebung .................................................................. 26
4.2 SERVICE-ARCHITEKTUR ............................................................................................................... 27 4.3 DATENHALTUNG IM CLIENT ......................................................................................................... 28 4.4 GESAMTARCHITEKTUR ................................................................................................................ 29
5 IMPLEMENTIERUNG ............................................................................................................................... 30
5.1 IMPLEMENTIERUNG DER SERVICESCHICHT ...................................................................................... 30 5.1.1 Vertrag ........................................................................................................................... 30
V
5.1.2 Zusammenspiel mit dem Entity Framework .................................................................. 31 5.1.3 Kommunikation mit dem Service ................................................................................... 36 5.1.4 Hosting........................................................................................................................... 41
5.2 GRUNDGERÜST FÜR MVVM ........................................................................................................ 42 5.2.1 ViewModelBase ............................................................................................................. 42 5.2.2 WorkspaceViewModel ................................................................................................... 44
5.3 UMSETZUNG DES PLUG-IN-KONZEPTS ........................................................................................... 45 5.3.1 Definieren des Vertrags ................................................................................................. 45 5.3.2 Implementierung des Vertrags ...................................................................................... 46 5.3.3 ModuleManager ............................................................................................................ 47 5.3.4 Bereitstellung der Module von der Laufzeitumgebung ................................................. 49
5.4 AUTHENTIFIZIERUNG .................................................................................................................. 53 5.5 DATENZUGRIFF IM CLIENT ........................................................................................................... 55
5.5.1 Repository ...................................................................................................................... 55 5.5.2 RepositoryManager ....................................................................................................... 56
6 FAZIT UND AUSBLICK ............................................................................................................................. 57 7 LISTINGVERZEICHNIS .............................................................................................................................. 58 8 ABBILDUNGSVERZEICHNIS ..................................................................................................................... 59 9 LITERATURVERZEICHNIS ......................................................................................................................... 60
1
1 Einleitung
1.1 Unternehmensprofil
Das Unternehmen Betex IT-Consulting & Solutions wurde im Jahr 2001 gegründet und
bietet ihren Geschäftskunden in ganz Deutschland Beratung und Unterstützung rund
um die IT an, von der Softwareentwicklung über Einrichtung von
Infrastrukturlösungen bis hin zu Telekommunikationslösungen.
In Bezug auf die Softwareentwicklung, liegt der Fokus der Firma auf den
Programmiersprachen PHP, Java und insbesondere .NET.
Betex hat ihren Standort in Münster und beschäftigt zurzeit 7 Mitarbeiter (Stand:
August 2012).
1.2 Hintergrund und Motivation
Bei den Leistungsnehmern des Unternehmens Betex, die ein Softwareprojekt in
Auftrag geben, ist ein steigendes Interesse an erweiterbaren Anwendungen zu
erkennen.
Aus der Sicht des Kunden lassen sich diese durch das Plug-In-Konzept leicht und
gezielt aktualisieren. Auch bei der Entwicklung von solchen Anwendungen ergeben
sich durch die modulare Struktur des Projekts Vorteile. Zu diesen zählen eine bessere
Übersichtlichkeit und Trennung.
1.3 Zielsetzung
Diese Arbeit beschäftigt sich mit der Erstellung einer Grundlage für erweiterbare
Anwendungen unter .NET, die sich leicht an unterschiedliche Anforderungen
anpassen lässt.
Basierend auf dieser Grundlage, wird die interne Zeiterfassung der Firma in Form von
mehreren Plug-Ins für die Anwendung realisiert, um diese unter realen Bedingungen
zu testen.
2
1.4 Gliederung der Arbeit
Im Folgenden wird im Kapitel Grundlagen dem Leser zunächst das notwendige
Grundwissen für das Verständnis dieser Arbeit vermittelt. Daraufhin folgt die
Beschreibung und Analyse der an das Projekt gestellten Anforderungen, worauf
basiert, die Anwendung anschließend entworfen wird. Das Kapitel Implementation
widmet sich der Umsetzung der Problemstellung. Abschließend wird im letzten
Kapitel ein Fazit gezogen und Ausblick auf die mögliche Zukunft des Projekts gegeben.
3
2 Grundlagen
2.1 C#
C# ist eine von Microsoft entwickelte Sprache für die .NET Common Language
Runtime (CLR). Im Folgenden werden Bestandteile der Sprache erläutert, die für das
Projekt eine wichtige Rolle spielen.
2.1.1 Eigenschaftsmethoden
In der objektorientierten Programmierung ist es üblich Zugriffsfunktionen, die s.g.
getter und setter, einzusetzen, um eine Eigenschaft eines Objekts abzufragen oder zu
ändern. Hinter dem Zugriff kann also eine Logik hinterlegt werden und somit werden
die Implementierungsdetails des Objekts verborgen (Black-Box-Prinzip).
Die Eigenschaftsmethoden in C#, auch genannt Properties/Eigenschaften, fassen die
Subroutinen für den Zugriff mit jeweils einem eigenen Anweisungsblock in einem
Container zusammen. Bei der Auswertung der Eigenschaft wird dann der get-Block
ausgeführt und bei einer Zuweisung entsprechend der set-Block. [1]
Anhand des Beispiels in Listing 2.1 ist zu erkennen, wie mithilfe einer Property der
Zugriff auf private Felder gesteuert werden kann. Die Verwendung von
Eigenschaftsmethoden macht den Code übersichtlicher, außerdem spielen sie eine
entscheidende Rolle in Verbindung mit Windows Presentation Foundation (s. 2.6.3).
private double seconds; public double Hours {
get { return seconds / 3600; } set { seconds = value * 3600; }
}
Listing 2.1 – Verwendung einer Eigenschaftsmethode
4
2.1.2 Partielle Klassen
Mithilfe von partiellen Klassen in .NET lassen sich die Klassendefinitionen über
mehrere Sourcedateien verteilen. Dadurch können mehrere Entwickler gleichzeitig an
der gleichen Klasse arbeiten. Auch lässt sich damit automatisch generierter Code sehr
leicht erweitern und sauber vom Code trennen, den der Entwickler schreibt
Bei der Verwendung von partiellen Typen muss lediglich eine Einschränkung beachtet
werden: Alle Klassenfragmente müssen sich in derselben Anwendung befinden.
Partielle Klassen werden durch den Modifizierer partial gekennzeichnet, der vor alle
Teildefinitionen gesetzt wird.
// in der Quellcodedatei 'Circle1.cs' partial class Circle { ... } // in der Quellcodedatei 'Circle2.cs' partial class Circle { ... }
Listing 2.2 – Beispiel: Verwendung von partiellen Klassen
2.1.3 Language Integrated Query
Language Integrated Query (LINQ) stellt eine Sprachergänzung von .NET dar und
wurde mit der Version 3.5 des Frameworks eingeführt, um den Zugriff auf die Daten
zu vereinheitlichen und zu vereinfachen.
Mithilfe einer LINQ Abfrage in Listing 2.3 wird beispielsweise eine Liste von Personen
zurückgegeben, die über 30 Jahre alt sind. Listing 2.4 stellt eine äquivalente Abfrage
ohne LINQ dar. Es ist zu erkennen, dass mithilfe von LINQ der Datenzugriff sehr
kompakt erfolgt, was die Lesbarkeit des Codes erhöht.
var pers = personen.Where(p => p.Alter > 30);
Listing 2.3 – Beispiel: Datenzugriff mithilfe von LINQ
var pers = new List<Person>(); foreach (var p in personen)
if (p.Alter > 30) pers.Add(p);
Listing 2.4 – Beispiel: Datenzugriff ohne LINQ
5
2.2 Entity Framework
Die meisten Anwendungen heutzutage müssen die in Objekten gespeicherten Daten
dauerhaft in Datenbanken sichern. Da die Datenbanken vorherrschend relational sind
und sie ihre Datenstrukturen entsprechend anders abbilden als Objektmodelle,
werden immer öfter objektrelationale Mapper (ORM) als eine zusätzliche Schicht
zwischen der Anwendung und der Datenbank eingesetzt, die sich um die Abbildung
von Objekten auf die Tabellenstruktur kümmert. [2]
Abb. 2.1 - Einsetzen eines O/R-Mappers
Entity Framework (EF) ist der Standard-ORM unter .NET und Teil des Frameworks.
2.2.1 Vorgehensweisen
Bei der Verwendung von EF gibt es folgende Vorgehensweisen:
Model-First:
Die Objekte, die s.g. Entitys, mit ihren Beziehungen werden zunächst in Visual Studio
modelliert und anschließend auf dieser Basis die entsprechende Tabellenstruktur für
die Datenbank generiert. Dieser Ansatz wird verwendet, wenn die Tabellenstruktur
zur Entwicklungszeit noch nicht existiert.
Database-First:
Das Objektmodell wird auf Basis einer bestehenden Datenbank von Visual Studio
automatisch generiert.
Client ORM DB
6
2.2.2 Abfragen und Kontext
Für die Kommunikation mit der Datenbank wird von Entity Framework automatisch
eine Proxy-Klasse, der s.g. Kontext, generiert. Der Kontext stellt Funktionen bereit,
mit denen die Entitätsdaten als Objekte abgefragt und bearbeitet werden können. [3]
In Listing 2.5 wird anhand eines Beispiels demonstriert, wie die Kommunikation mit
der Datenbank über den Kontext erfolgt. Dabei wird zunächst überprüft, ob ein
Mitarbeiter mit dem gegebenen Namen bereits in der Datenbank existiert. Ist dies
nicht der Fall, so wird den Mitarbeitern ein neuer Eintrag vom Typ EmployeeEntity
hinzugefügt und anschließend die Änderungen, durch den Aufruf der Methode
SaveChanges() an dem Kontext, gespeichert.
var c = new ObjectContext("..."); String name = "Koch"; if (c.Employees.FirstOrDefault(o => o.Name.Equals(name)) == null) {
c.Employees.AddObject(new EmployeeEntity { Name = name }); c.SaveChanges();
}
Listing 2.5 – Umgang mit dem Kontext
7
2.3 Windows Communication Foundation
Die Windows Communication Foundation (WCF) ist eine dienstorientierte
Kommunikationsplattform für verteilte Anwendungen in Microsoft Windows und
wurde mit .NET 3.0 eingeführt. Es führt viele Netzwerkfunktionen zusammen, um sie
den Entwicklern solcher Anwendungen standardisiert zur Verfügung zu stellen.
Hauptsächlich wird WCF bei der Entwicklung von Service-orientierten Architekturen
verwendet. [4]
Das Konzept des Endpunktes wird bei WCF durch eine Trennung in Address, Binding
und Contract abstrahiert (ABC Prinzip). [4]
Address (Adresse) beschreibt den Ort des Dienstes durch ein URI
Binding (Anbindung) definiert die Art der Kommunikation, unter anderem
durch die Angabe der Kodierung und des zu verwendenden Protokolls
Contract (Vertrag) stellt den Vertrag zwischen Service-Verwender und
Anbieter
2.3.1 Vertrag über ein Interface
Um eine Kommunikation zwischen dem WCF-Service und einem Client zu
ermöglichen, wird zwischen den beiden Seiten ein Vertrag in Form einer Schnittstelle
definiert. Diese beschreibt die Funktionen, die der Service zur Verfügung stellt.
Mithilfe von diesen Informationen wird vom WCF eine entsprechende Klasse
generiert, die im Client für den Zugriff auf den Service verwendet wird.
Wird unter Visual Studio ein neues WCF-Projekt angelegt, so befinden sich in diesem
bereits eine Schnittstelle und deren Implementierung. Die Schnittstelle ist dabei mit
einem ServiceContract-Attribut ausgestattet und wird dadurch von WCF als
Kommunikationsvertrag behandelt. Damit von der Schnittstelle definierte Methoden
automatisch Teil des Vertrags werden, müssen diese ebenfalls mit einem
entsprechenden Attribut versehen werden. Listing 2.6 demonstriert anhand eines
Beispiels die Definition eines WCF-Vertrags.
[ServiceContract] public interface ITimekeeping {
[OperationContract] bool Ping(); ...
Listing 2.6 – Definieren eines WCF-Vertrags
8
2.3.2 Datenverträge
Ein Datenvertrag ist eine formale Vereinbarung zwischen einem Dienst und seinem
Verwender, mit dem die auszutauschenden Daten abstrakt beschrieben werden. Für
die Kommunikation zwischen den beiden Seiten bedeutet dies also, dass sie nicht
denselben Typ verwenden, sondern nur dieselben Datenverträge. In einem
Datenvertrag wird für jeden Parameter oder Rückgabetyp genau definiert, welche
Daten für einen Austausch serialisiert werden. [5]
Zum Serialisieren und Deserialisieren von Daten verwendet WCF standardmäßig ein
Serialisierungsprogramm. Dabei können alle primitiven Typen von .NET Framework
wie Ganzzahlen und Zahlenfolgen, sowie bestimmte als Primitive behandelte Typen
wie DateTime und TimeSpan, ohne weitere Vorbereitung serialisiert werden, weil
diese Typen gewissermaßen mit Standardverträgen ausgestattet sind. [5]
Für neue komplexe Typen müssen entsprechende Datenverträge definiert werden,
damit sie serialisierbar sind. Mithilfe von Attributen wie DataContract und
DataMember kann ein Datenvertrag explizit erstellt werden. Dazu wird das
DataContract-Attribut auf den Typ angewendet, der eine Klasse, Struktur oder
Enumeration darstellt. Jeder Member des Datenvertragstyps wird zudem mit dem
DataMember-Attribut versehen. [5]
Listing 2.7 zeigt anhand eines Beispiels, wie ein Datenvertrag für den Typ Customer
definiert wird. Dieser Typ kann in einem Dienstvertrag für die Client/Service-
Kommunikation verwendet werden.
[DataContract] public class Customer {
[DataMember] public string Name { get; set; } [DataMember] public bool IsValid { get; set; }
}
Listing 2.7 – Definieren eines Datenvertrags
9
2.4 Managed Extensibility Framework
Managed Extensibility Framework (MEF) ist eine Bibliothek zur Lösung des Problems
der Erweiterbarkeit einer Anwendung zur Laufzeit und ist seit Version 4.0 Bestandteil
des .NET Frameworks.
2.4.1 Importe und Exporte
Im Wesentlichen geht es beim MEF um das Laden von Komponenten zur Laufzeit und
deren anschließende Bindung an die entsprechenden Variablen.
Mithilfe von folgenden Attributen werden dabei die Rollen definiert:
Export: Objekte, die mit diesem Attribut versehen sind, stellen Komponente
dar, die geladen und instanziiert werden sollen.
Import: die Importe sind die Variablen, an die die Komponenteninstanzen,
nachdem sie geladen sind, gebunden werden.
Listing 2.8 zeigt, wie die Rollenverteilung mithilfe von MEF-Attributen erfolgt: Die
Klasse Component1 wird als das zu exportierende Modul markiert, während die
Variable proxy die Instanz dieser Komponente nach dem Import enthalten soll. [6]
[Export] class Component1 : IComponent{} . . . [Import] IComponent proxy
Listing 2.8 – Definieren von Imports und Exports
10
2.4.2 Kompositionscontainer und Kataloge
Kompositionscontainer und Kataloge stellen die wichtigsten Module von MEF dar.
Ein Kompositionscontainer enthält alle verfügbare Komponente (Exporte) und führt
die Komposition aus, womit das Zuweisen von Importen zu Exporten gemeint ist. MEF
definiert mehrere Containertypen für unterschiedliche Problemstellungen.
CompositionContainer ist dabei der am häufigsten verwendete Typ und verwaltet die
Komposition von Exporten. [7]
Zur Ermittlung von verfügbaren Exporten wird von den Kompositionscontainern ein
Katalog verwendet. MEF stellt mehrere Katalogtypen bereit, um die Komponenten
beispielsweise in Assemblys oder in den Verzeichnissen ausfindig zu machen. Besteht
die Notwendigkeit, dass die Exporte aus anderen Quellen ermittelt werden sollen, so
können leicht neue Typen erstellt werden. [7]
Listing 2.9 demonstriert anhand eines Beispiels, wie mithilfe von MEF eine
Komposition erfolgt. Die Eigenschaft Component ist mit dem Import-Attribut
markiert. Zusätzlich wird der Typ der zu importierenden Komponente angegeben. In
der Funktion LoadComponent() erfolgt die Komposition. Dafür wird ein Katalog vom
Typ DirectoryCatalog definiert, welcher mit dem Pfad zu der zu importierenden
Komponente initialisiert wird. Anschließend wird ein CompositionContainer erstellt,
dem die Quelle des Exports in Form des Katalogs übergeben wird. Mit dem Aufruf der
Methode ComposeParts() am Container, initialisiert MEF die Komponente im
übergebenen Verzeichnis und speichert die Referenz auf diese in Component.
[Import(typeof(IExport))] private IExport Component { get; set; } public void LoadComponent() { var catalog = new DirectoryCatalog(path); var container = new CompositionContainer(catalog); container.ComposeParts(this);
} Listing 2.9 - Beispiel einer Komposition mit MEF
11
2.5 Windows Presentation Foundation
Windows Presentation Foundation (WPF) stellt das moderne Programmiermodell für
die Entwicklung von Benutzeroberflächen unter Windows dar und steht seit der
Einführung des .NET Frameworks 3.0 zu Verfügung.
2.5.1 Geschichtliche Entwicklung der GUI-Frameworks für Windows
Zu den Zeiten von Windows 1.0 gab es nur eine Möglichkeit Windows-Anwendungen
zu schreiben: Mit der Programmiersprache C und unter Verwendung der ebenfalls in
C geschriebenen Windows-Programmierschnittstelle (Windows-API). [8]
Die direkte Verwendung der Windows-API führte aufgrund des sehr niedrigen und
detaillierten Betriebssystem-Levels und sich daraus resultierenden vielen
Funktionsaufrufen zu Unmengen von Code, was die Übersichtlichkeit erheblich
beeinträchtigte. Es bestand also der Bedarf nach Programmbibliotheken, die den
Umgang mit der Windows-API vereinfachen, so dass bei der Entwicklung der Blick auf
das Wesentliche fällt. [8]
Microsoft Foundation Classes (MFC) war der erste Schritt in die richtige Richtung und
wurde von Microsoft für C++ als objektorientierte „Wrapper“-Bibliothek entwickelt.
MFC kapselte die Aufrufe der Windows-API und fasste diese zu logischen,
abstrakteren Einheiten zusammen. [8]
Mit der Einführung des .NET Frameworks kam mit Windows Forms ein neues
Programmiermodell zur Entwicklung von Windows-Anwendungen. Im Vergleich zu
dem Vorgänger MFC fiel der Einstieg in die neue Programmierschnittstelle leichter
und grafische Benutzeroberflächen ließen sich damit noch komfortabler erstellen.
Obwohl zwischen den Einführungen der beiden Programmiermodelle zehn Jahre
liegen, stellt auch Windows Forms lediglich einen Wrapper der existierenden
Windows API in Managed Code dar. [8]
12
2.5.2 Wesentliche Merkmale
WPF schlägt, anders als die bisherigen Programmiermodelle von Microsoft, die nur
dünne Wrapper um die Windows API darstellen, einen neuen, zeitgemäßen Weg ein
und stellt das erste Programmiermodell für Benutzeroberflächen dar, das fast
vollständig in .NET geschrieben ist.
Für die Darstellung setzt WPF DirectX anstelle von GDI ein und kann somit auf die
Leistung moderner Grafikkarten zurückgreifen, um höchst flexible und performante
Windows-Anwendungen zu erstellen.
Durch die Tatsache, dass DirectX für das Zeichnen der Komponente genutzt wird,
lässt sich mithilfe von Styles und Templates das Erscheinungsbild von Controls
komplett anpassen.
WPF zeichnet die Inhalte vektorbasiert und ermöglicht damit eine beliebige
Skalierung der Anwendung.
2.5.3 XAML
Die Extensible Application Markup Language (XAML) ist eine XML-basierte
Beschreibungssprache, die bei der WPF zu Erstellung von Benutzeroberflächen
eingesetzt wird. [8]
XAML bietet im Vergleich zu der alternativen Möglichkeit, die Oberflächen rein in C#
zu erstellen, folgende Vorteile:
Die Beschreibung einer GUI in XAML ist wesentlich kompakter.
Darstellung der Anwendung lässt sich besser von der Businesslogik trennen.
Bessere Aufgabenverteilung wird ermöglicht: Benutzeroberfläche kann von
einem Designer mithilfe von speziellen Tools wie Expression Blend komplett
entworfen werden, während ein Entwickler sich um die Implementierung der
Logik kümmert.
13
Listing 2.10 stellt einen gültigen XAML-Ausschnitt dar und beschreibt ein StackPanel,
welches ein Textfeld und ein Button beinhaltet. Das entsprechende Ergebnis ist in
Abb. 2.2 zu sehen.
<StackPanel>
<TextBlock Margin="20" HorizontalAlignment="Center">Hello XAML!</TextBlock> <Button Margin="10" HorizontalAlignment="Center">OK</Button>
</StackPanel>
Listing 2.10 – Beispiel: XAML-Ausschnitt
Abb. 2.2 – Beispiel: Mit XAML erstellte GUI
2.6 Das Model-View-ViewModel-Pattern
Das Model-View-ViewModel-Pattern (MVVM) stellt eine moderne Variante des
Model-View-Controller-Patterns (MVC) dar und erlaubt eine bessere Trennung von
UI-Design und UI-Logik. [8]
2.6.1 Konzept
MVVM-Pattern wurde im Zusammenhang mit der WPF eingeführt. Die Ziele des
Patterns sind eine lose Kopplung von Benutzeroberfläche und UI-Logik (Event Handler
& Co). Dies ermöglicht eine bessere Zusammenarbeit mit den Designern und eine
bessere Überprüfung der Logik mit Unit Tests. [8]
Das MVVM-Pattern basiert auf folgenden drei Komponenten:
View: die Benutzeroberfläche (XAML + Codebehind)
ViewModel: eine Klasse, die das Model kapselt und Properties (2.1.1)
bereitstellt, an die sich die View binden kann
Model: das Datenmodell; üblicherweise Klassen, die lediglich die Daten
enthalten
14
Abb. 2.3 zeigt die Abhängigkeiten der Komponenten im MVVM-Pattern. Die View
kennt das ViewModel. Das ViewModel kennt das Model, aber nicht die View (lose
Kopplung). Das Model kennt weder die View noch das ViewModel. [8]
Abb. 2.3 – Die Abhängigkeiten beim MVVM-Pattern
Das Model spielt im MVVM-Pattern dieselbe Rolle wie im MVC-Pattern und ist für die
Kapselung der Daten zuständig, die je nach Applikation in unterschiedlichen
Formaten vorliegen können. [8]
Die Aufgabe der View, das Darstellen von Daten, ist im MVVM- und MVC-Design
ebenfalls identisch. Die View enthält alle grafischen Elemente des User-Interfaces und
wird in der WPF typischerweise deklarativ in XAML definiert. [8]
Das ViewModel hat die Aufgabe, alle Informationen bereitzustellen, die für die
Aufbereitung der View benötigt werden. Dazu gehören sowohl die Daten des Models,
aber auch sehr UI-nahe Informationen, wie beispielsweise ob ein Button ausgegraut
(disabled) ist. Jegliche Logik für die Behandlung von Benutzereingaben ist im
ViewModel auch enthalten. Die Benutzereingaben werden über die View
entgegengenommen und direkt per Data Binding an das ViewModel weitergeleitet
und dort behandelt. Damit übernimmt das ViewModel auch einen großen Teil der
Funktionalität des Controllers im MVC-Pattern. [8]
View
ViewModel
Model
15
2.6.2 Verknüpfen der View mit ViewModel
Damit sich die UserControls in der View an die Properties des ViewModels binden
können, muss der View das entsprechende ViewModel bekannt gemacht werden.
Dies geschieht über das Setzen der DataContext-Property der View auf die
Bindungsquelle (ViewModel).
Prinzipiell gibt es dafür folgende Möglichkeiten:
Direktes Initialisieren des DataContexts der View, beispielsweise in der s.g.
Code-Behind-Datei
Definieren eines DataTemplates im Ressourcenbereich der View
In Listing 2.11 wird gezeigt, wie der DataContext von DayView direkt mit der
Bindungsquelle in der Code-Behind-Datei initialisiert wird.
// Interaktionslogik für DayView.xaml public partial class DayView : UserControl {
public DayView() { InitializeComponent(); this.DataContext = new DayViewModel(); }
}
Listing 2.11 – Direktes setzen des DataContexts der View im Codebehind
Listing 2.12 demonstriert, wie mithilfe eines DataTemplates in XAML für das
DayViewModel als View die DayView festgelegt wird. Damit wird WPF bekannt
gemacht, dass das DayViewModel als DayView gezeichnet wird. Außerdem wird der
DataContext der View automatisch auf das entsprechende ViewModel gesetzt und
somit die Bindung ermöglicht.
<DataTemplate DataType="{x:Type vm:DayViewModel}">
<vw:DayView /> </DataTemplate>
Listing 2.12 – Setzen des DataContexts der View per DataTemplate
Setzen des DataContexts per DataTemplate stellt die bessere der beiden
Möglichkeiten dar. Zum einen strebt MVVM an, dass die Code-Behind-Datei leer
bleibt und die View nur mittels XAML definiert wird, zum anderen lassen sich die
DateTemplates in einem Ressourcenwörterbuch ablegen, was eine bessere Übersicht
bietet und die Anpassungen zentral vorgenommen werden können.
16
2.6.3 Data Binding und Commands
Data Binding ermöglicht es, eine Dependency Property an eine andere Property zu
binden. Dependency Propertys stellen dabei gewöhnliche .NET Properties dar, die in
ihrer Funktionalität erweitert sind und durch Methoden wie Formatierung,
Datenbindung, Animation und Vererbung festgelegt werden können. [8]
Mithilfe von Data Binding können die Controls der View, wie bspw. ListView und
TextBlock, an die Eigenschaften des zugehörigen ViewModels gebunden werden.
Listing 2.13 zeigt, wie die Dependency Property Text von TextBlock an die Property
DayStatus des ViewModels gebunden wird, mit dem der DataContext der View
initialisiert ist.
<TextBlock Text="{Binding DayStatus}"/>
Listing 2.13 – Data Binding
Ein Command stellt unter WPF ein Objekt vom Typ ICommand dar und definiert eine
abstraktere, losgelöste Form eines Events. [8]
Einige Controls in WPF wie Button, bieten eine Dependency Property namens
Command an. An diese kann eine entsprechende Eigenschaft des ViewModels
gebunden werden, die das Ereignis behandelt.
Wie in Listing 2.14 zu sehen, enthält das Interface ICommand zwei Methoden und ein
Event.
public interface ICommand {
public bool CanExecute(object parameter); public void Execute(object parameter); public event EventHandler CanExecuteChanged;
}
Listing 2.14 – Das Interface ICommand
Mithilfe einer geeigneten Implementierung von ICommand können dem Command
Objekt als Parameter für die Methoden die s.g Delegaten übergeben werden, die
unter .NET Funktionszeiger darstellen.
17
2.6.4 Die INotifyPropertyChanged-Schnittstelle
Um der View bekannt zu geben, dass sich die Werte der Eigenschaften im ViewModel
geändert haben und die View die Bindungen entsprechend aktualisieren muss, muss
das ViewModel die Schnittstelle INotifyPropertyChanged implementieren.
Listing 2.15 zeigt die Deklaration der Schnittstelle, die lediglich ein Event namens
PropertyChanged definiert.
public interface INotifyPropertyChanged { event PropertyChangedEventHandler PropertyChanged; }
Listing 2.15 – Das Interface INotifyPropertyChanged
Beim setzen des DataContexts der View auf die Instanz eines ViewModels, registriert
sich die View automatisch an dem Event PropertyChanged, falls das Interface
INotifyPropertyChanged vom ViewModel implementiert ist. Um die View über die
Änderung an einer Property zu informieren, muss das Ereignis vom ViewModel
ausgelöst werden und als Ereignisparameter der Name der entsprechenden
Eigenschaft übergeben werden.
Listing 2.16 demonstriert die Verwendung des Ereignisses PropertyChanged. Nach der
Berechnung des Ergebnisses wird das Ereignis mit dem Namen der Property Result
ausgelöst. Der an diese Eigenschaft gebundene Control wird somit aktualisiert.
public class DemoViewModel : INotifyPropertyChanged { public int Result { get; set; } public event PropertyChangedEventHandler PropertyChanged; private void Calculate() { // Do something with result … // Actualize the binding if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs("Result")); } } }
Listing 2.16 – Aktualisierung der Bindung mit dem Event PropertyChanged
18
2.7 Server
2.7.1 Active Directory
Active Directory (AD) stellt den Verzeichnisdienst von Microsoft Windows Server, mit
dessen Hilfe verschiedene Objekte wie bspw. Benutzer, Gruppen, Computer und
andere Geräte wie Drucker und Scanner und deren Eigenschaften in einem Netzwerk
verwaltet werden können. Mit Hilfe von Active Directory können die Informationen
der Objekte durch einen Administrator organisiert, bereitgestellt und überwacht
werden. [9]
Abb. 2.4 – Active Directory in einem Windows Server Netzwerk (Quelle: Microsoft)
Active Directory ist hierarchisch gegliedert. Objekte werden in Containern
(Organisationseinheiten) abgelegt, auf die entsprechende Gruppenrichtlinien
angewandt werden können. Einige Organisationseinheiten sind vordefiniert,
beliebige weitere können als Subeinheiten erstellt werden. [9]
19
Gruppen
Mithilfe von Gruppen werden Benutzerkonten, Computerkonten und sonstige
Gruppenkonten zu leicht verwaltbaren Einheiten zusammengefasst. Durch die
Verwendung von Gruppen wird die Verwaltung vereinfacht, indem vielen Konten in
einem Schritt einheitliche Berechtigungen und Rechte zugewiesen werden können.
Active Directory kennt folgende zwei Typen von Gruppen: Verteilergruppen und
Sicherheitsgruppen. Mithilfe von Verteilergruppen können E-Mail-Verteilerlisten
erstellt, und mit Sicherheitsgruppen die Berechtigungen für freigegebene Ressourcen
zugewiesen werden. [10]
SID und deren Aufbau
Für die dauerhafte Identifizierung jeder Gruppe und jedes Benutzers wird von
Microsoft Windows NT automatisch ein Sicherheits-Identifikatior (Security Identifier,
SID) vergeben. An die SID werden festgelegte Zugriffsrechte gebunden. Sollten sich
die Namen von Systemen oder Gruppen ändern, so bleibt deren SID unverändert, was
eine problemlose Namensgebung ermöglicht. [11]
Eine SID kann wie folgt aussehen: S-1-5-21-7623811015-3361044348-030300820-1013
Aus folgenden Bestandteilen setzt sie sich zusammen:
S - Abkürzung für SID
1 - Revisionsnummer
5 - Identifier
21-7623811015-3361044348-030300820 - Domäne oder lokales System
1013 - Benutzernummer
Zugriff
Mithilfe von Lightweight Directory Access Protocol (LDAP), welches ein
Anwendungsprotokoll aus der Netzwerktechnik darstellt, lässt sich Active Directory
über ein IP-Netzwerk abfragen und modifizieren. [12] .NET stellt im Namespace
System.DirectoryServices verschiedene Klassen zur Verfügung für die Kommunikation
mit dem Verzeichnisdienst.
Authentifizierung in einer Anwendung mittels Active Directory
Durch das Herausfinden der SID des angemeldeten Benutzers in einer Anwendung
und die anschließende Überprüfung, zu welcher Sicherheitsgruppe in Active Directory
die SID gehört, kann leicht eine Authentifizierung realisiert werden.
20
2.7.2 Internet Information Services
Internet Information Services (IIS) stellt einen Webserver von Microsoft dar. IIS
unterstützt gängige Kommunikationsprotokolle wie HTTP, HTTPS, FTP, SMTP und
andere. Über IIS können ASP.NET – Anwendungen1 und WCF – Services (s. 2.3)
ausgeführt werden, sowie durch das Installieren von passenden Filtern – auch PHP2
und JSP3. [13]
1 Active Server Pages .NET ist eine serverseitige Technologie von Microsoft zum Erstellen dynamischer Webseiten, Webanwendungen und Webservices auf Basis des Microsoft-.NET-Frameworks 2 Hypertext Preprocessor ist eine Skriptsprache, die hauptsächlich zur Erstellung dynamischer Webseiten oder Webanwendungen verwendet wird 3 JavaServer Pages ist eine von Sun Microsystems entwickelte, auf JHTML basierende Web-Programmiersprache zur einfachen dynamischen Erzeugung von HTML- und XML-Ausgaben eines Webservers
21
3 Anforderungen
In erster Linie geht es bei diesem Projekt um die Entwicklung einer Anwendung, die
folgenden Anforderungen entspricht:
Funktionsumfang durch Plug-Ins erweiterbar
Service-orientierte Architektur
Benutzer- und Rollenverwaltung über Active Directory
WPF als GUI-Framework
Anschließend sollen für die Anwendung zu Testzwecken folgende Module erstellt
werden:
Verwaltung von Arbeitszeiten
Projekt und Kundenmanagement
Auswertung der Arbeitszeiten
3.1 Analyse
Pluginfähigkeit
Das Plug-In-Konzept bietet mehrere Vorteile:
Bessere Trennung des Codes, da jedes Modul als eigenständiges Projekt
entwickelt werden kann
Der Funktionsumfang der Anwendung lässt sich leicht zusammenstellen und
erweitern
Aktualisierung gestaltet sich leicht, denn es reicht aus, das entsprechende
Plug-In mit der neuen Version zu ersetzen
Benutzerverwaltung
In den meisten Firmen, die Windows einsetzen, geschieht die interne Verwaltung der
Mitarbeiter über Active Directory. Mithilfe von speziell für die Anwendung erstellten
Sicherheitsgruppen, denen die entsprechenden Mitarbeiter zugewiesen werden,
kann mit wenig Aufwand eine flexible und zentrale Verwaltung der Benutzer realisiert
werden.
22
Serviceorientierte Architektur
Da es sich im Falle der Arbeitszeiterfassung um verteilte Anwendungen handelt, die
alle die gleiche Funktionalität aufweisen und entsprechend identische Abfragen an
die Datenbank stellen, bietet es sich an, in Form eines Services eine zusätzliche
Schicht zwischen den Anwendungen (Clients) und der Datenbank einzuführen, die
sich um die Kommunikation kümmert.
Abb. 3.1 soll die Architektur verdeutlichen. Die Clients enthalten keinerlei Abfrage-
Logik, diese ist nämlich im Service untergebracht. Diese Architektur hat den Vorteil,
dass der Service unabhängig von seinen Konsumenten aktualisiert werden kann.
Beispielsweise kann dadurch leicht die Authentifizierung im Service angepasst
werden, ohne dass jeder einzelne Client dafür ein entsprechendes Update bekommt.
Abb. 3.1 – Service-orientierte Architektur
Grafik-Framework
Unter .NET stehen dem Entwickler zwei Programmierschnittstellen zu Erstellung
grafischer Oberflächen zur Verfügung: Windows Forms und Windows Presentation
Foundation.
Windows Forms stellt dabei das Auslaufmodel dar und wird von Microsoft nicht mehr
weiterentwickelt. WPF hingegen ist das aktuelle Programmiermodell und somit
zukunftssicher. Im Hinblick auf die Möglichkeiten ist WPF seinem Vorgänger zudem
deutlich überlegen und mit dem MVVM-Pattern lassen sich übersichtliche und gut
testbare Anwendungen erstellen.
DB
Service
Client Client Client
23
3.2 Grobkonzept
Der Funktionsumfang der Anwendung soll durch ihre Plug-Ins definiert werden. Der
Client stellt in diesem Fall die Laufzeitumgebung dar, die zur Laufzeit Module
einbindet und diese anschließend bereitstellt. Außerdem verfügt der Client über
grundlegende Funktionalitäten wie Einstellungen und Info.
Die Module sind eigenständige Klassenbibliothek-Projekte und liegen der
Laufzeitumgebung in Form von Assemblys vor. Das Laden von Komponenten wird
mithilfe von MEF realisiert.
Die Service-Schicht kümmert sich um alle Abfragen des Clients. Umgesetzt wird der
Service unter Verwendung von WCF. Für die Kommunikation mit der Datenbank wird
intern EF eingesetzt.
Abb. 3.2 – Grobentwurf
DB
Client
Entity Framework
bindet ein (MEF)
Service (WCF)
Modul (Assembly)
Abfragen
24
4 Entwurf
Basierend auf der vorhergehenden Analyse, wird die Anwendung im folgenden
Kapitel schrittweise entworfen.
4.1 Plug-In-Architektur
4.1.1 Laufzeitumgebung und die Module
Den Anforderungen nach soll als GUI-Framework WPF benutzt werden. Diese
Vorgabe hat einen entscheidenden Einfluss auf die Struktur der Module und die Art
und Weise, wie diese von der Laufzeitumgebung zur Verfügung gestellt werden.
Da als Architekturmuster das Model-View-View-Model-Pattern (s. 2.6) genutzt wird,
stellt ein Modul entsprechend ein Paket von ViewModels mit den dazugehörigen
Views und Models dar. Die Views lassen sich hierarchisch zusammenfassen mit einer
Haupt-View in der obersten Ebene. Durch das direkte Setzen von DataContext (s.
2.6.2) kann der Haupt-View im Modul das Haupt-ViewModel zugewiesen werden, das
die darunterliegenden ViewModels verwaltet. Somit muss der Laufzeitumgebung
unter anderem die oberste View des Moduls übergeben werden, um dieses im
Content-Bereich (s. 4.1.3) rendern zu können.
Abb. 4.1 zeigt den Aufbau eines Moduls aus der Architektursicht. Die Models sind aus
Übersichtlichkeitsgründen nicht in der Abbildung enthalten.
Abb. 4.1 – Hierarchische Struktur eines Moduls
MainView
MainViewModel
View
ViewModel
View
ViewModel
25
4.1.2 Projektstruktur und der Vertrag
Um eine direkte Kopplung zwischen den Modulen und der Laufzeitumgebung zu
vermeiden, wird ein Communication-Projekt angelegt, auf welches beide Seiten
verweisen. Communication stellt das zentrale Projekt in der Projektmappe dar und
beinhaltet alle Komponente, die beiden Seiten zur Verfügung stehen sollen.
Abb. 4.2 – Grundlegende Projektstruktur
Durch diese Trennung ergeben sich zudem folgende Vorteile:
Module können unabhängig vom Client entwickelt werden.
Durch die Anpassung des Projekts Communication kann die Anwendung leicht
an eine neue Problemstellung angepasst und somit universell eingesetzt
werden.
Zwischen der Laufzeitumgebung und den Modulen wird im Communication-Projekt
ein Vertrag in Form der IModule-Schnittstelle definiert. Diese wird von den Modulen
entsprechend implementiert, um vom Client mithilfe von MEF geladen werden zu
können.
Client Communication Modul <<access>> <<access>>
26
4.1.3 Benutzeroberfläche der Laufzeitumgebung
Wie in Abb. 4.3 zu sehen, gliedert sich die GUI des Clients in folgende Bereiche:
Plug-In Auswahl: hier erscheinen in Form einer dynamisch generierten Liste
von Schaltflächen, die Namen der eingebundenen Module, die ausgewählt
werden können.
Content: in diesem Bereich wird das aktive Modul bzw. Info/Einstellungen
gerendert.
Menü: von der Laufzeitumgebung fest definierte Menüpunkte.
Abb. 4.3 – GUI Entwurf der Laufzeitumgebung
Content
Einstellungen
Info
Plug-In #1
Plug-In #2
Plug-In Auswahl
Menü
27
4.2 Service-Architektur
Um eine bessere Trennung des Codes zu erreichen, soll die Implementierung des
Service-Vertrags nicht direkt mit dem Entity Framework-Kontext kommunizieren.
Stattdessen wird zwischen dem Service und dem ORM eine zusätzliche Schicht, in
Form der Klasse DataManager, eingeführt.
In Bezug auf die Wiederverwendbarkeit des Projekts, bedeutet die Einführung der
Datenzugriffschicht, dass für den Zugriff auf die Datenbank nicht unbedingt ein
Service notwendig ist. Durch die direkte Verwendung von DataManager für den
Datenzugriff kann das Projekt leicht für nicht verteilte Anwendungen angepasst
werden.
Abb. 4.4 verdeutlicht den Aufbau des Services und den alternativen Datenzugriff, im
Falle, dass das Projekt für nicht verteilte Anwendungen wiederverwendet wird.
Abb. 4.4 – Flexibler Datenzugriff durch den DataManager
Client
DB
Service
EF-Kontext
Data Manager
28
4.3 Datenhaltung im Client
Das Separation of Concerns (SoC) Prinzip gibt vor, dass die unterschiedlichen Bereiche
einer Anwendung klar getrennt und voneinander so unabhängig wie möglich sein
sollen. Die einzelnen Komponenten werden jeweils auf eine Aufgabe fokussiert und
lassen sich dadurch leicht verstehen. Darüber hinaus führt SoC auch zu gut testbaren
Komponenten, weil der Zweck einer Codeeinheit fokussiert wird und weniger breit
getestet werden muss. [14]
Dieses Prinzip soll auch beim Datenzugriff weitergeführt werden. Das ViewModel ist
zwar für die Domainlogik und damit Manipulation der Daten zuständig, der Code für
den Datenzugriff sollte aber nicht direkt im ViewModel erfolgen, sondern in
Repositories ausgelagert werden, die ihrerseits von einem RepositoryManager
verwaltet werden.
Da RepositoryManager allen ViewModels zur Verfügung stehen muss und somit eine
zentrale Einheit in der Anwendung darstellt, bietet es sich an, die Klasse entweder als
statisch oder als Singleton zu implementieren. Diese Vorgehensweisen bringen
jedoch gewisse Nachteile mit sich. Unter anderem kann dadurch das Testen
komplizierter sein. Aus diesem Grund wird RepositoryManager als eine normale
Klasse implementiert, die beim Start der Anwendung initialisiert und anschließend an
alle ViewModels, die mit den Daten arbeiten sollen, per Konstruktor übergeben wird.
Abb. 4.5 verdeutlicht das Konzept der Datenhaltung nach dem Repository-Pattern.
Abb. 4.5 – Datenhaltung im Client
ViewModel ViewModel ViewModel
Repository Typ A
Repository Typ B
RepositoryManager
29
4.4 Gesamtarchitektur
Abb. 4.6 repräsentiert die im Laufe der Entwurfsphase entstandene, grundlegende
Architektur der Anwendung.
Abb. 4.6 – Gesamtarchitektur
Client Modul
DB
IIS
Datenbank-Server
Communication
IModule lädt impl.
HTTP/SOAP
Service
EF-Kontext
Data Manager
30
5 Implementierung
Dieses Kapitel geht auf die Implementierungsdetails der wichtigen Komponenten der
Anwendung ein.
5.1 Implementierung der Serviceschicht
5.1.1 Vertrag
Um eine Kommunikation zwischen dem WCF-Service und dem Client zu ermöglichen,
wird zwischen den beiden Seiten ein Vertrag in Form einer Schnittstelle definiert.
Diese beschreibt die Funktionen, die der Service zur Verfügung stellt. Mithilfe von
diesen Informationen wird vom WCF eine entsprechende Klasse generiert, die im
Client für den Zugriff auf den Service verwendet wird.
Für den Service wird ein eigenes WCF-Projekt angelegt. Das Interface ITimekeeping
stellt den Servicevertrag dar, deren Umfang sich durch die Analyse der
Serviceanforderungen für die Zeiterfassung ergibt.
Abb. 5.1 – Diagramm: Servicevertrag
31
Listing 5.1 zeigt einen Ausschnitt mit einigen Methodensignaturen aus dem Vertrag.
[ServiceContract] public interface ITimekeeping {
[OperationContract] bool Ping();
[OperationContract] List<Employee> LoadAllEmployees();
... }
Listing 5.1 – Ausschnitt aus dem Servicevertrag
5.1.2 Zusammenspiel mit dem Entity Framework
Die Implementierung des Vertrags verwendet das Entity Framework für die
Kommunikation mit der Datenbank. Das Objektmodell wird nach dem Model-First-
Ansatz in Visual Studio erstellt und anschließend auf die Datenbank abgebildet.
Abb. 5.2 – Diagramm: Objektmodell
32
Serialisierung
Die vom EF aus dem Objektmodell generierten Entity-Objekte können zunächst nicht
direkt für die Client/Service-Kommunikation verwendet werden, da es sich um
komplexe Typen handelt und das Serialisierungsprogramm von .NET sie entsprechend
nicht automatisch serialisieren kann.
Um die Serialisierung von Entitys zu ermöglichen, wird für jedes Objekt ein
entsprechender Datenvertrag definiert und mithilfe von partiellen Klassen eine
Methode zur Konvertierung in den serialisierbaren Typ hinzugefügt.
Für die Datenverträge wird eine abstrakte Oberklasse DataObjectBase definiert, von
der sich alle Datenverträge ableiten. Anhand des Typs Employee in Abb. 5.3 ist zu
erkennen, dass alle wichtigen Eigenschaften von EmployeeEntity übernommen
werden. Um Employee als Authentifizierungsobjekt verwenden zu können, wird es
zusätzlich um die boolesche Eigenschaft IsAdmin ergänzt.
Abb. 5.3 – Diagramm: Datenverträge
33
Listing 5.2 zeigt die Definition eines Datenvertrags. Um die Objekte für WPF
bindungsfähig zu machen, sind alle Klassenattribute als Propertys deklariert.
[DataContract] public class Employee : DataObjectBase {
[DataMember] public string Name { get; set; } [DataMember] public string SID { get; set; } [DataMember] public bool IsInternal { get; set; } [DataMember] public bool IsAdmin { get; set; } [DataMember] public List<int> Projects { get; set; }
public override string ToString() { return Name; }
}
Listing 5.2 – Datenvertrag für Employee
Listing 5.3 zeigt, wie mittels einer partiellen Klasse EmployeeEntity um eine
Konvertierungsmethode erweitert wird, die alle Eigenschaften der Entity kopiert und
ein neues, serialisierbares Objekt zurückliefert.
public partial class EmployeeEntity {
public Employee ToDO() { List<int> projects = new List<int>(); foreach (var p in this.Projects) projects.Add(p.ID);
return new Employee { ID = this.ID, SID = this.SID, Name = this.Name, IsInternal = this.IsInternal, Projects = projects, ObjectStatus = DataObjectStatus.Normal }; }
}
Listing 5.3 – Erweitern eines EntityObjects um eine Konvertierungsmethode
34
Flexibler Umgang mit dem Entity Framework
Um die Möglichkeit zu haben, durch die Aktualisierung des Services die zu
verwendende Datenbank zu ändern (Test- und Produktivumgebung), muss die Instanz
der Kontext-Klasse anstatt mit dem in der Konfigurationsdatei festgelegten
ConnectionString mit änderbaren Verbindungsdaten erstellt werden.
Für die Verwaltung der Verbindungsdaten wird die Klasse DbConnectionData erstellt.
Um die Erstellung des Kontexts kümmert sich die abstrakte Klasse DataManagerBase.
Abb. 5.4 – Diagramm: DbConnectionData
Abb. 5.5 – Diagramm: DataManagerBase
35
Die Klasse DbConnectionData enthält keine Logik und dient nur dazu, die
Informationen für die Datenbankverbindung zusammenzufassen. Sie wird als
Parameter dem Konstruktor der Klasse DataManagerBase übergeben. Intern wird
mithilfe von Funktionen GetEntityConnectionString() und GetProviderString() ein
entsprechender ConnectionString generiert, mit dem eine Instanz des EF-Kontexts
initialisiert wird. Für den Zugriff auf den Kontext dient die öffentliche
Eigenschaftsmethode Context.
Implementierung
Listing 5.4 demonstriert die Implementierung der Methode LoadAllEmployees() des
Servicevertrags. Die Klasse DataManager stellt dabei eine Ableitung von
DataManagerBase dar und liefert mittels der gleichnamigen Methode eine Liste von
Mitarbeitern zurück. Schlägt der Zugriff fehl, so wird mithilfe der Klasse Logger die
entsprechende Fehlermeldung für die spätere Analysierung in einer Logdatei auf dem
Server abgelegt.
public List<Employee> LoadAllEmployees() {
try { var manager = new DataManager(dbc_data); return manager.LoadAllEmployees(); } catch (Exception e) { Debug.WriteLine("# Exc by Service-LoadAllEmployees(): " +
e.Message); Logger.Logger.WriteLine(path, "'LoadAllEmployees()'", e.Message);
return new List<Employee>(); }
}
Listing 5.4 – Implementierung des Servicevertrags
Listing 5.5 zeigt den entsprechenden Datenzugriff im DataManager.
public List<Employee> LoadAllEmployees() {
using (var c = this.Context) { List<Employee> employees = new List<Employee>();
foreach (var e in c.Employees) employees.Add(e.ToDO());
return employees; }
}
Listing 5.5 – Datenzugriff im DataManager
36
Wie in Listing 5.5 zu sehen, wird mithilfe des Kontexts innerhalb des using-Blocks eine
Liste von EntityObjects iteriert, wobei jeder Eintrag in den serialisierbaren Typ
konvertiert und zu einer neuen Liste hinzugefügt wird, die anschließend
zurückgegeben wird.
5.1.3 Kommunikation mit dem Service
RequestBase
Damit die Benutzeroberfläche jederzeit reaktionsfähig bleibt, müssen die Service-
Aufrufe asynchron zum Hauptthread ausgeführt werden. Aus diesem Grund wird eine
neue abstrakte Klasse RequestBase entwickelt, deren Aufgabe es ist, die Grundlage
für alle asynchronen Abfragen zu liefern und diesen intern den Service zur Verfügung
zu stellen.
.NET bietet mit BackgroundWorker eine Klasse an, die es einfach macht, einen
Vorgang auf einem separaten, dedizierten Thread auszuführen. Dazu bietet
BackgroundWorker zwei Ereignisse an:
DoWork: diesem kann in Form eines Ereignishandlers eine Operation
hinzugefügt werden, die im Hintergrund ausgeführt wird
RunWorkerCompleted: tritt ein, wenn die Operation abgeschlossen ist oder
während der Ausführung ein Fehler aufgetreten ist
Mit dem Aufruf der Methode RunWorkerAsync() lässt sich die Ausführung im
Hintergrund starten.
37
Abb. 5.6 – Diagramm: RequestBase
Anhand der Abb. 5.6 ist zu erkennen, dass RequestBase neben dem Konstruktor nur
eine öffentliche Methode Start() anbietet. ServiceCommunication stellt eine weitere
Klasse dar, die durch den Dienstverweis generierte Service-Proxy-Klasse mit der im
Client einzustellenden Service-Adresse initialisiert und anschließend anbietet. Somit
erhalten die Abfragen eine einfache Möglichkeit, auf den Service intern zuzugreifen.
Eine Instanz von RepositoryManager (s. 5.5.2) wird der Klasse im Konstruktor
übergeben, um die abgefragten Daten anwendungsweit ablegen zu können.
Außerdem enthält die Klasse das Ereignis RequestFinished, das die Fertigstellung
einer Abfrage signalisiert.
Im Folgenden werden wichtige Teile der Klasse genauer erläutert.
38
Listing 5.6 zeigt die Implementierung des Ereignishandlers DoWork(): Die
gleichnamige abstrakte Methode, die von einer konkreten Abfrage implementiert
werden soll, wird aufgerufen und im Falle, dass eine Ausnahme ausgelöst wird,
erfolgt eine entsprechende Ausgabe auf die Konsole. Durch die Einkapselung der
abstrakten Methode DoWork() in dem gleichnamigen Ereignishandler wird erreicht,
dass sie ohne, in diesem Fall, überflüssige Übergabeparameter vom Ereignis DoWork
auskommt. Außerdem ermöglicht dies eine interne Fehlerbehandlung.
protected abstract void DoWork(); private void DoWork(object sender, DoWorkEventArgs e) {
try { this.DoWork(); } catch (Exception exc) { Debug.WriteLine("# Exception by Request: " + exc.Message); }
}
Listing 5.6 – RequestBase: DoWork()
Wie in Listing 5.7 zu sehen, ähnelt das Prinzip der Implementierung des
Ereignishandlers WorkCompleted() dem von DoWork(), mit dem Unterschied, dass
hier keine Fehlerbehandlung erforderlich ist und nach der Abarbeitung der
abstrakten Methode FinishWork() das Ereignis RequestFinished ausgelöst wird.
protected abstract void FinishWork(); private void WorkCompleted(object sender, RunWorkerCompletedEventArgs e) {
this.FinishWork();
if (this.RequestFinished != null) this.RequestFinished(this, new EventArgs());
}
Listing 5.7 – RequestBase: FinishWork()
Listing 5.8 demonstriert die Implementierung der Methode Start(): Nach der
Erstellung einer neuen Instanz von BackgroundWorker, werden die Ereignishandler
den entsprechenden Ereignissen zugewiesen und die Ausführung gestartet.
public void Start() {
_repositoryManager.ProgressDescription = _progressDescription; _backgroundWorker = new BackgroundWorker(); _backgroundWorker.DoWork += new DoWorkEventHandler(DoWork); _backgroundWorker.RunWorkerCompleted += WorkCompleted; _backgroundWorker.RunWorkerAsync();
}
Listing 5.8 – RequestBase: Start()
39
Listing 5.9 zeigt anhand eines konkreten Beispiels die Implementierung einer Abfrage.
Die Klasse LoadAllEmployees leitet sich von RequestBase ab und überschreibt die
Methode DoWork() der Oberklasse mit dem Zugriff auf den Service, dessen
Rückgabewert in diesem Fall eine Liste von allen Mitarbeitern ist. In der ebenfalls
überschriebenen Methode FinishWork() werden anschließend, durch die Iteration der
vom Service gelieferten Liste, die Werte in dem RepositoryManager abgelegt.
public class LoadAllEmployees : RequestBase {
List<Employee> _employees;
public LoadAllEmployees(RepositoryManager rm) : base(rm) { this.ProgressDescription = "Mitarbeiter werden geladen..."; }
protected override void DoWork() { _employees = this.ServiceCommunication.Client.LoadAllEmployees(); }
protected override void FinishWork() { foreach (var e in _employees) this.RepositoryManager.GetRepository<Employee>().Add(e); }
}
Listing 5.9 – Beispiel einer Abfrage
RequestManager
In einigen Fällen muss eine bestimmte Reihenfolge von Abfragen ausgeführt werden,
wie beispielsweise beim Start der Anwendung:
Prüfen der Verbindung zum Service
Authentifizierung
Abfragen, die von einzelnen Modulen für den Start definiert werden
Dies allein mit RequestBase realisiert, würde eine unübersichtliche Kette von
Ereignishändlern ergeben. Außerdem wäre die Codemenge dadurch unnötig
vergrößert.
Aus diesem Grund wird zusätzlich die Klasse RequestManager entwickelt, deren
Aufgabe es ist, eine komfortable Möglichkeit zu bieten eine Reihe von Abfragen
nacheinander auszuführen.
40
RequestManager stellt alle notwendige Methoden zu Verfügung für die Ausführung
mehrerer Abfragen nacheinander. Die Verwaltung der Abfragen übernimmt die
Queue Requests (First In - First Out - Prinzip). Das Ereignis RequestChainFinished
informiert über das Ende der Abarbeitung. AddRequest() und OnRequestFinished()
stellen die wichtigsten Methoden der Klasse dar.
Abb. 5.7 – Diagramm: RequestManager
Listing 5.10 demonstriert das Hinzufügen einer neuen Abfrage. Um auf die
Fertigstellung einer asyncrhonen Operation reagieren zu können, wird am Ereignis
RequestFinished das private Ereignishändler OnRequestFinished() registriert.
Anschließend wird die Abfrage der Queue hinzugefügt.
request.RequestFinished += new EventHandler<EventArgs>(OnRequestFinished); this.Requests.Enqueue(request);
Listing 5.10 – RequestManager: AddRequest()
Die Methode OnRequestFinished() in Listing 5.11 ist für die Abarbeitung der Abfragen
zuständig und führt dafür folgende Schritte durch:
Abmelden vom Ereignis RequestFinished jeder Abfrage, die fertiggestellt
wurde, damit diese vom GarbageCollector freigegeben werden kann.
Befinden sich in der Queue noch weitere Abfragen, so wird die erste aus der
Warteschlange entnommen und gestartet, ansonsten wird das Ereignis
RequestChainFinished ausgelöst, um die Fertigstellung bekanntzugeben.
41
void OnRequestFinished(object sender, EventArgs e) {
(sender as RequestBase).RequestFinished -= OnRequestFinished;
if (this.Requests.Count != 0) Requests.Dequeue().Start(); else if (this.RequestChainFinished != null) this.RequestChainFinished(this, new EventArgs());
}
Listing 5.11 – RequestManager: OnRequestFinished()
Listing 5.12 zeigt, wie mithilfe von RequestManager eine Reihe von Abfragen
ausgeführt und auf deren Fertigstellung mittels einer Ereignisbehandlung reagiert
werden kann.
requestManager.AddRequest(new Authentication(_repositoryManager)); requestManager.AddRequest(new LoadAllEmployees(_repository)); requestManager.RequestChainFinished += ChainFinished; requestManager.Start();
Listing 5.12 – Beispiel: Verwendung von RequestManager
5.1.4 Hosting
Während des Entwicklungsprozesses kann der WCF-Service von Visual Studio lokal
gehostet werden. Um ihn anschließend in einer Produktivumgebung zu
veröffentlichen, existieren folgende Möglichkeiten:
Veröffentlichen auf IIS (s. 2.7.2)
Installieren als Windows-Dienst
Ausführen des Services in einer Konsolenanwendung
Die Veröffentlichung auf IIS stellt dabei die beste Variante dar, denn sie bietet im
Vergleich zu den anderen Optionen folgende Vorteile:
Unkomplizierte Einbindung des Services: keine Installationsroutine nötig
Aktualisierung gestaltet sich sehr einfach: lediglich die Assemblys müssen
ersetzt werden. Dafür ist kein Neustart der gehosteten Anwendung nötig
Änderung von relevanten Parametern (Port, Protokoll) jederzeit möglich
42
5.2 Grundgerüst für MVVM
5.2.1 ViewModelBase
Aus dem Grund, dass die meisten ViewModel-Klassen dieselben Features benötigen,
wie beispielsweise die Implementierung der INotifyPropertyChanged-Schnittstelle (s.
2.6.4), wird eine abstrakte ViewModelBase-Klasse entwickelt, die die Grundlage für
alle ViewModels bildet.
Abb. 5.8 - Diagramm: ViewModelBase
Wie in Abb. 5.8 zu sehen, implementiert ViewModelBase folgende Schnittstellen:
INotifyPropertyChanged: dient zur Benachrichtigung des Bindungssystem über
einen neuen Wert
IDisposable: ermöglicht das Freigeben von Fremdressourcen, damit der
belegte Speicher vom nicht mehr benötigten Objekt durch den Garbage
Collector freigegeben werden kann
43
Die Implementierung der INotifyPropertyChanged-Schnittstelle stellt den wichtigsten
Teil der Basisklasse dar und definiert ein PropertyChanged-Ereignis, das ausgelöst
werden kann, um das WPF-Bindungssystem zu aktualisieren.
Die PropertyChangedEventArgs-Klasse macht eine PropertyName-Eigenschaft vom
Typ String verfügbar, um den Eigenschaftsnamen angeben zu können. Da das
Auslösen des PropertyChanged-Ereignisses mit einem falschen Eigenschaftsnamen im
Ereignisargument zu Fehlern führen kann, die sich nur schwer aufspüren lassen, wird
die Routine für die Auslösung des Ereignisses um eine Ermittlung der Existenz der
Property ergänzt.
Wie in Listing 5.13 zu sehen, übernimmt die Methode VerifyPropertyName() die
Überprüfung, ob der übergebene Eigenschaftsname in einem ViewModel-Objekt
wirklich vorhanden ist. Weil die Methode mit einem Conditional-Attribut versehen
und somit nur im Debug-Modus verfügbar ist, ergeben sich bei deren Verwendung
zur Laufzeit der Anwendung keine Performance-Beeinträchtigungen.
public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) {
this.VerifyPropertyName(propertyName);
PropertyChangedEventHandler handler = this.PropertyChanged; if (handler != null) { var e = new PropertyChangedEventArgs(propertyName); handler(this, e); }
} [Conditional("DEBUG")] [DebuggerStepThrough] public void VerifyPropertyName(string propertyName) {
if (TypeDescriptor.GetProperties(this)[propertyName] == null) { string msg = "Invalid property name: " + propertyName;
if (this.ThrowOnInvalidPropertyName) throw new Exception(msg); else Debug.Fail(msg); }
}
Listing 5.13 – ViewModelBase: OnPropertyChanged()
44
Die Methode OnPropertyChanged() erwartet als Übergabeparameter einen
Eigenschaftsnamen in Form eines Strings, überprüft zunächst die Zeichenkette auf
ihre Gültigkeit, erstellt eine neue Instanz vom Ereignisargument
PropertyChangedEventArgs und löst das Ereignis anschließend damit aus.
Listing 5.14 zeigt, wie mithilfe der von ViewModelBase vererbten Methode
OnPropertyChanged() ein ViewModel die Bindung aktualisieren kann.
public DayStatus DayStatus {
get { return _dayStatus; } private set { _dayStatus = value; base.OnPropertyChanged("DayStatus"); }
}
Listing 5.14 – Verwendung von OnPropertyChanged()
5.2.2 WorkspaceViewModel
Die WorkspaceViewModel-Klasse erweitert ViewModelBase um eine Eigenschaft vom
Typ RepositoryManager (s. 5.5.2) und stellt die Basisklasse für alle ViewModels dar,
die mit den Daten arbeiten müssen.
Abb. 5.9 – Diagramm: WorkspaceViewModel
45
5.3 Umsetzung des Plug-in-Konzepts
5.3.1 Definieren des Vertrags
Basierend auf den Erkenntnissen aus der Entwurfsphase und an die Anwendung
gestellten Anforderungen, wird zwischen dem Client und den Modulen ein Vertrag in
Form der Schnittstelle IModule definiert.
Abb. 5.10 – Diagramm: IModule
Wie in Abb. 5.10 zu sehen, definiert IModule folgende Eigenschaften:
ForAdminOnly: kennzeichnet ein Modul nur für die Verwendung von
Administratoren.
Name: Name des Moduls.
StartRequests: definiert eine Liste von Abfragen, die beim Start der
Anwendung ausgeführt werden sollen.
View: Haupt-View in Form eines UserControls.
46
5.3.2 Implementierung des Vertrags
Der Vertrag muss von einem Modul implementiert werden, um von der
Laufzeitumgebung eingebunden werden zu können. Dies geschieht mithilfe der
Klasse Module, die im obersten Verzeichnis eines Modul-Projekts definiert wird.
Listing 5.15 zeigt die Implementierung der Schnittstelle IModule von einem Modul für
die Zeiterfassung.
[Export(typeof(IModule))] public class Module : IModule {
public UserControl View { get; set; } public string Name { get; set; } public bool ForAdminOnly { get; set; } public List<RequestBase> StartRequests { get; set; }
[ImportingConstructor] public Module([Import(typeof(RepositoryManager))]RepositoryManager rm) { this.Name = "Projekte/Kunden"; this.ForAdminOnly = true; this.View = new MainView(); this.View.DataContext = new MainViewModel(rm); this.StartRequests = new List<RequestBase>() { new LoadAllEmployees(rm) }; }
}
Listing 5.15 – Implementierung des Vertrags
Für die Instanziierung der Klasse MainViewModel wird eine Instanz von
RepositoryManager benötigt. Damit diese beim Importieren eines Moduls von der
Laufzeitumgebung übergeben werden kann, wird der Konstruktor der Klasse Modul
mit dem ImportingConstructor-Attribut markiert und der Typ des
Übergabeparameters mithilfe des Attributs Import festgelegt.
Da die Verwaltung, in diesem Fall von Projekten und Kunden, nur von
Administratoren durchgeführt werden darf, ist die Eigenschaft ForAdminOnly
entsprechend auf true gesetzt. In diesem Modul erfolgt auch die Zuweisung von
Projekten an entsprechende Mitarbeiter, also wird die Abfrage LoadAllEmployees der
Liste StartRequests hinzugefügt. Somit wird sichergestellt, dass nach dem Start der
Laufzeitumgebung im RepositoryManager sich eine aktuelle Liste von allen
Mitarbeitern befindet, auf die dieses Modul zugreifen kann.
47
5.3.3 ModuleManager
Um das Einbinden von Modulen im Client kümmert sich die Klasse ModuleManager.
Abb. 5.11 – Diagramm: ModuleManager
Die Eigenschaft LoadedModules stellt ein Array vom Type IModule dar. Damit beim
Import der Module diese in LoadedModules abgelegt werden, wird die Property
entsprechend mit dem Attribut ImportMany markiert und der Typ der zu
importierenden Schnittstelle angegeben (s. Listing 5.16).
[ImportMany(typeof(IModule))] private IModule[] LoadedModules { get; set; }
Listing 5.16 – Definieren einer Import-Eigenschaft (Modul-Container)
Um beim Import an die Implementierung der Schnittstelle eine Instanz von
RepositoryManager übergeben zu können, wird die private Eigenschaft
RepositoryManager mit dem Export-Attribute markiert (s. Listing 5.17). Diese
Eigenschaft wird im Konstruktor von ModuleManager mit dem Übergabeparameter
(s. Abb. 5.11) initialisiert.
[Export(typeof(RepositoryManager))] private RepositoryManager RepositoryManager { get; set; }
Listing 5.17 – Definieren einer Export-Eigenschaft
48
Listing 5.18 demonstriert das Laden von Modulen, was mithilfe der Funktion
LoadModules() geschieht. In dieser wird zunächst ein DirectoryCatalog erstellt, der
mit dem Pfad zu dem Ordner in dem die Module in Form von Assemblys vorliegen aus
den Einstellungen initialisiert wird. Mit diesem Katalog wird eine neue Instanz von
CompositionContainer erstellt. Anschließend wird der Import mithilfe der Funktion
ComposeParts() gestartet.
public void LoadModules() {
var catalog = new DirectoryCatalog(...Settings.Default.ExtensionsPath); var container = new CompositionContainer(catalog); container.ComposeParts(this);
}
Listing 5.18 – ModuleManager: LoadModules()
In Listing 5.19 ist die Methode PrepareModules() zu sehen. Diese führt eine Filterung
der importierten Module anhand der Rolle des authentifizierten Benutzers durch und
legt diese im RepositoryManager ab.
public void PrepareModules() {
if (RepositoryManager.LoggedInAs.IsAdmin) RepositoryManager.Modules = LoadedModules.ToList<IModule>(); else RepositoryManager.Modules = LoadedModules.Where(o =>
!o.ForAdminOnly).ToList<IModule>(); }
Listing 5.19 – ModuleManager: PrepareModules()
49
5.3.4 Bereitstellung der Module von der Laufzeitumgebung
Nachdem die Plug-Ins geladen und alle für den Start der Anwendung definierte
Abfragen ausgeführt sind, werden die Module vom Client dem Anwender zur
Verfügung gestellt. Dies geschieht in der MainWindowViewModel-Klasse, die das
zentrale ViewModel der Laufzeitumgebung darstellt und deren View das
Hauptfenster ist.
Abb. 5.12 – Diagramm: MainWindowViewModel
Zu den Aufgaben von MainWindowViewModel zählen in Bezug auf Module folgende:
Generieren der Plug-In Auswahl
Aktivierung eines Plug-Ins
50
Generieren der Plug-In Auswahl
Generierung der Auswahl wird durch die Funktion CreateCommands() bewerkstelligt,
deren Rückgabewert eine Liste vom Typ CommandViewModel ist.
CommandViewModel stellt eine Ableitung von ViewModelBase dar und erweitert
diese um folgende Eigenschaften:
Command: eine Eigenschaft vom Typ ICommand (s. 2.6.3). Dieser wird eine
Instanz des Typs RelayCommand, einer gängigen Implementierung der
ICommand-Schnittstelle, übergeben. Wird benötigt, um auf das Ereignis beim
Klicken auf eine Schaltfläche reagieren zu können.
IsActive: eine boolesche Eigenschaft, um die aktuelle Auswahl mithilfe eines
entsprechenden DataTemplates visuell hervorzuheben.
Abb. 5.13 – Diagramm: CommandViewModel
Wie in Listing 5.20 zu sehen, wird in CreateCommands() die Liste mit den Modulen
iteriert und für jeden Eintrag eine neue Instanz von CommandViewModel erstellt, die
zu der Rückgabeliste hinzugefügt wird.
List<CommandViewModel> CreateCommands() {
List<CommandViewModel> commands = new List<CommandViewModel>();
foreach (IModule module in _repositoryManager.Modules) { commands.Add(new CommandViewModel( module.Name, new RelayCommand(SetActiveWorkspace), false)); }
return commands;
}
Listing 5.20 – MainWindowViewModel: CreateCommands()
51
Listing 5.21 zeigt die Definiton der Commands-Property, die der Haupt-View der
Laufzeitumgebung die CommandViewModels zur Verfügung stellt. Diese besteht aus
einem get-Block, in dem bei einem Zugriff der View zunächst überprüft wird, ob die
Liste bereits erstellt wurde und bei Bedarf CreateCommands() aufgerufen.
Anschließend wird die Liste zurückgegeben.
public ReadOnlyCollection<CommandViewModel> Commands {
get { if (_commands == null) { List<CommandViewModel> cmds = this.CreateCommands();
_commands = new ReadOnlyCollection<CommandViewModel>(cmds); } return _commands; }
}
Listing 5.21 – MainWindowViewModel: Commands
Im Navigationsbereich der Haupt-View befindet sich ein ContentControl (s. Listing
5.22), der sich um die Darstellung der Plug-In Auswahl kümmert. Dessen Eigenschaft
Content ist an die Liste mit CommandViewModels des MainWindowViewModels
gebunden. Durch das Definieren eines DataTemplates in Form von
CommandsTemplate wird das Aussehen der Schaltflächen angepasst. Abb. 5.14 zeigt
die gerenderte Auswahl mit dem aktivierten Modul für die Verwaltung.
<ContentControl Grid.Row="0" Content="{Binding Path=Commands}" ContentTemplate="{StaticResource CommandsTemplate}" Style="{StaticResource MainHCCStyle}" />
Listing 5.22 – MainWindow: ContentControl zur Darstellung der Plug-In Auswahl
Abb. 5.14 – Plug-In Auswahl
52
Aktivierung eines Plug-Ins
Die Aktivierung eines Moduls beschränkt sich auf das Wechseln der aktuellen View im
Content-Bereich der Laufzeitumgebung (s. Abb. 4.3). Dieser ist als ContentControl
definiert, dessen Eigenschaft Content an die Workspace-Property vom Typ
UserControl gebunden ist.
<ContentControl Content="{Binding Path=Workspace}"/>
Listing 5.23 – MainWindow: ContentControl zur Darstellung vom Plug-In-Inhalt
Um die Aktivierung der Module kümmert sich die Funktion SetActiveWorkspace() (s.
Listing 5.24), die als Übergabeparameter den Namen des zu aktivierenden Plug-Ins
erwartet.
void SetActiveWorkspace(object parameter) {
// Get the first module by name var module = _repositoryManager.Modules.First(m =>
m.Name.Equals(parameter.ToString()));
// Set the view Workspace = module.View;
// Set the selected module as active and reset the rest foreach (var cvm in Commands) { if (cvm.DisplayName.Equals(parameter.ToString())) cvm.IsActive = true; else cvm.IsActive = false; }
}
Listing 5.24 – MainWindowViewModel: SetActiveWorkspace()
Zum Aktivieren eines Moduls führt SetActiveWorkspace() folgende Schritte durch:
Anhand des Übergabeparameters wird mithilfe einer LINQ-Abfrage das
entsprechende Modul in der Modul-Liste gefunden und zwischengespeichert.
Die Eigenschaft Workspace wird auf die View des aktivierten Moduls gesetzt
und wechselt somit den Inhalt des Content-Bereichs der Laufzeitumgebung.
Anschließend wird der Status der Schaltflächen aktualisiert.
53
5.4 Authentifizierung
Die Authentifizierung erfolgt in Form der Abfrage Authenticate, die eine Ableitung
von RequestBase (s. 5.1.3) darstellt. Authenticate greift intern auf den Service zu, der
seinerseits mit Active Directory kommuniziert und den Benutzer anhand seiner SID
authentifiziert.
Listing 5.25 zeigt die Implementierung der Abfrage Authenticate, die folgende
Schritte durchführt:
Zunächst wird die SID des aktuell angemeldeten Benutzers mithilfe der Klasse
WindowsIdentity ermittelt.
Die SID wird zu einem String konvertiert und anschließend an den Service
weitergegeben.
Ist die Authentifizierung erfolgt, so wird das vom Service gelieferte
Authentifizierungsobjekt im RepositoryManager abgelegt.
public class Authenticate : RequestBase { Employee _user; public Authenticate(RepositoryManager repositoryManager) : base(repositoryManager) { ProgressDescription = "Authentifizierung..."; } protected override void DoWork() { // Get the identity from user WindowsIdentity id = new WindowsIdentity(Environment.UserName); _user = ServiceCommunication.Client.Authentication(id.User.ToString()); } protected override void FinishWork() { _user.IsAdmin = true; RepositoryManager.LoggedInAs = _user; } }
Listing 5.25 – Authentifizierung
In Active Directory sind für die Anwendung zwei Sicherheitsgruppen angelegt, mit
deren Hilfe die Rollenverwaltung (Admin/Benutzer) realisiert wird. Diesen Gruppen
können einzelne Mitarbeiter zugewiesen werden oder auch weitere
Sicherheitsgruppen, die ebenfalls weitere Gruppen enthalten können.
54
Um die Feststellung, ob ein Benutzer sich in einer bestimmten Sicherheitsgruppe
befindet, kümmert sich die statische Funktion IsMemberOf(), die rekursiv arbeitet
und sich in der Helferklasse Authentication befindet.
Wie in Listing 5.26 zu sehen, benötigt die Funktion als Übergabeparameter die
abzufragende Gruppe in Form eines Objekts vom Typ GroupPrincipal und die SID des
Benutzers, nach der in der Gruppe gesucht werden soll.
Die Funktion GetMembers() von groupPrincipal liefert eine Liste mit den Mitgliedern
dieser Gruppe zurück, die iteriert und dabei jeder Eintrag auf seinen Typ überprüft
wird. Handelt es sich bei dem Eintrag um eine Gruppe, so wird IsMemberOf() auf
diese rekursiv angewandt. Ist der Eintrag hingegen keine Gruppe, erfolgt ein
Vergleich zwischen den SID’s.
public static bool IsMemberOf(GroupPrincipal groupPrincipal, string userSID) {
PrincipalSearchResult<Principal> results = groupPrincipal.GetMembers();
if (results != null) { foreach (var member in results) { // Member is a group? -> iterate it
if (member.GetType().Equals(typeof(GroupPrincipal))) { if (IsMemberOf(member as GroupPrincipal, userSID) ==
true) return true; } else { if (member.Sid.ToString().Equals(userSID)) return true; } } } return false;
} Listing 5.26 – Authentication: IsMemberOf()
Auf der Service-Seite werden bei der Authentifizierung zunächst zwei
Sicherheitsgruppen mit den von Active Directory vorgegebenen SID’s definiert. Auf
diese Gruppen wird die Funktion IsMemberOf() angewandt und das Ergebnis
zwischengespeichert. Liefert die Funktion in beiden Fällen ein false zurück, so schlägt
die Authentifizierung fehl und liefert ein ungültiges Authentifizierungsobjekt zurück.
Bei der erfolgreichen Ermittlung der Rolle wird zudem überprüft, ob der Benutzer
bereits in der Datenbank existiert, und bei Bedarf angelegt. Anschließend wird die
Rolle des Authentifizierungsobjekts gesetzt und das Objekt zurückgegeben.
55
5.5 Datenzugriff im Client
5.5.1 Repository
Die generische Klasse Repository stellt den Container für die Daten (Models) der
Anwendung dar und arbeitet intern mit einer Liste vom Typ ObservableCollection.
Diese Liste bietet ein Ereignis namens CollectionChanged an, mit dessen Hilfe die
ViewModels auf die Änderungen in der Datenauflistung reagieren können.
Abb. 5.15 - Diagramm: Repository
Repository vereinfacht den Umgang mit der Liste, indem sie für die meistgenutzten
Aufgaben die s.g. Wrapper-Funktionen bereitstellt. In der Funktion Add() wird
beispielsweise zunächst überprüft, ob es sich bei dem übergebenen Objekt um eine
gültige Instanz handelt, und bei Bedarf eine entsprechende Exception geworfen.
Enthält die Liste das Objekt bereits, wird ein false zurückgegeben, um das Ereignis
CollectionChanged nicht unnötig auszulösen.
Um den Datentyp, den die Repository-Klasse aufnimmt, auf die Ableitungen der
Klasse DataObjectBase zu beschränken, die die Oberklasse für alle Models darstellt,
wird bei der Deklaration der Klasse eine entsprechende where-Anweisung angegeben
(s. Listing 5.27).
public class Repository<T> : IDisposable where T : DataObjectBase
Listing 5.27 – Deklaration der Repository-Klasse
56
5.5.2 RepositoryManager
Die Klasse RepositoryManager verwaltet die Repositorys und stellt den zentralen
Zugriffspunkt auf die Daten der Anwendung dar.
Abb. 5.16 – Diagramm: RepositoryManager
Durch das Definieren von Funktionen für den Zugriff auf die Repositorys gestaltet sich
der Umgang mit den Daten in den ViewModels, wie in Listing 5.28 zu sehen, sehr
einfach.
this.RepositoryManager.GetRepository<Workday>().Add(workday);
Listing 5.28 – Beispiel: Verwendung des RepositoryManagers
Wegen der zentralen Stelle der Klasse, wird diese um einige zusätzliche Eigenschaften
erweitert, auf die jedes ViewModel Zugriff haben muss, wie beispielsweise der
RequestManager für eine komfortable Möglichkeit eine Reihe von Abfragen
ausführen zu können oder das Authentifizierungsobjekt in Form der Eigenschaft
LoggedInAs.
57
6 Fazit und Ausblick
Das Ziel dieser Arbeit war das Entwickeln einer zur Laufzeit erweiterbaren,
serviceorientierten und möglichst generischen Anwendung, die mit wenig Aufwand
an unterschiedlichste Anforderungen angepasst werden kann.
Letztendlich ließ sich das Plug-In-Konzept mit MEF leichter als gedacht umsetzen. Das
Framework für erweiterbare Anwendungen ist sehr mächtig und zudem logisch
aufgebaut. Mit MEF stellt .NET eine elegante Lösung zum Problem der
Erweiterbarkeit einer Anwendung zur Laufzeit bereit.
Der Datenzugriff und die Entwicklung des Datenmodells wurden durch das Einsetzen
des Entity Frameworks erheblich erleichtert. Die vom ORM generierten Entitys lassen
sich mithilfe von Datenverträgen serialisieren, was eine problemlose Nutzung des
Frameworks für den Datenzugriff im WCF-Service ermöglicht.
Die Entwicklung mit WPF nach dem MVVM-Muster hat zur Folge, dass durch die
weitestgehende Trennung der Logik von der Benutzeroberfläche, die Anwendung sich
gut überblicken und testen lässt. Die saubere Umsetzung des Musters ist jedoch nicht
immer problemlos möglich. WPF definiert eine Reihe von Ereignissen für die
Steuerelemente, deren Behandlung in der Anwendung manchmal notwendig ist. An
die Ereignisse lässt sich das ViewModel aber nicht direkt binden und deren
Behandlung in der CodeBehind-Datei würde den Vorgaben des Entwurfsmusters
widersprechen. Es existiert aber eine Reihe von Bibliotheken, die sich dieser
Problemstellung widmen und MVVM-konforme Lösungen anbieten.
Insgesamt lässt sich sagen, dass dieses Projekt die gesetzten Ziele erfüllt und für
erweiterbare Anwendungen unter .NET als Grundlage verwendet werden kann. Die
auf dieser Basis entwickelte Zeiterfassung der Firma Betex wird intern derzeit
erfolgreich eingesetzt.
Momentan ist für die Zeiterfassung die Entwicklung von zusätzlichen Plug-Ins geplant.
Über eine spätere Nutzung des Projekts in kommerziellen Zwecken wird ebenfalls
nachgedacht.
58
7 Listingverzeichnis
Listing 2.1 – Verwendung einer Eigenschaftsmethode .............................. 3 Listing 2.2 – Beispiel: Verwendung von partiellen Klassen ....................... 4 Listing 2.3 – Beispiel: Datenzugriff mithilfe von LINQ .......................... 4 Listing 2.4 – Beispiel: Datenzugriff ohne LINQ .................................. 4 Listing 2.5 – Umgang mit dem Kontext ............................................ 6 Listing 2.6 – Definieren eines WCF-Vertrags ..................................... 7 Listing 2.7 – Definieren eines Datenvertrags .................................... 8 Listing 2.8 – Definieren von Imports und Exports ................................ 9 Listing 2.9 - Beispiel einer Komposition mit MEF ............................... 10 Listing 2.10 – Beispiel: XAML-Ausschnitt ....................................... 13 Listing 2.11 – Direktes setzen des DataContexts der View im Codebehind ......... 15 Listing 2.12 – Setzen des DataContexts der View per DataTemplate ............... 15 Listing 2.13 – Data Binding .................................................... 16 Listing 2.14 – Das Interface ICommand .......................................... 16 Listing 2.15 – Das Interface INotifyPropertyChanged ............................ 17 Listing 2.16 – Aktualisierung der Bindung mit dem Event PropertyChanged ........ 17 Listing 5.1 – Ausschnitt aus dem Servicevertrag ................................ 31 Listing 5.2 – Datenvertrag für Employee ........................................ 33 Listing 5.3 – Erweitern eines EntityObjects um eine Konvertierungsmethode ...... 33 Listing 5.4 – Implementierung des Servicevertrags .............................. 35 Listing 5.5 – Datenzugriff im DataManager ...................................... 35 Listing 5.6 – RequestBase: DoWork() ............................................ 38 Listing 5.7 – RequestBase: FinishWork() ........................................ 38 Listing 5.8 – RequestBase: Start() ............................................. 38 Listing 5.9 – Beispiel einer Abfrage ........................................... 39 Listing 5.10 – RequestManager: AddRequest() .................................... 40 Listing 5.11 – RequestManager: OnRequestFinished() ............................. 41 Listing 5.12 – Beispiel: Verwendung von RequestManager ......................... 41 Listing 5.13 – ViewModelBase: OnPropertyChanged() .............................. 43 Listing 5.14 – Verwendung von OnPropertyChanged() .............................. 44 Listing 5.15 – Implementierung des Vertrags .................................... 46 Listing 5.16 – Definieren einer Import-Eigenschaft (Modul-Container) ........... 47 Listing 5.17 – Definieren einer Export-Eigenschaft ............................. 47 Listing 5.18 – ModuleManager: LoadModules() .................................... 48 Listing 5.19 – ModuleManager: PrepareModules() ................................. 48 Listing 5.20 – MainWindowViewModel: CreateCommands() ........................... 50 Listing 5.21 – MainWindowViewModel: Commands ................................... 51 Listing 5.22 – MainWindow: ContentControl zur Darstellung der Plug-In Auswahl .. 51 Listing 5.23 – MainWindow: ContentControl zur Darstellung vom Plug-In-Inhalt ... 52 Listing 5.24 – MainWindowViewModel: SetActiveWorkspace() ....................... 52 Listing 5.25 – Authentifizierung ............................................... 53 Listing 5.26 – Authentication: IsMemberOf() .................................... 54 Listing 5.27 – Deklaration der Repository-Klasse ............................... 55 Listing 5.28 – Beispiel: Verwendung des RepositoryManagers ..................... 56
59
8 Abbildungsverzeichnis
Abb. 2.1 - Einsetzen eines O/R-Mappers .......................................... 5 Abb. 2.2 – Beispiel: Mit XAML erstellte GUI .................................... 13 Abb. 2.3 – Die Abhängigkeiten beim MVVM-Pattern ................................ 14 Abb. 2.4 – Active Directory in einem Windows Server Netzwerk (Quelle: Microsoft) 18 Abb. 3.1 – Service-orientierte Architektur ..................................... 22 Abb. 3.2 – Grobentwurf ......................................................... 23 Abb. 4.1 – Hierarchische Struktur eines Moduls ................................. 24 Abb. 4.2 – Grundlegende Projektstruktur ........................................ 25 Abb. 4.3 – GUI Entwurf der Laufzeitumgebung .................................... 26 Abb. 4.4 – Flexibler Datenzugriff durch den DataManager ........................ 27 Abb. 4.5 – Datenhaltung im Client .............................................. 28 Abb. 4.6 – Gesamtarchitektur ................................................... 29 Abb. 5.1 – Diagramm: Servicevertrag ............................................ 30 Abb. 5.2 – Diagramm: Objektmodell .............................................. 31 Abb. 5.3 – Diagramm: Datenverträge ............................................. 32 Abb. 5.4 – Diagramm: DbConnectionData .......................................... 34 Abb. 5.5 – Diagramm: DataManagerBase ........................................... 34 Abb. 5.6 – Diagramm: RequestBase ............................................... 37 Abb. 5.7 – Diagramm: RequestManager ............................................ 40 Abb. 5.8 - Diagramm: ViewModelBase ............................................. 42 Abb. 5.9 – Diagramm: WorkspaceViewModel ........................................ 44 Abb. 5.10 – Diagramm: IModule .................................................. 45 Abb. 5.11 – Diagramm: ModuleManager ............................................ 47 Abb. 5.12 – Diagramm: MainWindowViewModel ...................................... 49 Abb. 5.13 – Diagramm: CommandViewModel ......................................... 50 Abb. 5.14 – Plug-In Auswahl .................................................... 51 Abb. 5.15 - Diagramm: Repository ............................................... 55 Abb. 5.16 – Diagramm: RepositoryManager ........................................ 56
60
9 Literaturverzeichnis
[1] A. Kühnel, Visual C# 2010: Das umfassende Handbuch (Galileo Computing), 2010. [2] D. H. Schwichtenberg,
„http://www.dotnetpro.de/Grafix/OnlineArticles/ormapper.pdf,“ dotnetpro, 2008. [Online].
[3] Microsoft, „http://msdn.microsoft.com/de-de/library/system.data.objects.objectcontext.aspx,“ [Online].
[4] „WCF - Wikipedia Enzyklopädie,“ [Online]. Available: http://de.wikipedia.org/wiki/Windows_Communication_Foundation.
[5] Microsoft, „Verwenden von Datenverträgen,“ [Online]. Available: http://msdn.microsoft.com/de-de/library/ms733127.aspx.
[6] „MEF - MSDN,“ [Online]. Available: http://msdn.microsoft.com/de-de/library/ee332203.aspx.
[7] „Übersicht über Managed Extensibility Framework - MSDN,“ [Online]. Available: http://msdn.microsoft.com/de-de/library/dd460648.aspx.
[8] T. C. Huber, Windows Presentation Foundation, 2010.
[9] „Active Directory – Wikipedia Enzyklopädie,“ Juni 2012. [Online]. Available: http://de.wikipedia.org/wiki/Active_Directory.
[10] Microsoft, „Active Directory - Gruppentypen,“ [Online]. Available: http://technet.microsoft.com/de-de/library/cc781446(v=ws.10).
[11] „SID - Wikipedia Enzyklopädie,“ April 2012. [Online]. Available: http://de.wikipedia.org/wiki/Security_Identifier.
[12] „LDAP – Wikipedia Enzyklopädie,“ August 2012. [Online]. Available: http://de.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol.
[13] „IIS - Wikipedia Enzyklopädie,“ [Online]. Available: http://de.wikipedia.org/wiki/Microsoft_Internet_Information_Services.
[14] „Clean Code Developer,“ [Online]. Available: http://www.clean-code-developer.de/Separation-of-Concerns-SoC.ashx.