【问题标题】:Dispatcher.Dispatch on the UI threadUI 线程上的 Dispatcher.Dispatch
【发布时间】:2012-04-02 16:23:38
【问题描述】:

我对何时使用 Dispatcher.Invoke 从不同的线程更新 UI 内容存有疑问。

这是我的代码...

public Window4()
    {
        InitializeComponent();
        this.DataContext = this;

      Task.Factory.StartNew(() => Test() );
    }

    private List<string> listOfString = new List<string>();

    public List<string> ListOfString
    {
        get { return listOfString; }
        set { listOfString = value; }
    }

    public void Test()
    {
        listOfString.Add("abc");
        listOfString.Add("abc");
        listOfString.Add("abc");
    }

 <Grid>
    <ListView ItemsSource="{Binding ListOfString}" />
</Grid>

我在不同的线程上开始一个新任务,我需要使用 Dispatcher.BeginInvoke 来更新 UI。

在这种情况下,它正在更新 UI,但我已经看到人们使用 Dispatcher.Invoke 或 BeginInvoke 从不同的线程更新 UI。

所以我的问题是我们什么时候必须这样做,为什么在这种情况下它可以正常工作。

感谢和问候, 哈维克

【问题讨论】:

    标签: c# wpf


    【解决方案1】:

    我对何时使用 Dispatcher.Invoke 进行更新存有疑问 来自不同线程的 UI 上的东西。

    当您在不同的线程上时,您将始终必须使用调度程序来更新属于另一个线程的 ui 组件。

    我在不同的线程上开始一个新任务,我需要使用 Dispatcher.BeginInvoke 以更新 UI。

    任务允许在不阻塞调用它们的线程的情况下执行多个操作,但这并不意味着它们位于不同的线程上。但是,当从 Task 内部更新 UI 时,您将需要使用调度程序。

    在这种情况下,它正在更新 UI,但我已经看到了一些场景 人们使用 Dispatcher.Invoke 或 BeginInvoke 更新 UI 不同的线程。

    Invoke 将在执行操作时阻塞调用线程,而 BeginInvoke 不会。 BeginInvoke 将立即将控制权返回给调用者,如果 Invoke 执行繁重的操作,可能会导致调用线程挂起。

    这是来自 msdn 文档,

    在 WPF 中,只有创建 DispatcherObject 的线程可以访问 那个物体。例如,从 主 UI 线程无法更新 Button 的内容 在 UI 线程上创建。为了让后台线程访问 Button的Content属性,后台线程必须 将工作委托给与 UI 线程关联的 Dispatcher。 这是通过使用 Invoke 或 BeginInvoke 来完成的。调用是 同步和 BeginInvoke 是异步的。

    编辑:为了回应您的评论,我进行了一些测试。

    从任务调用 Test() 时(不使用调度程序)我收到此错误“调用线程无法访问此对象,因为不同的线程拥有它。”

    所以我创建了一个名为 PrintThreadID() 的方法。我在进入任务之前打印了线程,然后从任务内部打印,它确实报告两者都在 same thread ID 上运行。

    该错误具有误导性,因为它说调用线程与拥有它的线程不同,PrintThreadID() 函数显示的不是真的,它们实际上在同一个线程上。如果不使用 Dispather.Invoke(),同一线程上的任务仍然无法更新 UI 组件。

    所以这是一个工作示例,它将从任务更新网格。


    public partial class MainWindow : Window
    {
        public List<string> myList { get; private set; }
    
        public MainWindow()
        {
            InitializeComponent();
            myList = new List<string>();
            label1.Content = Thread.CurrentThread.ManagedThreadId.ToString();
    
            Task.Factory.StartNew(PrintThreadID);
            Task.Factory.StartNew(Test);
    
        }
    
        private void PrintThreadID()
        {
            label1.Dispatcher.Invoke(new Action(() =>
                label1.Content += "..." + Thread.CurrentThread.ManagedThreadId.ToString()));
        }
    
        private void Test()
        {
            myList.Add("abc");
            myList.Add("abc");
            myList.Add("abc");
    
            // if you do not use the dispatcher you will get the error "The calling thread cannot access this object because a different thread owns it."
    
    
            dataGrid1.Dispatcher.Invoke(new Action(() =>
            {
                dataGrid1.ItemsSource = myList.Select(i => new { Item = i });
            }));
        }
    }
    

    【讨论】:

    • 但是如果我要更新或添加绑定到 UI 的列表,是否还需要使用 Dispatcher 的 Invoke 或 BeginInvoke...
    • 取决于列表类型。 List&lt;T&gt;:不,但更新不会出现在 UI 中。 ObservableCollection&lt;T&gt;:是的。
    • 从任务内部更新它时,是的,您必须调用 Dispatcher.Invoke(),我发布了我的测试的完整示例,以便在我的回答中显示以上内容。
    【解决方案2】:

    您的测试无效,因为它实际上并没有更新您的 UI。如果您需要证明,请添加此睡眠呼叫:

    public void Test()
    {
        Thread.Sleep(10000);
        listOfString.Add("abc");
        listOfString.Add("abc");
        listOfString.Add("abc");
    }
    

    您会发现您的 UI 出现并且列表为空。 10 秒、30 秒、3 个月后,列表将不包含您的字符串。

    相反,您的测试正在展示竞争条件 - 您的 Test() 方法完成得足够快,以至于字符串被添加到列表中 UI 出现在屏幕上并读取列表之前。

    要解决此问题,请将您的收藏更改为 ObservableCollection&lt;string&gt;。但是你会遇到下一个问题——你不能在后台线程上更新ObservableCollection。这就是Dispatcher 的用武之地。

    【讨论】:

    • 不太明白你的回答。 UI 线程将简单地休眠 10 秒,然后添加项目“abc”。
    猜你喜欢
    • 2015-10-19
    • 1970-01-01
    • 1970-01-01
    • 2015-07-24
    • 2012-11-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-13
    相关资源
    最近更新 更多