Category Archives: WPF

Preventing a bound TextBox from resetting the caret position

Someone posed a question on our internal mailing list today at work that reminded me of a problem I’d tackled previously whilst working as a developer of fortune.

Here’s the challenge. A TextBox is bound to a data value that is being constantly updated. In my scenario the TextBox was bound to a data feed coming from a serial port connected weigh bridge. Even though the value is being automatically updated the operator has the ability to override the value with their own – at which point it would normally cease being updated by the data service.

Sounds fairly straight-forward. The main problem is that every time the TextBox value is updated via data-binding the selection text and position of the caret is reset. This is particularly annoying if the operator positions the caret about to make their change and a fraction of a second before they press a key the caret moves to the left edge.

I can’t remember exactly how we solved this problem in my earlier engagement (Raaj if you’re listening you could jog my memory) but here was my quick re-attempt.

First I’ll set the scene with a mock environment.

public partial class MainWindow : Window
{
private ViewModel viewModel;
private DispatcherTimer timer;
public MainWindow()
{
InitializeComponent();
this.viewModel = new ViewModel();
this.DataContext = viewModel;
this.timer = new DispatcherTimer(
new TimeSpan(0, 0, 1),
DispatcherPriority.Background,
UpdateValue,
this.Dispatcher); } private void UpdateValue(object sender, EventArgs e) { this.viewModel.Value += 0.01; } }

This sets up a simple form whose DataContext refers to a ViewModel with a Value property. The Value property is updated every second by a thread safe timer.

<Window x:Class="TextBoxOverlay.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel Width="150">
<TextBox
Text="{Binding Value,StringFormat=0.00,UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</Grid>
</Window>

The XAML simple binds a TextBox to the Value property. Running this sample and the problem can be immediately realised. Attempting to edit the value in the TextBox using the keyboard is extremely frustrating. The caret won’t go where you want it to.

So – the next step is to create a TextBlock that overlays the TextBox and instead bind this to the Value property. We set the IsHitTestVisible property on this TextBlock to False so that the user can still interact with the TextBox underneath. Then – and this is where things get a little sneaky – we make the TextBox’s text transparent. This allows us the strange freedom to interact with the TextBox’s content by selecting it and moving the caret – and because we can see the same text in the overlaid TextBlock things appear as normal.

<Window x:Class="TextBoxOverlay.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel Width="150">
<Grid>
<TextBox
Text="{Binding ModifiedValue,StringFormat=0.00,
UpdateSourceTrigger=PropertyChanged}"
PreviewTextInput="TextBoxPreviewTextInput" Foreground="Transparent"/> <TextBlock IsHitTestVisible="False" Margin="5,0" Text="{Binding Value,StringFormat=0.00}"/> </Grid> </StackPanel> </Grid> </Window>

You can see from the XAML that the TextBox is bound to a new field on our ViewModel called ModifiedValue. We also hook up to the PreviewTextInput event. We could have used an attached behaviour here rather than resorting to code-behind – but I wanted to keep things simple. So the code behind on the form has:

        private void TextBoxPreviewTextInput(object sender, TextCompositionEventArgs e)
{
var textBox = sender as TextBox;
var selectionStart = textBox.SelectionStart;
var selectionLength = textBox.SelectionLength;
var caretIndex = textBox.CaretIndex;
this.viewModel.ModifiedValue = this.viewModel.Value;
textBox.CaretIndex = caretIndex; 
textBox.SelectionStart = selectionStart; textBox.SelectionLength = selectionLength; }

Here we save and restore the TextBox’s SelectionStart, SelectionLength and CaretIndex whilst updating the ModifiedValue that is about to be changed to equal the Value that the user can actually see (remember the ModifiedValue is transparent).

