VB.NET Kalender Steuerelement – Teil 2

Inhaltsverzeichnis
- 1 VB.NET Kalender Steuerelement – Teil 2
- 2 Erweiterungsmethoden erweitert
- 3 Termin-Funktionalitäten – VB.NET Kalender Steuerelement
- 4 IAppointmentService
- 5 AppointmentService
- 6 Termine laden – VB.NET Kalender Steuerelement
- 7 Zwischenstand – VB.NET Kalender Steuerelement
- 8 Termine hinzufügen
- 9 Termine bearbeiten – VB.NET Kalender Steuerelement
- 10 Weitere Klassen
- 11 Termin löschen – VB.NET Kalender Steuerelement
- 12 Kompletter Code – VB.NET Kalender Steuerelement
- 13 Downloads
VB.NET Kalender Steuerelement – Teil 2
Lerne in meinem zweiten Teil des VB.NET Kalender Steuerelement Beitrages, wie Du unter Anderem Termine lädst und Diese im Kalender anzeigst.
Ersten Teil verpasst!?
Du bist auf der Suche nach dem ersten Teil – hier wirst Du fündig!
Erweiterungsmethoden erweitert
Weitere Helferlein
Achtung Wortspiel – hier nehmen wir uns kurz die Zeit, unsere Erweiterungsmethoden wie folgt zu erweitern.
Im Laufe des Beispiels habe ich noch für mich z. B. noch die Notwendigkeit gesehen, einfach an den Start, bzw. Ende eines Tages zu kommen.
Da mir bisher nur die Date-Property einer Date-Instanz bekannt, ist aber ich die API bzw. die Erweiterungsmethoden „vollständig“ halten wollte, habe ich folgende beiden Funktionen implementiert:
Public Module modDateExtensions
    ' ... previous..
    <Extension>
    Public Function StartOfDay(dt As Date)
        Return dt.Date
    End Function
    <Extension>
    Public Function EndOfDay(dt As Date)
        Return New Date(dt.Year, dt.Month, dt.Day, 23, 59, 59)
    End Function
End Module
Diese beiden Funktionen geben jeweils den Anfang, bzw. das Ende eines Tages zurück.
Termin-Funktionalitäten – VB.NET Kalender Steuerelement
Wir müssen uns als nächstes darüber Gedanken machen, wie wir das Laden, Speichern, Bearbeiten, etc. von Terminen abbilden möchten.
Da Dein persönliches Projekt sich eventuell von meinem und Dies sich wieder von dem Projekt eines anderen Entwicklers unterscheiden wird, müssen wir das Ganze natürlich etwas dynamisch gestalten.
Wir haben gewisse „Verhaltensweisen„, Welche sich dennoch vermutlich kreuz und quer über die verschiedene Projekte wiederholen werden.
Daher denke ich, dass wir diese Verhaltensweisen gut in einem Interface zusammenfassen können.
Natürlich könnte man hier auch direkt mit einer IService-Schnittstelle beginnen und weiter abstrahieren, aber wir übertreiben es mal für dieses Beispiel hier zumindest nicht.
IAppointmentService
Da die heutige Welt der Programmierung zumeist asynchron abläuft, gestalten wir die Methoden asynchron.
Wer hat schon Lust, dass alles hängt und flackert, während man mit dem Kalender arbeitet..
Ich denke, dass die Methoden, bzw. Funktionen aufgrund der passenden Bezeichnungen für sich und Ihre Aufgabe selbst sprechen.
Public Interface IAppointmentService
    Function FindByIdAsync(id As Integer) As Task(Of IAppointment)
    Function FindAllByIdAsync(ids As IEnumerable(Of Integer)) As Task(Of IEnumerable(Of IAppointment))
    Function FindAllByAsync(params As Object) As Task(Of IEnumerable(Of IAppointment))
    Function CreateAsync(appointment As IAppointment) As Task
    Function UpdateAsync(appointment As IAppointment) As Task
    Function DeleteByIdAsync(id As Integer) As Task
    Function DeleteAsync(appointment As IAppointment) As Task
