Наблюдение ObservableCollection из другого потокаC#

Место общения программистов C#
Ответить
Anonymous
 Наблюдение ObservableCollection из другого потока

Сообщение Anonymous »

Примечание. Я публикую много кода для полностью воспроизводимого примера, но большая его часть здесь не актуальна, а просто для обеспечения необходимого контекста, большую часть кода можно пропустить.< /p>
Намерение: в моем приложении C#/WPF у меня есть Logger (реализованный как Singleton), который широко используется во всем приложении для регистрации различных поведение:

Код: Выделить всё

public class Logger
{
public ReadOnlyCollection Log => _logRo;
private readonly ObservableCollection _log;
private readonly ReadOnlyObservableCollection _logRo;

private static Logger? _instance;
private Dictionary _logActors;

public static Logger GetInstance()
{
return _instance ??= new Logger();
}

public Logger()
{
_log = [];
_logRo = new(_log);

_logActors = new Dictionary
{
{ LogLevels.Trace,    [new ConsoleWriter(), new FileWriter()] },
{ LogLevels.Debug,    [new ConsoleWriter(), new FileWriter()] },
{ LogLevels.Info,     [new ConsoleWriter(), new FileWriter()] },
{ LogLevels.Warning,  [new ConsoleWriter(), new FileWriter()] },
{ LogLevels.Error,    [new ConsoleWriter(), new FileWriter()] },
{ LogLevels.Fatal,    [new ConsoleWriter(), new FileWriter()] },
};
}

public static void Add(LogLevels level, string message)
{
var instance = GetInstance();

LogEntry entry = new(level, message);
instance?._log.Add(entry);

List? actors = instance?._logActors[level];
if (actors == null) return;
foreach (var actor in actors)
{
actor.Perform(entry);
}
}

public void Clear()
{
_log.Clear();
}

private interface ILogActor
{
void Perform(LogEntry entry);
}

private class ConsoleWriter : ILogActor
{
public void Perform(LogEntry entry)
{
Console.WriteLine(entry.ToString());
}
}

private class FileWriter : ILogActor
{
public void Perform(LogEntry entry)
{
var folder = Utilities.FileOperations.GetAppPath();
const string name = "debug.log";
var path = folder + name;
Utilities.FileOperations.AppendToFile(path, entry.ToString() + Environment.NewLine);
}
}

public class LogEntry(LogLevels level, string message)
{
public DateTime Time { get; init; } = DateTime.Now;
public LogLevels Level { get; init; } = level;
public string Message { get; init; } = message;

public override string ToString()
{
List entryItems = [Time.ToString(CultureInfo.InvariantCulture), Utilities.EnumTools.GetEnumDescription(Level), Message];

return entryItems.Aggregate(string.Empty, (current, item) => current + (item + "\t"));
}
}
}

[Flags]
public enum LogLevels
{
[Description("TRACE")]
Trace = 1,
[Description("DEBUG")]
Debug = 2,
[Description("INFO")]
Info = 4,
[Description("WARNING")]
Warning = 8,
[Description("ERROR")]
Error = 16,
[Description("FATAL")]
Fatal = 32
}
У меня также есть страница WPF и окно, которое ее размещает, для отображения logger.log contents.
Страница:

Код: Выделить всё

public partial class LoggerPage : Page
{
LoggerPageViewModel vm;

public LoggerPage(AppSettingsData.GeneralSettings generalSettings)
{
InitializeComponent();
vm = new LoggerPageViewModel(generalSettings);
DataContext = vm;
}

private void Page_Loaded(object sender, RoutedEventArgs e)
{
var view = (CollectionView)CollectionViewSource.GetDefaultView(Log_ListView.ItemsSource);
view.Filter = UserFilter;
}

private bool UserFilter(object item)
{
var level = (item as Logger.LogEntry).Level;

if (vm.LogDisplayLevels.HasFlag(level))
{
return true;
}

return false;
}

private void CheckboxChanged(object sender, RoutedEventArgs e)
{
CollectionViewSource.GetDefaultView(Log_ListView.ItemsSource).Refresh();
}

public class LoggerPageViewModel
{
public ReadOnlyCollection Log { get; set; }
public LogLevels LogDisplayLevels { get; init; }

public LoggerPageViewModel(AppSettingsData.GeneralSettings generalSettings)
{
Log = Logger.GetInstance().Log;
LogDisplayLevels = generalSettings.DisplayLogLevels;
}
}
}

