Wednesday, February 2, 2011

Designing for Browser-Zoom: Part 2

[UPDATED: Fixed broken links for download]

The ability to adjust the magnification level in your browser (using zoom + or -) has the potential to ruin your carefully crafted UI design. In Part 1 we took a look at the mechanisms involved in browser zoom and the effect it can have on your Silverlight app (particularly the EnableAutoZoom setting).

In this part we are going to handle browser zoom with visual states and use an attached behaviour that allows us to integrate zoom management into our views and control templates from Blend, without having to write any code.

A Demonstration

Here is the sample application we will be working with. This is just a mock layout of a simple utility app – it is contrived for the purposes of showing how scaling can affect your design:

to launch this in a separate window and use the the browser’s zooming feature to zoom in to 150%, 200%, then back to 100%. This demonstrates the desired behaviour. You will see the spacing content change at each of those zoom levels – and at 200% the images are no longer visible. There is a link for the source code at the end of the article.

If this app is zoomed into 200% within the same available space, without adjusting the design for the zoom level, then the result is the undesired behaviour which looks like this:

ZoomWithoutResizerCapture200

As you can see, the title bar now takes up half the available height, a complete row of buttons is missing, and the button captions are cut off. Remember, if the user has their DPI settings set to show large text this could already be happening to your existing Silverlight apps!

Using Visual States for Zoom Levels

I’m going to make an assumption here for the sake of demonstration. I’m going to assume that some user research was carried out and showed that people were zooming in (or it was happening automatically based on the host computer’s DPI setting) because they liked large text – the images are not important to them.

Given this assumption, I need to be able to adjust the layout for different zoom levels, giving priority to the text in the application (headings, button captions etc). Visual States are ideal for showing the same information in a different way. I’ll use Blend to show how I have set up the visual states here:

ZoomStates

The image above shows the VisualStateGroup “ZoomStates” with three VisualStates in the group: Unzoomed, Zoomed150, and Zoomed200. In each of these states I have adjusted the spacing available for the header and adjusted the margins around each of the panels (which are HeaderedContentControls).

I have a problem though: I have customized the control templates for the HeaderedContentControl and the Button controls. How do I include changes to the control templates as part of a visual state for the main view? I can’t! I could create 3 different control templates for each control I was customizing, but that would quickly become a maintenance nightmare for a decent sized project.

There is another answer to this problem that I will discuss below, but for now we have to work out how to switch states as the browser zoom changes.

The Application.Host.Content.Zoomed Event

The Content class in the System.Windows.Interop namespace (part of the Silverlight System.Windows assembly) is described by the documentation as follows:

“Reports details about the Silverlight-based application's instantiation settings, and exposes some of the HTML DOM values for the hosted Silverlight plug-in instance. System.Windows.Interop.Content contains APIs that specifically relate to the Silverlight content area (the area declared by the width and height in the initialization).”

One of its members is the Zoomed event. If the browser starts up and responds to the host system’s DPI settings, or if the user manually zooms, then this event is fired. We can hook into this event in the Loaded event of our main view and respond to changes in the zoom like this:

   1: public partial class MainPage : UserControl
   2: {
   3:     public MainPage()
   4:     {
   5:         // Required to initialize variables
   6:         InitializeComponent();
   7:         Application.Current.Host.Content.Zoomed += this.Content_Zoomed;
   8:     }
   9:  
  10:     private void Content_Zoomed(object sender, EventArgs e)
  11:     {
  12:         var zoom = Application.Current.Host.Content.ZoomFactor;
  13:         if (zoom <= 1)
  14:         {
  15:             VisualStateManager.GoToState(this, "Unzoomed", true);
  16:         }
  17:         else if (zoom <= 1.5)
  18:         {
  19:             VisualStateManager.GoToState(this, "Zoomed150", true);
  20:         }
  21:         else if (zoom <= 2)
  22:         {
  23:             VisualStateManager.GoToState(this, "Zoomed200", true);
  24:         }
  25:     }
  26: }


The above code hooks into the Zoomed event and, when it fires, checks what the current ZoomFactor is. It then uses that value to determine which visual state it should transition to.


Now we have a mechanism to adjust our design to the current zoom level, but we still have a few problems:



  1. We still can’t reach into control templates to adjust margins and content

  2. We have to re-write this code (paying careful attention to state names) for every view we create

  3. We have to write code! Not very friendly for dedicated designers who are working with Blend


Behavioural Zoom Management



An attached behaviour could help us solve all three of these problems. Let's look at each problem in reverse order:


Unfriendly to a designer: An attached behaviour is drag-n-drop easy and we can use a Blend SDK attribute to help us pick the state to transition to.


