Асинхронное обновление ObservableCollection <T>приводит к зависаниям и отсутствию обновления графического интерфейса. - PullRequest
2 голосов
/ 01 сентября 2011

Я реализую визуальную версию Tracert (в качестве учебного упражнения) в WPF, где результаты переходят в список. Проблемы заключаются в том, что (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);
    }
}

FYI Внутренняя реализация Деталь объекта экземпляра "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;
    }

Ответы [ 3 ]

2 голосов
/ 01 сентября 2011

Я не понимаю, как вы использовали AutoResetEvent, я думаю, он не должен использоваться таким образом:)

Но поскольку трассировка уже запущена в другом потоке, вы уверены, что нет события«OnTracertComplete» или что-то подобное в вашем классе Tracert?

Если нет, то почему вы просто не добавляете DispatchTimer в свое приложение?Этот таймер будет периодически опрашивать, пока tracert.IsDone становится истиной.Если вы блокируете выполнение потока приложения до завершения операции, вы блокируете выполнение цикла событий окна, чтобы окно никогда не обновлялось.

Еще одна важная вещь: вы не можете обновить ObservableCollections из другого потока.Будьте осторожны и убедитесь, что все, что обновляется в окне WPF, выполняется из того же потока окна.Не знаю, что именно делает ваш класс Trace, но вашей проблемой здесь, конечно, является цикл ожидания, который не имеет смысла в приложении с графическим интерфейсом.

Используйте события уведомлений или таймер для опросарезультат.Таймер с разрешением в 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);
    }
1 голос
/ 01 сентября 2011

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

Если вы назначите новый, вам нужно будет реализовать INotifyPropertyChanged в вашем классе приложения, я не уверен, как (или будет ли) это работать, хотя (я не пробовал это раньше).

1 голос
/ 01 сентября 2011

Проблема в том, что список не только не обновляется, но и зависает все мое приложение.

Вероятно, это связано с блокировкой AutoResetEvent в DoTrace. Вы явно вызываете Wait.WaitOne(); на дескрипторе события, но, насколько я могу судить, никогда не Set() это. Это приведет к зависанию приложения навсегда, как только вы позвоните Wait.WaitOne().

Похоже, tracert.Trace() - это асинхронный метод. Включает ли он некоторую форму обратного вызова / события, чтобы уведомить вас о завершении? Если это так, вы должны использовать это, а не опрашивать в цикле, чтобы определить, когда он завершится.

...