Выполнить код в потоке пользовательского интерфейса без объекта управления
В настоящее время я пытаюсь написать компонент, где некоторые его части должны запускаться в потоке пользовательского интерфейса (объяснение будет длинным).
Таким образом, самый простой способ - передать ему контроль и использовать 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? Таким образом, вы можете избежать передачи элемента управления.