Как обновить свойства визуального элемента управления (TextBlock.text), установленные внутри цикла? - PullRequest
12 голосов
/ 31 марта 2011

С каждой итерацией цикла я хочу визуально обновлять текст текстового блока. Моя проблема заключается в том, что окно или элемент управления WPF не обновляются визуально до завершения цикла.

for (int i = 0; i < 50; i++)
{
    System.Threading.Thread.Sleep(100);
    myTextBlock.Text = i.ToString();                
}

В VB6 я бы назвал DoEvents() или control.Refresh. На данный момент я бы хотел быстрое и грязное решение, похожее на DoEvents(), но я также хотел бы узнать об альтернативах или «правильном» способе сделать это. Можно ли добавить простое обязательное заявление? Какой синтаксис? Заранее спасибо.

Ответы [ 5 ]

26 голосов
/ 01 апреля 2011

Если вы действительно хотите быструю и грязную реализацию и не заботитесь о поддержке продукта в будущем или о пользовательском опыте, вы можете просто добавить ссылку на 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 (), который раздражает пользователя, и вы можете получить непредсказуемые результаты в зависимости от сложности программы (я видел одно приложение, в котором два метода выполнялись наПоток пользовательского интерфейса, каждый из которых вызывает DoEvents () несколько раз ...).

Вот как это должно быть сделано:

  1. В любое время, когда ваше приложение должнождать, пока что-то произойдет (например, чтение с диска, вызов веб-службы или Sleep ()), это должно происходить в отдельном потоке.
  2. Не следует устанавливать 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, и он идет вместе с небольшой практикой - его стоит изучить.

5 голосов
/ 31 мая 2011

Я попробовал решение, представленное здесь, и оно не работало для меня, пока я не добавил следующее:

Создайте метод расширения и убедитесь, что вы ссылаетесь на содержащую его сборку из вашего проекта.

public static void Refresh(this UIElement uiElement){
    uiElement.Dispatcher.Invoke(DispatcherPriority.Background, new Action( () => { }));
}

Затем вызовите его сразу после RaisePropertyChanged:

RaisePropertyChanged("MyValue");
myTextBlock.Refresh();

Это заставит поток пользовательского интерфейса ненадолго получить контроль и отправит все ожидающие изменения в элемент пользовательского интерфейса.

4 голосов
/ 31 марта 2011

Вот как вы это сделаете:

ThreadPool.QueueUserWorkItem(ignored =>
    {
        for (int i = 0; i < 50; i++) {
            System.Threading.Thread.Sleep(100);
            myTextBlock.Dispatcher.BeginInvoke(
                new Action(() => myTextBlock.Text = i.ToString()));
        }
    });

Это делегирует операцию потоку рабочего пула, который позволяет вашему потоку пользовательского интерфейса обрабатывать сообщения (и предотвращает зависание вашего пользовательского интерфейса). Поскольку рабочий поток не может получить доступ к myTextBlock напрямую, ему необходимо использовать BeginInvoke.

Хотя этот подход не является «способом WPF» для выполнения каких-либо задач, в этом нет ничего плохого (и, действительно, я не верю, что с этим небольшим кодом есть альтернатива). Но другой способ сделать это будет следующим:

  1. Свяжите Text TextBlock со свойством некоторого объекта, который реализует INotifyPropertyChanged
  2. В цикле установите для этого свойства желаемое значение; изменения будут автоматически переданы в пользовательский интерфейс
  3. Нет необходимости в потоках, 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}" />
1 голос
/ 31 марта 2011

Вы можете выполнить Dispatcher.BeginInvoke, чтобы выполнить переключение контекста потока, чтобы поток рендеринга мог выполнять свою работу. Тем не менее, это не правильный путь для таких вещей. Вы должны использовать Animation + Binding для подобных вещей, так как это бесполезный способ делать подобные вещи в WPF.

0 голосов
/ 07 мая 2014
for (int i = 0; i < 50; i++)
{
    System.Threading.Thread.Sleep(100);
    myTextBlock.Text = i.ToString();                
}

Запуск вышеуказанного кода внутри фонового рабочего компонента и использование привязки updatesourcetrigeer в качестве propertychanged немедленно отразит изменения в элементе управления пользовательского интерфейса.

...