WPF Multithreaded UI
참조 사이트: http://blogs.msdn.com/dwayneneed/archiv ··· ual.aspx
WPF Threading Model
일반적으로, WPF에 있는 객체들은 오직 자신을 생성한 Thread로 부터 접근이 가능하다. 때때로, 이 UI Thread의 제한은 헛갈리지만, 사실 객체가 다른 Thread에 사는 것은 완벽히 좋다. 하지만 일반적으로 하나의 Thread에 객체를 생성하고 다른 Thread에서 접근하는 것은 불가능 하다. 대부분 이런 상황에서는 “The calling thread cannot access this object because a different thread owns it.”라는 메시지로 시작하는 InvalidOperationException이 발생한다.
Freezables
당연히 UI Thread의 제한에 대해 예외가 존재한다. 잘 알려진 객체는 Freezable 클래스이다. Freezable 객체들은 어떤 특정 상황에서 읽기 전용이 되어서 Single-Thread 제한에서 벗어날 수 있다. 이 상황을 “frozen”이라고 하자. 이 “frozen”된 Freezable 객체들은 대표적으로 Brushes 클래스에서 가능한 표준 Brush들을 예로 들 수 있다. 이들 Brush들은 아무 시간이나 Thread로 부터 사용될 수 있다.
Separate Windows
불행하게도 읽기 전용 제한은 엄청난 문제가 될 수 있다. UI의 분리된 부분들을 분리된 Thread에서 실행하고 싶은 많은 시나리오들이 존재한다. 만약 이들 UI 부분들이 서로 독립적이라면, 여러분은 이들은 다른 윈도우에 호스트 하여, 윈도우들을 분리된 Thread로 실행하면 된다. 이것은 어떤 시나리오들에 있어서 의미 있는 해결책이 될 수 있다. 특히, 독립적인 최상위 윈도우들에서 분리된 Thread들이 실행될 수 있는 시나리오에 적합하다. 이 접근에 대한 큰 제한은 한 윈도우부터의 그래픽스들이 다른 윈도우의 그래픽스와 혼합될 수 없다. 따라서, 다른 Thread의 UI로 자식 윈도우를 사용하는 동안에는 시스템은 그냥 다른 객체 위에 객체를 그리게 된다. 여러분은 Transparency를 사용할 수 없으며, 다른 객체를 Brush로 사용할 수 없으며, 다른 객체 위에 그릴 수 없다.
HostVisual
만약 여러분의 시나리오가 상호작용(사용자 입력)이 요구되지 않는다면, WPF에는 HotVisual이라는 옵션을 제공해주고 있다. 이 옵션은 WPF에 강력한 Composition 엔진을 가동해 준다. 이 구성 엔진은 다중 Thread부터의 그려지는 Primitive들을 하나의 화면에 결집하는 것이 이미 가능하다. Worker Thread에 의해 소유된 Element Tree는 자신 안에 자신만의 Composition Target(VisualTarget이라고 함)에 그려져서 결과는 UI Thread에게 소유된 HostVisual에 합쳐진다.
Issue #1: Hosting a Visual in XAML
해결 해야 할 첫 번째 문제는 HostVisual은 Visual로 부터 상속 받는다. 이 Visual을 호스트하기 위해서는 Panel이나 Border를 사용할 수 없다. Border는 하나의 자식을 가진 Panel들을 위한 표준 Base 클래스인 Decorator로 부터 상속 받는다. 불행하게도, Decorator의 자식은 강력하게 UIElement로 제한한다. 우리는 UIElement로부터 상속 받지 않은 HostVisual 사용해야 한다. 더 나아가 표준 요소들(Border, Grid, Canvas 등)의 자식으로 Visual으로 설정할 방법이 없기 때문에 직접 만들어야 한다.
[ContentProperty("Child")]
public class VisualWrapper : FrameworkElement
{
public Visual Child
{
get
{
return _child;
}
set
{
if (_child != null)
{
RemoveVisualChild(_child);
}
_child = value;
if (_child != null)
{
AddVisualChild(_child);
}
}
}
protected override Visual GetVisualChild(int index)
{
if (_child != null && index == 0)
{
return _child;
}
else
{
throw new ArgumentOutOfRangeException("index");
}
}
protected override int VisualChildrenCount
{
get
{
return _child != null ? 1 : 0;
}
}
private Visual _child;
}
Issue #2: Layout and the Loaded event
WPF는 매우 편리한 “Loaded”라는 이벤트를 제공한다. 이 이벤트는 기본적으로 요소가 완벽히 Initialized, Measured, Arranged, Rendered 그리고 윈도우 같은 Presentation Source에 들어간 후를 알려준다. MediaElement을 포함한 많은 요소들이 이 이벤트들을 이용하지만, 슬프게도 이 이벤트는 Presentation Source에 들어가지 않은 Element Tree를 위해 호출 되지 않아서, HostVisual/VisualTarget을 통한 Element Tree 표시는 당연히 Loaded 이벤트가 호출되지 않는다. 따라서 우리는 우리 자신만의 Presentation Source를 만들어서 Worker Thread이 소유할 최상위 Element Tree로 사용할 것이다. 이것은 즉시 다른 문제로 발전한다. Layout은 Presentation Source에 의해 다시 명령되지 않으면 모든 요소들에 의해 멈춘다. 불행하게도 이것을 하기 위한 공식적인 메커니즘은 internal로 되어 있기 때문에, 최상의 방법은 명시적으로 최상위 요소를 measure와 arrange하는 것이다.
public class VisualTargetPresentationSource : PresentationSource
{
public VisualTargetPresentationSource(HostVisual hostVisual)
{
_visualTarget = new VisualTarget(hostVisual);
}
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;
}
public override bool IsDisposed
{
get
{
// We don't support disposing this object.
return false;
}
}
private VisualTarget _visualTarget;
}
Background Threads
C#에서는 Thread들을 만들기 쉽다. 다만 하나 주위 해야 할 것은 Thread을 “Background” Thread로 선언해야 한다. “Background” Thread로 선언하지 않으면 응용프로그램은 Thread가 살아 있을 때까지 계속 실행이 될 것이다. 또한 기억할 것은 WPF의 부분들은 COM의 “Single Threaded Apartment”로 선언되어야 한다.
Thread thread = new Thread(/*…*/);
thread.ApartmentState = ApartmentState.STA;
thread.IsBackground = true;
thread.Start(/*…*/);
The Demo
XAML
<Window x:Class="VisualTargetDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:VisualTargetDemo"
Title="VisualTargetDemo"
SizeToContent="WidthAndHeight"
Loaded="OnLoaded"
>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<local:VisualWrapper Grid.Column="0"
Width="200" Height="100" x:Name="Player1"/>
<local:VisualWrapper Grid.Column="1"
Width="200" Height="100" x:Name="Player2"/>
<local:VisualWrapper Grid.Column="2"
Width="200" Height="100" x:Name="Player3"/>
</Grid>
</Window>
Code
public partial class Window1 : System.Windows.Window
{
public Window1()
{
InitializeComponent();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
Player1.Child = CreateMediaElementOnWorkerThread();
Player2.Child = CreateMediaElementOnWorkerThread();
Player3.Child = CreateMediaElementOnWorkerThread();
}
private HostVisual CreateMediaElementOnWorkerThread()
{
// Create the HostVisual that will "contain" the VisualTarget
// on the worker thread.
HostVisual hostVisual = new HostVisual();
// Spin up a worker thread, and pass it the HostVisual that it
// should be part of.
Thread thread = new Thread(new ParameterizedThreadStart(MediaWorkerThread));
thread.ApartmentState = ApartmentState.STA;
thread.IsBackground = true;
thread.Start(hostVisual);
// Wait for the worker thread to spin up and create the VisualTarget.
s_event.WaitOne();
return hostVisual;
}
private FrameworkElement CreateMediaElement()
{
// Create a MediaElement, and give it some video content.
MediaElement mediaElement = new MediaElement();
mediaElement.BeginInit();
mediaElement.Source = new Uri("http://download.microsoft.com/download/2/C/4/2C433161-F56C-4BAB-BBC5-B8C6F240AFCC/SL_0410_448x256_300kb_2passCBR.wmv?amp;clcid=0x409");
mediaElement.Width = 200;
mediaElement.Height = 100;
mediaElement.EndInit();
return mediaElement;
}
private void MediaWorkerThread(object arg)
{
// Create the VisualTargetPresentationSource and then signal the
// calling thread, so that it can continue without waiting for us.
HostVisual hostVisual = (HostVisual)arg;
VisualTargetPresentationSource visualTargetPS = new VisualTargetPresentationSource(hostVisual);
s_event.Set();
// Create a MediaElement and use it as the root visual for the
// VisualTarget.
visualTargetPS.RootVisual = CreateMediaElement();
// Run a dispatcher for this worker thread. This is the central
// processing loop for WPF.
System.Windows.Threading.Dispatcher.Run();
}
private static AutoResetEvent s_event = new AutoResetEvent(false);
}




Leave your greetings here.