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.

Tuesday, November 30, 2010

Two-Way Binding on TreeView.SelectedItem

In this post I’m going to describe an attached-behavior for a TreeView control that allows you to achieve two-way binding on the SelectedItem property. You can grab the source code here.

[UPDATE 6 Dec 2010]: I fixed a bug that was causing it to fail to update the TreeView when the selected node was set (through binding) before the view had loaded.

Background

If you have ever tried using the TreeView control with it’s ItemsSource bound to some kind of data context then you probably know how frustrating it can be to work with the SelectedItem property. This is especially true if you are trying to follow an MVVM pattern.

Typically when you use an ItemsControl you want to create a two-way binding on two of its properties: ItemsSource and SelectedItem. But TreeView, unlike other ItemsControl subclasses, has a read-only SelectedItem property which means no two-way binding like this:

<sdk:TreeView 
ItemsSource="{Binding TreeNodes}"
SelectedItem="{Binding SelectedNode, Mode=TwoWay}"/>


There are a number of workarounds for this challenge (and it is a challenge – just try a Google search for Silverlight TreeView SelectedItem) but most of the solutions I’ve read either seem too complex to use, require you to compromise the MVVM pattern, or are unfriendly with Blend. Wouldn’t it be nice if you could just attach a behavior to the TreeView to make it work?


BindableTreeViewSelectedItemBehavior


Yeah – it’s a long name for a behavior, but at least you know what it does just by reading its name. Using it in Blend is pretty easy:


Using the Behavior in Blend


Just drop it on the TreeView control and set its Binding property.The XAML binding will look like this:


<sdk:TreeView ItemsSource="{Binding TreeNodes}" ...>
<i:Interaction.Behaviors>
<Behaviors:BindableTreeViewSelectedItemBehavior
Binding="{Binding SelectedNode}"/>
</i:Interaction.Behaviors>
</sdk:TreeView>


You don’t need to make it a two-way binding, the behavior looks after that for you. Here is a sample application that has two TreeView controls bound to the same data context. If you change the selected item on one TreeView it will update the selected item on the other TreeView:



Under The Hood


This attached behavior works by acting as a go-between using the TreeView.SelectedItemChanged event, the TreeView.SelectedItem property, and a private two-way binding on the data context. Here is a diagram that shows how the behavior wires itself up:


Diagram of relationships for behavior


Click on the diagram to see a larger version. The diagram can be broken down as follows:



  • The behavior examines it’s Binding property and uses that information to create a private two-way binding between the property on the data context of the tree view (in our example it’s against the SelectedNode property) and the behavior’s private SelectedItemProperty DependencyProperty.
  • When the value for the DataContext’s SelectedNode property changes, the change event for the SelectedItemProperty DependencyProperty (AssociatedObjectSelectedItemChanged) fires and we set the new value on the TreeView using the TreeView.SelectedItem property. This allows the tree view to have its selected item set from the data context.
  • The behavior also attaches an event handler for the TreeView.SelectedItemChanged event (also called SelectedItemChanged) which fires whenever the user changes the selected item.
  • When the SelectedItemChanged event handler is called, the behavior updates the value of its SelectedItemProperty DependencyProperty. This allows the data context to have its SelectedNode value updated when the user changes the selected item in the TreeView.

Summary


In this post I described an attached behavior that lets you achieve two-way binding on the TreeView.SelectedItem property. The behavior is especially useful if you are using the MVVM pattern to keep your views and view models separate, and want to avoid code in the View’s code-behind file. The source can be downloaded here.

Tuesday, November 23, 2010

A Behavior for Remembering Settings

I have been unusually busy for the last couple of months and have been neglecting my blog. Things are still pretty busy, but I hope to squeeze out a few posts if I can.

In this post I’m going to describe a behavior that you can attach to your controls and bind to a property on that control. The behavior will monitor the value of that property and store any changes to it in Isolated Storage. The next time you start the application the behavior will look in Isolated Storage and load the value if it can find it, then restore the value to the control. This is a handy behavior for things like grid splitters, sliders, or similar types of controls (dial’s too!) where you want any changes the user makes to be remembered the next time they run your app.

Demo Application

You can grab the behavior here. And this is the behavior in action:

If you refresh this page, the application will remember the opacity value for the border control, the position of the grid splitter, and the selected index of the list.