public class LogMaskValueConverter : IValueConverter
{
private LogLevels target;

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var mask = (LogLevels)parameter;
target = (LogLevels)value;
return (mask & target) != 0;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
target ^= (LogLevels)parameter;
return target;
}
}
< /code>





































































Окно:

Код: Выделить всё

public partial class LoggerWindow : Window
{
public LoggerWindow(AppSettingsData.GeneralSettings generalSettings)
{
InitializeComponent();
LogDisplay_Frame.Navigate(new LoggerPage(generalSettings));
}

private void ClearButton_Click(object sender, RoutedEventArgs e)
{
Logger.GetInstance().Clear();
}
}
< /code>












< /code>
Теперь я хочу отобразить это окно, когда начинается приложение, и для того, чтобы обновить его содержимое в прямом эфире, когда приложение запускается без замораживания, обновление, как только новые записи добавляются в Logger .Log 
. Поэтому моя идея заключалась в том, чтобы запустить пользовательский и основной приложение в отдельных потоках. Насколько я понимаю, в приложениях WPF пользовательский интерфейс должен работать в основном потоке, поэтому вместо этого я запускаю все остальное в отдельном потоке: < /p>

Код: Выделить всё

public partial class App : System.Windows.Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);

var loggerWindow = new LoggerWindow(GeneralSettings);
loggerWindow.Show();
RunTestsAsync();
}

private async void RunTestsAsync()
{
await Task.Run(() =>
{
Tests.Run(); //Various calls that take ~1 minute to complete, with many calls to Logger.Add
});
}
}
Обратите внимание, что GeneralSettings на самом деле является более сложным дочерним классом AppSettings (здесь не показано). Я опустил его и упростил GeneralSettings здесь для воспроизводимости. :

Код: Выделить всё

public static class GeneralSettings
{
public LogLevels DisplayLogLevels { get; init; } = LogLevels.Info | LogLevels.Error | LogLevels.Fatal;
}
Проблема: когда я пытаюсь запустить это, я получаю сообщение об ошибке в Logger.Add(), экземпляр строки?._log.Add( запись):

Этот тип CollectionView не поддерживает изменения в своей SourceCollection из потока, отличного от потока Dispatcher

Насколько я понимаю, проблема в том, что Logger.Log (ObservableCollection) обрабатывается двумя потоками: один из них изменяет его, а другой подписывается на события изменения его содержимого, что не разрешено. Однако это и есть мое фактическое намерение — я хочу, чтобы ядро ​​приложения продолжало писать в этот Logger.Log, а LoggerPage немедленно замечало эти изменения и обновляло пользовательский интерфейс. Я не знаю, как это решить.
Что я здесь делаю не так? Мой подход неверен? Если да, то как мне лучше с этим справиться?
Я вижу, что в подобных вопросах советовали использовать EnableCollectionSynchronization, но в документации говорится, что для работы его необходимо вызывать из потока пользовательского интерфейса (я попробовал это из фонового потока, который использует Logger, и это не сработало), поэтому я не уверен, что это можно применить к моему коду без добавления инвазивных общедоступных вызовов в Logger, чтобы предоставить этот вызов потоку пользовательского интерфейса. . Я прошу повторно открыть этот вопрос, если я не ошибаюсь и эта ситуация требует другого решения, но если я упускаю что-то очевидное, прокомментируйте.

Подробнее здесь: https://stackoverflow.com/questions/793 ... her-thread
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C#»