WaitSpin, ProgresPanel and threads





5.00/5 (3 votes)
WPF Progress controls and comparing related threads methods
- Download source code : WpfAppSlidingBar.zip - 54.6 KB
- Download demo : WpfAppSlidingBar_Release.zip - 29.2 KB
Introduction
With my project CBR on CodePlex, I had to work on the new Task Parallel Library to be compared with old classical threads. This leads me on infinite progress bar control and a non-blocking or intrusive progress UI.
To share my work, you will find below:
- How to write a WaitSpin control and create several designs in Expression Designer...
- Create a custom ItemsControl to be a sliding panel with multi-progress cancelable items...
- Multi-Threading : Threads, Taks and Background worker...
Screenshots
WaitSpin control
The Code
It is derivated from the Control
class. It is very simple,
define a few properties and methods. The requirement for the template is to have a
PART_LoadingAnimation
that define a storyboard to animate the control.
/// <summary>
/// Enumeration for representing state of an animation.
/// </summary>
public enum AnimationState
{
/// <summary>
/// The animation is playing.
/// </summary>
Playing,
/// <summary>
/// The animation is paused.
/// </summary>
Paused,
/// <summary>
/// The animation is stopped.
/// </summary>
Stopped
}
/// <summary>
/// A control that shows a loading animation.
/// </summary>
public class WaitSpin : Control
{
#region --------------------CONSTRUCTORS--------------------
static WaitSpin()
{
FrameworkElement.DefaultStyleKeyProperty.OverrideMetadata(typeof(WaitSpin),
new FrameworkPropertyMetadata(typeof(WaitSpin)));
}
/// <summary>
/// LoadingAnimation constructor.
/// </summary>
public WaitSpin()
{
this.DefaultStyleKey = typeof(WaitSpin);
}
#endregion
#region --------------------DEPENDENCY PROPERTIES--------------------
#region -------------------- fill--------------------
/// <summary>
/// fill property.
/// </summary>
public static readonly DependencyProperty ShapeFillProperty =
DependencyProperty.Register("ShapeFill", typeof(Brush), typeof(WaitSpin), null);
/// <summary>
/// Gets or sets the fill.
/// </summary>
[System.ComponentModel.Category("Loading Animation Properties"), System.ComponentModel.Description("The fill for the shapes.")]
public Brush ShapeFill
{
get { return (Brush)GetValue(ShapeFillProperty); }
set { SetValue(ShapeFillProperty, value); }
}
#endregion
#region -------------------- stroke--------------------
/// <summary>
/// Ellipse stroke property.
/// </summary>
public static readonly DependencyProperty ShapeStrokeProperty =
DependencyProperty.Register("ShapeStroke", typeof(Brush), typeof(WaitSpin), null);
/// <summary>
/// Gets or sets the ellipse stroke.
/// </summary>
[System.ComponentModel.Category("Loading Animation Properties"), System.ComponentModel.Description("The stroke for the shapes.")]
public Brush ShapeStroke
{...
#endregion
#region --------------------Is playing--------------------
/// <summary>
/// Playing status
/// </summary>
public static readonly DependencyProperty IsPlayingProperty = DependencyProperty.Register("IsPlaying", typeof(bool), typeof(WaitSpin),
new FrameworkPropertyMetadata(new PropertyChangedCallback(OnIsPlayingChanged)));
/// <summary>
/// OnIsPlayingChanged callback
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnIsPlayingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(d))
return;
WaitSpin element = d as WaitSpin;
element.ChangePlayMode((bool)e.NewValue);
}
/// <summary>
/// IsPlaying
/// </summary>
[System.ComponentModel.Category("Loading Animation Properties"), System.ComponentModel.Description("Incates wheter is playing or not.")]
public bool IsPlaying
{...
#endregion
#region --------------------Associated element--------------------
/// <summary>
/// Associated element to disable when loading
/// </summary>
public static readonly DependencyProperty AssociatedElementProperty = DependencyProperty.Register("AssociatedElement", typeof(UIElement), typeof(WaitSpin), null);
/// <summary>
/// Gets or sets the associated element to disable when loading
/// </summary>
[System.ComponentModel.Category("Loading Animation Properties"), System.ComponentModel.Description("Associated element that will be disabled when playing.")]
public UIElement AssociatedElement
{...
#endregion
#region --------------------AutoPlay--------------------
/// <summary>
/// Gets or sets a value indicating whether the animation should play on load.
/// </summary>
public static readonly DependencyProperty AutoPlayProperty = DependencyProperty.Register("AutoPlay", typeof(bool), typeof(WaitSpin),
new FrameworkPropertyMetadata(new PropertyChangedCallback(OnAutoPlayChanged)));
private static void OnAutoPlayChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(d))
return;
WaitSpin element = d as WaitSpin;
element.ChangePlayMode((bool)e.NewValue);
}
/// <summary>
/// Gets or sets a value indicating whether the animation should play on load.
/// </summary>
[System.ComponentModel.Category("Loading Animation Properties"), System.ComponentModel.Description("The animation should play on load.")]
public bool AutoPlay
{...
#endregion
#endregion
#region --------------------PROPERTIES--------------------
...
/// <summary>
/// Gets the animation state,
/// </summary>
public AnimationState AnimationState
{
get { return this._animationState; }
}
#endregion
/// <summary>
/// Gets the parts out of the template.
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
//retreive the animation part
this._loadingAnimation = (Storyboard)this.GetTemplateChild("PART_LoadingAnimation");
if (this.AutoPlay)
Begin();
}
/// <summary>
/// Begins the loading animation.
/// </summary>
internal void ChangePlayMode( bool playing )
{
if (this._loadingAnimation == null) return;
if (playing)
{
if (this._animationState != AnimationState.Playing)
Begin();
}
else
{
if (this._animationState != AnimationState.Stopped)
Stop();
}
}
/// <summary>
/// Begins the loading animation.
/// </summary>
public void Begin()
{
if (this._loadingAnimation != null)
{
this._animationState = AnimationState.Playing;
this._loadingAnimation.Begin();
this.Visibility = System.Windows.Visibility.Visible;
if (AssociatedElement != null)
AssociatedElement.IsEnabled = false;
}
}
/// <summary>
/// Pauses the animation.
/// </summary>
public void Pause()
{
if (this._loadingAnimation != null)
{
this._animationState = AnimationState.Paused;
this._loadingAnimation.Pause();
}
}
/// <summary>
/// Resumes the animation.
/// </summary>
public void Resume()
{
if (this._loadingAnimation != null)
{
this._animationState = AnimationState.Playing;
this._loadingAnimation.Resume();
}
}
/// <summary>
/// Stops the animation.
/// </summary>
public void Stop()
{
if (this._loadingAnimation != null)
{
this._animationState = AnimationState.Stopped;
this._loadingAnimation.Stop();
this.Visibility = System.Windows.Visibility.Hidden;
if (AssociatedElement != null)
AssociatedElement.IsEnabled = true;
}
}
}
The basic provided XAML style define the Visibility
, ShapeFill
and
ShapeStroke
properties. Joined is a simple ellipse template and a storyboard
to play with shape transparency.
Note that the template is surrounded by a Viewbox
binded to
the control dimensions - that's the easiest way i found to make it
adjustable because Canvas
control is a nightmare...Ellipse properties are
template binded to WaitSpin control properties
Stroke="{TemplateBinding ShapeStroke}" Fill="{TemplateBinding ShapeFill}"
<Style x:Key="{x:Type local:WaitSpin}" TargetType="{x:Type local:WaitSpin}">
<Setter Property="Visibility" Value="Visible" />
<Setter Property="ShapeFill" Value="White" />
<Setter Property="ShapeStroke" Value="#00000000" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:WaitSpin}">
<Viewbox Width="{TemplateBinding Width}" Height="{TemplateBinding Height}">
<Canvas x:Name="Document" Width="100" Height="100" Clip="F1 M 0,0L 100,0L 100,100L 0,100L 0,0">
<Canvas.Resources>
<Storyboard.....
</Canvas.Resources>
<Ellipse x:Name="ellipse8" Width="15" Height="15" Canvas.Left="12" Canvas.Top="12" Stretch="Fill" StrokeThickness="1" StrokeLineJoin="Round" Stroke="{TemplateBinding ShapeStroke}" Fill="{TemplateBinding ShapeFill}" Opacity="0.66" />
.....
</Canvas>
</Viewbox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Create Designs
The easiest way to create design is to combine Expression Designer and Blend. My model is based on a 100x100 document. In the Designer, create the shapes you want and export them with the following options: "selected objects" + (3rd) "XAML SL4 / CWPF canvas" + "Always name" + "Place grouped objects in container" + "Paths" + "Convert to effets"
Then, copy the canvas content from the exported file into a WaitSpin style copy to replace the ellipses.
Take also care about the order and name of elements if you use the pre-defined opacity storyboard that exist in the
basic style, then just rename the elements to "EllipseX" or change
the TargetName
of each KeyFrames
.
<Storyboard x:Key="PART_LoadingAnimation" x:Name="PART_LoadingAnimation" RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="ellipse1" Storyboard.TargetProperty="(UIElement.Opacity)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="1"/>
....
Or create a new storyboard in Blend like the rotating one defined in the "ie" style and printed below - Visual Studio don't like it in the designer, but it's working.
<Storyboard x:Key="PART_LoadingAnimation" x:Name="PART_LoadingAnimation" RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)" Storyboard.TargetName="Document">
<EasingDoubleKeyFrame KeyTime="00:00:02" Value="360"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
Note : Expression suite is out of the scope...I am not mastering it at all !
Multi-progress panel
For an online programm, often calls are made to a distant web service that is not allways responding a quick manner. So you launch some asynchronous methods and wait for a response. The idea is to make a "panel" like the download one in Internet Explorer, small, non intrusive, cancelable and multi-process in this case. I wan it to be MVVM compliant too !
First, we need to choose the base control and define the ViewModel :
ItemsControl
seems to answer the requirement, then lets write the
ViewModel
for our process items :
public class ProcessItem : ViewModelBase
{
//init data
public bool UseTempo { get; set; }
public DateTime StartTime { get; set; }
public bool CanCancel { get; set; }
public bool ShowProgress { get; set; }
public bool ShowPercentage { get; set; }
private string _Title;
public string Title
{
get { return _Title; }
set
{
if (_Title != value)
{
_Title = value;
RaisePropertyChanged("Title");
}
}
}
private string _Message;
public string Message
{
get { return _Message; }
set
{
if (_Message != value)
{
_Message = value;
RaisePropertyChanged("Message");
}
}
}
public bool WaitForCancel { get; set; }
#region cancel command
private ICommand cancelCommand;
public ICommand CancelCommand
{
get
{
if (cancelCommand == null)
cancelCommand = new DelegateCommand(CancelCommandExecute, CancelCommandCanExecute);
return cancelCommand;
}
}
virtual public bool CancelCommandCanExecute()
{
return CanCancel;
}
virtual public void CancelCommandExecute()
{
WaitForCancel = true;
}
#endregion
}
To write the control create a class that inherit from ItemsControl
.
public class ProcessPanel : ItemsControl
Then we override the OnApplyTemplate
to retreive the opening and closing
animation that are defined in the template
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_CloseStoryboard = (Storyboard)this.GetTemplateChild("PART_CloseStoryboard");
_CloseAnim = _CloseStoryboard.Children[0] as DoubleAnimation;
_OpenStoryboard = (Storyboard)this.GetTemplateChild("PART_OpenStoryboard");
_OpenAnim = _OpenStoryboard.Children[0] as DoubleAnimation;
}
After that we only subscribe to the items collection event to expand or
reduce the panel that will be animated by our storyboard. Note that the
StoryBoard
object arround the animation are the only ways to modify the
From
and To
properties. Otherwise you will get
exceptions with frezzed objects because theu are playing.
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
Console.WriteLine("play open : from " + _OpenAnim.From + "to" + _OpenAnim.To);
_OpenStoryboard.Begin();
_OpenAnim.From = _OpenAnim.To;
_OpenAnim.To += 35;
_CloseAnim.From = _OpenAnim.To;
_CloseAnim.To = _OpenAnim.From;
}...
The style for our control is like below : I define a DockPanel
that contains the storyboards, then a simple grid that has a border
for shaping the control and an ItemPresenter
that contains my
ItemsControl
collection. ItemTemplate
is replaced
with simple DataTemplate
that is binded to the
ProcessItem
ViewModel :
<Style x:Key="{x:Type local:ProcessPanel}" TargetType="{x:Type local:ProcessPanel}">
<Setter Property="Visibility" Value="Visible" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ProcessPanel}">
<DockPanel x:Name="PART_Container" VerticalAlignment="Bottom" Panel.ZIndex="1000">
<DockPanel.Resources>
<Storyboard x:Name="PART_CloseStoryboard" x:Key="PART_CloseStoryboard">
<DoubleAnimation x:Name="CloseAnim"
Storyboard.TargetName="PART_Container"
Storyboard.TargetProperty="(Height)"
From="41"
To="0"
Duration="00:00:00.4000000" />
</Storyboard>
<Storyboard x:Name="PART_OpenStoryboard" x:Key="PART_OpenStoryboard">
...
</DockPanel.Resources>
<Grid Margin="0">
<Border x:Name="top" Grid.ColumnSpan="4" CornerRadius="7,7,0,0">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
...
</LinearGradientBrush>
</Border.Background>
</Border>
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Grid>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsControl.ItemTemplate">
<Setter.Value>
<DataTemplate>
<Grid Margin="0" Height="35">
<Grid.ColumnDefinitions>
...
<TextBlock Grid.Column="0" VerticalAlignment="Center" Margin="5" Background="Transparent"
Text="{Binding Title}" TextWrapping="WrapWithOverflow" TextTrimming="WordEllipsis" />
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="5" Background="Transparent"
Text="{Binding Message}" TextWrapping="WrapWithOverflow" TextTrimming="WordEllipsis" />
<controls:WaitSpin Grid.Column="2" AutoPlay="True" Margin="6"></controls:WaitSpin>
<Button Grid.Column="3" Margin="6" Content="x" Command="{Binding CancelCommand}" />
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
Thread, Task and BackgroundWorker
Explanations
On my C.B.R.
project, I gave a try to the TPL to compare with my thread
implementation...that was a catastroph...So I choose to go deeper and try to
compare these methods. I choose to implement a "recursive disk/folder
parsing" method to find images. This allow us to continue also with how
to use the WaitSpin
and ProgressPanel
controls.
The algorithm is allways the same and basically like below - depending on the method : BtnClick => create a ProcessItem => add it to collection => start a "process thread" => when finished, remove ProcessItem
Thread way
Nothing special but note that I use the BeginInvoke
on the
application thread to add and remove the item. It is asynchron and can lead
into errors.
private void btnThread_Click(object sender, RoutedEventArgs e)
{
ProcessItem pi = new ProcessItem()
{
Title = "btnThread_Click",
Message = "btnThread_Click",
CanCancel = true,
ShowProgress = true,
ShowPercentage = false,
Data = this.tbFolder.Text,
StartTime = DateTime.Now,
UseTempo = chkUseTempo.IsChecked.Value
};
Thread t = new Thread(new ParameterizedThreadStart(LaunchThread));
t.IsBackground = true;
t.Priority = ThreadPriority.Normal;
t.Start(pi);
}
private void LaunchThread(object param)
{
try
{
ProcessItem pi = param as ProcessItem;
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate
{
_list.Add(pi);
listBox1.Items.Add(pi.StartTime);
});
pi.Message = "Processing folders";
ProcessMethod(pi, pi.Data as string);
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate
{
_list.Remove(pi);
listBox1.Items.Add(DateTime.Now - pi.StartTime);
});
}
...}
internal void ProcessMethod(object param, string folder)
{
ProcessItem pi = param as ProcessItem;
...
pi.Message = "Processing folder " + directory.Name;
foreach (FileInfo file in directory.GetFiles("*.*"))
{
if (pi.WaitForCancel)
{
pi.Message = "canceled by the user";
return;
}
pi.Message = "Processing file " + file.Name;
if (file.Extension == ".jpg")
{
Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate
{
listBox1.Items.Add(file.Name);
});
}
...
}
TPL - First try...
The idea was : wow ! make some parallel tasks and see how quick it is....but
I forgot that creating a Task
is CPU expensive and that my hard
disk is the bottleneck in this case ! Imaging that even if the framework can
create 20000 task in a second my hard disk is not capable of reading file so quick !
Below is the first try :
public void ParallelProcessMethod(object param, string folder)
{
ProcessItem pi = param as ProcessItem;
try
{
DirectoryInfo directory = new DirectoryInfo(folder);
if (!directory.Exists)
...
Parallel.ForEach<FileInfo>(directory.GetFiles("*.*"), fi =>
{
...
Parallel.ForEach<DirectoryInfo>(directory.GetDirectories("*", SearchOption.TopDirectoryOnly), dir =>
{
...
ParallelProcessMethod(param, dir.FullName);
...
Advise : Never do that! Be sure that the task number will not grow too much and that the processing is long enough to compensate the task creation cost.
Update : This is certainly due to Visual Studio and the debug mode ! See cpu and time comparison...
TPL - Second try...
Remove the Parallel.ForEach
and everything gets better ! Here is
the handler. I put the code for adding and removing the ProcessItem
here to take
advantage of the ContinueWith
method. The process code is the same.
private void btnTask_Click(object sender, RoutedEventArgs e)
{
ProcessItem pi = new ProcessItem()
{
Title = "btnTask_Click",
Message = "btnTask_Click",
CanCancel = true,
ShowProgress = true,
ShowPercentage = false,
Data = this.tbFolder.Text,
StartTime = DateTime.Now,
UseTempo = chkUseTempo.IsChecked.Value
};
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate
{
_list.Add(pi);
listBox1.Items.Add(pi.StartTime);
});
Task tk = Task.Factory.StartNew(() =>
{
try
{
pi.Message = "Processing folders";
ParallelProcessMethod(pi, pi.Data as string);
}
catch (Exception err)
{
}
}).ContinueWith(ant =>
{
_list.Remove(pi);
listBox1.Items.Add(DateTime.Now - pi.StartTime);
//updates UI no problem as we are using correct SynchronizationContext
}, TaskScheduler.FromCurrentSynchronizationContext());
}
...
Background Worder to finish
I implemented the BackgroundWorker
in a quick and classic
way
like below :
private void btnWorker_Click(object sender, RoutedEventArgs e)
{
BackgroundWorker _Worker = null;
//init the background worker process
_Worker = new BackgroundWorker();
_Worker.WorkerReportsProgress = true;
_Worker.WorkerSupportsCancellation = true;
_Worker.DoWork += new DoWorkEventHandler(bw_DoBuildWork);
_Worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
_Worker.ProgressChanged += new ProgressChangedEventHandler(bw_ProgressChanged);
ProcessItem pi = new ProcessItem()
{
Title = "btnWorker_ClickbtnWorker_Click",
Message = "btnWorker_Click",
CanCancel = true,
ShowProgress = true,
ShowPercentage = false,
Data = this.tbFolder.Text,
StartTime = DateTime.Now,
UseTempo = chkUseTempo.IsChecked.Value
};
// Start the asynchronous operation.
_Worker.RunWorkerAsync(pi);
}
...
void bw_DoBuildWork(object sender, DoWorkEventArgs e)
{
ProcessItem pi = e.Argument as ProcessItem;
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate
{
_list.Add(pi);
listBox1.Items.Add(pi.StartTime);
});
BackProcessMethod(pi, pi.Data as string);
e.Result = pi;
}
internal void BackProcessMethod(object param, string folder)
{
ProcessItem pi = param as ProcessItem;
...
//the same as others
}
void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
...empty
}
void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
ProcessItem pi = e.Result as ProcessItem;
// First, handle the case where an exception was thrown.
if (e.Error != null)
{
}
else if (e.Cancelled)
{
// Next, handle the case where the user canceled the operation.
// Note that due to a race condition in the DoWork event handler, the Cancelled
// flag may not have been set, even though CancelAsync was called.
}
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate
{
_list.Remove(pi);
listBox1.Items.Add(DateTime.Now - pi.StartTime);
});
}
Timing and CPU
My system disk "C:" is containing 125 241 files, 23 087 folders for a total of 163 Go.
BackgroundWorker = 00:00:08s 1070. Cpu is 50% max and use the 4 core
Task with Paralell (first try) = 00:00:04s with very high Cpu at 90% but we win 3s
Task = 00:00:07s 3214. Cpu is 50% max, but use less on the 4 core
Thread = 00:00:07s 7414. Cpu is 60% max, activity is the same as tasks
To summerize...
This little try, in my opinion, show :
- BackgroundWorker method has got a bit more code, seems a bit obsolet with his progress handler and more rigid
- Execution speed is nearly the same with a small advantage for Task
- Thread are more classical way of writing regarding linq style of the Tasks
- Tasks certainly reveals avantages in a more complex scenario !
- Use release mode out of Visual Studio to compare !
By the way, it is more a fashion than anything else - this is to make TPL fan overreact :-)
Conclusion
This article is a very quick draft of what you can use for further developpement. I hope you will enjoy it as much as I do...next improvment is to reduce the panel after user interaction...but I have troubles...
History
- v1.0
- First version: everything there. I would like to add a auto-hide function if mouse leave the panel...
-
v2.0
- Second version: Update with cpu and times. New advise about the first try, Tasks seems to be better in release mode.