2014-11-07 2 views
0

в моем приложении WPF - C#, у меня есть трудоемкая функция, которую я выполняю с BackgroundWorker. Задача этой функции состоит в том, чтобы добавить данные из файла в базу данных. Время от времени мне нужна некоторая обратная связь с пользователем, например, данные уже находятся в хранилище, и я хочу спросить пользователя, хочет ли он объединить данные или создать новый объект или полностью пропустить данные. Как показано на окнах диалоговых окон, я пытаюсь скопировать файл в папку, где уже существует файл с тем же именем.Взаимодействие пользователя в потоке, отличном от UI?

Проблема в том, что я не могу вызвать GUI-окно из не-GUI-потока. Как я мог реализовать это поведение?

Спасибо заранее,
Frank

+0

Вы используете WPF? Или вы используете WinForms? –

+0

Я использую WPF ... добавил эту информацию к вопросу – Aaginor

+0

Вы делаете MVVM?Если да, то вы можете создать проект с помощью INotifyPropertyChanged, чтобы получить событие в View из ViewModel (или даже модели); Я могу указать или указать вам ответ, если вы используете MVVM. –

ответ

0

От входа ответов здесь, я пришел к следующему решению:

(Mis) Используя ReportProgress-метод BackgroundWorker в комбинации с 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; 
    } 

Надеюсь, я не упустил некоторые важные проблемы. Я не настолько популярен в многопоточности - возможно, где-то должен быть какой-то замок (объект)?

1

Вы могли бы работать с EventWaitHandle оу AutoResetEvent, то всякий раз, когда вы хотите подсказать пользователю, вы могли бы сигнал UI, а затем ждать Отвечает. Информация о файле может храниться в переменной.

+0

Спасибо за ответ! Я проверил about TwoWaySignaling по адресу http://www.albahari.com/threading/part2.aspx Насколько я понимаю, поток GUI блокируется этим методом, потому что он ждет события из потока в фоновом режиме. Но я не хочу, чтобы графический интерфейс блокировался. – Aaginor

+0

Вы можете попробовать работать с 3 потоками: поток GUI, поток обработки и поток слушателя. Затем, когда вы начинаете метод yout, GUI запускает слушатель и поток обработки. Нить слушателя будет ждать сигнала из потока обработки, а затем, когда сигнализируется, он будет ссылаться на ваш метод подтверждения. – HDD

+0

Но нить слушателя не будет считаться графическим потоком и поэтому не сможет взаимодействовать с пользователем, или я ошибаюсь? – Aaginor

0

Если возможно ... мое предложение состоит в том, чтобы архитектор вашей долгосрочной задачи в атомарные операции. Затем вы можете создать очередь предметов, доступных как фоновым потоком, так и потоком пользовательского интерфейса.

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

Вы можете использовать что-то вроде этого класса. Он использует очередь для управления выполнением своих рабочих элементов, а также наблюдаемую коллекцию сигнала интерфейса:

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

Этот код не тестировался! Но основной принцип должен работать на вас.

+0

Это выглядит довольно элегантно, но я боюсь, что это вызовет больше проблем. Если использовать эту идею, я мог бы просто вызвать задачи из mainthread. Вероятно, это также будет проблемой, если одна задача опирается на информацию о другой задаче, то есть в состоянии «Требуется защита». – Aaginor

+0

Да, определенно. Для этого задачи должны быть полностью изолированы друг от друга. – rikkit

0

Специально для Вашего случая

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, но это может быть любой тип данных, очевидно.

Смежные вопросы