C# Multi threading- Move objects between threads - c#

i am working with a winforms control that is both a GUI element and also does some internal processing that has not been exposed to the developer. When this component is instantiated it may take between 5 and 15 seconds to become ready so what i want to do is put it on another thread and when its done bring it back to the gui thread and place it on my form. The problem is that this will (and has) cause a cross thread exception.
Normally when i work with worker threads its just with simple data objects i can push back when processing is complete and then use with controls already on the main thread but ive never needed to move an entire control in this fashion.
Does anyone know if this is possible and if so how? If not how does one deal with a problem like this where there is the potential to lock the main gui?

You don't need to lock the GUI, you just need to call invoke:
Controls in Windows Forms are bound to
a specific thread and are not thread
safe. Therefore, if you are calling a
control's method from a different
thread, you must use one of the
control's invoke methods to marshal
the call to the proper thread. This
property can be used to determine if
you must call an invoke method, which
can be useful if you do not know what
thread owns a control. ref
Here is how it looks in code:
public delegate void ComponentReadyDelegate(YourComponent component);
public void LoadComponent(YourComponent component)
{
if (this.InvokeRequired)
{
ComponentReadyDelegate e = new ComponentReadyDelegate(LoadComponent);
this.BeginInvoke(e, new object[]{component});
}
else
{
// The component is used by a UI control
component.DoSomething();
component.GetSomething();
}
}
// From the other thread just initialize the component
// and call the LoadComponent method on the GUI.
component.Initialize(); // 5-15 seconds
yourForm.LoadComponent(component);
Normally calling the LoadComponent from another thread will cause a cross-thread exception, but with the above implementation the method will be invoked on the GUI thread.
InvokeRequired tells you if:
the caller must call an invoke method
when making method calls to the
control because the caller is on a
different thread than the one the
control was created on.
ref
Update:
So if I understand you correctly the control object is created on a thread other than the GUI thread, therefore even if you were able to pass it to the GUI thread you still won't be able to use it without causing a cross-thread exception. The solution would be to create the object on the GUI thread, but initialize it on a separate thread:
public partial class MyForm : Form
{
public delegate void ComponentReadyDelegate(YourComponent component);
private YourComponent _component;
public MyForm()
{
InitializeComponent();
// The componet is created on the same thread as the GUI
_component = new YourComponent();
ThreadPool.QueueUserWorkItem(o =>
{
// The initialization takes 5-10 seconds
// so just initialize the component in separate thread
_component.Initialize();
LoadComponent(_component);
});
}
public void LoadComponent(YourComponent component)
{
if (this.InvokeRequired)
{
ComponentReadyDelegate e = new ComponentReadyDelegate(LoadComponent);
this.BeginInvoke(e, new object[]{component});
}
else
{
// The component is used by a UI control
component.DoSomething();
component.GetSomething();
}
}
}

Without knowing too much about the object. To avoid cross thread exceptions, you can make the initial thread invoke a call (Even if you are calling from a thread).
Copied and pasted from one of my own applications :
private delegate void UpdateStatusBoxDel(string status);
private void UpdateStatusBox(string status)
{
listBoxStats.Items.Add(status);
listBoxStats.SelectedIndex = listBoxStats.Items.Count - 1;
labelSuccessful.Text = SuccessfulSubmits.ToString();
labelFailed.Text = FailedSubmits.ToString();
}
private void UpdateStatusBoxAsync(string status)
{
if(!areWeStopping)
this.BeginInvoke(new UpdateStatusBoxDel(UpdateStatusBox), status);
}
So essentially the threaded task will call the "Async" method. Which will then tell the main form to begininvoke (Actually async itself).
I believe there is probably a shorter way to do all of this, without the need for creating delegates and two different methods. But this way is just ingrained into me. And it's what the Microsoft books teach to you do :p

The BackgroundWorker class is designed for exactly this situation. It will manage the thread for you, and let you start the thread, as well as cancel the thread. The thread can send events back to the GUI thread for status updates, or completion. The event handlers for these status and completion events are in the main GUI thread, and can update your WinForm controls. And the WinForm doesn't get locked. It's everything you need. (And works equally well in WPF and Silverlight, too.)

