调用线程不能访问这个对象,因为它属于另一个线程

我的代码如下

public CountryStandards()
{
InitializeComponent();
try
{
FillPageControls();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Country Standards", MessageBoxButton.OK, MessageBoxImage.Error);
}
}


/// <summary>
/// Fills the page controls.
/// </summary>
private void FillPageControls()
{
popUpProgressBar.IsOpen = true;
lblProgress.Content = "Loading. Please wait...";
progress.IsIndeterminate = true;
worker = new BackgroundWorker();
worker.DoWork += new System.ComponentModel.DoWorkEventHandler(worker_DoWork);
worker.ProgressChanged += new System.ComponentModel.ProgressChangedEventHandler(worker_ProgressChanged);
worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;
worker.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
worker.RunWorkerAsync();
}


private void worker_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
GetGridData(null, 0); // filling grid
}


private void worker_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
{
progress.Value = e.ProgressPercentage;
}


private void worker_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
worker = null;
popUpProgressBar.IsOpen = false;
//filling Region dropdown
Standards.UDMCountryStandards objUDMCountryStandards = new Standards.UDMCountryStandards();
objUDMCountryStandards.Operation = "SELECT_REGION";
DataSet dsRegionStandards = objStandardsBusinessLayer.GetCountryStandards(objUDMCountryStandards);
if (!StandardsDefault.IsNullOrEmptyDataTable(dsRegionStandards, 0))
StandardsDefault.FillComboBox(cmbRegion, dsRegionStandards.Tables[0], "Region", "RegionId");


//filling Currency dropdown
objUDMCountryStandards = new Standards.UDMCountryStandards();
objUDMCountryStandards.Operation = "SELECT_CURRENCY";
DataSet dsCurrencyStandards = objStandardsBusinessLayer.GetCountryStandards(objUDMCountryStandards);
if (!StandardsDefault.IsNullOrEmptyDataTable(dsCurrencyStandards, 0))
StandardsDefault.FillComboBox(cmbCurrency, dsCurrencyStandards.Tables[0], "CurrencyName", "CurrencyId");


if (Users.UserRole != "Admin")
btnSave.IsEnabled = false;


}


/// <summary>
/// Gets the grid data.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="pageIndex">Index of the page.( used in case of paging)   </pamam>
private void GetGridData(object sender, int pageIndex)
{
Standards.UDMCountryStandards objUDMCountryStandards = new Standards.UDMCountryStandards();
objUDMCountryStandards.Operation = "SELECT";
objUDMCountryStandards.Country = txtSearchCountry.Text.Trim() != string.Empty ? txtSearchCountry.Text : null;
DataSet dsCountryStandards = objStandardsBusinessLayer.GetCountryStandards(objUDMCountryStandards);
if (!StandardsDefault.IsNullOrEmptyDataTable(dsCountryStandards, 0) && (chkbxMarketsSearch.IsChecked == true || chkbxBudgetsSearch.IsChecked == true || chkbxProgramsSearch.IsChecked == true))
{
DataTable objDataTable = StandardsDefault.FilterDatatableForModules(dsCountryStandards.Tables[0], "Country", chkbxMarketsSearch, chkbxBudgetsSearch, chkbxProgramsSearch);
dgCountryList.ItemsSource = objDataTable.DefaultView;
}
else
{
MessageBox.Show("No Records Found", "Country Standards", MessageBoxButton.OK, MessageBoxImage.Information);
btnClear_Click(null, null);
}
}

获取网格数据中的步骤objUDMCountryStandards.Country = txtSearchCountry.Text.Trim() != string.Empty ? txtSearchCountry.Text : null;抛出异常

调用线程不能访问该对象,因为有不同的

这里出了什么问题?

347702 次浏览

问题是你从后台线程调用GetGridData。这个方法访问几个绑定到主线程的WPF控件。任何试图从后台线程访问它们的尝试都会导致此错误。

为了回到正确的线程,你应该使用SynchronizationContext.Current.Post。然而,在这种特殊情况下,你所做的大部分工作似乎都是基于UI的。因此,你将创建一个后台线程,只是为了立即回到UI线程并做一些工作。您需要稍微重构一下代码,以便它能够在后台线程上完成昂贵的工作,然后将新数据发布到UI线程

