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?
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.
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!
@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?
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).
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.
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!
@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!
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.
Cheers - Marek
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?
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.
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();
}
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
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 :]
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 :]
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
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,
or gotten an error msg in Completed Event:
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();
}
}
}
}
Can't find what you're looking for? Ask the community or share your knowledge.