WCF Callback locking up if called in same thread as current context - c#

I hope I can word this correctly. I have a WCF Service that I'm using (duplex channel communications) in which one client registers with the service. The service's registration method returns a value. I want the the method of the called service registration method to also call the callback method that will send out notification of the client registration (I have my reasons for this and explaining it here will only confuse the issue). The problem is that the client's implemented callback has to run in the main application thread to work correctly (due mostly to integration with a third-party application). The service registration method call is also occuring in this same thread, so it effectively locks up since the client is looking for a return from the service registration method holding on to the thread preventing the callback method from being able to run. If I tell it to call all callback methods for all contexts other than the one just registered, it works just fine. But if I tell it to include it, obviously it locks up because that thread is already locked up. I can set the callback attribute property for UseSynchronizationContext to false, but then this means the callback method is called on a separate thread from the main and now the rest of the program will not work. Any help would be greatly appreciated.
Here's basically that registration method (first draft..)
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession,
UseSynchronizationContext = false,
ConcurrencyMode = ConcurrencyMode.Multiple,
Namespace = "http://MyApp/Design/CADServiceTypeLibrary/2012/12")]
public class DTOTransactionService : IDTOTransactionService, IDisposable
{
//some more stuff
public CADManager RegisterCADManager(int processID, bool subscribeToMessages)
{
List<CADManager> cadMgrs = this.CADManagers;
bool registered = false;
//Create new CADManager mapped to process id
CADManager regCADManager = new CADManager(processID);
//Add to CADManagers List and subscribe to messages
if (regCADManager.IsInitialized)
{
cadMgrs.Add(regCADManager);
this.CADManagers = cadMgrs;
//Subscribe to callbacks
if (subscribeToMessages)
SubscribeCallBack(regCADManager.ID);
registered = true;
}
//Send registration change notification
RegistrationState state;
if (registered)
state = RegistrationState.Registered;
else
state = RegistrationState.RegistrationException;
foreach (CallBackSubscriber subscriber in this.CallBackSubscribers)
{
subscriber.CallBackProxy.CADManagerRegistrationNotification(regCADManager.ID, state);
}
return regCADManager;
}
}

I think I've got it figured out. It struck me that a little deeper what's happening is that since the call to the service method is expecting a return value and since the callback will occur in the same thread as the client method expecting a return value that this could be the result of the deadlock condition. I then decided to try calling the callback methods in the service using a different thread to work around the current thread condition. In other words, work around the current thread whose method has yet to have provided a return value from the service method. It worked! Was this the right approach? I have enough experience to be dangerous here, so if someone else's experience shows this to be the wrong way to handle this, I'm all ears.
Thread notifyThread = new Thread(delegate()
{
foreach (CallBackSubscriber subscriber in this.CallBackSubscribers)
{
subscriber.CallBackProxy.CADManagerRegistrationNotification(regCADManager.ID, state);
}
});
Update:
Yes, the threading and deadlock condition was the issue, however the more appropriate fix I recently discovered is to use SynchronizationContext. To use, create a property or field of the type SynchronizationContext, then assign the value to the field/property while in the context you wish to capture using SynchronizationContext.Current. Then, use the Post() method (providing it a delegate via the SendOrPostCallback object) in the callback method being called by the Service. A short example:
private SynchronizationContext _appSyncContext = null;
private DTOCommunicationsService()
{
this.AppSyncContext = SynchronizationContext.Current;
//Sets up the service proxy, etc, etc
Open();
}
// Callback method
public void ClientSubscriptionNotification(string clientID, SubscriptionState subscriptionState)
{
SendOrPostCallback callback = delegate(object state)
{
object[] inputArgs = (object[])state;
string argClientID = (string)inputArgs[0];
SubscriptionState argSubState = (SubscriptionState)inputArgs[1];
//Do stuff with arguments
};
_appSyncContext.Post(callback, new object[] { clientID, subscriptionState });
}

Related

Subsequent WCF calls cause blocking with unknown reason and finally cause TimeoutException?

