Revit API Forum
Welcome to Autodesk’s Revit API Forums. Share your knowledge, ask questions, and explore popular Revit API topics.
cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

Can anyone share a working implementation of WPF progress bar with abort button?

25 REPLIES 25
Reply
Message 1 of 26
adam.krug
5046 Views, 25 Replies

Can anyone share a working implementation of WPF progress bar with abort button?

I already had a few takes on that topic but none worked ideally.

 

My current problem is that while Revit is processing through my command I cannot click on the "abort" button because the progress bar window is not responding. It updates well but won't react on mouse clicks most of the time...

 

Maybe someone has already figured out such progress bar window with WPF and could share?

Tags (2)
25 REPLIES 25
Message 2 of 26
RPTHOMAS108
in reply to: adam.krug

The most common and straightforward workaround is to call System.Windows.Forms.Application.DoEvents periodically i.e. when you want to update the window or check for control click. A bit annoying to rely on a feature of forms for a WPF app.

 

I even resorted to having a separate executable for the progress bar and sending back and forth serialized data.

 

Ideally you would start a second thread for this.

 

Message 3 of 26
ricaun
in reply to: adam.krug

Call System.Windows.Forms.Application.DoEvents is the common and straightforward workaround like @RPTHOMAS108 comment.

 

@adam.krugI have this simples sample of a ProgressView:

 

// ProgressView.xaml.cs
using System;
using System.Threading.Tasks;
using System.Windows;

namespace RevitAddin10.Views
{
    public partial class ProgressView : Window, IDisposable
    {
        public bool IsClosed { get; private set; }
        private Task taskDoEvent { get; set; }

        public ProgressView(string title = "", double maximum = 100)
        {
            InitializeComponent();
            InitializeSize();
            this.Title = title;
            this.progressBar.Maximum = maximum;
            this.Closed += (s, e) =>
            {
                IsClosed = true;
            };
        }

        public void Dispose()
        {
            if (!IsClosed) Close();
        }

        public bool Update(double value = 1.0)
        {
            UpdateTaskDoEvent();
            if (this.progressBar.Value + value >= progressBar.Maximum)
            {
                progressBar.Maximum += value;
            }
            progressBar.Value += value;
            return IsClosed;
        }

        private void UpdateTaskDoEvent()
        {
            if (taskDoEvent == null) taskDoEvent = GetTaskUpdateEvent();
            if (taskDoEvent.IsCompleted)
            {
                Show();
                DoEvents();
                taskDoEvent = null;
            }
        }

        private Task GetTaskUpdateEvent()
        {
            return Task.Run(async () => { await Task.Delay(500); });
        }

        private void DoEvents()
        {
            System.Windows.Forms.Application.DoEvents();
            System.Windows.Forms.Cursor.Current = System.Windows.Forms.Cursors.WaitCursor;
        }

        private void InitializeSize()
        {
            this.SizeToContent = SizeToContent.WidthAndHeight;
            this.Topmost = true;
            this.ShowInTaskbar = false;
            this.ResizeMode = ResizeMode.NoResize;
            this.WindowStartupLocation = WindowStartupLocation.CenterScreen;
        }
    }
}
<!--ProgressView.xaml-->
<Window x:Class="RevitAddin10.Views.ProgressView"
	DataContext="{Binding RelativeSource={RelativeSource Self}}"
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
	xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
	xmlns:local="clr-namespace:RevitAddin10.Views"
	mc:Ignorable="d" 
	>

    <Grid Margin="15">
        <ProgressBar x:Name="progressBar" Height="15" Width="200"></ProgressBar>
    </Grid>
</Window>

 

And a simple command:

// ProgressCommand.cs
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using RevitAddin10.Views;
using System;
using System.Threading;

namespace RevitAddin10.Revit.Commands
{
    [Transaction(TransactionMode.Manual)]
    public class ProgressCommand : IExternalCommand
    {
        public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
        {
            UIApplication uiapp = commandData.Application;

            using (ProgressView progressView = new ProgressView("Sleep...", 100))
            {
                for (int i = 0; i < 100; i++)
                {
                    Thread.Sleep(100);
                    if (progressView.Update()) break;
                }
            }
            return Result.Succeeded;
        }
    }
}

 

