如何刷新一个循环中的可视化控件属性(TextBlock.text) ?

时间:2022-09-02 13:24:38

With each loop iteration I want to visually update the text of a textblock. My problem is the WPF window or control does not visually refresh until the loop is complete.

在每次循环迭代中,我希望可视化地更新文本块的文本。我的问题是WPF窗口或控件在循环完成之前不会在视觉上刷新。

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

In VB6 I would call DoEvents() or control.refresh. At the moment I'd just like a quick and dirty solution similar to DoEvents(), but I'd also like to know about alternatives or the "right" way to do this. Is there a simple binding statement I could add? What is the syntax? Thanks in advance.

在VB6中,我将调用DoEvents()或control.refresh。现在,我想要一个类似于DoEvents()的快速而又肮脏的解决方案,但是我也想知道替代方法或者“正确”的方法。我是否可以添加一个简单的绑定语句?语法是什么?提前谢谢。

5 个解决方案

#1


25  

If you really want the quick and dirty implementation and don't care about maintaining the product in the future or about the user experience, you can just add a reference to System.Windows.Forms and call System.Windows.Forms.Application.DoEvents():

如果您真的想要快速和脏的实现,并且不关心在将来维护产品或用户体验,您可以添加对System.Windows的引用。形式和调用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();
}

The downside is that it's really really bad. You're going to lock up the UI during the Thread.Sleep(), which annoys the user, and you could end up with unpredictable results depending on the complexity of the program (I have seen one application where two methods were running on the UI thread, each one calling DoEvents() repeatedly...).

它的缺点是真的很糟糕。您将在thread . sleep()期间锁定UI,这会使用户感到厌烦,并且您可能会根据程序的复杂性而得出不可预测的结果(我看到过一个应用程序,其中两个方法在UI线程上运行,每个方法都重复调用DoEvents())。

This is how it should be done:

  1. Any time your application has to wait for something to happen (ie a disk read, a web service call, or a Sleep()), it should be on a separate thread.
  2. 任何时候,只要应用程序需要等待某些事情发生(比如磁盘读取、web服务调用或Sleep())),它都应该位于一个单独的线程上。
  3. You should not set TextBlock.Text manually - bind it to a property and implement INotifyPropertyChanged.
  4. 不应该设置TextBlock。手动-将其绑定到一个属性并实现INotifyPropertyChanged。

Here is an example showing the functionality you've asked for. It only takes a few seconds longer to write and it's so much easier to work with - and it doesn't lock up the UI.

这里有一个示例,展示了您所要求的功能。它只需要多花几秒钟的时间来编写,而且它非常容易处理——而且它不会锁定UI。

Xaml:

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;
}

The code might seem a bit complicated, but it's a cornerstone of WPF, and it comes together with a bit of practice - it's well worth learning.

代码看起来可能有点复杂,但它是WPF的基础,它结合了一些实践——非常值得学习。

#2


4  

This is how you would do it normally:

这是你通常的做法:

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

This delegates the operation to a worker pool thread, which allows your UI thread to process messages (and keeps your UI from freezing). Because the worker thread cannot access myTextBlock directly, it needs to use BeginInvoke.

这将操作委托给一个worker池线程,该线程允许UI线程处理消息(并防止UI冻结)。由于工作线程不能直接访问myTextBlock,因此需要使用BeginInvoke方法。

Although this approach is not "the WPF way" to do things, there's nothing wrong with it (and indeed, I don't believe there's any alternative with this little code). But another way to do things would go like this:

尽管这种方法不是“WPF方法”,但它没有任何问题(实际上,我认为这个小代码没有任何替代方法)。但是另一种方法是这样的:

  1. Bind the Text of the TextBlock to a property of some object that implements INotifyPropertyChanged
  2. 将TextBlock的文本绑定到实现INotifyPropertyChanged的某个对象的属性
  3. From within the loop, set that property to the value you want; the changes will be propagated to the UI automatically
  4. 在循环中,将该属性设置为所需的值;更改将自动传播到UI
  5. No need for threads, BeginInvoke, or anything else
  6. 不需要线程、BeginInvoke或其他任何东西

If you already have an object and the UI has access to it, this is as simple as writing Text="{Binding MyObject.MyProperty}".

如果您已经有一个对象,并且UI可以访问它,这就像编写文本=“{Binding myobjecter . myproperty}”一样简单。

Update: For example, let's assume you have this:

更新:例如,假设你有:

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; }
}

The binding would be done like this in XAML:

绑定将在XAML中这样做:

<TextBlock Text="{Binding PropertyName.Number}" />

#3


4  

I tried the solution exposed here and it didn't work for me until I added the following.

我尝试了这里显示的解决方案,直到我添加了以下内容,它才对我起作用。

Create an extension method and make sure you reference it's containing assembly from your project.

创建一个扩展方法,并确保从项目中引用它包含的程序集。

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

Then call it right after RaisePropertyChanged.

然后在RaisePropertyChanged之后立即调用它。

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

That will force the UIThread to take control for small while and dispatch any pending changes on the UIElement.

这将强制UIThread控制一小段时间,并在UIElement上分派任何挂起的更改。

#4


1  

You can do Dispatcher.BeginInvoke in order to do a thread context-switch so that the rendering thread could do its job. However, this is not the right way for such things. You should use Animation + Binding for things like that, as this is the hack-free way of doing things like that in WPF.

