Showing posts with label Attached Property. Show all posts
Showing posts with label Attached Property. Show all posts

Saturday, July 31, 2010

A Dial (User) Control

I’ve had a busy month on the road visiting clients (and catching up afterwards), so my blog has been pretty quite. But I’ve finally had some time to sit down and play with a couple of ideas I’ve had around using the path list box. I’ve been wanting to make a dial control for quite a while and when the path list box came along in Silverlight 4 it sparked a couple of ideas of how I might do it fairly easily.

Here is a sample application with a single Dial UserControl scaled to a couple of different sizes. At the moment, the only way to change the value of the dial is to click on one of the numbers or lights (one goes to eleven):

You can grab the source here, although at this stage it is still in the “experimental” phase (and I think I left a couple of unused test control templates in there) so play with it at your own risk. There is a bit of left over unused code from the process of trying different things out. I also don’t do much in the way of error or bounds checking so it’s probably quite easy to set the Value and MaxValue to values that cause Silverlight and/or Blend to die horribly. I am going to turn it into a fully skinnable custom control with all the right kinds of checks, but thought it worth putting a post up now and going over some of the challenges I faced with this particular control. It has been nearly a month after all.

Working with the PathListBox

I used the PathListBox control for the dial numbers and lights. If you haven’t used the Path ListBox before then here is a good series on it. The finished UserControl has a MaxValue property that lets you specify how many numbers should appear on the dial, but at the start I just had a the shapes for the dial and used a PathListBox with some fixed items in it.

orientations The first challenge was to get the PathListBoxItem control template to show a mix of orientations. There are two kinds of orientations you can have with a PathListBox: None and OrientToPath. The effects of each of these settings can be seen in the image to the right. If I used Orientation = None the numbers end up being rotated. If I use OrientToPath the light ends up below the number.

What I needed was a way to have the numbers use Orientation = None, and the lights to use Orientation = OrientToPath.

The solution was to change the control template setting that Blend creates by default. The outermost grid in the control template has the following XAML when the template is created:

   1: <Grid Background="{TemplateBinding Background}" RenderTransformOrigin="0.5,0.5">
   2:     <Grid.RenderTransform>
   3:         <TransformGroup>
   4:             <ScaleTransform/>
   5:             <SkewTransform/>
   6:             <RotateTransform Angle="{Binding OrientationAngle, RelativeSource={RelativeSource TemplatedParent}}"/>
   7:             <TranslateTransform/>
   8:         </TransformGroup>
   9:     </Grid.RenderTransform>
  10:  
  11:     ....
  12:  
  13: </grid>

I changed this to use two child grids and moved the binding to OrientationAngle into one of the child grids:



   1: <Grid Background="{TemplateBinding Background}" RenderTransformOrigin="0.5,0.5">
   2:     <grid>
   3:         <Grid.RenderTransform>
   4:             <TransformGroup>
   5:                 <ScaleTransform/>
   6:                 <SkewTransform/>
   7:                 <RotateTransform Angle="{Binding OrientationAngle, RelativeSource={RelativeSource TemplatedParent}}"/>
   8:                 <TranslateTransform/>
   9:             </TransformGroup>
  10:         </Grid.RenderTransform>
  11:  
  12:     .... items in this grid will be oriented to the path
  13:  
  14:     </grid>
  15:     <grid>
  16:  
  17:     .... items in this grid will not be oriented
  18:  
  19:     </grid>
  20: </grid>

Now with the PathListBox.Orientation=OrientToPath, only the first grid (containing the light) will rotate, the second grid (containing the number) will be unaffected. Even though the light itself is round and isn’t visually affected by the Orientation, its placement is affected.


Creating the Dial Numbers


I added a Value and a MaxValue dependency property to the UserControl. The MaxValue determines how high the dial goes up to, and the Value determines the current value of the dial. The UserControl has it’s own internal list of items that it recreates whenever MaxValue changes. The PathListBox binds to this list for it’s ItemsSource, and the data template for the PathListBoxItem binds to values on the classes inside the list. This approach reflects a half solution for something that I may or may not do when converting it to a custom control, but the relevant part here is that the list items are driven by the MaxValue property on the UserControl.



Making the Knob


The dial knob presented a couple of challenges. The first was to create the ridged look around its edge. I played with a couple of ideas, but the simplest solution was to use the Star RegularPolygon, which is available under the “Shapes” category in Blend. The RegularPolygon has an InnerRadius property which I set to 98% so that the points are only on the edge, and it has a PointCount property to determine, unsurprisingly, the number of points around the edge. The challenge was that I wanted the dial to work at different sizes, but the PointCount that works for a large dial doesn’t work for a small dial.


