C# SynchronizationContext Usage
Timers are a key element in determining the observation cycle in programs requiring periodic monitoring. Unlike Windows services, monitoring programs typically have a screen (GUI). This article will explain why SynchronizationContext is necessary while implementing and advancing an app with a timer and a screen using C# WPF technology (.NET 9).
The app we’ll be building in this article has the following features: If you set a random period (Period) for a timer, a timer event will occur at that period, and each time an event occurs, the time it occurs (displayed to 0.1 second units) and the number of times the event occurs (Count) will be displayed on the screen. Additionally, the quotient (Ellapsed) is calculated by dividing the difference between the set period time and the time the timer event occurs by the period and displaying it on the screen. If the two values (Count, Ellapsed) differ, the reset button will be enabled; if they are the same, the reset button will be disabled. Assuming that a timer event hasn’t occurred at the specified period, the number of occurrences (Count) will be less than the quotient (Ellapsed). Pressing the reset button will reset the number of occurrences (Count) and the quotient (Ellapsed) to 0, restarting the timer.
The development tool is Microsoft Visual Studio Community 2022 (64-bit) Version 17.14.16.
Using DispatcherTimer and the Visual Studio Template Pattern
Create a WPF project using the development tool. This project is called [Visual Studio Template Pattern].
Modify MainWindow.xaml and MainWindow.xaml.cs as shown below.
<Window x:Class="Clock.UiTimer.VstPattern.MainWindow"
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:Clock.UiTimer.VstPattern"
mc:Ignorable="d"
Title="MainWindow" Height="160" Width="480" FontSize="14">
<Grid>
<DockPanel HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Background="{x:Static SystemColors.ControlBrush}">
<Label DockPanel.Dock="Left"/>
<TextBlock x:Name="TextBlockMessage" DockPanel.Dock="Left"/>
</DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label Grid.Column="0" Grid.RowSpan="2"/>
<Label Grid.Column="4" Grid.RowSpan="2"/>
<TextBlock Grid.Column="1" Grid.Row="0" Text="" TextAlignment="Center"/>
<TextBlock Grid.Column="2" Grid.Row="0" Text="Ellapsed" TextAlignment="Center"/>
<TextBlock Grid.Column="3" Grid.Row="0" Text="Count" TextAlignment="Center"/>
<TextBlock Grid.Column="1" Grid.Row="1" x:Name="TextBlockTime" TextAlignment="Center" VerticalAlignment="Center" FontSize="48"/>
<TextBlock Grid.Column="2" Grid.Row="1" x:Name="TextBlockEllapsedTime" TextAlignment="Center" VerticalAlignment="Center" FontSize="48"/>
<TextBlock Grid.Column="3" Grid.Row="1" x:Name="TextBlockCount" TextAlignment="Center" VerticalAlignment="Center" FontSize="48"/>
</Grid>
<DockPanel LastChildFill="False" HorizontalAlignment="Stretch" VerticalAlignment="Top">
<Label DockPanel.Dock="Left"/>
<Button x:Name="ButtonReset" Content="Reset" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<TextBox x:Name="TextBoxPeriod" TextAlignment="Right"/>
<TextBlock Text="msec" TextAlignment="Left"/>
</DockPanel>
</Grid>
</Window>using System.Windows;
using System.Windows.Threading;
namespace Clock.UiTimer.VstPattern
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly DispatcherTimer _dispatcherTimer;
private DateTimeOffset _resetTime;
private int _period;
public MainWindow()
{
InitializeComponent();
Title = TITLE;
ButtonReset.IsEnabled = false;
ButtonReset.Click += ButtonReset_Click;
TextBoxPeriod.TextChanged += TextBoxPeriod_TextChanged;
Loaded += MainWindow_Loaded;
Closing += MainWindow_Closing;
_dispatcherTimer = new DispatcherTimer();
_dispatcherTimer.Tick += DispatcherTimer_Tick;
TextBoxPeriod.Text = DEFAULT_PERIOD;
}
private void TextBoxPeriod_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
try
{
var period = Convert.ToInt32(TextBoxPeriod.Text);
if (period > 0)
{
_period = period;
Reset();
}
}
catch (FormatException)
{
}
}
private void ButtonReset_Click(object sender, RoutedEventArgs e)
{
Reset();
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Loaded -= MainWindow_Loaded;
Reset();
}
private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{
Closing -= MainWindow_Closing;
_dispatcherTimer.Stop();
TextBoxPeriod.TextChanged -= TextBoxPeriod_TextChanged;
ButtonReset.Click -= ButtonReset_Click;
_dispatcherTimer.Tick -= DispatcherTimer_Tick;
}
private void DispatcherTimer_Tick(object? sender, EventArgs e)
{
Time = DateTimeOffset.Now;
Count++;
Ellapsed = (Time.ToUnixTimeMilliseconds() - _resetTime.ToUnixTimeMilliseconds()) / _period;
var notEqual = (Count != Ellapsed);
ButtonReset.IsEnabled = notEqual;
if (string.IsNullOrEmpty(TextBlockMessage.Text))
{
if (notEqual)
{
TextBlockMessage.Text = string.Format(COUNT_IS_NOT_EQUAL_ELLAPSED, Count, Ellapsed, Time.ToString(TIME_FORMAT));
}
}
else
{
if (!notEqual)
{
TextBlockMessage.Text = string.Empty;
}
}
}
public DateTimeOffset Time
{
get => _time;
set
{
if (value != _time)
{
_time = value;
TextBlockTime.Text = value.ToString(TIME_FORMAT);
}
}
}
private DateTimeOffset _time;
public long Count
{
get => _count;
set
{
if (_count != value)
{
_count = value;
TextBlockCount.Text = value.ToString();
}
}
}
private long _count = -1;
public long Ellapsed
{
get => _ellapsedSeconds;
set
{
if (_ellapsedSeconds != value)
{
_ellapsedSeconds = value;
TextBlockEllapsedTime.Text = value.ToString();
}
}
}
private long _ellapsedSeconds = -1;
private void Reset()
{
_dispatcherTimer.Stop();
ButtonReset.IsEnabled = false;
TextBlockMessage.Text = string.Empty;
Count = 0;
Ellapsed = 0;
Time = DateTimeOffset.Now;
_resetTime = Time;
_dispatcherTimer.Interval = TimeSpan.FromMilliseconds(_period);
_dispatcherTimer.Start();
}
private readonly static string TITLE = "Clock - DispatcherTimer - VisualStudio Template Pattern";
private readonly static string DEFAULT_PERIOD = "1000";
private readonly static string TIME_FORMAT = "HH:mm:ss.f";
private readonly static string COUNT_IS_NOT_EQUAL_ELLAPSED = "Count({0}) is not equal to ellapsed({1}) at {2}.";
}
}To reuse the UI in future projects, all UI events were added/removed from MainWindow.xaml.cs.
[Start without debugging]. Change the period between 1000 msec and 1 msec. You’ll notice that the reset button activates more quickly as the period decreases. Because DispatcherTimer runs on the UI thread, the number of occurrences (Count) is less than the quotient (Ellapsed). This decrease in timer events can be a bug in some programs.
Using Threading.Timer and the Visual Studio Template Pattern
Get Lee, Cheul Woo’s stories in your inbox
Join Medium for free to get updates from this writer.
Create a WPF project using the development tool. Modify MainWindow.xaml and MainWindow.xaml.cs as shown below. The contents of MainWindow.xaml were copied from the project.
<Window x:Class="Clock.ThreadTimer.VstPattern.MainWindow"
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:Clock.ThreadTimer.VstPattern"
mc:Ignorable="d"
Title="MainWindow" Height="160" Width="480" FontSize="14">
<Grid>
<DockPanel HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Background="{x:Static SystemColors.ControlBrush}">
<Label DockPanel.Dock="Left"/>
<TextBlock x:Name="TextBlockMessage" DockPanel.Dock="Left"/>
</DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label Grid.Column="0" Grid.RowSpan="2"/>
<Label Grid.Column="4" Grid.RowSpan="2"/>
<TextBlock Grid.Column="1" Grid.Row="0" Text="" TextAlignment="Center"/>
<TextBlock Grid.Column="2" Grid.Row="0" Text="Ellapsed" TextAlignment="Center"/>
<TextBlock Grid.Column="3" Grid.Row="0" Text="Count" TextAlignment="Center"/>
<TextBlock Grid.Column="1" Grid.Row="1" x:Name="TextBlockTime" TextAlignment="Center" VerticalAlignment="Center" FontSize="48"/>
<TextBlock Grid.Column="2" Grid.Row="1" x:Name="TextBlockEllapsedTime" TextAlignment="Center" VerticalAlignment="Center" FontSize="48"/>
<TextBlock Grid.Column="3" Grid.Row="1" x:Name="TextBlockCount" TextAlignment="Center" VerticalAlignment="Center" FontSize="48"/>
</Grid>
<DockPanel LastChildFill="False" HorizontalAlignment="Stretch" VerticalAlignment="Top">
<Label DockPanel.Dock="Left"/>
<Button x:Name="ButtonReset" Content="Reset" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<TextBox x:Name="TextBoxPeriod" TextAlignment="Right"/>
<TextBlock Text="msec" TextAlignment="Left"/>
</DockPanel>
</Grid>
</Window>using System.Windows;
using System.Windows.Threading;
namespace Clock.ThreadTimer.VstPattern
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly Timer _threadingTimer;
private DateTimeOffset _resetTime;
private int _period;
public MainWindow()
{
InitializeComponent();
Title = TITLE;
ButtonReset.IsEnabled = false;
ButtonReset.Click += ButtonReset_Click;
TextBoxPeriod.TextChanged += TextBoxPeriod_TextChanged;
Loaded += MainWindow_Loaded;
Closing += MainWindow_Closing;
_threadingTimer = new Timer(Callback, null, Timeout.Infinite, Timeout.Infinite);
TextBoxPeriod.Text = DEFAULT_PERIOD;
}
private void TextBoxPeriod_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
try
{
var period = Convert.ToInt32(TextBoxPeriod.Text);
if (period > 0)
{
_period = period;
Reset();
}
}
catch (FormatException)
{
}
}
private void ButtonReset_Click(object sender, RoutedEventArgs e)
{
Reset();
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Loaded -= MainWindow_Loaded;
Reset();
}
private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{
Closing -= MainWindow_Closing;
_threadingTimer.Dispose();
TextBoxPeriod.TextChanged -= TextBoxPeriod_TextChanged;
ButtonReset.Click -= ButtonReset_Click;
}
private void Callback(object? state)
{
Dispatcher.BeginInvoke(() =>
{
Time = DateTimeOffset.Now;
Count++;
Ellapsed = (Time.ToUnixTimeMilliseconds() - _resetTime.ToUnixTimeMilliseconds()) / _period;
var notEqual = (Count != Ellapsed);
ButtonReset.IsEnabled = notEqual;
if (string.IsNullOrEmpty(TextBlockMessage.Text))
{
if (notEqual)
{
TextBlockMessage.Text = string.Format(COUNT_IS_NOT_EQUAL_ELLAPSED, Count, Ellapsed, Time.ToString(TIME_FORMAT));
}
}
else
{
if (!notEqual)
{
TextBlockMessage.Text = string.Empty;
}
}
});
}
public DateTimeOffset Time
{
get => _time;
set
{
if (value != _time)
{
_time = value;
TextBlockTime.Text = value.ToString(TIME_FORMAT);
}
}
}
private DateTimeOffset _time;
public long Count
{
get => _count;
set
{
if (_count != value)
{
_count = value;
TextBlockCount.Text = value.ToString();
}
}
}
private long _count = -1;
public long Ellapsed
{
get => _ellapsed;
set
{
if (_ellapsed != value)
{
_ellapsed = value;
TextBlockEllapsedTime.Text = value.ToString();
}
}
}
private long _ellapsed = -1;
private void Reset()
{
ButtonReset.IsEnabled = false;
TextBlockMessage.Text = string.Empty;
Count = 0;
Ellapsed = 0;
Time = DateTimeOffset.Now;
_resetTime = Time;
_threadingTimer.Change(_period, _period);
}
private readonly static string TITLE = "Clock - DispatcherTimer - VisualStudio Template Pattern";
private readonly static string DEFAULT_PERIOD = "1000";
private readonly static string TIME_FORMAT = "HH:mm:ss.f";
private readonly static string COUNT_IS_NOT_EQUAL_ELLAPSED = "Count({0}) is not equal to ellapsed({1}) at {2}.";
}
}The key point to note is the Callback(object? state) method. Since the callback runs on a different thread than the UI thread, the callback needs the MainWindow Dispatcher to resolve thread ownership issues that arise when updating the UI (visual display). [Start without debugging]. Change the period between 1000 msec and 1 msec. You’ll notice that the decrease frequency of timer events has decreased compared to the previous project.
Using Threading.Timer and the MVVM Pattern
Create a WPF project with the development tools and install CommunityToolkit.Mvvm Version 8.4.0.
Modify App.xaml and App.xaml.cs as shown below. Also, remove [StartupUri=”MainWindow.xaml”] from the Application tag.
<Application x:Class="Clock.ThreadTimer.Mvvm.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Clock.ThreadTimer.Mvvm">
<Application.Resources>
</Application.Resources>
</Application>using System.Windows;
namespace Clock.ThreadTimer.Mvvm
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
private ViewModel? _viewModel;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
_viewModel = new ViewModel(SynchronizationContext.Current!);
MainWindow = new MainWindow
{
DataContext = _viewModel
};
MainWindow.Loaded += MainWindow_Loaded;
MainWindow.Closing += MainWindow_Closing;
MainWindow.Show();
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
MainWindow.Loaded -= MainWindow_Loaded;
_viewModel!.Run();
_viewModel.Message = nameof(MainWindow_Loaded);
}
private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{
MainWindow.Closing -= MainWindow_Closing;
_viewModel!.Stop();
_viewModel.Message = nameof(MainWindow_Closing);
}
}
}Add ViewModel.cs as shown below. The code that changes the state of ResetCommand in the Callback(object? state) method requires SynchronizationContext.
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Clock.ThreadTimer.Mvvm
{
internal class ViewModel : ObservableObject
{
private readonly SynchronizationContext _context;
private readonly Timer _threadingTimer;
private DateTimeOffset _resetTime;
private int _period;
public ViewModel(SynchronizationContext context)
{
Title = TITLE;
_context = context;
_threadingTimer = new Timer(Callback, null, Timeout.Infinite, Timeout.Infinite);
Period = DEFAULT_PERIOD;
ResetCommand = new RelayCommand(Reset, CanReset);
}
public void Run()
{
Reset();
}
public void Stop()
{
_threadingTimer.Dispose();
}
public string Title { get; }
public DateTimeOffset Time
{
get => _time;
set => SetProperty(ref _time, value);
}
private DateTimeOffset _time;
public long Count
{
get => _count;
set => SetProperty(ref _count, value);
}
private long _count;
public long Ellapsed
{
get => _ellapsed;
set => SetProperty(ref _ellapsed, value);
}
private long _ellapsed;
public int Period
{
get => _period;
set
{
if ((value > 0) && (value != _period))
{
SetProperty(ref _period, value);
Reset();
}
}
}
public string Message
{
get => _message;
set => SetProperty(ref _message, value);
}
private string _message = string.Empty;
private void Callback(object? state)
{
Time = DateTimeOffset.Now;
Count++;
Ellapsed = (Time.ToUnixTimeMilliseconds() - _resetTime.ToUnixTimeMilliseconds()) / _period;
var notEqual = (Count != Ellapsed);
if (string.IsNullOrEmpty(Message))
{
if (notEqual)
{
Message = string.Format(COUNT_IS_NOT_EQUAL_ELLAPSED, Count, Ellapsed, Time.ToString(TIME_FORMAT));
}
}
else
{
if (!notEqual)
{
Message = string.Empty;
}
}
_context.Post((args) =>
{
ResetCommand.NotifyCanExecuteChanged();
}, null);
}
public IRelayCommand ResetCommand { get; }
private void Reset()
{
Count = 0;
Ellapsed = 0;
Time = DateTimeOffset.Now;
_resetTime = Time;
_threadingTimer.Change(Period, Period);
}
private bool CanReset()
{
return Count != Ellapsed;
}
private readonly static string TITLE = "Clock - Threading.Timer - MVVM Pattern";
private readonly static int DEFAULT_PERIOD = 1000;
private readonly static string TIME_FORMAT = "HH:mm:ss.f";
private readonly static string COUNT_IS_NOT_EQUAL_ELLAPSED = "Count({0}) is not equal to ellapsed({1}) at {2}.";
}
}[Start without debugging]. You can see that it works properly.
Conclusion
If you’re asynchronously modifying UI state in the ViewModel of the MVVM pattern, you’ll see that SynchronizationContext is necessary.
* The copyright of this article belongs to the author. If copied by AI, please be sure to credit the source and author.





