简体   繁体   中英

User interaction in non-UI thread?

in my WPF - C# application, I have a time consuming function, which I execute with a BackgroundWorker. The job of this function is to add given data from a file into a database. Now and then, I need some user feedback, for example the data is already in the store and I want to ask the user, whether he wants to merge the data or create a new object or skip the data completely. Much like the dialog windows shows, if I try to copy a file to a location, where a file with the same name already exists.

The problem is, that I cannot call a GUI-window from a non GUI-thread. How could I implement this behavior?

Thanks in advance,
Frank

You could work with EventWaitHandle ou AutoResetEvent, then whenever you want to prompt the user, you could the signal UI, and then wait for the responde. The information about the file could be stored on a variable.

If possible... my suggestion is to architect your long running task into atomic operations. Then you can create a queue of items accessible by both your background thread and UI thread.

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

You can use something like this class. It uses a queue to manage the execution of your work items, and an observable collection to signal the 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);
    }
}

Your UI codebehind could look something like

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

This code is untested! But the basic principle should work for you.

From the input of the answers here, I came to the following solution:

(Mis)Using the ReportProgress-method of the Backgroundworker in Combination with a EventWaitHandle. If I want to interact with the user, I call the ReportProgress-method and setting the background process on wait. In the Handler for the ReportProgress event I do the interaction and when finished, I release the 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;
    }

I hope I did not overlooked some critical issues. I'm not so familar with multithreading - maybe there should be some lock(object) somewhere?

Specifically to your case

    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 is a normal method which you have to invoke from background worker. Test1 can be called directly and uses safe pattern to invoke itself.

MessageBox.Show is similar to yourForm.ShowDialog (both are modal), you pass parameters to it ( text ) and you return value (can be a value of property of yourForm which is set when form is closed). I am using string , but it can be any data type obviously.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM