Laden...

ObservableCollection kann nach Update über Task nur noch über Task geupdatet werden

Erstellt von Quaneu vor 5 Jahren Letzter Beitrag vor 5 Jahren 2.674 Views
Quaneu Themenstarter:in
692 Beiträge seit 2008
vor 5 Jahren
ObservableCollection kann nach Update über Task nur noch über Task geupdatet werden

Hallo zusammen,

ich habe ein Problem mit einer ObservableCollection, die aus einem anderen Thread aktualisiert werden soll. Hierzu verwende ich BindingOperations.EnableCollectionSynchronization.


private readonly object _itemsLock = new object();
public Logger(string logPath)
{
	LogPath = logPath;
	LogEntries = new ObservableCollection<LogEntry>();
	BindingOperations.EnableCollectionSynchronization(LogEntries, _itemsLock);
}

public void AddInfo(string message)
{
	lock (_itemsLock)
	{
		Log(message, LogConstants.Info);
	}
}

Diese Klasse wird im MainThread angelegt.

Das Aktualiseren in einem anderen Thread klappt. Jedoch wirft jeder Update, der danach stattfindet eine Exception, wenn dieser nicht in einem anderen Thread stattfindet.

Fehlermeldung:
System.NotSupportedException: 'Von diesem CollectionView-Typ werden keine Änderungen der "SourceCollection" unterstützt, wenn diese nicht von einem Dispatcher-Thread aus erfolgen.'

Das Updaten mache ich wie folgt:


private void ExecuteOpenPackage(object packageFile)
{
	OpenPackage(packageFile as string);
}

private async void OpenPackage(string packageFile)
{
	var openPackageResult = await OpenPackageAsync(Logger, packageFile);
}

private static Task<OpenPackageResult> OpenPackageAsync(Logger logger, string packageFile)
{
	return Task<OpenPackageResult>.Factory.StartNew(() =>
	{
		// Lange Operation
		logger.AddInfo("test");
		return null;
	});
}

Kann mir jemand sagen was ich falsch mache? Sobald ich eben den Task starte kann ich die Collection nicht mehr ohne Task updaten.

Schöne Grüße und vielen Dank
Quaneu

16.827 Beiträge seit 2008
vor 5 Jahren

Was mir sofort auffällt:

  • Du hast ConfigureAwait(false) vergessen
  • Du kannst auch Task.Run schreiben
  • Insgesamt async/await falsch implementiert (zB ist async void ein Pitfall)
Quaneu Themenstarter:in
692 Beiträge seit 2008
vor 5 Jahren

Hallo Abt,

vielen Dank für deine Hinweise. Task.Run und async void habe ich gleich mal korrigert.

Könntest Du mir vielleicht sagen, was falsch implementiert ist? Wenn ich in meinem Beispiel nämlich ConfigureAwait(false) einbaue bekomme ich schon Fehler beim "ersten" Update.

Schöne Grüße
Quaneu

5.658 Beiträge seit 2006
vor 5 Jahren

Abgesehen davon sieht es so aus, als hast du hier alles in der Präsentationsschicht: Datenzugriff, Datenverarbeitung und Benutzeroberfläche.

Eigentlich sollte sich die DAL unabhängig von allen anderen Schichten auf den Dateizugriff beschränken, die BL die Daten verarbeiten (evtl. im Hintergrund oder in parallelen Tasks), und der GUI sollte es schließlich völlig egal sein, woher die Daten für die Darstellung kommen. Mit einer geeigneten Architektur würden solche Probleme gar nicht erst entstehen.

Lesetip: [Artikel] Drei-Schichten-Architektur

Weeks of programming can save you hours of planning

Quaneu Themenstarter:in
692 Beiträge seit 2008
vor 5 Jahren

Hallo MrSparkle,

das sieht vielleicht nur so aus. Es ist (bzw. sollte) nach MVVM gebaut sein.
Es gibt die UI, dann meine ViewModels und meine DatenObjekte. Man kann hier ischer noch einiges schönes machen, jedoch denke ich, dass dies nicht mein Problem verursacht. Aber ich werde nochmals einen Blick drauf werfen. Daher Danke für den Hinweis 😃

