My current job requires us to track our time spent on each project. It can get tricky to remember how much time you spent on each project when you are working on a handful at the same time. Back in 2018, I coded up a simple Time Tracker to make it easier to know what you worked on through out the day. My co-workers tested it and found various bugs and things, but overall it gets the job done.
The code wasn’t pretty. The GUI wasn’t pretty.
2018 - Time Keeper Not Pretty
Over the course of the next 5 years since then, I’ve learned more about C#, WPF, and coding practices in general. Currently, I’ve been going back through some of my old projects and cleaning them up.
I made some not-the-best code structure choices that I had to work around, but I managed to clean up a lot of the code, and also make the interface look more appealing and modern.
2023 - Time Keeper More Modern
One of the confusing things about WPF when you are first starting is the idea of property binding. It is hard to wrap your mind around your class properties getting magically updated from the controls directly. At the time in 2018, I still didn’t understand it, and I just used the GUI elements directly to get values like numbers out of a text box. During the 2023 refactor, I used Bindings and the INotifyPropertyChanged to decouple the View from the Model. In some places, I also used Dependency Properties.
2018 - TimeEdit.xaml.cs
private void secondsBox_LostFocus(object sender, RoutedEventArgs e)
{
ApplySeconds();
}
private void ApplySeconds()
{
if (secondsBox == null) return;
if (secondsBox.Text == "") return;
int s = Convert.ToInt32(secondsBox.Text);
SetSeconds(s);
}
private void SetSeconds(int s)
{
if (s > 59)
{
SetMinutes(minutes + (int)(s / 60));
s = s % 60;
}
seconds = s;
secondsBox.Text = seconds.ToString("D2");
}
2018 - TimeEdit.xaml
<TextBox x:Name="hoursBox" HorizontalAlignment="Left" Height="53" TextWrapping="Wrap" Text="999" VerticalAlignment="Top" Width="75" MaxLines="1" MaxLength="3" TabIndex="0" FontFamily="Segoe UI Black" FontSize="36" PreviewTextInput="numsOnly_PreviewTextInput" LostFocus="hoursBox_LostFocus" PreviewKeyDown="hoursBox_PreviewKeyDown"/>
<TextBox x:Name="minutesBox" HorizontalAlignment="Left" Height="53" TextWrapping="Wrap" Text="59" VerticalAlignment="Top" Width="52" MaxLines="1" MaxLength="2" TabIndex="0" FontFamily="Segoe UI Black" FontSize="36" Margin="75,0,0,0" PreviewTextInput="numsOnly_PreviewTextInput" LostFocus="minutesBox_LostFocus" PreviewKeyDown="minutesBox_PreviewKeyDown"/>
<TextBox x:Name="secondsBox" HorizontalAlignment="Left" Height="53" TextWrapping="Wrap" Text="59" VerticalAlignment="Top" Width="52" MaxLines="1" MaxLength="2" TabIndex="0" FontFamily="Segoe UI Black" FontSize="36" Margin="127,0,0,0" PreviewTextInput="numsOnly_PreviewTextInput" LostFocus="secondsBox_LostFocus" PreviewKeyDown="secondsBox_PreviewKeyDown"/>
Using binding and some other rework, makes things a lot more flexible
2023 - TimeEdit.xaml
<TextBox Template="{StaticResource HighlightTextBox}" Grid.Column="0" Text="{Binding Time.Hours, StringFormat=D2, FallbackValue=99}" MaxLength="3" TabIndex="0" PreviewKeyDown="TextBoxPreviewKeyDown" PreviewTextInput="NumbersOnlyPreviewTextInput" />
<TextBlock Grid.Column="1">:</TextBlock>
<TextBox Template="{StaticResource HighlightTextBox}" Grid.Column="2" Text="{Binding Time.Minutes, StringFormat=D2, FallbackValue=00}" MaxLength="2" TabIndex="1" PreviewKeyDown="TextBoxPreviewKeyDown" PreviewTextInput="NumbersOnlyPreviewTextInput"/>
<TextBlock Grid.Column="3">:</TextBlock>
<TextBox Template="{StaticResource HighlightTextBox}" Grid.Column="4" Text="{Binding Time.Seconds, StringFormat=D2, FallbackValue=00}" MaxLength="2" TabIndex="2" PreviewKeyDown="TextBoxPreviewKeyDown" PreviewTextInput="NumbersOnlyPreviewTextInput"/>
In 2018, I just styled all the elements directly. That made it difficult to make things look consistent and is hard to change. Also I just dragged and used margins to position the elements. During the 2023 refactor, I created styles that can be applied to the elements and I used grids to position the elements. Using a grid makes it much easier to position things around uniformly and change things later on.
2018 - TimerElement.xaml
<Button x:Name="workOnBtn" Content="WORK ON" HorizontalAlignment="Left" Margin="457,0,0,0" VerticalAlignment="Top" Width="85" FontFamily="Segoe UI Black" Background="#FF006A00" Foreground="White" Height="36" FontSize="14" Click="workOnBtn_Click"/>
<Button x:Name="removeBtn" Content="remove" HorizontalAlignment="Left" Margin="253,55,0,0" Width="54" FontFamily="Segoe UI Semibold" Background="#FF6A0000" Foreground="White" Height="23" VerticalAlignment="Top" VerticalContentAlignment="Top" Click="removeBtn_Click"/>
2023 - TimerElement.xaml
<Button Grid.Row="0" Grid.Column="3" Margin="4 0" Width="96" Height="32" HorizontalAlignment="Right" Content="Work On" Style="{StaticResource GreenButton}" Command="{Binding WorkOnCommand}"/>
<Button Grid.Row="0" Grid.Column="0" Margin="4 0" Content="X" Width="36" Height="36" Style="{StaticResource RedButton}" Command="{Binding RemoveCommand}"/>
In 2018, because I didn’t have an understanding of binding, or the concept of Model and View, I had a lot of business logic in the MainWindow.xaml.cs and other code behind areas. During the 2023 refactoring I split out as much as I could easily into a TimeCardController.cs.
2018 - MainWindow.xaml.cs
private void addBtn_Click(object sender, RoutedEventArgs e)
{
var telm = new TimerElement();
Timers.Add(telm);
telm.TimerActionPerformed += TimerActionCallback;
chargeNumberStack.Children.Add(telm);
}
private void pauseBtn_Click(object sender, RoutedEventArgs e)
{
WorkTimerPaused = true;
}
private void resetBtn_Click(object sender, RoutedEventArgs e)
{
chargedTimeClk.SetTime(new TimeSpan(0, 0, 0));
foreach (var telm in Timers)
{
telm.Clear();
}
}
During the refactor, I connected the buttons to commands that trigger the functions in the TimeCardController
2023 - TimeCardController.cs
public void PauseTimer()
{
IsWorkTimerRunning = false;
}
public void Reset()
{
_timeCard.Reset();
NotifyChange(nameof(TotalWorkTime));
}
public void AddNewChargeCode()
{
_ = _timeCard.AddNewChargeCode();
}
In 2018, I was all about getting to work functionally. I never thought that 5 years in the future I’d look at the code and be disgusted by it haha.
2018 - MainWindow.xaml.cs - logBtn_Click()
DateTime currTime = DateTime.Now;
string todaysColumn = currTime.ToShortDateString();
if (File.Exists(file))
{
//If I have the file already, we must add our time for today
CSCSV.Table lasttable = CSCSV.Table.LoadFromFile(file);
//If this table doesn't have a column for today, add one
if (!lasttable.ContainsHeader(todaysColumn))
{
lasttable.AddColumn(todaysColumn);
}
List<string> chargecodes = lasttable.GetColumn("Charge Codes").ToList();
int ccCount = chargecodes.Count();
foreach (string code in timeDict.Keys)
{
int idx = chargecodes.IndexOf(code);
//If the code is already in the list, set the time.
if (idx >= 0)
{
lasttable.SetValue(todaysColumn, idx, timeDict[code].GetTime().ToString());
}
else // We will have to add the code..
{
lasttable.RowCount += 1;
lasttable.SetValue("Charge Codes", ccCount, code);
lasttable.SetValue(todaysColumn, ccCount, timeDict[code].GetTime().ToString());
++ccCount;
}
}
In 2023, I made sure everything was using more descriptive names and consistent casing. 2023 - TimeCard.cs - WriteCSV()
DateTime currentTime = DateTime.Now;
string todayColumn = currentTime.ToShortDateString();
CSCSV.Table table = null;
if (File.Exists(path))
{
table = CSCSV.Table.ReadFromFile(path);
}
else
{
table = new CSCSV.Table();
}
if (!table.ContainsHeader("Charge Codes"))
{
table.AddColumn("Charge Codes");
}
//If this table doesn't have a column for today, add one
if (!table.ContainsHeader(todayColumn))
{
table.AddColumn(todayColumn);
}
List<string> chargeCodes = table.GetColumn("Charge Codes").ToList();
foreach (string code in chargeCodeTimerDict.Keys)
{
int chargeCodeIdx = chargeCodes.IndexOf(code);
//If the code is already in the list, set the time.
if (chargeCodeIdx >= 0)
{
table.SetValue(todayColumn, chargeCodeIdx, chargeCodeTimerDict[code].Time.ToTimeSpan().ToString());
}
else // We will have to add the code..
{
int rowIdx = table.AddRow();
table.SetValue("Charge Codes", rowIdx, code);
table.SetValue(todayColumn, rowIdx, chargeCodeTimerDict[code].Time.ToTimeSpan().ToString());
}
}
In 2018, I didn’t really organize the code in anyway. Everything was just in the main project folder.
2018 Folder Structure
2023 Folder Structure
It was a great exercise to go back through and look at what could have been done better with the lense of experience. Refactoring is an important part of programming. Language features have been changing at a rapid rate, and new technologies are released every day. What might have been several lines of code in the past, might now be simplified into one.