Sonntag, 10. Juni 2012

MVVM - Dynamic ViewModel

Wer Desktop-Applikationen für Windows programmiert wird sich bestimmt mit dem inzwischen auch nicht mehr neustem Framework WPF auseinandergesetzt haben. Dieses Framework bietet einen eleganten Weg die Backend-Funktionalität sauber von der graphischen Oberfläche abzukoppeln. Wie genau das geschieht ist im MVVM-Entwurfsmuster spezifiziert. So besteht das Programm im Groben aus 3 Teilen:
  1. Model (zB. Database-Objects)
  2. ViewModel
  3. View
Die Kommunikation zwischen ViewModel und Model findet über sogennante Bindings statt. Damit das aber auch reibungslos funktioniert, sind einige Mechanismen nötig, welche das ViewModel zu implementieren hat. In der Regel sind das das INotifyPropertyChanged-Interface und die Commands.
Jedoch erschwert die Implementierung oft die Arbeit und können auch den Code unleserlich machen. Das kann dann soweit führen, dass man für jedes Model eine Wrapper-Klasse erstellt.

Eine Ausführliche Erklärung des MVVM Patterns ist in diesem sehr zu empfehlenden Artikel zu finden:
Aber das .Net 4.0-Framework schafft Abhilfe. Es ermöglicht nämlich eine dynamische Programmierung. Mit diesem Feature ist es nun möglich diese Mechanismen für die entsprechenden ViewModels automatisch zu erzeugen. Dies ist übrigens auch WPF Version 4 zu verdanken:
WPF supports data binding to objects that implement IDynamicMetaObjectProvider. For example, if you create a dynamic object that inherits from DynamicObject in code, you can use markup extension to bind to the object in XAML. For more information, see the Binding Sources Overview.
(Quelle: http://msdn.microsoft.com/en-us/library/bb613588(VS.100).aspx#binding)

Damit der nachfolgende Artikel nachvollziehbar ist, solltet ihr euch auch einen Artikel über die DLR ansehen:
Wie in den Artikeln zu lesen, können Klassen geschaffen werden, deren Properties (sogar Methoden) dynamisch erstellt werden. Das machen wir uns zu nutze um das INotifyPropertyChange-Interface dynamisch zu  wrappen. Außerdem wäre es nicht schlecht, wenn man im ViewModel die Commands bequem über Attributes definieren könnte und sich somit die Initialisierung der ICommand-Properties spart. Ich habe mir das ungefähr so vorgestellt:
public class MainViewModel
{
 public MainViewModel() { }
 
 public IList<User> UserCollection { get; set; }
 public IList<Article> ArticleCollection { get; set; }
 
 public string Title { get; set; }
 
 [Command("AddArticleCommand", "CanAddArticle", 
         typeof(MainViewModel), new String[] {"Title"})]
 public void AddArticleCommandExecute(object objArticle)
 {
  Article article = objArticle as Article;
  Console.WriteLine("Add article " + article.Name);
  Title = article.Name;
 }
 
 public bool CanAddArticle(object objArticle) 
 {
  return objArticle != null;
 }
 
 [Command]
 public void HelpCommandExecute(object obj)
 {
  Console.WriteLine("Here you get some help.");
 }
}
Zu sehen ist die Klasse MainViewModel mit 3 Properties und 3 Methoden. Der Wrapper soll nun dafür sorgen, dass beim setzen eines Properties das PropertyChangedEvent ausgelöst wird. Außerdem sollen ICommand-Properties für jede Methode, die mit einem Command-Attribut versehen ist dynamisch erzeugt werden.

Schritt 1: INotifyPropertyChanged

Beginnen wir also mit der Implementierung der Wrapper-Klasse. Ich habe hier eine generische Klasse genommen. Das hat den Vorteil, dass man aufwendigen Reflection-Operationen für einen Typen nur einmal aufwenden muss. Diesen Teil der Arbeit habe ich nämlich in einen Static-Konstruktor gepackt:
public sealed class DynamicViewModel<T> : DynamicObject, INotifyPropertyChanged
{
 private static readonly SortedDictionary<string, PropertyInfo> properties = new ...;
 
 static DynamicViewModel()
 {
  // init all properties
  var props = typeof(T)
   .GetProperties()
   .Where(p => p.CanRead);
  
  foreach (var p in props)
   properties.Add(p.Name, p);
 }
        ...
}

Wie im Code zu sehen, werden alle Readable Properties des Types T abgefragt und in eine Map gespeichert. Diese Map benöigen wir jetzt für das Setzen und Zurückgeben der veimeindlichen Properties. Hierfür überschreiben wir die zwei Methoden der Klasse DynamicObject: TryGetMember, TrySetMember:
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
 PropertyInfo propInf = null;
 
 if (properties.TryGetValue(binder.Name, out propInf))
 {
  result = propInf.GetValue(instance, null);
 }
 else
 {
  // if debug mode do some extra stuff.
  result = null;
  return false;
 }
 
 return true;
}

