0

I am writing a wpf application to control an embedded device over TCP. Writing to the device is easy, but I am having trouble receiving packets.

I have created a tcp NetworkStream and am using NetworkStream.BeginRead() to listen for TCP data. However, once I have composed a full packet, I would like to update my GUI to reflect the new information. It seems that I am not allowed to do this from the asynchronous callback thread.

It appears that there is a way to do this through the dispatcher one request at a time, but I need to update practically every control on my GUI. I am not writing a dispatcher function for every packet case and every WPF control. How do I get complete access to the WPF controls from my asynchronous thread?

EDIT: Here is a code sample:

NetworkUnion union = (NetworkUnion)ar.AsyncState;
                union.BytesRead += tcpStream.EndRead(ar);

                if (union.BytesRead < union.TargetSize)
                    tcpStream.BeginRead(union.ByteArray, union.BytesRead, union.TargetSize - union.BytesRead, new AsyncCallback(ReadCommandCallback), union);
                else
                {
                    NetworkUnion payload = new NetworkUnion();
                    NetworkPacket pkt = (NetworkPacket)union.getArrayAsStruct();

                    // Respond to the packet
                    // Read the payload
                    payload.ByteArray = new byte[pkt.payloadSize];
                    tcpStream.Read(payload.ByteArray, 0, pkt.payloadSize);

                    // Determine what the payload is!
                    switch (pkt.code)
                    {
                        case (int)PacketCode.STATE:
                            payload.ObjectType = typeof(NetworkState);
                            NetworkState state = (NetworkState)payload.getArrayAsStruct();
                            Handle.fuelGauge.Value = Convert.ToDouble(state.mainFuel);
                            break;

When I try to update the fuel gauge, I get an InvalidOperationException, calling thread cannot access because another thread owns the object

The 'Handle' variable is used because this is from within a static utility class

The main question is, do I really have to replace every line of

Handle.fuelGauge.Value = Convert.ToDouble(state.mainFuel);

with

Handle.fuelGauge.Dispatcher.Invoke(
                                System.Windows.Threading.DispatcherPriority.Normal,
                                new Action(
                                    delegate()
                                    {
                                        Handle.fuelGauge.Value = Convert.ToDouble(state.mainFuel);
                                    }
                            ));

? It seems excessively repetitive. It would be easier to do something like:

Handle.mainWindow.Dispather.Lock();
Handle.fuelGauge.Value = Convert.ToDouble(state.mainFuel);
change everything else...
Handle.mainWindow.Dispatcher.Unlock();
AGuyInAPlace
  • 359
  • 4
  • 19
  • Have you tried using invoke or beinginvoke on the control you wish to update? – Botonomous Jun 01 '12 at 21:01
  • What have you tried? The problem that you mention does not exist in practice. Therefore, my guess is that you didn't even try. – usr Jun 01 '12 at 21:02
  • Looking into it now. Could you please provide a link or example? I'm still pretty new to this multithreaded thing :-) – AGuyInAPlace Jun 01 '12 at 21:03
  • @usr: I have added a code sample to my question – AGuyInAPlace Jun 01 '12 at 21:06
  • I guess you need to google for the error message to get your answer. It is a common problem. – usr Jun 01 '12 at 21:10
  • Show us your code where you are trying to update your control please. – Botonomous Jun 01 '12 at 21:13
  • @Anon, usr: The solution is simple using a new delegate and Dispatcher.Invoke call, but I have close to 50 unique messages and packets. Do I really have to make a custom delegate and invoker for every single event? That seems really repetitive, unless I'm just not using them correctly – AGuyInAPlace Jun 01 '12 at 21:14
  • 1
    @AGuyInAPlace Then it would seem the method your using to create the packet is not generic enough? Are you saying you have 50+ unique method calls? If you have one or two generic method, you can use the same invoker to call back to the control. – Botonomous Jun 01 '12 at 21:19

2 Answers2

1

Generally, in .NET, you need to marshal UI requests back to the main UI thread. This is a common scenario when trying to update UI after a network read, because when using Sockets, you are usually always receiving data on some other (threadpool) thread.

So, in your code which receives the data, you should wrap up the bytes into some useful object, and marshal that back to an entry point method on the main UI thread which can then take responsibility for farming out UI updates, on the now correct thread.

There are many ways to do this, but perhaps a more flexible approach than some, would be for your callers of the socket code to pass in a SynchronizationContext object, which can then be used to marshal the received data to.

See here for more info on SynchronizationContext

You won't need to post back every socket call, just move your swithc statement, that decides how to handle a packet, into a class on the UI thread, and invoke to that method. Then, that method can do UI updates as normal, as all calls from it are already marshalled correctly.

RJ Lohan
  • 6,497
  • 3
  • 34
  • 54
  • Aha! This sounds like exactly what I'm trying to do. Give me a few to play around with SynchronizationContext... – AGuyInAPlace Jun 01 '12 at 21:20
  • Searching for information about SynchronizationContext brought me to this thread: (http://stackoverflow.com/questions/1949789/using-synchronizationcontext-for-sending-events-back-to-the-ui-for-winforms-or-w). As per Ray's advice, I am using a single dispatcher call to pass my packet to a method inside the UI thread, where the packet is received and the UI is updated as desired. However, after an arbitrary number of packets, I receive System.Reflection.TargetInvocationException. I can't catch it because it is thrown from outside my code. My best guess is some sort of race condition? What now? – AGuyInAPlace Jun 01 '12 at 21:56
  • Look at the .InnerException property of that exception to get more info about what has happened. Likely, there is a problem in your UI code, and this bubbles up through to the SynchronizationContext.Post call (and is wrapped in a TargetInvocationException). Also, add some exception handling in the source of your .Post callback to make sure the exception doesn't break your socket handling. – RJ Lohan Jun 01 '12 at 21:58
  • That's the strange part; I can't seem to catch this exception anywhere. I wrapped the entire socket thread in try/catch, as well as the UI code. Without catching it, I can't examine the inner data – AGuyInAPlace Jun 01 '12 at 22:25
  • 1
    Aha! Found it! One of the variables in my switch statement was being used uninitialized. Fixed it up, and everything works like a top! Thanks! – AGuyInAPlace Jun 01 '12 at 22:36
0

No you dont need to replace every line like you said. How about something like this:

NetworkUnion union = (NetworkUnion)ar.AsyncState;
            union.BytesRead += tcpStream.EndRead(ar);

            if (union.BytesRead < union.TargetSize)
                tcpStream.BeginRead(union.ByteArray, union.BytesRead, union.TargetSize - union.BytesRead, new AsyncCallback(ReadCommandCallback), union);
            else
            {
                NetworkUnion payload = new NetworkUnion();
                NetworkPacket pkt = (NetworkPacket)union.getArrayAsStruct();

                // Respond to the packet
                // Read the payload
                payload.ByteArray = new byte[pkt.payloadSize];
                tcpStream.Read(payload.ByteArray, 0, pkt.payloadSize);

                // Determine what the payload is!
                double yourvalue = new double(); 
                switch (pkt.code)
                {
                    case (int)PacketCode.STATE:
                        payload.ObjectType = typeof(NetworkState);
                        NetworkState state = (NetworkState)payload.getArrayAsStruct();
                        yourvalue = Convert.ToDouble(state.mainFuel);
                        break;
            /**
                Other Cases....

            */
         // After the switch 

                    Handle.fuelGauge.Dispatcher.Invoke(
                            System.Windows.Threading.DispatcherPriority.Normal,
                            new Action(
                                delegate()
                                {
                                    Handle.fuelGauge.Value = yourvalue;
                                }
                        )); 
Botonomous
  • 1,746
  • 1
  • 16
  • 39
  • This is a better option, but still requires yourvalue00 through yourvalue52 to be previously declared. Lohan's solution seems more appropriate. – AGuyInAPlace Jun 01 '12 at 21:57