Как обновить свойства визуального контроля (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 будут немедленно отражать изменения в управлении пользовательским интерфейсом