public override bool TrySetMember(SetMemberBinder binder, object value)
{
 PropertyInfo propInf = null;
 
 if (properties.TryGetValue(binder.Name, out propInf) && propInf.CanWrite)
 {
  propInf.SetValue(instance, value, null);
  OnPropertyChanged(propInf.Name); /* Hier wird das PropertyChangedEvent ausgelöst */
  
  return true;
 }
 else
 { 
  // if debug mode do some extra stuff. 
  return false;
 }
}
Und siehe da, jetzt wird beim setzen der Properties automatisch das PropertyChangedEvent ausgelöst (Zeile 26):
dynamic viewModel = new DynamicViewModel<MainViewModel>(new MainViewModel(), true);

PropertyChangedEventHandler handler = (s, e) => 
   Console.WriteLine("Property Changed: " + e.PropertyName);
viewModel.PropertyChanged += handler;

viewModel.Title = "Neuer Titel!";

// Ausgabe: Property Changed: Title

Das ist schonmal nicht schlecht. Aber jetzt kommen wir zum etwas komplizierteren Teil: Die dynamische Erstellung der Commands.

Schritt 2: Commands

In WPF übergibt man die Funtkionalität des ViewModels über sogenannte Commands an die View. Commands sind Klassen, die das Interface ICommand implementieren. Das Inteface ist folgendermaßen definiert:
public interface ICommand
{
 void Execute(object param);
 bool CanExecute(object param);

 event EventHandler CanExecuteChanged
}
Die Aufgabe des Wrappers ist es nun, der View Properties des Typs ICommand für die Methoden, die mit einem CommandAttribut versehen sind, zur Verüfung zu stellen. Das CommandAttribute beinhaltet Informationen über den CommandName, der CanExeucte-MethodInfo und den betroffenen Properties, welche von dem Command verändert werden. Die Klasse sieht wie folgt aus:
public class CommandAttribute : Attribute
{
 private string canExecuteName;
 private Type type;
 
 public CommandAttribute()
 {
 }
 
 public CommandAttribute(string commandName, String canExecuteName = null, 
    Type type = null, String[] affectedProps = null)
 {
  if (canExecuteName != null && type == null)
    throw new ArgumentNullException("type");
  
  CommandName = commandName;
  AffectedProperties = affectedProps;
  this.canExecuteName = canExecuteName;
  this.type = type;
 }
 
 public CommandAttribute(String canExecuteName, Type type, 
    String[] affectedProps = null)
 {
  AffectedProperties = affectedProps;
  this.canExecuteName = canExecuteName;
  this.type = type;
 }
 
 public CommandAttribute(string commandName, String[] affectedProps)
 {
  CommandName = commandName;
  AffectedProperties = affectedProps;
 }
 public string CommandName { get; internal set; }
 
 public String[] AffectedProperties { get; private set; }
 
 public MethodInfo CanExecuteMethodInfo
 {
  get
  {
   if (canExecuteName != null)
    return type.GetMethod(canExecuteName); 
   else
    return null;
  }
 }
}
Nun müssen wir den Statischen Konstruktor von DynamicViewModel<T> erweitern, um die Methoden in eine Map zu speichern:
private static readonly List<Tuple<CommandAttribute, MethodInfo>> commands = ... ;

static DynamicViewModel()
{
 /* 
  * Init the properties...
  */


 // init all methods
 var methods = typeof(T)
  .GetMethods()
  .Select(m=>Tuple.Create(m.GetCustomAttributes(typeof(CommandAttribute),false),m))
  .Where(m => m.Item1.Length > 0)
  .Select(m => Tuple.Create(m.Item1.First() as CommandAttribute, m.Item2));

 foreach (var m in methods)
 {
  var mthdAttr = m.Item1;
  var mthdInf = m.Item2;
  
  // if there is no command name, get name over convention
  if (mthdAttr.CommandName == null || mthdAttr.CommandName == string.Empty) {
   int i = mthdInf.Name.IndexOf("Command") + "Command".Length;
   if (i != -1)
    mthdAttr.CommandName = mthdInf.Name.Substring(0, i);
   else throw new ArgumentException("The method attribute has no name!", 
         "CommandName");
  }
  
  commands.Add(m);
 }
}
Jetzt haben wir zwar die MethodInfo´s, jedoch müssen noch die ICommand-Properties erzeugt werden. Hierfür benötigen wir leider eine Instanz des ViewModels (schließlich handelt sich sich ja um eine Methode und nicht um eine Funktion). Deshalb geschieht dieser Schritt im "normalen" Konstruktor:
private readonly SortedDictionary<string, ICommand> propertyCommands = ...;

