Readme eingebaut und Projekt daran ausgerichtet

This commit is contained in:
mdn 2015-12-07 23:20:32 +01:00
parent 2abef7a2a4
commit f1e0bf104f
11 changed files with 286 additions and 14 deletions

47
TimeScheduler/Readme.txt Normal file
View file

@ -0,0 +1,47 @@
Beispielprojekt für die Anwendung von WPF.
Anforderung:
Mit der Applikation soll die Arbeitszeit automatisiert erfast und permanent gespeichert werden.
Dafür wird pro Zeiteinheit ein Element angelegt, das folgenden Eigenschaften besitzt:
- Beschreibung des Elementes
- Von-Zeitpunkt (1/4h genau)
- Bis-Zeitpunkt (1/4h genau)
- Kostenstelle
- Typ (Normal, Pause, Urlaub, Krank)
Die Darstellung des Elementes soll vom Typ abhängig sein, z.B. Hintergrundfarbe ist normal blau und bei Pause gelb.
Es sollen nicht immer alle Elemente dargestellt werden, sondern nur die das selektieren Tages.
Die aktuelle Arbeitszeit, soll graphisch markiert sein, und deren Endezeit soll automatisch angepasst werden.
Beim Sperren/Ruhezustand soll autmoatisch die aktuelle Arbeitszeit abgeschlossen werden und eine neue gestartet
werden, aber nur wenn die aktuelle Arbeitszeit > 15min ist.
Über ein Menü (z.B. Kontextmenü) erstellt man eine neue aktuelle oder löscht eine vorhandene. Zur Einfachheit
darf man die aktuelle Arbeitszeit nicht löschen.
Desweitern soll man die selektierte Arbeitszeit mit der nächsten Arbeitszeit verschmelzen können, wobei nur die
Endezeit auf das selektierte Objekt übernommen wird.
Um einen Überblick über die Daten zu bekommen, soll es eine Diagnoseoberfläche geben, in der man
Beim Start wird die zuletzt gespeicherten Daten geladen und die letzte Arbeitszeit des aktuellen Tages wird als
aktuelle Arbeitszeit hergenommen. Bei Änderungen an den Arbeitszeiten, sollen diese direkt permanent gespeichert
werden, spätestens beim beenden der Anwendung.
Das speichern sollte sehr robust werden, damit die Datei nicht zerstört werden, wenn während des speichern die
Anwendung abstürzt.
Zur Implementierung soll WPF mit dem MVVM-Pattern benutzt werden
Erweiterte Funktionaltiät:
- Bei Sperrungen/Ruhezustände über 8h soll das Element nicht als Arbeitzeit gelten sondern entfernt werden
- Es erscheint eine Nachfrage nach einer Entsperrung/Aufwachen (aus Ruhezustand) wie die Zeit seit der
letzten Sperrung/Ruhezustand eingetragen werden soll
- Kostenstellen-Editor einbauen
- Die Kostenstellen in Task aufteilen
Beschreibung der Struktur:
- Oberflächen sind direkt in der Root
- ViewModel: die ViewModels für die Oberflächen
- Resource: Resourcen die benötigt werden
- Model: Klassen, die die Datenbestände beinhalten
- Domain: Schnittstellen und Implementierungen für die Datenbeschaffung
- Common: Allgmeine Klassen die in jeder WPF-Anwendung benötigt werden

View file

