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

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

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

Ответ 1

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

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

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

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

Ответ 2

Вы правы, нехорошо передавать элементы управления потокам. Элементы управления 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)
        }
    }
}

Ответ 3

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

Ответ 4

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

Ответ 5

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