Welcome to MSDN Blogs Sign in | Join | Help
Clipping legacy content hosted inside a WPF scrolling region

Recently, I sat down at my computer in my home office tasked with making a better looking experience around Team Foundation Server work items. To date, we have a really cool Windows Forms version of the work item viewer inside Visual Studio but I wanted a more light-weight and portable means to view my work items. So, I chose Windows Presentation Foundation as the UI composition engine for my new work item viewer and began the task of writing a rendering engine in WPF to plug into the current Work Item store implementation. After getting neck-deep in the implementation, I encountered a nasty interop issue around the problem of Airspace.

Basically, my Airspace problem is one of Z-order: when you nest Windows Forms controls inside a WPF visual container (i.e. Window, Page, Panel, etc.) the Windows Forms control actually sits on top of the WPF window and maintains its own HWND. This, in and of itself, is not a problem because the fine developers of WPF took this into consideration when they wrote the interop control WindowsFormsHost. The WindowsFormsHost control acts as a placeholder for Windows Forms content so that the WPF layout engine can treat it as a native control.

You may be thinking: "Ok, so they figured it out...what's the big deal?" There is a nasty little behavior in the WindowsFormsHost that involves putting a Windows Forms control inside a WPF scrolling region (ScrollViewer).

Initial Repro 

The sample application above has a Windows Forms WebBrowser control inside a WPF scrolling region (along with several other controls) . This application simulates one of the conditions where lack of clipping on the hosted Windows Forms control is demonstrable. Below is the same application only with the scrolling region scrolled down until the web browser control "bleeds" through to the tab control and form above it:

repro_scroll_past_bounds

What we need here is the ability to either build our own custom forms host to support clipping of the hosted content, or use some GDI APIs to clip the hosted control at the HWND level. There are pros and cons of both approaches--the custom forms host has to support a large number of state changes in the hosted control and WPF visual tree while the GDI solution is more difficult to "tighten the screws." I'm going to talk about the second of these approaches in this blog (and will save the first for another entry).

GDI Approach

In order to use this approach we will derive from the WindowsFormsHost control--used by the WPF layout engine to provide a hosting platform for Win32 content. Once we have derived from this type we need to know when each of the scroll regions that we are contained within have scrolled. This can be accomplished using a class handler for the ScrollChanged event:

EventManager.RegisterClassHandler(typeof(ScrollViewer), ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(ScrollHandler));

Once we know when each scroll region we are contained within has scrolled we need to calculate how much of the control to keep and how much to clip. This is done by first getting the viewport of the most constrained scroll viewer and translating it to the containing windows' scale:

GeneralTransform transform = scrollViewer.TransformToAncestor(RootElement);
Point size = new Point(scrollViewer.ViewportWidth, scrollViewer.ViewportHeight);
Point location = new Point(0, 0);

return (transform.TransformBounds(new Rect(location.X, location.Y, size.X, size.Y)));

We will also need to grab the controls' extents and transform them to the containing windows' scale:

Point controlSize = new Point(RenderSize.Width, RenderSize.Height);
Point controlLocation = new Point(Padding.Left, Padding.Right);
GeneralTransform controlTransform = TransformToAncestor(RootElement);
Rect controlTransformRect = controlTransform.TransformBounds(new Rect(controlLocation.X, controlLocation.Y, controlSize.X, controlSize.Y));

Now that we have both sets of coordinates in the same scale, we can calculate the region that is the intersection between the two:

Rect intersectRect = Rect.Intersect(scrollRect, controlTransformRect);

This gives us the portion of the control that is visibly rendered within the viewport of the scroll region. Now that we have the visible region needed we can use the SetWindowRgn GDI function to perform the clipping for us:

[DllImport("User32.dll", SetLastError = true)]
private static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);

It is important to note that you will need to have the HWND of the control that the WPF interop control uses to synchronize the placeholder WPF element with your Windows Forms control.  Using reflection you can easily query the interop control for this handle  If you plan to support more than one font size (i.e. DPI) you will need to adjust your measurements accordingly:

CompositionTarget ct = source.CompositionTarget;
Matrix m = ct.TransformToDevice;
Point transformed = m.Transform(p);

