【问题标题】:User interaction in non-UI thread?非 UI 线程中的用户交互?
【发布时间】:2015-01-04 04:30:26
【问题描述】:

在我的 WPF - C# 应用程序中,我有一个耗时的函数,我使用 BackgroundWorker 执行该函数。此函数的工作是将给定数据从文件添加到数据库中。有时,我需要一些用户反馈,例如数据已经在存储中,我想问用户,他是要合并数据还是创建新对象或完全跳过数据。就像对话框窗口显示的一样,如果我尝试将文件复制到已存在同名文件的位置。

问题是,我不能从非 GUI 线程调用 GUI 窗口。我该如何实现这种行为?

提前致谢,
弗兰克

【问题讨论】:

  • 你在使用 WPF 吗?或者,您使用的是 WinForms 吗?
  • 我使用 WPF ...将此信息添加到问题中
  • 你在做 MVVM 吗?如果是这样,那么,您可以使用 INotifyPropertyChanged 进行设计,以从 ViewModel(甚至是 Model)获取 View 的事件;如果您使用 MVVM,我可以为您提供或指出答案。
  • 解决这个问题最好的办法是使用 Dispatcher.Invoke 或 Dispatcher.BeginInvoke。

标签: c# multithreading user-interface backgroundworker


【解决方案1】:

您可以使用 EventWaitHandle 或 AutoResetEvent,然后每当您想提示用户时,您可以向 UI 发出信号,然后等待响应。有关文件的信息可以存储在变量中。

【讨论】:

  • 感谢您的回复!我在albahari.com/threading/part2.aspx检查了TwoWaySignaling 据我了解,GUI线程被此方法阻塞,因为它在后台等待来自线程的事件。但我不希望 GUI 阻塞。
  • 您可以尝试使用 3 个线程:GUI 线程、处理线程和侦听器线程。然后,当您开始使用方法时,GUI 将启动侦听器和处理线程。侦听器线程将等待来自处理线程的信号,然后,只要有信号,它就会调用您的确认方法。
  • 但是监听线程不会算作 GUI 线程,因此无法与用户交互,还是我错了?
  • 不需要,它只会与GUI线程交互。每当发生冲突时,在您的 GUI 线程上调用一个方法来向用户请求订单。我猜你可能需要 Dispatcher.Invoke 之类的东西
【解决方案2】:

如果可能...我的建议是将您的长期运行任务构建为原子操作。然后,您可以创建一个可由后台线程和 UI 线程访问的项目队列。

public class WorkItem<T>
{
    public T Data { get; set; }
    public Func<bool> Validate { get; set; }
    public Func<T, bool> Action { get; set; }
}

你可以使用类似这个类的东西。它使用队列来管理工作项的执行,并使用可观察的集合来向 UI 发出信号:

public class TaskRunner<T>
{
    private readonly Queue<WorkItem<T>> _queue;

    public ObservableCollection<WorkItem<T>> NeedsAttention { get; private set; }

    public bool WorkRemaining
    {
        get { return NeedsAttention.Count > 0 && _queue.Count > 0; }
    }

    public TaskRunner(IEnumerable<WorkItem<T>> items)
    {
        _queue = new Queue<WorkItem<T>>(items);
        NeedsAttention = new ObservableCollection<WorkItem<T>>();
    }

    public event EventHandler WorkCompleted;

    public void LongRunningTask()
    {
        while (WorkRemaining)
        {
            if (_queue.Any())
            {
                var workItem = _queue.Dequeue();

                if (workItem.Validate())
                {
                    workItem.Action(workItem.Data);
                }
                else
                {
                    NeedsAttention.Add(workItem);
                }
            }
            else
            {
                Thread.Sleep(500); // check if the queue has items every 500ms
            }
        }

        var completedEvent = WorkCompleted;
        if (completedEvent != null)
        {
            completedEvent(this, EventArgs.Empty);
        }
    }

    public void Queue(WorkItem<T> item)
    {
        // TODO remove the item from the NeedsAttention collection
        _queue.Enqueue(item);
    }
}

您的 UI 代码隐藏可能类似于

public class TaskRunnerPage : Page
{
    private TaskRunner<XElement> _taskrunner;

    public void DoWork()
    {
        var work = Enumerable.Empty<WorkItem<XElement>>(); // TODO create your workItems

        _taskrunner = new TaskRunner<XElement>(work);

        _taskrunner.NeedsAttention.CollectionChanged += OnItemNeedsAttention;

        Task.Run(() => _taskrunner.LongRunningTask()); // run this on a non-UI thread
    }

