System.Windows.Threading.Dispatcher и WinForms?
Делает System.Windows.Threading.Dispatcher
работать над UI-потоком WinForms
приложение?
Если да, то почему? Он исходит от WindowsBase.dll, которая кажется WPF
составная часть.
Если нет, как я могу вызвать рабочие блоки обратно в поток пользовательского интерфейса? я обнаружил Control.BeginInvoke()
, но кажется неуклюжим создавать элемент управления только для ссылки на исходный поток.
8 ответов
Ты можешь использовать Dispatcher
даже в приложении WinForms.
Если вы уверены, что находитесь в потоке пользовательского интерфейса (например, в обработчике button.Click), Dispatcher.CurrentDispatcher
предоставляет диспетчер потоков пользовательского интерфейса, который впоследствии можно использовать для отправки из фоновых потоков в поток пользовательского интерфейса, как обычно.
Dispatcher - это компонент WPF, а не компонент WinForms.
Если вы хотите отправить рабочие элементы в потоке пользовательского интерфейса, вам придется либо использовать Control.BeginInvoke, как вы уже нашли, либо реагировать на ResetEvents/WaitObjects в разных потоках.
Обычно вызов рабочих элементов в потоке пользовательского интерфейса - это плохо, если только это не часть работы пользовательского интерфейса (т. Е. Обновление содержимого элемента управления или чего-то еще), в этом случае будет достаточно Control.BeginInvoke().
Я привел пример использования System.Windows.Threading.Dispatcher
в форме Windows в моем ответе на вопрос "Параллельное программирование с использованием TPL на WinForms" с момента предыдущего ответа на ваш вопрос:
Если вы уверены, что находитесь в потоке пользовательского интерфейса (например, в обработчике button.Click), Dispatcher.CurrentDispatcher предоставляет вам диспетчер потока пользовательского интерфейса, который можно использовать для отправки из фонового потока в поток пользовательского интерфейса, как обычно.
либо вводит в заблуждение, либо сбивает с толку, либо отсутствует конкретный контекст использования:
button.Click
обработчик не гарантирует быть в потоке пользовательского интерфейса;- если вы не в потоке пользовательского интерфейса, все еще можно использовать диспетчер потока пользовательского интерфейса формы WinForms
Можно получить диспетчер потока WinForm UI:
Dispatcher dispatcherUI = Dispatcher.CurrentDispatcher;
либо в обработчике события нажатия кнопки, либо в другом месте (в конструкторе формы)
А затем используйте его для выполнения в пользовательском интерфейсе из других потоков, см. Более подробную информацию о примере ниже в моем ответе:
private void button1_Click(object sender, EventArgs e)
{
Dispatcher dispUI = Dispatcher.CurrentDispatcher;
for (int i = 2; i < 20; i++)
{
int j = i;
var t = Task.Factory.StartNew
(() =>
{
var result = SumRootN(j);
dispUI.BeginInvoke
(new Action
(() => richTextBox1.Text += "root " + j.ToString()
+ " " + result.ToString() + Environment.NewLine
)
, null
);
}
);
}
Используйте фоновый рабочий поток, поскольку он работает с сообщениями интерфейса пользователя. В этой статье MSDN, хотя в основном о WPF, говорится, что BWT поддерживает интерфейс даже для оконных форм.
Я использую ViewModels с привязкой данных непосредственно к свойствам самой ViewModel. В некоторых случаях свойства обновляются в другом потоке. Чтобы избежать сбоев, я использую Dispatcher. Когда создается экземпляр ViewModel, он фиксирует текущий диспетчер и может использовать его позже по мере необходимости.
Единственное предположение, которое я делаю, заключается в том, что сама ViewModel создается в основном потоке, и это легко гарантировать, поскольку мои ViewModels всегда создаются в конструкторе связанного представления (Form/Control), которое всегда запускается в потоке пользовательского интерфейса по дизайну. .
Я создал вспомогательные методы для установки значения свойства. Этот помощник вызывает RaisePropertyChanged. Я сделал "потокобезопасное" переопределение, которое можно использовать для обеспечения запуска возбужденного события в основном потоке. Когда это делается таким образом, компонент пользовательского интерфейса, привязанный к свойству, будет обновляться в потоке пользовательского интерфейса, даже если свойство было обновлено в другом потоке.
Так что для меня это выглядит примерно так:
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Dispatcher _dispatcher;
public ViewModelBase()
{
_dispatcher = Dispatcher.CurrentDispatcher;
}
protected void RaisePropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
RaisePropertyChanged(propertyName);
return true;
}
protected bool SetFieldOnMainThread<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
RunOnUiThread(() => RaisePropertyChanged(propertyName));
return true;
}
protected void RunOnUiThread(Action action)
{
if (action != null)
{
_dispatcher.Invoke(action);
}
}
}
// Used like this:
public class TestViewModel : ViewModelBase
{
private string _name;
public string Name {
get => _name;
set => SetFieldOnMainThread(ref _name, value);
}
}
У меня была похожая проблема с использованием класса зависимостей Oracle, который работает в своем собственном потоке в Winforms,
Когда событие OnChange сработало из Oracle Dependency, я хотел показать изменения в DataGridView, просто установив DataSource в eventargs.Details (который по сути является DataTable), и он выдает:System.InvalidOperationException не обрабатывается пользовательским кодом Message=Cross-thread operation недопустимо: элемент управления dataGridView1 доступен из потока, отличного от потока, в котором он был создан.
Пользователь Stackru Брайан Пейрис (bpeiris@gmail.com), мой коллега, показал мне следующее:
void dep_OnChange(object sender, OracleNotificationEventArgs arg)
{
Console.WriteLine("Notification received");
int infoSum = int.Parse(arg.Details.Compute("Sum(Info)", "Info is not null").ToString());
InfoSum x = (InfoSum)infoSum;
foreach (DataRow dr in arg.Details.Rows)
{
Console.WriteLine(string.Format("Operation(InfoSum)= {0}", Enum.GetName(typeof(InfoSum), x)));
Console.WriteLine(string.Format("ontable={0} Rowid={1},info={2}", dr.Field<string>("ResourceName"), dr.Field<string>("rowid"), dr.Field<Int32>("info")));
}
// Following will throw cross-thread
// dataGridView1.DataSource = arg.Details;
// instead of line above use the following
dataGridView1.BeginInvoke((Action)(()=>dataGridView1.DataSource = arg.Details));
IsNotified = true;
}
}
Иногда компонент Timer полезен и прост в настройке в WinForms, просто установите его интервал и затем включите его, а затем убедитесь, что первое, что вы делаете в обработчике событий Tick, - это отключение самого себя.
Я думаю, что Timer запускает код в своем собственном потоке, поэтому вам все равно может понадобиться выполнить BeginInvoke (вызывается из объекта WinForm [this]) для запуска вашего Action.
private WebBrowserDocumentCompletedEventHandler handler; //need to make it a class field for the handler below (anonymous delegates seem to capture state at point of definition, so they can't capture their own reference)
private string imageFilename;
private bool exit;
public void CaptureScreenshot(Uri address = null, string imageFilename = null, int msecDelay = 0, bool exit = false)
{
handler = (s, e) =>
{
webBrowser.DocumentCompleted -= handler; //must do first
this.imageFilename = imageFilename;
this.exit = exit;
timerScreenshot.Interval = (msecDelay > 0)? msecDelay : 1;
timerScreenshot.Enabled = true;
};
webBrowser.DocumentCompleted += handler;
Go(address); //if address == null, will use URL from UI
}
private void timerScreenshot_Tick(object sender, EventArgs e)
{
timerScreenshot.Enabled = false; //must do first
BeginInvoke((Action)(() => //Invoke at UI thread
{ //run in UI thread
BringToFront();
Bitmap bitmap = webBrowser.GetScreenshot();
if (imageFilename == null)
imageFilename = bitmap.ShowSaveFileDialog();
if (imageFilename != null)
{
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(imageFilename))); //create any parent directories needed
bitmap.Save(imageFilename);
}
bitmap.Dispose(); //release bitmap resources
if (exit)
Close(); //this should close the app, since this is the main form
}), null);
}
Вы можете увидеть выше в действии в инструменте WebCapture ( http://gallery.clipflair.net/WebCapture, исходный код по адресу: http://clipflair.codeplex.com/, см. папку Tools/WebCapture), который захватывает скриншоты с веб-сайтов. Кстати, если вы хотите вызвать исполняемый файл из командной строки, убедитесь, что вы идете в Свойства проекта и на вкладке Безопасность отключите безопасность ClickOnce (иначе он не может получить доступ к командной строке)
Посмотрите на фон и посмотрите, соответствует ли он вашим потребностям.