The very last trick is within the ModifiedValue’s setter where we update the Value property. This ensures that whatever changes the operator makes to the TextBox are visible in the overlaid TextBlock. Of course the whole point of doing all of this is that the caret position and selection remains completely unchanged whilst the value appears to update.

        public double? ModifiedValue
{
get
{
return this.modifiedValue;
}
set
{
if (this.modifiedValue != value)
{
this.modifiedValue = value;
NotifyPropertyChanged("ModifiedValue");
if (this.ModifiedValue.HasValue)
Value = ModifiedValue.Value;
}
}
}

Source code here.

So aside from the tacky code-behind to keep the code here to a minimum, I’m wondering if there isn’t a neater solution?

UPDATE: Using an attached behaviour

It was pointed out to me by a colleague that there is a simpler, more versatile solution. Simple encapsulate the text change with selection restore within an attached property. Then we can use multiple bindings to achieve the effect.

        public static string GetNonIntrusiveText(DependencyObject obj)
{
return (string)obj.GetValue(NonIntrusiveTextProperty);
}
public static void SetNonIntrusiveText(DependencyObject obj, string value)
{
obj.SetValue(NonIntrusiveTextProperty, value);
}
public static readonly DependencyProperty NonIntrusiveTextProperty =
DependencyProperty.RegisterAttached(
"NonIntrusiveText",
typeof(string),
typeof(TextBoxExtensions), new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
NonIntrusiveTextChanged)); public static void NonIntrusiveTextChanged(
object sender,
DependencyPropertyChangedEventArgs e) { var textBox = sender as TextBox; if (textBox == null) return; var caretIndex = textBox.CaretIndex; var selectionStart = textBox.SelectionStart; var selectionLength = textBox.SelectionLength; textBox.Text = (string) e.NewValue; textBox.CaretIndex = caretIndex; textBox.SelectionStart = selectionStart; textBox.SelectionLength = selectionLength; }

Now the XAML no longer requires the tricky TextBlock overlay, we simple have a TextBox with two bindings.

<TextBox
Text="{Binding Value,StringFormat=0.00,
UpdateSourceTrigger=PropertyChanged,
Mode=OneWayToSource}"
local:TextBoxExtensions.NonIntrusiveText="{Binding Value,StringFormat=0.00,
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}"
/>

Conditional Formatting of a TextBox

FormatAndPaste

I recently came across a scenario where I needed to bind a TextBox to a domain property but also have the value formatted for display. To make things more interesting the format was to be dynamic and the value needed to be editable.

The initial investigation led me to consider a ValueConverter. Ideally the TextBox.Text property could be bound and the Converter could be used to format to/from the required on-screen value. For a dynamic format it would be nice to bind the ConverterParameter to a property that exposed the format. Of course that doesn’t work because ConverterParameter doesn’t support data binding. I found a hack that gets around this – but it isn’t pretty. There are also some examples of using a MultiValueConverter and passing both the value to format and the format string itself as separate individual bindings. This approach has some difficulties too when converting both ways and its just feels like an abuse of the ValueConverter.

This lead me to think about the problem a little more… maybe a different approach is required? Thinking back to the WinForms days and I realised that I had solved this problem before, several times in fact. My approach to this problem for WinForms had been:

  • Subclass TextBox and add a Value property of type object that allows data binding to data types other than just string. Common types that could be used with a TextBox include int, decimal, double, bool, DateTime and enums.
  • The inherited TextBox also has a Format property. On GotFocus the Value property is formatted and used to populate the Text property. On LostFocus the reverse happens, the Text property is parsed back into the Value property. Of course this requires the data type to be known so a DataType property is required as well.

The benefits that this has:

  • TextBox works for data types other than string.
  • The value is formatted as required for display but upon data entry (GotFocus) the formatting is removed. This actually makes it easier to enter/modify the value because you don’t need to parse currency symbols, percentage signs and the like.

So the approach sounds good and its worked well for me in WinForms but its… well… not very WPF’ish. Upon starting any major development the first requirement in WinForms was to subclass all the controls – because they were just so lacking if functionality and even more importantly didn’t expose a common set of interfaces. However, I very rarely subclass controls in WPF – instead we can use attached behaviors to extend the control.

