Walking Through Avalon and Windows Forms Interoperability in Code

 

Deepak Kapoor and Nick Kramer
Microsoft Corporation

February 2005

Applies to
   Visual Studio 2005 beta 1 Standard or Express Edition
   The November 2004 Community Technology Preview Release of Avalon

Note   Community Technical Preview (CTP) builds do not go through the same rigorous testing that beta builds undergo. While betas receive a much higher level of testing and feature work, CTPs are intended to expose developers to the latest working build. CTPs are therefore unsupported, prerelease software.

Summary: Demonstrates interoperability between Windows Forms and Avalon applications. Learn how to create an Avalon control and host it in a Windows Forms application, and then create a Windows control and host it in an Avalon application using XAML. (14 printed pages)

Download the code sample that accompanies this article, AvalonWinFormsInteropSample.msi.

Contents

Section 1: Avalon Control
Windows Form Application
Section 2: Windows Control
Avalon Application
When Windows Forms Interop Is Appropriate
Summary

At the 2003 PDC, Microsoft released a preview of Longhorn and the next generation of APIs called WinFX. The PDC release was exciting, but when Microsoft announced a Community Technical Preview release of Avalon (which is a part of WinFX) for Windows XP and Windows 2003, the interest revived once again.

One of the ways to ensure the fast adoption of WinFX is to provide interop features with existing technologies. This article focuses on several of these features. This article is segmented into two sections. In the first section we look at an Avalon control hosted within a Windows Form application. In the second Section we go through a Windows control that is hosted in an Avalon application. The applications built in this article are designed to display images within the hosted controls. These images will be fetched from the Windows directory, which is a good source for some bitmap files.

To use the code that accompanies this article (AvalonWinFormsInteropSample.msi), you'll need the following:

Section 1: Avalon Control

Now let's look at our Avalon control. The purpose of this control is to display an image. In order to do this, the control will inherit from System.Windows.Controls.Image. In Visual Studio we create a new project of type Avalon and template Avalon Control Library. For the sake of clarity, the default class CustomControl1 has been renamed to ImageViewer.cs in the attached code. Below is the code needed for the ImageViewer class.

public class ImageViewer : System.Windows.Controls.Image
{
  protected override void OnMouseEnter(System.Windows.Input.MouseEventArgs e)
  {
    this.ImageEffect = new System.Windows.Media.ImageEffectGrayscale();
  }
    
  protected override void OnMouseLeave(System.Windows.Input.MouseEventArgs e)
  {
    this.ImageEffect = null;
  }
}

In the code above we inherit our class from System.Windows.Controls.Image and override the OnMouseEnter and OnMouseLeave event handlers. Within OnMouseEnter we apply a grayscale effect to our image. (There are many other effects available with the System.Windows.Media namespace that can be applied to an image.) Within OnMouseLeave we revert back to the original image by setting the ImageEffect to null. Figure 1 below shows you a loaded image without any effects applied, and Figure 2 shows the image with ImageEffectGrayScale applied when the mouse pointer enters the image area.This is all we need to do for our image control.

Aa480170.avalonwinformsinterop01(en-us,MSDN.10).gif

Figure 1. Image without any effect applied

Aa480170.avalonwinformsinterop02(en-us,MSDN.10).gif

Figure 2. Image with grayscale effect applied

Windows Form Application

For our next step, we create our second project, a Windows Forms application. This application will host the Avalon control that we just created. A new project of type Windows and Windows Application as the template serves our purpose. In order to host the Avalon control we will need reference to the Avalon Control Project we created above, and to some Avalon libraries in our project. In addition to the project reference, we have references to these four libraries:

Name Description
WindowsBase.dll Includes namespaces such as System.ComponentModel, System.Threading, System.Windows, System.Windows.Media, etc. Classes within these namespaces provide services for threading, a 3x3 matrix, and UIContext. From this library we will use UIContext, which is described below.
WindowsFormsIntegration.dll This library contains one namespace, System.Windows.Forms.Integration. Classes within this namespace, such as ElementHost and WindowsFormsHost, provide the interoperability features of Avalon. Both WindowsFormsHost and ElementHost are used in the downloadable code and are explained below.
PresentationCore.dll As the name indicates, this library provides the core presentation infrastructure required for Avalon. ImageGrayEffect, which we saw above, is within the System.Windows.Media namespace, which in turn is in this library. This library also provides classes needed for animation and to draw 3-D objects.
PresentationFramework.dll This library contains hundreds of classes that are required to create Avalon controls, Avalon documents, shapes, and so on. Classes within this dll also provide support for binding and security.