Once you have the clipping code in place the result will be a Windows Forms control that clips correctly inside a WPF scrolling region:

fixed_initial fixed_scroll

Right-Click Test Execution

 

One of the great new features of Visual Studio 2008 is the ability to run your unit tests from the context of a test method or a test class.  This feature is known as right-click execution and provides a mouse-based complement to keyboard shortcuts for running a single test method or all the test methods in a test class or test assembly. 

When working with unit tests it is often useful to be able to run the current test method or all tests in the current test class.  This can be accomplished via the new Run Tests menu option on the unit test context menu (Fig. 1).  As you can see, the Run Tests menu option sits at the top of the unit test context menu, below the refactoring options.  Clicking this menu option will execute your tests in the current scope—where you are in your test project.  For example, if you right-click inside a test method, Run Tests will run just that test method.   If you right-click inside a test class, but not inside an actual test method—i.e. a method with the TestMethodAttribute attribute—Run Tests will run all the tests in the test class.  Last but not least, if you right-click inside a test project but neither inside a test method nor inside a test class, Run Tests will run all tests in the test project.

 

Figure 1: Unit Test Context Menu

Test Generation from a Compiled Assembly

Visual Studio 2008 (Codename "Orcas") can generate a basic test project including test classes and test methods from your C#, VB, or managed C++ source code.  But did you know it can also generate the same test project/classes/methods against a compiled assembly? 

In order to generate these tests, you will first need to compile an assembly.  The assembly may be compiled from C#, Visual Basic .NET, or managed C++.  Once you have your compiled assembly handy, open Visual Studio 2008, show the “Test View” tool window if it is not already showing (Test|Windows|Test View) and click on the “Create New Test” hyperlink in the middle of the tool window.  You will get a dialog like:

 

Figure 1: Add New Test Dialog

Choose “Unit Test Wizard” and click “OK.”  You will be prompted for a project name with a preset value.  Once you have named your new project, click “OK” again and you will be taken to the “Create Unit Test” dialog:

Figure 2: Create Unit Test Dialog

On the “Create Unit Test” dialog you will see a button at the bottom labeled “Add Assembly.”  This feature will allow you to add a managed assembly to the list of assemblies currently displayed in the tree view above.   Click the “Add Assembly…” button and choose your assembly. After clicking “OK” in the file chooser dialog you will be taken back to the “Create Unit Test” dialog and it will be populated with the types and members (in hierarchical order) from your assembly.

Figure 3: Create Unit Test Dialog with Types

 

You can choose the members for which you would like to generate tests by clicking the checkbox next to each name.  Once you have chosen the types you want to generate, click “OK” and the test generation engine will create a new test project in a new solution and generate basic test methods for all the types chosen in the types tree.  Below is a sample test project created for a compiled assembly:

Figure 4: Visual Studio with Test Project

You will have a test class file for each class in your CUT—Code Under Test—assembly.  In each test class file, you will see a test method for each method in your CUT class with the suffix “Test.”

Figure 5: Visual Studio with Open Test Class

If your assembly contains private types, our private accessor engine will create a “wrapper” assembly around your private types allowing you to test them via the test project.  In the sample above, notice that there is a Test Reference called “ConsoleApplication5.accessor” in your solution explorer.  This is a reference to the wrapper assembly—called an Accessor assembly—and will be what you use in your test code instead of the CUT type name when you want to author tests against a private type in the CUT assembly.  We add Inconclusive assertions to each of the test methods generated to be a reminder that you may need to add valid arguments and assertions inside each test method for testing the corresponding CUT method.  An example of this is in Figure 5 at the bottom of the ProgramConstructorTest method.  The assertion

Assert.Inconclusive("TODO: Implement code to verify target");

is added by the test generation engine to remind you that you need to fill out the rest of the test method.  In this case, you need to make sure that your target object reference contains a valid object reference:

Assert.IsNotNull(target);

You can run the test project at any time, including before you have completed the remaining test methods.  Below are a couple of screen shots taken before and after all the remaining test methods are filled in:

Figure 6: Test Results before Completing Test Methods

Figure 7: Test Results after Completing Test Methods

 

That’s it!  You can now write test code against a compiled assembly including testing private/internal types and members via the private accessor wrapper type.

 

 

Page view tracker