The attached behaviors required are:

  • object TypedValue
  • Type DataType 
  • string StringFormat

In XAML instead of binding to the TextBox.Text property we bind to the TypedValue attached property. The StringFormat can also be bound. The DataType can be inferred by the TypedValue – but for nullable types its best to be set explicitly. With a sample class as follows:

public class ModelItem
{
public object Value { get; set; }
public string Format { get; set; }
}

The XAML is then:

<DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Any Type TextBox">
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="local:TextBoxExtensions.StringFormat" Value="{Binding Format}"/>
<Setter Property="local:TextBoxExtensions.TypedValue" Value="{Binding Value}"/>
</Style>
</DataGridTextColumn.ElementStyle>
<DataGridTextColumn.EditingElementStyle>
<Style TargetType="{x:Type TextBox}">
<Setter Property="local:TextBoxExtensions.StringFormat" Value="{Binding Format}"/>
<Setter Property="local:TextBoxExtensions.TypedValue" Value="{Binding Value}"/>
</Style>
</DataGridTextColumn.EditingElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="Format" Binding="{Binding Format}" IsReadOnly="True"/>
<DataGridTextColumn Header="Value" Binding="{Binding Value}" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>

Which generates a DataGrid bound to a collection of ModelItems. Each ModelItem allows a different data type and format to be applied – great for a “user-defined fields” scenario.

Populating the ModelItems collection as follows in our main ViewModel:

    public class Model
{
public Model()
{
Primary = new ModelItem() { Format = "{0:#,##0.0}", Value = 12345678.765 };
Items = new ObservableCollection<ModelItem>();
Items.Add(new ModelItem() { Format = "{0:C2}", Value = 123.42 });
Items.Add(new ModelItem() { Format = "{02}", Value= 0.125 });
Items.Add(new ModelItem() { Format = "{0}", Value = "Fred" });
Items.Add(new ModelItem() { Format = null, Value = true });
Items.Add(new ModelItem() { Format = "Uncle {0}", Value = "George" });
Items.Add(new ModelItem() { Format = null, Value = Colors.Black });
Items.Add(new ModelItem() { Format = null, Value = System.DayOfWeek.Monday });
Items.Add(new ModelItem() { Format = "{0:0;minus 0;zip}", Value = -123.4 });
}
public ModelItem Primary { get; set; }
public ObservableCollection<ModelItem> Items { get; private set; }
}

Generates the following grid, which allows for editing of the strongly typed values.

FormatAndPaste

Porting WPF Word Puzzle to Windows Phone Silverlight – Part 1

To date I’ve avoided doing any serious development in Silverlight. Every time I’ve tried to tackle it I get so frustrated with all the missing pieces. Besides which I’ve never had a good reason to do any Silverlight work – I’ve never been a fan of applications that run in a browser.

With the release of the Windows Phone Series development tools however, I now have a good reason. So I figured I’d pick a relative simple, small scale WPF application that actually makes some sense to run on a mobile device. Rather than starting it from scratch I just wanted to port it from WPF – so I chose the Word Puzzle program that I wrote a couple of years back. I figured it was a good choice because it met the criteria above, plus I’d already stripped it back a little to make sure it could run as an XBAP application.

Inspired by Rob’s posts on porting NProf to Silverlight I thought it may be of some interest to list off the issues that I come up against as I go through the process of porting. This first list represents me starting a new Windows Phone project and copying over classes and XAML files to get something to compile and look recognizable. The following represents about 2 hours work:

WordPuzzle_Stage1