    private void OnItemNeedsAttention(object sender, NotifyCollectionChangedEventArgs e)
    {
        // e.NewItems contains items that need attention.
        foreach (var item in e.NewItems)
        {
            var workItem = (WorkItem<XElement>) item;
            // do something with workItem
            PromptUser();
        }
    }

    /// <summary>
    /// TODO Use this callback from your UI
    /// </summary>
    private void OnUserAction()
    {
        // TODO create a new workItem with your changed parameters
        var workItem = new WorkItem<XElement>();
        _taskrunner.Queue(workItem);
    }
}

此代码未经测试!但基本原则应该适合你。

【讨论】:

  • 这看起来很优雅,但我担心它会引发更多问题。如果使用这个想法,我也可以简单地从主线程调用任务。如果一个任务依赖于另一个任务的信息,也就是处于“NeedsAttention”状态,这可能也是一个问题。
  • 是的,当然。这些任务需要彼此完全隔离才能正常工作。
【解决方案3】:

具体到你的情况

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        Thread.Sleep(1000);
        var a = Test1("a");
        Thread.Sleep(1000);
        var b = (string)Invoke(new Func<string>(() => Test2("b")));
        MessageBox.Show(a + b);
    }

    private string Test1(string text)
    {
        if (this.InvokeRequired)
            return (string)this.Invoke(new Func<string>(() => Test1(text)));
        else
        {
            MessageBox.Show(text);
            return "test1";
        }
    }

    private string Test2(string text)
    {
        MessageBox.Show(text);
        return "test2";
    }

Test2 是您必须从后台工作人员调用 的常规方法。 Test1 可以直接调用,并使用安全模式调用自身。

MessageBox.Show 类似于yourForm.ShowDialog(都是模态的),你向它传递参数(text)然后你返回值(可以是yourForm 的属性值,它是在表单关闭时设置的)。我使用的是string,但显然它可以是任何数据类型。

【讨论】:

    【解决方案4】:

    从这里输入的答案,我得出以下解决方案:

    (错误)将 Backgroundworker 的 ReportProgress 方法与 EventWaitHandle 结合使用。如果我想与用户交互,我会调用 ReportProgress 方法并将后台进程设置为等待。在 ReportProgress 事件的处理程序中,我进行交互,完成后,我释放 EventWaitHandle。

        BackgroundWorker bgw;
    
        public MainWindow()
        {
            InitializeComponent();
    
            bgw = new BackgroundWorker();
            bgw.DoWork += new DoWorkEventHandler(bgw_DoWork);
            bgw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgw_RunWorkerCompleted);
            bgw.WorkerReportsProgress = true;
            bgw.ProgressChanged += new ProgressChangedEventHandler(bgw_ProgressChanged);
        }
    
        // Starting the time consuming operation
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            bgw.RunWorkerAsync();
        }
    
        // using the ProgressChanged-Handler to execute the user interaction
        void bgw_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            UserStateData usd = e.UserState as UserStateData;
    
            // UserStateData.Message is used to see **who** called the method
            if (usd.Message == "X")
            {
                // do the user interaction here
                UserInteraction wnd = new UserInteraction();
                wnd.ShowDialog();
    
                // A global variable to carry the information and the EventWaitHandle
                Controller.instance.TWS.Message = wnd.TextBox_Message.Text;
                Controller.instance.TWS.Background.Set();
            }
        }
    
        void bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            MessageBox.Show(e.Result.ToString());
        }
    
        // our time consuming operation
        void bgw_DoWork(object sender, DoWorkEventArgs e)
        {
            Thread.Sleep(2000);
    
            // need 4 userinteraction: raise the ReportProgress event and Wait
            bgw.ReportProgress(0, new UserStateData() { Message = "X", Data = "Test" });
            Controller.instance.TWS.Background.WaitOne();
    
            // The WaitHandle was released, the needed information should be written to global variable
            string first = Controller.instance.TWS.Message.ToString();
    
            // ... and again
            Thread.Sleep(2000);
    
            bgw.ReportProgress(0, new UserStateData() { Message = "X", Data = "Test" });
            Controller.instance.TWS.Background.WaitOne();
    
            e.Result = first + Controller.instance.TWS.Message;
        }
    

    我希望我没有忽略一些关键问题。我对多线程不太熟悉——也许某处应该有一些锁(对象)?

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-23
      • 1970-01-01
      • 1970-01-01
      • 2015-09-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多