I used the Width of the dial to drive the PointCount with a new property on the UserControl and a ValueConverter. I was originally going to just use a ValueConverter and bind to ActualWidth, but there is a known bug where the notification for the change in ActualWidth is not being fired so this always returns 0 to the converter unless the form is resized. The suggested work around is to attach to the SizeChanged event and force a re-layout, but I decided that was too much of a hack, so I added my own DialWidth dependency property and set that to the actual width in the SizeChanged event.


I wrote the ValueConverter to be a generic division converter that returned an integer number; the ConverterParameter can be set to the desired denominator. In the end I could probably have done away with the value converter and DialWidth property and just added my own PointCount property that was recalculated on SizeChanged – and I may yet do that when I convert this into a custom control.


The second challenge was to have the knob rotate to the correct value (including the ridges, center graphic, and position light), but keep the LinearGradient brush fills oriented the same so the light still appears to come from the top:


dialpositionsThe knob is contained within two child grids (I used two to have the lights sit above the drop shadow of the dial). The topmost grid has it’s rotation translation template-bound to a new RotationAngle dependency property on the UserControl which is calculated whenever the Value property changes. The gradient brushes of the shapes that make up the knob have their rotation translation bound to the RotationAngle too, but they also use another simple ValueConverter I wrote to return the negative of the value passed to it. So the gradients are rotated anti-clockwise the same amount that the grid containing the dial is rotated clockwise.


The Next Step


As part of converting it to a custom control, I plan to add some better interaction support. Currently the only way to interact with it is by clicking on the lights or numbers to spin the dial to that position. I will add support for the mouse wheel, and (hopefully) support for clicking the knob and dragging it to the desired position (with snapping). I also plan to structure it so that it’s easy to skin and produce some nicely different styles of dials.


So grab the source if you want to have a look, but be warned – it’s a little messy in there, and the documentation is almost non-existent.

Monday, June 28, 2010

A Chrome and Glass Theme- Part 8

Introduction

In this post I’m going to show you how to make the radio buttons I blogged about previously. These buttons have gone through many iterations and I’m still not sure I’m quite happy with them just yet, but I’ll cover them anyway since they introduce a few interesting attached properties. Here is the sample I’ve been building up, now with the radio buttons:

You can grab the source here.

This post is the 8th in a series. If you are not familiar with styling controls in Blend, or working with Resource Dictionaries then I would recommend that you start at the beginning of the series.

The Radio Button

When I first attempted this button I ended up with horribly complicated arrangement of shapes to get it to scale properly. I had placeholders and spacers and grids within grids, with some of them having their height bound to their widths (which doesn’t work very well). And I had half circles on the ends with gradient blends merging seamlessly into the gradient blend on a rectangle in the middle. It was pretty awful.

The previous blog about Automatic Rectangle Radius X and Y should give you an idea of a simpler way to achieve the look. In the end I went with the attached properties since they work nicely both at run-time and design-time. If you grab the source from the link above, you may have to build it in Blend to get it to use the attached properties on the design surface.

The RadioButton inherits from the ToggleButton, as does the CheckBox. They all have a common theme of the button being checked, unchecked, or indeterminate; The radio button just has a different visual appearance, and adds the GroupName property. This style could be adapted for the ToggleButton too, but it wouldn’t really work as a style for a CheckBox.

The contents for this button are grouped in a grid with two columns; the left column contains the orb, the right column contains the ContentPresenter element.

Creating a custom style template for a radio button shouldn’t be difficult at all if you have been following along with the series, but I thought it might be worthwhile to look at some of the elements that make up this button.

Scalable Rounded Ends

A rectangle has the RadiusX and RadiusY properties that let you turn the sharp corners into rounded corners. For this button I want the rounded ends to stay rounded, at the right proportion, regardless of the size of the button. Unfortunately, there is no easy way to set those properties to be half of the height of the rectangle.

As mentioned in the post about Automatic Rectangle Radius X and Y I used an attached property to achieve the rounded ends since that gave me the best result when working with Blend. I use 3 rectangles in the button: one for the rim using the ChromeBorder resource brush, one for the main body using the ChromeFill resource brush, and one the same size that uses the ChromeDarkeningLinear resource brush to make the white text stand out better.