However, along the way I came across this list of issues:

  • No Viewbox
  • No MouseDown or MouseUp
  • No UniformGrid
  • No Image.StretchDirection
  • x:Type is not supported
  • No Style.Triggers
  • No DockPanel
  • No RoutedCommand
  • No KeyGesture
  • No DataType on DataTemplate?
  • No ValueConversion
  • No DefiningGeometry on Shape
  • No BooleanToVisibilityConverter
  • No DynamicResource
  • No WrapPanel

I haven’t verified the above list yet – save that they gave me compilation errors. I easily found a replacement UniformGrid, but there are a few items on the list that may pose more of a problem.

The next step is to get some level of interaction working.

Continuous Tile Panel

A couple of weeks ago someone suggested to me an idea for a particular type of layout panel. The idea was to be able to display a fixed set of tiles which are “wrapped” horizontally. So as you scroll horizontally the tiles will move off one edge of the panel and eventually re-appear on the other side. Think of how we commonly see the earth projected onto a rectangle – scrolling left or right to bring the particular geography into the centre of the screen.

I jumped into this without giving too much thought about how I would want to interact with the panel from an API perspective. For that reason its probably I’ve taken altogether the wrong approach but I thought I would post it up here anyway, ‘cause its unlikely I’ll take it any further than this proof of concept.

Usage

Define some XAML like so:

<panels:TilePanel x:Name="tilePanel" Height="170"
Rows="3" Background="LightYellow" ClipToBounds="True"
XOffset="{Binding ElementName=xOffsetSlider,Path=Value,Mode=OneWay}"
YOffset="{Binding ElementName=yOffsetSlider,Path=Value,Mode=OneWay}"
Scale="{Binding ElementName=scaleSlider,Path=Value,Mode=OneWay}">
<panels:TilePanel.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="32pt"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Padding" Value="3"/>
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="Background" Value="LightBlue"/>
</Style>
</panels:TilePanel.Resources>
<TextBlock>A</TextBlock>
<TextBlock>B</TextBlock>
<TextBlock>C</TextBlock>
<TextBlock>D</TextBlock>
<TextBlock>E</TextBlock>
<TextBlock>F</TextBlock>
<TextBlock>G</TextBlock>
<TextBlock>H</TextBlock>
<TextBlock>I</TextBlock>
<TextBlock>J</TextBlock>
<TextBlock>K</TextBlock>
<TextBlock>L</TextBlock>
<TextBlock>M</TextBlock>
<TextBlock>N</TextBlock>
<TextBlock>O</TextBlock>
<TextBlock>P</TextBlock>
<TextBlock>Q</TextBlock>
<TextBlock>R</TextBlock>
<TextBlock>S</TextBlock>
<TextBlock>T</TextBlock>
<TextBlock>U</TextBlock>
<TextBlock>V</TextBlock>
<TextBlock>W</TextBlock>
<TextBlock>X</TextBlock>
<TextBlock>Y</TextBlock>
<TextBlock>Z</TextBlock>
<TextBlock>1</TextBlock>
<TextBlock>2</TextBlock>
<TextBlock>3</TextBlock>
<TextBlock>4</TextBlock>
<TextBlock>5</TextBlock>
<TextBlock>6</TextBlock>
<TextBlock>7</TextBlock>
<TextBlock>8</TextBlock>
<TextBlock>9</TextBlock>
<TextBlock>0</TextBlock>
</panels:TilePanel>

This generates the following layout where each of the items is laid out in three rows.

Continuous Tile Panel - Letters at Start

Items that are beyond the right edge are actually wrapped back to the left hand side (by subtracting the total width of all columns). So that when we supply a horizontal offset we get the desired effect.

Continuous Tile Panel - Letters Shifted

Note that in the example each column width is automatically calculated based on the widest element. Likewise row heights are calculated based on the tallest elements. Although this allows for different widths/heights per column/row its works well when all items are the same width and height.

One example usage of this control would be as an ItemsPanel for an ItemsControl that displays tiled images that form a single scene. It so happens that ICE generates these types of tiles (at various zoom levels) for publishing its panoramas.