@ -7,6 +7,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TimeScheduler", "TimeSchedu
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TimeScheduler.Test", "TimeScheduler.Test\TimeScheduler.Test.csproj", "{8B47037C-584E-41FB-B8B8-14FAD26D7BCA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{57AA67D7-E7F9-4D31-BAD8-242100BE21C8}"
ProjectSection(SolutionItems) = preProject
Readme.txt = Readme.txt
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU

View file

@ -0,0 +1,40 @@
<Window x:Class="TimeScheduler.Diagnose"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TimeScheduler"
xmlns:vm="clr-namespace:TimeScheduler.ViewModel"
mc:Ignorable="d"
Title="Diagnose" Height="400" Width="600">
<Window.DataContext>
<vm:DiagnoseViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock Margin="5" VerticalAlignment="Center" Text="Monat:"/>
<ComboBox Margin="5" ItemsSource="{Binding Months}" SelectedItem="{Binding SelectedMonth}" ItemStringFormat="{}{0:yyyy/mm}" MinWidth="120"/>
</StackPanel>
<ListView Grid.Row="1" Margin="3" ItemsSource="{Binding Items}">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn DisplayMemberBinding="{Binding Key}" Header="Schlüssel"/>
<GridViewColumn DisplayMemberBinding="{Binding Description}" Header="Beschreibung"/>
<GridViewColumn DisplayMemberBinding="{Binding From}" Header="Von"/>
<GridViewColumn DisplayMemberBinding="{Binding Till}" Header="Bis"/>
<GridViewColumn DisplayMemberBinding="{Binding Duration}" Header="Dauer"/>
<GridViewColumn DisplayMemberBinding="{Binding CostUnit}" Header="Kostenstelle"/>
<GridViewColumn DisplayMemberBinding="{Binding Type}" Header="Typ"/>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>

View file

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace TimeScheduler
{
/// <summary>
/// Interaktionslogik für Diagnose.xaml
/// </summary>
public partial class Diagnose : Window
{
public Diagnose()
{
InitializeComponent();
}
}
}

View file

@ -7,6 +7,9 @@ namespace TimeScheduler.Domain
/// <summary>Schnittstelle zum Provider der die Daten ermitteln</summary>
public interface IDomain
{
/// <summary>Ermittelt die Monate, für denen Daten vorliegen</summary>
/// <returns>Liste der Monate, in der Daten vorliegen</returns>
IEnumerable<DateTime> GetMonths();
/// <summary>Ermittelt die Werte für den angegebenen Tag</summary>
/// <param name="date">Der Tag für den Werte gesucht werden sollen</param>
/// <returns>Die Daten, für den angefragten Tag</returns>

View file

@ -81,6 +81,12 @@ namespace TimeScheduler.Domain.Impl
#endregion
#region IDomain
IEnumerable<DateTime> IDomain.GetMonths()
{
return allItems_
.GroupBy(x => x.From.Year * 100 + x.From.Month)
.Select(x => new DateTime(x.Key / 100, x.Key % 100, 1));
}
IEnumerable<ITimeItem> IDomain.GetItems(DateTime date)
{
return allItems_

View file

@ -64,6 +64,10 @@ namespace TimeScheduler.Domain.Impl
}
#endregion
#region Weitere Eigenschaften
public TimeSpan Duration { get { return till_ - from_; } }
#endregion
#region ITimeItem (Eigenschaften nicht implizit implementieren, sonst funktioniert das Binden nicht)
private int key_;
/// <inheritdoc/>
@ -81,8 +85,11 @@ namespace TimeScheduler.Domain.Impl
set
{
if (SetField(ref from_, value.Subtract(TimeSpan.FromMinutes(value.Minute % 15))))
{
if (From > Till)
Till = From;
OnPropertyChanged("Duration");
}
}
}
@ -94,8 +101,11 @@ namespace TimeScheduler.Domain.Impl
set
{
if (SetField(ref till_, value.Subtract(TimeSpan.FromMinutes(value.Minute % 15))))
{
if (Till < From)
From = Till;
OnPropertyChanged("Duration");
}
}
}

View file

@ -15,6 +15,13 @@
<Window.Resources>
<c:BindingProxy x:Key="proxy" Data="{Binding}"/>
</Window.Resources>
<Window.InputBindings>
<KeyBinding Key="S" Modifiers="Ctrl" Command="{Binding SaveCommand}"/>
<KeyBinding Key="N" Modifiers="Ctrl" Command="{Binding AddNewCommand}"/>
<KeyBinding Key="Delete" Command="{Binding DeleteCommand}"/>
<KeyBinding Key="M" Modifiers="Alt" Command="{Binding MergeCommand}"/>
<KeyBinding Key="D" Modifiers="Alt" Command="{Binding DiagnoseCommand}"/>
</Window.InputBindings>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
@ -26,13 +33,24 @@
</Grid.RowDefinitions>
<DockPanel Grid.Column="0">
<DatePicker DockPanel.Dock="Top" Margin="5" SelectedDate="{Binding Path=SelectedDate}"/>
<Grid DockPanel.Dock="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30"/>
<ColumnDefinition/>
<ColumnDefinition Width="30"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Margin="3" Content="&lt;" Command="{Binding NextDateCommand}"/>
<DatePicker Grid.Column="1" Margin="5" SelectedDate="{Binding Path=SelectedDate}"/>
<Button Grid.Column="2" Margin="3" Content="&gt;" Command="{Binding PreviousDateCommand}"/>
</Grid>
<ListBox x:Name="lbTimeElements" Margin="5" HorizontalContentAlignment="Stretch"
ItemsSource="{Binding TimeItems}" SelectedItem="{Binding SelectedTimeItem}">
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Command="{Binding AddNewCommand}" Header="Neues Element"/>
<MenuItem Command="{Binding DeleteCommand}" Header="Lösche selektiertes Element"/>
<MenuItem Command="{Binding MergeCommand}" Header="Zusammenführen" ToolTip="Mit dem nachfolgenden Element zusammenführen"/>
<MenuItem Command="{Binding DiagnoseCommand}" Header="Diagnose"/>
</ContextMenu>
</ListBox.ContextMenu>
<ListBox.ItemContainerStyle>