Schöne Grüße
Quaneu

Quaneu Themenstarter:in
692 Beiträge seit 2008
vor 5 Jahren

Ich habe jetzt eine Lösung gefunden jedoch würde ich gern verstehen wie dieser Fehler zustande kommt.

Meine Lösung:
Wenn man diese AsyncObservableCollection benutzt
http://www.thomaslevesque.com/2009/04/17/wpf-binding-to-an-asynchronous-collection/

und


BindingOperations.EnableCollectionSynchronization(LogEntries, _itemsLock);

entfernt klappt es wie erwartet. Aber wie schon gesagt, wenn jemand weiß woran es liegt wäre ich sehr dankbar.

Schöne Grüße
Quaneu

5.658 Beiträge seit 2006
vor 5 Jahren

Der Fehler, den du hier zu beheben versuchst, ist doch nur ein Symptom deiner (leider offensichtlich ungeeigneten) Architektur. Das Problem hat auch nichts mit WPF zu tun. Und du bist nicht der erste, der mit Tasks und Thread arbeiten will. Du hast ja auch schon Hinweise bekommen, und wenn dir das nicht ausreicht, dann solltest du dir mal die Basics zum Arbeiten mit Tasks bzw. async / await anschauen. Das Einbinden einer "AsyncObservableCollection" ist schließlich nur ein Workaround, der das Symptom, aber nicht die Ursache behebt.

Weeks of programming can save you hours of planning

Quaneu Themenstarter:in
692 Beiträge seit 2008
vor 5 Jahren

Ich hab das Problem gefunden. Es lag nicht an der Architektur und wohl auch nicht am async / await, bzw. nur indirekt.

Hier ein Beispiel Code (hier habe ich auf Architektur verzichtet 😄)


<Window x:Class="Sample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Sample"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
		<Grid.RowDefinitions>
			<RowDefinition Height="*"/>
			<RowDefinition Height="Auto"/>
		</Grid.RowDefinitions>
		<ListBox Grid.Row="0" ItemsSource="{Binding Names, Mode=OneTime}"/>
		<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
			<Button HorizontalAlignment="Right" Content="WorkAsync" Click="Button_Click" Margin="2"/>
			<Button HorizontalAlignment="Right" Content="AddSingleName" Click="Button_Click_1" Margin="2"/>
		</StackPanel>
	</Grid>
</Window>


using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;

namespace Sample
{
	public partial class MainWindow : Window
	{
		private readonly object _itemsLock;
		public MainWindow()
		{
			_itemsLock = new object();
			Names = new ObservableCollection<string>();	
			BindingOperations.EnableCollectionSynchronization(Names, _itemsLock);
			DataContext = this;
			InitializeComponent();
			Names.CollectionChanged += Names_CollectionChanged;
		}

		private void Names_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
		{
			var listCollectionView = ((ListCollectionView)CollectionViewSource.GetDefaultView(Names));
		}

		public ObservableCollection<string> Names { get; }

		private async void Button_Click(object sender, RoutedEventArgs e)
		{
			await Task.Run(() => HeavyMethod());
		}

		internal void HeavyMethod()
		{
			for (int i = 0; i < 3; i++)
			{
				Thread.Sleep(1000);
				AddName($"Name_{i}");
			}
		}

		private void Button_Click_1(object sender, RoutedEventArgs e)
		{
			AddName("SingleName");
		}

		private void AddName(string name)
		{
			lock (_itemsLock)
			{
				Names.Add(name);
			}
		}
	}
}

Drückt man erst auf WorkAsync und danach auf AddSingleName kommt es zur Exception. WorkAsync kann man aber beliebig oft drücken.
Das Problem macht folgende Zeile


var listCollectionView = ((ListCollectionView)CollectionViewSource.GetDefaultView(Names));

Ohne diese läuft es immer... Leider verstehe ich es aber immer noch nicht 🤔

Schöne Grüße
Quaneu