Безопасный доступ к потоку пользовательского интерфейса (основного) в WPF

99

У меня есть приложение, которое обновляет мою сетку данных каждый раз, когда файл журнала, который я наблюдаю, обновляется (с добавлением нового текста) следующим образом:

private void DGAddRow(string name, FunctionType ft)
    {
                ASCIIEncoding ascii = new ASCIIEncoding();

    CommDGDataSource ds = new CommDGDataSource();

    int position = 0;
    string[] data_split = ft.Data.Split(' ');
    foreach (AttributeType at in ft.Types)
    {
        if (at.IsAddress)
        {

            ds.Source = HexString2Ascii(data_split[position]);
            ds.Destination = HexString2Ascii(data_split[position+1]);
            break;
        }
        else
        {
            position += at.Size;
        }
    }
    ds.Protocol = name;
    ds.Number = rowCount;
    ds.Data = ft.Data;
    ds.Time = ft.Time;

    dataGridRows.Add(ds); 

    rowCount++;
    }
    ...
    private void FileSystemWatcher()
    {
        FileSystemWatcher watcher = new FileSystemWatcher(Environment.CurrentDirectory);
        watcher.Filter = syslogPath;
        watcher.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite
            | NotifyFilters.FileName | NotifyFilters.DirectoryName;
        watcher.Changed += new FileSystemEventHandler(watcher_Changed);
        watcher.EnableRaisingEvents = true;
    }

    private void watcher_Changed(object sender, FileSystemEventArgs e)
    {
        if (File.Exists(syslogPath))
        {
            string line = GetLine(syslogPath,currentLine);
            foreach (CommRuleParser crp in crpList)
            {
                FunctionType ft = new FunctionType();
                if (crp.ParseLine(line, out ft))
                {
                    DGAddRow(crp.Protocol, ft);
                }
            }
            currentLine++;
        }
        else
            MessageBox.Show(UIConstant.COMM_SYSLOG_NON_EXIST_WARNING);
    }

Когда событие возникает для FileWatcher, потому что он создает отдельный поток, когда я пытаюсь запустить dataGridRows.Add (ds); чтобы добавить новую строку, программа просто вылетает без предупреждения в режиме отладки.

В Winforms это легко решалось с помощью функции Invoke, но я не уверен, как это сделать в WPF.

l46kok
источник

Ответы:

204

Ты можешь использовать

Dispatcher.Invoke(Delegate, object[])

на диспетчере Application(или любом UIElementдругом).

Вы можете использовать это, например, так:

Application.Current.Dispatcher.Invoke(new Action(() => { /* Your code here */ }));

или

someControl.Dispatcher.Invoke(new Action(() => { /* Your code here */ }));
Botz3000
источник
Вышеупомянутый подход давал ошибку, потому что Application.Current имеет значение NULL во время выполнения строки. Почему это так?
l46kok
Вы можете просто использовать для этого любой UIElement, поскольку каждый UIElement имеет свойство Dispatcher.
Wolfgang Ziegler
1
@ l46kok Причины могут быть разные (консольное приложение, хостинг от winforms и т. д.). Как сказал @WolfgangZiegler, вы можете использовать для этого любой UIElement. Я обычно использую Application.Currentего, потому что он мне кажется чище.
Botz3000
@ Botz3000 Я думаю, что у меня тоже есть проблема с условиями гонки. После добавления кода, приведенного выше, код работает отлично, когда я перехожу в режим отладки и вручную выполняю пошаговые инструкции, но код вылетает, когда я запускаю приложение без отладки. Я не уверен, что здесь заблокировать, что вызывает проблему.
l46kok
1
@ l46kok Если вы считаете, что это тупик, вы также можете позвонить Dispatcher.BeginInvoke. Этот метод просто ставит делегата в очередь на выполнение.
Botz3000
52

Лучший способ сделать это - получить SynchronizationContextиз потока пользовательского интерфейса и использовать его. Этот класс абстрагирует вызовы маршалинга в другие потоки и упрощает тестирование (в отличие от Dispatcherпрямого использования WPF ). Например:

class MyViewModel
{
    private readonly SynchronizationContext _syncContext;

    public MyViewModel()
    {
        // we assume this ctor is called from the UI thread!
        _syncContext = SynchronizationContext.Current;
    }

    // ...

    private void watcher_Changed(object sender, FileSystemEventArgs e)
    {
         _syncContext.Post(o => DGAddRow(crp.Protocol, ft), null);
    }
}
Эли Арбель
источник
Большое спасибо! Принятое решение зависало при каждом вызове, но это работает.
Дов
Он также работает, когда вызывается из сборки, содержащей модель представления, но не «настоящего» WPF, т.е. является библиотекой классов.
Онур
Это очень полезный совет, особенно если у вас есть не-wpf-компонент с потоком, в который вы хотите маршалировать действия. конечно, другой способ сделать это - использовать продолжения TPL
MaYaN
Сначала я не понял этого, но у меня это сработало ... приятно. (Следует отметить, что DGAddRow - частный метод)
Тим Дэвис,
5

Используйте [Dispatcher.Invoke (DispatcherPriority, Delegate)] чтобы изменить пользовательский интерфейс из другого потока или из фона.

Шаг 1 . Используйте следующие пространства имен

using System.Windows;
using System.Threading;
using System.Windows.Threading;

Шаг 2 . Поместите следующую строку, где вам нужно обновить пользовательский интерфейс

Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new ThreadStart(delegate
{
    //Update UI here
}));

Синтаксис

[BrowsableAttribute(false)]
public object Invoke(
  DispatcherPriority priority,
  Delegate method
)

Параметры

priority

Тип: System.Windows.Threading.DispatcherPriority

Приоритет по отношению к другим ожидающим операциям в очереди событий Dispatcher, указанный метод вызывается.

method

Тип: System.Delegate

Делегат метода, который не принимает аргументов, который помещается в очередь событий Dispatcher.

Возвращаемое значение

Тип: System.Object

Возвращаемое значение от вызываемого делегата или null, если делегат не имеет возвращаемого значения.

Информация о версии

Доступно с .NET Framework 3.0

Винит Чоудхари
источник