End Interface
Methoden
FindByIdAsync(id)
Gibt einen Termin mit der angegebenen Id zurück, falls nicht gefunden, sollte die Implementierung Nothing/null zurückgeben.
FindAllByIdAsync(ids)
Sucht mehrere Termine anhand ihrer Ids aus der Datenquelle und gibt Diese zurück.
Wenn kein einziger Termin gefunden wurde, sollte eine leere Liste zurückkommen.
FindAllByAsync(params)
Der Kalender ruft diese Methode des übergebenen Services auf, um die Termine zu laden, dafür muss man ihm mitteilen, wie die Query-Parameter zu generieren sind.
CreateAsync(appointment)
Erstellt einen Termin in der Datenquelle (persistiert Ihn) und weist dem Termin die jeweilige Id zu.
UpdateAsync(appointment)
Schreibt die überarbeiteten Daten eines Termins anhand seiner Id zurück in die Datenquelle.
DeleteByIdAsync(id)
Löscht einen Termin anhand seiner Id aus der Datenquelle.
DeleteAsync(appointment)
Ermöglicht die Löschung eines Termins anhand der Termin–Referenz.
Ruft intern die DeleteByIdAsync-Methode auf.
AppointmentService
Aufgrund der nicht vorhandenen Datenbank (oder Ähnlichem), fake ich hier praktisch eine Datenquelle mit der Service–Klasse und generiere Testdaten.
Public Class AppointmentService
    Implements IAppointmentService
    Private Property Appointments As List(Of Appointment)
    Sub New()
        Appointments = New List(Of Appointment)
        MockUpData()
    End Sub
    Private Sub MockUpData()
        Appointments.Add(New Appointment() With {.Id = 1, .Subject = "Meeting with Max", .Start = Date.Now.AddDays(-2), .[End] = Date.Now.AddDays(-2).AddHours(2)})
        Appointments.Add(New Appointment() With {.Id = 2, .Subject = "Dog training", .Start = Date.Now.AddHours(1), .[End] = Date.Now.AddHours(2)})
        Appointments.Add(New Appointment() With {.Id = 3, .Subject = "Work due!", .Start = Date.Now.AddDays(3), .[End] = Date.Now.AddDays(3).AddHours(1)})
    End Sub
    Public Function FindByIdAsync(id As Integer) As Task(Of IAppointment) Implements IAppointmentService.FindByIdAsync
        Dim result = Appointments.SingleOrDefault(Function(x) x.Id = id)
        Return Task.FromResult(CType(result, IAppointment))
    End Function
    Public Function FindAllByIdAsync(ids As IEnumerable(Of Integer)) As Task(Of IEnumerable(Of IAppointment)) Implements IAppointmentService.FindAllByIdAsync
        Dim results = Appointments.Where(Function(x) ids.Contains(x.Id))
        Return Task.FromResult(CType(results, IEnumerable(Of IAppointment)))
    End Function
    Public Function FindAllByAsync(params As Object) As Task(Of IEnumerable(Of IAppointment)) Implements IAppointmentService.FindAllByAsync
        Dim query = CType(params, QueryParams)
        Dim results = Appointments.Where(Function(x) x.Start >= query.From AndAlso x.Start <= query.To)
        Return Task.FromResult(CType(results, IEnumerable(Of IAppointment)))
    End Function
    Public Function CreateAsync(appointment As IAppointment) As Task Implements IAppointmentService.CreateAsync
        Dim newId = Appointments.Max(Function(x) x.Id)
        newId += 1
        appointment.Id = newId
        Appointments.Add(appointment)
        Return Task.CompletedTask
    End Function
    Public Function UpdateAsync(appointment As IAppointment) As Task Implements IAppointmentService.UpdateAsync
        Dim old = Appointments.Single(Function(x) x.Id = appointment.Id)
        With appointment
            old.Start = .Start
            old.End = .End
            old.Subject = .Subject
            old.Circle = .Circle
        End With
        Return Task.CompletedTask
    End Function
    Public Function DeleteByIdAsync(id As Integer) As Task Implements IAppointmentService.DeleteByIdAsync
        Dim appointment = Appointments.Single(Function(x) x.Id = id)
        Appointments.Remove(appointment)
        Return Task.CompletedTask
    End Function
    Public Function DeleteAsync(appointment As IAppointment) As Task Implements IAppointmentService.DeleteAsync
        Return DeleteByIdAsync(appointment.Id)
    End Function
