Repository navigation
Fixed exception if the project was saved from another thread. Added o… - #509
davidebazzi wants to merge 2 commits into
Conversation
…verloading to hide the progress bar when saving the project.
dsn27
left a comment
There was a problem hiding this comment.
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.
- The first save from a background thread still runs on that thread.
InvokeRequiredreturnsfalsewhile theProgressFormhas no window handle yet. Its handle is only created at the firstShow(), so the first background save of a session still shows the form on the worker thread. That is exactly the scenario from #508. Marshalling throughOwner(the main form) avoids this. Details are inline. - Use
BeginInvokeinstead ofInvoke. The synchronousInvokecan 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
InvokeRequiredbranches 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
| 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); |
There was a problem hiding this comment.
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.
InvokeRequiredisfalse, soInit()(RectangleToScreencreates the handle) andShow()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),
InvokeRequiredistrue.Invokethen 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 threadGenerated by Claude Code
| this.Show(); | ||
| Cursor.Current = Cursors.WaitCursor; | ||
| //Application.DoEvents(); | ||
| Invoke(new Action<bool, double, string>(ShowProgressBar), show, percent, title); |
There was a problem hiding this comment.
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
FormClosingwaits for withtask.Wait(). - Aborted save: if the main window is closed or disposed during a background save,
Invokethrows. The exception is raised insideUIProgressBar.DoStep, i.e. in the middle ofJsonSerialize.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
| if (title != null) | ||
| { | ||
| this.BeginInvoke((Action)delegate () | ||
| if (this.InvokeRequired) |
There was a problem hiding this comment.
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
| { | ||
| return WriteToFile(FileName, true); | ||
| } | ||
| public virtual bool WriteToFile(string FileName, bool useProgress) |
There was a problem hiding this comment.
Minor points on the new public overload:
- Documentation: it has no XML doc. It should describe the parameter. It should also say that
FileName == nullopens a save file dialog, which fails from a background thread just like the progress bar did.showProgressmight be a clearer name thanuseProgress. - Two virtual overloads: both overloads are
virtual. A subclass that overridesWriteToFile(string)no longer sees calls toWriteToFile(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.
…verloading to hide the progress bar when saving the project.