public DynamicViewModel(T instance, bool addMethods = false)
{ 
 this.instance = instance;
 
 if (addMethods) foreach (var cmd in commands)
 {
  var mthdAttr = cmd.Item1;
  var mthdExecInf = cmd.Item2;
  var mthdCanExecInf = mthdAttr.CanExecuteMethodInfo;
  
  Action<object> execute = (Action<object>) Delegate.CreateDelegate(
    typeof(Action<object>), 
    instance, mthdExecInf);

  Action<object> finalExecute = p => {
   execute(p);
   if (mthdAttr.AffectedProperties != null) 
   {
    foreach (var str in mthdAttr.AffectedProperties) 
     OnPropertyChanged(str);
   }
  };
  
  Predicate<object> canExecute = null;
  if (mthdCanExecInf != null)
  {
   canExecute = (Predicate<object>) Delegate.CreateDelegate(
    typeof(Predicate<object>), 
    instance, mthdCanExecInf);
  }
  else
  {
   canExecute = p => true;
  }
  
  propertyCommands.Add(mthdAttr.CommandName, new CommandImpl(finalExecute, canExecute));
 }
}
Auffallend ist vielleicht die Funktion Delegate.CreateDelegate. Hier werden (wie der Name verrät) zwei Delegates für die Instanz instance erzeugt: canExecute und execute. Das Delegate execute wird in Zeile 15 um die Fähigkeit erweitert, durch das Delegate (bzw. Methode) veränderte Properties den Event-Abonnenten über das PropertyChangedEvent zu melden. Anschließend wird in der Map propertyCommands ein neuer Eintrag mit dem Namen des Commands als Key und dem Command, welches aus den beiden Delegates gebildet wird, als Value hinzugefügt. Zu guter Letzt muss noch die TryGetMember-Methode erweitert werden:
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
 PropertyInfo propInf = null;
 ICommand command = null;
 
 if (properties.TryGetValue(binder.Name, out propInf))
 {
  result = propInf.GetValue(instance, null);
 }
 else if (propertyCommands.TryGetValue(binder.Name, out command))
 {
  result = command;
 }
 else
 {
  // if debug mode do some extra stuff.
  result = null;
  return false;
 }
 
 return true;
}
Fertig ist der DynamicViewModel-Wrapper. Natürlich kann dieser Wrapper noch an einigen Stellen verbessert werden, die Idee sollte aber nun klar sein.

Inspiriert wurde ich von folgendem MSDN-Artikel:
Der Download des Beispielcodes ist hier zu finden: Datei.

Für Anregungen und Kritik wäre ich wie immer dakbar ;)

Dienstag, 12. Juli 2011

Präsentation: GKasse v.0.5.9

Heute ist es soweit, meine ersten Ergebnisse des Projektes GKasse zu präsentieren. Das Program gliedert sich in zwei Teile. Zunächst wenden wir uns dem ersten Teil, dem FrontOffice, zu, danach gehen wir dann direkt zum zweiten Teil, dem BackOffice, über.

Das FrontOffice-Modul ist, wie der Name schon verrät, für die äußeren Handlungen eines Restaurants zuständig. Dieser Teil wird auch hauptsächlich von den Kellnern, welche ihre Kunden zu bedienen und kassieren haben, bedient. Nach dem Start des Programms finden sich die Raumübersicht auf der linken Seite und die Auswahl aller anderen Räume auf der rechten Seite wieder. Wird auf ein Tisch geklickt, so öffnet sich die Kassenansicht mit der aktuellen Bestellung des Tisches. Hat ein Kellner einen Tisch geöffnet, ist er der einzige, der die Bestellung des Tisches verwalten kann. Dadurch werden mehrere Kellner, welche alle ein eigenes Konto mit eigenem Passwort haben, auf einmal unterstützt. So ist es möglich eine getrennte Kasse zu führen. Außerdem erleichtert dies erheblich die Koordination der Bedienungen, da das System jedem Kellner individuell anzeigt, ob der jeweilige Tisch von diesem Kellner oder von einem anderen Kellner bedient wird oder gar unbedient ist. Dies wird durch eine farbliche Markierung des Tisches ermöglicht.

