簡體   English   中英

非UI線程中的用戶交互?

[英]User interaction in non-UI thread?

在我的WPF-C#應用程序中,我有一個耗時的函數,該函數與BackgroundWorker一起執行。 此功能的工作是將文件中的給定數據添加到數據庫中。 有時,我需要一些用戶反饋,例如數據已經存儲在商店中,並且我想問用戶是要合並數據還是創建新對象還是完全跳過數據。 就像對話框窗口顯示的一樣,如果我嘗試將文件復制到已經存在相同名稱文件的位置。

問題是,我無法從非GUI線程調用GUI窗口。 我該如何實施這種行為?

提前致謝,
坦率

您可以使用EventWaitHandle或AutoResetEvent,然后每當要提示用戶時,您都可以發出信號UI,然后等待響應者。 有關文件的信息可以存儲在變量中。

如果可能的話...我的建議是將長期運行的任務構造為原子操作。 然后,您可以創建可由后台線程和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);
    }
}

此代碼未經測試! 但是基本原理應該對您有用。

從這里答案的輸入中,我得出以下解決方案:

(錯誤)將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;
    }

我希望我不要忽略一些關鍵問題。 我對多線程不是很熟悉-也許某處應該有一些鎖(對象)?

具體針對您的情況

    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 ,但是顯然它可以是任何數據類型。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM