A valid Revit API context is provided within the handlers executed by Revit call-backs, and nowhere else.

For instance,
an external command defines
an Execute methods and registers itself with Revit.

Revit calls that method and supplies a valid Revit API context to it via its ExternalCommandData input argument.

A Windows form can be displayed either using Show or ShowDialog.

If you use Show, it becomes a modeless dialogue and all interaction which it is outside of any valid Revit API context.

If you use ShowDialog, it becomes modal and remains in the same thread that launched it, possibly inheriting a valid Revit API context.

If you happen to be in a modeless context, you can implement a solution to access a valid Revit API context by implementing
an external event and calling that.

Similarly to the external command, it also defines an Execute methods and registers itself with Revit.

Revit calls that method and supplies a valid Revit API context to it when it is prompted by an external trigger.

The official approach for implementing an external event is demonstrated by a Revit SDK sample:

ModelessDialog/ModelessForm_ExternalEvent

It may require some redesign of your application logic to use.

Ryan Singleton added some good real-world advice to this:

To add my experience to what Jeremy said (you need External Events to make your transactions work modelessly), I have built a dockable dialog with WPF that used a lot of external events. Autodesk recommends that you try to use modal dialogs as much as you can instead of modeless. It will make your project much easier.

However, I can tell you that a fully functional suite of tools with external events is possible. But you need to be sure that the added complexity is worth it.

If you are going to have multiple external events, I strongly recommend that you look at the Revit SDK sample for External Events specifically. It shows how to use enums and a handler to swap out external events so that you do not have a sloppy mess of external events in your application.

You can also have a modeless dialog that, when you press a button to start a transaction, will open a modal window (progress bar or some other window), and that window will start the transaction instead. Once done, it closes, and the user feels like they are using a modeless context. A system like that would be much easier to maintain.