启用禁用+多线程后控制事件未触发 [英] Control events not firing after enable disable + multithreading

查看:51
本文介绍了启用禁用+多线程后控制事件未触发的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述



我正在使用线程池执行并行任务。当用户单击go按钮时,我只需禁用go按钮,直到完成所有任务。我在将enabled属性设置为true或false之前检查button.InvokeRequired。

我在这里遇到问题,

1.当我让所有任务完成之前再次启用go按钮然后我可以在按钮上触发点击事件。

2.但是当我取消其间的剩余任务并再次重新启用按钮时,它没有响应任何按钮点击事件。



我最终缩小到在不同线程的TabPage_validating上设置e.cancel,这在这里做错了,并使完整的标签页无法响应事件。取消财产似乎是在共享状态,并没有得到正确设置...什么是正确的解决方案。请帮忙。



以下是POC的完整源代码,我已经复制了这个问题,

Hi,
I am doing a parallel task using thread pool. When user clicks go button, I simply disable the go button until all the tasks are completed. I am checking button.InvokeRequired before setting the enabled property to true or false.
I facing a problem here,
1. When I let all the tasks to complete themselves before enabling the go button again then I am able to fire a click event on the button.
2. But when I cancel the remaining tasks in between and re-enable the button again, its not responding to any click events.

I finally narrowed down to setting e.cancel on TabPage_validating from different thread which is doing something wrong here and is making complete tab page not responding to events. Cancel property seems to be in sharedstate and is not getting set propely... what's the correct solution. Please help.

Following is the complete source code of a POC where I've replicated the issue,