End Class
Termine laden – VB.NET Kalender Steuerelement
Wenn nicht jetzt, wann dann?
Nunja, die erste aufkommende Frage wird wohl sein, wann man die Termine laden, bzw. wann man den AppointmentService dazu delegieren sollte.
Hier gibt es bei dem VB.NET Kalender wohl verschiedene Ansatzmöglichkeiten, wie beim Navigieren durch die Knöpfe, beim Starten des Programmes und somit durch das Laden des Kalenders und und und.
Was ist aber, wenn der Lade-Prozess noch im Gange ist und der Nutzer in der Zeit schon 5 weitere Male auf „zum nächsten Monat springen“ gedrückt hat?
Einen Unterschied, Welchen wir hier natürlich berücksichtigen müssen, ist z. B. der Datenspeicher, denn während dieses Beispiel zu Demonstrationszwecken nur die Mockup–Daten in der Klasse nutzt, haben wir für gewöhnlich natürlich noch den Netzwerkverkehr und dessen Latenz.
Ladevorgang
QueryParams
Hier finden wir die Filter–Daten für den Aufruf an die Such-Funktion des Services
Public Class QueryParams
    Public Property From As Date
    Public Property [To] As Date
    Sub New()
        From = Date.Now.Date.StartOfDay()
        [To] = Date.Now.EndOfDay()
    End Sub
    Sub New(from As Date, [to] As Date)
        Me.From = from
        Me.To = [to]
    End Sub
End Class
QueryParamsEventArgs
Diese Klasse dient der Kapselung von Argumenten an das zugehörige Event.
Public Class QueryParamsEventArgs
    Inherits EventArgs
    Public Property Params As QueryParams
    Sub New()
    End Sub
    Sub New(params As QueryParams)
        Me.Params = params
    End Sub
End Class
Um die Termine dann letztendlich zu laden, erweitern wir unsere „RefreshData„-Methode um folgenden Code:
RefreshData
Private Sub RefreshData()
    ClearEntries()
    RefreshDateLabel()
    RefreshRowHeaders()
    RefreshDays()
    If AppointmentService IsNot Nothing Then
        RefreshAppointments()
    End If
End Sub
RefreshAppointments
Die RefreshAppointments-Methode, Welche ich bewusst async gemacht habe, sieht so aus:
Public Async Sub RefreshAppointments()
    Dim queryParams = New QueryParams([Date].StartOfDay(), [Date].EndOfDay())
    Dim args = New QueryParamsEventArgs(queryParams)
    OnGatheringQueryParams(args)
    Dim results = Await AppointmentService.FindAllByAsync(args.Params)
    DisplayAppointments(results)
End Sub
Protected Sub OnGatheringQueryParams(e As QueryParamsEventArgs)
    RaiseEvent GatheringQueryParams(Me, e)
End Sub
Public Event GatheringQueryParams As EventHandler
Im ersten Schritt erstellen wir neue „QueryParams“ mit den aktuell bekannten Daten des Kalenders, sprich dem ausgewählten Datum.
Danach wird dort drin eine neue Instanz der QueryParamsEventArgs-Klasse erstellt, Welche wir mit den QueryParams befüllen.
Zum Schluss wird die OnGatheringQueryParams()-Methode mit den erstellten QueryParamsEventArgs als Parameter aufgerufen.
Calendar1_GatheringQueryParams
Anschließend könnten wir in der Form1 auf das Ereignis des Kalenders reagieren und die Daten manipulieren, die zur Suche verwendet werden.
    Private Sub Calendar1_GatheringQueryParams(sender As Object, e As QueryParamsEventArgs) Handles Calendar1.GatheringQueryParams
        ' Dim startOfToday = Date.Now.StartOfDay()
        ' Dim endOfToday = Date.Now.EndOfDay()
        ' e.Params.From = startOfToday
        ' e.Params.To = endOfToday
    End Sub
DisplayAppointments
Nachdem die Suche abgeschlossen ist, wird die DisplayAppointments-Methode mit den Ergebnissen aufgerufen und die Termine werden erstmal sortiert.
Nach der Sortierung werden die Termine – unter Berücksichtigung Ihrer Position innerhalb des Tages – in der Oberfläche dargestellt.
    Private Sub DisplayAppointments(appointments As IEnumerable(Of IAppointment))
        appointments = appointments.OrderBy(Function(x) x.Start)
        For Each day In Days
            Dim startOfDaysDate = day.Date.StartOfDay()
            Dim endOfDaysDate = day.Date.EndOfDay()
            Dim appointmentsForThisDay = appointments.Where(Function(x) x.Start >= startOfDaysDate AndAlso x.Start <= endOfDaysDate)
            AddAllDayAppointments(appointmentsForThisDay, day)
        Next
    End Sub