这是人们刚开始时遇到的一个普遍问题。每当你从主线程以外的线程更新UI元素时,你需要使用:

this.Dispatcher.Invoke(() =>
{
...// your code here.
});

你也可以使用control.Dispatcher.CheckAccess()来检查当前线程是否拥有该控件。如果它确实拥有它,那么您的代码看起来就正常。否则,使用上述图案。

你需要在UI线程上做。使用:

Dispatcher.BeginInvoke(new Action(() => {GetGridData(null, 0)}));

出于某种原因,坎迪德的回答并不可信。尽管如此,它还是很有帮助的,因为它让我发现了这个,它非常有效:

System.Windows.Threading.Dispatcher.CurrentDispatcher.Invoke((Action)(() =>
{
//your code here...
}));

补充一下我的意见,即使你通过System.Windows.Threading.Dispatcher.CurrentDispatcher.Invoke()调用你的代码,也会发生异常。关键是你必须调用您正在尝试访问的控件DispatcherInvoke(),在某些情况下可能与System.Windows.Threading.Dispatcher.CurrentDispatcher不相同。所以为了安全起见,你应该使用YourControl.Dispatcher.Invoke()。我敲了几个小时的头才意识到这一点。

更新

对于未来的读者来说,这一点在。net的新版本(4.0及以上版本)中似乎有所改变。现在,在VM中更新ui支持属性时,您不再需要担心正确的调度程序。WPF引擎将在正确的UI线程上编组跨线程调用。查看更多详细信息在这里。感谢@aaronburro提供的信息和链接。你也可以在评论中阅读我们的对话。

更新2

既然这篇文章现在很受欢迎,我想分享一下我在接下来的几年里的经历。行为似乎是任何属性绑定将在跨线程调用中正确更新(不需要编组;WPF将为您处理)。OTOH 命令绑定将需要委托给UI分派器。我已经用MVVM Light和相对较新的Community Toolkit对它进行了测试,在旧的框架和新的。net 5和。6中似乎都是这样。当从非UI线程调用AsyncRelayCommand时,无法更新UI(这种情况发生在CanExecuteChanged从更新按钮的Enabled属性的工作线程中触发时)。当然,解决方案是在启动时将UI分派器存储在虚拟机全局空间中的某个位置,然后在更新UI时使用它。

如果你遇到这个问题,并且UI控件是在WPF中使用BitmapSourceImageSource时在独立的工人线程上创建的,在将BitmapSourceImageSource作为参数传递给任何方法之前,首先调用Freeze()方法。在这种情况下,使用Application.Current.Dispatcher.Invoke()不起作用

这种情况发生在我身上,因为我试图在another thread insted of UI threadaccess UI组件

像这样

private void button_Click(object sender, RoutedEventArgs e)
{
new Thread(SyncProcces).Start();
}


private void SyncProcces()
{
string val1 = null, val2 = null;
//here is the problem
val1 = textBox1.Text;//access UI in another thread
val2 = textBox2.Text;//access UI in another thread
localStore = new LocalStore(val1);
remoteStore = new RemoteStore(val2);
}

为了解决这个问题,将任何ui调用包装在老实人在他的回答中提到了什么

private void SyncProcces()
{
string val1 = null, val2 = null;
this.Dispatcher.Invoke((Action)(() =>
{//this refer to form in WPF application
val1 = textBox.Text;
val2 = textBox_Copy.Text;
}));
localStore = new LocalStore(val1);
remoteStore = new RemoteStore(val2 );
}

另外,另一个解决方案是确保控件是在UI线程中创建的,而不是由后台工作线程创建的。

我还发现System.Windows.Threading.Dispatcher.CurrentDispatcher.Invoke()并不总是目标控制的调度器,就像dotNet在他的回答中写的那样。我没有访问控件自己的调度程序,所以我使用Application.Current.Dispatcher,它解决了这个问题。

正如前面提到的在这里Dispatcher.Invoke可以冻结UI。应该使用Dispatcher.BeginInvoke代替。

下面是一个方便的扩展类,可以简化检查和调用调度程序调用。

示例用法:(从WPF窗口调用)

this Dispatcher.InvokeIfRequired(new Action(() =>
{
logTextbox.AppendText(message);
logTextbox.ScrollToEnd();
}));

扩展类:

using System;
using System.Windows.Threading;


