更新ObservableCollection异步导致挂起,并且没有GUI更新

本文关键字:更新 GUI 挂起 ObservableCollection 异步 | 更新日期: 2023-09-27 18:07:20

我正在WPF中实现Tracert的可视化版本(作为学习练习),其中结果将显示在列表框中。问题是(1)绑定到tracertDataView的列表框没有更新,但是(2)我的整个应用程序挂起。

我确定#2是一个线程问题,但我不确定如何纠正它(以正确的方式)。此外,我不确定我更新/绑定"DoTrace"结果的技术是否正确。

这是我的数据源在App.xaml

<Window.Resources>
<CollectionViewSource 
          Source="{Binding Source={x:Static Application.Current}, Path=TracertResultNodes}"   
          x:Key="tracertDataView" />
</Window.Resources>

App.xaml.cs

public partial class App : Application
{
    private ObservableCollection<TracertNode> tracertResultNodes = new ObservableCollection<TracertNode>();
    public void AppStartup(object sender, StartupEventArgs e)
    {
          // NOTE: Load sample data does work correctly.. and displays on the screen.  
         //      subsequent updates do not display
        LoadSampleData();
    }
    private void LoadSampleData()
    {
         TracertResultNodes = new ObservableCollection<TracertNode>();
        TracertNode t = new TracertNode();
        t.Address = new System.Net.IPAddress(0x2414188f);
        t.RoundTripTime = 30;
        t.Status = System.Net.NetworkInformation.IPStatus.BadRoute;
            TracertResultNodes.Add(t);
    }
    public ObservableCollection<TracertNode> TracertResultNodes
    {
        get { return this.tracertResultNodes; }
        set { this.tracertResultNodes = value; }
    }
}
下面是MainWindow代码
  public partial class MainWindow : Window
{
    CollectionViewSource tracertDataView;
    TraceWrapper _tracertWrapper = null;
    public MainWindow()
    {
        InitializeComponent();
         _tracertWrapper = new TraceWrapper();
        tracertDataView = (CollectionViewSource)(this.Resources["tracertDataView"]);
    }
    private void DoTrace_Click(object sender, RoutedEventArgs e)
    {
       ((App)Application.Current).TracertResultNodes = _tracertWrapper.Results;
       _tracertWrapper.DoTrace("8.8.8.8", 30, 50);
    }
}

供参考实例对象"traceWrapper"的内部实现细节。DoTrace"

    /// <summary>
    /// Trace a host.  Note that this object internally calls the Async implementation of .NET's PING. 
    // It works perfectly fine in a CMD host, but not in WPF
    /// </summary>
     public ObservableCollection<TracertNode> DoTrace(string HostOrIP, int maxHops, int TimeOut)
    {
        tracert = new Tracert();
        // The following is triggered for every host that is found, or upon timeout
         //  (up to 30 times by default)
        AutoResetEvent wait = new AutoResetEvent(false);
       tracert.waiter = wait;
        tracert.HostNameOrAddress = HostOrIP;
        tracert.Trace();
        this.Results = tracert.NodeList;
        while (tracert.IsDone == false)
        {
            wait.WaitOne();
            IsDone = tracert.IsDone;
        }
        return tracert.NodeList;
    }

更新ObservableCollection<T>异步导致挂起,并且没有GUI更新

我不明白你是如何使用AutoResetEvent的,我猜它不应该这样使用:)

但是因为Trace已经在另一个线程中运行,你确定在你的Tracert类中没有一个事件"OnTracertComplete"或类似的东西吗?

如果没有,为什么不把DispatchTimer放到应用程序中呢?该计时器将定期轮询,直到tracert。IsDone变成了真的。如果你在一个操作完成之前阻塞了应用程序线程的执行,你就阻塞了窗口事件循环的执行,这样窗口就永远不会被更新。

另一个重要的事情:你不能从另一个线程更新ObservableCollections。要小心,并确保在WPF窗口中更新的所有内容都是从该窗口的同一线程执行的。我不知道Trace类到底是做什么的,但这里的问题似乎当然是等待循环,这在GUI应用程序中是没有意义的。

使用通知事件或计时器轮询结果。对于这个特定的实现,我认为具有1秒分辨率的计时器似乎很好,并且性能影响绝对是最小的。