I hope this sample helps!

 

See Yaa!

Luiz Henrique Cassettari

ricaun.com - Revit API Developer

AppLoader EasyConduit WireInConduit ConduitMaterial CircuitName ElectricalUtils

Message 4 of 26
adam.krug
in reply to: RPTHOMAS108

Somehow the trick with calling System.Windows.Forms.Application.DoEvents() doesn't seem to help with my current code.

My current implementation handles the progress window on a separate thread that I explicitly start and with each update of the progress I use dispatcher.

I'd prefer to avoid having a separate app to handle the progress window.
Message 5 of 26
adam.krug
in reply to: ricaun

@ricaun thanks for your example, very interesting. Could you help me out to understand some details?

 

Could you explain the role of taskDoEvent? Why is it needed? Why is it just waiting for half second? Why is does it have to run each time when the progress should be updated?

 

If I understand the code well, each time you update the progress you also call Window.Show() - could you explain why you're calling it on already shown window?

Message 6 of 26
RPTHOMAS108
in reply to: adam.krug

DoEvents doesn't require a second thread, actually if you are using dispatcher you shouldn't need to call it. The UI in WPF is on it's own thread hence dispatcher is used to access that in a thread safe way:

 

Threading Model - WPF .NET Framework | Microsoft Docs

Any use of thread.sleep here is to simulate blocking only i.e. not a required aspect.

 

DoEvents was a way of forcing the main thread to process the messaging queue i.e. usually it just blocks it until after it has completed. Either way you still need a point in your Revit code where you update the UI. If you are asking Revit itself to do a long running task such as export view then you can only query the progress information on that.

 

For threading the workflow should be to start the progress bar thread and keep it active not restart it with each update. In your main code you would then get the dispatcher from the Window (Window.Dispatcher) and use begin invoke to call a function on the Window in the other thread (which updates it on that other thread).

Message 7 of 26
RPTHOMAS108
in reply to: RPTHOMAS108

To clarify my statement "The UI in WPF is on it's own thread" which isn't exactly true, I try to summarise what is written in the above Microsoft link but it is not easy:

 

My experience is that if you were writing a single threaded WPF application (not Revit add-in) the UI would be considered as being on the main thread of that application (even through it may be split by WPF internally). Alternatively you could have a multi threaded WPF application where you started the window on a different thread. This is what you could do with Revit add-in (if you are able to start second thread) start the progress window on it's own thread and access that from the API thread allocated to your add-in.

Message 8 of 26
ricaun
in reply to: adam.krug

I was trying other alternatives and find this.

 

Using 'Dispatcher' like @RPTHOMAS108 comments.

 

private void DoEvents()
        {
            //System.Windows.Forms.Application.DoEvents();
            this.Dispatcher.Invoke(() => true, DispatcherPriority.Background);
            System.Windows.Forms.Cursor.Current = System.Windows.Forms.Cursors.WaitCursor;
        }

 

See ya!

 

Luiz Henrique Cassettari

ricaun.com - Revit API Developer

AppLoader EasyConduit WireInConduit ConduitMaterial CircuitName ElectricalUtils

Message 9 of 26
ricaun
in reply to: adam.krug


@adam.krug  escreveu:

@ricaun thanks for your example, very interesting. Could you help me out to understand some details?

 

Could you explain the role of taskDoEvent? Why is it needed? Why is it just waiting for half second? Why is does it have to run each time when the progress should be updated?


The main idea of the 'taskDoEvent' is to call the 'DoEvents' every 500 milliseconds and open the ProgressBar if not open yet.

 

The example is not the best I could use 'Stopwatch' instead of a 'Task'.


If you call the 'DoEvents' each loop you waste too much time verifying the Revit Application. If your loop is big like 10000 is better to focus on the loop intend of updating the progress bar perfectly.

 

 

@adam.krug  escreveu:

If I understand the code well, each time you update the progress you also call Window.Show() - could you explain why you're calling it on already shown window?


I'm lazy and call 'Window.Show()' more the once and works.

 

I have created a project to share some progress bar: https://github.com/ricaun/RevitAddin_ProgressBar

 

See yaa!

 

Luiz Henrique Cassettari

ricaun.com - Revit API Developer

