Walkthrough: Hosting a Windows Presentation Foundation Control in Windows Forms
Windows Presentation Foundation (WPF) provides a rich environment for creating applications. However, when you have a substantial investment in Windows Forms code, it can be more effective to extend your existing Windows Forms application with WPF rather than to rewrite it from scratch. A common scenario is when you want to embed one or more pages implemented with WPF within your Windows Forms application
This walkthrough steps you through an application that hosts a WPF page in a Windows Forms application. The page is a simple data-entry application that is packaged in a DLL. This example is designed to be nearly identical in appearance and functionality to Hosting a Windows Forms Control in a Windows Presentation Foundation Page. The primary difference is that the hosting scenario is reversed.
Note: |
---|
The hosted page is not a WPF control in the strict sense of the term. Rather, it is an ordinary WPF page that is packaged in a DLL. However, the techniques for hosting a WPF control on a form are identical to those used to host an ordinary page. See Control Customization for further discussion of WPF controls. |
The walkthrough is divided into two sections. The first section briefly describes the implementation of the WPF page. The second section discusses in detail how to host the page in a Windows Forms application, receive events from the page, and access some of the page's properties.
Tasks illustrated in this walkthrough include:
Implementing the Windows Presentation Foundation page.
Implementing the Windows Forms host application.
For a complete code listing of the tasks illustrated in this walkthrough, see Hosting a Simple Windows Presentation Foundation Control in a Windows Form.
Prerequisites
To complete this walkthrough you will need:
- Development Tools for .NET Framework 3.0, which enable you to create a WPF application project. For information on installing these tools, see Installation Instructions for the Windows SDK.
Implementing the Windows Presentation Foundation Page
The WPF page used in this example is a simple data-entry form that takes the user's name and address. When the user clicks one of two buttons to indicate that the task is finished, the page raises a custom event to return that information to the host. This page is essentially identical in form and function to the Windows Forms control used in Walkthrough: Hosting a Windows Forms Composite Control in Windows Presentation Foundation. There is nothing special about the page to customize it for hosting on a Windows Forms form. You could just as easily load it into a Frame on a larger WPF page. The following illustration shows the rendered page.
Windows Presentation Foundation page
Creating the Project
To start the project:
Launch Microsoft Visual Studio, and open the New Project dialog box.
Select the Windows Application (WPF) template.
Name the new project MyControls, and place it in a conveniently named top-level folder, for example, WfHostingWpf. Later, you will put the host application in this folder as well. Click OK to create the project. The default project contains a single page named
Page1
.Right-click the project name in Solution Explorer, and select Properties.
Set the Output type to Class Library to compile the page as a DLL.
Delete the application definition files, MyApp.xaml and MyApp.xaml.cs, from the project. You need those files only if you are implementing the page as an application.
Note: |
---|
When you compile a WPF application as a class library, you cannot launch it to view the rendered page. For that reason, you may find it convenient to leave the output type as "Windows Application" until the application is completely implemented. That enables you to check the appearance of the pages by launching the application. When you are satisfied, delete the application definition files and change the output type to "Class Library" to compile it as a DLL. |
Your project should have references to the following system DLLs. If any of these DLLs aren't included by default, add them to your project.
System
PresentationCore
PresentationFramework
WindowsBase
Implementing the Page's User Interface
The WPF page's user interface (UI) is implemented with Extensible Application Markup Language (XAML). It is designed to be similar in appearance and functionality to the Windows Forms control discussed in Walkthrough: Hosting a Windows Forms Composite Control in Windows Presentation Foundation. The page's data-entry UI consists of five TextBox elements. Each TextBox element has an associated TextBlock element that serves as a label. There are two Button elements at the bottom of the page, OK and Cancel. When the user clicks either button, the page raises a custom event to return the information to the host.
Basic Layout
The various UI elements are contained in a Grid element. You can use Grid to arrange the contents of the page in much the same way you would use a Table element in HTML. WPF also has a Table element, but Grid is more lightweight and better suited for simple layout tasks.
The following example shows the basic layout code. This code defines the overall structure of the page by specifying the number of columns and rows in the Grid element. Use it to replace the code in Page1.xaml.
<Grid xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MyControls.Page1"
Background="#DCDCDC"
Width="375"
Height="250"
Name="rootElement"
Loaded="Init">
...
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
Adding TextBlock and TextBox Elements to the Grid
You place a UI element in the grid by setting the element's RowProperty and ColumnProperty attributes to the appropriate row and column number. Remember that row and column numbering are zero-based. You can have an element span multiple columns by setting its ColumnSpanProperty attribute. See How to: Create a Grid Element for more information on Grid elements.
The following example shows the page's TextBox and TextBlock elements with their RowProperty and ColumnProperty attributes, which are set to place the elements properly in the grid. Add this code to Page1.xaml, just below the Grid element.
<TextBlock Grid.Column="0"
Grid.Row="0"
Grid.ColumnSpan="4"
Margin="10,5,10,0"
HorizontalAlignment="Center"
Style="{StaticResource titleText}">Simple WPF Control</TextBlock>
<TextBlock Grid.Column="0"
Grid.Row="1"
Style="{StaticResource inlineText}"
Name="nameLabel">Name</TextBlock>
<TextBox Grid.Column="1"
Grid.Row="1"
Grid.ColumnSpan="3"
Name="txtName"/>
<TextBlock Grid.Column="0"
Grid.Row="2"
Style="{StaticResource inlineText}"
Name="addressLabel">Street Address</TextBlock>
<TextBox Grid.Column="1"
Grid.Row="2"
Grid.ColumnSpan="3"
Name="txtAddress"/>
<TextBlock Grid.Column="0"
Grid.Row="3"
Style="{StaticResource inlineText}"
Name="cityLabel">City</TextBlock>
<TextBox Grid.Column="1"
Grid.Row="3"
Width="100"
Name="txtCity"/>
<TextBlock Grid.Column="2"
Grid.Row="3"
Style="{StaticResource inlineText}"
Name="stateLabel">State</TextBlock>
<TextBox Grid.Column="3"
Grid.Row="3"
Width="50"
Name="txtState"/>
<TextBlock Grid.Column="0"
Grid.Row="4"
Style="{StaticResource inlineText}"
Name="zipLabel">Zip</TextBlock>
<TextBox Grid.Column="1"
Grid.Row="4"
Width="100"
Name="txtZip"/>
Styling the UI Elements
Many of the elements on the data-entry form have a similar appearance, which means that they have identical settings for several of their properties. Rather than setting each element's attributes separately, the example code uses Style elements to define standard property settings for classes of elements. This approach reduces the complexity of the page and enables you to change the appearance of multiple elements through a single style attribute.
The Style elements are contained in the Grid element's Resources property, so they can be used by all elements on the page. If a style is named, you apply it to an element by adding a Style element set to the style's name. Styles that aren't named become the default style for the element. See Styling and Templating for additional information on WPF styles.
The following example shows the Style elements for the WPF page. Add the code to Page1.xaml, just below the Grid element. To see how the styles are applied to elements, see the previous code example. For example, the last TextBlock element has the inlineText
style, and the last TextBox element uses the default style.
<Grid.Resources>
<Style x:Key="inlineText" TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="10,5,10,0"/>
<Setter Property="FontWeight" Value="Normal"/>
<Setter Property="FontSize" Value="12"/>
</Style>
<Style x:Key="titleText" TargetType="{x:Type TextBlock}">
<Setter Property="DockPanel.Dock" Value="Top"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Margin" Value="10,5,10,0"/>
</Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Margin" Value="10,5,10,0"/>
<Setter Property="Width" Value="60"/>
</Style>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Margin" Value="10,5,10,0"/>
</Style>
</Grid.Resources>
Adding the OK and Cancel Buttons
The final elements on the page are the OK and Cancel Button elements, which occupy the first two columns of the last row of the Grid. These elements use a common event handler, ButtonClicked
, and the default Button style defined in the previous code example. Add the following code to Page1.xaml, just below the final TextBox element. The XAML part of the page is now complete.
<Button Grid.Row="5"
Grid.Column="0"
Name="btnOK"
Click="ButtonClicked">OK</Button>
<Button Grid.Row="5"
Grid.Column="1"
Name="btnCancel"
Click="ButtonClicked">Cancel</Button>
Implementing the Page's Code-Behind File
The WPF page's code-behind file, Page1.xaml.cs, implements four essential tasks:
Registers the page's DLL name with the Application object, so that it knows where to load the page from.
Handles the event that occurs when the user clicks one of the buttons.
Retrieves the data from the TextBox elements, and packages it in a custom event argument object.
Raises the custom
OnButtonClick
event, which notifies the host that the user is finished and passes the data back to the host.
The page also exposes a number of color and font properties that enable you to control the page's appearance. Unlike the WindowsFormsHost class, which is used to host a Windows Forms control, the ElementHost class exposes the page's Background property only. To maintain the similarity between this code example and the example discussed in Walkthrough: Hosting a Windows Forms Composite Control in Windows Presentation Foundation, the page exposes the remaining properties directly.
The Basic Structure of the Code-Behind File
The code-behind file consists of a single namespace, MyControls
, which contains two classes, Page1
and MyControlEventArgs
. Replace the code in Page1.xaml.cs with the following.
using System;
using System.Windows;
using System.Windows.Navigation;
using System.Windows.Controls;
using System.Windows.Media;
namespace MyControls
{
public partial class Page1 : Grid
{
//...
}
public class MyControlEventArgs : EventArgs
{
//...
}
}
The first class, Page1
, is a partial class containing the code that implements the functionality of the UI defined in Page1.xaml. When Page1.xaml is parsed, the XAML is converted to the same partial class, and the two partial classes are merged to form the compiled page. For this reason, the class name in the code-behind file must match the class name assigned to Page1.xaml, and it must inherit from the root element of the page. The second class, MyControlEventArgs
, is an event arguments class that is used to send the data back to the host.
Initializing the Page1 Class
The following code example implements several basic tasks:
Declares a private event,
OnButtonClick
, and its associated delegate,MyControlEventHandler
.Creates several private global variables that store the user's data. This data is exposed through corresponding properties.
Implements a handler,
Init
, for the page's Loaded event. This handler initializes the global variables by assigning them the values defined in Page1.xaml. To do this, it uses the Name assigned to a typical TextBlock element,nameLabel
, to access that element's property settings.
Add the following code to your Page1 class.
public partial class Page1 : Grid
{
public delegate void MyControlEventHandler(object sender, MyControlEventArgs args);
public event MyControlEventHandler OnButtonClick;
private FontWeight _fontWeight;
private double _fontSize;
private FontFamily _fontFamily;
private FontStyle _fontStyle;
private SolidColorBrush _foreground;
private SolidColorBrush _background;
public void Init(object sender, EventArgs e)
{
//They all have the same style, so use nameLabel to set initial values.
_fontWeight = nameLabel.FontWeight;
_fontSize = nameLabel.FontSize;
_fontFamily = nameLabel.FontFamily;
_fontStyle = nameLabel.FontStyle;
_foreground = (SolidColorBrush)nameLabel.Foreground;
_background = (SolidColorBrush)rootElement.Background;
}
Handling the Buttons' Click Events
The user indicates that the data-entry task is finished by clicking either the OK button or the Cancel button at the bottom of the page. Both buttons use the same Click event handler, ButtonClicked
. Both buttons have a name, btnOK
or btnFalse
, that enables the handler to determine which button was clicked by examining the value of the sender argument. The handler does the following:
Creates a
MyControlEventArgs
object that contains the data from the page's TextBox elements.If the user clicked the Cancel button, sets the
MyControlEventArgs
object'sIsOK
property to false.Raises the
OnButtonClick
event to indicate to the host that the user is finished, and passes back the collected data.
Add the following code to your Page1 class, below the Init
method.
private void ButtonClicked(object sender, RoutedEventArgs e)
{
MyControlEventArgs retvals = new MyControlEventArgs(true,
txtName.Text,
txtAddress.Text,
txtCity.Text,
txtState.Text,
txtZip.Text);
if (sender == btnCancel)
{
retvals.IsOK = false;
}
if (OnButtonClick != null)
OnButtonClick(this, retvals);
}
Creating Properties
The remainder of the class simply exposes properties that correspond to the global variables discussed above. When a property changes, the set accessor modifies the appearance of the page by changing the corresponding element properties and updating the underlying global variables.
Add the following code to your Page1
class.
public FontWeight MyControl_FontWeight
{
get { return _fontWeight; }
set
{
_fontWeight = value;
nameLabel.FontWeight = value;
addressLabel.FontWeight = value;
cityLabel.FontWeight = value;
stateLabel.FontWeight = value;
zipLabel.FontWeight = value;
}
}
public double MyControl_FontSize
{
get { return _fontSize; }
set
{
_fontSize = value;
nameLabel.FontSize = value;
addressLabel.FontSize = value;
cityLabel.FontSize = value;
stateLabel.FontSize = value;
zipLabel.FontSize = value;
}
}
public FontStyle MyControl_FontStyle
{
get { return _fontStyle; }
set
{
_fontStyle = value;
nameLabel.FontStyle = value;
addressLabel.FontStyle = value;
cityLabel.FontStyle = value;
stateLabel.FontStyle = value;
zipLabel.FontStyle = value;
}
}
public FontFamily MyControl_FontFamily
{
get { return _fontFamily; }
set
{
_fontFamily = value;
nameLabel.FontFamily = value;
addressLabel.FontFamily = value;
cityLabel.FontFamily = value;
stateLabel.FontFamily = value;
zipLabel.FontFamily = value;
}
}
public SolidColorBrush MyControl_Background
{
get { return _background; }
set
{
_background = value;
rootElement.Background = value;
}
}
public SolidColorBrush MyControl_Foreground
{
get { return _foreground; }
set
{
_foreground = value;
nameLabel.Foreground = value;
addressLabel.Foreground = value;
cityLabel.Foreground = value;
stateLabel.Foreground = value;
zipLabel.Foreground = value;
}
}
Sending the Data Back to the Host
The final component in the file is the MyControlEventArgs
class, which is used to send the collected data back to the host. Add the following code to your MyControls
namespace. The implementation is straightforward, and is not discussed further.
public class MyControlEventArgs : EventArgs
{
private string _Name;
private string _StreetAddress;
private string _City;
private string _State;
private string _Zip;
private bool _IsOK;
public MyControlEventArgs(bool result,
string name,
string address,
string city,
string state,
string zip)
{
_IsOK = result;
_Name = name;
_StreetAddress = address;
_City = city;
_State = state;
_Zip = zip;
}
public string MyName
{
get { return _Name; }
set { _Name = value; }
}
public string MyStreetAddress
{
get { return _StreetAddress; }
set { _StreetAddress = value; }
}
public string MyCity
{
get { return _City; }
set { _City = value; }
}
public string MyState
{
get { return _State; }
set { _State = value; }
}
public string MyZip
{
get { return _Zip; }
set { _Zip = value; }
}
public bool IsOK
{
get { return _IsOK; }
set { _IsOK = value; }
}
}
Implementing the Windows Forms Host Application
The Windows Forms host application uses an ElementHost object to host the WPF page on the form. The application handles the page's OnButtonClick
event to receive the data from the form. The application also has a set of option buttons that you can use to modify the page's appearance. The following screen shot shows the rendered form.
Windows Presentation Foundation page hosted in a Windows Forms application
Creating the Project
To start the project:
Launch Visual Studio, and open the New Project dialog box.
Select C# Projects with the Windows Application template.
Name the new project WFHost, and place it in the same top-level folder that contains the MyControls project. Click OK to create the project.
You also need to add a reference to the DLL that contains the WPF page:
Click the project name in Solution Explorer, and select Add Reference.
Click the Browse tab, and navigate to the folder that contains MyControls.dll.
Select MyControls.dll, and click OK to add the DLL to the list of references.
In Solution Explorer, add a reference to the WindowsFormsIntegration assembly, which is named WindowsFormsIntegration.dll.
The default location for this file is %programfiles%\Reference Assemblies\Microsoft\Framework\v3.0\WindowsFormsIntegration.dll.
Implementing the Form's User Interface Design
Open the Windows Forms Designer, and lay out the form as it appears in the illustration shown in the Implementing the Windows Forms Host Application section:
Expand the default form to accommodate the controls and the WPF page.
Add a System.Windows.Forms.Panel control to the upper-right corner of the form to hold the WPF page.
Add six sets of System.Windows.Forms.RadioButton controls, as shown in the illustration.
Add five System.Windows.Forms.Label controls to the lower-right corner of the form, as shown in the illustration. These controls act as labels for the data returned by the WPF control.
Add a Label control to the right of each Label control added in the previous step. Set the Text property of each control to "". These controls display the data returned by the WPF control.
Add another Label control to serve as a title for the group of controls from the last two steps. Because this Label is intended to serve as the title for the group, make the font size two points larger than the controls in the group.
Initializing the Form
You generally implement the hosting code in the form's Load event handler. In the Windows Forms Designer, double-click the form to create a Load event handler method. The following code example includes the sample's Load event handler, a handler for the WPF page's Loaded event, and declarations for several global variables that are used later. Replace the code in Form1.cs with the following.
partial class Form1 : Form
{
private ElementHost ctrlHost;
private MyControls.Page1 wpfAddressCtrl;
System.Windows.FontWeight initFontWeight;
double initFontSize;
System.Windows.FontStyle initFontStyle;
System.Windows.Media.SolidColorBrush initBackBrush;
System.Windows.Media.SolidColorBrush initForeBrush;
FontFamily initFontFamily;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
ctrlHost = new ElementHost();
ctrlHost.Dock = DockStyle.Fill;
panel1.Controls.Add(ctrlHost);
wpfAddressCtrl = new MyControls.Page1();
wpfAddressCtrl.InitializeComponent();
ctrlHost.Child = wpfAddressCtrl;
wpfAddressCtrl.OnButtonClick +=
new MyControls.Page1.MyControlEventHandler(
avAddressCtrl_OnButtonClick);
wpfAddressCtrl.Loaded += new RoutedEventHandler(
avAddressCtrl_Loaded);
}
void avAddressCtrl_Loaded(object sender, EventArgs e)
{
initBackBrush = (SolidColorBrush)wpfAddressCtrl.MyControl_Background;
initForeBrush = wpfAddressCtrl.MyControl_Foreground;
initFontFamily = wpfAddressCtrl.MyControl_FontFamily;
initFontSize = wpfAddressCtrl.MyControl_FontSize;
initFontWeight = wpfAddressCtrl.MyControl_FontWeight;
initFontStyle = wpfAddressCtrl.MyControl_FontStyle;
}
The Form1_Load
method in the preceding code example shows the general procedure for hosting a WPF control:
Create a new ElementHost object.
Set the control's Dock property to System.Windows.Forms.DockStyle.Fill.
Add the ElementHost control to the Panel control's Controls collection.
Create an instance of the WPF page.
Host the page on the form by assigning the page to the ElementHost control's Child property.
The remaining two lines in the Form1_Load
method attach handlers to two page events:
OnButtonClick
is a custom event that is fired by the page when the user clicks the OK or Cancel button. You handle the event to get the user's response and to collect any data that the user filled in.Loaded is a standard event that is raised by a WPF page when it is fully loaded. The event is used here because the sample needs to initialize several global variables using properties from the page. At the time of the form's Load event, the page is not fully loaded and those values are still set to null. You need to wait until the page's Loaded event occurs before you can access those properties.
The Loaded event handler is shown in the preceding code example. The OnButtonClick
handler is discussed in the next section.
Handling OnButtonClick
The OnButtonClick
event occurs when the user clicks the OK or Cancel button.
The event handler checks the event argument's IsOK
field to determine which button was clicked. The lbl
data variables correspond to the invisible Label controls that were discussed earlier. If the user clicked the OK button, the data from the page's TextBox controls is assigned to the corresponding Label control. If the user clicked Cancel, the Text values are set to null.
Add the following code to Form1.cs. You can now compile and run the application.
void avAddressCtrl_OnButtonClick(
object sender,
MyControls.MyControlEventArgs args)
{
if (args.IsOK)
{
lblAddress.Text = "Street Address: " + args.MyStreetAddress;
lblCity.Text = "City: " + args.MyCity;
lblName.Text = "Name: " + args.MyName;
lblState.Text = "State: " + args.MyState;
lblZip.Text = "Zip: " + args.MyZip;
}
else
{
lblAddress.Text = "Street Address: ";
lblCity.Text = "City: ";
lblName.Text = "Name: ";
lblState.Text = "State: ";
lblZip.Text = "Zip: ";
}
}
Modifying the Windows Presentation Foundation Page's Appearance
The RadioButton controls on the left side of the form enable the user to change the WPF page's foreground and background colors as well as several font properties. The background color is exposed by the ElementHost object. The remaining properties are exposed as custom properties of the page.
Double-click the RadioButton controls on the form to create templates for the corresponding CheckedChanged event handlers. Extract the code from the following handlers, and add it to the corresponding handlers in Form1.cs.
private void radioBackgroundOriginal_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_Background = initBackBrush;
}
private void radioBackgroundLightGreen_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_Background = new SolidColorBrush(Colors.LightGreen);
}
private void radioBackgroundLightSalmon_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_Background = new SolidColorBrush(Colors.LightSalmon);
}
private void radioForegroundOriginal_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_Foreground = initForeBrush;
}
private void radioForegroundRed_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_Foreground = new System.Windows.Media.SolidColorBrush(Colors.Red);
}
private void radioForegroundYellow_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_Foreground = new System.Windows.Media.SolidColorBrush(Colors.Yellow);
}
private void radioFamilyOriginal_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_FontFamily = initFontFamily;
}
private void radioFamilyTimes_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_FontFamily = new FontFamily("Times New Roman");
}
private void radioFamilyWingDings_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_FontFamily = new FontFamily("WingDings");
}
private void radioSizeOriginal_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_FontSize = initFontSize;
}
private void radioSizeTen_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_FontSize = 10;
}
private void radioSizeTwelve_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_FontSize = 12;
}
private void radioStyleOriginal_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_FontStyle = initFontStyle;
}
private void radioStyleItalic_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_FontStyle = System.Windows.FontStyles.Italic;
}
private void radioWeightOriginal_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_FontWeight = initFontWeight;
}
private void radioWeightBold_CheckedChanged(object sender, EventArgs e)
{
wpfAddressCtrl.MyControl_FontWeight = FontWeights.Bold;
}
See Also
Tasks
Walkthrough: Hosting a Windows Presentation Foundation Composite Control in Windows Forms
Reference
Concepts
Walkthrough: Hosting a Windows Forms Composite Control in Windows Presentation Foundation