Zwischenstand – VB.NET Kalender Steuerelement
Mit dem aktuellen Zwischenstand können wir Termine wie folgt darstellen:

Nicht vergessen!
Damit das Ganze funktioniert, darfst Du nicht vergessen, dem Kalender einen AppointmentService zu geben, sonst kann er keine Termine laden!
Das kannst Du zum Beispiel durch Dependency-Injection in der Form erledigen, oder manuell zuweisen:
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
    Calendar1.AppointmentService = New AppointmentService()
    Calendar1.RefreshAppointments()
End Sub
Ich habe mich bewusst für diese Lösung entschieden, da ich nicht wollte, dass der Designer rummeckert, weil Ihm der Service–Parameter beim erstellen des Kalenders im Konstruktor fehlt.
Deshalb muss man den Service dann nachher zuweisen..
Termine hinzufügen
Im folgenden Teil zeige ich Dir, wie ich das Hinzufügen von Terminen gedacht und umgesetzt habe.

Zwei Möglichkeiten
Ich habe mir dabei zwei Möglichkeiten ausgedacht, in unserem „VB.NET Kalender Steuerelement„-Tutorial Termine hinzuzufügen:
Über Plus-Knopf im Kalender
Die erste Möglichkeit wäre ein allgemeiner Plus–Knopf im Kalender, bzw. dessen Verarbeitung in der Form.
Wenn der Plus-Knopf im Kalender gedrückt wird, geben wir ein passendes Event aus und reagieren darauf in der Form.
    ' Calendar Code
    Private Sub btnAddAppointment_Click(sender As Object, e As EventArgs) Handles btnAddAppointment.Click
        OnAddingAppointment(EventArgs.Empty)
    End Sub
    Protected Sub OnAddingAppointment(e As EventArgs)
        RaiseEvent AddingAppointment(Me, e)
    End Sub
    Public Event AddingAppointment As EventHandler
' Form1
    Private Async Sub Calendar1_AddingAppointment(sender As Object, e As EventArgs) Handles Calendar1.AddingAppointment
        Dim appointment = New Appointment()
        With appointment
            .Start = Date.Now
            .End = Date.Now.AddHours(1)
        End With
        Using dlg = New dlgAppointment()
            Dim result = dlg.ShowDialog()
            If result = DialogResult.OK Then
                Await AppointmentService.CreateAsync(appointment)
                ' refresh whole calendar?
                ' refresh only the day?
                ' add the new appointment?
                Calendar1.AddAppointment(appointment)
            End If
        End Using
    End Sub
Doppelklick auf Tag
Die zweite Möglichkeit einen Termin hinzuzufügen wäre, einen Doppelklick auf einen Tag durchzuführen, so könnte man auch direkt den Tag für den Termin vereinfacht wählen.
Bei einem Doppelklick auf einen Tag, verfahren wir ähnlich wie hier drüber:
    ' Calendar Code
    Private Sub Me_DayDoubleClicked(sender As Object, e As DayEventArgs)
        ' or create custom args for passing more info..
        OnDayDoubleClicked(e)
    End Sub
    Protected Sub OnDayDoubleClicked(e As DayEventArgs)
        RaiseEvent DayDoubleClicked(Me, e)
    End Sub
    Public Event DayDoubleClicked As EventHandler(Of DayEventArgs)
    ' Form1 Code
    Private Async Sub Calendar1_DayDoubleClicked(sender As Object, e As DayEventArgs) Handles Calendar1.DayDoubleClicked
        Dim day = e.Day
        Dim appointment = New Appointment()
        With appointment
            .Start = New Date(day.Date.Year, day.Date.Month, day.Date.Day, 8, 0, 0)
            .End = .Start.AddHours(1)
        End With
        Using dlg = New dlgAppointment(appointment)
            Dim result = dlg.ShowDialog()
            If result = DialogResult.OK Then
                Await AppointmentService.CreateAsync(appointment)
                ' refresh whole calendar?
                ' refresh only the day?
                ' add the new appointment?
                Calendar1.AddAppointment(appointment)
            End If
        End Using
    End Sub
