Yes, there is the traditional approach you describe where you programmatically enable/disable features, but WPF also opens up several new possiblities that were really not possible in WinForms and older technologies.
I will explain four WPF-specific ways to do this:
You can secretly and automatically replace a window's contents with a picture of its contents using a Rectangle with a VisualBrush, thereby effectively disabling it. To the user it will look as if the window is unchanged, but the actual contents will be there underneath the picture, so you can use it for hit-testing and even forward selected events to it.
You can add a MergedDictionary to your window's ResourceDictionary that causes all TextBoxes to become TextBlocks, all Buttons to become disabled, etc, except as explicitly overridden using custom attached properties. So instead of looping through all your UI selectively enabling/disabling, you simply add or remove an object from the MergedDictionaries collection.
You can use the InputManager to programmatically generate and process real mouse events in particular parts of a disabled window, disallowing any mouse events that don't hit-test to something "approved."
Use data binding and styles to enable/disable individual controls rather than iterating through them
Details on replacing window with picture of window
For this solution, iterate your app windows and replace each content with a Grid containing the original Content and a Rectangle, like this:
<Window ...>
<Grid>
<ContentPresenter x:Name="OriginalContent" />
<Rectangle>
<Rectangle.Fill>
<VisualBrush Visual="{Binding ElementName=OriginalContent}" />
</Rectangle.Fill>
</Rectangle>
</Grid>
</Window>
This can be done programmatically or by using a template on the Window, but my preference is to create a custom control and create the above structure using its template. If this is done, you can code your windows as simply this:
<Window ...>
<my:SelectiveDisabler>
<Grid x:Name="LayoutRoot"> ... </Grid> <!-- Original content -->
</my:SelectiveDisabler>
</Window>
By adding mouse event handlers to the Rectangle and calling VisualTreeHelper.HitTest
on the ContentPresenter to determine what object was clicked in the original content. From this point you can choose to ignore the mouse event, forward it to the original content for processing, or in the case of an eyedropper control or an object selection feature, simply extract the desired objects/information.
Details on MergedDictionary approach
Obviously you can restyle your whole UI using a ResourceDictionary merged into your window's resources.
A naiive way to do this is to simply create implicit styles in the merged ResourceDictionary to make all TextBoxes appear as TextBlocks, all Buttons appear as Borders, etc. This does not work very well because any TextBox with its own style or ControlTemplate explicitly set may miss the updates. In addition, you may not get all objects as desired, and there is no way to easily remove the Commands or Click events from buttons because they are explicitly specified and the style won't override that.
A better way to work this is to have the styles in the merged ResourceDictionary set an attached property, then use code-behind in the PropertyChangedCallback to update the properties you really want to change. Your attached "ModalMode" property, if set to true, would save all the local values and bindings for a number of properties (Template, Command, Click, IsEnabled, etc) in a private DependencyProperty on the object, then overwrite these with standard values. For example a button's Command property would be set to null temporarily. When the attached "ModalMode" property goes false, all the original local values and bindings are copied back from the temporary storage and the temporary storage is cleared.
This method provides a convenient way to selectively enable/disable portions of your UI by simply adding another attached property "IgnoreModalMode". You can manually set this to True on any UIElements that you don't want the ModalMode changes to apply to. Your ModalMode PropertyChangedCallback then checks this and if is true, it does nothing.
Details on InputManager approach
If you capture the mouse, you can get mouse coordinates no matter where it is moved. Translate these to screen coordinates using CompositionTarget.TransformToDevice(), then use CompositionTarget.TransformFromDevice() on each candidate window. If the mouse coordinates are in bounds, hit-test the disabled window (this can still be done even when a window is disabled), and if you like the object the user clicked on, use InputManager.ProcesInput to cause the mouse event to be processed in the other window exactly as if it was not disabled.
Details on using data binding
You can use a styles to bind the IsEnabled property of Buttons, MenuItems, etc to a static value like this:
<Setter Property="IsEnabled" Value="{Binding NonModal, Source={x:Static local:ModalModeTracker.Instance}}" />
Now by default all items with these styles will automatically disable when your NonModal property goes false. However any individual control can override with IsEnabled="true"
to stay enabled even in your modal mode. More complex bindings can be done with MultiBinding and EDF ExpressionBinding to set whatever rules you want.
None of these approaches require iterating through your visual interface, enabling and disabling functionality. Which of these you actually select is a matter of what functionality you actually want to provide during modal mode, and how the rest of your UI is designed.
In any case, WPF makes this much easier than it was in WinForms days. Don't you just love WPF's power?