Page 1 of 1

C# Multi-threading in a GUI Environment

#1 CodingSup3rnatur@l-360  Icon User is offline

  • D.I.C Addict
  • member icon

Reputation: 991
  • View blog
  • Posts: 971
  • Joined: 30-September 10

Posted 12 September 2011 - 03:14 PM

*
POPULAR

C# Multi-Threading In A GUI Environment

Introduction

Hi everyone, and welcome to this spur of the moment tutorial!


Aim of this tutorial

The aim of this tutorial is to provide insight into the tools available for separating asynchronous and multi threaded work from the UI. The general idea is to separate the logical work out into a separate class(es), instantiate that class (initializing it with any state data required by the work operation), and then get the result, and update the GUI via a callback, in a nice, decoupled manner.

Despite this, the general patterns still apply even if you do not use a separate class to encapsulate the work, although I would argue that it is a good idea to use a separate class for any meaningful operation.


Moving on, the tutorial will build on this tutorial, but will introduce new techniques, of which aren't tightly coupled to the WinForms technology.


Required Knowledge

Before you begin this tutorial, you should have a solid base knowledge of all C#'s fundamental concepts, and have at least a general knowledge of the different API's available for performing multithreaded and asynchronous operations in C#. The examples are kept very very simple, but basic knowledge is still required :)


Why I decided to write this tutorial

So, what has brought this on? Well, I responded to this thread made in the C# forum today about the standard cross thread communication issues that we have all faced when using multithreading in a GUI environment. For example, we have all seen code like this to marshal updates onto the UI thread, I am sure:

this.txtMyTextBox.Invoke(new MethodInvoker(() => this.txtMyTextBox.Text = "Updated Text!"));


Anyway, tlhIn`toq then made a very good point about how it is usually better to decouple the operation running on a background thread from the UI, so I'd like to provide a few examples :bigsmile:, and expand on this point.

The gist of the point was that rather than have the background thread directly update the UI directly using Invoke(), you should try and favour having the background thread raise an event (of which contains the data used to update the UI), have the form subscribe to this event, and thus have the form update itself when the data from the background thread becomes available.

This would mean that the operation knows nothing of the UI, making the program more maintainable, and the operation itself reusable in the future. The UI is left with its single responsibility - updating the UI, and the class encapsulating the background operation is left with its single responsibility - perform the operation.


Although, even with events, you do still have to be aware of the cross thread communication issue, of course. If you simply raise an event (in the standard, everyday way) from the background thread, and have the form handle the event, the handler method will be run on that background thread, thus meaning you would still have to invoke UI updates onto the main thread.

This wouldn't be the end of the world, as you have still effectively decoupled the logical operation from the UI, but we would like to avoid even having to use Invoke() if possible, thus keeping the UI code as focused and simple as possible :)


Examples

So, let's move onto the examples, staring with the events technique, and moving onto new techniques that have become available in .NET 4.0...


All the examples revolve around executing some arbitrary work, on a background thread, of which eventually completes at which point the GUI is alerted, and the GUI is allowed to update itself, without needing to use Invoke(). They move progressively up from traditional techniques to newer techniques using features new to .NET 4.0.

I am going to encapsulate the work in a class called WorkItem (you could name it something more specific to your application. For example, you could have a class called PrimeNumberCalculator, have a method called Calculate() to calculate the numbers, and have an event called PrimesCalculated when the calculation was complete), and the work is always going to return a string as a result (just for example's sake, you can change this, or make it more general if you wish (return a generic Result object perhaps...)).

So, a button called btnStart is going to start the work going, and a textbox called txtResult is going to be updated with the result.


Example 1: Raising an event to update the UI - The More Traditional Approach

This concept is very closely related to the Event Based Asynchronous pattern, and, thus, in many cases, you may be able to use built in classes that already implement that pattern (or perhaps sub class those classes, and add custom functionality).

The BackgroundWorker class is a commonly used example of this pattern, of which is used to perform compute bound operations on a background thread. Here is a tutorial detailing how to use that class.

That class uses the thread pool (by calling the BeginInvoke() method on a WorkerThreadStartDelegate instance).

This technique is still very worthwhile to know how to implement yourself though. For example, if you don't want to use the thread pool, you may want to create your own, dedicated class. Or, you may just want to make your own class with domain specific events and classes, as it fits better with your scenario/program.

Plus, it doesn't hurt to get an idea of how classes like the BackgroundWorker work internally (at a general level), and it demonstrates the point tlhIn`toq was making.