Termine bearbeiten – VB.NET Kalender Steuerelement
Im nächsten Schritt verfahren wir analog zum Termin-Hinzufügen wie oben.
Doppelklick auf Termin
Wenn ein Termin doppelt angeklickt wird, löst der Kalender auch hier ein passendes Event aus und wir reagieren in der Form darauf.
    ' Calendar Code
    Private Sub Me_EntryDoubleClicked(sender As Object, e As EntryEventArgs)
        ' or create custom args for passing more info..
        OnEntryDoubleClicked(e)
    End Sub
    Protected Sub OnEntryDoubleClicked(e As EntryEventArgs)
        RaiseEvent EntryDoubleClicked(Me, e)
    End Sub
    Public Event EntryDoubleClicked As EventHandler(Of EntryEventArgs)
    ' Form Code
    Private Async Sub Calendar1_EntryDoubleClicked(sender As Object, e As EntryEventArgs) Handles Calendar1.EntryDoubleClicked
        Dim entry = e.Entry
        Dim copy = CType(entry.Appointment.Clone(), Appointment)
        ' i prefer working with copies first..
        Using dlg = New dlgAppointment(copy)
            Dim result = dlg.ShowDialog()
            If result = DialogResult.OK Then
                Await AppointmentService.UpdateAsync(copy)
                entry.Appointment = copy
            End If
        End Using
    End Sub
Weitere Klassen
Hier sind noch weitere Klassen aus den Beispielen, ich empfehle allerdings dringend, lieber den Code zu laden, da es bei der Menge schwer ist alle Änderungen auch hier im Beitrag zu pflegen.
Public Class DayEventArgs
    Inherits EventArgs
    Public Property Day As Day
    Sub New()
    End Sub
    Sub New(day As Day)
        Me.day = day
    End Sub
End Class
Public Class EntryEventArgs
    Inherits EventArgs
    Public Property Entry As Entry
    Sub New()
    End Sub
    Sub New(entry As Entry)
        Me.Entry = entry
    End Sub
End Class
Termin löschen – VB.NET Kalender Steuerelement
Zu guter Letzt kommen wir zum Punkt, wie man einen Termin löschen kann.
Dafür klickt man einen Termin doppelt an, um Ihn zur Bearbeitung zu öffnen und bekommt dann den Lösch–Button angezeigt.
Im Code des Termin-Dialoges setzen wir das DialogResult (um nicht vom aktuellen Stand durch Events abzuweichen) einfach auf „Abort„, Welches wir dann in der Form ebenfalls abfragen können.
Ebenso fügen wir dem Kalender eine neue Methode hinzu.
    ' Form Code
    Private Async Sub Calendar1_EntryDoubleClicked(sender As Object, e As EntryEventArgs) Handles Calendar1.EntryDoubleClicked
        Dim entry = e.Entry
        Dim copy = CType(entry.Appointment.Clone(), Appointment)
        ' i prefer working with copies first..
        Using dlg = New dlgAppointment(copy)
            Dim result = dlg.ShowDialog()
            If result = DialogResult.OK Then
                Await AppointmentService.UpdateAsync(copy)
                entry.Appointment = copy
            ElseIf result = DialogResult.Abort Then
                Await AppointmentService.DeleteAsync(entry.Appointment)
                Calendar1.DeleteAppointment(entry.Appointment)
            End If
        End Using
    End Sub
    ' Calendar Code
    Public Sub DeleteAppointment(appointment As IAppointment)
        Dim targetDay = Days.SingleOrDefault(Function(x) appointment.Start >= x.Date.StartOfDay() AndAlso appointment.Start <= x.Date.EndOfDay())
        Dim entry = targetDay.Entries.SingleOrDefault(Function(x) x.Appointment.Id = appointment.Id)
        targetDay.RemoveEntry(entry)
    End Sub
Kompletter Code – VB.NET Kalender Steuerelement
In diesem Beitrag lasse ich die Darstellung des kompletten Code aus, da es so viel ist.
Ich denke es ist für Dich sinnvoller, einfach das Beispielprojekt herunterzuladen!
 
