WPF: In an attached property, how to wait until vi

2019-05-11 04:04发布

问题:

I have an Attached Property in a WPF app.

The code below is inside the OnLoad event, but it doesn't work unless I add a hacky 500 millisecond delay in.

Is there some way to avoid this delay, and detect when the visual tree has been loaded?

private static void FrameworkElement_Loaded(object sender, RoutedEventArgs e)
{
    // ... snip...
    Window window = GetParentWindow(dependencyObject);

    // Without this delay, changing properties does nothing.
    Task task = Task.Run(
        async () =>
        {
            {
                // Without this delay, changing properties does nothing.
                await Task.Delay(TimeSpan.FromMilliseconds(500));

                Application.Current.Dispatcher.Invoke(
                    () =>
                    {
                        // Set False >> True to trigger the dependency property.
                        window.Topmost = false;
                        window.Topmost = true;                                              
                    });
            }
        });
 }

Update 1

As per the answer from @Will, "use the dispatcher and select the appropriate priority". This works brilliantly:

private static void FrameworkElement_Loaded(object sender, RoutedEventArgs e)
{
   // Wrap *everything* in a Dispatcher.Invoke with an appropriately
   // low priority, to defer until the visual tree has finished updating.
   Application.Current.Dispatcher.Invoke(
   async () =>
        {
            // This puts us to the back of the dispatcher queue.
            await Task.Yield();

            // ... snip...
            Window window = GetParentWindow(dependencyObject);

            window.Topmost = true;                                              
        }, 
        // Use an appropriately low priority to defer until 
        // visual tree updated.
        DispatcherPriority.ApplicationIdle);
 }

Update 2

If using DevExpress, the LayoutTreeHelper class is useful for scanning up and down the visual tree.

For sample code that deals with scanning up and down the visual tree, see:

  • DevExpress: In an attached property, how to wait until we are 100% sure that DevExpress has finished building its Visual Tree?
  • DevExpress and How to ensure that a LayoutPanel is always in front of everything else?

Update 3

If you are in an attached property, the only way to reliably load the visual tree is to execute the code in or after the Loaded event handler. If we are not aware of this limitation, then everything will fail intermittently. Waiting until after the OnLoaded event has fired is far superior to any other method that attempts to introduce other random forms of delays, as noted above.

If you are using DevExpress, this is even more critical: attempting to do anything before the Loaded event can, in some circumstances, result in a crash.

For example: - Calling window.Show() before the Loaded event has resulted in a crash in some circumstances. - Hooking into the event handler for IsVisibleChanged before the Loaded event has result in a crash in some circumstances.


Disclaimer: I am not associated with DevExpress, it one of many good WPF libraries, I also recommend Infragistics.

回答1:

If you want to wait to do something in the UI thread, use the Dispatcher. How do you snag it? That's a dupe.

How do I get the UI thread Dispatcher?

You can use the DispatcherPriority to select a time in the future. Priorities are based on UI activities, so you can't say, for example, next week. But you can say "let me wait until after bindings have been processed", for example.



回答2:

I prefer this version because it works nicer with ReSharper (suggest method stubs) and takes parameters in order of precedence.

public static class FrameworkElementExtensions
{
    public static void BeginInvoke(this DispatcherObject element, Action action, DispatcherPriority priority = DispatcherPriority.ApplicationIdle)
    {
        element.Dispatcher.BeginInvoke(priority, new Action(async () =>
        {
            await Task.Yield();
            action();
        }));
    }
}