Запустить код в потоке пользовательского интерфейса без объекта управления

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

любые идеи, спасибо Мартин


person Martin Moser    schedule 19.01.2009    source источник
comment
Пожалуйста, отметьте ответ как принятый, если он решит вашу проблему.   -  person SandRock    schedule 24.02.2012


Ответы (5)


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

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

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

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

person Judah Gabriel Himango    schedule 02.02.2009
comment
SynchronizationContext.Current имеет значение null, когда я пытаюсь его вызвать... D: - person Ling Xing; 15.03.2013
comment
Получите доступ к SyncrhonizationContext.Current, находясь в потоке пользовательского интерфейса. Сохраните это на потом, когда будете в другой теме. - person Judah Gabriel Himango; 16.03.2013

Во-первых, в конструкторе формы сохраните ссылку в классе на объект 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

person SandRock    schedule 24.02.2012

Вы правы, нехорошо передавать управление потокам. Элементы управления 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)
        }
    }
}
person Dour High Arch    schedule 20.01.2009
comment
Я предпочитаю этот метод, поскольку он четко отделяет код пользовательского интерфейса от фоновой работы. Это также позволяет нескольким слушателям реагировать на обновления статуса фоновой работы. - person Mike Caron; 26.11.2013

Поместите манипуляцию с пользовательским интерфейсом в метод формы, которой нужно управлять, и передайте делегат коду, который выполняется в фоновом потоке, а-ля 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
  }
}
person Peter Wone    schedule 28.01.2009

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

person Maurice Flanagan    schedule 02.02.2009