AppLoader EasyConduit WireInConduit ConduitMaterial CircuitName ElectricalUtils

Message 10 of 26
Songohu_85
in reply to: adam.krug

Hi Adam, it's been 2 years now and I'm sure that you found a good solution for this.

But if someone doesn't have a way to implement progress bar in wpf window with cancellation then I attached an example. I recreated it as simplified application that can be run in macro manager.

Revit_mrNI96rI1c.gif

Cheers - Marek

Message 11 of 26
kirylXDQF6
in reply to: Songohu_85

Cool solution, also interesting method to share Revit API projects.

Message 12 of 26
harrymattison
in reply to: adam.krug

These tools work nicely with a simple bit of code in a loop (like create 500 3d views)

But when the computations in the loop are much more complex and time-consuming, either the progress bar dialog never appears at all or it never updates. Has anyone solved that?

Message 13 of 26

For that, it needs to be modeless and provide async callback methods to step forward etc. I implemented one using Windows Forms in 2012:

  

  

Sorry it is not WPF.

  

  

Jeremy Tammik Developer Advocacy and Support + The Building Coder + Autodesk Developer Network + ADN Open
Message 14 of 26
harrymattison
in reply to: adam.krug

Hi @jeremy_tammik 
I looked at the progress bar implementation in https://thebuildingcoder.typepad.com/files/adn_rme_2013.zip

But I don't see the async callback methods to step forward. Am I looking in the right place?

Thanks

 

        using( ProgressForm pf = new ProgressForm( caption, s, n ) )
        {
          foreach( Space space in spaces )
          {
            if( terminalsPerSpace.ContainsKey( space.Number ) )
            {
              AssignFlowToTerminalsForSpace( terminalsPerSpace[space.Number], space );
            }
            pf.Increment();
          }
        }

 

 

 

 

    public ProgressForm( string caption, string format, int max )
    {
      _format = format;
      InitializeComponent();
      Text = caption;
      label1.Text = (null == format) ? caption : string.Format( format, 0 );
      progressBar1.Minimum = 0;
      progressBar1.Maximum = max;
      progressBar1.Value = 0;
      Show();
      Application.DoEvents();
    }

 

 

    public void Increment()
    {
      ++progressBar1.Value;
      if( null != _format )
      {
        label1.Text = string.Format( _format, progressBar1.Value );
      }
      Application.DoEvents();
    }
Message 15 of 26

Dear Harry,

 

it was implemented before stuff like async and await had been invented! So, pointless to try to search for those keywords.

  

The asynchronous modeless communication was done by hand, simply by calling back and forthe between different threads.

  

I have forgotten the details, I would have to do the same research as anyone else to remember how it is done  🙂

  

Cheers

  

Jeremy

  

Jeremy Tammik Developer Advocacy and Support + The Building Coder + Autodesk Developer Network + ADN Open
Message 16 of 26
sonicer
in reply to: adam.krug

Message 17 of 26
zefreestijl
in reply to: Songohu_85

Hi, thanks for the solution, I've tried to implement the method,

but it seems the background worker doesn't recognize many parameters in the document.

 

like the current view will return [null], so all we can collect are whole elements in the document,

instead of by specific view or by other filtering statements,

 

any idea how to solve this?

 

thanks in advance :]

Message 18 of 26
zefreestijl
in reply to: sonicer

Hi, I've tried the library, 

is it possible to also implement it for a non-transaction processing by the MeterProgress()?

 

looks like the method only works for modifying the existing document,

but if I want to create a list or to read some data, the library just doesn't work...

or am I did it the wrong way?

 

Thanks in advance :]

 

Message 19 of 26
Songohu_85
in reply to: zefreestijl

Hi, background worker is just a way not to freeze your application UI. It comes from System.ComponentModel and will not give you any access to Revit elements. Everything else is done the same way as if you didn't use background worker. In modeless windows you may use IExternalEventHandler to do some Revit API related tasks.  When you run background worker you should raise an event that executes what you specified in a class that implements IExternalEventHandler. This way you can proccess everything in valid revit data context with access to all elements. An example that I attached shows everything you may need. There is no malicious code in the macro.

 

Best

Marek

Message 20 of 26
zefreestijl
in reply to: Songohu_85

Hi, thanks for the reply,

I've tried the macro in your previous attachment

