Form ProgressBar with cancel Button on new thread

Form ProgressBar with cancel Button on new thread

atir5UTNF
Advocate Advocate
1,058 Views
7 Replies
Message 1 of 8

Form ProgressBar with cancel Button on new thread

atir5UTNF
Advocate
Advocate

Hello,

 

I want to add a progrresbar to my app.

If I understood correctly, all Revit API calls must be no the main thread(or at the same thread? but not main?).

So I want to create the dialog on a new thread.

Can someone give me an simple example or guide me what are the main steps to do it?

 

I have found many solutions on the net with BackgroundWorker, but it is not what I need since the time-consuming operation is not in the background. I need a UI in new thread.

 

thanks.

 

0 Likes
1,059 Views
7 Replies
Replies (7)
Message 2 of 8

sonicer
Collaborator
Collaborator

It's not posible create new thread.

 

0 Likes
Message 3 of 8

RPTHOMAS108
Mentor
Mentor

Technically you can create other threads but they can't be used to interact with the API objects since they are not thread safe and never really can be.

 

It isn't straightforward to implement but putting a progress bar on separate thread is one way to keep it responsive as Revit does something, while also avoiding having to call 'do events'. The extra complication with WPF especially is that the UI thread has to be the main thread, so the thread you start to create and show new UI items (the progress bar) has to be single apartment I believe. Then to update progress bar from API thread you have to get the dispatcher for progress bar and invoke the member that updates the progress bar (i.e. update it indirectly). None of that uses API objects so should be fine. Cancel can be a bit of an issue but is possible to look for such a state change during an iteration of a loop that the API is doing.

 

Worked a few years back but can never guarantee it will still work now. I had a few different options for WPF progress bars over the years and I'm not sure the separate thread one was the best option (it worked but I always wondered if it was right to do). Likely it is just easier to call 'do events' (although I never really liked that either).

0 Likes
Message 4 of 8

andres.buitragoY4KG4
Contributor
Contributor

Hey,

I assume you have some WPF code with a simple progress Bar View and a Cancel Button. A simple solution is to add a property inside your progressBar that can be used to tell if a cancellation was requested. It can be a simple bool.

 

When your lenghty operation starts, create an instance of your Dialog with progress Bar.

 

Then, at the beginning of the loop, check if the Cancellation property is true. Then handle your cancellation accordingly (e.g close the transaction, rollback, etc.)

 

 

 

using(ProgressBarView progressBar = new("Copying windows", "Copying Window ", 100)) {
  progressBar.Show();

  // This transaction does something lengthy
  using Transaction t = new(doc, $ "Some long operation");
  t.Start();
  foreach(FamilyInstance win in link_model_windows) {
    if (progressBar.CancelRequest) {
      t.RollBack();
      throw new UserCanceledException();
    }
    progressBar.Update();
    // Do the loop
  }
}

 

 

 

This is the method that is binded to the click of the cancel button on the ProgressBarView class:

 

 

 

    /// <summary>
    /// Interaction logic for ProgressBarView.xaml
    /// </summary>
    public partial class ProgressBarView : Window, IDisposable
    {
        public bool CancelRequest{ get; private set; }
        

        private void CancelBtn_Click(object sender, RoutedEventArgs e)
        {
            CancelRequest = true;
        }
}

 

I handle the fact that it runs on another thread and I put that inside the update() method of the ProgressBarView:

 

        public bool Update(double value = 1.0)
        {
            Dispatcher.Invoke(() =>
            {
                progressBar.Value += value;
            }, System.Windows.Threading.DispatcherPriority.Background);

            return IsClosed;
        }

 

0 Likes
Message 5 of 8

atir5UTNF
Advocate
Advocate

Thanks! 

 

Actually, I had Windows.Forms ProgressBar as I mentioned in the title...

I'll try these solutions.

 

0 Likes
Message 6 of 8

Kennan.Chen
Advocate
Advocate

To achieve a similar effect, one approach is to divide your job into multiple smaller tasks. You can then create an ExternalEvent for each task and execute them in a chain, where the completion of one task triggers the next one. This allows you to cancel the entire job in the middle if needed. However, keep in mind that this approach may come with some trade-offs, such as losing certain features like Transaction.

0 Likes
Message 7 of 8

Kennan.Chen
Advocate
Advocate

A demo for the "Exporting Families" use case.

Note that this demo is incomplete, fix any bug yourself.

 

public class MyForm : Form
{
  private ExportFamiliesEventHandler ExportFamiliesEventHandler { get; }
  private ExternalEvent ExportFamiliesExternalEvent { get; }

