Hier ein paar gute Scrum Artikel:
The Change Agent
Hier ein paar gute Scrum Artikel:
Ich habe diese Woche eine kleine TestApp geschrieben, um zu prüfen wie viel Aufwand eine Twitter Implementierung in unser hauseigenes ERP-System bedeutet. Wieso habe ich das gemacht? Meiner Meinung nach gibt es eine begrenzte Anzahl an Anwendungen, die ein Anwender täglich mehrfach verwenden kann und will. Die Anzahl bewegt sich zwischen 3 und 4 an der Zahl. Ich setze voraus, dass das ERP-System und der Emailclient (in der Regel Outlook) in jedem Fall hierzu zählen. Hinzu können je nach interner IT Landschaft noch ein Intranet (z.B. SharePoint), ein CTI Client (z.B. Swyx) und bzw. oder ein Messenger (z.B. der Live Messenger) kommen. Wohlgemerkt ist dies nur praktikabel, wenn man mit zwei Monitoren arbeitet. Bei lediglich einem Monitor, wäre diese Anzahl schon zu viel des Guten.
Offensichtlich ist hier nur wenig Spielraum für weitere Clients. Ein manueller Aufruf über die Homepage der jeweiligen Web 2.0 Sites schließe ich aus, weil damit der Browser ständig offen sein müsste und weil die Leute jeden Tag daran denken müssten, diese Seite zu öffnen und sich einzuloggen. Eine große Verbesserung bietet auch die Option von Chrome und Firefox nichts, dass man die Site als Desktop Anwendung verknüpft.
Doch gerade bei diesen Kommunikationskanälen ist es wichtig, dass sie ständig geöffnet sind, da sonst der Vorteil des schnellen Informationsaustausch verloren geht. Kritiker werden jetzt natürlich einwenden, dass die ständigen Updates das Arbeiten behindern. Dem will ich entgegen setzen, dass sie dann diese Systeme nicht dem Business gemäß nutzen, sondern das Verhalten privater Nutzer adaptieren. Beispielsweise kann man lediglich einen Tweet, nämlich den Unternehmens-Tweet, abonnieren, sodass nur diese Informationen angezeigt werden. Man kann auch den Tweet als privat deklarieren.
Um also Twitter erfolgreich in die täglichen Geschäfts- und Kommunikationsprozesse zu integrieren, sollte man Web 2.0 Portale in das ERP System implementieren. Ich will dabei gar nicht darauf eingehen wie das visuell erfolgen könnte. Vielmehr ist mein Anliegen zu zeigen, dass dies schnell und einfach erfolgen kann:
Unser ERP-System basiert auf der .NET Plattform. Deshalb nahm ich mir die Seite www.codeplex.com her und suchte nach einer freien Library, die mir den Zugriff auf Twitter abstrahierte. Ich fand nicht nur ein Framework, sondern gleich mehrere. Ich lud mir NTwitter herunter, erstellte ein Projekt, band die Assembly ein und schrieb folgenden Code, den ich an eine Listbox (ctrl_Output) gehängt habe:
1: var twitterApi = new NTwitter.Twitter("hecoGmbH", passwordDectrypted);
2: var tweets = _twitterApi.GetUserTimeline(twitterer);
3:
4: foreach (tweet s in tweets)
5: {
6: ctrl_Output.Items.Add(s.CreatedAt.ToString() +" Uhr" + (char) 9 +
HttpUtility.HtmlDecode(s.Text));
7: }
Leider musste ich zum decodieren von HTML Zeichen die Assembly System.Web referenzieren, die nur in Projekten zur Verfügung steht, welche als Target das vollständige .NET Framework voraussetzen. In der Regel sind aber auf Clients nur die Client Versionen von .NET installiert. Deshalb habe ich die Assembly mit ins Projekt kopiert. Die Lösung funktionierte auf Anhieb!
Aber Vorsicht: Twitter will keine weiteren Clients von Dritten. Ich gehe jetzt einfach mal davon aus, damit nicht die Integration ins eigene ERP-System gemeint war
Da man in der Regel immer einen Testserver betreibt, habe ich eine kurze Anleitung geschrieben wie man die Inhaltsdatenbank aus dem Live System in das Testsystem kopiert.
Der Ablauf ist wie folgt:
1. Löschen der bisherigen Inhaltsdatenbank auf dem Testserver über Zentraladministration –> Anwendungsverwaltung –> Inhaltsdatenbanken verwalten. Wählt rechts oben die passende Webanwendung aus. Dann klickt ihr auf die Datenbank. Hier gibt es ganz unten die Option “Inhaltsdatenbank entfernen”. Aktiviert den Haken und klickt auf Ok. Merkt euch den Namen der Datenbank, da ihr diesen in Punkt 4 noch benötigt.
2. Kopiert euch die aktuelle Datenbank vom Live Server. Am einfachsten geht das mit dem SQL Management Studio. Falls ihr euch damit nicht auskennt, dann googelt kurz. Dazu gibt es genügend Einträge. Kopiert die Sicherung auf den Testserver.
3. Beendet auf dem Testsystem im IIS-Manager den Anwendungspool, der mit der Webanwendung verbunden ist.
4. Startet den SQL Dienst auf dem Testserver neu
5. Löscht die immer noch im SQL Server vorhandene Datenbank (den Namen habt ihr euch aus 1 gemerkt) mit dem Management Studio. Aktiviert die Option “Bestehende Verbindungen schließen” im Dialog (optional).
6. Stellt die gesicherte Datenbank wieder her.
7. Startet den Anwendungspool wieder.
8. Geht in der Zentraladministration wieder unter Inhaltsdatenbanken verwalten und wechselt auf die richtige Webanwendung. Klickt auf “Inhaltsdatenbank hinzufügen” und gebt den Datenbankserver und den Datenbanknamen an.
Wichtig:
Wer wie ich in einem eigenen Feature berechnete Spalten definieren muss, wird sicherlich auf ähnliche Probleme stoßen. Im Gegensatz zu Formeln, die man über die Oberfläche eingeben kann, befindet man sich bei Spaltendefinitionen innerhalb von XML, sodass Sonderzeichen immer ein leidiges Thema sind. Hier zwei Beispiele:
1: <Field ID="{FC13F3E2-ED5A-41B6-99A7-FEA6A2605D9E}"
2: Name="Heco_ITAM_MachineGroup"
3: StaticName="Heco_ITAM_MachineGroup"
4: DisplayName="Machine Group"
5: Type="Calculated"
6: ShowInNewForm="TRUE"
7: ShowInEditForm="TRUE"
8: Group=" heco ITAM"
9: ReadOnly="TRUE"
10: Indexed="TRUE"
11: Hidden="FALSE"
12: Description="Dient zum Gruppieren von zusammengebundener Soft- und Hardware"
13: ResultType="Text"
14: SourceID="http://schemas.microsoft.com/sharepoint/v3" >
15: <FieldRefs>
16: <FieldRef Name="Heco_ITAM_AssetNo"/>
17: <FieldRef Name="Heco_ITAM_MachineRefId"/>
18: </FieldRefs>
19: <Formula>=IF(Heco_ITAM_MachineRefId="",Heco_ITAM_AssetNo,
Heco_ITAM_MachineRefId)</Formula>
20: </Field>
1: <Field ID="{9379E5FF-0DDA-4772-91AB-61CAFB9F88E9}"
2: Name="Heco_ITAM_OperatingSystem"
3: StaticName="Heco_ITAM_OperatingSystem"
4: DisplayName="Operating System"
5: Type="Calculated"
6: ShowInNewForm="TRUE"
7: ShowInEditForm="TRUE"
8: Group=" heco ITAM"
9: ReadOnly="TRUE"
10: Indexed="TRUE"
11: Description="Dient zum Gruppieren von zusammengebundener Soft- und Hardware"
12: ResultType="Text"
13: SourceID="http://schemas.microsoft.com/sharepoint/v3" >
14: <FieldRefs>
15: <FieldRef Name="Heco_ITAM_WindowsVersion"/>
16: <FieldRef Name="Heco_ITAM_Architecture"/>
17: <FieldRef Name="Heco_ITAM_ServicePack"/>
18: </FieldRefs>
19: <Formula>=Heco_ITAM_WindowsVersion &" " &
Heco_ITAM_ServicePack &" "& Heco_ITAM_Architecture</Formula>
20: </Field>
Auf Folgendes sollte man achten:
Seit langem können bei uns die Mitarbeiter ihren Urlaub in SharePoint verwalten. Verwalten bedeutet, dass sie Urlaub eintragen, löschen und übersichtlich anschauen können. Außerdem sehen die Kollegen auf der Startseite übersichtlich, wann wer nicht da ist. Die Eingabemaske sieht wie folgt aus:
Und meine eigene Urlaubsübersicht sieht so aus (37,0 sind die Anzahl Tage, die mir dieses Jahr zur Verfügung stehen):
Und die rechte Leiste unserer Startseite sieht beispielsweise so aus (Ausschnitt):
Kurz zum technischen Hintergrund:
Wir verwenden dafür einen eigenen Inhaltstyp Urlaub. Daneben gibt es in unserem Kalender noch die Inhaltstypen ‘Krank’, ‘Außer Haus’ und ‘Im Haus’. Zum Erzeugen der Ausgabe (Spalte Titel), wie man sie auf der Startseite sieht, verwenden wir inzwischen einen Eventhandler, der diese zusammenbaut. Früher war dies ein berechnetes Feld. Wer also nicht programmieren will, muss dies nicht notwendigerweise.
Den Geschäftsprozess optimieren und Kosten senken:
Bisher war es so, dass die Buchhaltung am Ende des Monats die Daten in unser Lohnsystem manuell übernommen haben. Das benötigte ca. 1 Manntag. Wenn ihr euch jetzt fragt, ob das dann überhaupt Sinn macht bzw. sich lohnt, dann ist die Antwort ja, da
Um den Geschäftsprozess nun vollständig zu automatisieren, benötigten wir noch die Möglichkeit die Daten in unser Lohnsystem von GDI zu exportieren.
Dazu habe ich die Liste der Mitarbeiter, welche gefüllt ist mit Einträgen vom Inhaltstyp ‘heco Mitarbeiter’ um zwei Einträge erweitert:
Beide Spalten sind vom Typ Ja/Nein. Das sieht dann so aus:
Die Spaltennamen sind selbsterklärend. Nun habe ich mit freundlicher Unterstützung von Olaf Didszun eine Kommandozeilenanwendung geschrieben, die anhand der gesetzten Eigenschaften in der Mitarbeiterliste die Daten aus dem Kalender ausliest und als CSV Datei exportiert (vgl. meinen Artikel dazu hier).
In dem ganzen Prozess gab es zwei besondere Herausforderungen:
Bei Punkt 1 wäre eine Option gewesen über Gültigkeitsprüfungen (ab SharePoint 2010 für Listen und Spalten out of the box möglich) solche Einträge zu unterbinden. Wir haben uns dagegen entschieden und haben die Logik so programmiert, dass das Programm solche Einträge erkennt und entsprechend monatsgenau abrechnet.
Zu Punkt 2: Das Problem ist, dass es möglich wäre, dass Mitarbeiter im Nachhinein Änderungen vornehmen, die im Vormonat liegen. Das stellt insofern ein Problem dar, als dass wir immer nur am letzten des Monats den Export vornehmen. Das Programm hat und sollte keine Logik dafür enthalten. Deswegen sind wir so Vorgegangen, dass wir eine SharePoint Gruppe angelegt haben, die sich “CalendarAdmins” nennt. Hier sind die Mitarbeiter der Buchhaltung enthalten. Nachdem ein Urlaubseintrag exportiert wurde, wird die Berechtigungsvererbung für das Element gestoppt und es werden alle Berechtigungsgruppen außer die ‘Besucher’ (die haben nur Lesezugriff) und die ‚CalendarAdmins’ entfernt. So ist gewährleistet, dass keine Änderungen nach einem Export mehr durchgeführt werden können, ohne über die die entsprechenden Stellen zu gehen. Der Code hierfür sieht so aus:
1: urlaubItem.BreakRoleInheritance(true);
2: SPGroup groupToRemove = urlaubItem.Web.AssociatedOwnerGroup;
3: urlaubItem.RoleAssignments.Remove(groupToRemove);
4: groupToRemove = urlaubItem.Web.AssociatedMemberGroup;
5: urlaubItem.RoleAssignments.Remove(groupToRemove);
6: urlaubItem.Update();
Das Thema Mitarbeitergespräche hat mich die letzten Monaten verstärkt beschäftigt. Ich habe viele Bücher und Artikel dazu gelesen, ich habe mit vielen Kollegen sowohl intern, als auch extern darüber diskutiert, ich habe natürlich Mitarbeitergespräche geführt und ich wurde durch unsere DHBW Studentin im Rahmen ihrer Bachlorthesis dazu befragt.
Die folgenden Punkte sind meine persönlich Quintessenz zu diesem sehr spannenden und wichtigen Thema. Ich will noch Anmerken, dass dies meine Erfahrungen im Rahmen meiner Arbeit bei einer mittelständischen Firma mit einer sehr überschaubaren IT sind.
Das sind meine 5 wesentlichen Punkte. Es gibt sicher noch viele andere Elemente, die es wert sind angesprochen zu werden, aber das würde den Rahmen sprengen.
Eine Anmerkung hätte ich noch: Neben den großen, zentralen Vorteilen bringen Mitarbeitergespräche viele weitere, kleinere Goodies mit. Beispiel: Oftmals wird man informiert, dass der Mitarbeiter mit dem Gedanken des Firmenwechsels spielt (wenn man den angesprochenen Wohlfühlraum geschaffen hat!). Hier bietet sich an, dass man ggf. schon zusammen mit dem Mitarbeiter einen Lösungsweg bespricht, um die Rahmenbedingungen für dessen Erhalt zu schaffen oder aber einen Nachfolger rechtzeitig nachzuziehen.
In meinem Betrieb setzen wir SharePoint als Intranetplattform und GDI als Buchhaltungssystem ein. Kürzlich hatte ich die Anforderung, dass man Urlaubsdaten, welche die Mitarbeiter über SharePoint pflegen, ins GDI Lohnsystem zu überführen. Als ich bei GDI anfragte, gab man mir eine Beschreibung einer Standard CSV-Import-Schnittstelle oder einfach ausgedrückt: Eine Liste wie ich ich eine CSV Datei mit Datensätzen aufbauen muss.
Um das Programm, welches die Daten aus SharePoint extrahiert und GDI als CSV zur Verfügung stellt, für etwaige Dritte, die das Programm später pflegen müssen, einfach zu halten, habe ich mit sogenannten Fluent Interfaces eine rudimentäre API implementiert. Das hat den Vorteil, dass die Logik, welche zum Erstellen einer gültigen Export-Datei benötigt wird, sauber abstrahiert ist. Keiner, der z.B. den Zugriff auf SharePoint ändern will, muss sich noch mit dem Datenformat auseinandersetzen. Wie bin ich vorgegangen?
Zunächst habe ich mir für jedes Feld, welches bei Urlaubseinträgen gefüllt sein muss, ein Interface geschrieben. Der Einfachheit halber habe ich alles in die Datei IGdiApi.cs gepackt. Die Datei sieht so aus:
1: public interface IGdiContracts_Personalnr
2: {
3: IGdiContracts_Fehlzeitennr SetPersonalnr(Int32 personnelNo);
4: }
5:
6: public interface IGdiContracts_Fehlzeitennr
7: {
8: IGdiContracts_Beginn SetFehlzeitennr(Int32 timesAbsentNo);
9: }
10:
11: public interface IGdiContracts_Beginn
12: {
13: IGdiContracts_Ende SetBeginn(DateTime begin);
14: }
15:
16: public interface IGdiContracts_Ende
17: {
18: IGdiContracts_GanzeTage SetEnde(DateTime end);
19: }
20:
21: public interface IGdiContracts_GanzeTage
22: {
23: IGdiContracts_GdiEntry SetGanzeTage(bool allDayEvent);
24: }
25:
26: public interface IGdiContracts_GdiEntry
27: {
28: string GetCsvLine();
29: }
Im Anschluss habe ich die Datei GdiApi.cs erstellt. Die Datei enthält zwei Klassen (wie clean das ist, sei mal dahingestellt). Die erste Klasse bildet den späteren Einstiegspunkt für die API:
1: public class GdiApi : IGdiContracts_Personalnr
2: {
3: public IGdiContracts_Fehlzeitennr SetPersonalnr(int personnelNo)
4: {
5: return new GdiEntry(personnelNo);
6: }
7: }
Es folgte die zweite Klasse, die die restlichen Interfaces implementiert:
1: class GdiEntry : IGdiContracts_Fehlzeitennr, IGdiContracts_Beginn, IGdiContracts_Ende, IGdiContracts_GanzeTage, IGdiContracts_GdiEntry
2: {
3: Int32 _personnelNummer;
4: Int32 _timesAbsentNo;
5: DateTime _begin;
6: DateTime _end;
7: char _beginIsAHalfDay;
8: char _endIsAHalfDay;
9:
10: public GdiEntry(Int32 personalNummer)
11: {
12: _personnelNummer = personalNummer;
13: }
14:
15: public IGdiContracts_Beginn SetFehlzeitennr(Int32 timesAbsentNo)
16: {
17: _timesAbsentNo = (Int32) timesAbsentNo;
18: return this;
19: }
20:
21: public IGdiContracts_Ende SetBeginn(DateTime begin)
22: {
23: _begin = begin;
24: return this;
25: }
26:
27: public IGdiContracts_GanzeTage SetEnde(DateTime end)
28: {
29: _end = end;
30: return this;
31: }
32:
33: public IGdiContracts_GdiEntry SetGanzeTage(bool allDayEvent)
34: {
35: //Logik zur Berechnung von _beginIsAHalfDay und _endIsAHalfDay
36: return this;
37: }
38:
39: public string GetCsvLine()
40: {
41: System.Text.StringBuilder result = new StringBuilder();
42:
43: result.Append(DateTime.Now.ToString("yyyy") + ";"); //aktuelles Jahr
44: result.Append(DateTime.Now.ToString("MM") + ";"); //aktueller Monat
45: result.Append(_personnelNummer.ToString() + ";"); //Personalnummer
46: //es folgen viele weitere leere Felder
47: result.Append(_timesAbsentNo.ToString() + ";"); //Fehlzeitennummer
48: result.Append(";"); //Bemerkung zur Fehlzeit
49: result.Append(_begin.ToShortDateString() + ";"); //Fehlzeiten Beginn
50: result.Append(_end.ToShortDateString() + ";"); //Fehlzeiten Ende
51: result.Append(_beginIsAHalfDay + ";"); //Halber Tag Beginn
52: result.Append(_endIsAHalfDay + ";"); //Halber Tag Ende
53: result.Append("N;"); //Keine Führung auf der Lohnsteuerkarte
54: result.Append("N;"); //Arbeitsunfähigkeit durch Unfall verursacht
55: result.Append("N;"); //Arbeitunfähigkeitsbescheinigung liegt vor
56: result.Append("N;"); //Fehlzeiten fürs Baugewerbe
57:
58: return result.ToString();
59: }
60: }
1: new GdiApi().
2: SetPersonalnr(iPersonalNr).
3: SetFehlzeitennr(iFehlzeit).
4: SetBeginn(dtStart).
5: SetEnde(dtEnd).
6: SetGanzeTage(bAllDayEvent).
7: GetCsvLine();
Der Vorteil ist, dass der Aufrufer der API genau in dieser Reihenfolge vorgehen muss, um an den benötigten Datensatz zu kommen. Ihm wird genau gesagt wie er Vorgehen muss. Die Logik, wie sich der Datensatz zusammenbaut, ist dem Aufrufer unbekannt. Er weiß beispielsweise nichts von der Berechnung der halben Tage oder den unzähligen zusätzlichen, leeren Feldern.
Warum habe ich nicht einfach alle Interfaces in der ersten Klasse implementiert? Das hätte den Nachteil, dass der Aufrufer zu diesem Zeitpunkt noch alle Methoden gesehen und nicht gewusst hätte, mit welcher er anfangen soll.
Warum habe ich nicht für jedes Interface eine separate Klasse verwendet? Das hätte ich auch machen können, allerdings hätte ich dann die bereits gesetzten Informationen in irgendeiner Form weiter übergeben müssen. Eine Möglichkeit wäre gewesen, ein DTO zu definieren und dieses immer im Konstruktor in die nächste Klasse weiter zu reichen.
Warum habe ich es so gemacht? Weil die Methoden größtenteils nur 2 Zeilen beinhalten und weil eine starke Kohäsion der Daten bzw. Methoden besteht. Vergleicht bitte dazu das Prinzip tell don’t ask.
Zu guter Letzt will ich euch noch auf den Artikel Generische Fluent API mit flexiblen Modulen, den ich soeben noch gefunden habe, aufmerksam machen. Zugegebenermaßen ist er eher advanced und sehr lang, sodass auch ich ihn nicht vollständig durchgelesen habe, aber es ist ein sehr gutes Beispiel.