共用方式為


Hosting Windows Forms Controls in a WPF App

(Tunes I'm listening to while I blog: Soul Coughing- "Ruby Vroom")

Okay so we've been talking about the inter-op stuff a little bit, now it's time to get down to business and actually use it.  In this posting I will cover the details necessary to host a Windows Forms control in a WPF application.  Why would I ever need to host a Windows Forms control in a WPF app?  There are several reasons.  First of all, you may have existing Windows Forms user controls or custom controls that you have already invested a great deal of time and/or money in and you want to leverage those in a WPF application.  Secondly, you may discover that WPF does not have complete control parity with Windows Forms yet since it's a v1 release and it may take time for the control set to be as complete and rich as Windows Forms is today.

So for this discussion, we will create a rather boring little sample application, but it will demonstrate the necessary techniques without clouding the issue with other logic.  Let's say we want to do something as simple as host a Windows Forms ListBox in a WPF application.  I know, I know, that's a bad example because WPF already has a ListBox control that is perfectly acceptable and frankly you can do more fancy stuff with the WPF ListBox than you can the Windows Forms ListBox.  I have my reasons for choosing the ListBox as you will see later, so just chill out and read on...

First step is to create a WPF application using Visual Studio (VS):

Next we need to add references to System.Windows.Forms and System.Windows.Forms.Integration.  This is because the project templates for WPF applications do not automatically add references to these namespaces.  Obviously if we are going to use Windows Forms controls we need the System.Windows.Forms reference and the System.Windows.Forms.Integration namespace contains all of the Windows Forms - WPF interoperability "goo".

 

Note that the System.Windows.Forms.Integration namespace is defined in WindowsFormsIntegration.dll which can be found in the "\Program Files\Reference Assemblies\Microsoft\Avalon\v2.0.50215" directory.  This of course assumes you are using the September CTP version of WPF.  Huh?  Why is this dll way the hell over there and not with the rest of the WPF dlls.  Well, this is because the integration technology is lagging behind core WPF in terms of development and therefore quality.  It is currently provided in "tech preview" form as part of the WinFX SDK.  Once we get closer to shipping the technology it will merge into the standard redist and be just like the rest of the product, but in the meantime it lives where it lives.

Now we will be in the XML editor and the Window1.xaml file will be open for editing.  Since we are already staring at the XAML for the application, let's go ahead and decide to use XAML as the way we will add the Windows Forms ListBox to the WPF window.  We have a choice, you say?  Yep, we can either use XAML or we can choose to do it all using the code behind.  We'll cover both in the posting, but let's deal with the XAML first.

By default, XAML does not know about the classes that are defined in System.Windows.Forms or System.Windows.Forms.Integration because they are not defined as part of the schema that XAML uses.  In order to tell the XAML how to find these classes, we have to give it a little help.  Whate we need to do is add what are known as "mapping statements" to our XAML so that we can create namespace prefixes that map back to the specific assemblies where these classes live.

<?

Mapping XmlNamespace="wfi" ClrNamespace="System.Windows.Forms.Integration" Assembly="WindowsFormsIntegration"?>
<?Mapping XmlNamespace="wf" ClrNamespace="System.Windows.Forms" Assembly="System.Windows.Forms"?>

Next we need to set up an XML prefix that will refer back to the assemblies that we referenced in the mapping statements.  We'll do this just below where the other xmlns prefixes are defined in the Window element:

...
xmlns:wfi="wfi"
xmlns:wf="wf"
...

Now we can simply add the WindowsFormsHost control to the XAML using our namespace prefix and then add the ListBox control as a child of the WindowsFormsHost control.  The complete XAML would look something like the following:

<?

Mapping XmlNamespace="wfi" ClrNamespace="System.Windows.Forms.Integration" Assembly="WindowsFormsIntegration"?>
<?Mapping XmlNamespace="wf" ClrNamespace="System.Windows.Forms" Assembly="System.Windows.Forms"?>
<Window x:Class="AvalonApplication18.Window1"
xmlns="https://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="https://schemas.microsoft.com/winfx/xaml/2005"
xmlns:wfi="wfi"
xmlns:wf="wf"
Title="AvalonApplication18"

>
<Grid>
<wfi:WindowsFormsHost>
<wf:ListBox>

</wf:ListBox>
</wfi:WindowsFormsHost>
</Grid>
</Window>

Next we want to populate the ListBox with something, but how do we do that using XAML?  Well, you should understand the the XAML parser is really fairly generic and that it will work with any CLR types.  That means that XAML is capable of instantiating and populating Windows Forms controls or any other managed class.  You'll notice that we simply put <wf:ListBox> in the XAML above that the XAML parser is smart enough to locate the class in the referenced assembly and instantiate the ListBox class from the System.Windows.Forms namespace.  Okay, but how do I populate the ListBox?  You need to think about the XAML for Windows Forms just like the code that it would take to perform the same operation.  In this case, we want to populate the ListBox and that is done by adding objects to the Items collection of the ListBox.  So then the XAML would look like:

<

Grid>
   <wfi:WindowsFormsHost>
<wf:ListBox>
<wf:ListBox.Items>

</wf:ListBox.Items>
</wf:ListBox>
</wfi:WindowsFormsHost>
</Grid>

But what do we usually add to ListBoxes?  Strings right?  Okay, how do we add strings to the ListBox using XAML?  If you think about it for just a minute you will probably know the answer.  We already mentioned that XAML is capable of working with any managed type, so you should know that you can simply reference a string object via XAML in the same way that we referenced our ListBox control.  However, just as XAML didn't inherently know about the Windows Forms types, it also doesn't know about the String type which is defined in the System namespace.  Therefore, we have to add a mapping statement for System just like we did for the others and then we will be able to reference those types as well.  This is why I chose to use the ListBox, so that I could illustrate how to do things that may not be obvioius using XAML.  Now the complete XAML looks like:

<?Mapping XmlNamespace="wfi" ClrNamespace="System.Windows.Forms.Integration" Assembly="WindowsFormsIntegration"?>
<?Mapping XmlNamespace="wf" ClrNamespace="System.Windows.Forms" Assembly="System.Windows.Forms"?>
<?Mapping XmlNamespace="sys" ClrNamespace="System" Assembly="System"?>
<Window x:Class="AvalonApplication18.Window1"
xmlns="https://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="https://schemas.microsoft.com/winfx/xaml/2005"
xmlns:wfi="wfi"
xmlns:wf="wf"
xmlns:sys="sys"
Title="AvalonApplication18"
>
<Grid>
<wfi:WindowsFormsHost>
<wf:ListBox>
<wf:ListBox.Items>
<sys:String>Item1</sys:String>
<sys:String>Item2</sys:String>
<sys:String>Item3</sys:String>
<sys:String>Item4</sys:String>
</wf:ListBox.Items>
</wf:ListBox>
</wfi:WindowsFormsHost>
</Grid>
</Window>

Okay, now we have our Windows Forms ListBox in a WPF window.  Pretty simple, huh?  Could we do the same thing just using code behind and not use XAML?  Sure.  In order to do that we need to first remove all of the XAML inside the <Grid> tag.  And then we need to wire up the "Loaded" event for the window so that we can add our code for the ListBox when the WPF window is initially loaded (kind of like Form_Load in Windows Forms land).  Notice that we no longer really need the mapping statements since we are not referencing the Windows Forms, inter-op or System types in the XAML.

<Window x:Class="AvalonApplication18.Window1"
xmlns="https://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="https://schemas.microsoft.com/winfx/xaml/2005"
Title="AvalonApplication18"
Loaded="WindowLoaded"

>
<Grid x:Name="grid1">

   </Grid>
</Window>

Notice that we also gave the Grid tag a name.  This was so we can refer to this tag in our code behind and make it easier to add child controls to it.  Now go to the code behind file which is called Window1.xaml.cs.  Uncomment the line that has the WindowLoaded event handler.  Now we need to add code to this handler to instantiate our WindowsFormsHost control and ListBox control.

private void WindowLoaded(object sender, RoutedEventArgs e)
{
   WindowsFormsHost host = new WindowsFormsHost();
System.Windows.Forms.ListBox lb = new System.Windows.Forms.ListBox();
lb.Items.Add("Item1");
lb.Items.Add("Item2");
lb.Items.Add("Item3");
lb.Items.Add("Item4");
host.Children.Add(lb);
   this.grid1.Children.Add(host);
}

That's pretty much all there is to it.  I hope this helps you get started hosting Windows Forms controls in WPF applications.  Feel free to comment on this article...Later!