  public MyForm(ExportFamiliesEventHandler exportFamiliesEventHandler)
  {
    ExportFamiliesEventHandler = exportFamiliesExternalEvent;
    ExportFamiliesExternalEvent = ExternalEvent.Create(ExportFamiliesEventHandler);
    InitializeComponent();
  }
  public void Button_Export_Click(object sender, EventArgs e)
  {
    ExportFamiliesEventHandler.ProgressChanged += OnProgressChanged;
    ExportFamiliesEventHandler.Finished += OnFinished;
    ExportFamiliesEventHandler.Cancelled += OnCancelled;
    ExportFamiliesExternalEvent.Raise();
  }

  public void Button_Cancel_Click(object sender, EventArgs e)
  {
    ExportFamiliesEventHandler.Cancel();
  }

  private void OnProgressChanged(object sender, int progressPercentage)
  {
    this.Invoke(() => {
      ProgressBar.Value = progressPercentage;
    })
  }

  private void OnFinished(object sender, EventArgs e)
  {
    ExportFamiliesEventHandler.ProgressChanged -= OnProgressChanged;
    ExportFamiliesEventHandler.Finished -= OnFinished;
    ExportFamiliesEventHandler.Cancelled -= OnCancelled;
    MessageBox.Show("Finished");
  }

  private void OnCancelled(object sender, EventArgs e)
  {
    ExportFamiliesEventHandler.ProgressChanged -= OnProgressChanged;
    ExportFamiliesEventHandler.Finished -= OnFinished;
    ExportFamiliesEventHandler.Cancelled -= OnCancelled;
    MessageBox.Show("Cancelled");
  }

  private void OnError(object sender, ErrorEventArgs e)
  {
    ExportFamiliesEventHandler.ProgressChanged -= OnProgressChanged;
    ExportFamiliesEventHandler.Finished -= OnFinished;
    ExportFamiliesEventHandler.Cancelled -= OnCancelled;
    MessageBox.Show(e.Exception.Message);
  }
}

public class ExportFamiliesEventHandler : IExternalEventHandler
{
  public int ProgressPercentage { get; private set; }
  private ChainedJobExecuter<Family, boolean> Executer { get; set; }
  public event EventHandler<int> ProgressChanged;
  public event EventHandler Finished;
  public event EventHandler Cancelled;
  public event EventHandler<ErrorEventArgs> Error;

  public string GetName()
  {
    return "Export Families";
  }

  public void Execute(UIApplication app)
  {
    var families = new FilteredElementCollector(app.ActiveUIDocument.Document)
      .OfClass(typeof(Family))
      .Cast<Family>()
      .Where(f => f.IsEditable)
      .ToList();
    var jobs = families.Select(f => new ExportFamilyJob(f, @"C:\Temp")).Cast<IJob<Family, boolean>>().ToArray();
    ChainedJobExecuter = new ChainedJobExecuter<Family, boolean>(jobs);
    ChainedJobExecuter.Finished += OnFinished;
    ChainedJobExecuter.ProgressChanged += OnProgressChanged;
    ChainedJobExecuter.Cancelled += OnCancelled;
    ChainedJobExecuter.Error += OnError;
    ChainedJobExecuter.Execute();
    return Result.Succeeded;
  }

  public void Cancel()
  {
    ChainedJobExecuter.Cancel();
  }

  private void OnFinished(object sender, EventArgs e)
  {
    Finished?.Invoke(this, null);
    Unsubscribe();
  }

  private void OnProgressChanged(object sender, ProgressChangedEventArgs e)
  {
    ProgressPercentage = Math.Round(e.Progress * 1.0 / e.Total);
    ProgressChanged?.Invoke(this, ProgressPercentage);
  }

  private void OnCancelled(object sender, EventArgs e)
  {
    Cancelled?.Invoke(this, null);
    Unsubscribe();
  }

  private void OnError(object sender, ErrorEventArgs e)
  {
    Error?.Invoke(this, e);
    Unsubscribe();
  }

  private void Unsubscribe()
  {
    ChainedJobExecuter.Finished -= OnFinished;
    ChainedJobExecuter.ProgressChanged -= OnProgressChanged;
    ChainedJobExecuter.Cancelled -= OnCancelled;
    ChainedJobExecuter.Error -= OnError;
  }
}

public class ChainedJobExecuter<TParameter, TResult>
{
  private bool IsCancellationRequested { get; set; } = false;
  private Queue<IJob<TParameter, TResult>> Jobs { get; };
  private int Total { get; };
  private List<TResult> Results { get; };
  public event EventHandler Finished;
  public event EventHandler<ProgressChangedEventArgs> ProgressChanged;
  public event EventHandler Cancelled;
  public event EventHandler<ErrorEventArgs> Error;
  public JobExecuter(IJob<TParameter, TResult>[] job)
  {
    Jobs = new Queue<IJob<TParameter, TResult>>(jobs);
    Total = jobs.Length;
  }
  public void Execute()
  {
    if (IsCancellationRequested)
    {
      Cancelled?.Invoke(this, null);
      return;
    }
    var job = Jobs.Dequeue();
    if (job == null)
    {
      Finished?.Invoke(this, null);
      return;
    }
    var externalEvent = ExternalEvent.Create(job);
    externalEvent.Raise();
    job.Finished += OnJobFinished;
    job.Error += OnJobError;
  }

