C#/ WPF从另一个类中创建的另一个线程更新UI

时间:2014-04-30 20:31:08

标签: c# wpf multithreading user-interface dispatcher

我来自嵌入式C背景,我正在研究我的第一个C#应用程序而且我已经在这方面遇到了障碍,我的研究还没有完成,所以我想我会问这里。

简单的应用程序,到目前为止。我有一个MainWindow,在一堆其他东西中,在点击按钮上启动一个TCPClient线程:

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);
    }

在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();
        }

我想做的第一件事,现在TCP连接工作是将消息发送回UI,例如“客户端线程连接...”,“客户端线程连接......”并将其显示在networkListbox。

在Connect()方法中,我能够通过使用委托来实现这一点,但这显然不适用于新线程,因为无法从另一个线程直接访问UI控件。

我已经阅读了大量关于此的文章,我知道我可能想使用Dispatcher来执行此操作。但是,我见过的几乎所有示例都在当前类中创建了一个新线程,例如,将一个匿名方法传递给Dispatcher.Invoke()。

this讨论的一个例外,它主张使用EventHandler并在主窗口中初始化它。这似乎不太理想,但也许我错了。

再往下,其他人主张数据共享。再一次,这对我来说似乎不太理想。

我读过的其他文章似乎已经过时了。

所以,我欢迎任何解释如何解决这个问题。可能是因为我只是在句法上被挂起但是我怀疑,尽管我认为我对代表,lambdas等大部分都很清楚,但我可能会对完全需要完成的事情感到困惑。

如果您可以通过一些解释说明在具体示例中如何完成,我将非常感激。

也许有些问题对我来说有点模糊:

1)我的工作人员可以独立访问它,还是必须通过UI的Dispatcher提供?

2)UI是否应该提供执行调度的委托,还是应该在worker任务中编写调度,引用UI Dispatcher?

非常感谢。

2 个答案:

答案 0 :(得分:0)

关于提供样本的问题,如果有像......这样的工人阶级。

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);
            }
        });
    }
}

...正在不同的线程上执行后台工作,并通过委托建议调用者。委托是一个简单的香草Action,它带有一个字符串。然后应该实现视图模型,使其不关心消息源自哪个线程。这是VM中的相应代码......

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

此代码使用SynchronizationContext,但可以轻松使用调度程序。关键是在UI线程上同步的责任不属于工作者。工作人员不应该关心,类似地,VM与线程无关,并通过其SynchronizationContext发布所有内容

StatusText属性的代码如下所示......

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

最后,在用户界面上,它就像这样呈现......

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

...

所以回顾一下你的问题:工作者线程可以访问它,但是他们应该必须处理同步UI。这个责任就是虚拟机。 VM应与线程无关,并通过调度程序或同步上下文或其他方法同步UI。

如果您正在操作作为绑定主题的集合(例如,ObservableCollection),则通过Dispatcher进行调度是合适的。否则SynchronizationContext是合适的(它的重量更轻)。

答案 1 :(得分:-1)

只需添加委托并传递对主表单的引用

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);
        }
    }
}
TCPConnection中的

添加一个带有MainWindow实例的构造函数

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!!")
        }
    }
}