Let's start with the basic EventArg subclass that will hold the result of our operation (it only has one constructor for this example, you can add more).


//event args containing the result of the WorkItem's work
public class WorkItemCompletedEventArgs : EventArgs {

    public string Result { get; set; }

    public WorkItemCompletedEventArgs(string result) {
       this.Result = result;
    }
}



Pretty straight forward. Its just a class to hold the result our work operation produces.

Next, the WorkItem class, of which will actually do our work.

Note: if you wanted to pass arguments (state data) for use in the operation, you could store them arguments in properties in this class, passing them in via the WorkItem constructor maybe(???). You can then use those properties in your operation performed by DoWork.


     public class WorkItem {
        //async operation representing the work item
        private AsyncOperation op;

        //event handler to be run when work has completed with a result
        public event EventHandler<WorkItemCompletedEventArgs> Completed;

        public void DoWork() {
            //get new async op object ***from current synchronisation context***
            //which is the caller's sync context (i.e. the form's)
            this.op = AsyncOperationManager.CreateOperation(null);
            //queue work so a thread from the thread pool can pick it
            //up and execute it
            ThreadPool.QueueUserWorkItem((o) => this.PerformWork);
        }

        private void PerformWork() {
          //do work here...
          //The work could use passed state data 
          //held in properties of this class
          //if we needed to pass in data from the UI
          //for example 
            Thread.Sleep(5000);
            //once completed, call the post completed method, passing in the result
            this.PostCompleted("Update with result!");
        }


        private void PostCompleted(string result) {
            //complete the async operation, calling OnCompleted, passing in the result to it
            // The lambda passed into this method is invoked on the synchronisation context
            //the async operation was created on (i.e. the form's)
            op.PostOperationCompleted((o) => this.OnCompleted(new WorkItemCompletedEventArgs(o.ToString())), result);
        }


        protected virtual void OnCompleted(WorkItemCompletedEventArgs e) {
            //raise the Completed event ***on the form's synchronisation context***
            EventHandler<WorkItemCompletedEventArgs> temp = this.Completed;
            if (temp != null) {
                temp.Invoke(this, e);
            }
        }
    }



Now, here is the form's code:

public partial class Form1 : Form {
       
        private string Result { get { return this.txtResult.Text; } set { this.txtResult.Text = value; } }

        public Form1() {
            InitializeComponent();
        }

        private void btnStart_Click(object sender, EventArgs e) {
            //start work
            this.StartBackgroundWork();
        }

        private void StartBackgroundWork() {
            //create new work item
            //We could pass in any state data to use in the
            //operation into the constructor here. We'd have
            //to write the constructor first through, obviously ;)/>
            WorkItem item = new WorkItem();
            //subscribe to be notified when result is ready
            item.Completed += item_Completed;
            //start work going from form
            item.DoWork();
        }

        //handler method to run when work has completed
        private void item_Completed(object sender, WorkItemCompletedEventArgs e) {
            //GUI is free to update itself
            this.Result = e.Result;
        }
    }



Right, so let's go through what happens when the user presses the start button...

1) StartBackgroundWork() on the form is called.

2) StartBackgroundWork() creates a new WorkItem instance, and subscribes to its Completed event. So, when that WorkItem raises the Completed event, the item_Completed method will be called. Finally, it sets the work going by calling DoWork().

Note how the form has no idea how the work is done. All it knows is that the work will be completed with a result, sometime in the future.

3) DoWork() first captures the current synchronisation context, and encapsulates in a AsyncOperation object. As DoWork() was called from the form, this means that it captures the context of the UI thread. This AsyncOperation object, quite simply, represents our operation! Finally, DoWork() calls PerformWork() on a new background thread from the thread pool.

4) PerformWork() simulates genuine work by sleeping the thread, and then calls PostCompleted(), passing in the result of the work (which is a hardcoded string in this example).

5) PostCompleted() calls PostOperationCompleted() on the AsyncOperation we created in DoWork(). What this does is calls the lambda expression specified in the first arguement (passing in the result string to that lambda via the second argument), on the synchronisation context we captured when we created the AsyncOperation object. Thus, we are now back on the UI thread. So, when the lambda expression calls OnCompleted(), it is run on the UI thread. We also create a new WorkItemCompletedEventArgs instance to wrap our string result.