The ellipse is used on the right end of the button to add some dark shading to make the button look rounder, and the Orb grid uses the same principle as this post.

I created an attached property called IsRounded that, when true, uses another attached property called RoundCapsRatio (which defaults to 0.5 if unset) that it multiplies against height or width (whichever is smaller) and applies to the RadiusX and RadiusY properties of the rectangle it is attached to. You may have to build the solution once in Blend to have it applied properly, but then the corner radius automatically updates in blend as the rectangle it is applied to changes size.

Scalable Orb

The other attached properties I created are WidthRatio and HeightRatio (not used in this example). I use the WidthRatio attached property on the Orb grid to keep it perfectly square so the ellipses inside it stay round. I can’t have this happen without an attached property because there is no other way (that works in Blend) to set the width of an element to be relative to the height. I set the WidthRatio to the value “1” to achieve this. I could have just used a Boolean attached property, but having the ratio makes it more reusable.

If you use both the attached properties on a visual element, the handler will prefer the WidthRatio over the HeightRatio. Here is the method that applies the property values. It only applies the values if they have actually been set:

   1: private static void UpdateSizeRatio(FrameworkElement element)
   2: {
   3:     double ratio;
   4:     if (element.ReadLocalValue(WidthRatioProperty) != DependencyProperty.UnsetValue)
   5:     {
   6:         ratio = (double)element.GetValue(WidthRatioProperty);
   7:         double height = element.ActualHeight;
   8:         if (!double.IsNaN(height) && height > 0)
   9:         {
  10:             element.Width = height * ratio;
  11:         }
  12:     }
  13:     else if (element.ReadLocalValue(HeightRatioProperty) != DependencyProperty.UnsetValue)
  14:     {
  15:         ratio = (double)element.GetValue(HeightRatioProperty);
  16:         double width = element.ActualWidth;
  17:         if (!double.IsNaN(width) && width > 0)
  18:         {
  19:             element.Height = width * ratio;
  20:         }
  21:     }
  22: }

Conclusion


As far as radio buttons go, these are fairly limited in their application since they are so big. They are too big to use in the same way as you would normally use a radio button, but they do make quite good navigation buttons instead of using tabs.


The use of Attached Properties seems the best solution for adding behavior to a visual element that works at design time in Blend, but it’s not ideal – I would really rather have a way to do this using Blend that didn’t need me to swap into the XAML.


References:


The icons are from the Tango Desktop Project.

Monday, June 14, 2010

Automatic Rectangle Radius X and Y

Introduction

I created a design for a radio button that looks like this:

I'm going to write a future post on how to do this button, but I want to refine it a bit first. In this post I’m going to describe the approach I used for the rounded end-caps in the hopes that someone will help me perfect it.

I have the following requirements for this radio button style:
  • It must be re-sizable
  • The rounded ends must stay perfect half circles in proportion to the height of the button
  • It must behave correctly in Blend
  • Optional Extra: It would be nice to not have to manually edit XAML in Blend

I have the first three ticked off but the best approach so far still requires editing the XAML by hand. Here is a simple graphic to illustrate the end result when applied to a Rectangle element. The RadiusX and RadiusY are set to half the height of the rectangle:

Having the RadiusX and RadiusY calculated automatically is crucial to this design since I want it to be fully scalable (within reason).

I’ve been through several different approaches trying to get the full 4 marks, but 3 is the best I can manage. If you can spot any improvements or alternative approaches I would welcome suggestions.

Attached Property: 3 out of 4