namespace POCUI
{
 partial class ThreadingTabIssue
 {
 /// <summary>
 /// Required designer variable.
 /// </summary>
 private System.ComponentModel.IContainer components = null;
 /// <summary>
 /// Clean up any resources being used.
 /// </summary>
 /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
 protected override void Dispose(bool disposing)
 {
  if (disposing && (components != null))
  {
  components.Dispose();
  }
  base.Dispose(disposing);
 }
 #region Windows Form Designer generated code
 /// <summary>
 /// Required method for Designer support - do not modify
 /// the contents of this method with the code editor.
 /// </summary>
 private void InitializeComponent()
 {
  this.tabControl1 = new System.Windows.Forms.TabControl();
  this.tabPage1 = new System.Windows.Forms.TabPage();
  this.btnCancel = new System.Windows.Forms.Button();
  this.lblError = new System.Windows.Forms.Label();
  this.textBox1 = new System.Windows.Forms.TextBox();
  this.btnStart = new System.Windows.Forms.Button();
  this.tabPage2 = new System.Windows.Forms.TabPage();
  this.tabControl1.SuspendLayout();
  this.tabPage1.SuspendLayout();
  this.SuspendLayout();
  // 
  // tabControl1
  // 
  this.tabControl1.Controls.Add(this.tabPage1);
  this.tabControl1.Controls.Add(this.tabPage2);
  this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
  this.tabControl1.Location = new System.Drawing.Point(0, 0);
  this.tabControl1.Name = "tabControl1";
  this.tabControl1.SelectedIndex = 0;
  this.tabControl1.Size = new System.Drawing.Size(869, 562);
  this.tabControl1.TabIndex = 0;
  // 
  // tabPage1
  // 
  this.tabPage1.Controls.Add(this.btnCancel);
  this.tabPage1.Controls.Add(this.lblError);
  this.tabPage1.Controls.Add(this.textBox1);
  this.tabPage1.Controls.Add(this.btnStart);
  this.tabPage1.Location = new System.Drawing.Point(4, 22);
  this.tabPage1.Name = "tabPage1";
  this.tabPage1.Padding = new System.Windows.Forms.Padding(3);
  this.tabPage1.Size = new System.Drawing.Size(861, 536);
  this.tabPage1.TabIndex = 0;
  this.tabPage1.Text = "tabPage1";
  this.tabPage1.UseVisualStyleBackColor = true;
  this.tabPage1.Validating += new System.ComponentModel.CancelEventHandler(this.tabPage1_Validating);
  // 
  // btnCancel
  // 
  this.btnCancel.Location = new System.Drawing.Point(147, 6);
  this.btnCancel.Name = "btnCancel";
  this.btnCancel.Size = new System.Drawing.Size(89, 23);
  this.btnCancel.TabIndex = 3;
  this.btnCancel.Text = "Cancel";
  this.btnCancel.UseVisualStyleBackColor = true;
  this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
  // 
  // lblError
  // 
  this.lblError.AutoSize = true;
  this.lblError.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
  this.lblError.ForeColor = System.Drawing.Color.Red;
  this.lblError.Location = new System.Drawing.Point(242, 11);
  this.lblError.Name = "lblError";
  this.lblError.Size = new System.Drawing.Size(0, 13);
  this.lblError.TabIndex = 2;
  // 
  // textBox1
  // 
  this.textBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
   | System.Windows.Forms.AnchorStyles.Left)
   | System.Windows.Forms.AnchorStyles.Right)));
  this.textBox1.Location = new System.Drawing.Point(3, 35);
  this.textBox1.Multiline = true;
  this.textBox1.Name = "textBox1";
  this.textBox1.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
  this.textBox1.Size = new System.Drawing.Size(855, 498);
  this.textBox1.TabIndex = 1;
  // 
  // btnStart
  // 
  this.btnStart.Location = new System.Drawing.Point(3, 6);
  this.btnStart.Name = "btnStart";
  this.btnStart.Size = new System.Drawing.Size(138, 23);
  this.btnStart.TabIndex = 0;
  this.btnStart.Text = "Start Long Operation";
  this.btnStart.UseVisualStyleBackColor = true;
  this.btnStart.Click += new System.EventHandler(this.btnStart_Click);
  // 
  // tabPage2
  // 
  this.tabPage2.Location = new System.Drawing.Point(4, 22);
  this.tabPage2.Name = "tabPage2";
  this.tabPage2.Padding = new System.Windows.Forms.Padding(3);
  this.tabPage2.Size = new System.Drawing.Size(861, 536);
  this.tabPage2.TabIndex = 1;
  this.tabPage2.Text = "tabPage2";
  this.tabPage2.UseVisualStyleBackColor = true;
  // 
  // ThreadingTabIssue
  // 
  this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
  this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
  this.ClientSize = new System.Drawing.Size(869, 562);
  this.Controls.Add(this.tabControl1);
  this.Name = "ThreadingTabIssue";
  this.Text = "ThreadingTabIssue";
  this.tabControl1.ResumeLayout(false);
  this.tabPage1.ResumeLayout(false);
  this.tabPage1.PerformLayout();
  this.ResumeLayout(false);
 }
 #endregion
 private System.Windows.Forms.TabControl tabControl1;
 private System.Windows.Forms.TabPage tabPage1;
 private System.Windows.Forms.TextBox textBox1;
 private System.Windows.Forms.Button btnStart;
 private System.Windows.Forms.TabPage tabPage2;
 private System.Windows.Forms.Label lblError;
 private System.Windows.Forms.Button btnCancel;
 }
}
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Windows.Forms;
using System.Threading;
using POCLib;
namespace POCUI
{
 public partial class ThreadingTabIssue : Form
 {
 private Int32 _numberOfObjects;
 AutoResetEvent _doneEvent;
 List<SpreadSheetHandler> handlers;
 protected Boolean SendOperationInProgress { get; set; }
 protected Boolean IsCancelled { get; set; }
 public ThreadingTabIssue()
 {
  InitializeComponent();
  ThreadPool.SetMaxThreads(100, 100);
 }
 private void btnStart_Click(object sender, EventArgs e)
 {
  _numberOfObjects = 65;
  _doneEvent = new AutoResetEvent(false);
  this.ActivateDeactivateUI(false);
  StartSpreadsheetEngine();
 }
 protected void StartSpreadsheetEngine()
 {
  try
  {
  Thread mainThread = new Thread(new ThreadStart(ProcesTasks));
  mainThread.IsBackground = true;
  mainThread.Name = "Main Worker Thread";
  mainThread.Start();
  }
  catch (Exception ee)
  {
  this.SetControlProperty(lblError, "Text", ee.Message);
  }
 }
 private void ProcesTasks()
 {
  try
  {
  IsCancelled = false;
  this.SetControlProperty(lblError, "Text", "Please Wait");
  this.SetControlProperty(textBox1, "Text", "Item Status\r\n \r\n ");
  handlers = new List<SpreadSheetHandler>();
  SendOperationInProgress = true;
  for (Int32 i = 0; i < _numberOfObjects; i++)
  {
   handlers.Add(ParallelSpreadSheetHandler.QueueSpreadSheet(Task, i));
  }
  _doneEvent.WaitOne();
  if (!IsCancelled)
   this.SetControlProperty(lblError, "Text", "Done");
  else
   this.SetControlProperty(lblError, "Text", "Cancelled");
  this.ActivateDeactivateUI(true);
  SendOperationInProgress = false;
  }
  catch (Exception ee)
  {
  this.SetControlProperty(lblError, "Text", ee.Message);
  }
 }
 protected void Task(Object obj)
 {
  try
  {
  Int32 inst = (Int32)obj;
  ThreadingTabIssueImplementation implementation = new ThreadingTabIssueImplementation();
  SpreadsheetItem item = implementation.DoSomething(inst);
  this.UpdateText(item);
  }
  catch (Exception) { }
  finally { if (Interlocked.Decrement(ref _numberOfObjects) == 0) { _doneEvent.Set(); } }
 }
 protected void CancelSend(SpreadSheetHandler[] items)
 {
  this.SetControlProperty(lblError, "Text", "Please wait while operation is cancelled");
  foreach (SpreadSheetHandler item in items)
  {
  SpreadsheetStatus status = ParallelSpreadSheetHandler.Cancel(item, false);
  if (status == SpreadsheetStatus.Cancelled)
  {
   if (Interlocked.Decrement(ref _numberOfObjects) == 0) { _doneEvent.Set(); }
  }
  }
 }
 #region UI Update Code
 private void UpdateText(SpreadsheetItem item)
 {  
  try
  {
  this.UpdateTextBoxStatus(item);
  }
  catch { }
 }
 delegate void updatetext(SpreadsheetItem item);
 void UpdateTextBoxStatus(SpreadsheetItem item)
 {
  if (textBox1.InvokeRequired)
  {
  textBox1.BeginInvoke(new updatetext(UpdateTextBoxStatus),
   new object[] { item });
  }
  else { textBox1.Text = textBox1.Text + "\r\n" + item.Message; }
 }
 delegate DialogResult ShowCustomMessage(String value);
 DialogResult ShowDialog(String value)
 {
  if (InvokeRequired)
  {
  BeginInvoke(new ShowCustomMessage(ShowDialog), new object[] { value });
  return DialogResult.OK;
  }
  return MessageBox.Show(value, "Test Poc", MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
 }
 private delegate void ControlPropertyDel(Control control, string propertyName, object propertyValue);
 void SetControlProperty(Control cntrl, string propertyName, object propertyValue)
 {
  try
  {
  if (InvokeRequired)
  {
   BeginInvoke(new ControlPropertyDel(SetControlProperty),
   new object[] { cntrl, propertyName, propertyValue });
  }
  else
   cntrl.GetType().InvokeMember(propertyName, BindingFlags.SetProperty,
   null, cntrl, new object[] { propertyValue });
  }
  catch { }
 }
 void ActivateDeactivateUI(Boolean value)
 {
  try
  {
  this.SetControlProperty(btnStart, "Enabled", value);
  }
  catch (Exception ee) { throw ee; }
 }
 private delegate void ValidatingEvent(object sender, System.ComponentModel.CancelEventArgs e);
 private void tabPage1_Validating(object sender, CancelEventArgs e)
 {
  if (tabPage1.InvokeRequired)
  {
  tabPage1.BeginInvoke(new ValidatingEvent(tabPage1_Validating),
   new object[] { sender, e });
  }
  else
  {
  if (this.SendOperationInProgress)
  {
   e.Cancel = true;
   if (this.ShowDialog("Do you want to cancel") == DialogResult.OK)
   {
   IsCancelled = true;
   this.CancelSend(this.handlers.ToArray());
   }
  }
  }
 }
 private void btnCancel_Click(object sender, EventArgs e)
 {
  if (this.SendOperationInProgress)
  {
  if (this.ShowDialog("Do you want to cancel") == DialogResult.OK)
  {
   IsCancelled = true;
   this.CancelSend(this.handlers.ToArray());
  }
  }
  else { this.ShowDialog("Nothing to cancel"); }
 }
 #endregion UI Update Code
 
 }
}
using System;
using System.Collections.Generic;
using System.Threading;
namespace POCLib
{
 public sealed class SpreadSheetHandler
 {
 private WaitCallback appCallBack;
 private Object objState;
 private ExecutionContext appContext;
 internal SpreadSheetHandler(WaitCallback wCallBack, Object oState, ExecutionContext context)
 {
  appCallBack = wCallBack;
  objState = oState;
  appContext = context;
 }
 internal WaitCallback AppCallback { get { return appCallBack; } }
 internal object ObjectState { get { return objState; } }
 internal ExecutionContext Context { get { return appContext; } }
 }
 public enum SpreadsheetStatus
 {
 Completed, Cancelled, Executing, Aborted
 }
 public static class ParallelSpreadSheetHandler
 {
 private static LinkedList<SpreadSheetHandler> appCallBacks =
  new LinkedList<SpreadSheetHandler>();
 private static Dictionary<SpreadSheetHandler, Thread> executingThreads =
  new Dictionary<SpreadSheetHandler, Thread>();
 public static SpreadSheetHandler QueueSpreadSheet(
  WaitCallback callback, object state)
 {
  if (callback == null) throw new ArgumentNullException("Spreadsheet Callback method is null");
  SpreadSheetHandler item = new SpreadSheetHandler(
  callback, state, ExecutionContext.Capture());
  lock (appCallBacks) appCallBacks.AddLast(item);
  ThreadPool.QueueUserWorkItem(new WaitCallback(HandleItem));
  return item;
 }
 private static void HandleItem(object ignored)
 {
  SpreadSheetHandler item = null;
  try
  {
  lock (appCallBacks)
  {
   if (appCallBacks.Count > 0)
   {
   item = appCallBacks.First.Value;
   appCallBacks.RemoveFirst();
   }
   if (item == null) return;
   executingThreads.Add(item, Thread.CurrentThread);
  }
  ExecutionContext.Run(item.Context,
   delegate { item.AppCallback(item.ObjectState); }, null);
  }
  finally
  {
  lock (appCallBacks)
  {
   if (item != null) executingThreads.Remove(item);
  }
  }
 }
 public static SpreadsheetStatus Cancel(SpreadSheetHandler item, bool allowAbort)
 {
  if (item == null) throw new ArgumentNullException("Parallet Sheet handler is null");
  lock (appCallBacks)
  {
  LinkedListNode<SpreadSheetHandler> node = appCallBacks.Find(item);
  if (node != null)
  {
   appCallBacks.Remove(node);
   return SpreadsheetStatus.Cancelled;
  }
  else if (executingThreads.ContainsKey(item))
  {
   if (allowAbort)
   {
   executingThreads[item].Abort();
   executingThreads.Remove(item);
   return SpreadsheetStatus.Aborted;
   }
   else return SpreadsheetStatus.Executing;
  }
  else return SpreadsheetStatus.Completed;
  }
 }
 }
}
using System;
using System.Collections.Generic;
using System.Threading;
namespace POCLib
{
 public class ThreadingTabIssueImplementation
 {
 public SpreadsheetItem DoSomething(Int32 instance)
 {
  SpreadsheetItem retVal = new SpreadsheetItem();
  try
  {
  Thread.Sleep(4000);
  retVal.Message = "Completed on : " + instance;
  retVal.ItemStatus = SpreadsheetItemStatus.Success;
  }
  catch (Exception ee)
  {
  retVal.ItemStatus = SpreadsheetItemStatus.HardError;
  retVal.Message = "Error on : " + instance.ToString() + ", Message : " + ee.Message;
  }
  return retVal;
 }
 }
 [Serializable]
 public class SpreadsheetItem
 {
 private String _message = String.Empty;
 public Int32 Identity { get; set; }
 public SpreadsheetItemStatus ItemStatus { get; set; }
 public String Message { get { return _message; } set { _message = value; } }
 public EventWaitHandle ResetEvent { get; set; }
 }
 [Serializable]
 public enum SpreadsheetItemStatus
 {
 None, Warning, HardError, Success
 }
}

推荐答案

Seriously - that is WAY too much code, especially considering that nobody here is going to want to create a whole new project with your source code just to debug something that you could more easily debug yourself.
Seriously - that is WAY too much code, especially considering that nobody here is going to want to create a whole new project with your source code just to debug something that you could more easily debug yourself.


Hey John,



I can understand your frustration. But, I just wanted to give the complete project \"Proof Of Concept\" to explain my point. E.Cancel here is simply making complete tabpage controls not responding to events.



I looks for answer on Microsoft documentation for this property and I couldn’t find anything.



Only following code is worth seeing here,



Hey John,

I can understand your frustration. But, I just wanted to give the complete project "Proof Of Concept" to explain my point. E.Cancel here is simply making complete tabpage controls not responding to events.

I looks for answer on Microsoft documentation for this property and I couldn't find anything.

Only following code is worth seeing here,

private delegate void ValidatingEvent(object sender, System.ComponentModel.CancelEventArgs e);
private void tabPage1_Validating(object sender, CancelEventArgs e)
{
 if (tabPage1.InvokeRequired)
 {
 tabPage1.BeginInvoke(new ValidatingEvent(tabPage1_Validating),
  new object[] { sender, e });
 }
 else
 {
 if (this.SendOperationInProgress)
 {
  e.Cancel = true;
  if (this.ShowDialog("Do you want to cancel") == DialogResult.OK)
  {
  IsCancelled = true;
  this.CancelSend(this.handlers.ToArray());
  }
 }
 }
}





hope to find an answer here. Thanks!



Regards, Vinay



hope to find an answer here. Thanks!

Regards, Vinay


It’s hard to scan all you code, but I see a design issues here. I looked at your code of button event handler and see that you’re created a thread on the click. This can work in principle, but such design is more expensive (which can be quite accessible) and it is really prone to exact same problems you have described: you try to manage pretty complex issue with the life cycle control of your thread, which is not very easy.



There is much better way: create a permanent thread from the very beginning. For example, you can created a thread in the form in the handler of its event System.Windows.Forms.Form.Shown. You

also need to make sure the thread if finished or aborted when the user closed the form.



The thread should be kept from running by an instance of System.Threading.EventWaitHandle. Your buttons should only Set or Reset this instance to control the thread behavior. Initial state of the handle should be not set (reset). You thread should implement an infinite cycle calling System.Threading.EventWaitHandle.WaitOne on the same instance from the very beginning. If the instance is not set, the thread is kept by the OS in a wait state: the OS switched your thread of and never schedule it back to execution until it is waken up. You can wake up it when you tasks are ready for the thread processing, but your button click which would call System.Threading.EventWaitHandle.Set. Use auto-reset mode for this event handle. Also, the thread can be awaken by timeout (if you use any, perhaps not in this case), System.Threading.Thread.Abort or System.Threading.Thread.Interrupt.



When all the tasks are complete, the thread should notify the UI via System.Threading.Dispatcher or System.Windows.Forms.Control Invoke or BeginInvoke (Dispatcher.Invoke works like Control.Invoke but also works for WPF, not just for Forms). One of the things done by UI on this notification would re-enabling of your Start button.



By the way your InvokeRequired is not required is you know that it is called from non-UI thread — it will always return true. This is just a fool-proof method, rarely really needed, only if it is called in some method which can be called from different thread.



And advanced variant of this EventWaitHandle technique is a blocking queue when you not only synchronize thread but also feed the data to be used for processing. It can be a delegate, to abstract our the task itself.



Please see my Tips/Tricks article on the topic. I provide a full source code and usage samples. You can also use the queue available in v.4.0, but my usage also works in this case. See:

Simple Blocking Queue for Thread Communication and Inter-thread Invocation.



For detailed explanation of invocation, see also my past Solutions:

Control.Invoke() vs. Control.BeginInvoke(),

Problem with Treeview Scanner And MD5.



For passing data to a thread I provide a very convenient wrapper; see my other past Solution:

How to pass ref parameter to the thread.



Here is a collection of the references to my past answers on the related topics:

How to get a keydown event to operate on a different thread in vb.net (perhaps nothing new compared to all the above links, look just in case).



—SA
It's hard to scan all you code, but I see a design issues here. I looked at your code of button event handler and see that you're created a thread on the click. This can work in principle, but such design is more expensive (which can be quite accessible) and it is really prone to exact same problems you have described: you try to manage pretty complex issue with the life cycle control of your thread, which is not very easy.

There is much better way: create a permanent thread from the very beginning. For example, you can created a thread in the form in the handler of its event System.Windows.Forms.Form.Shown. You
also need to make sure the thread if finished or aborted when the user closed the form.

The thread should be kept from running by an instance of System.Threading.EventWaitHandle. Your buttons should only Set or Reset this instance to control the thread behavior. Initial state of the handle should be not set (reset). You thread should implement an infinite cycle calling System.Threading.EventWaitHandle.WaitOne on the same instance from the very beginning. If the instance is not set, the thread is kept by the OS in a wait state: the OS switched your thread of and never schedule it back to execution until it is waken up. You can wake up it when you tasks are ready for the thread processing, but your button click which would call System.Threading.EventWaitHandle.Set. Use auto-reset mode for this event handle. Also, the thread can be awaken by timeout (if you use any, perhaps not in this case), System.Threading.Thread.Abort or System.Threading.Thread.Interrupt.

When all the tasks are complete, the thread should notify the UI via System.Threading.Dispatcher or System.Windows.Forms.Control Invoke or BeginInvoke (Dispatcher.Invoke works like Control.Invoke but also works for WPF, not just for Forms). One of the things done by UI on this notification would re-enabling of your Start button.

By the way your InvokeRequired is not required is you know that it is called from non-UI thread — it will always return true. This is just a fool-proof method, rarely really needed, only if it is called in some method which can be called from different thread.

And advanced variant of this EventWaitHandle technique is a blocking queue when you not only synchronize thread but also feed the data to be used for processing. It can be a delegate, to abstract our the task itself.

Please see my Tips/Tricks article on the topic. I provide a full source code and usage samples. You can also use the queue available in v.4.0, but my usage also works in this case. See:
Simple Blocking Queue for Thread Communication and Inter-thread Invocation.

For detailed explanation of invocation, see also my past Solutions:
Control.Invoke() vs. Control.BeginInvoke(),
Problem with Treeview Scanner And MD5.

For passing data to a thread I provide a very convenient wrapper; see my other past Solution:
How to pass ref parameter to the thread.

Here is a collection of the references to my past answers on the related topics:
How to get a keydown event to operate on a different thread in vb.net (perhaps nothing new compared to all the above links, look just in case).

—SA


这篇关于启用禁用+多线程后控制事件未触发的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