The blog of dlaa.me

The Code Not Taken [Comparing two ways of creating a full-size Popup in Silverlight - then picking the best]

Update on 2010-06-17: Silverlight 4's behavior is different than Silverlight 3's (which is described below); I examine the difference in this follow-up post.

 

When it comes time to display a Popup that overlays the entire Silverlight plug-in, most people do it the same way. Unfortunately, although the typical approach appears correct at first glance (and may even survive a test pass!), there are some problems that are just about guaranteed to surface once the application is released. The purpose of this post is to educate people about those issues - and suggest an alternative approach that is easier to get right.

 

[Click here to download the complete source code for the sample (a Visual Studio 2010 (Beta 2) project targeting Silverlight 3)]

 

Let's start with a pretty typical first attempt at creating a full-plug-in Popup. This code uses Application.Current.Host.Content to get the size of the plug-in, creates a Popup with some simple content, sizes the content to match the plug-in's dimensions, and opens the Popup:

var root = Application.Current.Host.Content;
var popup = new Popup();
var child = CreateChildElement();
child.Width = root.ActualWidth;
child.Height = root.ActualHeight;
popup.Child = child;
popup.IsOpen = true;
Aside: I'm following Raymond Chen's convention that code in italics is wrong. So please don't start copy/pasting quite yet... :)

Here's how the sample application looks when we run it:

Initial state

When you press the top button to run the snippet above, the CreateChildElement method creates an Image element (showing the Silverlight logo) set to stretch horizontally and vertically. Everything looks fine immediately after pressing the button, but here's what happens if you make the browser a little bigger while the Popup is still on screen:

Resize not handled

Oops, we forgot to handle the resize event. No big deal - except that it's actually worse than that... Here's what things look like if you instead press the top button when the browser's zoom setting is set to 50% (as indicated by the magnifying glass at the bottom right of the browser window):

Zoom not respected

Ouch! As it turns out, this browser zoom problem is what seems to trip people up the most. When the browser is zoomed, some of the coordinates Silverlight exposes are affected - and Host.Content is one of them. Unless the browser zoom is at exactly 100%, basing anything on Host.Content's ActualWidth/ActualHeight without manually compensating for the zoom results in sizes that are too small (like we see here) or too large.

 

Okaaay, let's correct the resize thing first because it's easy and then we'll worry about zoom. Here's a simple modification of the code above to handle the Host.Content's Resized event:

var root = Application.Current.Host.Content;
var popup = new Popup();
var child = CreateChildElement();
EventHandler rootResized = delegate
{
    child.Width = root.ActualWidth;
    child.Height = root.ActualHeight;
};
root.Resized += rootResized;
rootResized(null, null);
popup.Child = child;
popup.IsOpen = true;
Aside: You may be wondering why I didn't use a Binding to synchronize the Width and Height properties instead of futzing around with an event handler like this. Well, I would have liked to, but there's the small issue that Silverlight doesn't provide property change notifications for ActualWidth/ActualHeight changes. So while I do recommend using Binding whenever you can, that's not really an option here.

So let's run the sample application and press the middle button to see how those changes worked out:

Zoom undone

Well, things look right at first. Though I don't demonstrate it here, we've successfully fixed the browser resize issue and that's good. But something is still wrong in the image above... If you look carefully, you can see that the browser zoom is set at 50% - yet somehow the image is sized correctly despite us not doing any work to handle the browser's zoom setting yet. How can that be? Hold on, Sherlock, there's another clue in the image: look at the size of the buttons. Yeah, those buttons are not the size they should be for the 50% zoom setting that's active (refer back to the previous image if you don't believe me). Those buttons are at the 100% size - wha??

Aside: Hey, don't feel bad, it weirded me out, too. :)

It turns out that when you attach an event handler to the Resized event, Silverlight disables its support for browser zoom. The reason being that Silverlight assumes the application has chosen to handle that event because it wants full control over the zoom experience (via ZoomFactor and Zoomed, perhaps). Now that's really kind of thoughtful of it and everything - but in this case it's not what we want. In fact, that behavior introduces a somewhat jarring experience because the graphics visibly snap between 50% and 100% as the Resized event handler is attached and detached. Our sample application is perfectly happy to respect the browser's zoom settings; it just wants to know when it has been resized so it can update the Popup's dimensions. Irresistible force, meet immovable object...

 

Like I said at the beginning, most people seem to gravitate to the approach above. But there's another option - one which I'll suggest is better because it doesn't suffer from either of these problems: use Application.Current.RootVisual instead!

var root = Application.Current.RootVisual as FrameworkElement;
var popup = new Popup();
var child = CreateChildElement();
SizeChangedEventHandler rootSizeChanged = delegate
{
    child.Width = root.ActualWidth;
    child.Height = root.ActualHeight;
};
root.SizeChanged += rootSizeChanged;
rootSizeChanged(null, null);
popup.Child = child;
popup.IsOpen = true;

As you can see, the code looks nearly identical to what we had before (except it's not in italics!). But by keeping all the math inside the domain of the plug-in, the need to account for browser zoom or hook the troublesome Resized event goes away completely! Instead, this code responds to the RootVisual's SizeChanged event and behaves correctly at first, when the browser is resized, for different zoom settings, and even when the zoom setting is changed while the Popup is visible!

Success!

What more could you ask for?

 

There are just two things worth calling out:

  1. The RootVisual must be an instance of a FrameworkElement-based class (which it is by default and 99.9% of the rest of the time; MainPage is a UserControl and that derives from FrameworkElement).
  2. If the size of the RootVisual is explicitly set for some reason, the Popup will match that size and not the size of the plug-in itself (though I can't imagine why you'd do this).

Those small caveats aside, it's been my experience that the RootVisual approach is more naturally correct - by which I mean that it tends to be right by virtue of its simplicity and the fact that it hooks up to the right thing. I don't promise that it's the best choice for all scenarios, but I'll suggest that it's worth considering first for most scenarios. There are probably ways to make either approach do what you want - but I prefer to start with the one that's less troublesome! :)