A couple of examples, using a System.Windows.Forms.Timer that replaces the System.Timers.Timer you're using now.
The latter raises its Elapsed
event in ThreadPool Threads. DataBindings, as many other objects, don't work cross-Thread.
You can read some other details here:
Why does invoking an event in a timer callback cause following code to be ignored?
The System.Windows.Forms.Timer
is instead already synchronized, its Tick
event is raised in the UI Thread.
New Model class using this Timer:
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Forms;
public class Model : INotifyPropertyChanged, IDisposable
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private readonly Timer timer;
private int counter;
public Model() {
counter = 0;
timer = new Timer() { Interval = 1000 };
timer.Tick += this.Timer_Elapsed;
StartTimer();
}
public int Counter {
get => counter;
set {
if (counter != value) {
counter = value;
NotifyPropertyChanged();
}
}
}
public void StartTimer() => timer.Start();
public void StopTimer() => timer.Stop();
private void Timer_Elapsed(object sender, EventArgs e) => Counter++;
public void Dispose(){
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing) {
lock (this) {
if (timer != null) timer.Tick -= Timer_Elapsed;
timer?.Stop();
timer?.Dispose();
}
}
}
}
In case you want to use a System.Threading.Timer
, you need to synchronize its Elapsed event with the UI Thread, since, as mentioned, a PropertyChanged notification cannot be marshalled across Threads and your DataBindings won't work.
You can use (mainly) two methods:
- Use the SynchronizationContext class to capture the current
WindowsFormsSynchronizationContext
and Post to it to update a Property value.
- Add a Constructor to your class that also accepts an UI element that implements ISynchronizeInvoke (any Control, in practice). You can use this object to set the
System.Threading.Timer
's SynchronizingObject property.
When set, the Elapsed
event will be raised in the same Thread as the Sync object.
Note: You cannot declare a Model object as a Field and initialize at the same time: there's no SynchronizationContext until after the starting Form has been initialized. You can initialize a new Instance in the Constructor of a Form or any time after:
public partial class Form1 : Form
{
Model model = new Model(); // <= Won't work
// ------------------------------------------
Model model = null; // <= It's OK
public Form1()
{
InitializeComponent();
// Using the SynchronizationContext
model = new Model();
// Or, using A Synchronizing Object
model = new Model(this);
var binding = new Binding("Text", model, "Counter", true, DataSourceUpdateMode.OnPropertyChanged);
label1.DataBindings.Add(binding);
}
}
A modified Model class that makes use of both:
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Timers;
public class Model : INotifyPropertyChanged, IDisposable
{
public event PropertyChangedEventHandler PropertyChanged;
internal readonly SynchronizationContext syncContext = null;
internal ISynchronizeInvoke syncObj = null;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private System.Timers.Timer timer;
private int counter;
public Model() : this(null) { }
public Model(ISynchronizeInvoke synchObject)
{
syncContext = SynchronizationContext.Current;
syncObj = synchObject;
timer = new System.Timers.Timer();
timer.SynchronizingObject = syncObj;
timer.Elapsed += Timer_Elapsed;
StartTimer(1000);
}
public int Counter {
get => counter;
set {
if (counter != value) {
counter = value;
NotifyPropertyChanged();
}
}
}
public void StartTimer(int interval) {
timer.Interval = interval;
timer.AutoReset = true;
timer.Start();
}
public void StopTimer(int interval) => timer.Stop();
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
if (syncObj is null) {
syncContext.Post((spcb) => Counter += 1, null);
}
else {
Counter += 1;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing) {
lock (this) {
if (timer != null) timer.Elapsed -= Timer_Elapsed;
timer?.Stop();
timer?.Dispose();
}
}
}
}