I am using a visual control in my project that is from a library that I do not have the source to.
It takes too long to update (200ms, roughly) for good UI responsiveness with three of these controls on-screen at once. (I might need to update all three at once, which leaves my UI stuck for ~600ms while they are all thinking).
I have read some posts about TaskSchedulers, and am beginning to investigate the Parallel task features as a way of running each of these controls in their own thread. The platform will be multi-core, so I want to take advantage of simultaineous processing.
The problem is that I don't even know what I don't know about how to go about this, though..
Is there a suitable design pattern for running a control in a separate thread from the main UI thread in WPF?
Specifically: it is a third party map control, that when given a new location or zoom level takes far too long to redraw (~200ms). With perhaps three of these updating at a maximum of 4Hz - obviously they won't keep up..
I have encapsulated the WPF control in a usercontrol, and need to run each instance in it's own thread, while still capturing user input (mouse clicks, for example).
UPDATE: while I am feeling around for a solution, I have implemented the following so far.
My main (UI) thread spawns a thread that creates a new window that contains the control in question, and locates it in the correct position (so that it looks like it is just a normal control).
_leftTopThread = new Thread(() =>
{
_topLeftMap = new MapWindow()
{
WindowStartupLocation = WindowStartupLocation.Manual,
Width = leftLocation.Width,
Height = leftLocation.Height,
Left = leftLocation.X,
Top = leftLocation.Y,
CommandQueue = _leftMapCommandQueue,
};
_topLeftMap.Show();
System.Windows.Threading.Dispatcher.Run();
});
_leftTopThread.SetApartmentState(ApartmentState.STA);
_leftTopThread.IsBackground = true;
_leftTopThread.Name = "LeftTop";
_leftTopThread.Start();
Where CommandQueue
is a Thread-safe BlockingCollection Queue for sending commands to the map (moving the location, etc).
The problem is now that I can either
- have user input due to the
System.Windows.Threading.Dispatcher.Run()
call
- or block on the CommandQueue, listening for commands sent by the main thread
I can't spin waiting for commands, because it would soak up all my thread CPU!
Is it possible to block and have the event message-pump working?
Well, I have a method that works - but it may well not be the most elegant..
I have a window that contains my third party (slow-rendering) control in the XAML.
public partial class MapWindow : Window
{
private ConcurrentQueue<MapCommand> _mapCommandQueue;
private HwndSource _source;
// ...
}
My main (UI) thread contructs and starts this window on a thread:
_leftTopThread = new Thread(() =>
{
_topLeftMap = new MapWindow()
{
WindowStartupLocation = WindowStartupLocation.Manual,
CommandQueue = _leftMapCommendQueue,
};
_topLeftMap.Show();
System.Windows.Threading.Dispatcher.Run();
});
_leftTopThread.SetApartmentState(ApartmentState.STA);
_leftTopThread.IsBackground = true;
_leftTopThread.Name = "LeftTop";
_leftTopThread.Start();
I then get a handle to the window in the thread (after it has initialised):
private IntPtr LeftHandMapWindowHandle
{
get
{
if (_leftHandMapWindowHandle == IntPtr.Zero)
{
if (!_topLeftMap.Dispatcher.CheckAccess())
{
_leftHandMapWindowHandle = (IntPtr)_topLeftMap.Dispatcher.Invoke(
new Func<IntPtr>(() => new WindowInteropHelper(_topLeftMap).Handle)
);
}
else
{
_leftHandMapWindowHandle = new WindowInteropHelper(_topLeftMap).Handle;
}
}
return _leftHandMapWindowHandle;
}
}
.. and after putting a command onto the thread-safe queue that is shared with the threaded window:
var command = new MapCommand(MapCommand.CommandType.AircraftLocation, new object[] {RandomLatLon});
_leftMapCommendQueue.Enqueue(command);
.. I let it know it can check the queue:
PostMessage(LeftHandMapWindowHandle, MapWindow.WmCustomCheckForCommandsInQueue, IntPtr.Zero, IntPtr.Zero);
The window can receive my message because it has hooked into the window messages:
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
_source = PresentationSource.FromVisual(this) as HwndSource;
if (_source != null) _source.AddHook(WndProc);
}
..which it then can check:
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) //
{
// Handle messages...
var result = IntPtr.Zero;
switch (msg)
{
case WmCustomCheckForCommandsInQueue:
CheckForNewTasks();
break;
}
return result;
}
..and then execute on the thread!
private void CheckForNewTasks()
{
MapCommand newCommand;
while (_mapCommandQueue.TryDequeue(out newCommand))
{
switch (newCommand.Type)
{
case MapCommand.CommandType.AircraftLocation:
SetAircraftLocation((LatLon)newCommand.Arguments[0]);
break;
default:
Console.WriteLine(String.Format("Unknown command '0x{0}'for window", newCommand.Type));
break;
}
}
}
Easy as that.. :)
I've been looking into this as well, and the most relevant information I could find was in this blog post (however I have not tested it yet):
http://blogs.msdn.com/b/dwayneneed/archive/2007/04/26/multithreaded-ui-hostvisual.aspx
it creates a HostVisual on the UI thread, then spins up a background thread, creates a MediaElement, puts it inside a VisualTarget (which points back to the HostVisual), and puts it all inside our hacky VisualTargetPresentationSource.
The problem with this method is that apparently the user won't be able to interact with the controls that are running in the new thread.