Showing posts with label ContentControl. Show all posts
Showing posts with label ContentControl. Show all posts

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.

Wednesday, May 26, 2010

Separating Content and Presentation with the ContentControl

The ContentControl is often overlooked when building Silverlight apps. It’s used inside many controls such as the Button or ChildWindow, but it also turns out to be quite useful on it’s own for separating content from presentation.

Here is a sample of the technique I will be describing. The buttons swap between two completely different “themes” for the same UserControl but the XAML for the UserControl doesn’t contain any theme elements at all, just layout and content. It’s not Picasso, but it will do to illustrate the concept:


You can grab the source files and read my full article on Silverlight Show here.

Wednesday, April 7, 2010

Changing Data-Templates at run-time from the VM

[Updated – Fixed code samples]
What do you do when you have a list box or data grid that has a collection of items to display - but some items need to be presented differently than others? For example, I may have a collection of company staff members to display in a single ListBox, but the managerial staff need to have extra information displayed (they always do!), and each level is styled slightly differently like this for example:


In this post I'll describe a technique I have used successfully before - I'll stick with the list of staff example above to illustrate the solution, but the class types are going to be a little contrived for the sake of simplicity.

The Data
Lets define a StaffMember class like this:
   1: public enum StaffRoleType { Manager, MiddleManager, LowerManager, Pleb, Contractor }
   2:  
   3: public enum CoffeeType { Espresso, Cappuccino, HotChocolate, Other }
   4:  
   5: public class StaffMemeber  
   6: {
   7:     public string Title { get; set; }
   8:     public string FirstName { get; set; }
   9:     public string LastName { get; set; }
  10:     public StaffRoleType StaffRole { get; set; }
  11:     public CoffeeType CoffeeType { get; set; }
  12:     public Color FavouriteColor { get; set; }
  13:     public string CarType { get; set; }
  14: }

The StaffRoleType will be used to differentiate the managers from the plebs so we can display the really important information for managers like what kind of coffee they drink and what their favourite color is. If this was real code we might have a different class for each role type, and they would all implement the INotifyPropertyChanged interface. But that class will do for demonstration purposes.


Different Data = Different Data Templates

A nice way to solve this problem would be if we could somehow use different data templates to display each type of staff- but do this inside the existing controls like a ListBox. Ideally we could use Blend to design these different data types too. Of course, the ListBox doesn't let us define more than one data template for the ListBoxItem; but rather than try and rewrite the ListBox, we can achieve our goals with a couple of simple additions to the ContentControl:




   1: using System.Collections.ObjectModel;
   2: using System.Linq;
   3: using System.Windows;
   4: using System.Windows.Controls;
   5: public class SelectableContentControl : ContentControl
   6: {
   7:   public static readonly DependencyProperty TemplateNameProperty = DependencyProperty.Register(
   8:      "TemplateName",
   9:      typeof(string),
  10:      typeof(SelectableContentControl),
  11:      new PropertyMetadata(string.Empty, TemplateNameChanged));
  12:   
  13:   private readonly ObservableCollection<DataTemplate> templateCollection = new ObservableCollection<DataTemplate>();
  14:   
  15:   /// <summary>
  16:   /// Gets the collection of templates
  17:   /// </summary>
  18:   public ObservableCollection<DataTemplate> Templates
  19:   {
  20:      get
  21:      {
  22:          return this.templateCollection;
  23:      }
  24:   }
  25:   
  26:   /// <summary>
  27:   /// Gets or sets the name of the template to use
  28:   /// </summary>
  29:   public string TemplateName
  30:   {
  31:      get
  32:      {
  33:          return (string)GetValue(TemplateNameProperty);
  34:      }
  35:   
  36:      set
  37:      {
  38:          SetValue(TemplateNameProperty, value);
  39:      }
  40:   }
  41:   
  42:   /// <summary>
  43:   /// Select the appropriate DataTemplate when the Content changes.
  44:   /// </summary>
  45:   /// <param name="oldContent">The old Content value.</param>
  46:   /// <param name="newContent">The new Content value.</param>
  47:   protected override void OnContentChanged(object oldContent, object newContent)
  48:   {
  49:      base.OnContentChanged(oldContent, newContent);
  50:      this.SelectTemplate();
  51:   }
  52:   
  53:   private static void TemplateNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  54:   {
  55:      ((SelectableContentControl)d).SelectTemplate();
  56:   }
  57:   
  58:   private void SelectTemplate()
  59:   {
  60:      if (!string.IsNullOrEmpty(this.TemplateName))
  61:      {
  62:          DataTemplate namedTemplate =
  63:              this.Templates.FirstOrDefault(
  64:                  t => t.GetValue(FrameworkElement.NameProperty).Equals(this.TemplateName));
  65:          if (null != namedTemplate)
  66:          {
  67:              this.ContentTemplate = namedTemplate;
  68:              return;
  69:          }
  70:      }
  71:   
  72:      // default to the first template
  73:      if (this.Templates.Count > 0)
  74:      {
  75:          this.ContentTemplate = this.Templates[0];
  76:      }
  77:   }
  78: }

 

