Как обновить свойства визуального контроля (TextBlock.text), установленные внутри цикла?
С каждой итерацией цикла я хочу визуально обновить текст текстового блока. Моя проблема заключается в том, что окно WPF или элемент управления не визуально обновляется до завершения цикла.
for (int i = 0; i < 50; i++)
{
System.Threading.Thread.Sleep(100);
myTextBlock.Text = i.ToString();
}
В VB6 я бы назвал DoEvents() или control.refresh. На данный момент мне бы хотелось, чтобы быстрое и грязное решение было похоже на DoEvents(), но я также хотел бы узнать об альтернативах или "правильном" способе сделать это. Я могу добавить простой оператор привязки? Что такое синтаксис?
Спасибо заранее.
Ответы
Ответ 1
Если вы действительно хотите выполнить быструю и грязную реализацию и не заботитесь о сохранении продукта в будущем или о работе пользователя, вы можете просто добавить ссылку на System.Windows.Forms и вызвать System.Windows.Forms.Application.DoEvents()
:
for (int i = 0; i < 50; i++)
{
System.Threading.Thread.Sleep(100);
MyTextBlock.Text = i.ToString();
System.Windows.Forms.Application.DoEvents();
}
Недостатком является то, что он действительно очень плохой. Вы заблокируете пользовательский интерфейс во время Thread.Sleep(), что раздражает пользователя, и вы можете получить непредсказуемые результаты в зависимости от сложности программы (я видел одно приложение, в котором два метода выполнялись на UI, каждый из которых вызывает DoEvents() несколько раз...).
Вот как это сделать:
- В любое время, когда ваше приложение должно ждать чего-то (например, чтение диска, вызов веб-службы или Sleep()), он должен находиться в отдельном потоке.
- Вы не должны вручную устанавливать TextBlock.Text - привязать его к свойству и реализовать INotifyPropertyChanged.
Вот пример, показывающий функциональность, о которой вы просили. Для записи требуется всего несколько секунд дольше, и с ним гораздо проще работать - и он не блокирует пользовательский интерфейс.
Xaml:
<StackPanel>
<TextBlock Name="MyTextBlock" Text="{Binding Path=MyValue}"></TextBlock>
<Button Click="Button_Click">OK</Button>
</StackPanel>
CodeBehind:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Task.Factory.StartNew(() =>
{
for (int i = 0; i < 50; i++)
{
System.Threading.Thread.Sleep(100);
MyValue = i.ToString();
}
});
}
private string myValue;
public string MyValue
{
get { return myValue; }
set
{
myValue = value;
RaisePropertyChanged("MyValue");
}
}
private void RaisePropertyChanged(string propName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Код может показаться немного сложным, но он является краеугольным камнем WPF, и он сочетается с некоторой практикой - это стоит изучать.
Ответ 2
Вот как вы обычно это делаете:
ThreadPool.QueueUserWorkItem(ignored =>
{
for (int i = 0; i < 50; i++) {
System.Threading.Thread.Sleep(100);
myTextBlock.Dispatcher.BeginInvoke(
new Action(() => myTextBlock.Text = i.ToString()));
}
});
Это делегирует операцию потоку рабочего пула, который позволяет потоку пользовательского интерфейса обрабатывать сообщения (и не позволяет вашему UI замораживаться). Поскольку рабочий поток не может напрямую обращаться к myTextBlock
, ему нужно использовать BeginInvoke
.
Хотя этот подход не является "способом WPF", чтобы делать что-то, в этом нет ничего плохого (и, действительно, я не считаю, что есть альтернатива этому маленькому коду). Но другой способ сделать это будет следующим:
- Привяжите
Text
к TextBlock
к свойству некоторого объекта, который реализует INotifyPropertyChanged
- Внутри цикла установите для этого свойства требуемое значение; изменения будут автоматически распространяться на пользовательский интерфейс.
- Нет необходимости в потоках,
BeginInvoke
или что-либо еще
Если у вас уже есть объект, и пользовательский интерфейс имеет к нему доступ, это так же просто, как писать Text="{Binding MyObject.MyProperty}"
.
Обновление: Например, предположим, что у вас есть это:
class Foo : INotifyPropertyChanged // you need to implement the interface
{
private int number;
public int Number {
get { return this.number; }
set {
this.number = value;
// Raise PropertyChanged here
}
}
}
class MyWindow : Window
{
public Foo PropertyName { get; set; }
}
Связывание будет выполнено так же, как в XAML:
<TextBlock Text="{Binding PropertyName.Number}" />
Ответ 3
Я попробовал решение, представленное здесь, и это не сработало для меня, пока я не добавил следующее.
Создайте метод расширения и убедитесь, что вы ссылаетесь на его сборку из вашего проекта.
public static void Refresh(this UIElement uiElement)
{
uiElement.Dispatcher.Invoke(DispatcherPriority.Background, new Action( () => { }));
}
Затем вызовите его сразу после RaisePropertyChanged.
RaisePropertyChanged("MyValue");
myTextBlock.Refresh();
Это заставит UIThread взять под контроль небольшое время и отправить любые ожидающие изменения в UIElement.
Ответ 4
Вы можете сделать Dispatcher.BeginInvoke, чтобы сделать контекстный переключатель потока, чтобы поток рендеринга мог выполнять свою работу.
Однако, это не правильный путь для таких вещей. Вы должны использовать анимацию + привязку для подобных вещей, так как это простой способ делать такие вещи в WPF.
Ответ 5
for (int i = 0; i < 50; i++)
{
System.Threading.Thread.Sleep(100);
myTextBlock.Text = i.ToString();
}
Выполнение вышеуказанного кода внутри рабочего компонента-компонента и использование привязки updateourcetrigeer как propertychanged будут немедленно отражать изменения в управлении пользовательским интерфейсом