5 Replies - 415 Views - Last Post: 23 January 2013 - 12:46 PM Rate Topic: -----

#1 Pademelon  Icon User is offline

  • New D.I.C Head

Reputation: 1
  • View blog
  • Posts: 6
  • Joined: 20-January 13

Form instanced SerialManager - rx event can't update textbox on Fo

Posted 21 January 2013 - 04:03 AM

Hello everyone,

This is my first post after registering today. I've been reading bits and pieces on this site for a while now, and thought I'd take a crack at trying to apply seperating the form logic from the communications logic from a serial communications program called SimpleSerial (http://csharp.simpleserial.com/) using the "Quick and easy custom events" tutorial.

So what I did was create a new class called SerialManager and moved all the comms specific stuff to the new class, then added the events tutorial code to the bottom. I then create an event like so: RaiseFeedback("Opening Com Port"); with Form1 subscribing to this event.

Form1 has:
    public partial class Form1 : Form
    {
        // Add this variable 
        SerialManager Arduino;

        public Form1()
        {
            InitializeComponent();
            DebugText("Hello");
            Arduino = new SerialManager();
            Arduino.Feedback += new EventHandler<TextArgs>(Feedback_Received);
            Arduino.Open();
        }

        public void DebugText(string p)
        {
            textBox1.AppendText(p);
        }

        void Feedback_Received(object sender, TextArgs e)
        {
            if (e != null && e.Message != null)
                textBox1.AppendText(e.Message);
        }



plus a bit more, that's not relevant to the issue at hand.

SerialManager has the following:
    public partial class SerialManager
    {
        private SerialPort serialPort1;

        public SerialManager()
        {

            serialPort1 = new SerialPort();
            serialPort1.DataReceived += new SerialDataReceivedEventHandler(onSerialDataReceived); 

        }

        public void Open()
        {
            RaiseFeedback("Opening Com Port");
            serialPort1.PortName = "COM6";
            serialPort1.BaudRate = 9600;


The RaiseFeedback("Opening Com Port"); works fine and I get this on Form1.
The problem comes in the onSerialDataReceived event of the comms port.
I've added a bunch of comments from what I learned in the code below:
        private string rxString;
        private void onSerialDataReceived(object sender, SerialDataReceivedEventArgs e)
        {

            rxString = serialPort1.ReadExisting();

            // serialPort1 runs in it own separate thread behind the scenes.
            // This thread cannot directly call any functions in the main thread of our application.
            // However, a special function, Invoke( ), will allow it.
            // So we use Invoke to call our DisplayText( ) function. RxString is the global string variable accessable by both threads

            // Original SimpleSerial way of writing to the textBox1 on Form1.
            this.Invoke(new EventHandler(DisplayText));

            // this = SerialManager not Form1 in this case and Invoke is not defined

            // Only works if:-
            // public partial class SerialManager : Form1 & SerialManager is called as a method - not as a constructor of the instance SerialManager


            // Adding this causes the following error on Form1:
            // Cross-thread operation not valid: Control 'textBox1' accessed from a thread other than the thread it was created on. 
            //
            RaiseFeedback(rxString);  
        }

        // This works in the context of public partial class SerialManager : Form1, but won't work if this is an instance
        // I don't want to do this, I want to use the event (below) as I may not be sending data to a form eventually.
        private void DisplayText(object sender, EventArgs e)
        {
            //textBox1.AppendText(RxString);
        }


So the comms port RX event is running on a different thread. Everything I read so far has the communications code running in the form class and uses this.Invoke(new EventHandler(DisplayText)); or some slight variation to get the text back on the forms thread. But I haven't found anything that makes the whole serial communications code run as a completely seperate class that is instanced from the form.

If I make SerialManager a method that is part of the Form1 class, and do ths same thing SimpleSerial does, I can get it to work, but the whole point of the exercise was to make it a completely seperate class that wasn't tightly coupled to the form.

I don't know if Form1 is running on 1 thread, SerialManager is running on another and the rx event is running on yet another or if both SerialManager and rx event are on the same thread.

In the end, the rx data from the serial port may go to another class that does "stuff" with it and not a form at all, so I really don't want to link the SerialManager class with From1 at the SerialManager level, brcause Form1 will untimately go away.

I tried having the rx event call DisplayText and DisplayText then raise the event back to From1, which just gave me a stack overflow for my trouble. I tried reading a couple of the multi-threading communications tutorial, but they are way over my head.

So now I really don't know where to turn next. There must be a simple way to do this, but I can't figure it out.

Any help would be appreciated.

Is This A Good Question/Topic? 0
  • +

Replies To: Form instanced SerialManager - rx event can't update textbox on Fo

#2 CodingSup3rnatur@l-360  Icon User is offline

  • D.I.C Addict
  • member icon

Reputation: 982
  • View blog
  • Posts: 969
  • Joined: 30-September 10

Re: Form instanced SerialManager - rx event can't update textbox on Fo

Posted 21 January 2013 - 04:37 AM

Hi,

The problem is that event handler methods (like your Feedback_Received method) run on whichever thread the associated event was raised on. Therefore, your Feedback_Received method will run on the same thread as the onSerialDataReceived method. Further, as that thread is different from the UI thread that your UI controls were created on, accessing the UI controls from the Feedback_Received method will cause a cross thread operation error.

Therefore, the simplest way to solve this problem is to marshal the UI updates (like textBox1.AppendText()) back onto the UI thread.

To do this, you can use Invoke():

if (e != null && e.Message != null)
      this.textBox1.Invoke(new MethodInvoker(() => this.textBox1.AppendText("test")));



That will ensure the text box is updated on the UI thread it was created on, thus avoiding the cross thread operation error.

Finally, I don't know if this was one of the tutorials you read, but you could also use the technique described in example 1 of that tutorial to actually raise your event on the thread the SerialManager instance was created (or opened) on (the UI thread, in this case), thus keeping the form class clean, and free of any threading related knowledge.

It may not be necessary for your case, but it's just another option :)

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

Was This Post Helpful? 3
  • +
  • -

#3 Pademelon  Icon User is offline

  • New D.I.C Head

Reputation: 1
  • View blog
  • Posts: 6
  • Joined: 20-January 13

Re: Form instanced SerialManager - rx event can't update textbox on Fo

Posted 21 January 2013 - 03:09 PM

Thanks for the reply,

So if I understand this correctly from the threading perspective:
  • Form1 and the instanced class of SerialManager are on the same thread.
  • onSerialDataReceived and any event it raises are both on the same thread, but it is a different thread to Form1 and SerialManager.


Quote

To do this, you can use Invoke():
if (e != null && e.Message != null)
      this.textBox1.Invoke(new MethodInvoker(() => this.textBox1.AppendText("test")));



The way I understand it:
In the case of all the serial code being part of the Form1 class, this. refers to the Form1 object
but
In the case of all the serial code being part of the SerialManager class, this. refers to the SerialManager object, which doesn't contain textBox1.

Ultimately, the SerialManager class should have no reference to textBox1. (I might want to instance SerialManager from a console app, who knows)

So I'm thinking then, that I need to have onSerialDataReceived do something similar to the this.textBox1.Invoke above, but run a method in SerialManager (which is already on the From1 thread), which then raises an event by calling RaiseFeedback(rxString); same as if I just called it directly from:
public void Open()  
     {  
         RaiseFeedback("Opening Com Port");  



Quote

Finally, I don't know if this was one of the tutorials you read,

Yes it was, and I didn't understand it really, and couldn't see how it would adapt to something that was already running on another thread, as opposed to something I'd started.

I don't have my code with me at the moment, so I can't test anything out yet, but I wanted to make sure my understanding and thought process was correct.
Was This Post Helpful? 0
  • +
  • -

#4 CodingSup3rnatur@l-360  Icon User is offline

  • D.I.C Addict
  • member icon

Reputation: 982
  • View blog
  • Posts: 969
  • Joined: 30-September 10

Re: Form instanced SerialManager - rx event can't update textbox on Fo

Posted 21 January 2013 - 04:05 PM

Quote

Form1 and the instanced class of SerialManager are on the same thread.
onSerialDataReceived and any event it raises are both on the same thread, but it is a different thread to Form1 and SerialManager.


Yes.

Quote

In the case of all the serial code being part of the Form1 class, this. refers to the Form1 object
but
In the case of all the serial code being part of the SerialManager class, this. refers to the SerialManager object, which doesn't contain textBox1.


this just refers to the current instance of the class you are in. When this is used in the Form1 class, it is referring to the current instance of the Form1 class; when used in the SerialManager class, it is referring to the current instance of the SerialManager class.

Quote

Ultimately, the SerialManager class should have no reference to textBox1


Without question! And in my example, the SerialManager class has absolutely no knowledge of the textBox1, or the UI in general.

Quote

So I'm thinking then, that I need to have onSerialDataReceived do something similar to the this.textBox1.Invoke above, but run a method in SerialManager (which is already on the From1 thread), which then raises an event by calling RaiseFeedback(rxString)...


Right, so it seems like your goal is in fact to have the actual event raised on the UI thread. In that case, one of the cleanest, easiest, and most general ways to do that is with example 1 in the tutorial I linked originally.


The idea being that you capture the current synchronization context (info about the current thread) when the SerialManager is created or when Open() is called, using a call to AsyncOperationManager.CreateOperation(null); in the constructor, or Open() method of the SerialManager, and store the returned AsyncOperation in an instance variable. Exactly as I do in that tutorial.

Then, when it is time to raise the feedback event, you raise it on the thread captured by the call to AsyncOperationManager.CreateOperation(null);, by calling Post() on the AsyncOperation object you stored earlier, which "posts" the event back to the thread we captured earlier (i.e. the UI thread, in this case). That would look something like:

private void onSerialDataReceived(object sender, SerialDataReceivedEventArgs e)
{
    String rxString = serialPort1.ReadExisting();
    this.op.Post(state => this.RaiseFeedback(rxString), null); //raise event on thread captured earlier, where "op" is the AsyncOperation object captured earlier.
}

private void RaiseFeedback(String feedback) {
    EventHandler<TextEventArgs> temp = this.Feedback;
    if (temp != null) { temp(this, new TextEventArgs(feedback)); }
}


Was This Post Helpful? 1
  • +
  • -

#5 Pademelon  Icon User is offline

  • New D.I.C Head

Reputation: 1
  • View blog
  • Posts: 6
  • Joined: 20-January 13

Re: Form instanced SerialManager - rx event can't update textbox on Fo

Posted 23 January 2013 - 03:05 AM

Thanks again for your help,

Quote

Right, so it seems like your goal is in fact to have the actual event raised on the UI thread.

Well it turned out to be my goal, but originally I started out just trying to apply the simple custom events tutorial to a real world piece of code, that I had been using to display serial messages from my Arduino microcontroller, before I tried to do something a little bit more complicated with it. Sort of as a practice run after reading the tutorial.

I didn't expct it to turn into a multi-threaded nightmare, well for me anyway. But in the end I guess it was good that it did. Had I just tried this method of seperating form logic from code logic with something simple, I would never have run across this problem until some time down the track.

I had another look at your tutorial today, and I now sort of see how it relates to the serial port problem. The code you provided, relating directly to the task I was trying to achieve made it a lot clearer. There is still a lot stuff in the tutorial I don't understand, because I only dabble at home with programmming computers (it's not my day job), and not anything related to the tutorial.

I finally had a chance to try your solution tonight, and it came up a winner.

Interestingly, all the serial communications examples I've looked at, including the one from this site, don't apply the serial stuff as an independant class, that is completely seperated from the form code, at the serial class level. (The one here nearly does, but it passes the forms textbox to the serial class)

One interesting thing I discovered, when you close the form, you can still have an outstanding event that wants to write to textbox1, even though it has been disposed of, causing an exceprion.

The quick fix was to do this:
        void Feedback_Received(object sender, TextEventArgs e)
        {
            if (e != null && e.Message != null)
                if(!textBox1.IsDisposed) 
                    textBox1.AppendText(e.Message);
        }


I'm not sure what the proper way to deal with this is though.

Anyway, thanks again for helping out.
Was This Post Helpful? 0
  • +
  • -

#6 CodingSup3rnatur@l-360  Icon User is offline

  • D.I.C Addict
  • member icon

Reputation: 982
  • View blog
  • Posts: 969
  • Joined: 30-September 10

Re: Form instanced SerialManager - rx event can't update textbox on Fo

Posted 23 January 2013 - 12:46 PM

Quote

One interesting thing I discovered, when you close the form, you can still have an outstanding event that wants to write to textbox1, even though it has been disposed of, causing an exceprion.


Make sure you are unsubscribing from the Feedback event, and that you are closing/disposing of the SerialPort object when the form gets closed. That's really the correct way to solve this problem.

For example,

protected override void OnClosed(EventArgs e) { //code in this method runs when the form is closed
    base.OnClosed(e);
    this.Arduino.Feedback -= new EventHandler<TextArgs>(Feedback_Received); //we don't want to here about feedback anymore.
    this.Arduino.Close(); //Call close on the SerialManager, which should then call Close() or Dispose() on the SerialPort object to release the resources it used, and to tell it to stop reading input.
}



Alternatively, that code can go in the Dispose(bool) method of Form1, which is in the Form1.designer.cs file:

protected override void Dispose(bool disposing) {
     if (disposing) {
         if (components != null) { components.Dispose(); }
         this.Arduino.Feedback -= new EventHandler<TextArgs>(Feedback_Received);
         this.Arduino.Close();
     }
     base.Dispose(disposing);
}



Whichever you prefer.

This post has been edited by CodingSup3rnatur@l-360: 23 January 2013 - 02:50 PM
Reason for edit:: Added clarification.

Was This Post Helpful? 0
  • +
  • -

Page 1 of 1