  private void OnJobFinished(object sender, TResult result)
  {
    var job = sender as IJob<TParameter, TResult>;
    job.Finished -= OnJobFinished;
    job.Error -= OnJobError;
    Results.Add(result);
    ProgressChanged?.Invoke(this, new ProgressChangedEventArgs(Results.Count, Total));
    Execute();
  }

  private void OnJobError(object sender, ErrorEventArgs e)
  {
    var job = sender as IJob<TParameter, TResult>;
    job.Error -= OnJobError;
    job.Finished -= OnJobFinished;
    Error?.Invoke(this, e);
  }

  public Task Cancel()
  {
    IsCancellationRequested = true;
  }

  public TResult[] GetResults()
  {
    return Results.ToArray();
  }
}

public class ExportFamilyJob : GenericJob<Family, boolean>
{
  private string Directory { get; }
  public ExportFamilyJob(Family family, string directory) : base(family)
  {
    Directory = directory;
  }
  public override string GetName()
  {
    return $"Export Family {Parameter.Name}";
  }
  protected override boolean InternalExecute(UIApplication app)
  {
    var family = Parameter;
    var doc = app.ActiveUIDocument.Document;
    var path = Path.Combine(Directory, $"{family.Name}.rfa");
    var options = new SaveAsOptions
    {
      OverwriteExistingFile = true,
      Compact = true,
      MaximumBackups = 1
    };
    doc.SaveAs(path, options);
    return true;
  }
}

public interface IJob<TParameter, TResult> : IExternalEventHandler
{
  TParameter Parameter { get; }
  event EventHandler<TResult> Finished;
  event EventHandler<ErrorEventArgs> Error;
}

public abstract class GenericJob<TParameter, TResult> : IJob<TParameter, TResult>
{
  public TParameter Parameter { get; }
  public event EventHandler<TResult> Finished;
  public event EventHandler<ErrorEventArgs> Error;
  public GenericJob(TParameter parameter)
  {
    Parameter = parameter;
  }

  public abstract string GetName();
  public TResult Execute(UIApplication app)
  {
    try
    {
      var result = InternalExecute(app);
      OnFinished();
    }
    catch (Exception e)
    {
      Error?.Invoke(this, new ErrorEventArgs(e));
    }
  }

  protected abstract TResult InternalExecute(UIApplication app);
}

 

 

0 Likes
Message 8 of 8

atir5UTNF
Advocate
Advocate

I think I found a simple solution. It seems to work

calling Application.Run() to create a message loop for the new thread.

 

 

public delegate void ElemenDoneEventHandler();
public delegate void EndBarEventHandler();

public event ElemenDoneEventHandler ElemenDone;
private PorgressBarDlg progWindow;
internal static EventWaitHandle _progressWindowWaitHandle;
public event EndBarEventHandler EndBar;


public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
   _progressWindowWaitHandle = new AutoResetEvent(false);
   Thread progressBarThread = new Thread(ShowProgWindow);
   progressBarThread.Start();
   _progressWindowWaitHandle.WaitOne();

   for (int i = 0; i < 1000; i++)
   {
      Thread.Sleep(50);
      if (progWindow.winExists)
         progWindow.Invoke(ElemenDone);

      if (progWindow.cancelRequested) break;
   }
   progWindow.Invoke(EndBar);
   return Result.Succeeded;
}

private void ShowProgWindow()
{
   //creates and shows the progress window
   progWindow = new PorgressBarDlg();
   progWindow.Show();
   progWindow.winExists = true;
   ElemenDone += new ElemenDoneEventHandler(progWindow.UpdateProgress);
   EndBar += new EndBarEventHandler(progWindow.EndBar);

   //Notifies command thread the window has been created
   _progressWindowWaitHandle.Set();
   Application.Run();
}

///ProgressBarDlg:
private int nTotalElements = 1000;
private int nDoneElements;
internal bool winExists = false;
internal bool cancelRequested = false;
private Button button;
private ProgressBar progressBar;
private Label label;

public void UpdateProgress()
{
   nDoneElements++;
   int percentComplete = (int)((float)nDoneElements / (float)nTotalElements * 100);
   if (percentComplete > progressBar.Value)
   {
      progressBar.Value = percentComplete;
      label.Text = percentComplete.ToString() + "%";
   }
}

public void EndBar()
{
   Hide();
   Application.ExitThread();
}

private void button_Cancel(object sender, EventArgs e)
{
   cancelRequested = true;
}

 

0 Likes