6) OnCompleted() then raises our Completed event (passing to it the WorkItemCompletedEventArgs instance containing our result), which calls item_Completed on the UI thread (remember, the form registered to be notified when the Completed event was raised).

7) As we are on the UI thread, item_Completed is free to update the UI without Invoke().

The beauty of that is that the UI knows nothing of how the WorkItem does its work, so it can just concentrate on handling the UI. Further, the WorkItem knows nothing of the UI, and so can be reused with an infinite number of different projects.

That is generally how all the event based asynchronous classes work (WebClient, BackgroundWorker etc), and is a nice, clear pattern to use to produce well designed, asynchronous software.


Example 2: Using new .NET 4.0 Task to perform operation on thread pool thread

I mentioned that the AsyncOperation represents our asynchronous operation. However, .NET 4.0 provides an optimised, easy to use class that abstracts the idea of an asynchronous operation further. It is the Task class, and is the central part of a new API called the Task Parallel Library (TPL).

We can implement the above pattern in a similar way, without events (albeit using the same general concept of callbacks). However, Tasks provide a number of beneficial, easy to use features that stand it apart from BackgroundWorker class, and the event based pattern in general. I wrote a basic introductory tutorial here.


Let me demonstrate by converting the above example to use the TPL:

//notice, this class is greatly simplified using Tasks
    public class WorkItem {
        
        public Task<string> DoWork() {
            //create task, of which runs our work of a thread pool thread
            return Task.Factory.StartNew<string>(this.PerformWork);
        }

        private string PerformWork() {
            Thread.Sleep(5000);//do work here...
            //return result of work
            return "Update with result!";
        }
    }



Notice how much simpler our WorkItem class is now.

Here is our form class:

public partial class Form1 : Form {

        public string Result { get { return this.txtResult.Text; } set { this.txtResult.Text = value; } }

        public Form1() {
            InitializeComponent();
        }

        private void btnStart_Click(object sender, EventArgs e) {
            //start work going, and register a call back using ContinueWith() that is called
            //when the work completes, of which updates the UI with the result
            this.StartBackgroundWork()
                                  .ContinueWith((t) => this.Result = t.Result, TaskScheduler.FromCurrentSynchronizationContext()); ;
        }

        private Task<string> StartBackgroundWork() {
            //create new work item, start work and return
            //the task representing the asynchronous work item
            return new WorkItem().DoWork();
        }
    }



Right, let's go through that now, starting at the WorkItem class this time...

DoWork() starts a Task (of which returns a string), of which grabs a background thread from the thread pool. That thread then runs PerformWork(), of which does the work and returns the resulting string, as before.

Notice, however, that DoWork() returns the Task<string> to the caller. So, we are returning our abstract asynchronous operation to the caller. However, this operation may (if now exceptions occur etc) produce a result in the future, and we want the UI to update itself with the result.

Well, the Task<string> representing the operation is returned to the UI, so it has access to it, but how do we get the result?

Easy! We register a callback that is run when the operation completes (this is essentially what we were doing with the Completed event in the event based example).

We do this by calling ContinueWith() on the Task<string>. The lambda passed to ContinueWith() will be called when the Task<string> completes, and that completed task will be passed to it, so we can get its result.

In that lambda, we update the UI with that result. However, notice this line:

TaskScheduler.FromCurrentSynchronizationContext();

Remember how we used an AsyncOperation object to capture the UI's synchronisation context so we could update the UI from the UI thread. Well, that is what that line is doing. It is saying, 'run this callback (represented by the lambda passed as the first arguement to ContinueWith()) on the current synchronisation context.' The current context is the UI thread, so that callback (and thus the code to update our textbox), is run on the UI thread, avoiding the need for Invoke().

So, the Task class provides a potentially easier alternative to using standard events. Plus, the Task class has been optimised to work with the thread pool (which will be more efficient than firing up a dedicated thread using the Thread class, as you can reuse previous threads that are sitting in the thread pool, instead of creating a brand new thread every time (which is expensive)).

In fact, the TPL (Task Parallel Library), of which Tasks are central to, is currently the only API that makes use of certain optimisations to the thread pool that the CLR team made. It offers more features the the BackgroundWorker class, and the event based pattern in general.


Example 3: Wrapping a none Task implementation in a Task