The new control has two key properties:



  • Templates - an ObservableCollection of DataTemplate
  • TemplateName - a string representing the name of the template to choose.
The Templates property allows us to declare multiple DataTemplates in the XAML, and the TemplateName property allows us to bind to a property on the data to select which template to use. If we had multiple class types in our list, such as a Manager class etc, then we could bind on the ClassName, or even the class itself with a value converter.



This is what the XAML for a ListBox may look like:


   1: <ListBox ItemsSource="{Binding StaffList}" >
   2:     <ListBox.ItemTemplate>
   3:         <ss:SelectableContentControl TemplateName="{Binding StaffRole}" Content="{Binding}">
   4:             <ss:SelectableContentControl.Templates>
   5:  
   6:                 <!-- default for unmatched StaffRoleType values -->
   7:                 <DataTemplate>
   8:                     <local:PlebianUC />
   9:                 </DataTemplate>
  10:  
  11:                 <!-- Contractor template -->
  12:                 <DataTemplate x:Name="Contractor">
  13:                     <local:ContractorUC />
  14:                 </DataTemplate>
  15:  
  16:                 <!-- Contractor template -->
  17:                 <DataTemplate x:Name="MiddleManager">
  18:                     <local:MiddleManagerUC />
  19:                 </DataTemplate>
  20:  
  21:                 <!-- Contractor template -->
  22:                 <DataTemplate x:Name="Manager">
  23:                     <local:ManagerUC />
  24:                 </DataTemplate>
  25:             </ss:SelectableContentControl.Templates>
  26:         </ss:SelectableContentControl>
  27:     </ListBox.ItemTemplate>
  28: </ListBox>



So what's going on here?



The ListBox has it's ItemsSource bound to a property on the DataContext called StaffList, which contains a collection of StaffMember instances. The ListBox.ItemTemplate contains a single SelectableContentControl with its TemplateName property set to "StaffRole" and it's Content bound to the DataContext of the ListBoxItem. At run time when a DataTemplate is being created for each item in the listbox and the item is databound to it, the SelectableContentControl will read the StaffRole property and find a template with the same name as the StaffRole value. The first DataTemplate in the Templates collection will be used for any data items that have a StaffRole value that doesn't match a Template name. We don't even need a value converter in this example, since the StaffRole property will convert to a string without one.



In order to make this technique friendly to Blend you need to create a UserControl for each of the DataTemplates so that the content can be styled in Blend. If you are more comfortable writing XAML than using Blend then you can skip the UserControls and put the content directly in the DataTemplates. Either way, you will still need to specify the DataTemplate collection by hand in XAML since Blend doesn't provide and easy way to edit a collection of DataTemplate instances (at least that I know of)



Feel free to copy the code above and use it in your own projects. Let me know if you find it useful.