Das FrontOffice beinhaltet natürlich noch die Kassenansicht, welche mit der Bestellungsübersicht, Schnellwahlartikel und einem Nummernblock für die Eingabe der Artikel durch Artikelnummer  bespickt ist. Weitere Merkmale dieser Kasse sind spezielle Funktionen (rechts oben auf dem rechten Bild zu erkennen), wie das Wechseln des Tisches oder die Gestaltung eines freien Preises (für eventuelle Extra-Artikel), eine Storno-Funktion, damit versehentliche Bestellungen zurückgenommen werden können und eine Beschreibung der in der Bestellübersicht ausgewählten Artikel. Für eine schnelle und einfache Bedienung werden auch Schnellwahltasten unterstützt. So ist es mit nur einem Tastendruck möglich, die aktuelle Bestellung auszudrucken und zur Raumansicht zu wechseln.

Der BackOffice-Bereich ist für den Inhaber des Restaurants bestimmt. Dort wird es dem so genannten Administrator des Programms ermöglicht das Kassensystem zu gestalten, verändern und zu verwalten. So hat er dort die Möglichkeit Warengruppen und Artikel, welche er der Kundschaft anbieten möchte, zu erstellen, verändern und zu löschen. Neben den normalen Artikeln wird auch die Möglichkeit geboten, so genannte Schnellwahlartikel zu erstellen. Diese sind für eine saubere und schnelle Bedienung im FrontOffice-Bereich von Nöten. Hier kann der Administrator nämlich für eine ordentliche Strukturierung sorgen.
Eine Verwaltung für alle Mitarbeiter des Restaurantes ist auch enthalten. Hier können die Passwörter der Kellner gesetzt werden. Außerdem wird hier auch die Einstellungen des Administrators festgesetzt.

Ein weiterer wichtiger Bestandteil des BackOffices ist die Finanzanalyse. Mittels ihr können alle Geschäftsprozesse im Restaurant analysiert und ausgewertet werden. Dafür stehen verschiedene Kategorien, wie die Tagesübersicht, Warenverkäufe und Kellnerübersicht zur Verfügung. Natürlich können alle Übersichten ausgedruckt werden. Um das Maß an Flexibilität zu steigern, wird eine Exportfunktion unterstützt, mit der alle Statistiken auch in anderen Programmen wie Excel ausgewertet werden können.

Das Programm wird derzeit noch von verschiedenen Restaurants geprüft, so dass eine vorzeitige Veröffentlichung der Software nicht möglich ist. Ich hoffe jedoch trotzdem, dass ich euch einen kleinen Einblick in das Projekt GKasse verschaffen konnte.
Falls noch Fragen bestehen, so bitte ich euch einen Kommentar zu schreiben.

Hier eine kleine Diashow mit Einblicken in das Programm:

Sonntag, 26. Juni 2011

Einen Taschenrechner in D programmieren

In diesem Tutorial möchte ich euch zeigen, wie man einen Taschenrechner in der Sprache D erstellt. Das soll kein einfacher Taschenrechner werden, der über irgendwelchen switch-Statements überprüft, welcher mathematischer Operator ausgewählt werden soll und dann die entsprechenden Parameter einliest, nein er soll komplexere Terme wie "max(sin(43),cos(31))*4^2" ausrechnen können.

Als ersten Schritt möchte ich mit der Frage beginnen, wie man überhaupt so einen Term auflöst. Sicher ist es ratsam zuerst den Term zu lexen, d.h in in seine Einzelteile zu zerlegen. Ein weiterer wichtiger Schritt ist die richtige Anordnung des Terms (hier kommt der Shunting-Yard-Algorithmus zum Einsatz), da es sonst sehr schwer wird, diesen abzuarbeiten.

Beginnen wir also mit der Planung des Programms:
  • Es soll eine Hauptfunktion namens 'calculate' geben, welche alle Arbeitsschritte des Moduls zusammenfasst
  • Funktion für die korrekte Trennung der Rechentypen (Zahl, Operator und Funktion)
  • Funkion für die richtige Anordnung für die spätere Umwandlung in ein Double-Wert (Der Shunting-Yard-Algorithmus, welcher den String in Reversed Polnish Notation umwandelt)
  • Funktion für die Abarbeitung und des Strings (Hier wird gerechnet)
  • kleinere Helfer-Methoden, die gut zu gebrauchen sind
  • Datentypen, die die Operatoren und Funktionen beschreiben. Diese werden später benötigt, da sie in einem Stack abgelegt werden und dort entsprechend abgearbeitet werden (zB. für die Änderung der Anordnung der Operatoren/Funktionen)
