Als selbständiger oder freiberuflicher Softwareentwickler braucht man am Ende vor allem eines: eine Anwendung zum Erstellen von Rechnungen. Diese wollen wir im vorliegenden Artikel programmieren – vom Entwurf des Datenmodells über die Erstellung des Entity Data Models und die Benutzeroberfläche bis zum Ausdrucken der Rechnung als PDF oder mit dem Drucker.
Funktionen der Rechnungsverwaltung
Welche Aufgaben wollen wir mit der Rechnungsverwaltung erledigen – welche Eingangsdaten haben wir, was soll am Ende herauskommen Und welche Funktionen bilden wir dabei mit der Benutzeroberfläche ab
Wir wollen eine kleine Anwendung bauen, die verschiedene Funktionen über das Ribbon anbietet. Dazu gehören die Verwaltung von Rechnungsempfängern und der Rechnungen selbst. Für die Rechnungsempfänger wollen wir eine Übersicht der vorhandenen Kunden in Listenform anbieten und ein Fenster zum Anlegen eines neuen Kunden. Die Seite zum Anlegen eines Kunden wollen wir auch für die Detailansicht dieses Kunden nutzen. Praktisch ist dann, wenn wir direkt die Rechnungen, die bisher für diesen Kunden erstellt wurden, in einer Liste in dieser Ansicht anzeigen. Außerdem soll das Fenster jeweils eine Schaltfläche zum Anlegen einer neuen Rechnung enthalten und eine zum Ausdrucken einer markierten Rechnung.
Für die Rechnungen wollen wir ebenfalls ein eigenes Fenster vorsehen. Dieses enthält Steuer-elemente zur Eingabe der allgemeinen Daten einer Rechnung, also Rechnungsdatum, Rechnungsbetreff, Rechnungstext und Rechnungsuntertext. Außerdem soll ein Listensteuerelement alle Rechnungspositionen mit Bezeichnung, Nettobetrag, Menge und Mehrwertsteuersatz anzeigen.
Schließlich fehlt noch die Ausgabe der Rechnung – diese wollen wir über ein FlowDocument realisieren, das in einem entsprechenden Steuer-element in einem eigenen Fenster angezeigt wird und ausgedruckt oder als PDF gespeichert werden kann.
Datenmodell der Anwendung
Das Datenmodell haben wir, wenn wir schon im Artikel Access zu EDM: Dateien erstellen (www.datenbankentwickler.net/219) eine kleine Funktion zum Erstellen eines Entity Data Models programmiert haben, schnell mit Access definiert (siehe Bild 1). Wir verwenden eine Tabelle namens tblKunden, welche die üblichen Kundendaten enthält. Die Anreden speichern wir in einer eigenen Tabelle namens tblAnreden, die von der Tabelle tblKunden aus über das Feld AnredeID referenziert wird. Die grundlegenden Rechnungsdaten speichern wir in der Tabelle tblRechnungen. Hier finden Sie das Rechnungsdatum und die übrigen Informationen, die in der Rechnung rund um die Auflistung der Rechnungspositionen abgebildet werden. Außerdem enthält die Rechnungstabelle ein Fremdschlüsselfeld namens KundeID, mit dem der Kunde zur jeweiligen Rechnung festgelegt wird. Schließlich folgen noch die Rechnungspositionen in der Tabelle tblRechnungspositionen. Die einzelnen Positionen sind über das Feld RechnungID mit der Tabelle tblRechnungen verknüpft.
Bild 1: Datenmodell der Rechnungsverwaltung
Achtung: Damit das nachfolgende Erstellen des Entity Data Models funktioniert, müssen die Primärschlüsselfelder die Entität im Namen enthalten, bei Kunde also beispielsweise KundeID.
Entity Data Model erstellen
Mit dem folgenden Aufruf der Funktion EDMDateienErstellen legen wir im Unterverzeichnis RechnungsverwaltungContext der Access-Datenbank einige Verzeichnisse und Dateien an, die wir gleich im Anschluss weiterverarbeiten:
EDMDateienErstellen False, "RechnungsverwaltungContext", "DataModel", True, True
Zuvor erstellen wir noch ein neues Visual Studio-Projekt mit der Vorlage Visual Basic|Windows Desktop|WPF-App namens Rechnungsverwaltung.
Nun öffnen wir mit dem Kontextmenü-Eintrag Hinzufügen|Neues Element… des Projekt-Elements im Projektmappen-Explorer den Dialog Neues Element hinzufügen, wählen dort das Element ADO.NET Entity Data Model aus und geben dafür den Namen RechnungsverwaltungContext ein.
Im folgenden Schritt wählen wir den Eintrag Leeres Code First-Modell aus (siehe Bild 2).
Bild 2: Typ des Modells auswählen
Danach ziehen Sie den Inhalt aus dem soeben von Access aus erstellten Ordner RechnungsverwaltungContext (alle enthaltenen Elemente, nicht den Ordner selbst) auf das Projekt-Element Rechnungsverwaltung. Dies ersetzt die Datei RechnungsverwaltungContext.vb, was sich Visual Studio mit der Meldung aus Bild 3 bestätigen lässt.
Bild 3: Überschreiben der Datei bestätigen
Mit einer weiteren Meldung bestätigen Sie außerdem, dass die extern geänderten Elemente in Visual Studio neu geladen werden sollen.
Datenbank erstellen
Wenn Sie jetzt noch in der Paket-Manager-Konsole über den Menüeintrag Extras|Nuget-Paket-Manager|Paket-Manager-Konsole die folgenden beiden Befehle aufrufen, wird auch noch eine SQL Server-Datenbank auf Basis des Entity Data Models erstellt, die wir im weiteren Verlauf benötigen:
add-migration init update-database
Danach finden Sie die Datenbank im SQL Server-Objekt-Explorer vor und können gleichzeitig über das Entity Data Model auf die enthaltenen Daten zugreifen beziehungsweise neue Daten schreiben.
Ribbon der Anwendung
Damit wir die einzelnen Bereiche der Anwendung öffnen können, fügen wir dem Fenster Main.xaml ein Ribbon hinzu. Dazu müssen wir zunächst einen Verweis hinzufügen (Menüeintrag Projekt|Verweis hinzufügen…), und zwar auf die Bibliothek System.Windows.Controls.Ribbon (siehe Bild 4). Danach fügen wir dem Code des Fensters Main.xaml die folgende Ribbon-Definition hinzu:
Bild 4: Hinzufügen eines Verweises auf die Bibliothek System.Windows.Controls.Ribbon
<Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <Ribbon> <RibbonTab Header="Rechnungsverwaltung"> <RibbonGroup Header="Kunden"> <RibbonButton x:Name="btnKundenuebersicht" Label="Übersicht" LargeImageSource="Images/users.png" Click="BtnKundenuebersicht_Click"></RibbonButton> <RibbonButton x:Name="btnKundeHinzufuegen" Label="Neuer Kunde" LargeImageSource="Images/user_add.png" Click="BtnKundeHinzufuegen_Click"></RibbonButton> </RibbonGroup> <RibbonGroup Header="Rechnungen"> <RibbonButton x:Name="btnRechnungsuebersicht" Label="Übersicht" LargeImageSource="Images/invoice.png" Click="BtnRechnungsuebersicht_Click"></RibbonButton> <RibbonButton x:Name="btnRechnungHinzufuegen" Label="Neue Rechnung" LargeImageSource="Images/invoice_add.png" Click="BtnRechnungHinzufuegen_Click"></RibbonButton> </RibbonGroup> </RibbonTab> </Ribbon> </Grid>
Damit erstellen wir ein Ribbon, das wie in Bild 5 aussieht. Die Ereignismethoden für die Schaltflächen werden automatisch angelegt, wenn Sie im XAML-Code Click gefolgt vom Gleichheitszeichen eingeben und dann die Tabulator-Taste betätigen.
Bild 5: Ribbon der Anwendung
Kunden anlegen
Wir beginnen mit der Seite zum Anlegen eines neuen Kunden, da wir ja noch keine Kundendaten in der Datenbank haben – und diese sollten ja vorhanden sein, damit wir Material für die Übersichtsseite der Kunden haben. Wir erstellen also explizit kein neues Fenster, sondern eine Seite (Page). Diese fügen wir hinzu, indem wir den Kontextmenü-Befehl des Projekt-Elements im Projektmappen-Explorer betätigen und im Dialog Neues Element hinzufügen den Eintrag Seite (WPF) auswählen. Die neue Seite soll Kundendetails heißen und nicht nur zum Anlegen neuer Kunden, sondern auch zur Anzeige der Details bereits angelegter Kunden dienen. Der XAML-Code sieht in gekürzter Form wie folgt aus:
<Page x:Class="Kundendetails" ...> <Page.Resources>...</Page.Resources> <Grid> ...
Bei den TextBox-Elementen wird der Inhalt des jeweiligen Kunde-Elements durch eine Bindung an das Feld Kunde.ID erreicht:
<Label Content="ID:" Grid.Column="0" /> <TextBox x:Name="txtID" Grid.Column="1" HorizontalAlignment="Left" Text="{Binding Kunde.ID, Mode=TwoWay}" Width="50" IsEnabled="False" BorderBrush="Transparent" />
Die Ausnahme ist hier die das ComboBox-Element cboAnrede. Es verwendet eine ganze Reihe von Attributen, damit es die Daten der Tabelle Anreden anzeigt. ItemsSource gibt den Namen der Eigenschaft der Code behind-Datei an, welche die Daten für das ComboBox-Element liefert. SelectedItem legt fest, wie der ausgewählte Eintrag selektiert wird. SelectedValuePath gibt den Namen des Feldes an, über den das selektierte Element ermittelt wird.
Schließlich gibt das Binding-Element im DataTemplate-Element noch an, welches Feld der Anrede-Objekte im Kombinationsfeld angezeigt wird:
<Label Content="ID:" Grid.Column="0" /> <TextBox x:Name="txtID" Grid.Column="1" HorizontalAlignment="Left" Text="{Binding Kunde.ID, Mode=TwoWay}" Width="50" IsEnabled="False" BorderBrush="Transparent" /> <Label Content="Anrede:" Grid.Column="0" Grid.Row="1" /> <ComboBox x:Name="cboAnredeID" Grid.Row="1" Grid.Column="1" Width="100" ItemsSource="{Binding Anreden}" SelectedItem="{Binding Kunde.Anrede, ValidatesOnDataErrors=True}" SelectedValuePath="ID" SelectionChanged="Anrede_SelectionChanged"> <ComboBox.ItemTemplate> <DataTemplate> <TextBlock> <TextBlock.Text> <MultiBinding StringFormat="{}{0}"> <Binding Path="Bezeichnung" /> </MultiBinding> </TextBlock.Text> </TextBlock> </DataTemplate> </ComboBox.ItemTemplate> </ComboBox> <Label Content="Vorname:" Grid.Column="0" Grid.Row="2" /> <TextBox x:Name="txtVorname" Grid.Column="1" Grid.Row="2" Width="200" Text="{Binding Kunde.Vorname, Mode=TwoWay, ValidatesOnDataErrors=True}" /> <Label Content="Nachname:" Grid.Row="3" /> <TextBox x:Name="txtNachname" Grid.Column="1" Grid.Row="3" Width="200" Text="{Binding Kunde.Nachname, Mode=TwoWay, ValidatesOnDataErrors=True}" /> <Label Content="Firma:" Grid.Row="4" /> <TextBox x:Name="txtFirma" Grid.Column="1" Grid.Row="4" Width="200" Text="{Binding Kunde.Firma, Mode=TwoWay, ValidatesOnDataErrors=True}" /> <Label Content="Straße:" Grid.Row="5" /> <TextBox x:Name="txtStrasse" Grid.Column="1" Grid.Row="5" Width="200" Text="{Binding Kunde.Strasse, Mode=TwoWay, ValidatesOnDataErrors=True}" /> <Label Content="PLZ:" Grid.Row="6" /> <TextBox x:Name="txtPLZ" Grid.Column="1" Grid.Row="6" Width="50" Text="{Binding Kunde.PLZ, Mode=TwoWay, ValidatesOnDataErrors=True}" /> <Label Content="Ort:" Grid.Row="7" /> <TextBox x:Name="txtOrt" Grid.Column="1" Grid.Row="7" Width="200" Text="{Binding Kunde.Ort, Mode=TwoWay, ValidatesOnDataErrors=True}" /> <Label Content="Land:" Grid.Row="8" /> <TextBox x:Name="txtLand" Grid.Column="1" Grid.Row="8" Width="200" Text="{Binding Kunde.Land, Mode=TwoWay, ValidatesOnDataErrors=True}" /> <Label Content="E-Mail:" Grid.Row="9" /> <TextBox x:Name="txtEMail" Grid.Column="1" Grid.Row="9" Width="200" Text="{Binding Kunde.EMail, Mode=TwoWay, ValidatesOnDataErrors=True}" /> <StackPanel Orientation="Horizontal" Grid.Row="10" Grid.ColumnSpan="4" > <Button x:Name="btnSpeichern" Click="btnSpeichern_Click" Content="Speichern"> <Button.Style> <Style TargetType="{x:Type Button}" BasedOn="{StaticResource {x:Type Button}}"> <Style.Triggers> <DataTrigger Binding="{Binding ElementName=cboAnrede, Path=(Validation.HasError)}" Value="True"> <Setter Property="IsEnabled" Value="False"></Setter> </DataTrigger> ... </Style.Triggers> </Style> </Button.Style> </Button> <Button x:Name="btnVerwerfen" Click="btnVerwerfen_Click" Content="Verwerfen"></Button> </StackPanel> </Grid> </Page>
Wir betten hier direkt auch die Funktion zum Validieren der Textfelder ein – daher finden Sie im Binding-Element auch jeweils die Eigenschaft ValidationOnDataErrors mit dem Wert True.
Die so erstellte Seite betten wir beim Anklicken des Ribbon-Eintrags btnKundeAnlegen über die folgende Methode ein:
Private Sub BtnKundeHinzufuegen_Click(sender As Object, e As RoutedEventArgs) Dim pge As Kundendetails pge = New Kundendetails(Me.WorkZone) WorkZone.Content = pge End Sub
Wir erstellen also ein neues Objekt auf Basis des Page-Elements Kundendetails und übergeben diesem einen Verweis auf das Frame-Element WorkZone, in dem wir es anzeigen wollen.
Diesen benötigen wir später, um die Seite wieder aus dem Frame-Element zu entfernen. Dann weisen wir es dem mit WorkZone benannten Frame-Element zu, das wir wie folgt im Fenster Main.xaml eingefügt haben:
<Frame x:Name="WorkZone" Grid.Row="1" NavigationUI-Visibility="Hidden" Navigated="WorkZone_Navigated"></Frame>
Nach dem Starten des Projekts und einem Klick auf die Ribbon-Schaltfläche Neuer Kunde sieht das Fenster wie in Bild 6 aus. Hier wird gerade die Anrede für einen neuen Kunden ausgewählt.
Bild 6: Die Seite Kundendetails.xaml im Fenster Main.xaml
Damit die Daten so wie im Screenshot auf der Seite landen, sind noch einige Zeilen in der Code behind-Datei nötig. Diese sehen wie folgt aus. Die Klasse deklariert zunächst drei Variablen. Die erste namens _kunde nimmt den aktuell angezeigten Kunden auf, die zweite namens _anreden ist eine Auflistung von Objekten des Typs Anrede und die dritte namens dbContext referenziert das Context-Objekt:
Class Kundendetails Private _kunde As Kunde Private _anreden As List(Of Anrede) Private dbContext As RechnungsverwaltungContext
Außerdem benötigen wir noch eine Variable, mit der wir das Frame-Objekt referenzieren, in dem das Page-Element angezeigt wird:
Private _frame As Frame
Die private Variable _kunde machen wir über die öffentliche Eigenschaft Kunde schreib- und lesbar:
Public Property Kunde As Kunde Get Return _kunde End Get Set(value As Kunde) _kunde = value End Set End Property
Möchten Sie weiterlesen? Dann lösen Sie Ihr Ticket!
Hier geht es zur Bestellung des Jahresabonnements des Magazins Visual Basic Entwickler:
Zur Bestellung ...
Danach greifen Sie sofort auf alle rund 200 Artikel unseres Angebots zu - auch auf diesen hier!
Oder haben Sie bereits Zugangsdaten? Dann loggen Sie sich gleich hier ein: