Why does my code work only once?

RogerSchlueter-7899 1,781 Reputation points
2026-08-22T02:35:16.3133333+00:00

My application includes a stopwatch plus a lap counter. The stopwatch is working fine using the following code:

Private Sub StartTimer(sender As Object, e As RoutedEventArgs) Handles btnStart.Click
	CurrentTime = TimeSpan.Zero
	LapTime = TimeSpan.Zero
	LapCount = 0
	tmr = New Timer(1000) With {.Enabled = True}
	AddHandler tmr.Elapsed, AddressOf UpdateClock
End Sub

Private Sub UpdateClock(source As Object, e As ElapsedEventArgs)
	Dispatcher.Invoke(Sub() PopulateTimer())
End Sub


Private Sub PopulateTimer()
	CurrentTime = CurrentTime.Add(OneSecond)
	txtDuration.Text = CurrentTime.ToString
End Sub

Saving laps uses this code:

Public Property Laps As List(Of Lap)

Private Sub CreateLap(sender As Object, e As RoutedEventArgs) Handles btnLap.Click
	Dim t As Task = Task.Run(
		Sub()
			SaveLap()
		End Sub)
End Sub

Private Sub SaveLap()
	LapTime = CurrentTime - LapTime
	LapCount += 1
	Laps.Add(New Lap With {.Count = LapCount, .Duration = LapTime})
	RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(Laps)))
	LapTime = CurrentTime
End Sub

The XAML for showing laps is as follows:

<DataGrid
	x:Name="dgLaps"
	AutoGenerateColumns="False"
	ItemsSource="{Binding Path=Laps}"
	Margin="0,0,10,0">
	<DataGrid.Columns>
		<DataGridTextColumn
			Binding="{Binding Path=Count}"
			Header="Lap"
			Width="30" />
		<DataGridTextColumn
			Binding="{Binding Path=Duration}"
			Header="Duration"
			Width="*" />
	</DataGrid.Columns>
</DataGrid>

This all works - for the first lap. After that, the SaveLap subroutine is processed but the additional laps are not displayed in the DataGrid. I have confirmed that Laps does contain all the added laps so that is not the problem. Why does the code only work once?

Developer technologies | VB

Answer accepted by question author
Tony Thach (WICLOUD CORPORATION) 560 Reputation points Microsoft External Staff Moderator
2026-08-24T06:48:41.01+00:00

Hi @RogerSchlueter-7899 ,

Thank you for providing a clear description and confirming that Laps contains all the added items. That detail was especially helpful because it shows that the lap-saving logic is running and that the problem is with how the bound collection is updated.

I recreated a minimal WPF project using your posted code as closely as possible, adding only the components required to build and run it. I was able to reproduce the issue: the first lap appeared correctly, but additional clicks of the Lap button did not update the UI:

User's image

After changing the collection to ObservableCollection(Of Lap) and ensuring that Laps.Add(...) runs on the UI thread, all laps appeared correctly in the DataGrid.

Result after the fix:

User's image

I have also prepared two minimal project files: MainWindow.xaml.txt , MainWindow.xaml.vb.txt

The posted code has two separate issues.

1. List(Of Lap) does not notify the DataGrid

Your property is currently declared as:

Public Property Laps As List(Of Lap)

List(Of T) does not send collection-change notifications when Add() or Remove() is called.

This line:

RaiseEvent PropertyChanged(
    Me,
    New PropertyChangedEventArgs(NameOf(Laps)))

indicates that the Laps property itself has changed. However, the code does not assign a new list to Laps. It only modifies the contents of the existing list:

Laps.Add(...)

For this scenario, use ObservableCollection(Of Lap), which notifies WPF whenever an item is added, removed, or the collection is cleared.

2. Task.Run modifies the bound collection from a worker thread

The following code runs SaveLap() on a thread-pool thread:

Task.Run(
    Sub()
        SaveLap()
    End Sub)

This means that both Laps.Add(...) and PropertyChanged are executed outside the WPF UI thread.

The exception you received after switching to ObservableCollection confirms that, at the time of the exception, the collection was still being changed from a thread other than its Dispatcher thread:

This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.

Dispatching only PropertyChanged is not sufficient. The collection modification itself, specifically Laps.Add(...), must run on the UI thread.

Because SaveLap() performs only a small amount of work and the button click already runs on the UI thread, Task.Run is unnecessary here.

The complete implementation is included in the attached project files. The essential changes are shown below.

Public ReadOnly Property Laps As New ObservableCollection(Of Lap)()
Private Sub CreateLap(
    sender As Object,
    e As RoutedEventArgs
) Handles btnLap.Click

    SaveLap()
End Sub
Private Sub SaveLap()
    Dim duration As TimeSpan =
        CurrentTime - PreviousLapTime

    LapCount += 1

    Laps.Add(
        New Lap With {
            .Count = LapCount,
            .Duration = duration
        })

    PreviousLapTime = CurrentTime
End Sub

Your existing DataGrid binding can remain essentially unchanged.

There is no need to raise PropertyChanged(NameOf(Laps)) after each Add(). ObservableCollection sends the required collection notification automatically.

The Duration calculation above preserves the behavior of your original code:

  • The first lap is the elapsed time from Start to the first Lap click.
  • Each subsequent lap is the time since the previous Lap click.

If pressing Start should also remove the laps from the previous session, add:

Laps.Clear()

The corrected minimal project successfully displays every lap. Although the provided code does not include enough lifecycle and binding context to determine exactly why only the first lap appeared in your original application, the two problems visible in the posted code are resolved by:

  1. Replacing List(Of Lap) with ObservableCollection(Of Lap).
  2. Removing Task.Run and modifying the collection on the UI thread.

If the exception remains after applying this version, please share only the current Laps declaration, constructor or DataContext setup, CreateLap(), and SaveLap(). That should be sufficient to identify any remaining background code path. Please remove any private or business-sensitive information before posting.

If this instruction is applicable to your situation, I would greatly appreciate it if you could follow the instruction here so others experiencing similar behavior can benefit from it as well.  

Was this answer helpful?

3 people found this answer helpful.

2 additional answers

Sort by: Most helpful
  1. Tony Thach (WICLOUD CORPORATION) 560 Reputation points Microsoft External Staff Moderator
    2026-08-24T02:14:03.09+00:00

    We are actively investigating this issue and will share updates as soon as they become available. Thank you for your patience and understanding.

    Was this answer helpful?

    0 comments No comments

  2. Bruce (SqlWork.com) 85,111 Reputation points
    2026-08-22T17:20:16.14+00:00

    Just like in UpdateClock, you need to use Dispatcher.Invoke() to call RaiseEvent PropertyChanged so it runs on the ui thread.

    Was this answer helpful?


Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.