Des weiteren beinhaltet dieses Modul Funktionen, die nicht in der Standard-Lib vorhanden sind (die habe ich geschrieben), aber einer genaueren Erläuterung nicht Wert sind. Achtung: D bietet die Möglichkeit an, diese Methoden als Extension zu verwenden, weshalb man glauben könnte, dass es sich bei dem betreffenden Datentyp um ein Objekt handelt, was aber nicht der Fall ist. Ein Beispiel:


1
2
3
4
...
char[] hallo = "Hallo Welt";
int i = hallo.findFirst('l');
...
Diese Methoden sind im Anhang (dort ist auch ein Beispielprogramm des Moduls untergebracht) unter extd/array.d zu finden (Nachdem ihr die Datei entpackt habt). Hier die Liste der hier nicht aufgeführten Methoden:
  • int findFirst(T)(T[] array, T item);
  • int findFirst(T)(T[] array, T[] item);
  • bool contains(T)(T[] array, T item);
  • bool contains(T)(T[] array, T[] item);
  • T[] reverse(T)(T[] array);
  • T pushLast(T[] array);


Jetzt kommen wir zur Implementierung. Ich möchte hier mit der Hauptfunktion beginnen, damit ihr eine Übersicht habt, welche Funktionen wie heißen, und wie sie mit dem gesamten Modul in Verbindung stehen:

(Wenn ihr das selber macht, solltet ihr das natürlich anders angehen (Im Kopf und(oder auf einem Blatt Papier... ;-) )

Als nächstes machen wir mit den Operatoren und Funktionen weiter. Diese müssen folgende Kriterien erfüllen:
  • Bekannte Parameterzahl (Damit später bekannt ist, wie viele Werte vom Stack, in dem die Ergebnisse temporär gespeichert werden, zu holen sind)
  • Bekannte Assoziation (Links oder rechts)
  • Bekannte Priorität (Wichtig für die "Punkt vor Strich-Regel")
  • Zeiger auf Funktion (für die Rechnung)
Nichts liegt näher, als diese ein einem Typ (in diesem Fall eine Struktur) zusammenzufassen:

Damit man diese Operatoren und Funktionen auch leichter ansprechen kann, habe ich jeweils ein assoziatives Array (Das Array der Operatoren ist selbstverständlich konstant) erstellt, was die Operatoren und Funktionen mit dem Passenden Schlüssel speichert. Die Initialisierung findet im Konstruktor des Moduls statt:

Hier sind die kleinen Helfer-Methoden, welche für die Identifizierung der Teil-Terme zuständig sind:



Das wäre geschafft. Jetzt können wir mit der ersten eigentlichen Funktion "lexString" beginnen. Diese Funktion dient dazu, den String in seine Einzel-Teile zu zerlegen. Das sollte auch kein sonderlich großes Problem sein:

So jetzt haben wir einen funktionstüchtigen Lexer, der uns alle verschiedene Typen in ein extra Array speichert ( '(' und ')' sind auch Operatoren).

Jetzt kommt eigentlich der schwierigste Teil des Tutorials. Hier wird der Term (der schon vom Lexer bearbeitet worden ist) so verändert, dass er später sehr leicht ab zu arbeiten ist. Hierzu habe ich mir den Shunting Yard-Algorithmus ausgesucht, der mir den Term in die Polnische Notation umwandelt. Da diese Themen relativ komplex sind und die Länge des Tutorials um Längen sprengen würde, bitte ich euch das Prinzip des Shunting-Yard Algo's und das der Polnischen Notation im Internet oder sonst wo zu recherchieren. Nur so viel sei zum Shunting-Yard-Algorithmus zu erwähnen: Man packt alle Operatoren in einen extra Stack und gibt sie je nach Priorität auch wieder aus, so dass am Ende die Operatoren hinter den Parametern stehen. (Auch eine Verschachtelung ist möglich) Kleines Beispiel: 4+5*3 wird zu 4 5 3 * +.
Genauer Infos findet ihr hier: http://en.wikipedia.org/wiki/Shunting-yard_algorithm
Das zur Theorie, hier der Code:



Wichtige Variablen:
  • lxdTerm: Dieses String-Array wird behandelt (Parameter der Funktion)
  • output: das in der RPN (Reversed Polnish Notation) formatierte String-Array (Result der Funktion)
  • opStack: Hier werden die Operationen zwischengespeichert
  • pCurOp: Zeiger auf aktuelle Operation (im lxdTerm-Array)
Beschreibung:
Wie wir am Code sehen können, wird jeder einzelne String im String-Array über eine for-Schleife behandelt. Dabei wird abgefragt, um welchen Typ es sich handelt. Ist es eine ganz normale Nummer, so wird diese dem output hinzugefügt, bei einer '('-Klammer dem Operatorenstack (hier ist keine Überprüfung nötig). Wird das Zeichen ',' gefunden (im Normalfall zur Trennung der Parameter gedacht) werden alle Operatoren bis zu ')' dem output hinzugefügt (so kann man schön alle Parameter abarbeiten). Wird das ')'-Zeichen gefunden, passiert im Grunde das selbe, wie beim ','(irgendwie logisch). Wird jedoch eine Operation anderer Art gefunden, so wird, falls die oberste Operation auf dem opStack eine höhere bzw. gleiche Priorität hat, wie die aktuelle Operation, diese (die Operation auf dem Stack) dem output hinzugefügt.

So, das war der schwierigste Teil des Tutorials. Jetzt kommen wir zur finalen Arbeit: Wir arbeiten das Resultat des Shunting-Yard-Algorithmus von Zahl zu Zahl, von Operator zu Operator und von Funktion zu Funktion ab. Dabei speichern wir die Ergebnisse (Zahl liefert auch ein Ergebnis) in den Result-Stack, und sobald ein Operator oder eine Funktion kommt, welche mehr wie einen Operator benötigen, fassen diese die obersten Elemente (Anzahl der Elemente von der Parameteranzahl der Funktion abhännig) in ein Stack-Element zusammen. Das ganze kann dann ungefähr so aussehen:



Das war schon alles. Natürlich können wir die Liste der Operatoren vergrößern, indem man zB. ein Plugin-System einbaut, welches die benötigten Strukturen lädt und in den function-hash hinzufügt. Außerdem wären Variablen nicht schlecht. Allerdings sollte man diese mit einem $ beginnen lassen (so wie zB. in PHP), damit man diese nicht mit einer Funktion verwechseln kann.

Ich hoffe, dass es sich (für euch) gelohnt hat dieses Tutorial zu lesen und ihr jetzt das Problem, falls ihr das jemals haben werden, angehen könnt!

Download eines Beispiel-Programms mit diesem Modul
Download 

GKasse - eine Gastrokasse

GKasse ist ein Projekt - realisiert mit C# und der WPF-Technologie, welches Aufgaben in Restaurants deutlich vereinfachen soll. Dieses Vorhaben wird durch verschiedene Module des Programms gelöst, wobei die Kasse sicherlich das wichtigste Modul ist. Bei dem Desing wurde auf größtmögliche Benutzerfreundlichkeit geachtet, was unter anderem durch eine moderne Umgebung ermöglicht wurde. Außerdem wird auf die Flexibilität des Programms ein großer Wert gelegt, damit es für alle denkbaren Szenarien in der Gastronomie gewappnet ist. Weitere Aufgaben der GKasse sind im FrontOffice-Bereich die Unterstützung mehrerer Bedienungen, damit eine getrennte Kasse ermöglicht wird, und ein Reservierungsplaner. Der BackOffice-Bereich ist für die Verwaltung aller Räume, Tische, Mitarbeiter und deren Rechte, Waren und Schnellwahltasten zuständig. Außerdem sorgt eine umfassende Finanzübersicht für umfassenden Überblick. Nach der Fertigstellung des Programms wird es zum Verkauf angeboten. Das Projekt soll über mehrere Jahre gepflegt werden.
Hier gelangt ihr zu den Homepages:
Ich werde regelmäßig über Änderungen und Fortschritte dieses Projektes bloggen.

    Hello World

    HeyHo an alle die das hier lesen!
    Hier möchte ich euch über Neuigkeiten meiner Programme informieren und euch ein paar interessante Themen zeigen. Außerdem werde ich hier ein paar Tutorien posten. Ich wünsche euch viel Spaß beim Lesen und würde mich außerdem über Feedbacks und Kommentare sehr freuen ;-)

    Euer Blogger Simon