【问题标题】:How do I refresh visual control properties (TextBlock.text) set inside a loop?如何刷新循环内设置的可视控件属性 (TextBlock.text)?
【发布时间】:2011-07-27 03:07:32
【问题描述】:

每次循环迭代时,我都想直观地更新文本块的文本。我的问题是 WPF 窗口或控件在循环完成之前不会在视觉上刷新。

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

在 VB6 中,我会调用 DoEvents()control.Refresh。目前我只想要一个类似于DoEvents() 的快速而肮脏的解决方案,但我也想知道替代方案或“正确”的方法来做到这一点。我可以添加一个简单的绑定语句吗?语法是什么? 提前致谢。

【问题讨论】:

    标签: wpf wpf-controls binding inotifypropertychanged


    【解决方案1】:

    您可以执行 Dispatcher.BeginInvoke 以执行线程上下文切换,以便渲染线程可以完成其工作。 但是,这不是处理此类事情的正确方法。你应该使用 Animation + Binding 来完成类似的事情,因为这是在 WPF 中做类似事情的免破解方式。

    【讨论】:

    • 只有当 UI 更新是纯粹的吸引眼球时,您才能使用动画。如果您确实希望 UI 反映某个进程的状态,则无法做到这一点。
    • 我不同意。动画也可以做到这一点。您唯一需要做的就是在您希望动画发生的地方实现一个 DP。
    • @EladKatz:当然,您可以绑定到属性并使用适当的转换器来实现 UI 更改。动画从何而来?
    • 您为 DP 制作动画。 TextBox 将绑定到该 DP。
    • @EladKatz:当你想让它反映进程的状态时,为什么要为它制作动画?
    【解决方案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 线程处理消息(并防止您的 UI 冻结)。因为工作线程不能直接访问myTextBlock,所以需要使用BeginInvoke

    虽然这种方法不是“WPF 方式”做事,但它没有任何问题(事实上,我不相信这个小代码有任何替代方案)。但另一种做事方式是这样的:

    1. TextBlockText 绑定到实现INotifyPropertyChanged 的某个对象的属性
    2. 在循环中,将该属性设置为您想要的值;更改将自动传播到 UI
    3. 不需要线程、BeginInvoke 或其他任何东西

    如果你已经有一个对象并且 UI 可以访问它,这就像写 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}" />
    

    【讨论】:

    • 唯一实际做的是 Dispatcher.BeginInvoke。我之前已经说过...
    • @EladKatz:将代码移动到另一个线程什么都不做?我想是的,如果将您的 UI 冻结 100 毫秒算作“无”。
    • +1 表示答案第二部分的 3 个步骤。这完美地工作并且很容易实现。这也是 MVVM 的重要组成部分。
    • 虽然您的代码示例运行良好,但它太复杂了,我无法记住。我可以为 this.MyProperty 使用什么绑定语法?我快速而肮脏的形式只使用背后的代码 - 没有外部对象。我没有得到正确的绑定语法。如何用引用 WPF 窗口上的属性的绑定语句替换您的 MyObject.MyProperty?我不会直接设置 myTextBlock.text,而是更新 Window 属性,该属性又会更新绑定的 textblock.text。
    【解决方案3】:

    如果您真的想要快速而肮脏的实现,并且不关心将来维护产品或用户体验,您可以添加对 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,这会惹恼用户,并且您最终可能会根据程序的复杂性得出不可预测的结果(我见过一个应用程序,其中两个方法在UI线程,每一个都重复调用DoEvents()...)。

    应该这样做:

    1. 任何时候您的应用程序必须等待某些事情发生(即磁盘读取、Web 服务调用或 Sleep()),它都应该在一个单独的线程上。
    2. 您不应手动设置 TextBlock.Text - 将其绑定到属性并实现 INotifyPropertyChanged。

    这是一个示例,展示了您所要求的功能。编写只需要几秒钟的时间,而且使用起来更容易 - 而且它不会锁定 UI。

    Xaml:

    <StackPanel>
        <TextBlock Name="MyTextBlock" Text="{Binding Path=MyValue}"></TextBlock>
        <Button Click="Button_Click">OK</Button>
    </StackPanel>
    

    代码隐藏:

    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 的基石,并且需要一些练习 - 非常值得学习。

    【讨论】:

    • 太棒了!其他答案很好,但我最了解这个答案,它包括我要求的快速而肮脏的解决方案。为了使 Greg 的解决方案发挥作用,我添加了: using System.ComponentModel; (和)使用 System.Threading.Tasks;。不要忘记参考 System.Windows.Forms 以使 DoEvents() 解决方案起作用。我学到了很多东西。谢谢!
    • 这段代码在 .net 3.5 中是什么样的(特别是 Task.Factory.StartNew)
    • Task.Factory.StartNew 是在 .net 4 中引入的。只需搜索 .net 3.5 threading 即可找到遗留机制。
    【解决方案4】:

    我尝试了此处公开的解决方案,但在我添加以下内容之前它对我不起作用:

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

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

    然后在RaisePropertyChanged之后直接调用:

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

    这将强制 UI 线程控制一小段时间并调度 UI 元素上的任何未决更改。

    【讨论】:

      【解决方案5】:
      for (int i = 0; i < 50; i++)
      {
          System.Threading.Thread.Sleep(100);
          myTextBlock.Text = i.ToString();                
      }
      

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-04-17
        • 2018-05-19
        相关资源
        最近更新 更多