Using the behavior looks like this:

   1: <Border x:Name="border">
   2:     <i:Interaction.Behaviors>
   3:         <TotalRecall:TotalRecallBehavior Binding="{Binding Opacity, ElementName=border}"/>
   4:     </i:Interaction.Behaviors>
   5: </Border>

There are two things worth discussing about this behavior; the Binding to a control’s property and the the storing/retrieving using Isolated Storage.


Binding to a Control’s Property


The XAML above shows the TotalRecallBehavior being used to remember the opacity of the border element. Internally, the behavior needs to know when the property being bound to (in this instance it is the Border.Opacity property) changes value so it can store it away for next time. To do this, the behavior has an internal DependencyProperty called BindingValueProperty that it uses to create another two-way binding to the same property on the control. BindingValueProperty has an event handler for when the value changes so it can store the new value in Isolated Storage.


The code that uses the behavior’s Binding property to set up a private two-way binding looks like this:



   1: private void CreateBindings()
   2: {
   3:     var expression = this.ReadLocalValue(BindingProperty) as BindingExpression;
   4:     var watcher = new Binding();
   5:     if (expression.ParentBinding.Source != null)
   6:     {
   7:         watcher.Source = expression.ParentBinding.Source;
   8:     }
   9:     else
  10:     {
  11:         watcher.ElementName = expression.ParentBinding.ElementName;
  12:     }
  13:  
  14:     watcher.Path = new PropertyPath(expression.ParentBinding.Path.Path);
  15:     watcher.Mode = BindingMode.TwoWay;
  16:     BindingOperations.SetBinding(this, BindingValueProperty, watcher);
  17:  
  18:     this.recallValue = new RecallValue(this.AssociatedObject.GetType().Name + "." + 
  19:         AssociatedObject.Name + "." + expression.ParentBinding.Path.Path);
  20: }

