由于不同的线程拥有该对象,因此调用线程无法访问该对象。

434

我的代码如下:

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; 这一步会抛出异常:

调用线程无法访问此对象,因为另一个线程拥有它。

这里出了什么问题?


6
可能是重复的问题:https://dev59.com/4HE85IYBdhLWcg3wikAu,http://stackoverflow.com/questions/3146942/the-calling-thread-cannot-access-this-object-because-a-different-thread-owns-it,http://stackoverflow.com/questions/7684206/threading-issue-the-calling-thread-cannot-access-this-object-because-a-differen,http://stackoverflow.com/questions/8950347/the-calling-thread-cannot-access-this-object-because-a-different-thread-owns-it。 - user166390
15个回答

862

这是一个初学者常见的问题。每当您从主线程以外的线程更新UI元素时,您需要使用:

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

您还可以使用control.Dispatcher.CheckAccess()来检查当前线程是否拥有控件。如果拥有它,您的代码看起来就像平常一样。否则,请使用上面的模式。


3
我和原帖作者遇到了相同的问题;我现在的问题是该事件现在导致堆栈溢出。 :\ - Malavos
2
回到了我的旧项目并解决了这个问题。此外,我还忘记+1了这个方法。这种方法非常有效!仅通过使用线程来加载本地化资源,就可以将应用程序的加载时间缩短10秒甚至更多。干杯! - Malavos
4
如果我没错的话,你甚至不能从非所有者线程读取UI对象;这让我感到有些惊讶。 - Elliot
51
根据这个答案,如果不在UI线程上,可以使用Application.Current.Dispatcher.Invoke(MyMethod, DispatcherPriority.ContextIdle);来获取调度程序。 - JumpingJezza
4
+1. 哈!我曾经在 WPF 中使用这个技巧来保持解耦。因为我处于静态上下文中,所以无法使用 this.Dispatcher.Invoke ... 相反... 我使用了 myControl.Dispatcher.Invoke :) 我需要返回一个对象,所以我使用了 myControlDispatcher.Invoke<object>(() => myControl.DataContext); - C. Tewalt
显示剩余7条评论

62

补充一下,即使您通过 System.Windows.Threading.Dispatcher.CurrentDispatcher.Invoke() 调用代码也可能会出现异常。
重点是您必须调用要访问的 控件DispatcherInvoke(),而这在某些情况下可能与 System.Windows.Threading.Dispatcher.CurrentDispatcher 不同。因此,为了安全起见,您应该使用 YourControl.Dispatcher.Invoke()。在我意识到这一点之前,我已经头痛了几个小时。

更新

对于未来的读者,看起来这在较新版本的 .NET(4.0 及以上)中已经发生了改变。现在,当更新 VM 中支持 UI 的属性时,您不再需要担心正确的 Dispatcher。WPF 引擎将在正确的 UI 线程上进行跨线程调用的编排。在此处了解更多详细信息here。感谢 @aaronburro 提供的信息和链接。您还可以阅读下面评论中我们的交流。

更新2

由于这是一篇受欢迎的文章,所以我想分享一下我在接下来的几年中的经验。行为似乎是任何 属性绑定 都将在跨线程调用中正确更新(无需编排;WPF 会为您处理)。另一方面,命令绑定 将需要委托给 UI 调度程序。我已经测试过 MVVM Light 和相对较新的 Community Toolkit,发现在旧框架和新的 .NET 5 和 6 中都是如此。当从非 UI 线程调用时,AsyncRelayCommand 在更新 UI 时会失败(例如,当从更新按钮的 Enabled 属性的工作线程中触发 CanExecuteChanged 时会发生这种情况)。解决方案当然是在 VM 启动时将 UI 调度程序存储在全局空间中,然后在更新 UI 时使用它。