View file

@ -59,13 +59,21 @@
</ApplicationDefinition>
<Compile Include="Common\BindingProxy.cs" />
<Compile Include="Common\MathUtil.cs" />
<Compile Include="Diagnose.xaml.cs">
<DependentUpon>Diagnose.xaml</DependentUpon>
</Compile>
<Compile Include="Domain\IDomain.cs" />
<Compile Include="Domain\Impl\FileDomain.cs" />
<Compile Include="Domain\Impl\TimeItem.cs" />
<Compile Include="Domain\Impl\SimpleTimes.cs" />
<Compile Include="Domain\TimeItemType.cs" />
<Compile Include="Model\ITimeItem.cs" />
<Compile Include="ViewModel\DiagnoseViewModel.cs" />
<Compile Include="ViewModel\MainViewModel.cs" />
<Page Include="Diagnose.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>

View file

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using TimeScheduler.Common;
using TimeScheduler.Domain;
using TimeScheduler.Model;
namespace TimeScheduler.ViewModel
{
/// <summary>ViewModel für die Diangoseoberfläche</summary>
class DiagnoseViewModel : NotifyableObject
{
#region Konstruktion
public DiagnoseViewModel() : this(new Domain.Impl.FileDomain()) { Refresh(); }
/// <summary>Konstruktion</summary>
public DiagnoseViewModel(IDomain provider)
{
Provider = provider;
CreateCommands();
}
/// <summary>Datenbeschaffer</summary>
protected IDomain Provider { get; private set; }
#endregion
#region Eigenschaften
private IList<DateTime> months_;
/// <summary>Auflistung der Monate mit Datenbestände</summary>
public IList<DateTime> Months { get { return months_; } set { if (SetField(ref months_, value) && months_ != null && months_.Count > 0) SelectedMonth = months_.First(); } }
private DateTime selectedMonth_;
/// <summary>Aktuell selektierter Monat</summary>
public DateTime SelectedMonth { get { return selectedMonth_; } set { if (SetField(ref selectedMonth_, value)) Refresh(selectedMonth_); } }
private IList<ITimeItem> items_;
public IList<ITimeItem> Items { get { return items_; } set { SetField(ref items_, value); } }
#endregion
#region Kommandos
private void CreateCommands()
{
}
private void Refresh() { Months = Provider.GetMonths().ToList(); }
private void Refresh(DateTime month)
{
// Start und Ende festlegen
var start = month;
var end = month.AddMonths(1);
// Schleife und zwischenspeicherung aufbauen
var items = new List<ITimeItem>();
while (start < end)
{
// Einzelne Tage abrufen und speichern
items.AddRange(Provider.GetItems(start));
start = start.AddDays(1);
}
// Und der Oberfläche bekannt geben
Items = items;
}
#endregion
}
}

View file

