Skip to content

Fixed exception if the project was saved from another thread. Added o… - #509

Open
davidebazzi wants to merge 2 commits into
masterfrom
508-progressbar-improvements
Open

davidebazzi wants to merge 2 commits into
masterfrom
508-progressbar-improvements

Conversation

@davidebazzi

Copy link
Copy Markdown
Collaborator

…verloading to hide the progress bar when saving the project.

…verloading to hide the progress bar when saving the project.
@davidebazzi davidebazzi linked an issue Oct 28, 2026 that may be closed by this pull request

@dsn27 dsn27 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix. Marshalling the progress display to the UI thread is the right approach, but there are two problems that should be fixed before merging.

  1. The first save from a background thread still runs on that thread. InvokeRequired returns false while the ProgressForm has no window handle yet. Its handle is only created at the first Show(), so the first background save of a session still shows the form on the worker thread. That is exactly the scenario from #508. Marshalling through Owner (the main form) avoids this. Details are inline.
  2. Use BeginInvoke instead of Invoke. The synchronous Invoke can deadlock if the UI thread waits for the save. It can also abort the save if the main window is closed while saving.

Minor points:

  • The inner InvokeRequired branches are now dead code.
  • The new WriteToFile(string, bool) overload has no XML doc.
  • Both files lost their UTF-8 BOM, which is probably an editor setting. Harmless, but it adds noise to the diff.
  • The PR title is cut off ("Added o…").

Generated by Claude Code

Comment thread CADability.Forms/ProgressForm.cs Outdated
Comment on lines +50 to +53
if (Owner == null) return; // has been killed
if (show && !Visible)
if (InvokeRequired)
{
// show it
Init(title);
this.Show();
Cursor.Current = Cursors.WaitCursor;
//Application.DoEvents();
Invoke(new Action<bool, double, string>(ShowProgressBar), show, percent, title);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InvokeRequired is false while the form has no window handle yet. This means the first save from a background thread still runs on that thread.

InvokeRequired returns false when the control's handle has not been created yet, even if the call comes from another thread. CadForm and MainForm2 create the ProgressForm with Visible = false and never show it, so its handle only appears at the first Show().

Scenario: the user creates a new project and the first save runs on a worker thread.

  • InvokeRequired is false, so Init() (RectangleToScreen creates the handle) and Show() run on the worker thread.
  • The progress window now belongs to a thread without a message loop and is never painted.
  • The next time the UI thread shows progress (e.g. when opening a file), InvokeRequired is true. Invoke then waits for a thread that never pumps messages, or throws if that thread has ended.

This probably goes unnoticed if a file was opened on the UI thread before saving, because that already created the handle.

Suggestion: marshal through the owner. It is the main form, whose handle always exists. Use BeginInvoke (see the next comment):

Form owner = Owner;
if (owner == null || owner.IsDisposed) return; // has been killed
if (owner.InvokeRequired)
{
    owner.BeginInvoke(new Action<bool, double, string>(ShowProgressBar), show, percent, title);
    return;
}
// from here on always on the UI thread

Generated by Claude Code

Comment thread CADability.Forms/ProgressForm.cs Outdated
this.Show();
Cursor.Current = Cursors.WaitCursor;
//Application.DoEvents();
Invoke(new Action<bool, double, string>(ShowProgressBar), show, percent, title);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer BeginInvoke over Invoke here. The synchronous Invoke causes two problems.

  • Deadlock: the worker thread blocks until the UI thread runs the delegate. If the UI thread itself waits for the save, the application hangs. A typical case is a background autosave that FormClosing waits for with task.Wait().
  • Aborted save: if the main window is closed or disposed during a background save, Invoke throws. The exception is raised inside UIProgressBar.DoStep, i.e. in the middle of JsonSerialize.ToStream, so the save is aborted because of a progress display.

BeginInvoke avoids both problems. Calls posted from one thread keep their order, so the final ShowProgressBar(false) still arrives after the last progress value. The arguments are value types or strings, so nothing can change before the delegate runs.


Generated by Claude Code

Comment thread CADability.Forms/ProgressForm.cs Outdated
if (title != null)
{
this.BeginInvoke((Action)delegate ()
if (this.InvokeRequired)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this branch is always on the UI thread, so this.InvokeRequired is always false here. The same applies at line 92. Both inner BeginInvoke blocks are dead code and can go, which also makes the diff much smaller.


Generated by Claude Code

Comment thread CADability/Project.cs
{
return WriteToFile(FileName, true);
}
public virtual bool WriteToFile(string FileName, bool useProgress)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor points on the new public overload:

  • Documentation: it has no XML doc. It should describe the parameter. It should also say that FileName == null opens a save file dialog, which fails from a background thread just like the progress bar did. showProgress might be a clearer name than useProgress.
  • Two virtual overloads: both overloads are virtual. A subclass that overrides WriteToFile(string) no longer sees calls to WriteToFile(name, false). Changing that would be a breaking change, so keeping it is fine, but the release notes should mention it.
  • Public API: this is new public API, which fits the 1.8.0 minor release.

Generated by Claude Code

…created.

- Changed back encoding to UTF-8 BOM.
@davidebazzi
davidebazzi requested a review from dsn27 October 30, 2026 09:28

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Progressbar improvements

2 participants