The Task API (TPL (Task Parallel Library)) is all well and good. However, sometimes, the thread pool (of which Tasks use) cannot really be used for your operations.

For example, if you have a loop that has many iterations (1000+), and each iteration fires up a Task that may take a while to complete, you run the risk of starving the thread pool, and running out of threads (I think the pool currently has a default maximum (in a 32-bit process) of around 2000 threads (this is always subject to change by Microsoft). You can change that, but it isn't usually a great idea to do so).

If you have a very long running task, it is potentially better, and more efficient to start a brand new, dedicated thread, rather than tie up a thread pool thread. However, you then lose the benefit of having the 'fluffy', simple to use Task object to work with.

So, what to do?

Well, you can use the Thread class to start work items going and do the work, but wrap the implementation in a Task, so that callers can work with a Task, thus getting the practical benefits of using the Thread class to run a long running piece of computationally intensive work, but getting the ease of use of the TPL API also!

To do this, here is the updated WorkItem class:

      public class WorkItem {
        //this produces a task for us
        private TaskCompletionSource<string> completionSource;

        public Task<string> DoWork() {
            //create a new source of tasks
            this.completionSource = new TaskCompletionSource<string>();
            //start work going using ***Thread class****...
            new Thread(this.PerformWork).Start();
            //...however, return a task from the source to the caller
            //so they get to work with the easy to use Task.
            //We are providing a Task facade around the operation
            //running on the dedicated thread
            return this.completionSource.Task;
        }

        private void PerformWork() {
            Thread.Sleep(5000);//do work here...
            //set result of the Task here, which completes the task
            //and thus schedules any callbacks the called registered
            //with ContinueWith to run
            this.completionSource.SetResult("Update with result!");
        }
    }



The Form class stays exactly as it was in the previous (Task) example, as we are still returning a Task<string> to the UI.

So, what are we doing here?

Well, when DoWork() is called, an new TaskCompletionSource<string> object is created. This object has the capability to produce a Task<string> object for us, on demand.

So, we start the work going on a background thread using the Thread class (nothing to do with the Task class), and we then grab a Task<string> from the TaskCompletionSource<string> object, and return that to the caller (the UI).

Therefore, the UI still has its Task<string> representing the operation, and it can register callbacks using ContinueWith() and do everything that the Task<T> class allows!

What about when the work is finished though? Even though the caller has a Task<string>, we aren't actually using a Task<string> to do our work. We are using the Thread class! So, how do we signal that the work has completed, allowing any ContinueWith() callbacks the calling Form class has registered to run?

Simple! We call SetResult() (passing in the hardcoded string result) on the TaskCompletionSource<string> object that produced the Task<string>. That transitions the Task<string> that the UI is working with into a 'completed' state, and schedules any registered callbacks to run!


I find this quite spectacular actually. The Task<T> class provides a very easy to use interface for us developers to interact with. Now, with TaskCompletionSource<string>, we can wrap any operation we want in a Task!

We can change the WorkItem class to use the Thread class behind the scenes, instead of the ThreadPool class, and the Form class would never know, as it would still be getting its Task<string> so it would be happy. Hell, we can even change the WorkItem class to use the BackgroundWorker class behind the scenes, or we could even run the work synchronously, with no extra threads at all, if we really wanted!


Quick note on I/O vs Compute Bound Operations

The BackgroundWorker class, and the Thread and ThreadPool classes are for compute bound operations. So, what if you have an I/O bound operation (reading from a file, for example)?

Well, firstly, note that running IO bound operations in a dedicated background thread is technically wasteful, as the executing thread just sits blocked, doing nothing, while the I/O subsystem completes the operation. Whether this is really a problem in reality will depend largely on the specific software you are developing (for example, a busy server application with thousands of concurrent users may benefit greatly from eliminating this waste, but a standalone desktop application running on a users PC may not benefit so much).

Anyway, it is because of this potential inefficiency, .NET provides methods that perform asynchronous I/O. This is asynchrony without the use of a dedicated thread, using I/O completion ports.

The key example of these methods are in the APM API. It uses pairs of methods that take the following pattern - BeginXXX()/EndXXX() to perform asynchronous I/O in the most efficent way possible (using I/O completion ports).

How can we use these with our Task's though? Well, we can use the technique shown in the previous example to wrap the APM pattern in a Task facade. This means we gain the efficiency benefits of asynchronous I/O for our I/O bound operation, but maintain many of the benefits of the TPL.

(Note: For a little more info on why you shouldn't technically use the a dedicated thread) for I/O bound operations, see here).


Happily, the TaskFactory<T> and TaskFactory classes have actually already had methods built in for this I/O bound scenario. The FromAsync() methods of those classes allow you to wrap I/O bound operations implementing the BeginXXX()/EndXXX() APM pattern in a Task facade automatically for you


FromAsync() uses TaskCompletionSource<T> behind the scenes to wrap the APM implementation in a Task object.

An introduction to the FromAsync() method is given here, and see here also.



Anyway, the point to take from this is that we can put a Task facade around any operation (I/O bound or compute bound), and this provides unbelievable flexibility, as it means you can use tasks for ANY arbitrary operation, and I believe this is what you should now do (if using .NET 4.0) when looking to introduce asynchrony into your applications!


Example 4: New Async CTP

I couldn't talk about this topic without briefly mentioning the new Async CTP (note that it is still only a CTP at the moment, so no guarantees are made with it, and you have to download the .dll to use it).

If (or rather, when) this pattern does become a mainstream part of the language, it's going to make things like this even more awesome! It is based heavily on the TPL, and so any Task implementation can be converted to use this pattern.

For example, both the ordinary Task implementation and the 'Task Wrapper' implementation (shown in example 2 and 3 respectively above) can be simplified further (and converted to the new async pattern) just by changing the btnStart_Click method handler to this:

private async void btnStart_Click(object sender, EventArgs e) {
     this.Result = await this.StartBackgroundWork();
}



No (visible) callbacks are now needed in your code, and no changes were made to any of the other code from example 2 or 3!. I simply inserted two strategically placed contextual keywords into the above method, and removed the ContinueWith() call.

That is the beauty of this pattern. You can hardly tell the difference in the calling code between synchronous and asynchronous code any more! The calling code is almost completely unaware that it is calling a method asynchronously!

In short, Tasks are the way .NET asynchrony is going, and I think you should first turn to them when looking to perform any asynchronous operation; compute bound (using standard tasks to grab a thread pool thread to run the work on) OR I/O bound (wrap the asynchronous I/O methods that exist in the framework in a Task facade. Actually, this is particularly relevant for APM pattern, which typically does not produce very readable code. The Task wrapper would add readability and ease of use :)).


Conclusion

So there you go, 4 different ways to achieve better designed (all demonstrating the same general pattern), loosely coupled asynchronous code going forward, and there is no Invoke() anywhere in sight :) It may not always be totally viable (or even necessary), but where it is, it is generally a good idea to try and decouple the caller (UI) from any meaningful operation running on a background thread.

