Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

wpf window (ui) blocked by long rendering operation - possible to use background thread for rendering?

The app window blocks while a rendering operation is active. I.e. when the Content property of a ContentControl is set. A user control which is a DataTemplate for the content is drawn. The freeze lasts between 5 and 10 seconds depending on the PC being used.

This user control is not too complex (about 250 simple controls - images, text boxes, text blocks, buttons, etc). Layout is far from perfect, I didn't write it, and I neither have time, nor want to optimize the layout, as the problem could at best be reduced.

The best I was able to accomplish is to wrap the control in a 'container' which manages to draw a loading animation and show a busy cursor before the ui/app window freeze. I give a complete code listing for it below.

I commented 'freeze starts here' in the code, at the bottom of the question in the wrapper custom control code. That's when the WPF rendering engine starts drawing the user control (i.e. the grid inside it).

I used my favorite search engine a lot, and I learned that WPF has a special 'render' thread which is separate from the UI thread.

Other that hiding the App window while it is frozen and displaying a 'loading' animation window during that time (or some derivative of this), which is easy given the code below but absurd - is there some way to mitigate the situation?

Here is the code, first the use-case:

<!-- while I am being rendered, I block the UI thread. -->
<UserControl x:Class="MyUserControl"
             xmlns:loading="clr-namespace:Common.UI.Controls.Loading;assembly=Common.UI.Controls">    
    <loading:VisualElementContainer>
        <loading:VisualElementContainer.VisualElement>
            <Grid>
                <!-- some 500 lines of using other controls with binding, templates, resources, etc.. 
                for the same effect try having a canvas with maaany rectangles..-->
            </Grid>
        </loading:VisualElementContainer.VisualElement>
    </loading:VisualElementContainer>    
</UserControl>

The wrapper custom control layout:

<Style TargetType="{x:Type loading:VisualElementContainer}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type loading:VisualElementContainer}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <Grid>
                        <loading:LoadingAnimation x:Name="LoadingAnimation" VerticalAlignment="Center" HorizontalAlignment="Center"/>
                        <ContentControl x:Name="ContentHost"/>
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

And wrapper custom control code:

/// <summary>Hosts the visual element and displays a 'loading' animation and busy cursor while it is being rendered.</summary>
public class VisualElementContainer : Control
{
    static VisualElementContainer()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(VisualElementContainer), new FrameworkPropertyMetadata(typeof(VisualElementContainer)));
    }

    private Window MyWindow;
    private ContentControl ContentHost;
    private LoadingAnimation LoadingAnimation;

    public override void OnApplyTemplate()
    {
        this.ContentHost = this.GetTemplateChild("ContentHost") as ContentControl;
        this.LoadingAnimation = this.GetTemplateChild("LoadingAnimation") as LoadingAnimation;

        base.OnApplyTemplate();

        this.MyWindow = this.FindVisualParent(typeof(Window)) as Window;

        this.SetVisual(this.VisualElement);
    }

    private static DependencyProperty VisualElementProperty =
        DependencyProperty.Register(
            "VisualElement",
            typeof(FrameworkElement),
            typeof(VisualElementContainer),
            new PropertyMetadata(null, new PropertyChangedCallback(VisualElementPropertyChanged)));

    public FrameworkElement VisualElement
    {
        get { return GetValue(VisualElementProperty) as FrameworkElement; }
        set { SetValue(VisualElementProperty, value); }
    }

    private static void VisualElementPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var me = sender as VisualElementContainer;

        if (me == null || me.ContentHost == null || me.LoadingAnimation == null)
            return;

        me.RemoveVisual(e.OldValue as FrameworkElement);
        me.SetVisual(e.NewValue as FrameworkElement);
    }

    private void RemoveVisual(FrameworkElement fwElement)
    {
        this.ContentHost.Content = null;

        if (fwElement != null)
            fwElement.Loaded -= fwElement_Loaded;
    }

    private void SetVisual(FrameworkElement fwElement)
    {
        if (fwElement == null)
        {
            this.ContentHost.Content = fwElement;
        }
        else
        {
            fwElement.Loaded += fwElement_Loaded;

            this.SetContentVisibility(false);

            this.Dispatcher
                .BeginInvoke(
                //freeze begins here
                    new Action(() => this.ContentHost.Content = fwElement),
                    System.Windows.Threading.DispatcherPriority.ContextIdle);
        }
    }

    private void fwElement_Loaded(object sender, RoutedEventArgs e)
    {
        this.SetContentVisibility(true);
        //freeze ends here.
    }

    private void SetContentVisibility(bool isContentVisible)
    {
        if (isContentVisible)
        {
            this.MyWindow.Cursor = Cursors.Arrow;

            this.LoadingAnimation.Visibility = Visibility.Collapsed;
            this.ContentHost.Visibility = Visibility.Visible;
        }
        else
        {
            this.MyWindow.Cursor = Cursors.Wait;

            this.ContentHost.Visibility = Visibility.Hidden; //Setting to collapsed results in the loaded event never fireing. 
            this.LoadingAnimation.Visibility = Visibility.Visible;
        }
    }
}
like image 203
h.alex Avatar asked Oct 31 '22 18:10

h.alex


1 Answers

I really do not think that your problem is actually related to the rendering or the layout. Especially with only 250 controls as I saw wpf munching 100 times more without any problem (its rendering engine is inefficient but not that inefficient). Unless maybe if you abuse rich effects (bitmap effects, opacity mask) with a poor hardware or a poor driver, or do something really odd.

Consider all the data you need. Are there large images or other large resources to load from the disk? Network operations? Long computations?

Depending on the answer it may be possible to defer some tasks to another thread. But without more informations the only solution I can suggest is to use HostVisual to nest controls that will live in another thread. Unfortunately this is only suitable for non-interactive children (children that do not need to receive user inputs).

like image 133
Victor Victis Avatar answered Nov 15 '22 05:11

Victor Victis