The winning approach so far is to use an Attached Property that calculates the radii when the rectangle changes size. I made the attached property a little more generic by adding another attached property for specifying a ratio; 0.5 (the default) would give the result above. I also made it work off the lesser of the height and width values to cater for rectangles that are longer than they are wide. Here is the code for the attached property:

   1: using System;
   2: using System.Windows;
   3: using System.Windows.Shapes;
   4:  
   5: /// <summary>
   6: /// Contains attached properties for specifiying rounded ends on a Rectangle
   7: /// </summary>
   8: public static class RoundCaps
   9: {
  10:     /// <summary>
  11:     /// Attached Dependency Property for the IsRounded property
  12:     /// </summary>
  13:     public static readonly DependencyProperty IsRoundedProperty = DependencyProperty.RegisterAttached(
  14:         "IsRounded",
  15:         typeof(bool),
  16:         typeof(RoundCaps),
  17:         new PropertyMetadata(false, IsRoundedChanged));
  18:  
  19:     /// <summary>
  20:     /// Attached Dependency Property for the RoundCapsRatio property
  21:     /// </summary>
  22:     public static readonly DependencyProperty RoundCapsRatioProperty = DependencyProperty.RegisterAttached(
  23:         "RoundCapsRatio",
  24:         typeof(double),
  25:         typeof(RoundCaps),
  26:         new PropertyMetadata(0.5, RoundCapsRatioChanged));
  27:  
  28:     /// <summary>
  29:     /// Gets a value indicating if the Rectangle is rounded.
  30:     /// </summary>
  31:     /// <param name="d">The rectangle.</param>
  32:     /// <returns>true if the rectangle is rounded, else false</returns>
  33:     public static bool GetIsRounded(DependencyObject rectangle)
  34:     {
  35:         return (bool)rectangle.GetValue(IsRoundedProperty);
  36:     }
  37:  
  38:     /// <summary>
  39:     /// Sets a value indicating if the Rectangle is rounded.
  40:     /// </summary>
  41:     /// <param name="rectangle">The rectangle.</param>
  42:     /// <param name="value">true if the rectangle is rounded, else false.</param>
  43:     public static void SetIsRounded(DependencyObject rectangle, object value)
  44:     {
  45:         rectangle.SetValue(IsRoundedProperty, value);
  46:     }
  47:  
  48:     /// <summary>
  49:     /// Gets the roundedness ratio of the rectangle.
  50:     /// </summary>
  51:     /// <param name="rectangle">The ratio, relative to the lesser value of the rectangle's height and width.</param>
  52:     /// <returns>The ratio currently applied when calculating the roundedness</returns>
  53:     public static double GetRoundCapsRatio(DependencyObject rectangle)
  54:     {
  55:         return (double)rectangle.GetValue(RoundCapsRatioProperty);
  56:     }
  57:  
  58:     /// <summary>
  59:     /// Sets the roundedness ratio of the rectangle.
  60:     /// </summary>
  61:     /// <param name="rectangle">The ratio, relative to the lesser value of the rectangle's height and width.</param>
  62:     /// <returns>The ratio to be applied when calculating the roundedness</returns>
  63:     public static void SetRoundCapsRatio(DependencyObject d, object value)
  64:     {
  65:         d.SetValue(RoundCapsRatioProperty, value);
  66:     }
  67:  
  68:     private static void IsRoundedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  69:     {
  70:         Rectangle rect = d as Rectangle;
  71:         if (null == d)
  72:             return;
  73:  
  74:         bool isRounded = (bool)e.NewValue == true;
  75:         if (isRounded)
  76:         {
  77:             rect.SizeChanged += RectSizeChanged;
  78:             UpdateRectangle(rect);
  79:         }
  80:         else
  81:         {
  82:             rect.SizeChanged -= RectSizeChanged;
  83:         }
  84:     }
  85:  
  86:     private static void RoundCapsRatioChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  87:     {
  88:         Rectangle rect = d as Rectangle;
  89:         if (null == rect)
  90:         {
  91:             return;
  92:         }
  93:  
  94:         UpdateRectangle(rect);
  95:     }
  96:  
  97:     private static void RectSizeChanged(object sender, SizeChangedEventArgs e)
  98:     {
  99:         Rectangle rect = sender as Rectangle;
 100:         if (null == rect)
 101:         {
 102:             return;
 103:         }
 104:  
 105:         UpdateRectangle(rect);
 106:     }
 107:  
 108:     private static void UpdateRectangle(Rectangle rect)
 109:     {
 110:         double ratio = (double)rect.GetValue(RoundCapsRatioProperty);
 111:  
 112:         rect.RadiusX = Math.Min(rect.ActualHeight, rect.ActualWidth) * ratio;
 113:         rect.RadiusY = rect.RadiusX;
 114:     }
 115: }

This is how the XAML looks (without the RoundedRatio property which defaults to 0.5 anyway):


<Rectangle x:Name="rectangle" local:RoundCaps.IsRounded="True" Fill="#FF2D2D2D"/>


This approach works nicely at both run time and design time (1 point each) and is fully scalable (1 point). Given the awkwardness of the other approaches I’ve tried, this is the winning solution so far.


Here are the two other approaches I’ve tried that didn’t fare so well.


Value Converter: 1 Out Of 4