I'm not sure why it could be so. But I'm sure if using asynchronous calls, there won't be no blocking.
The scenario here is I have 2 WCF method calls, the first one will trigger some callback call to the client (using CallbackContract). The second one is just a normal WCF method call (even an empty method having no code at all).
The methods content is not important, here is just some kind of pseudo code:
public void FirstMethod(){
//some logic here...
//here I use some Callback method to client side
clientCallbackInterface.SomeMethod();//commenting this out won't
//cause any blocking.
}
public void SecondMethod(){
//this is even empty
}
//call the 2 methods synchronously in a sequence
client.FirstMethod();
client.SecondMethod();
Without calling the SecondMethod, it runs just fine. If using asynchronous calls, it also runs just fine. Or if I comment out the call (using the client callback interface), it will also run just fine.
At the time the exception TimeoutException is thrown, it shows that the method SecondMethod is actually done and in the phase of responding to client.
The ServiceBehavior has InstanceContextMode of PerSession and ConcurrencyMode of Multiple.
I hope someone here has experienced with this and understands the cause behind this issue.
UPDATE:
I've just tried a new thing by setting ConcurrencyMode to Single instead and it also run just fine. So I would like to know more on how to make it run fine with ConcurrencyMode of Multiple?
UPDATE:
I'm really confused about what is wrong here, in fact there is some old code which does not even use CallbackBehavior and it simply works fine with ConcurrencyMode of Multiple. While my code need CallbackBehavior and it failed at the second time of executing the 2 methods. Here is the minimum code I can post, I've tried it and the method content does not really matter, it can be just empty:
//the service interface
[ServiceContract(CallbackContract = typeof(IMyClient), SessionMode = SessionMode.Allowed)]
public interface IMyService
{
bool MyMethod();
}
//the client callback interface
public interface IMyClient
{
[OperationContract(IsOneWay = true)]
void OnSomething(SomeEventArgs e);
}
//the service class
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Multiple)]
[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, UseSynchronizationContext = false)]
public class MyService : IMyService
{
static Dictionary<ClientInfo, IMyClient> clients;
static Dictionary<ClientInfo, IMyClient> Clients
{
get
{
if (clients == null)
clients = new Dictionary<ClientInfo, IMyClient>();
return clients;
}
}
static void raiseEvents(Action<IMyClient> raiser, params Guid[] toClients)
{
if (raiser == null)
throw new ArgumentNullException("raiser cannot be null.");
System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(o => {
lock (clients)
{
//ClientInfo is just some class holding some info about
//the client such as its ClientGuid
Func<KeyValuePair<ClientInfo, IMyClient>, bool> filter = c => toClients.Length == 0 || toClients.Any(e => e == c.Key.ClientGuid);
foreach (var client in Clients.Where(filter).Select(e => e.Value))
{
raiser(client);
}
}
}));
}
public bool MyMethod(){
//do nothing before trying to trigger some callback to the client
raiseEvents(e => e.OnSomething(new SomeEventArgs()));
return true;
}
}
The raiseEvents method above in fact is what I followed the old code (as I mentioned which works just fine), as before it looks much simpler like this (and also does not work):
static void raiseEvents(Action<IMyClient> raiser, params Guid[] toClients)
{
if (raiser == null)
throw new ArgumentNullException("raiser cannot be null.");
Func<KeyValuePair<ClientInfo, IPosClient>, bool> filter = c => toClients.Length == 0 || toClients.Any(e => e == c.Key.ClientGuid);
foreach (var client in Clients.Where(filter).Select(e => e.Value))
{
Task.Run(() => raiser(client));
}
}
One possible difference between the old code and the one I'm writing is in the configuration file but I'm not really sure which could lead to this issue. In fact I've tried cloning the configuration as much as I can (about the <behaviors>).
As initially described there are 2 methods involved here. However this time I have just 1 method as in the code. The first time it's called OK, the next time calling it will freeze the UI (like as having some deadlock). Calling it is just simple when you have the client proxy class (which is auto-generated by the Add Service Reference wizard):
//this is put in some Execute method of some Command (in WPF)
myServiceClient.MyMethod();
In fact I can work-around this issue by using the async version of MyMethod or simply put that call in a thread but the old code does not need to do that and I'm really curious about why it works the first time but keeps freezing (until TimeoutException is thrown) the next time.
By default, WCF will execute callback on current SynchronizationContext. That means when you call WCF service from UI thread in for example WPF or WinForms application - callback will also be executed on UI thread. But this thread is already blocked by your call to service and so your call to service and service's callback to client will deadlock. First - don't call remote services from UI thread, that bad from user experience point of view anyway (your interface will freeze while waiting for result of the call). But if you still do that, at least tell WCF to not use current synhronization context for callbacks, by using CallbackBehavior attribute:
[CallbackBehavior(ConcurrencyMode=ConcurrencyMode.Reentrant, UseSynchronizationContext=false)]
class Callback : IClientCallback
{
}