Re-writing code: An attached behaviour encapsulates the code and lets us reuse it with ease.


Reaching into the control template: Hmmm, this one is a little more interesting. A control template already comes with pre-defined states (some controls have none) and using VisualStateManager.GoToState does not work for any additional states you add by hand (our ZoomedStates group). So if we customize a control template and add our new zoom states to it, can we find a way to transition to those states? As it turns out, there is a way: A visual state is a wrapper around a storyboard – so we could write our attached behaviour to locate the appropriate state, reach inside it for the storyboard, and just play the storyboard. This actually works, but there is a caveat that I will discuss below.


You can download the code for the ZoomResponder attached behaviour here. Without giving a full listing, I’ll go over the main parts of the code.


First up, here are the dependency properties for the attached behaviour:



   1: public static readonly DependencyProperty ZoomRangeStartProperty = DependencyProperty.Register(
   2:     "ZoomRangeStart", typeof(double), typeof(BrowserZoomStateBehavior), null);
   3:  
   4: public static readonly DependencyProperty ZoomRangeEndProperty = DependencyProperty.Register(
   5:     "ZoomRangeEnd", typeof(double), typeof(BrowserZoomStateBehavior), null);
   6:  
   7: public static readonly DependencyProperty StateNameProperty = DependencyProperty.Register(
   8:     "StateName", typeof(string), typeof(BrowserZoomStateBehavior), null);


Each dependency property also has a public class property with a getter and a setter.


This attached behaviour is responsible for one zoom state (“StateName”). To respond to the 150% and 200% zoom levels we need 3 of these attached behaviours (the third one is to transition back to the unzoomed state). The “ZoomRangeStart” is inclusive and the “ZoomRangeEnd” is exclusive. Therefore, the behaviour for switching to the 150% will have ZoomRangeStart = 1.5 and ZoomRangeEnd = 2 since there will be a different instance of the behaviour responsible for switching to 200%.


The public class property for the StateName property is decorated with the CustomPropertyValueEditor attribute from the System.Windows.Interactivity namespace:



   1:  
   2: [CustomPropertyValueEditor(CustomPropertyValueEditor.StateName)]
   3: public string StateName
   4: {
   5:     get
   6:     {
   7:         return (string)this.GetValue(StateNameProperty);
   8:     }
   9:     set
  10:     {
  11:         this.SetValue(StateNameProperty, value);
  12:     }
  13: }


That attribute lets Blend know that the property is the name of an existing state in the XAML. It changes the property editor for the the StateName property from a text box to a drop down combo box that lists the available states that have been defined. Handy!


So the view will have three of these templates attached to its root:AttachedZoomResponderBehaviors



Here is the main code that handles the state change:



   1: private void UpdateZoomedState()
   2: {
   3:     var zoomed = Application.Current.Host.Content.ZoomFactor;
   4:     if (zoomed >= this.ZoomRangeStart && zoomed < this.ZoomRangeEnd)
   5:     {
   6:         if (!VisualStateManager.GoToState(
   7:             this.AssociatedObject, 
   8:             this.StateName,
   9:             true))
  10:         {
  11:             if (null == this.AssociatedObject.Parent &&
  12:                 VisualTreeHelper.GetParent(this.AssociatedObject) is Control)
  13:             {
  14:                 // play the storyboard - we are probably inside a control template
  15:                 var stateGroups = VisualStateManager.GetVisualStateGroups(
  16:                     this.AssociatedObject);
  17:                 foreach (VisualStateGroup stateGroup in stateGroups)
  18:                 {
  19:                     foreach (VisualState visualState in stateGroup.States)
  20:                     {
  21:                         if (null != visualState.Storyboard &&
  22:                             visualState.Name.Equals(this.StateName))
  23:                         {
  24:                             visualState.Storyboard.Begin();
  25:                             return;
  26:                         }
  27:                     }
  28:                 }
  29:             }
  30:         }
  31:     }
  32: }


The behaviour first tries to use the VisualStateManager to transition to the associated state. If the result of the call to VisualStateManager.GoToState is false then we know the call failed, possibly because the associated object is inside a control template (as discussed above). The code checks to see if the logical parent of the associated object is null and if the visual parent of the associated object is a control. If both of these conditions are true then we will assume we are attached to a control template and try to find a matching state associated with it. If we find one, we grab its storyboard and play it.


Caveats



There is one thing to be aware of when using this inside a control template. Do not use storyboards with “From” values in the key frames, otherwise you will end up with bizarre animations playing when they should not. For example, if you had the visual state for “Unzoomed” specify a “From” value for a key frame, you would end up playing that storyboard regardless of the zoom factor the browser was last set at, including values that fall between the different states (e.g. 125%).