@ -12,8 +12,6 @@ namespace TimeScheduler.ViewModel
class MainViewModel : NotifyableObject
{
#region Konstruktion
/// <summary>aktuelles Ele</summary>
private ITimeItem lastTimeItem_;
/// <summary>Zyklische Aktuallisierung</summary>
private System.Timers.Timer timer_;
/// <summary>Funktion zum ermitteln der aktuellen Uhrzeit</summary>
@ -85,7 +83,6 @@ namespace TimeScheduler.ViewModel
}
private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { UpdateLastItem(); }
/// <summary>Datenbeschaffer</summary>
protected IDomain Provider { get; private set; }
#endregion
@ -118,6 +115,19 @@ namespace TimeScheduler.ViewModel
private ITimeItem selectedTimeItem_;
/// <summary>Das aktuell selektierte Element, von dem weitere Details angezeigt werden sollen</summary>
public ITimeItem SelectedTimeItem { get { return selectedTimeItem_; } set { SetField(ref selectedTimeItem_, value); } }
private ITimeItem currentTimeItem_;
/// <summary>Aktuelle Arbeitszeit</summary>
public ITimeItem CurrentTimeItem
{
get { return currentTimeItem_; }
set
{
currentTimeItem_.IsCurrentItem = false;
currentTimeItem_ = value;
currentTimeItem_.IsCurrentItem = true;
}
}
#endregion
#region Kommandos
@ -126,7 +136,12 @@ namespace TimeScheduler.ViewModel
RefreshCommand = new RelayCommand(Refresh);
AddNewCommand = new RelayCommand(AddNew);
DeleteCommand = new RelayCommand(Delete, CanDelete);
MergeCommand = new RelayCommand(Merge, CanMerge);
DiagnoseCommand = new RelayCommand(Diagnose);
SaveCommand = new RelayCommand(Save);
NextDateCommand = new RelayCommand(NextDate);
PreviousDateCommand = new RelayCommand(PreviousDate);
}
public ICommand RefreshCommand { get; private set; }
@ -152,8 +167,8 @@ namespace TimeScheduler.ViewModel
TimeItems.Add(lastItem);
}
// und abspeichern
lastTimeItem_ = lastItem;
lastTimeItem_.IsCurrentItem = true;
currentTimeItem_ = lastItem;
currentTimeItem_.IsCurrentItem = true;
// Und direkt den aktuellen Wert speichern
UpdateLastItem();
}
@ -196,10 +211,36 @@ namespace TimeScheduler.ViewModel
public ICommand DeleteCommand { get; private set; }
private void Delete() { TimeItems.Remove(SelectedTimeItem); }
public bool CanDelete() { return SelectedTimeItem != null; }
public bool CanDelete() { return SelectedTimeItem != null && !SelectedTimeItem.IsCurrentItem; }
public ICommand MergeCommand { get; private set; }
private void Merge()
{
var elem = TimeItems.Where(x => x.From > SelectedTimeItem.From).OrderBy(x => x.From).FirstOrDefault();
if(elem != null)
{
// Zeit einverleiben
SelectedTimeItem.Till = elem.Till;
// Wenn das zu verschmelzende Elemente, die aktuelle Arbeitszeit ist, dann ersetzen
if (elem.IsCurrentItem)
CurrentTimeItem = SelectedTimeItem;
// Und nun noch das zu verschmelzende Element entfernen
TimeItems.Remove(elem);
}
}
public bool CanMerge() { return SelectedTimeItem != null; }
public ICommand DiagnoseCommand { get; private set; }
private void Diagnose() { var diag = new Diagnose(); diag.ShowDialog(); }
public ICommand SaveCommand { get; private set; }
private void Save() { UpdateLastItem(); }
public ICommand NextDateCommand { get; private set; }
private void NextDate() { SelectedDate = SelectedDate + TimeSpan.FromDays(1); }
public ICommand PreviousDateCommand { get; private set; }
private void PreviousDate() { SelectedDate = SelectedDate + TimeSpan.FromDays(-1); }
#endregion
#region Hilfsfunktionen
@ -210,24 +251,22 @@ namespace TimeScheduler.ViewModel
UpdateLastItem();
// Wenn ein letztes Element existiert und dies eine Dauer >15min besitzt, dann erst ein neues erzeugen
if (lastTimeItem_ != null && (lastTimeItem_.Till - lastTimeItem_.From) > TimeSpan.FromMinutes(15))
if (currentTimeItem_ != null && (currentTimeItem_.Till - currentTimeItem_.From) > TimeSpan.FromMinutes(15))
{
// neues Element erzeugen und belegen
var newItem = Provider.NewItem();
newItem.From = lastTimeItem_.Till;
newItem.IsCurrentItem = true;
newItem.From = currentTimeItem_.Till;
// Aufnehmen und altes LastItem durch neues ersetzen
TimeItems.Add(newItem);
lastTimeItem_.IsCurrentItem = false;
lastTimeItem_ = newItem;
CurrentTimeItem = newItem;
}
}
/// <summary>Aktualisiert das "Letzte Element"</summary>
private void UpdateLastItem()
{
if (lastTimeItem_ != null)
lastTimeItem_.Till = currentTime_();
if (currentTimeItem_ != null)
currentTimeItem_.Till = currentTime_();
}
#endregion