Reputation: 87
I've created a TimerManager
class for my WPF application.
This class handles the start and stop the dispatcher timer.
Here is the class:
public static class TimerManager
{
static DispatcherTimer disTimer;
static Model m = Model.GetInstance();
static TimerManager()
{
disTimer = new DispatcherTimer();
disTimer.Tick += disTimer_tick;
disTimer.Interval = new TimeSpan(0, 0, 1);
}
public static void StartTimer()
{
disTimer.Start();
}
public static void StopTimer()
{
disTimer.Stop();
}
private static void disTimer_tick(object sender, EventArgs e)
{
m.Tick++;
}
}
And I've created a Model
class that represents the ticking in the UI.
(Binding in MainWindow.xaml -> xy textbox text field "{Binding Tick}"
).
class Model : INotifyPropertyChanged
{
private Model()
{
}
static Model instance;
public static Model GetInstance()
{
if (instance == null)
{
instance = new Model();
}
return instance;
}
int tick;
public event PropertyChangedEventHandler PropertyChanged;
public void OnNotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}
public int Tick
{
get
{
return tick;
}
set
{
tick = value;
OnNotifyPropertyChanged();
}
}
}
And here is the MainWindow
class:
Model m;
public MainWindow()
{
InitializeComponent();
m = Model.GetInstance();
this.DataContext = m;
}
private void startButton_Click(object sender, RoutedEventArgs e)
{
ThreadPool.QueueUserWorkItem(o =>
{
TimerManager.StartTimer();
});
//TimerManager.StartTimer();
}
private void stopButton_Click(object sender, RoutedEventArgs e)
{
TimerManager.StopTimer();
}
When I click the start button I use the ThreadPool.QueueUserWorkItem()
method. In that method, I start the timer but the timer tick is not run at every one second.
When I don't use ThreadPool
this works. But this solution is not good for me; ThreadPool
is important for me because I use an HTTP web server (in local).
My question is: why is the ticking not working if I use ThreadPool
?
Upvotes: 1
Views: 1038
Reputation: 70661
The DispatcherTimer
object has thread affinity. That is, it is tied to a specific thread. In particular, it is designed specifically to raise its Tick
event in the thread in which it was created, using the Dispatcher
for that thread.
Your ThreadManager
class's static constructor will be called when the type is first used. In your non-working example, this occurs in the queued work item method, causing the static constructor to be executed in the thread pool thread used to execute that work item method. This in turn causes the DispatcherTimer
object you create to be owned by that thread, and to have its Tick
event raised in that thread by the Dispatcher
for that thread.
Except, thread pool threads don't have Dispatcher
s. So there's no Dispatcher
there to raise the Tick
event for the DispatcherTimer
object. Even if there was, without a call to Application.Run()
to have the dispatcher loop executed, the Dispatcher
wouldn't actually get to dispatch anything, including the Tick
event.
What you need is to make sure that when you create the DispatcherTimer
object, the code that creates that object is executed in the dispatcher thread, which is your main UI thread.
There are a couple of ways to do that. IMHO, the best way is to make your ThreadManager
class not a static
class and to create an instance of it in your MainWindow
constructor. For example:
class TimerManager
{
DispatcherTimer disTimer;
Model m = Model.GetInstance();
public TimerManager()
{
disTimer = new DispatcherTimer();
disTimer.Tick += disTimer_tick;
disTimer.Interval = new TimeSpan(0, 0, 1);
}
public void StartTimer()
{
disTimer.Start();
}
public void StopTimer()
{
disTimer.Stop();
}
private void disTimer_tick(object sender, EventArgs e)
{
m.Tick++;
}
}
and:
public partial class MainWindow : Window
{
TimerManager _timerManager = new TimerManager();
public MainWindow()
{
InitializeComponent();
this.DataContext = Model.GetInstance();
}
private void startButton_Click(object sender, RoutedEventArgs e)
{
ThreadPool.QueueUserWorkItem(o =>
{
_timerManager.StartTimer();
});
}
private void stopButton_Click(object sender, RoutedEventArgs e)
{
_timerManager.StopTimer();
}
}
Since you know your MainWindow
object has to be created in the dispatcher thread, and you know that non-static field initialization happens at the same time the constructor is called, in that same dispatcher thread, the above ensures that your TimerManager
object is created in the dispatcher thread.
This gives you complete control over the lifetime of the TimerManager
object, particularly when it's created but of course also when it can be discarded. Given the nature of the DispatcherTimer
object itself, it's my opinion that this is better than maintaining a statically-held instance.
This approach also gives you the option of having a manager object for each dispatcher thread (in rare cases, a program might have more than one…you should try very hard to avoid getting into that situation, but it can be useful for types to at least be compatible with such a situation).
That said, if you really want to keep the static
implementation, you can do that by providing a method that can be called explicitly when you want to initialize the class, so you can make sure that the initialization happens in the right thread:
static class TimerManager
{
static DispatcherTimer disTimer;
static Model m = Model.GetInstance();
public static void Initialize()
{
disTimer = new DispatcherTimer();
disTimer.Tick += disTimer_tick;
disTimer.Interval = new TimeSpan(0, 0, 1);
}
public static void StartTimer()
{
disTimer.Start();
}
public static void StopTimer()
{
disTimer.Stop();
}
private static void disTimer_tick(object sender, EventArgs e)
{
m.Tick++;
}
}
Then in your MainWindow
class:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = Model.GetInstance();
StaticTimerManager.Initialize();
}
private void startButton_Click(object sender, RoutedEventArgs e)
{
ThreadPool.QueueUserWorkItem(o =>
{
StaticTimerManager.StartTimer();
});
}
private void stopButton_Click(object sender, RoutedEventArgs e)
{
StaticTimerManager.StopTimer();
}
}
All you need to do here is make sure you call the Initialize()
method from the main UI thread where you actually have a running dispatcher, before you attempt to call either of the other two static
methods in the class.
This approach could also be made to work with multiple threads (i.e. if you have more than one dispatcher thread), but it would be trickier, especially if you want to be able to call the StartTimer()
method from a different thread that actually owns the timer object. I'd recommend against the static
class approach if you really did wind up in that situation.
Upvotes: 1