5
WPF支持在一个应用程序中使用多个UI线程,每个线程都有自己的“Dispatcher”。在这些情况下(尽管罕见),调用“Control.Dispatcher”是安全的方法。您可以参考这篇文章以及这篇SO帖子(特别是Squidward的答案)。 - dotNET
1
有趣的是,当我谷歌搜索并着陆在这个页面时,我也遇到了这个异常,像我们大多数人一样,尝试了得票最高的答案,但那时并没有解决我的问题。然后我发现了这个原因,并在这里为同行开发者发布了它。 - dotNET
1
@l33t,如果你正确使用MVVM,那么这不应该是一个问题。视图必须知道它正在使用的Dispatcher,而ViewModels和Models不知道控件的存在,也不需要知道控件的存在。 - aaronburro
2
@aaronburro:问题在于虚拟机可能希望在其他线程上启动操作(例如任务、基于计时器的操作、并行查询),随着操作的进行,可能希望通过 RaisePropertyChanged 等方式更新 UI,这将尝试从非 UI 线程访问 UI 控件,从而导致此异常。我不知道有什么正确的 MVVM 方法可以解决这个问题。 - dotNET
1
@aaronburro:我记不清我读过的确切文章,但是这里有一篇2014年的文章,它说的正是我之前所相信的。看看他如何需要调用标量属性更改通知来避免应用程序崩溃。我也曾经历过这种情况,所以这一定是在最近几个.NET版本中发生了变化的事情。 - dotNET
显示剩余17条评论

48
如果您在使用 WPF 中的 BitmapSourceImageSource 时,遇到了控件在一个独立线程上创建的问题,那么在传递 BitmapSourceImageSource 作为参数给任何方法之前,请先调用 Freeze() 方法。在这种情况下,使用 Application.Current.Dispatcher.Invoke() 是无效的。

37
啊,没有什么比一个模糊而神秘的诀窍更能解决那些无人理解的问题了。 - Edwin
2
我想了解更多关于为什么这个方法有效以及如何自己找出答案的信息。 - Xavier Shay
@XavierShay https://learn.microsoft.com/zh-cn/dotnet/framework/wpf/advanced/freezable-objects-overview - juFo

34

这件事情发生是因为我试图在另一个线程而不是UI线程中访问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调用包装在Candide在他的答案中提到的中。

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

2
点赞,因为这不是重复的答案或剽窃,而是提供了其他答案缺乏的好例子,同时也给予之前发布的内容应有的认可。 - Panzercrisis
点赞是为了一个清晰的答案。虽然其他人也写过相同的内容,但这使得任何卡住的人都能明白。 - NishantM

22
你需要在UI线程上执行此操作。使用:
Dispatcher.BeginInvoke(new Action(() => {GetGridData(null, 0)})); 

15

由于某种原因,Candide的答案没有生成。不过它还是有帮助的,因为它让我找到了这个,而这个方案完美解决了我的问题:

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

有可能您没有从该表单的类中调用。您可以抓取窗口的引用,或者您可能可以使用您建议的方法。 - Simone
9
如果它对你起作用了,一开始就不必使用它。System.Windows.Threading.Dispatcher.CurrentDispatcher当前线程的调度程序。这意味着,如果你在后台线程上运行,它不会是 UI 线程的调度程序。要访问 UI 线程的调度程序,请使用 System.Windows.Application.Current.Dispatcher - user1228

6
这对我有效。
new Thread(() =>
        {

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

          //Your Code here.

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

这段代码会冻结用户界面。 - abdou93

5

此处所述,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();
        }
    }
}

4

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


3
问题在于您正在从后台线程调用 GetGridData 方法。此方法访问几个绑定到主线程的 WPF 控件。任何尝试从后台线程访问它们都会导致此错误。
为了回到正确的线程,您应该使用 SynchronizationContext.Current.Post。但是,在这种情况下,似乎大部分工作都是基于 UI 的。因此,您将创建一个后台线程,然后立即返回到 UI 线程并执行一些工作。您需要稍微重构代码,以便它可以在后台线程上执行昂贵的工作,然后在之后将新数据发布到 UI 线程。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接