The control must be created and modified from the UI thread, there's no way around that.
In order to keep the UI responsive while doing long-running initialization, keep the process on a background thread and invoke any control access. The UI should remain responsive, but if it doesn't, you can add some wait time to the background thread. This is an example, using .Net 4 parallel tools: http://www.lovethedot.net/2009/01/parallel-programming-in-net-40-and_30.html
If interaction with the specific control being initialized can't be allowed until initialization finishes, then hide or disable it until complete.

Related

Different results when passing a MetroFramework.Forms.MetroForm as Argument to a Method [duplicate]

UPDATE: Just to summarize what my question has boiled down to:
I was hoping that constructing .NET forms and controls did NOT create any window handles -- hoping that process was delayed until Form.Show/Form.ShowDialog
Can anyone confirm or deny whether that is true?
I've got a large WinForms form with tab control, many many controls on the form, that pauses while loading for a couple seconds. I've narrowed it down to the designer generated code in InitializeComponent, rather than any of my logic in the constructor or OnLoad.
I'm well aware that I can't be trying to interact with the UI on any thread other than the main UI thread, but what I'd like to do is to have the application pre-load this form (run the constructor) in the background, so it's ready for display on the UI thread instantly as soon as the user wants to open it. However, when constructing in a background thread, on this line in the designer:
this.cmbComboBox.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.Suggest;
I'm getting the error
Current thread must be set to single
thread apartment (STA) mode before OLE
calls can be made. Ensure that your
Main function has STAThreadAttribute
marked on it.
Now this is halfway down the designer file, which gives me hope that in general this strategy will work. But this particular line seems to be trying to instantly kick off some kind of OLE call.
Any ideas?
EDIT:
I think I'm not making myself clear here. The delay seems to take place during the construction of a bazillion controls during the designer-generated code.
My hope was that all this initialization code took place without actually trying to touch any real Win32 window objects since the form hasn't actually been shown yet.
The fact that I can set (for example) Label texts and positions from this background thread gave me hope that this was true. However it may not be true for all properties.
While it is not possible to create a form on one thread, and display it using another thread, it is certainly possible to create a form in a non main GUI thread. The current accepted answer seems to say this is not possible.
Windows Forms enforces the Single Threaded Apartment model. In summary this means that there can only be one Window message loop per thread and vice versa. Also, if for example threadA wants to interact with the message loop of threadB, it must marshal the call through mechanisms such as BeginInvoke.
However, if you create a new thread and provide it with it's own message loop, that thread will happily process events independently until it is told to end the message loop.
So to demonstrate, below is Windows Forms code for creating and displaying a form on a non GUI thread:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
label1.Text = Thread.CurrentThread.ManagedThreadId.ToString();
}
private void button1_Click(object sender, EventArgs e)
{
ThreadStart ts = new ThreadStart(OpenForm);
Thread t = new Thread(ts);
t.IsBackground=false;
t.Start();
}
private void OpenForm()
{
Form2 f2 = new Form2();
f2.ShowDialog();
}
}
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
}
private void Form2_Load(object sender, EventArgs e)
{
label1.Text = Thread.CurrentThread.ManagedThreadId.ToString() ;
}
}
The OpenForm method runs in a new thread and creates an instance of Form2.
Form2 is actually given it's own separate message loop by calling ShowDialog(). If you were to call Show() instead, no message loop would be provided and Form2 would close immediately.
Also, if you try accessing Form1 within OpenForm() (such as using 'this') you will receive a runtime error as you are trying to do cross-thread UI access.
The t.IsBackground=false sets the thread as a foreground thread. We need a foreground thread because background threads are killed immediately when the main form is closed without first calling FormClosing or FormClosed events.
Apart from these points, Form2 can now be used just like any other form. You'll notice that Form1 is still happily running as usual with it's own message lopp. This means you can click on the button and create multiple instances of Form2, each with their own separate message loop and thread.
You do need to be careful about cross Form access which is now actually cross-thread. You also need to ensure that you handle closing of the main form to ensure any non main thread forms are closed correctly.
I think your understanding is a little off. Controls must be touched from the thread that created them, not the main UI thread. You could have numerous UI threads in a application, each with its own set of controls. Thus creating a control on a different thread will not allow you to work with it from the main thread without marshalling all of the calls over using Invoke or BeginInvoke.
EDIT
Some references for multiple UI threads:
MSDN on Message Loops
MSDN social discussion
Multiple threads in WPF
The answer is no.
If you create a window handle on any thread other than the GUI thread you can never show it.
Edit: It is completely possible to create Forms and controls and
display them in a thread other than the main GUI thread. Of course if
you do this you can only access the multi threaded GUI from the thread
that created it, but it is possible. – Ashley Henderson
You need to perform any heavy lifting on a bg thread and then load the data into you GUI widget
In general, properties of the form need to be accessed from the same thread running the message loop. That means, in order to construct the form on another thread, you would need to marshal any calls to actually set properties using BeginInvoke. This is true for property sets from the constructor, too, if they end up generating a message that needs to be processed (as is happening to you now).
Even if you get that to work, what does it buy you? It will be a bit slower, not faster, overall.
Perhaps just show a splash screen while this form is loading?
Alternatively, review why your form takes so long to construct in the first place. It's not common for this to take seconds.
I believe it is possible to add the components created on the non-UI thread to the main UI, I've done it.
So there are 2 threads, 'NewCompThread' and 'MainThread'.
You spin off NewCompThread and it creates components for you - all ready to be displayed on the MainUI (created on MainThread).
But ... you WILL get an exception if you try something like this on NewCompThread:
ComponentCreatedOnNewCompTHread.parent = ComponentCreatedOnMainThread;
But you can add this:
if (ComponentCreatedOnMainThread.InvokeRequired) {
ComponentCreatedOnMainThread.Invoke(appropriate delegate...);
} else {
ComponentCreatedOnNewCompTHread.parent = ComponentCreatedOnMainThread;
}
And it will work. I've done it.
The strange thing (to me) is that then the ComponentCreatedOnNewCompTHread 'thinks' it was created on the MainThread.
If you do the following from the NewCompThread:
ComponentCreatedOnNewCompTHread.InvokeRequired
it will return TRUE, and you'll need to create a delegate and use Invoke to get back to the MainThread.
Creating a control in a background thread is possible but only on an STA thread.
I created an extension method in order to use this with the async/await pattern
private async void treeview1_AfterSelect(object sender, TreeViewEventArgs e)
{
var control = await CreateControlAsync(e.Node);
if (e.Node.Equals(treeview1.SelectedNode)
{
panel1.Controls.Clear();
panel1.Controls.Add(control);
}
else
{
control.Dispose();
}
}
private async Control CreateControlAsync(TreeNode node)
{
return await Task.Factory.StartNew(() => CreateControl(node), ApartmentState.STA);
}
private Control CreateControl(TreeNode node)
{
// return some control which takes some time to create
}
This is the extension method. Task does not allow to set the apartment so I use a thread internally.
public static Task<T> StartNew<T>(this TaskFactory t, Func<T> func, ApartmentState state)
{
var tcs = new TaskCompletionSource<T>();
var thread = new Thread(() =>
{
try
{
tcs.SetResult(func());
}
catch (Exception e)
{
tcs.SetException(e);
}
});
thread.IsBackground = true;
thread.SetApartmentState(state);
thread.Start();
return tcs.Task;
}

BackgroundWorker, Methods

So putting the standard background worker crap aside. i was looking into how i can use the background worker a little more than (Worker DoWork { add some commands for it to do })
So this is what i have come up with so far. in this scenario its doing some random WMI stuff
View/ViewModel/Model
The model is called ManagementModel
public void Start(String Args)
{
if (!Worker.IsBusy)
{
//Objectivly here you can spawn an instance of a class and perform a method.. or put the function in the background worker itself depending on what you want the thing to do
ManagementModel BackgroundManagementTask = new ManagementModel();
Worker.RunWorkerAsync(BackgroundManagementTask); //Starts the background worker.
//If you specify the class to shove into the background worker, you need to put the commands of what to do in the DoWork section.
}
}
I have a methods and whatever in the class that was spawned in the Start Method
here is the method for the DoWork
private void Workers_DoWork(object sender, DoWorkEventArgs Args)
{
//This is run on a completly seperate thread, you cannot make any changes to anything outside in this method.
//you instead pass data through the Args.ReportProgress or Args.Result
if (Worker.CancellationPending)
{
Worker.ReportProgress(100, "Cancelled By User");
return;
}
else
{
//if you passed a method here, you will need to convert Args.Arguments back to what ever you passed it in as
ManagementModel Internal = Args.Argument as ManagementModel;
//bunch of stuff in the class that already works
Internal.ComputerName = System.Environment.MachineName;
Internal.Connect();
Internal.ChangeWmiLocation("cimv2",null);
ManagementObjectCollection ResultCollection = Internal.Query("Win32_Process");
ClassProperties ResultProperties = Internal.DisplayProperties;
//now return the results to the program thread
Args.Result = ResultProperties;
//now you need to deal with the data in the WorkerCompleted Event
Worker.ReportProgress(100, "Completed");
Thread.Sleep(60); //this is required at the end of each iteration of function
}
}
So my simple questions are.
is this concept possible, can i initiate an instance of a whole class and throw it into the background worker and have the background worker perform methods and functionality inside the class
if i have to pass data back to the UI. do you think that a Struct would be the best way to go.
How would i let the ViewModel know that the Background worker is completed and update the its exposed properties for the view to update.
or am i way off base ?
Yes, the concept is possible. You can basically have a background worker do and access anything that you can do and access outside of the background worker's methods; the crucial point is that no two threads should be working with objects that haven't been explicitly laid out for that at the same time, which is why for example you should not access your UI from the background worker's working thread (the UI usually constantly being used by internal methods of the UI thread as well).
Whether you use a struct or a class is irrelevant with respect to threads; it is not the data that belongs to a given thread, but the operations. The only difference could be that when passing a struct, a copy of that struct would be created and you could keep working with your struct variable in the background worker thread - but even then, the same is true for a method that invokes callbacks within its own thread before further modifying a local struct/object, so this decision is really not related to threading.
Invoke a UI method that causes the updates to the UI once the background worker has completed its work and make sure it is called on the UI thread. The latter part can be accomplished by various means, depending on the UI toolkit the UI controls may offer a dispatcher object or something like that that allows synchronizing calls with the UI thread.

C# Getting to original thread to set textbox values

I've got my main form Form1 running the main bulk of my program.
I have a separate thread started to perform an algorithm.
When I run the method from the new thread, method MyAlgorithm() I get the error
InvalidOperationException with the message, "Control control name accessed from a thread other than the thread it was created on."
How do I get back to the original thread so that I can run the method to update my text boxes with the latest values?
This is the method that I want to run contained in Form1, the main class in my application.
// Reset the results values
public void ShowResults()
{
while (true)
{
loopsNum.Text = Convert.ToString(resultLoopsNum);
nodesVisitedNum.Text = Convert.ToString(resultNodesVisitedNum);
nodesResolvedNum.Text = Convert.ToString(resultNodesResolvedNum);
cpuLoopsNum.Text = Convert.ToString(resultCpuLoopsNum);
shortestPathCostNum.Text = Convert.ToString(resultShortestPathCost);
}
}
I've looked at the Invoke() methods, but I don't know how to get the original instance of my Form1 from the threaded method.
I'm invoking my thread like this...
// Set the algorithm method up in it's own thread
Thread thread = new Thread(new ThreadStart(MyAlgorithm));
// Run the algorithm
thread.Start();
How do I get back to the original thread so that I can run the method to update my text boxes with the latest values?
In Windows Forms, you'd either use Control.Invoke/BeginInvoke or use a BackgroundWorker and perform the update in the progress event handler.
In WPF you'd use Dispatcher.Invoke/BeginInvoke.
In C# 5 and .NET 4.5 you'll be able to use async methods which should make a lot of this much simpler...
I've looked at the Invoke() methods, but I don't know how to get the original instance of my Form1 from the threaded method.
If the "threaded method" is just an instance method of the Form, then you've already got the this reference. If it's not, you'll need to provide that information - ideally as an ISynchronizeInvoke to avoid a direct dependency on Windows Forms if you can express the "update" part separately. (That interface is somewhat deprecated these days, in favour of synchronization contexts, but it still works perfectly well.)
Have a look at Control.Invoke():
public void ShowResults()
{
while (true)
{
Thread.Sleep(1000); // don't spam the UI thread
if (this.InvokeRequired)
{
this.Invoke((Action)UpdateGui);
}
else
{
UpdateGui();
}
}
}
private void UpdateGui()
{
loopsNum.Text = Convert.ToString(resultLoopsNum);
nodesVisitedNum.Text = Convert.ToString(resultNodesVisitedNum);
nodesResolvedNum.Text = Convert.ToString(resultNodesResolvedNum);
cpuLoopsNum.Text = Convert.ToString(resultCpuLoopsNum);
shortestPathCostNum.Text = Convert.ToString(resultShortestPathCost);
}
You can use:
myform.Invoke(ShowResults);
There's other options here too:
Alternately use a System.Forms.Timer to call ShowResults periodically. Or another option would be not to use another thread to do the operation; do it in the GUI thread and call Application.DoEvents() from within the operation when you want to let the GUI update.
The first option is nice because it keeps you from accidentally flooding the GUI with Invoke requests, and the second option is nice because it's all on the GUI thread and allows you to have fine-grain control over when things get displayed on the GUI.

Windows Client GUI Design Advice - Accessing UI Thread from Long Running Task

Web Developer here and need some advice on how to achieve what must be a common requirement in Windows Forms.
I have a windows client app that calls a business object in a separate project to perform some long running tasks. Difference to other examples is that the process live in another class library i.e. Business.LongRunningTask();
I have a list box in the client that I would like to have logged to by the task. I can run the process on the UI thread passsing in the instance of the textbox and calling Application.DoEvents() when I log to the textbox from within the task. All fine, but not elegant and would prefer not to call Application.DoEvents();
If I run the long running process on a separate thread using delegates I cannot access the textbox or delegates created in the windows client form which rules out BeginInvoke calls.
Surely this is bad design on my part and would appreciate some feedback.
You're looking for the BackgroundWorker class.
To execute a time-consuming operation in the background, create a BackgroundWorker and listen for events that report the progress of your operation and signal when your operation is finished.
You can find a complete example here: http://msdn.microsoft.com/en-us/library/b2zk6580(v=VS.100).aspx#Y1351
I can run the process on the UI thread
passsing in the instance of the
textbox and calling
Application.DoEvents() when I log to
the textbox from within the task.
Yes, you could also pass in an instance of ILoggingINnterface that you have used to put in the code to write to the text box FROM WITHIN THE UI and thus have taken care of all the nice BginInvoke stuff ;)
If I run the long running process on a
separate thread using delegates I
cannot access the textbox or delegates
created in the windows client form
which rules out BeginInvoke calls.
Ah. No. You just most invoke back to the dispatcher thread then you can access all the UI elemente you like.
Yeah, avoid Application.DoEvents().
To marshall the call back onto the UI thread, call this.Invoke(YourDelegate)
To access UI elements from a different thread, you can use control.Invoke to call a delegate on the owning thread.
I used this at one point to create a live log screen which was updated from a timer while a different worker thread was running. Heres a simplified version:
public class DifferentClassLibrary
{
public delegate void StringDataDelegate(string data);
public event StringDataDelegate UpdatedData;
public void DoStuff()
{
if (UpdatedData != null)
{
Thread.Sleep(10000);
UpdatedData("data");
}
}
}
And in the winform:
public void UpdateTextBoxCallback(string data)
{
if (uiTextBoxLiveLogView.InvokeRequired)
{
uiTextBoxLiveLogView.Invoke(new DifferentClassLibrary.StringDataDelegate(UpdateTextBoxCallback), data);
}
else
{
uiTextBoxLiveLogView.Text += data;
}
}
void Main()
{
DifferentClassLibrary test = new DifferentClassLibrary();
test.UpdatedData += UpdateTextBoxCallback;
Thread thread = new Thread(new ThreadStart(test.DoStuff));
thread.Start();
}