如果你能够修改Tracert类,这是一个可能的实现。

    public delegate void TracertCallbacHandler(Tracert sender, TracertNode newNode);
    public class Tracert
    {
        public event TracertCallbacHandler NewNodeFound;
        public event EventHandler TracertCompleted;
        public void Trace()
        {
            ....
        }
        // This function gets called in tracert thread'async method.
        private void FunctionCalledInThreadWhenPingCompletes(TracertNode newNode)
        {
            var handler = this.NewNodeFound;
            if (handler != null)
                handler(this, newNode);
        }
        // This function gets called in tracert thread'async methods when everything ends.
        private void FunctionCalledWhenEverythingDone()
        {
            var handler = this.TracertCompleted;
            if (handler != null)
                handler(this, EventArgs.Empty);
        }
    }

下面是运行tracert的代码,我是TracertWrapper。

    // Keep the observable collection as a field.
    private ObservableCollection<TracertNode> pTracertNodes;
    // Keep the instance of the running tracert as a field, we need it.
    private Tracert pTracert;
    public bool IsTracertRunning
    {
        get { return this.pTracert != null; }
    }
    public ObservableCollection<TracertNode> DoTrace(string hostOrIP, int maxHops, int timeOut)
    {
        // If we are not already running a tracert...
        if (this.pTracert == null)
        {
            // Clear or creates the list of tracert nodes.
            if (this.pTracertNodes == null)
                this.pTracertNodes = new ObservableCollection<TracertNode>();
            else
                this.pTracertNodes.Clear();
            var tracert = new Tracert();
            tracert.HostNameOrAddress = hostOrIP;
            tracert.MaxHops = maxHops;
            tracert.TimeOut = timeOut;
            tracert.NewNodeFound += delegate(Tracert sender, TracertNode newNode)
            {
                // This method is called inside Tracert thread.
                // We need to use synchronization context to execute this method in our main window thread.
                SynchronizationContext.Current.Post(delegate(object state)
                {
                    // This method is called inside window thread.
                    this.OnTracertNodeFound(this.pTracertNodes, newNode);
                }, null);
            };
            tracert.TracertCompleted += delegate(object sender, EventArgs e)
            {
                // This method is called inside Tracert thread.
                // We need to use synchronization context to execute this method in our main window thread.
                SynchronizationContext.Current.Post(delegate(object state)
                {
                    // This method is called inside window thread.
                    this.OnTracertCompleted();
                }, null);
            };
            tracert.Trace();
            this.pTracert = tracert;
        }
        return this.pTracertNodes;
    }
    protected virtual void OnTracertCompleted()
    {
        // Remove tracert object,
        // we need this to let the garbage collector being able to release that objects.
        // We need also to allow another traceroute since the previous one completed.
        this.pTracert = null;
        System.Windows.MessageBox.Show("TraceRoute completed!");
    }
    protected virtual void OnTracertNodeFound(ObservableCollection<TracertNode> collection, TracertNode newNode)
    {
        // Add our tracert node.
        collection.Add(newNode);
    }

问题是,不仅列表框没有更新,而且我的整个应用程序都挂起了。

这可能是由于AutoResetEvent阻塞在DoTrace。您显式地在事件句柄上调用Wait.WaitOne();,但据我所知,从未调用Set()。这将导致应用程序永远挂起,只要你调用Wait.WaitOne()

听起来像tracert.Trace()是一个异步方法。它是否包含某种形式的回调/事件,以便在完成时通知您?如果是这样,您应该使用它来确定何时完成,而不是在循环中轮询。

(1)绑定到tracertDataView的列表框没有更新

您将看不到列表框的更新,因为您将新集合分配给TracertResultNodes属性,在这种情况下绑定根本不起作用,因为分配了一个新集合。

除了确保集合在同一个线程中更新之外,您应该只从现有集合中添加或删除项,而不是分配由DoTrace函数生成的新项。

private void DoTrace_Click(object sender, RoutedEventArgs e)
    {
       foreach(var traceNode in _tracertWrapper.Results)
       {
          ((App)Application.Current).TracertResultNodes.Add(traceNode);
       }
       _tracertWrapper.DoTrace("8.8.8.8", 30, 50);
    }

如果你分配一个新的,那么你需要在你的App类上实现INotifyPropertyChanged,我不确定如何(或是否)这会工作(我以前没有尝试过)。