你可以做调度。BeginInvoke方法用于执行线程上下文切换,以便呈现线程能够完成它的工作。然而,这并不是正确的方式。您应该使用动画+绑定这样的东西,因为这是在WPF中做类似事情的无黑客的方式。

#5


0  

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

Running the above code inside a background worker component and using a binding updatesourcetrigeer as propertychanged will reflect the changes immediately in UI control

在后台工作组件中运行上述代码,并使用绑定updatesourcetrigeer作为propertychanged,将立即反映UI控件中的更改。

#1


25  

If you really want the quick and dirty implementation and don't care about maintaining the product in the future or about the user experience, you can just add a reference to System.Windows.Forms and call System.Windows.Forms.Application.DoEvents():

如果您真的想要快速和脏的实现,并且不关心在将来维护产品或用户体验,您可以添加对System.Windows的引用。形式和调用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();
}

The downside is that it's really really bad. You're going to lock up the UI during the Thread.Sleep(), which annoys the user, and you could end up with unpredictable results depending on the complexity of the program (I have seen one application where two methods were running on the UI thread, each one calling DoEvents() repeatedly...).

它的缺点是真的很糟糕。您将在thread . sleep()期间锁定UI,这会使用户感到厌烦,并且您可能会根据程序的复杂性而得出不可预测的结果(我看到过一个应用程序,其中两个方法在UI线程上运行,每个方法都重复调用DoEvents())。

This is how it should be done:

  1. Any time your application has to wait for something to happen (ie a disk read, a web service call, or a Sleep()), it should be on a separate thread.
  2. 任何时候,只要应用程序需要等待某些事情发生(比如磁盘读取、web服务调用或Sleep())),它都应该位于一个单独的线程上。
  3. You should not set TextBlock.Text manually - bind it to a property and implement INotifyPropertyChanged.
  4. 不应该设置TextBlock。手动-将其绑定到一个属性并实现INotifyPropertyChanged。

Here is an example showing the functionality you've asked for. It only takes a few seconds longer to write and it's so much easier to work with - and it doesn't lock up the UI.

这里有一个示例,展示了您所要求的功能。它只需要多花几秒钟的时间来编写,而且它非常容易处理——而且它不会锁定UI。

Xaml:

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;
}

The code might seem a bit complicated, but it's a cornerstone of WPF, and it comes together with a bit of practice - it's well worth learning.

代码看起来可能有点复杂,但它是WPF的基础,它结合了一些实践——非常值得学习。

#2


4  

This is how you would do it normally:

这是你通常的做法:

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

This delegates the operation to a worker pool thread, which allows your UI thread to process messages (and keeps your UI from freezing). Because the worker thread cannot access myTextBlock directly, it needs to use BeginInvoke.

这将操作委托给一个worker池线程,该线程允许UI线程处理消息(并防止UI冻结)。由于工作线程不能直接访问myTextBlock,因此需要使用BeginInvoke方法。

Although this approach is not "the WPF way" to do things, there's nothing wrong with it (and indeed, I don't believe there's any alternative with this little code). But another way to do things would go like this:

尽管这种方法不是“WPF方法”,但它没有任何问题(实际上,我认为这个小代码没有任何替代方法)。但是另一种方法是这样的:

  1. Bind the Text of the TextBlock to a property of some object that implements INotifyPropertyChanged
  2. 将TextBlock的文本绑定到实现INotifyPropertyChanged的某个对象的属性
  3. From within the loop, set that property to the value you want; the changes will be propagated to the UI automatically
  4. 在循环中,将该属性设置为所需的值;更改将自动传播到UI
  5. No need for threads, BeginInvoke, or anything else
  6. 不需要线程、BeginInvoke或其他任何东西

If you already have an object and the UI has access to it, this is as simple as writing Text="{Binding MyObject.MyProperty}".

如果您已经有一个对象,并且UI可以访问它,这就像编写文本=“{Binding myobjecter . myproperty}”一样简单。

Update: For example, let's assume you have this:

更新:例如,假设你有:

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; }
}

The binding would be done like this in XAML:

绑定将在XAML中这样做:

<TextBlock Text="{Binding PropertyName.Number}" />

#3


4  

I tried the solution exposed here and it didn't work for me until I added the following.

我尝试了这里显示的解决方案,直到我添加了以下内容,它才对我起作用。

Create an extension method and make sure you reference it's containing assembly from your project.

创建一个扩展方法,并确保从项目中引用它包含的程序集。

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

Then call it right after RaisePropertyChanged.

然后在RaisePropertyChanged之后立即调用它。

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

That will force the UIThread to take control for small while and dispatch any pending changes on the UIElement.

这将强制UIThread控制一小段时间,并在UIElement上分派任何挂起的更改。

#4


1  

You can do Dispatcher.BeginInvoke in order to do a thread context-switch so that the rendering thread could do its job. However, this is not the right way for such things. You should use Animation + Binding for things like that, as this is the hack-free way of doing things like that in WPF.

你可以做调度。BeginInvoke方法用于执行线程上下文切换,以便呈现线程能够完成它的工作。然而,这并不是正确的方式。您应该使用动画+绑定这样的东西,因为这是在WPF中做类似事情的无黑客的方式。

#5


0  

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

Running the above code inside a background worker component and using a binding updatesourcetrigeer as propertychanged will reflect the changes immediately in UI control

在后台工作组件中运行上述代码,并使用绑定updatesourcetrigeer作为propertychanged,将立即反映UI控件中的更改。