Introduction
Click here to view the source code
Here are the past articles in the WPF Round Table Series:
In this series I want to express some of the knowledge I gained in WPF over the years when tackling unique situations. For today’s post though I would like to discuss something that was created quite recently after a brief discussion with coworkers about multi UI threaded controls. I always knew how to create a window on a separate UI thread, but what if you wanted a control to be part of a main window, yet have its own dispatcher message pump?
New Window UI
Well to start off we need to understand how to even spawn a new WPF supported UI thread. This article explains how to launch a window on a completely new UI thread. The creation process is actually quite simple as demonstrated in this code snippet:
Thread thread = new Thread(() => { Window1 w = new Window1(); w.Show(); w.Closed += (sender2, e2) => w.Dispatcher.InvokeShutdown(); System.Windows.Threading.Dispatcher.Run(); }); thread.SetApartmentState(ApartmentState.STA); thread.Start(); |
Here we start by simply creating a new thread which will host our new Window. Inside the thread we create a window and make sure the Dispatcher (which will get automatically created on demand for this thread when accessed) starts the message pump. We also handle shutting down the message pump on the window’s Closed event. At the end we set the thread’s ApartmentState to be single-threaded apartment (STA) rather than multithreaded partment (MTA) since WPF UI threads cannot be multithreaded. Once we start the thread we can see our new window now runs on its own UI thread.
Non-Interacting Host
Although a new window has its benefits, what if you want a UI independent control placed inside your main window? Well this MSDN articleexplains how this process can occur using a HostVisual class. The greatest benefit HostVisual provides is a way to arbitrarily connect any Visual to a parent visual tree. Unfortunately, there is not a way to fully measure, arrange, and render an item through a HostVisual without a presentation source. So we create our own presentation source which simply contains and displays our HostVisual to show in our window. Here is the main components of the class:
private readonly VisualTarget _visualTarget; public VisualTargetPresentationSource( HostVisual hostVisual ) { _visualTarget = new VisualTarget( hostVisual ); AddSource(); } public override Visual RootVisual { get { return _visualTarget.RootVisual; } set { Visual oldRoot = _visualTarget.RootVisual; // Set the root visual of the VisualTarget. This visual will // now be used to visually compose the scene. _visualTarget.RootVisual = value; // Tell the PresentationSource that the root visual has // changed. This kicks off a bunch of stuff like the // Loaded event. RootChanged( oldRoot, value ); // Kickoff layout... UIElement rootElement = value as UIElement; if ( rootElement != null ) { rootElement.Measure( new Size( Double.PositiveInfinity, Double.PositiveInfinity ) ); rootElement.Arrange( new Rect( rootElement.DesiredSize ) ); } } } protected override CompositionTarget GetCompositionTargetCore() { return _visualTarget; } |
And running the sample project you can test this by toggling the busy indicator:
The main caveat with this method is that you are unable to interact with the control, which is fine for the purpose I want for this control. But even though I was able to create a control independent of the UI I still had issues positioning the thread separated control in relation to my main window.
Decorator with Child Elements
I managed to stumble upon another article that not only addressed the issue of alignment, but goes one step further by allowing the control to have child elements as well. I’ll include a ‘Child’ property along with a ‘ContentProperty’ attribute at the header of my class so that I can create UIElements right into XAML. Here is the logic that helps display our UI content onto a separate thread:
protected virtual void CreateThreadSeparatedElement() { _hostVisual = new HostVisual(); AddLogicalChild( _hostVisual ); AddVisualChild( _hostVisual ); // Spin up a worker thread, and pass it the HostVisual that it // should be part of. var thread = new Thread( CreateContentOnSeparateThread ) { IsBackground = true }; thread.SetApartmentState( ApartmentState.STA ); thread.Start(); // Wait for the worker thread to spin up and create the VisualTarget. _resentEvent.WaitOne(); InvalidateMeasure(); } |
Since we are creating a new HostVisual we need to make sure we define the parent-child relationship between the HostVisual and our UI control by calling ‘AddLogicalChild’ and ‘AddVisualChild’. Let’s take a look at how we are creating our UI content on a separate thread:
private void CreateContentOnSeparateThread() { if ( _hostVisual != null ) { // Create the VisualTargetPresentationSource and then signal the // calling thread, so that it can continue without waiting for us. var visualTarget = new VisualTargetPresentationSource( _hostVisual ); _uiContent = CreateUiContent(); if (_uiContent == null ) { throw new InvalidOperationException( "Created UI Content cannot return null. Either override 'CreateUiContent()' or assign a style to 'ThreadSeparatedStyle'" ); } _threadSeparatedDispatcher = _uiContent.Dispatcher; _resentEvent.Set(); visualTarget.RootVisual = _uiContent; // Run a dispatcher for this worker thread. This is the central // processing loop for WPF. Dispatcher.Run(); visualTarget.Dispose(); } } |
Here we can see us using our VisualTargetPresentationSource custom class to contain the HostVisual. The ‘CreateUiContent’ method is simply a protected virtual method that creates our content for us and can be overrided by inheriting classes. To make sure both our child content and the HostVisual is represented in our control we need to override the ‘VisualChildrenCount’, ‘LogicalChildren’, and ‘GetVisualChild’ methods to take both elements into account. Although this will allow for allow our content to render, our UI separated content will have measuring issues if the Child content has limited size or merely does not exist. To fix this we are going to override the ‘Measure’ and ‘Arrange’ methods like so:
protected override Size MeasureOverride( Size constraint ) { var childSize = new Size(); var uiSize = new Size(); if ( Child != null ) { Child.Measure( constraint ); var element = Child as FrameworkElement; childSize.Width = element != null ? element.ActualWidth : Child.DesiredSize.Width; childSize.Height = element != null ? element.ActualHeight : Child.DesiredSize.Height; } if ( _uiContent != null ) { _uiContent.Dispatcher.Invoke( DispatcherPriority.Background, new Action( () => _uiContent.Measure( constraint ) ) ); uiSize.Width = _uiContent.ActualWidth; uiSize.Height = _uiContent.ActualHeight; } var size = new Size( Math.Max( childSize.Width, uiSize.Width), Math.Max( childSize.Height, uiSize.Height) );; return size; } protected override Size ArrangeOverride( Size finalSize ) { if ( Child != null ) { Child.Arrange( new Rect( finalSize ) ); } if ( _uiContent != null ) { _uiContent.Dispatcher.BeginInvoke( DispatcherPriority.Background, new Action( () => _uiContent.Arrange( new Rect( finalSize ) ) ) ); } return finalSize; } |
As you can see I am treating our main parent control mostly like a panel where I either fill out the space given or take the max size of either my Child element or the size of the element on the separate thread.
Thread Separated Style
Although we have our ‘CreateUiContent’ method to instantiate our control from code, what if we want to create our control from a style right within XAML? Well we can create a DependencyProperty called ‘ThreadSeparatedStyle’, but the style itself must be instantiated on the new UI thread or else we’ll run into thread exceptions. In order to get around this issue we are going to recreate the style on the fly using reflection through an anonymous call. Here you can see how this occurs when the style changes:
private static void OnThreadSeparatedStyleChanged( DependencyObject d, DependencyPropertyChangedEventArgs e ) { var control = (UiThreadSeparatedControl)d; var style = e.NewValue as Style; if ( style != null ) { var invokingType = style.TargetType; var setters = style.Setters.ToArray(); control._createContentFromStyle = () => { var newStyle = new Style { TargetType = invokingType, }; foreach ( var setter in setters ) { newStyle.Setters.Add( setter ); } var contentt = (FrameworkElement)Activator.CreateInstance( newStyle.TargetType ); contentt.Style = newStyle; return contentt; }; } else { control._createContentFromStyle = null ; } } |
Since I use the style’s target type to instantiate the control, the assigned target type in the style should not refer to a base control. I am also holding onto all the setter values for the style so they are preserved on recreation. Although I could avoid using reflection and recreating the style altogether by placing the style in a Themes defined folder Generic.xaml, by doing it this way it allows me to define the style at the same time I create the control:
< multi:UiThreadSeparatedControl IsContentShowing = "{Binding ElementName=Toggle, Path=IsChecked}" > < multi:UiThreadSeparatedControl.ThreadSeparatedStyle > < Style TargetType = "multi:BusyIndicator" > < Setter Property = "IsBusy" Value = "True" /> </ Style > </ multi:UiThreadSeparatedControl.ThreadSeparatedStyle > </ multi:UiThreadSeparatedControl > |
The convenience of having this as an option seemed to outweigh trying to avoid reflection. Especially since it is much more intuitive to define your styles anywhere in XAML, not just UI independent resource dictionaries.
FrozenProcessControl
Now how could we use this type of control in our application? One scenario could be a case where you want to display a busy cursor if the window happens to freeze. Although it is quite a bad practice for your application to ever freeze. Usually this problem can be circumvented by offloading certain functionality onto a separate, non-UI thread. But sometimes you are left without a choice in the matter. For instance, say you are using a third party control that has become an integral part of your application and suddenly adding new, large amount of data causes the control to inefficiently load all its components on the UI thread. You may not have access to the controls source code or do not have time to replace the control. It would be a much better user experience to at least display something to the user to let it know some form of action is still happening rather than staring at a frozen screen. This is where our FrozenProcessControl comes into play.
At first we will extend our UiThreadSeparatedControl and override the ‘CreateUiContent’ method:
protected override FrameworkElement CreateUiContent() { return new BusyIndicator { IsBusy = true , HorizontalAlignment = HorizontalAlignment.Center }; } |
We will also have two Timers; one for polling the main window for no response, and another when the window is non-responsive for too long. Here is how our polling method is handled:
private void PollMainWindowTimerOnElapsed( object sender, ElapsedEventArgs elapsedEventArgs ) { _pollMainWindowTimer.Stop(); _nonResponseTimer.Start(); if ( _mainWindowProcess.Responding ) { _nonResponseTimer.Stop(); if ( _isContentDisplaying ) { _isContentDisplaying = false ; _threadSeparatedDispatcher.BeginInvoke( DispatcherPriority.Render, new Action( () => { _uiContent.Visibility = Visibility.Hidden; _pollMainWindowTimer.Start(); } ) ); } else { _pollMainWindowTimer.Start(); } } } |
As you can see we immediately start our non-responsive timer because if the main window’s process is unable to respond, accessing the process will cause the thread to freeze until activity happens again. If we do happen to gain response again and our busy indicator is displaying we need to update its visibility using its UI thread dispatcher to access the separate UI thread. Here we can see how our non-response timer is handled:
private void NonResponseTimer_Elapsed( object sender, ElapsedEventArgs e ) { _pollMainWindowTimer.Stop(); _nonResponseTimer.Stop(); _isContentDisplaying = true ; _threadSeparatedDispatcher.BeginInvoke( DispatcherPriority.Render, new Action( () => { _uiContent.Visibility = Visibility.Visible; } ) ); } |
This is pretty straight forward, if the poll timer is frozen from accessing the process we do not want any further events to happen until the window is active again. After that we update the visibility using the separate UI thread to show our busy indicator. Here we can see the control in action in our demo by hitting the Freeze button, viewing the busy indicator on the far right freeze, and then suddenly seeing our thread separated control run on top:
Conclusion
Overall, this is quite a useful control, but the major caveat to using this is there is no ability to accept user input. Other than that this could easily help offload the build time for certain, display-only controls.