The examples given are in the simplest form, but demonstrate the general tools and ideas available, of which you can adapt to your application.

So to summarize, generally (there will naturally be exceptions), I would use example 1's event based technique (potentially using the built in BackgroundWorker for compute bound operations, depending on the scenario) if I wasn't using .NET 4.0 (or greater). If I had .NET 4.0 (or greater) at my disposal, (at the moment) I would use the technique of creating tasks used in example 2 for compute bound operations, and example 3's technique for I/O bound operations. If and when the async CTP becomes a mainstream part of the language, I would use that for pretty much everything!

Further, using these techniques, you can change the UI (add new controls, take controls away, rename controls, even switch to a different UI technology altogether (switch from WinForms to WPF, for example)), and it won't effect the class containing the operation logic in any way!


Thanks for reading!

This post has been edited by CodingSup3rnatur@l-360: 30 December 2013 - 10:57 AM


Is This A Good Question/Topic? 13
  • +

Replies To: C# Multi-threading in a GUI Environment

#2 n8wxs  Icon User is offline

  • --... ...-- -.. . -. ---.. .-- -..- ...
  • member icon

Reputation: 972
  • View blog
  • Posts: 3,878
  • Joined: 07-January 08

Posted 12 September 2011 - 03:40 PM

WHEW!! :D
Was This Post Helpful? 0
  • +
  • -

#3 tlhIn`toq  Icon User is offline

  • Please show what you have already tried when asking a question.
  • member icon

Reputation: 5436
  • View blog
  • Posts: 11,659
  • Joined: 02-June 10

Posted 13 September 2011 - 07:58 AM

My apologies to everyone else but... This is the best tutorial I've read in a long time.

