Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
541 views
in Technique[技术] by (71.8m points)

c# - How to update progress bar while working in the UI thread

I have a ProgressBar and a TreeView.

I populated the TreeView with a bunch of data, once it is applied I run through the visual tree of the TreeView basically forcing it to generate each of the TreeViewItems. I would like the ProgressBar to show how this is progressing.

This is the behavior code that I run to create the TreeViewItems. It starts processing the items once ItemsLoaded property is set to true. It in turn updates a property in a singleton class to update the progress.

public class TreeViewBehaviors
{
    public static readonly DependencyProperty ItemsLoadedProperty =
        DependencyProperty.RegisterAttached("ItemsLoaded", typeof(bool), typeof(TreeViewBehaviors),
        new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnItemsLoadedPropertyChanged)));

    public static bool GetItemsLoaded(DependencyObject obj)
    {
        return (bool)obj.GetValue(ItemsLoadedProperty);
    }

    public static void SetItemsLoaded(DependencyObject obj, bool value)
    {
        obj.SetValue(ItemsLoadedProperty, value);
    }

    private static void OnItemsLoadedPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        if ((bool)e.NewValue)
        {
            GetTotalNTreeViewItems((TreeView)sender, sender);
        }
    }

    public static readonly DependencyProperty NodesProcessedProperty =
        DependencyProperty.RegisterAttached("NodesProcessed", typeof(int), typeof(TreeViewBehaviors),
        new FrameworkPropertyMetadata(default(int), new PropertyChangedCallback(OnNodesProcessedPropertyChanged)));

    public static int GetNodesProcessed(DependencyObject obj)
    {
        return (int)obj.GetValue(NodesProcessedProperty);
    }

    public static void SetNodesProcessed(DependencyObject obj, int value)
    {
        if (GetNodesProcessed(obj) != value)
        {
            obj.SetValue(NodesProcessedProperty, value);
        }
    }

    private static void OnNodesProcessedPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue != null)
        {
            double trouble = Math.Round(((GetProgressMaximum(sender) / GetTotalNodesToProcess(sender)) * (int)e.NewValue), 1);
            TreeViewSingletonClass.Instance.DisplayProgress = trouble;
        }
    }

    public static readonly DependencyProperty TotalNodesToProcessProperty =
        DependencyProperty.RegisterAttached("TotalNodesToProcess", typeof(double), typeof(TreeViewBehaviors),
        new FrameworkPropertyMetadata(default(double)));

    public static double GetTotalNodesToProcess(DependencyObject obj)
    {
        return (double)obj.GetValue(TotalNodesToProcessProperty);
    }

    public static void SetTotalNodesToProcess(DependencyObject obj, double value)
    {
        obj.SetValue(TotalNodesToProcessProperty, value);
    }


    public static readonly DependencyProperty ProgressMaximumProperty =
        DependencyProperty.RegisterAttached("ProgressMaximum", typeof(double), typeof(TreeViewBehaviors),
        new FrameworkPropertyMetadata(default(double)));

    public static double GetProgressMaximum(DependencyObject obj)
    {
        return (double)obj.GetValue(ProgressMaximumProperty);
    }

    public static void SetProgressMaximum(DependencyObject obj, double value)
    {
        obj.SetValue(ProgressMaximumProperty, value);
    }

    private static void GetTotalNTreeViewItems(ItemsControl container, DependencyObject sender)
    {
        if (container != null)
        {
            container.ApplyTemplate();
            ItemsPresenter itemsPresenter = (ItemsPresenter)container.Template.FindName("ItemsHost", container);
            if (itemsPresenter != null)
            {
                itemsPresenter.ApplyTemplate();
            }
            else
            {
                // The Tree template has not named the ItemsPresenter, 
                // so walk the descendents and find the child.
                itemsPresenter = FindVisualChild<ItemsPresenter>(container);
                if (itemsPresenter == null)
                {
                    container.UpdateLayout();
                    itemsPresenter = FindVisualChild<ItemsPresenter>(container);
                }
            }

            Panel itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);

            // Ensure that the generator for this panel has been created.
            UIElementCollection children = itemsHostPanel.Children;
            for (int i = 0, count = container.Items.Count; i < count; i++)
            {
                TreeViewItem subContainer = (TreeViewItem)container.ItemContainerGenerator.ContainerFromIndex(i);
                GetTotalNTreeViewItems(subContainer, sender);
                SetNodesProcessed(sender, GetNodesProcessed(sender) + 1);
            }
        }
    }

    private static T FindVisualChild<T>(Visual visual) where T : Visual
    {
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
        {
            Visual child = (Visual)VisualTreeHelper.GetChild(visual, i);
            if (child != null)
            {
                T correctlyTyped = child as T;
                if (correctlyTyped != null)
                    return correctlyTyped;

                T descendent = FindVisualChild<T>(child);
                if (descendent != null)
                    return descendent;
            }
        }
        return null;
    }
}