Having trouble creating a form on a second thread

I'm writing a plug-in for another application through C#.NET. Some of the processes my plug-in must perform are rather time consuming so I want to take advantage of multiple threads so I can show the user a progress bar of how the current task if progressing rather then the whole thing just hanging.
Typically the UI for something like this would be created in the main thread, and a secondary thread would be created to do the work, such as through the BackGroundWorker class. However, in my case the work must be done in the main thread because the application I'm writing the plug-in for isn't to happy with threads other then the thread it created for the plug-in accessing it.
So instead I'm creating a second thread to create my UI in (a WinForms Form), which then communicates back to the main thread to do any real work.
I'm able to create my Form in the main thread just fine, yet when I try to instantiate my form in the second thread I get an InvalidOperationException. This occurs in the designer file for the form where the name property of a column in a list view is being set.
Here are the details of the exception.
System.InvalidOperationException was caught
Message=ColumnInfo cannot be set.
Source=System.Windows.Forms
StackTrace:
at System.Windows.Forms.ListView.SetColumnInfo(Int32 mask, ColumnHeader ch)
at System.Windows.Forms.ColumnHeader.set_Text(String value)
at QA.Revit.RevitQAForm.InitializeComponent() in C:\Documents and Settings\eric.anastas\My Documents\_SVN WC\QA Tool\RevitModelCheckerPlugIn\RevitQAForm.Designer.cs:line 758
at QA.Revit.RevitQAForm..ctor() in C:\Documents and Settings\eric.anastas\My Documents\_SVN WC\QA Tool\RevitModelCheckerPlugIn\RevitQAForm.cs:line 34
at QA.Revit.RevitQAToolApp.FormMethod() in C:\Documents and Settings\eric.anastas\My Documents\_SVN WC\QA Tool\RevitModelCheckerPlugIn\RevitModelCheckerCmd.cs:line 99
InnerException:
Update
I seemed to have gotten this working now by changing the ApartmentState of the secondary UI thread to STA. Although I'm totaly new to this multithreading stuff and have no idea what ApartmentState or STA means.
Here's my code.
//property used to store a reference to the form
internal RevitQAForm RevitQAForm { get; set; }
//monitor object that when pulsed shows the form
public static readonly Object showFormLock = new object();
//this method is called by the parent app when it starts
public Autodesk.Revit.UI.Result OnStartup(Autodesk.Revit.UI.UIControlledApplication application)
{
//this creates the form UI Thread
_formThread = new System.Threading.Thread(new System.Threading.ThreadStart(FormMethod));
_formThread.Name = "Form Thread";
_formThread.SetApartmentState(System.Threading.ApartmentState.STA);
_formThread.Start();
//returns that the plug-in startup succeeded
return Autodesk.Revit.UI.Result.Succeeded;
}
//the method is started on the second thread
private void FormMethod()
{
try
{
//creates the form
RevitQAForm = new RevitQAForm();
lock (showFormLock)
{
while (true)
{
//waits for a pulse
System.Threading.Monitor.Wait(showFormLock);
RevitQAForm.ShowDialog();
}
}
}
catch (System.Threading.ThreadAbortException)
{
//disposes the form if the thread is aborted
RevitQAForm.Dispose();
}
}
//this is called when the user request the form be shown
public void ShowForm()
{
lock (showFormLock)
{
System.Threading.Monitor.Pulse(showFormLock);
}
}
//this is called when the program closes
public Autodesk.Revit.UI.Result OnShutdown(Autodesk.Revit.UI.UIControlledApplication application)
{
//aborts the form thread
formThread.Abort();
return Autodesk.Revit.UI.Result.Succeeded;
}
Like I said this seems to work now. I'm able to start the app with my plug-in and show the form repeatedly. The form is also disposed when I close the program.
Yet now I'm trying to figure out how this form can communicate back to the main thread. The form will need to be able to trigger the main thread to start processing, the main thread will then need to be able to periodically report it's progress back to the form thread. At any point the form thread should be able to tell the main thread to cancel processing. Finally the main thread will need to notify the form when the processing is complete.
Any one have any tips on how I could do this?
This won't work. All forms need to use the underlying message pump in Windows, and to do that they need to be on the original thread.
To trigger the processing in main thread, you can use any WaitHandle derived class such as say ManualResetEvent/AutoResetEvent - essentially, Main thread will wait on to the wait handle and form thread can signal the event to start processing.
For communicating progress back from main thread to your UI/Form thread, you can use events or delegates. The simplest way would be to declare the process update delegate, instantiate it with some form's method. Then main thread can invoke it - which will essentially run the method within your form class (on main thread). Within this method, you must need to marshall call to your form's thread using Invoke method the form.
Try to call method, which uses
System.Windows.Forms.ListView.SetColumnInfo(Int32 mask, ColumnHeader ch)
by using method Invoke.

Categories

Resources