Summary



In these two posts I have shown how the browser zoom can play a big part in how your design appears to the end user. Introducing visual states to accommodate different zoom levels is one way to handle it. An attached behaviour that wraps up the management of switching states in response to the zoom event provides an elegant response to this issue that designers can use.


You can grab the code here.

Tuesday, February 1, 2011

Designing for Browser-Zoom: Part 1

Zooming is good for you

The ability to zoom your web browser content is often over-looked when designing either traditional or Silverlight web applications - but it can also be a feature, rather than a problem. Silverlight’s native ability to respond to the browser-zoom provides the potential to really enhance your user’s experience of the application.

I was recently giving a demo of a Silverlight mock-up (with Sketch Flow) and one of the clients complained that he liked to increase the DPI setting on his computer because he had difficulty reading standard sized text. I showed him, on the demo computer, how he could zoom the content in the browser (in IE it’s Ctrl + and Ctrl – to zoom in and out) and the Silverlight app scaled along with it. Thankfully, the mock-ups happened to scale nicely and everything still fit snuggly in the available screen-space.

But it got me thinking about how to respond better to the browser content zooming in and out. I happen to manage the UX side of my current employer’s software development and it occurred to me that if a Silverlight application didn’t respond well to the zoom setting, it could completely ruin the user’s experience of that Silverlight Application.

So this will be a two-part series on how the user experience of your Silverlight application can be enhanced by accounting for the browser zoom feature:

  1. The first part (this part) will cover the zoom mechanism and the different pieces that are involved.
  2. In the second part I will show you how to adjust your design with a little bit of code and some visual states.I will also present an attached behaviour you can use in Blend to respond to the zoom event without having to write code.

How zooming works

There are four things you should know about the zooming feature of a browser and it’s interaction with Silverlight:

1. EnableAutoZoom

All the big-name browsers support zooming the page content. Internet Explorer (possibly others too) also respects the DPI settings on the host computer and automatically zooms the browser content to match it. In Windows 7, one way to change this DPI value is through the Control Panel –> Display settings page:

adjustscreendpiIf these settings are changed then, by default, when IE starts it will scale your whole web page, including your Silverlight application, automatically to reflect these DPI settings. The effect is the same as if you had scaled the whole page manually. The Internet Explorer team call this Adaptive Zooming.

If your Silverlight application takes up the whole page, or is part of a page design that uses the remaining space that other scalable elements don’t need, then this may cause problems for your layout. You may end up with headers or footers that unnecessarily take up way too much room and leave too little room for grids and lists of details.

At the very least, you should know how to disable this automatic behaviour if you don’t plan on accommodating it; you can control this behavior with the Silverlight object embed “EnableAutoZoom” setting which looks like this:

<object ...>
<param name="enableautozoom" value="bool"/>
...
</object>



To turn it off, set it to “False”. But I would recommend, instead of turning it off, that you use it to your advantage and keep your application usable at different zoom levels.


2. OnZoom

If you are writing JavaScript for your page for custom layout effects etc, then you can add a handler for the zoom event by attaching it to the OnZoom event on the Silverlight object. I only mention this in passing for the sake of completeness since I am focusing here on the Silverlight side of things. For more information on this event, you should read about the Html Bridge between managed and unmanaged code.


3. Content.Zoomed

You can respond to zoom events in managed code, inside your Silverlight app, by attaching a handler to the Content.Zoomed event like this:


Application.Current.Host.Content.Zoomed += new EventHandler(this.ContentZoomed);

This event will fire when the user manually zooms the page (e.g. Ctrl + and Ctrl - in IE) and will also fire when the application first loads (for IE at least) if the DPI settings for the computer have been changed from the standard setting.


The event handler takes the standard sender object and a basic EventArgs that contains no useful information. So how do we know what the current zoom settings is? With the Content.ZoomFactor property.


4. Content.ZoomFactor

The Content.ZoomFactor gives the current zoom ratio for the whole page including your Silverlight Application. A value of 1 means 100%, 1.5 means 150% etc.


What to do about zoom?


So how do we take advantage of this built in ability to scale the whole application? How do we accommodate the users who like having their DPI settings increased? We need to think carefully about how our screen layouts and designs would look when scaled to different zoom levels.


Just as an experiment, between now and the next post, fire up one of your full screen Silverlight apps, resize the browser to the smallest size where everything still fits nicely, and then zoom in to 150%, or even 200%. How does it look?


In the next post I’ll discuss a way that we can design for this feature and still have our screens look beautiful.