I think the rest of us are going to have to step-up our game after this.
Was This Post Helpful? 3
  • +
  • -

#4 CodingSup3rnatur@l-360  Icon User is offline

  • D.I.C Addict
  • member icon

Reputation: 991
  • View blog
  • Posts: 971
  • Joined: 30-September 10

Posted 14 September 2011 - 03:56 PM

I didn't expect quite that level of positive feedback! Thanks a lot, I appreciate it,

Anyway, you can take some credit for it, since it was you that gave me the idea to write it ;)

I personally cannot wait for the async/await pattern to become a main stream part of the language (as I am sure it will sometime soon). I think it is a massive step forward for asynchronous programming. It will allow us all to write cleaner, more concise, and, actually, more efficient asynchronous code much more easily. As I mentioned briefly in the tutorial, you don't even need to code any callbacks yourself. It is all done for you by the compiler!

This post has been edited by CodingSup3rnatur@l-360: 21 January 2013 - 06:33 AM

Was This Post Helpful? 0
  • +
  • -

#5 GirlCalledPete  Icon User is offline

  • New D.I.C Head

Reputation: 2
  • View blog
  • Posts: 7
  • Joined: 26-October 11

Posted 26 October 2011 - 05:21 AM

This is a really useful overview - I've been playing with this code today :)

It's nice to be able to get data processing done without having to worry about all those loose threads and invokes! :)

			
public Task<MyReturnType> DoWork(MySendType sendingData)
{   
   return Task.Factory.StartNew(() => PerformWork(sendingData));
}


Was This Post Helpful? 1
  • +
  • -

#6 mrdaniel  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 1
  • Joined: 07-May 14

Posted 07 May 2014 - 08:42 PM

allo author of article "c# multi-threading in GUI environment"

i'd tried to duplicate your event-based tutorial but got system null reference exception. Tried repositioning variables as local or global but did not solved the problem for the last
2 days before i write here.

the code snippet is minimalist and attached for your perusal and advise.
repeated below for your convenience:
thanks
daniel


===============
snippet 1 of 2:
mainThread.cs
nb: only related code is copied here.

/// variables
appAsyncOpEvtArgInvokeClass.Work asopeW;
private string 
	sResult {
		get {return "sResult";}
		set {sResult = value;}
	}
///functionality
asopeW = new appAsyncOpEvtArgInvokeClass.Work(); 
asopeW.completed += asyncWorkCompleted;
asopeW.requestToDoWork("sDummy");

/// the subscriber
public void asyncWorkCompleted(object oSender, appAsyncOpEvtArgInvokeClass.WorkCompleted_evtArg asopeWc){
    this.sResult = asopeWc.sResult;	
}

===============
snippet 2 of 2:
appAsyncOpEvtArgInvokeClass.cs
nb: only related code is copied here.
consists of 2 classes.
===============
/// Work class
/// variables
private System.ComponentModel.AsyncOperation asOp;
public event EventHandler<WorkCompleted_evtArg> completed;
public string sTextResult;

public Work(){}

public void workToDo(string sTextIn){
	this.asOp = AsyncOperationManager.CreateOperation(null);
	ThreadPool.QueueUserWorkItem( (o) => this.workToPerform(sTextIn));
}

void workToPerform(string sTextToPerform){
	sTextResult = sTextToPerform;
	this.workCompletedPost(sTextResult);
}

private void workCompletedPost(string sTextResultIn){
	asOp.PostOperationCompleted((o) => this.onCompleted(new WorkCompleted_evtArg(o.ToString())), sTextResultIn);
}

protected virtual void onCompleted(WorkCompleted_evtArg ea){
	EventHandler<WorkCompleted_evtArg> evtHandler = this.completed;
	try{
		if(evtHandler != null){
			if(ea != null)
				evtHandler.Invoke(this, ea);
			}
		}catch(Exception x){
	}
}

==================
/// EventArg class
public class WorkCompleted_evtArg:EventArg{
	public string sResult{get; set;}
	
	/// constructor 1
	public WorkCompleted_evtArg(string sResIn){
			this.sResult = sResIn;
	}
	
}

=================
public void requestToDoWork(string sTextIn){
	this.workToDo(sTextIn);	
}


///end of appAsyncOpEvtArgInvokeClass.cs






Was This Post Helpful? 0
  • +
  • -

Page 1 of 1