but looks like it was merely a listing tool instead of modifying the document...

 

and I've tried follow your suggestion to call a functionality from a ExternalEventHandler class,

but still didn't work..

either had no response at all in DoWork Event,

err2.png

 

 

or gotten an error msg in Completed Event:

 

error.png

 

my current achievable way is by using Transaction inside function itself before Executed when a DoWork Event Triggered,

but it will create tons of undo-history because each list item will have its own handling session...

 

is it possible to concludes those Transactions into a TransactionGroup before Initialize BackgroundWorker?

 

 

Thanks in advance :]

 

Here is the entire code block:

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Windows.Controls;
using Autodesk.Revit.ApplicationServices;
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using System.Linq;
using System.Collections;



namespace DLN_AddinV2
{
    [Transaction(TransactionMode.Manual)]
    internal class DeleteWallsInBackgroundCommand : IExternalCommand
    {
        public Document _Doc1;
        public List<ElementId> _List1 = new List<ElementId>();

        private ExternalEvent externalEvent;
        private ExternalEventHandler externalEventHandler;


        public Result Execute( ExternalCommandData commandData, ref string message, ElementSet elements)
        {
            try
            {
                UIApplication app1 = commandData.Application;
                Document doc1 = app1.ActiveUIDocument.Document;
                _Doc1 = doc1;

                // Initialize ExternalEvent and ExternalEventHandler
                externalEventHandler = new ExternalEventHandler(commandData);
                externalEvent = ExternalEvent.Create(externalEventHandler);



                // Initialize BackgroundWorker
                BackgroundWorker backgroundWorker = new BackgroundWorker();


                backgroundWorker.DoWork += BackgroundWorker_DoWork;
                backgroundWorker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;

                // Start BackgroundWorker
                backgroundWorker.RunWorkerAsync();



                return Result.Succeeded;
            }
            catch (Exception ex)
            {
                message = ex.Message;

                TaskDialog.Show("catch", message);
                return Result.Failed;
            }

        }

        private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            _List1 = C0_GetData.GetAllElementsAll(_Doc1).Where(x => _Doc1.GetElement(x).Category.Name.Contains("Wall")).ToList();


            // Perform background work (e.g., simulation)
            try
            {
                for (int i = 0; i < _List1.Count(); i++)
                {
                    // Simulate work being done
                    Thread.Sleep(100);

                    try
                    {
                        externalEventHandler._Id1 = _List1[i];

                    }
                    catch (Exception ex) { TaskDialog.Show("catch", ex.Message); }
                    



                    // Report progress to the main thread using ExternalEvent
                    externalEvent.Raise();
                }

            }
            catch (Exception ex) { TaskDialog.Show("catch", ex.Message); }


            /* Cannot open a Transaction session before called function or it won't work
            using (Transaction transaction = new Transaction(_Doc1, "DeleteWallsTransaction"))
            {
                transaction.Start();

                externalEventHandler.DeleteWallByID(_Doc1, externalEventHandler._Id1);


                transaction.Commit();
            }
            */

        }

        private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            // BackgroundWorker completed
            externalEvent.Dispose(); // Dispose of ExternalEvent
        }
    }



    public class ExternalEventHandler : IExternalEventHandler
    {
        public ExternalCommandData CommandData { get; private set; }
        public ElementId _Id1 { get; set; }

        public ExternalEventHandler(ExternalCommandData commandData)
        {
            CommandData = commandData;
        }

        public void Execute(UIApplication app)
        {
            // Code to execute on the main thread
            // Update UI or perform Revit API operations           

            DeleteWallByID(app.ActiveUIDocument.Document, _Id1);

        }

        public string GetName()
        {
            return "ExternalEventHandler";
        }


        public void DeleteWallByID(Document doc1, ElementId id1)
        {

            // the Transaction session needs to be used inside the function or it won't work
            using (Transaction transaction = new Transaction(doc1, "DeleteWallsTransaction"))
            {
                transaction.Start();

                // Call the method to delete all walls in the view
                doc1.Delete(id1);

                transaction.Commit();
            }


        }


    }

}

 

 

Tags (1)

Can't find what you're looking for? Ask the community or share your knowledge.

Post to forums  

Forma Design Contest


Rail Community