Reputation: 2616
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.
Upvotes: 5
Views: 4850
Reputation: 3404
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.
Upvotes: 6