The overview provided above is quite narrow. These libraries provide most of the building-blocks required for writing Avalon applications. A thorough coverage of all the classes is beyond the scope of the article.

For our user interface we need a TextBox, a ListBox, and a Panel. Figure 3 shows what our user interface should look like.

Aa480170.avalonwinformsinterop03(en-us,MSDN.10).gif

Figure 3. Windows Form with a ListBox, Button, and ImageViewer

Now for our Windows Forms application, we will need some private members as shown below:

partial class Form1 : Form
{
  private AvalonControls.ImageViewer imageViewer;

  private System.Windows.Forms.Integration.ElementHost host;
  private System.Threading.UIContext context;
  private System.Windows.HwndDispatcher dispatcher;
  private string[] _Files;

  ...
  ...
  ...
}

The panel we have added to the form will serve as a container for our Avalon control.

In the Form1_load method, add the following code:

private void Form1_Load(object sender, EventArgs e)
{
  context = new System.Threading.UIContext();
  dispatcher = new System.Windows.HwndDispatcher();
  
  dispatcher.RegisterContext(context);
  //enter the context and add dispatcher
  context.Enter();
  dispatcher.Attach();

  host = new System.Windows.Forms.Integration.ElementHost();  
  panel1.Controls.Add(host);
  host.Dock = DockStyle.Fill;

  imageViewer = new AvalonControls.ImageViewer();
  host.AddChild(imageViewer);

}

In the first line of our Form_Load we initialize our context object to a new instance of UIContext. What is UIContext? And why is it required? Here is the answer: In order to understand UIContext let's step ahead to the next line, in which we create a new HwndDispatcher. HwndDispatcher, which inherits from Win32Dispatcher, is a dispatcher for a Win32 message pump. A dispatcher basically handles any messages sent to the application by the message pump. In the old days of Windows programming this functionality was generally provided by the WndProc method. HwndDispatcher takes away the need to worry about the processing of messages coming from the pump. Further down in our code we register our context, which means that our dispatcher will dispatch messages only for the context we have created, and not the default context under which our window is created. The reason we need another context is because Avalon works with threads idifferently than a Windows Forms application. The BeginInvoke method, which under Windows Forms belongs to the controls, now belongs to the UIContext, and thus UIContext now handles the execution of delegates asynchronously. So UIContext, in our case, caters for the processing of threads associated with our Avalon control.

Next, we enter the context and attach the dispatcher. By attaching the dispatcher we are telling it to start monitoring our context, and take care of any messages that come through. We then initialize our host object to an instance of ElementHost. ElementHost is located in the windowsformsintegration.dll under the System.Windows.Forms.Integration namespace. Classes in this namespace provide functionality for Windows Forms and Avalon application integration. ElementHost, which actually is a Windows control, allows us to host an Avalon control. It acts as a container for our Avalon control and provides a least-effort approach to work with the hosted control's events and properties.

We then add our ElementHost control to the panel and set its dock property to Fill. After this we create an instance of the ImageViewer and add it to our ElementHost. By calling the AddChild method on the ElementHost we add a control to its Controls collection. This tells us that we can add more than one control in the same ElementHost.

The next step is to add some code that lets us load the data when we click the button and displays it when we couble-click the list box. The code below lets us load the ListBox on the Click event of the button.

private void btnLoadData_Click(object sender, EventArgs e)
{
  Files = Directory.GetFiles(
    System.Environment.ExpandEnvironmentVariables("%WinDir%"), "*.bmp");

  foreach (string file in _Files)
  {
    lstData.Items.Add(System.IO.Path.GetFileName(file));
  }
}

In the code below we display the image in our Avalon control. This code is executed on the DoubleClick event of the ListBox.

