Выполнить код в потоке пользовательского интерфейса без объекта управления - PullRequest
11 голосов
/ 19 января 2009

Я сейчас пытаюсь написать компонент, где некоторые его части должны работать в потоке пользовательского интерфейса (объяснение будет длинным). Таким образом, самый простой способ - передать ему элемент управления и использовать для него InvokeRequired / Invoke. Но я не думаю, что это хороший дизайн, чтобы передавать ссылку на элемент управления «data / background» -компоненту, поэтому я ищу способ запуска кода в потоке пользовательского интерфейса без необходимости иметь доступный элемент управления , Что-то вроде Application.Dispatcher.Invoke в WPF ...

любые идеи, Спасибо Martin

Ответы [ 5 ]

19 голосов
/ 03 февраля 2009

Есть лучший, более абстрактный способ сделать это, который работает как на WinForms, так и на WPF:

System.Threading.SynchronizationContext.Current.Post(theMethod, state);

Это работает, потому что WindowsForms устанавливает объект WindowsFormsSynchronizationContext в качестве текущего контекста синхронизации. WPF делает нечто подобное, устанавливая собственный специализированный контекст синхронизации (DispatcherSynchronizationContext).

.Post соответствует control.BeginInvoke, а .Send соответствует control.Invoke.

2 голосов
/ 24 февраля 2012

Во-первых, в конструкторе формы сохраните ссылку в классе на объект SynchronizationContext.Current (который на самом деле WindowsFormsSynchronizationContext).

public partial class MyForm : Form {
    private SynchronizationContext syncContext;
    public MyForm() {
        this.syncContext = SynchronizationContext.Current;
    }
}

Затем в любом месте вашего класса используйте этот контекст для отправки сообщений в пользовательский интерфейс:

public partial class MyForm : Form {
    public void DoStuff() {
        ThreadPool.QueueUserWorkItem(_ => {
            // worker thread starts
            // invoke UI from here
            this.syncContext.Send(() =>
                this.myButton.Text = "Updated from worker thread");
            // continue background work
            this.syncContext.Send(() => {
                this.myText1.Text = "Updated from worker thread";
                this.myText2.Text = "Updated from worker thread";
            });
            // continue background work
        });
    }
}

Вам понадобятся следующие методы расширения для работы с лямбда-выражениями: http://codepaste.net/zje4k6

2 голосов
/ 20 января 2009

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

Создание этой двунаправленной связи между потоками, которая не блокирует поток пользовательского интерфейса, является большой работой. Вот очень сокращенный пример использования класса BackgroundWorker:

public class MyBackgroundThread : BackgroundWorker
{
    public event EventHandler<ClassToPassToUI> IWantTheUIToDoSomething;

    public MyStatus TheUIWantsToKnowThis { get { whatever... } }

    public void TheUIWantsMeToDoSomething()
    {
        // Do something...
    }

    protected override void OnDoWork(DoWorkEventArgs e)
    {
        // This is called when the thread is started
        while (!CancellationPending)
        {
            // The UI will set IWantTheUIToDoSomething when it is ready to do things.
            if ((IWantTheUIToDoSomething != null) && IHaveUIData())
                IWantTheUIToDoSomething( this, new ClassToPassToUI(uiData) );
        }
    }
}


public partial class MyUIClass : Form
{
    MyBackgroundThread backgroundThread;

    delegate void ChangeUICallback(object sender, ClassToPassToUI uiData);

    ...

    public MyUIClass
    {
        backgroundThread = new MyBackgroundThread();

        // Do this when you're ready for requests from background threads:
        backgroundThread.IWantTheUIToDoSomething += new EventHandler<ClassToPassToUI>(SomeoneWantsToChangeTheUI);

        // This will run MyBackgroundThread.OnDoWork in a background thread:
        backgroundThread.RunWorkerAsync();
    }


    private void UserClickedAButtonOrSomething(object sender, EventArgs e)
    {
        // Really this should be done in the background thread,
        // it is here as an example of calling a background task from the UI.
        if (backgroundThread.TheUIWantsToKnowThis == MyStatus.ThreadIsInAStateToHandleUserRequests)
            backgroundThread.TheUIWantsMeToDoSomething();

        // The UI can change the UI as well, this will not need marshalling.
        SomeoneWantsToChangeTheUI( this, new ClassToPassToUI(localData) );
    }

    void SomeoneWantsToChangeTheUI(object sender, ClassToPassToUI uiData)
    {
        if (InvokeRequired)
        {
            // A background thread wants to change the UI.
            if (iAmInAStateWhereTheUICanBeChanged)
            {
                var callback = new ChangeUICallback(SomeoneWantsToChangeTheUI);
                Invoke(callback, new object[] { sender, uiData });
            }
        }
        else
        {
            // This is on the UI thread, either because it was called from the UI or was marshalled.
            ChangeTheUI(uiData)
        }
    }
}
1 голос
/ 02 февраля 2009

А как насчет передачи System.ComponentModel.ISynchronizeInvoke? Таким образом, вы можете избежать передачи контроля.

1 голос
/ 28 января 2009

Поместите манипулирование пользовательским интерфейсом в метод в форме, которой нужно манипулировать, и передайте делегат коду, который выполняется в фоновом потоке, например, APM. Вам не нужно использовать params object p, вы можете строго ввести его в соответствии со своими целями. Это простой общий пример.

delegate UiSafeCall(delegate d, params object p);
void SomeUiSafeCall(delegate d, params object p)
{
  if (InvokeRequired) 
    BeginInvoke(d,p);        
  else
  {
    //do stuff to UI
  }
}

Этот подход основан на том факте, что делегат ссылается на метод в конкретном экземпляре; сделав реализацию методом формы, вы переводите форму в область действия this. Следующее семантически идентично.

delegate UiSafeCall(delegate d, params object p);
void SomeUiSafeCall(delegate d, params object p)
{
  if (this.InvokeRequired) 
    this.BeginInvoke(d,p);        
  else
  {
    //do stuff to UI
  }
}
...