And*_*mos 2 .net c# events timer winforms
我正在编写一个简单的游戏,它使用system.threading命名空间中的计时器来模拟操作的等待时间。我的目标是让计时器每秒执行一次 x 秒。为了实现这一点,我在计时器回调中添加了一个计数器。
问题是我在调用DeliveryProgressChangedEvent事件后放置的任何代码似乎都被忽略了。我的计数器永远不会增加,因此允许计时器永远运行。
如果我在增加计数器后调用该事件,则一切正常。调用事件后什么都不会执行。如果不解决这个问题,我想了解而不是走简单的路线。
我对 system.threading 计时器对象和事件进行了大量研究,但找不到与我的问题相关的任何信息。
我为我的项目创建了一个简单的例子来演示下面的问题。
class Game
{
private Timer _deliveryTimer;
private int _counter = 0;
public event EventHandler DeliveryProgressChangedEvent;
public event EventHandler DeliveryCompletedEvent;
public Game()
{
_deliveryTimer = new Timer(MakeDelivery);
}
public void StartDelivery()
{
_deliveryTimer.Change(0, 1000);
}
private void MakeDelivery(object state)
{
if (_counter == 5)
{
_deliveryTimer.Change(0, Timeout.Infinite);
DeliveryCompletedEvent?.Invoke(this, EventArgs.Empty);
}
DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
++_counter;
}
}
Run Code Online (Sandbox Code Playgroud)
public partial class Form1 : Form
{
Game _game = new Game();
public Form1()
{
InitializeComponent();
_game.DeliveryProgressChangedEvent += onDeliveryProgressChanged;
_game.DeliveryCompletedEvent += onDeliveryCompleted;
pbDelivery.Maximum = 5;
}
private void onDeliveryProgressChanged(object sender, EventArgs e)
{
if (InvokeRequired)
pbDelivery.BeginInvoke((MethodInvoker)delegate { pbDelivery.Increment(1); });
MessageBox.Show("Delivery Inprogress");
}
private void onDeliveryCompleted(object sender, EventArgs e)
{
MessageBox.Show("Delivery Completed");
}
private void button1_Click(object sender, EventArgs e)
{
_game.StartDelivery();
}
}
Run Code Online (Sandbox Code Playgroud)
编辑
只是为了澄清我的意思。我后面的任何代码DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);都不会执行。在我的例子中++_counter不会运行。事件确实触发并且onDeliveryProgressChanged处理程序确实运行。
问题:
使用System.Threading.Timer类,当TimerCallback被调用时,事件被触发,以通知的用户DeliveryProgressChangedEvent和DeliveryCompletedEvent定制Game类的程序的进展情况和它的终止。
在示例类中,订阅者(此处为 Form 类)更新 UI、设置 ProgressBar 控件的值并显示 MessageBox(此处所示类示例的实际实现中使用)。
似乎在调用第一个事件之后:
DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
++_counter;
Run Code Online (Sandbox Code Playgroud)
_counter永远不会到达应该增加的行,因此检查_counter将计时器设置为新值的代码永远不会执行。
会发生什么:
该System.Threading.Timer由线程池线程(不止一个)提供服务。它的回调是在 UI 线程以外的线程上调用的。从回调调用的事件也在 ThreadPool 线程中引发。
处理程序委托中的代码 onDeliveryProgressChanged 然后在同一个线程上运行。
private void onDeliveryProgressChanged(object sender, EventArgs e)
{
if (InvokeRequired)
pbDelivery.BeginInvoke((MethodInvoker)delegate { pbDelivery.Increment(1); });
MessageBox.Show("Delivery Inprogress");
}
Run Code Online (Sandbox Code Playgroud)
当 MessageBox 显示时——它是一个模态窗口——它会像往常一样从它运行的地方阻止线程。永远不会到达调用事件的行后面的代码,因此_counter永远不会增加:
DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
++_counter;
Run Code Online (Sandbox Code Playgroud)
该System.Threading.Timer可以由多个线程提供服务。我只是在这一点上引用文档,它非常简单:
定时器执行的回调方法应该是可重入的,因为它是在 ThreadPool 线程上调用的。如果定时器间隔小于执行回调所需的时间,或者如果所有线程池线程都在使用并且回调多次排队,则可以在两个线程池线程上同时执行回调。
在实践中发生的情况是,虽然执行 CallBack 的线程被 MessageBox 阻塞,但这并不能阻止 Timer 从另一个线程执行 CallBack:调用事件时会显示一个新的 MessageBox,并且一直运行直到它有资源。
MessageBox 没有所有者。当 MessageBox 显示时没有指定所有者,它的类使用GetActiveWindow()来查找 MessageBox 窗口的所有者。此函数尝试返回附加到调用线程的消息队列的活动窗口的句柄。但是运行 MessageBox 的线程没有活动窗口,因此所有者是桌面(实际上,IntPtr.Zero在这里)。
这可以通过激活(单击)调用 MessageBox 的窗体来手动验证:MessageBox 窗口将在窗体下消失,因为它不属于它。
如何解决:
? 此处提供的代码只是用于重现问题的 WinForms 实现,因此这些代码可能不适用于所有上下文。
使用System.Timers.Timer:SynchronizingObject属性提供了将事件编组回创建当前类实例的线程的方法(与具体实现上下文相关的考虑相同)。
使用AsyncOperationManager.CreateOperation()方法生成AsyncOperation,然后使用SendOrPostCallback委托让调用SynchronizationContext.Post()方法(经典的 BackGroundWorker 样式)。AsyncOperation
BeginInvoke() 消息框,将其附加到 UI 线程SynchronizationContext。例如,:
this.BeginInvoke(new Action(() => MessageBox.Show(this, "Delivery Completed")));
Run Code Online (Sandbox Code Playgroud)
现在 MessageBox 归 Form 所有,它将像往常一样运行。ThreadPool 线程可以自由继续:Modal Window 与 UI 线程同步。
避免将 MessageBox 用于此类通知,因为它真的很烦人 :) 有许多其他方法可以通知用户状态更改。MessageBox 可能不太周到。
为了让它们按预期工作,而不改变当前的实现,可以像这样重构Game和Form1类:
class Game
{
private System.Threading.Timer deliveryTimer = null;
private int counter;
public event EventHandler DeliveryProgressChangedEvent;
public event EventHandler DeliveryCompletedEvent;
public Game(int eventsCount) { counter = eventsCount; }
public void StartDelivery() {
deliveryTimer = new System.Threading.Timer(MakeDelivery);
deliveryTimer.Change(1000, 1000);
}
public void StopDelivery() {
deliveryTimer?.Dispose();
deliveryTimer = null;
}
private void MakeDelivery(object state) {
if (deliveryTimer is null) return;
DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
counter -= 1;
if (counter == 0) {
deliveryTimer?.Dispose();
deliveryTimer = null;
DeliveryCompletedEvent?.Invoke(this, EventArgs.Empty);
}
}
}
public partial class Form1 : Form
{
Game game = null;
public Form1() {
InitializeComponent();
pbDelivery.Maximum = 5;
game = new Game(pbDelivery.Maximum);
game.DeliveryProgressChangedEvent += onDeliveryProgressChanged;
game.DeliveryCompletedEvent += onDeliveryCompleted;
}
private void onDeliveryProgressChanged(object sender, EventArgs e)
{
this.BeginInvoke(new MethodInvoker(() => {
pbDelivery.Increment(1);
// This MessageBox is used to test the progression of the events and
// to verify that the Dialog is now modal to the owner Form.
// Of course it's not used in an actual implentation.
MessageBox.Show(this, "Delivery In progress");
}));
}
private void onDeliveryCompleted(object sender, EventArgs e)
{
this.BeginInvoke(new Action(() => MessageBox.Show(this, "Delivery Completed")));
}
private void button1_Click(object sender, EventArgs e)
{
game.StartDelivery();
}
}
Run Code Online (Sandbox Code Playgroud)