namespace WpfUtility
{
public static class DispatcherExtension
{
public static void InvokeIfRequired(this Dispatcher dispatcher, Action action)
{
if (dispatcher == null)
{
return;
}
if (!dispatcher.CheckAccess())
{
dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle);
return;
}
action();
}
}
}

这对我很有用。

new Thread(() =>
{


Thread.CurrentThread.IsBackground = false;
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, (SendOrPostCallback)delegate {


//Your Code here.


}, null);
}).Start();

当我添加级联组合框到我的WPF应用程序时,我一直得到错误,并通过使用这个API解决了这个错误:

    using System.Windows.Data;


private readonly object _lock = new object();
private CustomObservableCollection<string> _myUiBoundProperty;
public CustomObservableCollection<string> MyUiBoundProperty
{
get { return _myUiBoundProperty; }
set
{
if (value == _myUiBoundProperty) return;
_myUiBoundProperty = value;
NotifyPropertyChanged(nameof(MyUiBoundProperty));
}
}


public MyViewModelCtor(INavigationService navigationService)
{
// Other code...
BindingOperations.EnableCollectionSynchronization(AvailableDefectSubCategories, _lock );


}

详情请参见https://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1& l = EN-US& k = k (System.Windows.Data.BindingOperations.EnableCollectionSynchronization); k (TargetFrameworkMoniker -.NETFramework,版本% 3 dv4.7); k (DevLang-csharp)和rd = true

根据您的需要,肯定有不同的方法来做到这一点。

我使用UI更新线程(它不是主UI线程)的一种方法是让线程开始一个循环,其中整个逻辑处理循环被调用到UI线程上。

例子:

public SomeFunction()
{
bool working = true;
Thread t = new Thread(() =>
{
// Don't put the working bool in here, otherwise it will
// belong to the new thread and not the main UI thread.
while (working)
{
Application.Current.Dispatcher.Invoke(() =>
{
// Put your entire logic code in here.
// All of this code will process on the main UI thread because
//  of the Invoke.
// By doing it this way, you don't have to worry about Invoking individual
//  elements as they are needed.
});
}
});
}

这样,代码就完全在主UI线程上执行。对于那些难以理解跨线程操作的业余程序员来说,这可能是一种专业方法。然而,对于更复杂的ui(特别是在执行动画时),它很容易成为一个骗局。实际上,这只是为了伪造一个更新UI的系统,然后返回来处理任何已经触发的事件,以代替有效的跨线程操作。

有时,抛出异常的可能是您创建的对象,而不是我明显看到的目标。

在我的代码中:

xaml文件:

<Grid Margin="0,0,0,0" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" >
<TextBlock x:Name="tbScreenLog" VerticalAlignment="Stretch" Background="Black" FontSize="12" Foreground="#FF919191" HorizontalAlignment="Stretch"/>
</Grid>

xaml.cs文件:

System.Windows.Documents.Run rnLine = new System.Windows.Documents.Run(Message.Item2 + "\r\n");
rnLine.Foreground = LineAlternate ? Brushes.Green : Brushes.Orange;


Dispatcher.Invoke(()=> {
tbScreenLog.Inlines.Add(rnLine);
});
LineAlternate = !LineAlternate;

我得到了关于从不同线程访问对象的异常,但我在UI线程上调用它??

过了一段时间,它吓倒了我,这不是关于TextBlock对象,而是关于运行对象我创建之前调用。

把代码改成这样就解决了我的问题:

Dispatcher.Invoke(()=> {
Run rnLine = new Run(Message.Item2 + "\r\n");
rnLine.Foreground = LineAlternate ? Brushes.Green : Brushes.Orange;
tbScreenLog.Inlines.Add(rnLine);
});
LineAlternate = !LineAlternate;

我在从WPF控件中选择的第二个项目上奇怪地遇到了这个错误。

原因是我将数据加载到RX SourceCache中,加载的元素将observablecollection作为导航属性包装到CollectionView中。observablecollection连接到UIThread,数据由WorkerThread加载。由于CollectionView只在显示第一个元素时被填充,不同线程的问题只发生在被选择的第二个项目上。

解决方案是将子列表作为ReadOnlyObservableCollections移动到ViewModel中,并根据当前选择的主元素筛选子元素表的完整列表。