private void lstData_DoubleClick(object sender, EventArgs e)
{
  if (lstData.Items.Count > 0)
  {
    System.Windows.Media.ImageData imageData = new  
     System.Windows.Media.ImageData(
       new Uri(_Files[lstData.SelectedIndex]));

    imageViewer.Source = imageData;
    imageViewer.Height = new System.Windows.Length(
      Convert.ToDouble(imageViewer.Source.Height));
    
    imageViewer.Width = new  
      System.Windows.Length(
        Convert.ToDouble(imageViewer.Source.Width));
  }

We now have a Windows Forms application that hosts an Avalon control. The images are initially loaded with a default color, and if we move our mouse pointer over the image the grayscale effect is applied.

Aa480170.avalonwinformsinterop04(en-us,MSDN.10).gif

Figure 4. Windows Form displaying an image inside an Avalon control

Section 2: Windows Control

Thus far, we have created an Avalon control and hosted it in a Windows Forms application. We will now do the opposite of this and create a Windows control and host it in an Avalon application using XAML. The control and the application will again do the same thing, and the user interface will look like that shown in Figure 5.

We start by creating our windows Control. A new project of Type Windows and Windows Control Library lets us create our Windows control. Once again for the sake of clarity we will name our class ImageViewer. Our class should look similar to this:

public class ImageViewer : System.Windows.Forms.PictureBox 
{
  public ImageViewer()
  {
        
  }
}

Earlier in our Avalon control we applied a grayscale effect; recall that we were able to do that by creating an instance of the ImageEffectGrayscale object. In order to achieve a similar effect for our Windows control we will need to write a fair amount of code. I will not get into it as it's not the purpose of this article. The code that accompanies this article shows you one of the ways to do this.

Now that our Windows control is in place, let's get to the interesting part of hosting this control in an Avalon application.

Avalon Application

In order to develop our Avalon application a new project of type Avalon and Avalon Application as the template was created. Visual Studio creates MyApp.XAML and Window1.XAML files in the project. Window1.XAML is where we create our user interface. Before writing our UI code we add the required references. We have added a reference to the System.Windows.Forms dll, and a reference to the WindowsControl project.

We use Grid and FlowPanel to create the user interface represented in Figure 5. Parts of our XAML code are explained below. Here is the code for the user interface.

<?Mapping XmlNamespace="wfh" ClrNamespace="System.Windows.Forms.Integration" Assembly="WindowsFormsIntegration"?>
<?Mapping XmlNamespace="wcl" ClrNamespace="WindowsControls" Assembly="WindowsControls"?>
<Window def:Class="AvalonHost.Window1"
  xmlns="https://schemas.microsoft.com/2003/xaml" 
  xmlns:def="Definition"
  xmlns:wfh="wfh"
  xmlns:wcl="wcl"
  Text="AvalonHost"
  Width="465"
  Height="400">

  <Grid>
    <ColumnDefinition Width="5%"/>
    <ColumnDefinition Width="35%"/>
    <ColumnDefinition Width="60%"/>
        
    <RowDefinition Height="50"/>
    <RowDefinition Height="300"/>
    <RowDefinition Height="50"/>
       
    <FlowPanel Grid.Row="1" Grid.Column="1">
      <ListBox ID="lstData" Width="136" Height="251"      
        PreviewMouseDoubleClick="lstDataDoubleClick" />
    </FlowPanel>
        
    <FlowPanel Grid.Row="2" Grid.Column="1">
      <Button ID="btnLoadData" Content="Click" Click="ButtonClick" />
        </FlowPanel>
        
    <FlowPanel ID="HostPanel" Grid.Row="1" 
      Grid.Column="2" Width="267" Height="243">
      <wfh:WindowsFormsHost ID="windowsFormsHost">
        <wfh:WindowsFormsHost.Controls>
          <wcl:ImageViewer Name="imageViewer" def:ID="imageViewer"/>
        </wfh:WindowsFormsHost.Controls>
      </wfh:WindowsFormsHost>
    </FlowPanel>
  </Grid>
</Window>

Aa480170.avalonwinformsinterop05(en-us,MSDN.10).gif

Figure 5. Avalon Window with ListBox, Button, and WindowsFormsHost

At the top of Window1.xaml we have these two declarations:

<?Mapping XmlNamespace="wfh" 
  ClrNamespace="System.Windows.Forms.Integration"   
  Assembly="WindowsFormsIntegration"?>
<?Mapping XmlNamespace="wcl" 
  ClrNamespace="WindowsControls" 
  Assembly="WindowsControls"?>

These are namespace declarations for the System.Windows.Forms.Integration and WindowsControls. As discussed earlier, the System.Windows.Forms.Integeration namespace provides classes used to integrate Windows Forms and Avalon applications. In order to host our Windows control we need a WindowsFormsHost object. The following code lets us create and place the object within our XAML.

<FlowPanel ID="HostPanel" Grid.Row="1" 
  Grid.Column="2" Width="267" Height="243">
  <wfh:WindowsFormsHost ID="windowsFormsHost">
    <wfh:WindowsFormsHost.Controls>
      <wcl:ImageViewer Name="imageViewer" def:ID="imageViewer"/>
    </wfh:WindowsFormsHost.Controls>
  </wfh:WindowsFormsHost>
</FlowPanel>

We have placed our ImageViewer within the WindowsFormsHost. We can do the same thing in our code-behind C# file. For this article we will stick with the XAML method.

Next we add our ListBox and Button and add the code to load the data and display the image when double-clicked. We will add this code in Window1.xaml.cs file, which is the code-behind file for our Window1.xaml. This code is similar to what we wrote for the Windows application above. The only little difference is in the code that displays the image on the control:

private void lstDataDoubleClick(object sender, EventArgs e)
{
  System.Drawing.Image image = 
  System.Drawing.Image.FromFile(_Files[lstData.SelectedIndex]);
  imageViewer.Image = image;
  imageViewer.Height = image.Height;
  imageViewer.Width = image.Width;
}

The last three statements basically just set the image property, and the height and width property of the ImageViewer control. Build the project and run it. You will see that when you click the button a list is loaded in the list box, and when you double-click an item in the list box an image is displayed in the ImageViewer control. This is illustrated in Figure 6.

Aa480170.avalonwinformsinterop06(en-us,MSDN.10).gif

Figure 6. Avalon application displaying an image using the WindowsFormsHost control

When Windows Forms Interop Is Appropriate

Although we have demonstrated how to provide interoperability between Windows Forms and Avalon applications, there are a few things that should be kept in mind. Windows Forms interop works best when the code being hosted is fairly sizable. If you host a small bit of code, you may find yourself spending more time writing interop code then writing your application. It's usually best if what's being hosted is a self-contained chunk of UI that could be readily described to an end-user, such as an entire dialog box, a larger control like a DataGridView, or a UserControl.

WindowsFormsHost mostly behaves like any other Avalon FrameworkElement, although there are some important differences around output (drawing and graphics) and input (mouse and keyboard). Differences in output behavior include:

  • WindowsFormsHost cannot be rotated, scaled, skewed, or otherwise affected by a Transform.
  • WindowsFormsHost does not support the Opacity property (aka alpha blending). If content inside the WindowsFormsHost does System.Drawing operations that include alpha, that's fine, but the WindowsFormsHost itself only supports Opacity = 100% and can only be contained within other elements that are Opacity = 100%.
  • WindowsFormsHost will appear on top of other Avalon elements in the same top-level window. (But note that menus, ToolTips, and combo box drop-downs are separate top-level windows, and so should work fine with WindowsFormsHost.)
  • WindowsFormsHost does not respect the clipping region of its parent UIElement.

Differences in input behavior include:

  • While the mouse is over the WindowsFormsHost, you won't receive Avalon mouse events, and Avalon's IsMouseOver property will return false.
  • While the WindowsFormsHost has keyboard focus, you won't receive Avalon keyboard events, and Avalon's IsFocusWithin property will return false.
  • When focus is within the WindowsFormsHost and changes to another control inside the WindowsFormsHost, you won't receive Avalon GotFocus/LostFocus events.

Readers well versed in Win32 may recognize that the above differences stem from the way Windows Forms and Avalon use hwnds. Each Windows Forms control has its own hwnd, but Avalon elements within the same top-level window share the same hwnd. Because WindowsFormsHost is a separate hwnd, it can only do things that hwnds can do, which doesn't include rotation, scaling, opacity, and so on.

Summary

This article has shown that Avalon has the flexibility to host Windows Controls, and the vice-versa. This is an important feature that will allow organizations to make a smooth transition towards Avalon by using a lot of code already written for Windows Forms. In this article we show how to use the ElementHost and WindowsFormsHost classes to facilitate interop requirements for our applications. We also show the power of XAML and how we can create an interface using XAML. Finally, we have discussed when it is appropriate to use Windows Forms interoperability.

 

About the authors

Deepak Kapoor works as a Developer in Australia. He has been working with Microsoft technologies for more than seven years. He designed and developed many Web-based solutions using ASP, and recently using ASP.NET. Deepak has also been working with the .NET Framework since its early Beta release, and plans to do the same with WinFX. He has created a community Web site for WinFX at http://www.deepwinfx.com. He can be reached at Deepak@DeepWinFX.com.

Nick Kramer joined Microsoft in 1998 as a developer in Windows Forms. Today he is a program manager on the Windows Client Platform team working on Avalon. He is responsible for the Avalon "element services" technologies, which include the XAML parser, property engine and style system, interop, and input/commanding. After-hours he can be found in the great outdoors hiking, bicycling, or snowboarding. If you have questions about Avalon and Windows Forms interop, you can contact Nick at nkramer@microsoft.com.