<ItemsControl ItemsSource="{Binding TileSet}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderThickness="1" BorderBrush="LightGray">
<Grid>
<Image Source="{Binding ImagePath}" Stretch="Uniform" Height="Auto"/>
                    <TextBlock Margin="4" TextAlignment="Center" FontSize="6pt" Foreground="White" Opacity="0.5" 
VerticalAlignment="Center" HorizontalAlignment
="Center"> <TextBlock.Text> <MultiBinding StringFormat="{}{0},{1}" > <Binding Path="ColumnIndex"/> <Binding Path="RowIndex"/> </MultiBinding> </TextBlock.Text> </TextBlock> </Grid> </Border> </DataTemplate> </ItemsControl.ItemTemplate> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <panels:TilePanel Margin="10" Background="LightYellow" ClipToBounds="True" Rows="{Binding TileSet.RowCount}" XOffset="{Binding ElementName=xOffsetSlider,Path=Value,Mode=OneWay}" YOffset="{Binding ElementName=yOffsetSlider,Path=Value,Mode=OneWay}" Scale="{Binding ElementName=scaleSlider,Path=Value,Mode=OneWay}"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl>

Continuous Tile Panel - ICE

 

Code

To load the ICE images into a collection of “Tiles” I wrote the following.

    public static class TileLoader
{
public static TileSet LoadFromFolder( string path, int zoomLevel )
{
var result = new TileSet(path, zoomLevel);
if ( !Directory.Exists( path ) )
throw new ArgumentException( "The specified tile folder does not exist.", "path" );
if ( zoomLevel < 0 )
throw new ArgumentException( "The zoom level must be >= 0.", "zoomLevel" );
DirectoryInfo tileLevelFolder;
try
{
var rootFolder = new DirectoryInfo( path );
var tileFolder = new DirectoryInfo( Path.Combine( path, "tiles" ) );
tileLevelFolder = new DirectoryInfo( Path.Combine( tileFolder.FullName, "l_" + zoomLevel ) );
}
catch ( System.IO.IOException ex )
{
throw new ArgumentException( 
string.Format( "The tile set specified by the path {0} is corrupt.", path ), "path", ex ); } var column = 0;
// Order the directories by numerical ascending, e.g. c_3 before c_21 foreach ( var columnFolder in tileLevelFolder.GetDirectories( "c_*" )
.OrderBy( d => int.Parse( d.Name.Substring( 2 ) ) ) ) { var row = 0; foreach ( var imageFile in columnFolder.GetFiles( "tile_*.wdp" )
.OrderBy( i => int.Parse( i.Name.Substring( 5, i.Name.IndexOf( '.' ) - 5 ) ) ) ) { var tile = new Tile() { ColumnIndex = column, RowIndex = row, ImagePath = imageFile.FullName }; result.Add( tile ); row++; if ( result.RowCount < row ) result.RowCount = row; } column++; } return result; } } [DebuggerDisplay("Column: {ColumnIndex}, Row: {RowIndex}, {ImagePath}")] public class
Tile { public string ImagePath { get; set; } public int ColumnIndex { get; set; } public int RowIndex { get; set; } } public class TileSet : List<Tile> { public TileSet( string path, int zoomLevel ) { Path = path; ZoomLevel = zoomLevel; } public string Path { get; protected set; } public int ZoomLevel { get; protected set; } public int RowCount { get; set; } }

The panel’s arrange override code is as follows:

