سؤال

I come from an embedded C background and I am working on my first C# application and I have hit a wall on this and my research is not panning out so I thought I would ask here.

Simple app, so far. I have a MainWindow that, among a bunch of other stuff, starts a TCPClient thread on a button click:

public partial class MainWindow : Window
{
 ....
       TCPConnection myCon = new TCPConnection();
 ....
   private void connectButton_Click(object sender, RoutedEventArgs e)
    {
        networkListBox.Items.Add("Connecting...");
        myCon.Connect("localhost", updateNetworkListBox);
    }
 }

 ....
    public void updateNetworkListBox(string message)
    {
        networkListBox.Items.Add(message);
    }

And in TCPConnection.cs:

   public class TCPConnection
   {
   ....
    public void Connect(string server, ReportDelegate reportDelegate)
    {
        this.server = server;
        clientThread = new Thread(() => Client(this.server));
        clientThread.Start();
        reportDelegate("Started client thread...");
    }

    static void Client(string server)
    {
        try
        {
            Int32 port = 25565;
            TcpClient client = new TcpClient(server, port);
            Byte[] outgoingBytes = new Byte[1024];
            string outgoingString = "Hello! I am " + Guid.NewGuid();
            outgoingBytes = System.Text.Encoding.ASCII.GetBytes(outgoingString);
            NetworkStream stream = client.GetStream();
            stream.Write(outgoingBytes, 0, outgoingBytes.Length);
            stream.Close();
            client.Close();
        }

The first thing I would like to do, now that TCP connection works is send a message back to the UI such as "Client thread connecting...", "Client thread connected..." and have it show up in the networkListbox.

Within the Connect() method, I was able to do this by using the delegate but this obviously will not work in the new thread since one is not able to directly access UI controls from another thread.

I have read loads of articles on this and I know that I probably want to use the Dispatcher to to do this. However, almost all of the examples I have seen have created a new thread within the current class and, for example, passed an anonymous method to Dispatcher.Invoke().

One exception to this discussion which advocated using an EventHandler and initializing it in the main window. That seems less than ideal but maybe I am wrong.

Further down, someone else advocated data sharing. Again, that seems less than ideal to me.

Other articles I have read appear to be out of date.

So, I welcome any explanations on how to go about this. It may be that I am just getting hung up syntactically but I suspect that, although I think I am mostly clear on delegates, lambdas, etc., I am probably hung up on what exactly needs to get done.

If you can show how it would be done in this specific example with some explanation, I would greatly appreciate it.

And maybe some specific questions on some points that are a little hazy for me:

1) Can my worker task access on it on its own or must it be provided with the UI's Dispatcher?

2) Should the UI provide a delegate that performs the dispatch or should the dispatch be coded in the worker task, referencing the UI Dispatcher?

Thanks very much.

هل كانت مفيدة؟

المحلول

For your question about providing a sample, if there is a worker class like...

public class Worker
{
    public Worker(Action<string>action)
    {
        Task.Run(() =>
        {
            int i = 0;
            while (true)
            {
                ++i;
                Task.Run(() => { action("Current value " + i); });
                Task.Run(() =>
                {
                    // doing some work here
                });
                Thread.Sleep(1000);
            }
        });
    }
}

...which is performing background work on different threads and advising the caller via the delegate. The delegate is a plain vanilla Action that takes a string. Then the View Model should be implemented such that it does not care on which thread the message originated. Here's the corresponding code in the VM...

    private readonly SynchronizationContext _context = SynchronizationContext.Current;
    private void StartWorker()
    {
        Worker w = new Worker((s) => _context.Post(delegate { StatusText = s; }, null));
    }

This code uses a SynchronizationContext, but could just as easily use a dispatcher. The point being that the responsibility for sync'ing up on the UI thread doesn't belong to a worker. The worker shouldn't care, and similarly the VM is thread-agnostic and posts everything via its SynchronizationContext.

The code for the StatusText property looks like this...

    private string _statusText;
    public string StatusText
    {
        [DebuggerStepThrough]
        get { return _statusText; }
        [DebuggerStepThrough]
        set
        {
            if (value != _statusText)
            {
                _statusText = value;
                OnPropertyChanged("StatusText");
            }
        }
    }

And finally, on the UI, it is presented like this...

        <StatusBar DockPanel.Dock="Bottom">
            <TextBlock Text="{Binding StatusText}"/>
        </StatusBar>

...

So to recap your questions: the worker threads can access it, but they should not have to deal with sync'ing up the UI. That responsibility is the VM's. And VM should be thread-agnostic and sync the UI through the dispatcher or synchronization context or other methods.

Scheduling through the Dispatcher is appropriate if you are manipulating a collection that is the subject of a binding (e.g., an ObservableCollection); otherwise SynchronizationContext is appropriate (it's a bit more light-weight).

نصائح أخرى

just add the delegate and pass a reference to your main form

public partial class MainWindow : Window
{

   TCPConnection myCon = new TCPConnection();

   private void connectButton_Click(object sender, RoutedEventArgs e)
   {
       networkListBox.Items.Add("Connecting...");
       myCon.Connect("localhost", updateNetworkListBox);
   }



    public delegate void updateNetworkListBoxDelegate(string message);
    public void updateNetworkListBox(string message)
    {
        if(this.invokeRequired())
        {
            this.invoke(new updateNetworkListBoxDelegate(updateNetworkListBox), message);
        }
        else
        {
            networkListBox.Items.Add(message);
        }
    }
}

in TCPConnection add a constructor that takes a MainWindow instance

public class TCPConnection
{
    //add member to hold instance
    private _mainWindow;
    //add constructor taking instance
    public TCPConnection(MainWindow  mw)
    {
        _mainWindow = mw;
    }
    public void Connect(string server, ReportDelegate reportDelegate)
    {
        this.server = server;
        clientThread = new Thread(() => Client(this.server));
        clientThread.Start();
        //reportDelegate("Started client thread...");
        //call the method on the UI thread
        _mainWindow.updateNetworkListBox("Started client thread...");
    }

    static void Client(string server)
    {
        try
        {
            Int32 port = 25565;
            TcpClient client = new TcpClient(server, port);
            Byte[] outgoingBytes = new Byte[1024];
            string outgoingString = "Hello! I am " + Guid.NewGuid();
            outgoingBytes = System.Text.Encoding.ASCII.GetBytes(outgoingString);
            NetworkStream stream = client.GetStream();
            stream.Write(outgoingBytes, 0, outgoingBytes.Length);
            stream.Close();
            client.Close();
            //call the method ont he ui thread
            _mainWindow.updateNetworkListBox("DONE!!")
        }
    }
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top