top of page
avicoren

How to add a Spinner

Updated: Jun 29, 2023


There are times you'll need your app to execute a process that takes a significant amount of time to finish. By significant time, I mean, a period of time that will make the user notice that the app isn't quite responsive and she has to wait for the app to finish processing to be able to continue using it.


On such cases we want the user to KNOW this is happening without having to guess that something is going on, or wrongfully think the app has hanged.


The Spinner, also called "Circular Loading Animation", is a very common way of letting the user know that the app is currently busy.


There are many kinds of spinners out there and, you guessed it right, Material Design has some too.


Material Design's spinner is nothing but a progress bar with a circular shape and for its motion it uses WPF's storyboards, animations, transitions etc. We will use a progress bar with a style "MaterialDesignCircularProgressBar".

Example 15 in my GitHub repo will give you a starting point on Spinners. I will go through this example and explain the way it works.


The first thing we do is create a new overlay layer with a black background and 0.5 opacity, this will give us the darkening effect. You can increase or decrease the opacity value to get your preferred lighting level. This layer will only contain the progress bar and its Visibility="Collapsed" which makes it hidden by default.

 Style="{StaticResource MaterialDesignCircularProgressBar}"

The above style is a really nice ready made spinner. The only downside for me is its lack of a thickness property. To get more thickness, we will have to add a 'LayoutTransform' attribute to it and then play with the 'ScaleTransform'.

Fill free to change the values in Example15.xaml and find your best looking set.

Whenever we want to show the spinner, we just set the grid visibility to "Visible".

Seriously? that easy? well... yes.. but.


Our PowerShell app is running inside a PowerShell session, where data is being processed and displayed.

Our cute spinner is spinning so gracefully that we never asked ourselves "how does it spin?" the answer is very simple - it's a computer program! it sends instructions to the computer on how to paint it on the screen so we can see it spinning. So, it's a computer program that, when runs, the processor and other hardware components are working on it continuously. AND it also runs on the same PowerShell session as our app's.

Why am I bubbling about it? that's because when our app will be doing any heavy processing job (thus - displaying the spinner to the user), this job will INTERRUPT the instructions that spins the spinner! it will actually make it stop spinning!

The computer will be so busy processing our job, that it will stop giving it's resources to the part that spins the spinner, and make it freeze.


The solution for this problem is to send those process intensive jobs to a new runspace. A runspace is a neat feature in PowerShell, that will let you run jobs in the background (same idea as Start-Job ), and yet keep an opened communication channel with the main runspace that sent it to work!

A new runspace will open a new PowerShell engine and will execute a predefined script as an autonomous unit. Whenever we open a PowerShell console, a new runspace is created. We can call it the "main" runspace. Its ID will always be 1.

You can check the list of current runspaces by typing

Get-Runspace

or get the current one of your main session:

[System.Management.Automation.Runspaces.Runspace]::DefaultRunSpace

As mentioned before, a runspace is only opening a new PowerShell engine, so there is no console output. It will not display its results or errors on out-host.


There are a few things we should do when creating a runspace:

  1. Create a global HashTable type Variable that will pass the Variables from our main runspace to the new one.

  2. Populate the HashTable with all the UI controls that you would want the runspace to update. $Window variable is mandatory.

  3. Runspaces do not dispose themselves upon completion. We need to take care of it on our own. Not doing so, will increase your app's memory consumption and might cause performance issues. There are two events that will handle the disposal of completed runspaces. Find them in the runspace code part of Example15.

  4. The actual script that we want to run, needs to be entered right after $Worker = [PowerShell]::Create().AddScript({ You can create a script block with the code to be executed and then just provide it like: $Worker = [PowerShell]::Create().AddScript({ $MyScriptBlock })


Whenever we want to update a UI control in our app, we need to use the $SyncHash.Window.Dispatcher.Invoke method. For example, if you want a textbox named ""MyTxtBox", you pass it in the 'SyncHash' variable and you do:

$SyncHash.Window.Dispatcher.Invoke(
[action]{$SyncHash.MyTxtBox.Text = "Hello from new runspace!"}, "Normal")

Be careful though. Passing data to the main runspace during the job execution will again interrupt the spinner!. make sure you update the UI only when the job has completed or when it's really necessary to do so during its execution (with the cost of the spinner being laggy).


On the Example15 script, the background job is searching for any number between 1 and 10 million that is a division of the number 2560583. It's just for the sake of showing that the spinner runs smoothly during this execution.

At the end of the runspace script it writes the results to the textbox and hides the spinner layer.


Last thing I would want to do is disable all input options while processing the job, so the user could not interact with the app. It's optional but recommended.

You can just put a $Window.IsEnabled=$false before you start the job, and from the new runspace code, at the end of its script you do

$SyncHash.Window.Dispatcher.Invoke([action]{ $SyncHash.Window.IsEnabled=$true },"Normal")

You can also disable only the application layer grid, if your logic can accept this configuration. In example15 that's what I did. The 'ApplicationLayer' variable is the name of the grid that holds my main screen.





We just added another cool feature to our app! more are coming soon...


2,466 views2 comments

Recent Posts

See All

2 Comments


Guest
Apr 19, 2023

Where is example 5 on github?

Like
avicoren
May 10, 2023
Replying to

Hi. It's Example *15* 😀

Like
bottom of page