Silverlight Apps that Resize Themselves
December 17, 2009
New York, N.Y.
Yesterday I was working on a Silverlight application that adjusted its size within the browser page when I began encountering erratic behavior. Turns out I hadn't taken account of the zooming feature implemented in recent versions of Internet Explorer (and other browsers), and now I'm not sure I should need to.
In IE, you can zoom a whole page using the Ctrl key in combination with + and –, or you can twiddle the mouse wheel while holding down the Ctrl key. This zooming is independent of the Text Size feature that's been in IE for many years. The zoom percentage can also be set (or reset) in the Zoom menu item in the View menu, or by a little flip-up down in the lower right corner of the browser. The zoom affects pretty much everything — including images, video, ads, and Silverlight applications.
This blog entry is certainly not an exhaustive discussion of zooming in relation to Silverlight applications. I won't be discussing how you can turn off zooming in your Silverlight app with the enableautozoom parameter of the plug-in object in the HTML or ASPX file, or in code through the Settings object of the SilverlightHost object. Nor will I be discussing how to handle zooming behavior in your Silverlight app yourself by installing a handler for the Zoomed event of the Content object of SilverlightHost (which then disables automatic zooming), but I'll be touching briefly on getting access to the multiplicative zoom factor through that same Content object.
When you create a new Silverlight project in Visual Studio, HTML and ASPX files are created that contain an <object> tag for the Silverlight plug-in, which is the host for your application. The <object> start tag looks like this (though not formatted quite so neatly):
-
<object data="data:application/x-silverlight-2,"
type="application/x-silverlight-2"
width="100%" height="100%">
The width and height attributes of 100% mean that your application will be given an area equal to the current page size within the browser. Very often, this is exactly what you want when you create an HTML page that contains nothing more than a single Silverlight app. Here's an example:
This particular application simply shows its size, which is the same size as the browser page. As you resize the window, the application displays an updated size.
You can also zoom that window using one of the methods I described above. (I commonly experience a redraw problem when zooming in Internet Explorer 8, but simply change the window size slightly and everything gets updated properly.) You'll see that zooming indeed affects the Silverlight application: When the zoom factor goes above 100%, the application believes that it's getting smaller because it still needs to fit within the window. Zoom out, and the application thinks that it's getting larger. But this is exactly the behavior you want for a "demo" program that adapts itself to the size of its container.
Let me show you the source code for that program. I wanted to do everything in XAML so I defined all those lines as Rectangle elements that could be stretched and aligned and rotated to look like arrows:
<UserControl x:Class="ShowSizeApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid x:Name="LayoutRoot">
<Rectangle Width="2" Fill="Blue" HorizontalAlignment="Center" />
<Rectangle Width="2" Height="24" Fill="Blue"
HorizontalAlignment="Center" VerticalAlignment="Top"
RenderTransform="0.7 0.7 -0.7 0.7 0 0" />
<Rectangle Width="2" Height="24" Fill="Blue"
HorizontalAlignment="Center" VerticalAlignment="Top"
RenderTransform="0.7 -0.7 0.7 0.7 0 0" />
<Rectangle Width="2" Height="24" Fill="Blue"
HorizontalAlignment="Center" VerticalAlignment="Bottom"
RenderTransform="-0.7 0.7 -0.7 -0.7 3 24" />
<Rectangle Width="2" Height="24" Fill="Blue"
HorizontalAlignment="Center" VerticalAlignment="Bottom"
RenderTransform="-0.7 -0.7 0.7 -0.7 3 24" />
<Rectangle Height="2" Fill="Blue" VerticalAlignment="Center" />
<Rectangle Height="2" Width="24" Fill="Blue"
HorizontalAlignment="Left" VerticalAlignment="Center"
RenderTransform="0.7 0.7 -0.7 0.7 0 0" />
<Rectangle Height="2" Width="24" Fill="Blue"
HorizontalAlignment="Left" VerticalAlignment="Center"
RenderTransform="0.7 -0.7 0.7 0.7 0 0" />
<Rectangle Height="2" Width="24" Fill="Blue"
HorizontalAlignment="Right" VerticalAlignment="Center"
RenderTransform="-0.7 0.7 -0.7 -0.7 24 3" />
<Rectangle Height="2" Width="24" Fill="Blue"
HorizontalAlignment="Right" VerticalAlignment="Center"
RenderTransform="-0.7 -0.7 0.7 -0.7 24 3" />
<Border BorderBrush="Blue" BorderThickness="2" Background="White"
HorizontalAlignment="Center" VerticalAlignment="Center"
Padding="12" CornerRadius="12">
<TextBlock Name="txtblk" Foreground="Blue" />
</Border>
</Grid>
</UserControl>
For the text in the center, I wanted to set up ElementName bindings to the ActualWidth and ActualHeight properties of the UserControl. The bindings seemed to hooked up properly but they always showed values of 0. (At this point, I'm not sure I've ever persuaded an ElementName binding to work in Silverlight!) For that reason I had to set the text from code:
using System;
using System.Windows;
using System.Windows.Controls;
namespace ShowSizeApp
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
SizeChanged += OnPageSizeChanged;
}
void OnPageSizeChanged(object sender, SizeChangedEventArgs args)
{
txtblk.Text = String.Format("{0:F1} × {1:F1}", args.NewSize.Width,
args.NewSize.Height);
}
}
}
Using width and height attributes of 100% is ideal when you want a Silverlight application to live on a page by itself and assume the entire dimensions of the browser page. It does not make sense when the application is surrounded by HTML, for example, embedded in a blog entry. In those cases, you want to give the <object> tag a specific pixel width and height. The <object> might look like this, for example:
-
<object data="data:application/x-silverlight-2,"
type="application/x-silverlight-2"
width="384px" height="288px">
Those numbers aren't as strange as they seem! If you assume 96 pixels to the inch, the Silverlight application is actually being sized to a dimension of 4 inches by 3 inches. You can also put these explicit dimensions on the <div> tag that usually encloses the <object> tag, or the <form> tag that encloses the <div> tag but you'll need to make them styles, like this:
-
style="width: 384px; height: 288px"
If the Silverlight application does not set its own size (such as with Width and Height properties set on the UserControl derivative generally called MainPage) the application will get the size indicated in the <object> tag. Here's the same application as the one referenced above but with a specific size defined right in the HTML for this blog entry:
I've also put a
-
style="text-align: center"
attribute on the <div> element to center it. You could also align it at the right or replace that attribute with a:
-
style="float: left"
or:
-
style="float: right"
so that text flows around the application.
If you now zoom the browser window, you'll see the Silverlight application getting larger and smaller, but the application continues to perceive a size of 384 by 288. The zooming is for the benefit of the user and has no effect on the application.
Sometimes Silverlight applications need to adjust their size within the browser. The application can do this by reaching into the <object> tag and adjusting the width and height attributes. The application gets access to this element with the static HtmlPage.Plugin property, which returns an object of type HtmlElement that refers to the <object> tag. You then use the methods GetAttribute and SetAttribute to get and set the width and height attributes. (Alternatively, you can use HtmlPage.Plugin.Parent to get access to the <div> element that usually encloses the <object> element and then use GetStyleAttribute and SetStyleAttribute to adjust styles.)
To the left is a Silverlight program that does just that. The style attribute on the <div> element sets float: left and margin: 12 so it won't be too crowded by this paragraph. The two gray bars are basically sizing borders that you can manipulate with the mouse. Move the bottom one up and down, and the right one left and right to effectively resize the Silverlight application. As you do this, the browser reflows the HTML around the application:
Play around with this long enough, and you're sure to find some flaws. For example, collapse the application into its narrowist width, which is set in code at 72 pixels, but keep moving the mouse. Now with the mouse button still pressed, come back and make the application wider. You'll see that the sizing border and mouse are now out of sync.
Things get really nuts when you scroll the page all the way to the bottom, and then try to make the Silverlight application shorter. (You won't be able to do that here unless you have a very tall monitor or zoom out a lot.) Because the page itself is decreasing in size as the Silverlight object is getting shorter, it amplifies the mouse movements.
Both of these problems are related to the simple technique I used. Those gray bars are just templated Thumb controls, and they're generating DragDelta events based on mouse movement without reference to any underlying control. I'll be working on something better soon.
Here's the XAML:
<UserControl x:Class="SizableApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:SizableApp">
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Thumb Grid.Row="1"
Grid.Column="0"
Cursor="SizeNS"
DragDelta="OnBottomThumbDragDelta">
<Thumb.Template>
<ControlTemplate>
<Rectangle Height="6" Fill="Gray" />
</ControlTemplate>
</Thumb.Template>
</Thumb>
<Thumb Grid.Row="0"
Grid.Column="1"
Cursor="SizeWE"
DragDelta="OnRightThumbDragDelta">
<Thumb.Template>
<ControlTemplate>
<Rectangle Width="6" Fill="Gray" />
</ControlTemplate>
</Thumb.Template>
</Thumb>
<src:ShowSizeControl Grid.Row="0"
Grid.Column="0" />
</Grid>
</UserControl>
It's just a 4 by 4 Grid with the two Thumb controls and a UserControl derivative named ShowSizeControl that is basically the same as the earlier program. (I normally would put another Thumb in the lower-right corner, but there's no cursor in Silverlight for diagonal sizing!)
The code basically handles the two DragDelta events:
using System;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Interop;
namespace SizableApp
{
public partial class MainPage : UserControl
{
SilverlightHost silverlightHost = Application.Current.Host;
HtmlElement silverlightPlugin = HtmlPage.Plugin;
public MainPage()
{
InitializeComponent();
}
void OnBottomThumbDragDelta(object sender, DragDeltaEventArgs args)
{
double height = GetDimensionPixelValue("height");
height += silverlightHost.Content.ZoomFactor * args.VerticalChange;
height = Math.Max(height, 72);
SetDimensionPixelValue("height", height);
}
void OnRightThumbDragDelta(object sender, DragDeltaEventArgs args)
{
double width = GetDimensionPixelValue("width");
width += silverlightHost.Content.ZoomFactor * args.HorizontalChange;
width = Math.Max(width, 72);
SetDimensionPixelValue("width", width);
}
double GetDimensionPixelValue(string style)
{
string dimension = silverlightPlugin.GetAttribute(style);
if (String.IsNullOrEmpty(dimension))
return 0;
if (dimension.EndsWith("px"))
dimension = dimension.Substring(0, dimension.Length - 2);
return Double.Parse(dimension);
}
void SetDimensionPixelValue(string style, double value)
{
silverlightPlugin.SetAttribute(style, ((int)Math.Round(value)).ToString() + "px");
}
}
}
Notice the two fields at the top: The SilverlightHost object is used for its Content property, which has the current ZoomFactor. This is necessary for calculations. The HtmlElement called silverlightPlugin is the <object> tag with the height and width attributes.
From the two DragDelta events, the GetDimensionPixelValue obtains the current height or width value from the <object> element, strips off the "px" suffix, and converts it to a double. One would think that it would only be necessary to add to that value the HorizontalChange or VerticalChange of the event. The SetDimensionPixelValue then appends the "px" back on the end and set the attribute. This approach worked fine when the browser window wasn't zoomed, but behaved poorly otherwise. The borders lagged behind the mouse.
I discovered that it's necessary to multiply the HorizontalChange and VerticalChange values by the ZoomFactor. I'm convinced this is a bug in Thumb, or some kludge to make the Thumb work with scrollbars and sliders. (Try running my Thumb-based ClickAndDeformText app at 200% and then TouchAndDeformText, which uses my replacement for the Thumb.) When the browser is zoomed 200%, moving the mouse by a single pixel causes a MouseMove event to indicate a change of half a pixel, which is correct, but the Thumb indicates a delta of a quarter pixel.