protected override Size ArrangeOverride(Size finalSize)
{
int row = 0;
int column = 0;
double fullWidth = _columnWidths.Sum();
double fullHeight = _rowHeights.Sum();
int maxColumns = (int) Math.Ceiling( (double) finalSize.Width / (double) _maxChildWidth);
double x = -( XOffset * _maxChildWidth );
double y = - YOffset;
foreach (UIElement child in Children)
{
x = x % fullWidth;
if (x >= Math.Min(_maxChildWidth * maxColumns, fullWidth - _maxChildWidth + 1))
x -= fullWidth;
else if (x < - _maxChildWidth)
x += fullWidth;
if ( x > -_maxChildWidth && x <= _maxChildWidth * maxColumns &&
y > -_maxChildHeight && y <= fullHeight )
{
child.Arrange( 
new Rect( new Point( x * Scale, y * Scale ),
new Size( child.DesiredSize.Width * Scale, child.DesiredSize.Height * Scale ) ) ); child.Visibility = Visibility.Visible; }
else { child.Visibility = Visibility.Hidden; child.Arrange( new Rect( new Point( 0, 0 ), new Size( child.DesiredSize.Width * Scale, child.DesiredSize.Height * Scale ) ) ); } y += _rowHeights[ row ]; row = ( row + 1 ) % Rows; if ( row == 0 ) { x += _columnWidths[ column ]; //column * _childWidth ; y = -YOffset ; column++; } } //RenderTransform = new ScaleTransform(Scale, Scale, 0.0, YOffset); return finalSize; }

This is far from complete – sample project can be found here. [It’s a VS2010 project but should be easy enough to re-assemble for VS2008.]

 

Desk Calendar UserControl – Source Code

I had to remove the source code from my previous post because it was doing weird things to my RSS feed. For those that may be interested though a sample project file can be found here. Note that its far from a finished control – the animation that happens when you click on it was just me messing around with Blend and the intention is for it to allow full navigation and selection.

XBAP demo (27kb) can be launched from here – note that XBAP version doesn’t have any drop shadows.

Desk Calendar UserControl

I had a lot of fun late this afternoon putting together a Desk Calendar UserControl. The idea was triggered by me using a screenshot of the Vista Calendar Gadget in my last post. Also I’m working on a project at the moment that could use a nice date/calendar display and my previous WinForms attempts never looked that great.


Believing that imitation is the greatest form of flattery – here is my new Desk Calendar UserControl in all its glory. Of course being WPF its fully zoomable – no nasty bitmaps here – click the image to see it at higher res.


DeskCalendar


For the moment the control is read-only, although I did start working on a “flip page” animation. The layout was done exclusively in Blend (v3 Preview) hence the mark-up has some redundant elements and over-precise co-ords. In general though I was really happy with how easy this was to put together using Blend – the product has certainly matured well.

[Edit: Source code and XBAP demo can be found on subsequent post here.]

Box Layout Panel

Here’s another quick WPF custom control derived from Panel. This one is a cross between a StackPanel and a WrapPanel. It supports two modes of layout – Inline and Block.


Inline elements are wrapped left to right much as the same as a WrapPanel with the default Orientation = Horizontal.


Block elements are stacked as per a StackPanel with the default Orientation = Vertical.


The benefit of combining (a simplified version) of this logic into one layout control is that an attached property can then be used on a per child basis to determine the layout (LayoutMode = Block or Inline). This is even more convenient when the property is set via an implicit style, e.g. all TextBlocks set to Inline by default, all Buttons as Block.


I also added some dependency properties for Padding (space between edge and top/left/right/bottom-most controls) and InternalPadding (the vertical and horizontal spacing between wrapped/stacked controls).

<panels:BoxPanel Height=”Auto” Background=”PaleGreen” InternalPadding=”4,2″ Padding=”50,8,8,8″>
<
TextBlock>Some inline text.</TextBlock
>
<
TextBlock>Followed by some more inline text.</TextBlock
>
<
TextBlock>Followed by yet some more inline text.</TextBlock
>
<
TextBlock panels:BoxPanel.LayoutMode=”Block”>A new paragraph.</TextBlock
>
<
Button panels:BoxPanel.LayoutMode=”Block”>A Button</Button
>
<
TextBlock>Below are three buttons in a line</TextBlock
>
<
Button panels:BoxPanel.LayoutMode=”Inline”>One</Button
>
<
Button panels:BoxPanel.LayoutMode=”Inline”>Two</Button
>
<
Button panels:BoxPanel.LayoutMode=”Inline”>Three</Button
>
<
panels:BoxPanel Background=”LightYellow” Padding=”10,2″ InternalPadding
=”4,0″>
<
TextBlock>The</TextBlock
>
<
Image Source=”Passport_Photo_with_blue_background_half_size.png” Width
=”24″/>
<
TextBlock>End.</TextBlock
>
</
panels:BoxPanel
>
</
panels:BoxPanel>

BoxPanel - Layout Measures


 


Sound a bit obscure? Trust me, I had a reason .

Circular Layout Panel v2

On the flight to Remix09 last week I had fun putting together a simple WPF Circular Layout Panel. Today I decided that a nice “extra” feature would be to have the child elements optionally rotated so that they are normalised with the centre of the layout panel.


So after adding a new attached dependency property, IsNormalised I updated my sample clock and fan menu. The following XAML…

<Window x:Class=”PanelTest.Window1″
xmlns
=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x
=”http://schemas.microsoft.com/winfx/2006/xaml”
Title=”Circular Panel Examples” Height=”405″ Width
=”383″
xmlns:local
=”clr-namespace: PanelTest”
xmlns:panels=”clr-namespace:Spencen.Panels;assembly=Spencen.Panels”
>
<
DockPanel
>
<
DockPanel.Resources
>
<!– Stuff to make buttons pretty –>
</DockPanel.Resources
>
<Ellipse Fill=”LightYellow” Stroke=”LightGray” StrokeThickness=”1″ StrokeDashArray
=”0,1,1,0″ />
<panels:CircularPanel StartAngle=”-90″ EndAngle=”630″
TextBlock.FontSize=”24pt”>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>XII</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>I</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>II</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>III</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>IV</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>V</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>VI</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>VII</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>VIII</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>IX</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>X</TextBlock
>
<
TextBlock panels:CircularPanel.IsNormalised=”True”>XI</TextBlock
>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>
<
Ellipse Width=”5″ Height=”5″ Fill=”Black”
panels:CircularPanel.RadiusScaleX=”0.9″ panels:CircularPanel.RadiusScaleY
=”0.9″/>




<
Line x:Name=”secondHand” Fill=”Black” X1=”0″ X2=”0″ Y1=”0″ Y2=”100″
Stroke=”Red” StrokeThickness=”1″ StrokeStartLineCap=”Triangle” StrokeEndLineCap
=”Round”
panels:CircularPanel.FixedAngle
=”-90″
panels:CircularPanel.RadiusScaleX
=”0.4″
panels:CircularPanel.RadiusScaleY
=”0.4″
panels:CircularPanel.IsNormalised
=”True”/>
<
Line x:Name=”minuteHand” Fill=”Black” X1=”2″ X2=”2″ Y1=”0″ Y2=”70″
Stroke=”Black” StrokeThickness=”5″ StrokeStartLineCap=”Triangle” StrokeEndLineCap
=”Round”
panels:CircularPanel.FixedAngle
=”-60″
panels:CircularPanel.RadiusScaleX
=”0.3″
panels:CircularPanel.RadiusScaleY
=”0.3″
panels:CircularPanel.IsNormalised
=”True”/>
<
Line x:Name=”hourHand” Fill=”Black” X1=”5″ X2=”5″ Y1=”0″ Y2=”50″
Stroke=”Black” StrokeThickness=”10″ StrokeStartLineCap=”Triangle” StrokeEndLineCap
=”Round”
panels:CircularPanel.FixedAngle
=”-180″
panels:CircularPanel.RadiusScaleX
=”0.2″
panels:CircularPanel.RadiusScaleY
=”0.2″
panels:CircularPanel.IsNormalised
=”True”/>
<
Ellipse Fill
=”Black”
panels:CircularPanel.FixedAngle
=”0″
panels:CircularPanel.RadiusScaleX
=”0″
panels:CircularPanel.RadiusScaleY=”0″ Width=”30″ Height
=”30″ />
<
panels:CircularPanel.Triggers
>
<
EventTrigger RoutedEvent
=”Loaded”>
<
BeginStoryboard
>
<
Storyboard
>
<
DoubleAnimation Duration=”0:1:0″ By=”360″ RepeatBehavior=”Forever”
Storyboard.TargetName=”secondHand”
Storyboard.TargetProperty
=”(panels:CircularPanel.FixedAngle)”/>
<
DoubleAnimation Duration=”1:0:0″ By=”360″ RepeatBehavior=”Forever”
Storyboard.TargetName=”minuteHand”
Storyboard.TargetProperty
=”(panels:CircularPanel.FixedAngle)”/>
<
DoubleAnimation Duration=”12:0:0″ By=”360″ RepeatBehavior=”Forever”
Storyboard.TargetName=”hourHand”
Storyboard.TargetProperty
=”(panels:CircularPanel.FixedAngle)”/>
</
Storyboard
>
</
BeginStoryboard
>
</
EventTrigger
>
</
panels:CircularPanel.Triggers
>
</
panels:CircularPanel
>
<
panels:CircularPanel Padding=”45″ StartAngle=”-90″ EndAngle=”-90″
>
<
Button panels:CircularPanel.IsNormalised=”true”>1</Button
>
<
Button panels:CircularPanel.IsNormalised=”true”>2</Button
>
<
Button panels:CircularPanel.IsNormalised=”true”>3</Button
>
<
Button panels:CircularPanel.IsNormalised=”true”>4</Button
>
<
Button Panel.ZIndex=”1″ Background=”Gray”>Menu</Button
>
<
Button panels:CircularPanel.IsNormalised=”true”>5</Button
>
<
Button panels:CircularPanel.IsNormalised=”true”>6</Button
>
<
Button panels:CircularPanel.IsNormalised=”true”>7</Button
>
<
Button panels:CircularPanel.IsNormalised=”true”>8</Button
>
<
panels:CircularPanel.Triggers
>
<
EventTrigger RoutedEvent
=”MouseEnter”>
<
BeginStoryboard
>
<
Storyboard
>
<
DoubleAnimation To=”-180″ Storyboard.TargetProperty
=”StartAngle”>
<
DoubleAnimation.EasingFunction
>
<
ElasticEase Springiness=”10″ Oscillations
=”2″/>
</
DoubleAnimation.EasingFunction
>
</
DoubleAnimation
>
<
DoubleAnimation To=”0″ Storyboard.TargetProperty
=”EndAngle”>
<
DoubleAnimation.EasingFunction
>
<
ElasticEase Springiness=”10″ Oscillations
=”2″/>
</
DoubleAnimation.EasingFunction
>
</
DoubleAnimation
>
</
Storyboard
>
</
BeginStoryboard
>
</
EventTrigger
>
<
EventTrigger RoutedEvent
=”MouseLeave”>
<
BeginStoryboard
>
<
Storyboard
>
<
DoubleAnimation To=”-90″ AccelerationRatio=”0.5″ DecelerationRatio=”0.5″
Storyboard.TargetProperty
=”StartAngle”/>
<
DoubleAnimation To=”-90″ AccelerationRatio=”0.5″ DecelerationRatio=”0.5″
Storyboard.TargetProperty
=”EndAngle”/>
</
Storyboard
>
</
BeginStoryboard
>
</
EventTrigger
>
</
panels:CircularPanel.Triggers
>
</
panels:CircularPanel
>
</DockPanel
>
</
Window>

Now produces…


 CircularPanel - Normalised


For reference this is so much easier than doing the same thing in WinForms. Custom layout panels really were quite a pain.


Updated source code for CircularPanel is here.