The call to “this.ReadLocalValue(BindingProperty) as BindingExpression” is exactly what UIElement.GetBindingExpression() does, but the Behavior is not a UIElement so I just make the same call. Getting hold of the BindingExpression is important since it gives us access to the correct binding source and property path. The BindingExpression only exists on the source object of the binding (even if it is two-way), so on line 2 of the XAML snippet the binding is set on the behavior and must therefore be read from the behavior with the call to ReadLocalValue (line 16 of the C# code above). If you try and read the BindingExpression on the element being bound to you will get null returned.


The behavior sets up the binding and creates an instance of the RecallValue class. I have future plans for the RecallValue class, but for now it is simply a wrapper around a string value.


Using the ApplicationSettings on IsolatedStorage


You can use isolated storage to store all kinds of information on the user’s local machine. You can read all about local storage in Silverlight here. One of the less frequently covered uses of Isolated Storage is the IsolatedStorageSettings.ApplicationSettings static property. This property is an IDictionary and has an indexer you can use to read and write with. These are very handy for storing atomic name-value pairs; exactly the kind of thing I want to do with this behavior.


The code that read’s the value from isolated storage is as follows:



   1: if (IsolatedStorageSettings.ApplicationSettings.Contains(this.recallValue.Key))
   2: {
   3:     this.recallValue.Value = (string)IsolatedStorageSettings.ApplicationSettings[this.recallValue.Key];
   4: }
   5: if (null != this.recallValue.Value)
   6: {
   7:     this.SetValue(BindingValueProperty, this.recallValue.Value);
   8: }

The value is read from isolated storage if it exists, and set as the value for our private BindingValueProperty dependency property, which is by now two-way bound to the control’s property. The IsolatedStorageSettings class takes care of creating a location in isolated storage for the values so there’s no other code needed for setting up file streams and creating directories etc. 

Similarly, the code for writing to the isolated storage settings is as follows:


IsolatedStorageSettings.ApplicationSettings[behavior.recallValue.Key] = behavior.recallValue.Value;


The rest of the behavior is just plumbing.


Future Improvements


As I said earlier, I have plans for the RecallValue class. I want to support the saving/restoring of complex objects and values that don’t parse easily to and from string.


I would also like to improve the design-time experience – the Binding type doesn’t play very nice with Blend’s element binding design tool, so I can’t use the CustomPropertyValueEditor attribute. I would really like the design time property editor for the Binding to list the properties on the attached object the same way the ChangePropertyAction does.


Summary


In this post I’ve described a behavior that remembers the value of a property on the control it is attached to. I described the use of the BindingExpression class to create a private two-way binding, and described how to use the IsolatedStorageSettings.ApplicationSettings dictionary. In the code download there are examples of the behavior being used on different kinds of controls and properties.

Monday, September 6, 2010

A Tiled Image Brush for Silverlight

The Silverlight ImageBrush does not support the TileMode property that exists in WPF, and as a result there is no built in way to have tiled images as a background brush for your controls and styles.

The following control may be useful to you if you find yourself wanting to achieve the same things as a WPF tiled image brush.

You can grab the source here. It is a control that behaves like a Border control, except that it also has a TiledImageSource property that you can use to select an image resource from the project.

The TiledBGControl control (um, didn’t think too hard about naming it) is a viewless control, so you can change its default control template the same way you can for any other control. This is the structure of the default control template:

ControlStructure

The template has a grid with two Border controls in it. The back-most Border  (the first one) is a control part that must exist, the source code of the control looks for this control and applies a shader effect to it to produce the tiled effect. The second Border is simply to provide the visible border around the control. I can’t use the first Border control to draw the visible border because all the pixels of that Border control (including the visible border) are replaced by the shader effect. The ContentPresenter is placed in the second Border control so that it appears on top of the tiled background.

The following two sections give a quick description of the pixel shaders and then an overview of how the control works.

Wrapping up a Pixel Shader

I’ve gone looking for a tiled image brush for Silverlight before. It’s just not possible (to the best of my knowledge) to create in Silverlight the same kind of tile brush that WPF has. The closest I’ve seen to accomplishing this effect is with a pixel shader. A pixel shader is a set of instructions that can change the pixels in a given area. For example, the built in Silverlight blur and drop-shadow effects uses a pixel shader. The instructions are written in a language called HLSL, compiled, and wrapped up in the .Net framework ShaderEffect class.

Walt Ritcher’s excellent free Shazzam tool can be used to create and test pixel shaders. It comes with a shader called “Tiler”, authored by A.Boschin, that allows you to achieve the tiled background effect but it is a little awkward to use as-is for a fluid interface. The Tiler shader has four properties for controlling how it is applied:

  • VerticalTileCount – how many tiles to squeeze in vertically in the given space
  • HorizontalTileCount – how many tiles to squeeze in horizontally in the given space
  • HorizontalOffset – a horizontal offset to apply to the first tile
  • VerticalOffset – a vertical offset to apply to the first tile

Using a shader like this is a little awkward because a shader doesn’t know how big an area it is being applied to (in pixels). It processes each location in the area by using a value between 0 and 1 for an x and y coordinate. If you want to keep the tile image at a 1:1 scale, you need to calculate how many tiles fit into the area the shader is applied to, and update this every time the area changes size. In WPF, the pixel shaders are executed on the GPU which saves the CPU for application logic, but Silverlight does not use the GPU (and probably never will), so the shaders must be used sparingly to prevent the CPU from slowing to a crawl.

I used the Tiler sample shader as a starting point and modified it, replacing the properties above with these ones:

  • TextureMap – the tile image to repeat as a background
  • DestinationHeight – the height of the destination area. This needs to be updated as the target area changes size
  • DestinationWidth – the width of the destination area. This also needs to be updated as the target area changes size
  • TileHeight – the height of the tile to be repeated as a background
  • TileWidth – the width of the tile to be repeated as a background

The main reason I did it this way was to learn HLSL; I could have just as easily used the same approach as the Tiler shader and calculated the number of vertical and horizontal tiles when the container changed size. The code for my pixel shader is as follows:

   1: /// <class>TilerXY</class>
   2: /// <description>Pixel shader tiles the image to size according to destination width and height</description>
   3: ///
   4:  
   5: // Created by P.Middlemiss: phil.middlemiss@gmail.com
   6: // blog: http://silverscratch.blogspot.com/
   7:  
   8: sampler2D TextureMap : register(s2);
   9:  
  10:  
  11: /// <summary>The height of the target area.</summary>
  12: /// <minValue>0</minValue>
  13: /// <maxValue>3000</maxValue>
  14: /// <defaultValue>300</defaultValue>
  15: float DestinationHeight : register(C1);
  16:  
  17: /// <summary>The width of the target area.</summary>
  18: /// <minValue>0</minValue>
  19: /// <maxValue>3000</maxValue>
  20: /// <defaultValue>300</defaultValue>
  21: float DestinationWidth : register(C2);
  22:  
  23: /// <summary>The height of the tile.</summary>
  24: /// <minValue>0</minValue>
  25: /// <maxValue>500</maxValue>
  26: /// <defaultValue>100</defaultValue>
  27: float TileHeight : register(C3);
  28:  
  29: /// <summary>The width of the tile.</summary>
  30: /// <minValue>0</minValue>
  31: /// <maxValue>500</maxValue>
  32: /// <defaultValue>100</defaultValue>
  33: float TileWidth : register(C4);
  34:  
  35: sampler2D SourceSampler : register(S0);
  36:  
  37: float4 main(float2 uv : TEXCOORD) : COLOR
  38: {
  39:     float xx = ((uv.x * DestinationWidth % TileWidth) / TileWidth);
  40:     float yy = ((uv.y * DestinationHeight % TileHeight) / TileHeight);
  41:     float2 newUv = float2(xx , yy) ;
  42:     float4 color= tex2D( TextureMap, newUv );
  43:     float4 source = tex2D( SourceSampler, uv);
  44:     color *= source.a;
  45:     return color;
  46: }


I’m still learning HLSL so I’m sure the code above could be improved. I have a fairly new computer and the CPU doesn’t really budge at all when using this component, but I would be interested to hear back from anyone with an older PC to see if performance is an issue, or from anyone who can suggest improvements to the HLSL.


The TiledBGControl


The TiledBGControl wraps up all of the awkwardness of using the pixel shader and lets you just supply the image source to use. The OnApplyTemplate method is overriden and carries out the following tasks:



  • The control looks for the template part called “PART_TiledBorder” and attaches the above shader effect to it.
  • An ImageBrush is created using the supplied TiledImageSource value and set as the value of the TextureMap property of the shader.
  • An event is attached to the border that updates the DestinationHeight and DestinationWidth when the border changes size.

The TileWidth and TileHeight properties of the shader have to be updated whenever the TileImageSource changes. The TileImageSource is a property of type ImageSource and is used as the value for the ImageBrush. There are a couple of things to know about the ImageBrush and ImageSource classes:



  • An ImageSource is not evaluated until is used, which means you don’t necessarily know the size of the image when you assign it.
  • The ImageBrush fires an ImageOpened event when the ImageSource is resolved
  • The resolved image is cached, so if you set the ImageSource to a value that has already been resolved you won’t get an ImageOpened event fired again.
  • The ImageSource, once resolved, can be cast to the BitmapImage type which has the PixelWidth and PixelHeight properties
  • Setting the ImageBrush.Stretch mode to None does not give the desired result – I thought it would just render the image in its actual size, but it doesn’t. I used Stretch.Fill instead

The code for creating the ImageBrush from the provided ImageSource is as follows:



   1: ImageBrush brush = new ImageBrush();
   2: brush.ImageSource = this.TiledImageSource;
   3: bool isOpened = 0 != ((BitmapImage)brush.ImageSource).PixelWidth;
   4: if (!isOpened)
   5: {
   6:     // we don't have the size yet so work that out when the ImageSource is resolved
   7:     brush.ImageOpened += (sender, args) =>
   8:         {
   9:             brush.Stretch = Stretch.Fill;
  10:             this.tilerXY.TileWidth = ((BitmapImage)brush.ImageSource).PixelWidth;
  11:             this.tilerXY.TileHeight = ((BitmapImage)brush.ImageSource).PixelHeight;
  12:         };
  13: }
  14: else
  15: {
  16:     brush.Stretch = Stretch.Fill;
  17:     this.tilerXY.TileWidth = ((BitmapImage)brush.ImageSource).PixelWidth;
  18:     this.tilerXY.TileHeight = ((BitmapImage)brush.ImageSource).PixelHeight;
  19: }
  20: this.tilerXY.TextureMap = brush;

The tile images in the sample above are from www.repeatxy.com. I’ve put the control up on the Expression Gallery, so feel free to grab the source and use it according to the license there.