Parallelize / Multi-Thread a singleton object method call

Code Details:
// Singleton class CollectionObject
public class CollectionObject
{
private static CollectionObject instance = null;
// GetInstance() is not called from multiple threads
public static CollectionObject GetInstance()
{
if (CollectionObject.instance == null)
CollectionObject.instance = new CollectionObject();
return CollectionObject.instance;
}
// Dictionary object contains Service ID (int) as key and Service object as the value
// Dictionary is filled up during initiation, before the method call ReadServiceMatrix detailed underneath
public Dictionary<int, Service> serviceCollectionDictionary = new Dictionary<int,Service>();
public Service GetServiceByIDFromDictionary(int servID)
{
if (this.serviceCollectionDictionary.ContainsKey(servID))
return this.serviceCollectionDictionary[servID];
else
return null;
}
}
DataTable serviceMatrix = new DataTable();
// Fill serviceMatrix data table from the database
private int ReadServiceMatrix()
{
// Access the Singleton class object
CollectionObject collectionObject = CollectionObject.GetInstance();
// Parallel processing of the data table rows
Parallel.ForEach<DataRow>(serviceMatrix.AsEnumerable(), row =>
{
//Access Service ID from the Data table
string servIDStr = row["ServID"].ToString().Trim();
// Access other column details for each row of the data table
string currLocIDStr = row["CurrLocId"].ToString().Trim();
string CurrLocLoadFlagStr = row["CurrLocLoadFlag"].ToString().Trim();
string nextLocIDStr = row["NextLocId"].ToString().Trim();
string nextLocBreakFlagStr = row["NextLocBreakFlag"].ToString().Trim();
string seqStr = row["Seq"].ToString().Trim();
int servID = Int32.Parse(servIDStr);
int currLocID = Int32.Parse(currLocIDStr);
int nextLocID = Int32.Parse(nextLocIDStr);
bool nextLocBreakFlag = Int32.Parse(nextLocBreakFlagStr) > 0 ? true : false;
bool currLocBreakFlag = Int32.Parse(CurrLocLoadFlagStr) > 0 ? true : false;
int seq = Int32.Parse(seqStr);
// Method call leading to the issue (definition in Collection Object class)
// Fetch service object using the Service ID from the DB
Service service = collectionObject.GetServiceByIDFromDictionary(servID);
// Call a Service class method
service.InitLanes.Add(new Service.LaneNode(currLoc.SequentialID, currLocBreakFlag, nextLoc.SequentialID, nextLocBreakFlag, seq));
}
Issue that happens is:
In the code above for all the Service objects in the dictionary, the subsequent method call is not made, leading to issues in further processing. It has to o with fetching the Service object from the dictionary in parallel mode
The db an dictionary contains all the Ids /Service objects, but my understanding is when processing in Parallel mode for the Singleton class, few of the objects are skipped leading to the issue.
In my understanding the service id passed and service object created is local to a thread, so there shouldn't be an issue that I am facing. This kind of issue is only possible, when for a given method call one thread replace service id value of another thread by its, thus both end up with Service object and few are thus skipped, which is strange in my view until and unless I do not understand the Multi threading in this case correctly
Currently I am able to run the same code in non threaded mode by using the foreach loop instead of Parallel.ForEach / Parallel.Invoke
Please review and let me know your view or any pointer that can help me resolve the issue
In my understanding the service id passed and service object created
is local to a thread
Your understanding is incorrect, if two threads request the same service id the two threads will be both working on the same singular object. If you wanted separate objects you would need to put some kind of new Service() call in GetServiceByIDFromDictionary instead of a dictionary of existing values.
Because multiple threads could be using the same service objects I think your problem lies from the fact that service.InitLanes.Add is likely not thread safe.
The easiest fix is to just lock on that single step
//...SNIP...
Service service = collectionObject.GetServiceByIDFromDictionary(servID);
// Call a Service class method, only let one thread do it for this specific service instance,
// other threads locking on other instances will not block, only other threads using the same instance will block
lock(service)
{
service.InitLanes.Add(new Service.LaneNode(currLoc.SequentialID, currLocBreakFlag, nextLoc.SequentialID, nextLocBreakFlag, seq));
}
}
This assumes that this Parallel.Foreach is the only location collectionObject.GetServiceByIDFromDictionary is used concurrently. If it is not, any other locations that could potentially be calling any methods on returned services must also lock on service.
However if Service is under your control and you can somehow modify service.InitLanes.Add to be thread safe (perhaps change InitLanes out with a thread safe collection from the System.Collections.Concurrent namespace) that would be a better solution than locking.
1.Implementing singleton always think about using of it in mulithreaded way. Always use multithreaded singleton pattern variant, one of them - lazy singleton. Use Lazy singleton using System.Lazy with appropriate LazyThreadSafeMode consturctor argument:
public class LazySingleton3
{
// static holder for instance, need to use lambda to enter code here
//construct since constructor private
private static readonly Lazy<LazySingleton3> _instance
= new Lazy<LazySingleton3>(() => new LazySingleton3(),
LazyThreadSafeMode.PublicationOnly);
// private to prevent direct instantiation.
private LazySingleton3()
{
}
// accessor for instance
public static LazySingleton3 Instance
{
get
{
return _instance.Value;
}
}
}
Read about it here
2.Use lock-ing of your service variable in parallel loop body
// Method call leading to the issue (definition in Collection Object class)
// Fetch service object using the Service ID from the DB
Service service = collectionObject.GetServiceByIDFromDictionary(servID);
lock (service)
{
// Call a Service class method
service.InitLanes.Add(new Service.LaneNode(currLoc.SequentialID,
currLocBreakFlag, nextLoc.SequentialID,
nextLocBreakFlag, seq));
}
3.Consider to use multithreading here. Using lock-ing code make your code not so perfomant as synchronous. So make sure you multithreaded/paralelised code gives you advantages
4.Use appropriate concurrent collections instead of reinventing wheel - System.Collections.Concurrent Namespace

Returning a value from async callback in WCF

My WPF application with the MVVM pattern shall basically perform the following:
Button view binds to a command in the view model. --> Check!
Command in view model asynchronously queries a webservice for a list of CProject objects for putting it into a ProjectList property. --> Check!
This looks like this...
Command in the view model:
proxy = new SomeService();
proxy.GetProjectList(GetProjectListCallback, username, password);
Callback in the view model:
private void GetProjectListCallback(object sender, GetProjectListCompletedEventArgs e) {
this.ProjectList = e.Result;
}
SomeService implements an interface ISomeService.
public void GetProjectList(EventHandler<GetProjectListCompletedEventArgs> callback, string username, string password) {
service.GetProjectListCompleted += callback;
service.GetProjectListAsync(username, password);
}
So far this works fine. However I feel that I would like to move this callback to the service itself so that the view model only calls something like:
proxy = new SomeService();
this.ProjectList = proxy.GetProjectList(username, password);
But when moving the callback to the service how could it return e.Result to the calling view model? Or would using a Task a better idea?
The easiest way is to re-create your WCF service proxy with VS2012. This will change your asynchronous method signatures to be something like:
Task<MyProjectList> GetProjectListAsync(string username, string password);
and your command becomes:
proxy = new SomeService();
this.ProjectList = await proxy.GetProjectListAsync(username, password);
If you don't want to re-create your WCF service proxy (it will update all your method signatures), then you can wrap the Begin*/End* methods as such:
public static Task<MyProjectList> GetProjectListTaskAsync(this SomeService #this, string username, string password)
{
return Task<MyProjectList>.Factory.FromAsync(#this.BeginGetProjectList, #this.EndProjectList, username, password, null);
}
I have a full example of this kind of wrapping on my blog.
or the existing *Async/*Completed members as such:
public static Task<MyProjectList> GetProjectListTaskAsync(this SomeService #this, string username, string password)
{
var tcs = new TaskCompletionSource<MyProjectList>();
EventHandler<GetProjectListCompletedEventArgs> callback = null;
callback = args =>
{
#this.GetProjectListCompleted -= callback;
if (args.Cancelled) tcs.TrySetCanceled();
else if (args.Error != null) tcs.TrySetException(args.Error);
else tcs.TrySetResult(args.Result);
};
#this.GetProjectListCompleted += callback;
#this.GetProjectListAsync(username, password);
}
Unfortunately you can't return a value from an asynchronous operation like that - you'd have to block that thread to wait for it to complete, which rather defeats the object of what you're doing. You always need a callback or continuation of some kind to run when the result is available. In C# 5 the async/await syntax does a lot of the plumbing for that for you, but ultimately it's still doing that under the hood, using things like Task.ContinueWith.
Without involving TPL and without having a C# 5 compiler available, the pattern you're using at the moment looks like a good one to me, if you're happy with the asynchronous operations that WCF provides.
In my own code I've previously built things a bit differently - using synchronous WCF operations called from threads on the threadpool, with the concurrency and callbacks managed by Reactive Extensions. However, the effect is much the same, it's all down to what kind of syntax and conceptual model you want. Using Rx is a natural fit for an application which is already built using a lot of Rx kit, because it keeps us in the same domain of IObservable being how data gets moved around.

State Machine Question

There is a State Machine with two Transitions in the same function.
static readonly object _object = new object();
lock (_object)
{
// I want Host received the event of SMTrans01 first .
Obj.StateMachine.Send((int)MyStateMachine.EventType.SMTrans01, new object[2] { Obj, MyStateMachine.EventType.SMTrans01 });
}
lock (_object)
{
// And then I want Host received the event of SMTrans02.
Obj.StateMachine.Send((int)MyStateMachine.EventType.SMTrans02, new object[2] { Obj, MyStateMachine.EventType.SMTrans02 });
}
I implemented my state machine code as above. I am not sure I understand Lock statement correctly or not?
I need the events followed the the right order (Host received SMTrans01 first, and then host received SMTrans02 event).
After testing I found sometime host will receive the SMTrans02 event first. Looks like Lock statement not work. I don't know why.
Are there any good methods to approach it?
It looks like your problem has nothing to do with threads and locking.
I suspect that your Send method is asynchronous. The solution is to use a synchronous approach instead. Do not send the second event until you get an acknowledgement that the first event has been handled.
Alternatively, rewrite your receiving code so that it can handle the events coming out of order.
If you provide the code for Send and describe more how your method is being called that would help debugging the problem.
if the order matters
EventType _state = EventType.SMTrans02;
if(_state == EventType.SMTrans02 )
{
_state =EventType.SMTrans01;
Obj.StateMachine.Send((int)_state, new object[2] { Obj, _state });
}
else
{
_state = EventType.SMTrans02;
Obj.StateMachine.Send((int)_state, new object[2] { Obj, _state });
}
for more complex situation you may use switch block or even use State Pattern
you need Lock only to sync the threads which may call those events.

How to update textbox on GUI from another thread [duplicate]

This question already has answers here:
How do I update the GUI from another thread?
(47 answers)
Closed 5 years ago.
I'm new with C# and I'm trying to make a simple client server chat application.
I have RichTextBox on my client windows form and I am trying to update that control from server which is in another class. When I try to do it I get the error: "Cross-thread operation not valid: Control textBox1 accessed from a thread other than the thread it was created on".
Here the code of my Windows form:
private Topic topic;
public RichTextBox textbox1;
bool check = topic.addUser(textBoxNickname.Text, ref textbox1, ref listitems);
Topic class:
public class Topic : MarshalByRefObject
{
//Some code
public bool addUser(string user, ref RichTextBox textBox1, ref List<string> listBox1)
{
//here i am trying to update that control and where i get that exception
textBox1.Text += "Connected to server... \n";
}
So how to do that? How can I update the textbox control from another thread?
I'm trying to make some basic chat client/server application using .net remoting.
I want to make windows form client application and console server application as separate .exe files. Here im trying to call server function AddUser from client and i want to AddUser function update my GUI. Ive modified code as you suggested Jon but now instead of cross-thread exception i've got this exception ... "SerializationException: Type Topic in Assembly is not marked as serializable".
Ill post my whole code bellow, will try to keep it simple as possible.
Any suggestion is welcome. Many thanks.
Server:
namespace Test
{
[Serializable]
public class Topic : MarshalByRefObject
{
public bool AddUser(string user, RichTextBox textBox1, List<string> listBox1)
{
//Send to message only to the client connected
MethodInvoker action = delegate { textBox1.Text += "Connected to server... \n"; };
textBox1.BeginInvoke(action);
//...
return true;
}
public class TheServer
{
public static void Main()
{
int listeningChannel = 1099;
BinaryServerFormatterSinkProvider srvFormatter = new BinaryServerFormatterSinkProvider();
srvFormatter.TypeFilterLevel = TypeFilterLevel.Full;
BinaryClientFormatterSinkProvider clntFormatter = new BinaryClientFormatterSinkProvider();
IDictionary props = new Hashtable();
props["port"] = listeningChannel;
HttpChannel channel = new HttpChannel(props, clntFormatter, srvFormatter);
// Register the channel with the runtime
ChannelServices.RegisterChannel(channel, false);
// Expose the Calculator Object from this Server
RemotingConfiguration.RegisterWellKnownServiceType(typeof(Topic),
"Topic.soap",
WellKnownObjectMode.Singleton);
// Keep the Server running until the user presses enter
Console.WriteLine("The Topic Server is up and running on port {0}", listeningChannel);
Console.WriteLine("Press enter to stop the server...");
Console.ReadLine();
}
}
}
}
Windows form client:
// Create and register a channel to communicate to the server
// The Client will use the port passed in as args to listen for callbacks
BinaryServerFormatterSinkProvider srvFormatter = new BinaryServerFormatterSinkProvider();
srvFormatter.TypeFilterLevel = TypeFilterLevel.Full;
BinaryClientFormatterSinkProvider clntFormatter = new BinaryClientFormatterSinkProvider();
IDictionary props = new Hashtable();
props["port"] = 0;
channel = new HttpChannel(props, clntFormatter, srvFormatter);
//channel = new HttpChannel(listeningChannel);
ChannelServices.RegisterChannel(channel, false);
// Create an instance on the remote server and call a method remotely
topic = (Topic)Activator.GetObject(typeof(Topic), // type to create
"http://localhost:1099/Topic.soap" // URI
);
private Topic topic;
public RichTextBox textbox1;
bool check = topic.addUser(textBoxNickname.Text,textBox1, listitems);
You need to either use BackgroundWorker, or Control.Invoke/BeginInvoke. Anonymous functions - either anonymous methods (C# 2.0) or lambda expressions (C# 3.0) make this easier than it was before.
In your case, you can change your code to:
public bool AddUser(string user, RichTextBox textBox1, List listBox1)
{
MethodInvoker action = delegate
{ textBox1.Text += "Connected to server... \n"; };
textBox1.BeginInvoke(action);
}
A few things to note:
To conform with .NET conventions, this should be called AddUser
You don't need to pass the textbox or listbox by reference. I suspect you don't quite understand what ref really means - see my article on parameter passing for more details.
The difference between Invoke and BeginInvoke is that BeginInvoke won't wait for the delegate to be called on the UI thread before it continues - so AddUser may return before the textbox has actually been updated. If you don't want that asynchronous behaviour, use Invoke.
In many samples (including some of mine!) you'll find people using Control.InvokeRequired to see whether they need to call Invoke/BeginInvoke. This is actually overkill in most cases - there's no real harm in calling Invoke/BeginInvoke even if you don't need to, and often the handler will only ever be called from a non-UI thread anyway. Omitting the check makes the code simpler.
You can also use BackgroundWorker as I mentioned before; this is particularly suited to progress bars etc, but in this case it's probably just as easy to keep your current model.
For more information on this and other threading topics, see my threading tutorial or Joe Albahari's one.
Use Invoke method
// Updates the textbox text.
private void UpdateText(string text)
{
// Set the textbox text.
yourTextBox.Text = text;
}
Now, create a delegate that has the same signature as the method that was previously defined:
public delegate void UpdateTextCallback(string text);
In your thread, you can call the Invoke method on yourTextBox, passing the delegate to call, as well as the parameters.
yourTextBox.Invoke(new UpdateTextCallback(this.UpdateText),
new object[]{”Text generated on non-UI thread.”});

Categories