Как обновить пользовательский интерфейс из другого потока, работающего в другом классе
В настоящее время я пишу свою первую программу на C#, и я очень плохо знаком с языком (до сих пор работал только с C). Я провел много исследований, но все ответы были слишком общими, и я просто не мог заставить его работать.
Итак, вот моя (очень распространенная) проблема: у меня есть приложение WPF, которое принимает входные данные из нескольких текстовых полей, заполненных пользователем, а затем использует их для выполнения с ними большого количества вычислений. Они должны занять около 2-3 минут, поэтому я хотел бы обновить индикатор выполнения и текстовый блок, сообщающий мне, каков текущий статус. Также мне нужно сохранить входные данные пользовательского интерфейса и передать их потоку, поэтому у меня есть третий класс, который я использую для создания объекта и хотел бы передать этот объект в фоновый поток. Очевидно, что я выполняю вычисления в другом потоке, чтобы пользовательский интерфейс не зависал, но я не знаю, как обновить пользовательский интерфейс, поскольку все методы вычисления являются частью другого класса. После многих исследований я думаю, что лучшим способом будет использовать диспетчеров и TPL, а не фонового работника, но, честно говоря, я не уверен, как они работают, и после примерно 20 часов проб и ошибок с другими ответами я решил спросить вопрос сам
Вот очень простая структура моей программы:
public partial class MainWindow : Window
{
public MainWindow()
{
Initialize Component();
}
private void startCalc(object sender, RoutedEventArgs e)
{
inputValues input = new inputValues();
calcClass calculations = new calcClass();
try
{
input.pota = Convert.ToDouble(aVar.Text);
input.potb = Convert.ToDouble(bVar.Text);
input.potc = Convert.ToDouble(cVar.Text);
input.potd = Convert.ToDouble(dVar.Text);
input.potf = Convert.ToDouble(fVar.Text);
input.potA = Convert.ToDouble(AVar.Text);
input.potB = Convert.ToDouble(BVar.Text);
input.initStart = Convert.ToDouble(initStart.Text);
input.initEnd = Convert.ToDouble(initEnd.Text);
input.inita = Convert.ToDouble(inita.Text);
input.initb = Convert.ToDouble(initb.Text);
input.initc = Convert.ToDouble(initb.Text);
}
catch
{
MessageBox.Show("Some input values are not of the expected Type.", "Wrong Input", MessageBoxButton.OK, MessageBoxImage.Error);
}
Thread calcthread = new Thread(new ParameterizedThreadStart(calculations.testMethod);
calcthread.Start(input);
}
public class inputValues
{
public double pota, potb, potc, potd, potf, potA, potB;
public double initStart, initEnd, inita, initb, initc;
}
public class calcClass
{
public void testmethod(inputValues input)
{
Thread.CurrentThread.Priority = ThreadPriority.Lowest;
int i;
//the input object will be used somehow, but that doesn't matter for my problem
for (i = 0; i < 1000; i++)
{
Thread.Sleep(10);
}
}
}
Я был бы очень признателен, если бы у кого-то было простое объяснение, как обновить пользовательский интерфейс внутри метода тестирования. Поскольку я новичок в C# и объектно-ориентированном программировании, слишком сложные ответы я, скорее всего, не пойму, хотя я сделаю все возможное.
Также, если у кого-то есть лучшая идея в целом (может быть, с помощью backgroundworker или что-то еще), я открыт, чтобы увидеть это.
8 ответов
Сначала вам нужно использовать Dispatcher.Invoke
чтобы изменить пользовательский интерфейс из другого потока и сделать это из другого класса, вы можете использовать события.
Затем вы можете зарегистрироваться на это событие (события) в основном классе и отправить изменения в пользовательский интерфейс, а в классе вычислений вы выбрасываете событие, когда хотите уведомить пользовательский интерфейс:
class MainWindow : Window
{
private void startCalc()
{
//your code
CalcClass calc = new CalcClass();
calc.ProgressUpdate += (s, e) => {
Dispatcher.Invoke((Action)delegate() { /* update UI */ });
};
Thread calcthread = new Thread(new ParameterizedThreadStart(calc.testMethod));
calcthread.Start(input);
}
}
class CalcClass
{
public event EventHandler ProgressUpdate;
public void testMethod(object input)
{
//part 1
if(ProgressUpdate != null)
ProgressUpdate(this, new YourEventArgs(status));
//part 2
}
}
ОБНОВИТЬ:
Поскольку кажется, что это все еще часто посещаемый вопрос и ответ, я хочу обновить этот ответ, указав, как бы я это делал сейчас (с.NET 4.5) - это немного дольше, поскольку я покажу некоторые другие возможности:
class MainWindow : Window
{
Task calcTask = null;
void buttonStartCalc_Clicked(object sender, EventArgs e) { StartCalc(); } // #1
async void buttonDoCalc_Clicked(object sender, EventArgs e) // #2
{
await CalcAsync(); // #2
}
void StartCalc()
{
var calc = PrepareCalc();
calcTask = Task.Run(() => calc.TestMethod(input)); // #3
}
Task CalcAsync()
{
var calc = PrepareCalc();
return Task.Run(() => calc.TestMethod(input)); // #4
}
CalcClass PrepareCalc()
{
//your code
var calc = new CalcClass();
calc.ProgressUpdate += (s, e) => Dispatcher.Invoke((Action)delegate()
{
// update UI
});
return calc;
}
}
class CalcClass
{
public event EventHandler<EventArgs<YourStatus>> ProgressUpdate; // #5
public TestMethod(InputValues input)
{
//part 1
ProgressUpdate.Raise(this, status); // #6 - status is of type YourStatus
//part 2
}
}
static class EventExtensions
{
public static void Raise<T>(this EventHandler<EventArgs<T>> theEvent,
object sender, T args)
{
if (theEvent != null)
theEvent(sender, new EventArgs<T>(args));
}
}
@ 1) Как запустить "синхронные" вычисления и запустить их в фоновом режиме
@2) Как запустить его "асинхронно" и "ожидать": здесь вычисление выполняется и завершается до возврата метода, но из-за async
/ await
пользовательский интерфейс не заблокирован (кстати: такие обработчики событий являются единственными допустимыми async void
так как обработчик события должен вернуть void
- использовать async Task
во всех остальных случаях)
@ 3) вместо нового Thread
теперь мы используем Task
, Чтобы потом иметь возможность проверить его (успешное) завершение, мы сохраняем его в глобальном calcTask
член. В фоновом режиме это также запускает новый поток и выполняет действие там, но это намного проще в обработке и имеет некоторые другие преимущества.
@ 4) Здесь мы также запускаем действие, но на этот раз мы возвращаем задачу, поэтому "обработчик асинхронных событий" может "ожидать его". Мы могли бы также создать async Task CalcAsync()
а потом await Task.Run(() => calc.TestMethod(input)).ConfigureAwait(false);
(К вашему сведению: ConfigureAwait(false)
чтобы избежать тупиков, вы должны прочитать об этом, если вы используете async
/ await
как было бы много, чтобы объяснить здесь), что приведет к тому же рабочему процессу, но как Task.Run
является единственной "ожидаемой операцией" и является последней, которую мы можем просто вернуть в задачу и сохранить один переключатель контекста, что экономит некоторое время выполнения.
@ 5) Здесь я теперь использую "строго типизированное родовое событие", чтобы мы могли легко передавать и получать наш "объект статуса"
@6) Здесь я использую расширение, определенное ниже, которое (помимо простоты использования) решает возможные условия гонки в старом примере. Там могло случиться, что событие получило null
после if
-check, но перед вызовом, если обработчик событий был удален в другом потоке именно в этот момент. Это не может произойти здесь, так как расширения получают "копию" делегата события, и в той же ситуации обработчик все еще зарегистрирован внутри Raise
метод.
Я собираюсь бросить вам кривой мяч здесь. Если бы я сказал это однажды, я бы сказал это сто раз. Маршалинг операции, такие как Invoke
или же BeginInvoke
не всегда лучшие методы для обновления пользовательского интерфейса с прогрессом рабочего потока.
В этом случае обычно лучше, если рабочий поток публикует свою информацию о прогрессе в общей структуре данных, которую поток пользовательского интерфейса затем опрашивает через регулярные промежутки времени. Это имеет несколько преимуществ.
- Это нарушает тесную связь между пользовательским интерфейсом и рабочим потоком, что
Invoke
навязывает. - Поток пользовательского интерфейса должен диктовать, когда элементы управления пользовательского интерфейса обновляются... так, как это должно быть в любом случае, когда вы действительно думаете об этом.
- Нет риска переполнения очереди сообщений пользовательского интерфейса, как в случае, если
BeginInvoke
были использованы из рабочего потока. - Рабочий поток не должен ждать ответа от потока пользовательского интерфейса, как в случае с
Invoke
, - Вы получаете большую пропускную способность как в пользовательском интерфейсе, так и в рабочих потоках.
Invoke
а такжеBeginInvoke
дорогие операции.
Так в вашем calcClass
создайте структуру данных, которая будет содержать информацию о прогрессе.
public class calcClass
{
private double percentComplete = 0;
public double PercentComplete
{
get
{
// Do a thread-safe read here.
return Interlocked.CompareExchange(ref percentComplete, 0, 0);
}
}
public testMethod(object input)
{
int count = 1000;
for (int i = 0; i < count; i++)
{
Thread.Sleep(10);
double newvalue = ((double)i + 1) / (double)count;
Interlocked.Exchange(ref percentComplete, newvalue);
}
}
}
Тогда в вашем MainWindow
класс использовать DispatcherTimer
периодически опрашивать информацию о прогрессе. Настройте DispatcherTimer
поднять Tick
событие на любом интервале, наиболее подходящем для вашей ситуации.
public partial class MainWindow : Window
{
public void YourDispatcherTimer_Tick(object sender, EventArgs args)
{
YourProgressBar.Value = calculation.PercentComplete;
}
}
Вы правы, что вы должны использовать Dispatcher
обновить элементы управления в потоке пользовательского интерфейса, а также правильно, что долго выполняющиеся процессы не должны выполняться в потоке пользовательского интерфейса. Даже если вы выполняете длительный процесс асинхронно в потоке пользовательского интерфейса, это может вызвать проблемы с производительностью.
Следует отметить, что Dispatcher.CurrentDispatcher
вернет диспетчер для текущего потока, не обязательно для потока пользовательского интерфейса. Я думаю, что вы можете использовать Application.Current.Dispatcher
чтобы получить ссылку на диспетчер потока пользовательского интерфейса, если он вам доступен, но если нет, вам придется передать диспетчер пользовательского интерфейса в фоновый поток.
Обычно я использую параллельную библиотеку задач для потоковых операций вместо BackgroundWorker
, Мне просто проще в использовании.
Например,
Task.Factory.StartNew(() =>
SomeObject.RunLongProcess(someDataObject));
где
void RunLongProcess(SomeViewModel someDataObject)
{
for (int i = 0; i <= 1000; i++)
{
Thread.Sleep(10);
// Update every 10 executions
if (i % 10 == 0)
{
// Send message to UI thread
Application.Current.Dispatcher.BeginInvoke(
DispatcherPriority.Normal,
(Action)(() => someDataObject.ProgressValue = (i / 1000)));
}
}
}
Все, что взаимодействует с пользовательским интерфейсом, должно вызываться в потоке пользовательского интерфейса (если это не замороженный объект). Для этого вы можете воспользоваться диспетчером.
var disp = /* Get the UI dispatcher, each WPF object has a dispatcher which you can query*/
disp.BeginInvoke(DispatcherPriority.Normal,
(Action)(() => /*Do your UI Stuff here*/));
Я использую BeginInvoke здесь, обычно фоновщик не должен ждать обновления пользовательского интерфейса. Если вы хотите подождать, вы можете использовать Invoke
, Но вы должны быть осторожны, чтобы не вызывать BeginInvoke слишком часто, это может стать очень неприятным.
Кстати, класс BackgroundWorker помогает с подобными задачами. Он позволяет сообщать об изменениях, например, в процентах, и автоматически отправляет их из фонового потока в поток пользовательского интерфейса. Для большинства задач по обновлению пользовательского интерфейса BackgroundWorker - отличный инструмент.
Вам придется вернуться к своей основной теме (также называемой UI thread
) чтобы update
пользовательский интерфейс. Любой другой поток, пытающийся обновить ваш интерфейс, просто вызовет exceptions
быть брошенным повсюду.
Так как вы находитесь в WPF, вы можете использовать Dispatcher
а конкретнее beginInvoke
на этом dispatcher
, Это позволит вам выполнить то, что нужно сделать (как правило, обновить пользовательский интерфейс) в потоке пользовательского интерфейса.
Вы также можете "зарегистрировать" UI
в вашем business
, поддерживая ссылку на элемент управления / форму, чтобы вы могли использовать его dispatcher
,
Если это долгий расчет, то я бы пошел фоновый рабочий. Имеет поддержку прогресса. Также есть поддержка отмены.
http://msdn.microsoft.com/en-us/library/cc221403(v=VS.95).aspx
Здесь у меня есть TextBox, связанный с содержанием.
private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
Debug.Write("backgroundWorker_RunWorkerCompleted");
if (e.Cancelled)
{
contents = "Cancelled get contents.";
NotifyPropertyChanged("Contents");
}
else if (e.Error != null)
{
contents = "An Error Occured in get contents";
NotifyPropertyChanged("Contents");
}
else
{
contents = (string)e.Result;
if (contentTabSelectd) NotifyPropertyChanged("Contents");
}
}
Слава Богу, Microsoft получила это в WPF:)
каждый Control
, как индикатор выполнения, кнопка, форма и т. д. имеет Dispatcher
в теме. Вы можете дать Dispatcher
Action
это должно быть выполнено, и он автоматически вызовет его в правильном потоке (Action
это как делегат функции).
Вы можете найти пример здесь.
Конечно, вы должны иметь доступ к элементу управления из других классов, например, сделав его public
и передать ссылку на Window
к вашему другому классу, или, возможно, передавая ссылку только на индикатор выполнения.
Почувствовал необходимость добавить этот лучший ответ, так как ничего кроме BackgroundWorker
казалось, помог мне, и ответ, касающийся этого до сих пор, был ужасно неполным. Вот так вы бы обновили страницу XAML под названием MainWindow
который имеет тег изображения, как это:
<Image Name="imgNtwkInd" Source="Images/network_on.jpg" Width="50" />
с BackgroundWorker
процесс, чтобы показать, если вы подключены к сети или нет:
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
public partial class MainWindow : Window
{
private BackgroundWorker bw = new BackgroundWorker();
public MainWindow()
{
InitializeComponent();
// Set up background worker to allow progress reporting and cancellation
bw.WorkerReportsProgress = true;
bw.WorkerSupportsCancellation = true;
// This is your main work process that records progress
bw.DoWork += new DoWorkEventHandler(SomeClass.DoWork);
// This will update your page based on that progress
bw.ProgressChanged += new ProgressChangedEventHandler(bw_ProgressChanged);
// This starts your background worker and "DoWork()"
bw.RunWorkerAsync();
// When this page closes, this will run and cancel your background worker
this.Closing += new CancelEventHandler(Page_Unload);
}
private void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
BitmapImage bImg = new BitmapImage();
bool connected = false;
string response = e.ProgressPercentage.ToString(); // will either be 1 or 0 for true/false -- this is the result recorded in DoWork()
if (response == "1")
connected = true;
// Do something with the result we got
if (!connected)
{
bImg.BeginInit();
bImg.UriSource = new Uri("Images/network_off.jpg", UriKind.Relative);
bImg.EndInit();
imgNtwkInd.Source = bImg;
}
else
{
bImg.BeginInit();
bImg.UriSource = new Uri("Images/network_on.jpg", UriKind.Relative);
bImg.EndInit();
imgNtwkInd.Source = bImg;
}
}
private void Page_Unload(object sender, CancelEventArgs e)
{
bw.CancelAsync(); // stops the background worker when unloading the page
}
}
public class SomeClass
{
public static bool connected = false;
public void DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = sender as BackgroundWorker;
int i = 0;
do
{
connected = CheckConn(); // do some task and get the result
if (bw.CancellationPending == true)
{
e.Cancel = true;
break;
}
else
{
Thread.Sleep(1000);
// Record your result here
if (connected)
bw.ReportProgress(1);
else
bw.ReportProgress(0);
}
}
while (i == 0);
}
private static bool CheckConn()
{
bool conn = false;
Ping png = new Ping();
string host = "SomeComputerNameHere";
try
{
PingReply pngReply = png.Send(host);
if (pngReply.Status == IPStatus.Success)
conn = true;
}
catch (PingException ex)
{
// write exception to log
}
return conn;
}
}
Для получения дополнительной информации: https://msdn.microsoft.com/en-us/library/cc221403(v=VS.95).aspx