One approach was to bind the RadiusX and RadiusY properties of the rectangle to itself and use an IValueConverter to calculate the values based off the height and width of the rectangle. In case you’re interested, here is the code for the value converter:



   1: public class RatioConverter : IValueConverter
   2: {
   3:  
   4:     public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
   5:     {
   6:         FrameworkElement element = (FrameworkElement)value;
   7:         double height = element.ActualHeight;
   8:         double number = height;
   9:         double ratio = double.Parse((string)parameter);
  10:         return number * ratio;
  11:     }
  12:  
  13:     public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  14:     {
  15:         throw new NotSupportedException();
  16:     }
  17: }

The converter doesn’t work at all at run time because it evaluates during the Measure and Arrange cycle for the visual tree, which means that ActualHeight and ActualWidth haven’t actually been calculated and are still 0. I will give it half a point because if I explicitly set the height rather than use “Auto” it does work; but having to explicitly set the height is not good if I’m using this approach inside a resizable Button’s control template.


I will also give it 0.5 for working in Blend – almost. Again, if I explicitly set the height it works in Blend. Blend does somehow manage to apply it correctly for Auto height but only when I build the project (no extra points).


I tried a couple of variations around this, such as binding against the width and height of a sibling element (or cousin) but it started getting a little bit ridiculous and the code-smell was pretty terrible.


Attached Behavior: The Wrong 3 Out Of 4


Another thing I tried was writing an attached behavior for a Rectangle that would automatically set the corner radius values based on the dimensions of the rectangle. That works fine for a running application, but behaviors don't execute in Blend, so the design-time experience is ruined since the corners remain square.


This is the code for the behavior:



   1: public class RoundCapsBehavior : Behavior<Rectangle>
   2: {
   3:     public static readonly DependencyProperty RoundedRatioProperty = DependencyProperty.Register(
   4:         "RoundedRatio", typeof(double), typeof(RoundCapsBehavior), new PropertyMetadata(0.5, RoundedRatioChanged));
   5:  
   6:     public double RoundedRatio
   7:     {
   8:         get { return (double)this.GetValue(RoundedRatioProperty); }
   9:         set { this.SetValue(RoundedRatioProperty, value); }
  10:     }
  11:  
  12:     protected override void OnAttached()
  13:     {
  14:         base.OnAttached();
  15:         this.AssociatedObject.SizeChanged += AssociatedObjectSizeChanged;
  16:     }
  17:  
  18:     protected override void OnDetaching()
  19:     {
  20:         base.OnDetaching();
  21:         this.AssociatedObject.SizeChanged -= AssociatedObjectSizeChanged;
  22:     }
  23:  
  24:     void AssociatedObjectSizeChanged(object sender, SizeChangedEventArgs e)
  25:     {
  26:         this.UpdateRadiusXY();
  27:     }
  28:  
  29:     private void UpdateRadiusXY()
  30:     {
  31:         this.AssociatedObject.RadiusX =
  32:             Math.Min(this.AssociatedObject.ActualHeight, this.AssociatedObject.ActualWidth)
  33:             * this.RoundedRatio;
  34:         this.AssociatedObject.RadiusY = this.AssociatedObject.RadiusX;
  35:     }
  36:  
  37:     private static void RoundedRatioChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  38:     {
  39:         RoundCapsBehavior behavior = d as RoundCapsBehavior;
  40:         if (null == behavior)
  41:             return;
  42:         behavior.UpdateRadiusXY();
  43:     }
  44: }

It’s pretty simple – it just attaches a handler to the SizeChanged event and recalculates the corners when the event fires. It uses a property on the behavior that you can change the ratio with; the default 0.5 so the radius values will be half the height or width (whichever is larger).


It gets 1 point for working perfectly at run time, and 1 point for being scalable. It also gets the bonus point for not requiring any direct editing of the XAML, which gives it 3 out of 4, but one of those 3 was optional and it missed the third required point.


This one is probably the most frustrating because it feels like it should work. Behaviors, after all, were created as a convenience for the design surface – attached properties can do everything a behavior can – but the OnAttached and OnDetached methods are not called when the behaviors are used inside Blend.


Summary


So the Attached Dependency Property is currently winning the race, and unless someone comes up with a better solution, it will be the method-of-choice for the radio button style at the top. I just wish that Blend exposed a visual mechanism for adding custom attached properties to elements. Attached Behaviors were meant to give us that, but the OnAttached and OnDetached don’t get called so the behavior is not applied in the design surface.