Singleton Class

public class TreeViewSingletonClass : INotifyPropertyChanged
{
    private static double m_DisplayProgress = 0;
    public double DisplayProgress
    {
        get { return m_DisplayProgress; }
        set
        {
            if (m_DisplayProgress == value)
                return;
            m_DisplayProgress = value;
            NotifyPropertyChanged();
        }
    }

    private static TreeViewSingletonClass m_Instance;
    public static TreeViewSingletonClass Instance
    {
        get
        {
            if (m_Instance == null)
                m_Instance = new TreeViewSingletonClass();
            return m_Instance;
        }
    }

    private TreeViewSingletonClass(){}

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

XAML:

<ProgressBar Grid.Column="2" Grid.Row="1" Margin="5" 
             Width="20" Height="150" 
             VerticalAlignment="Top" 
             Value="{Binding Source={x:Static helpers:TreeViewSingletonClass.Instance}, Path=DisplayProgress}" 
             Maximum="{Binding ProgressMaximum}"  />

My issue is, every thing is being processed correctly, just the ProgressBar is not updating until the very end. I realise that the two are both working inline on the same UI thread so that will be the problem.

So my question, with both of these working on the same thread how can I get this ProgressBar to update.

[EDIT]

This WPF is a UserControl in a WinForm ElementHost, I just placed the following into the WinForm so I can access Application.Current

if ( null == System.Windows.Application.Current )
{
   new System.Windows.Application();
}

After trying to implement the second suggestion of Xavier's: Split up the work into smaller pieces and queue those pieces up individually with the dispatcher using BeginInvoke (for example, convert the body of a loop into a dispatcher call)

So inside the for loop I stuck the following:

for (int i = 0, count = container.Items.Count; i < count; i++)
{
    Application.Current.Dispatcher.BeginInvoke(new Action(delegate()
    {
        TreeViewItem subContainer = (TreeViewItem)container.ItemContainerGenerator.ContainerFromIndex(i);
        GetTotalNTreeViewItems(subContainer, sender);
        SetNodesProcessed(sender, GetNodesProcessed(sender) + 1); 
    }));
}

Unfortunately this has not worked, must be doing something wrong.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

The UI thread in WPF uses the Dispatcher to schedule and process all updating of the UI. The dispatcher basically maintains a queue of tasks to run on the thread. If you are monopolizing the thread, the queue will just back up until you give it a chance to run again.

There are multiple potential solutions to your issue. Here are a couple...

Work on a separate thread

The solution I would likely consider first is moving your long-running task to another thread instead of taking over the UI thread. Any updates you need to make to the UI from that thread can be done by going through the Dispatcher for the UI thread using the BeginInvoke method. For example, if I wanted to add 1 to the value of a progress bar, I might do something like this:

Dispatcher.BeginInvoke(new Action(delegate() { mProgress.Value += 1.0; }));

Note: Make sure your worker thread has a way to reference the dispatcher from the UI thread. Don't call Dispatcher.CurrentDispatcher from the worker thread or you will get a dispatcher for that thread instead, which cannot access the UI. Instead, you can pass in the dispatcher to the thread, or access it via a member or property that was setup from the UI thread.

Use the Dispatcher to share the UI thread

If you really want to perform all of the work on the UI thread for one reason or another (which you might if you are doing a lot of visual tree walking or other UI-focused tasks), consider one of the following:

  • Split up the work into smaller pieces and queue those pieces up individually with the dispatcher using BeginInvoke. Make sure the priority is low enough that the UI updates will not get stuck waiting until the end. For example:

    for (int i = 0; i < 100; ++i)
    {
        Dispatcher.CurrentDispatcher.BeginInvoke(new Action(delegate()
        {
            mProgress.Value += 1.0;
            // Only sleeping to artificially simulate a long running operation
            Thread.Sleep(100);
        }), DispatcherPriority.Background);
    }
    
  • Process the dispatcher queue as desired during your long running operation. There is an example of creating a DoEvents method for this purpose in the "Remarks" section of the documentation for the PushFrame method.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...