Click here to Skip to main content
Click here to Skip to main content
Go to top

WPF 3D FlipPanel

, 13 Mar 2012
Rate this:
Please Sign up or sign in to vote.
WPF Implementation of the spinning panel frequently used on the iPhone
FlipPanelCS

Introduction

This article demonstrates a VB.NET way of duplicating the spinning panel effect widely used on the iPhone to let a panel host content both on its front and its back. I find that that type of control has its uses both on the small screens of hand held devices as well as normal computer monitors as a way of conserving screen real estate.

An example of it in action is available here on YouTube.

Background

I've written this control for a friend which is why it's in VB.NET instead of C# that I am more familiar with and I like to apologise in advance for any oddities that might exist in the VB.NET code due to my inexperience with that language. For completeness, I've included a C# version as well.

There are other controls available, but I haven't found one that was suitable for what my friend asked for:

  • 1. It must be have like a normal WPF Panel, add it to any container and it should resize to fit its parent.
  • 2. It must work in the Visual Studio designer showing (at least) the front side during design.
  • 3. It must be simple and easy to understand so that it can be modified without too much hassle.
There will be a very limited amount of 3D math in this article, but people with no prior experience in 3D math should still be able to follow the article without problems.

Using the code

Download the appropriate solution (VB.NET or C#), unzip and open. Both solutions have a class library and a WPF test app showing of a very simple sample implementation. The C# has two sample apps, one simple and one slightly more involved.
It's all been written using VS2010 Express Editions.

The basics

Neither a bird, nor a Superman

The 3D models used to host the UIElements are two identical planes, or quads, arranged to occupy the same "volume" of space but one is rotated 180 degrees. Since back face culling in 3D graphics allows us to only render the front of a mesh it's perfectly acceptable for the two meshes to occupy the same space as long as one is facing in the opposite direction of the other.

A class called FlipPanel, which is discussed in details later in this article, is responsible for positioning and rotating these two planes as well as providing the materials for them. In order to set up the meshes though, I've written a helper class called Plane.

Plane has a method for adding another plane to an already existing mesh, such a method might seem overkill for this type of application but I've left it in there as it's really a subset of a bigger set of helpers I use when working with 3D in WPF. The method AddPlaneToMesh takes an existing MeshGeometry3D along with the properties of the plane and add those to the bigger mesh whilst making sure things like texture coordinates are added as well.
A wrapper method called Create is used to create a mesh containing just one plane, and yet another wrapper method called CreateXY allows for creating a mesh containing a plane defined in the XY plane of the 3D space.

Public Class Plane
  Public Shared Function AddPlaneToMesh(ByVal mesh As MeshGeometry3D, ByVal normal As Vector3D, ByVal upperLeft As Point3D, ByVal lowerLeft As Point3D, ByVal lowerRight As Point3D, ByVal upperRight As Point3D) As MeshGeometry3D
    Dim offset As Integer = 0

    mesh.Positions.Add(upperLeft)
    mesh.Positions.Add(lowerLeft)
    mesh.Positions.Add(upperRight)
    mesh.Positions.Add(lowerRight)

    mesh.Normals.Add(normal)
    mesh.Normals.Add(normal)
    mesh.Normals.Add(normal)
    mesh.Normals.Add(normal)

    mesh.TextureCoordinates.Add(New Point(0, 0))
    mesh.TextureCoordinates.Add(New Point(0, 1))
    mesh.TextureCoordinates.Add(New Point(1, 1))
    mesh.TextureCoordinates.Add(New Point(1, 0))

    mesh.TriangleIndices.Add(offset + 0)
    mesh.TriangleIndices.Add(offset + 1)
    mesh.TriangleIndices.Add(offset + 2)
    mesh.TriangleIndices.Add(offset + 0)
    mesh.TriangleIndices.Add(offset + 2)
    mesh.TriangleIndices.Add(offset + 3)

    Return mesh
  End Function

  Public Shared Function Create(ByVal normal As Vector3D, ByVal upperLeft As Point3D, ByVal lowerLeft As Point3D, ByVal lowerRight As Point3D, ByVal upperRight As Point3D) As MeshGeometry3D
    Return AddPlaneToMesh(New MeshGeometry3D(), normal, upperLeft, lowerLeft, upperRight, lowerRight)
  End Function

  Public Shared Function CreateXY(ByVal normal As Vector3D, ByVal width As Double, ByVal height As Double) As MeshGeometry3D
    Dim w As Double = width / 2.0
    Dim h As Double = height / 2.0

    Return Create(normal, New Point3D(-w, h, 0), New Point3D(-w, -h, 0), New Point3D(w, -h, 0), New Point3D(w, h, 0))
  End Function
End Class

The planes are created so that the object local point (0, 0, 0) is the center of the plane.

FlipPanel

338671/Example.png
FlipPanel showing one of many panels spinning from showing a graph to showing stock information.

In order to achieve a panel spinning in 3D I've used the 3D capabilities of WPF, it could have been implemented using only 2D transformations but I find it simpler to just use actual 3D since the logic then becomes a lot clearer and the solution is not as forced as any 2D version I could come up with would have been.

The members of the FlipPanel class are these;

Public Class FlipPanel
  Inherits Panel

  ...

  ' Definitions of the three axises
  Private Shared ReadOnly AxisX As Vector3D = New Vector3D(1, 0, 0)
  Private Shared ReadOnly AxisY As Vector3D = New Vector3D(0, 1, 0)
  Private Shared ReadOnly AxisZ As Vector3D = New Vector3D(0, 0, 1)

  ' Material and mesh, these are Shared
  Private Shared ReadOnly visualHostMaterial As Material = New DiffuseMaterial(Brushes.White)
  Private Shared ReadOnly mesh As MeshGeometry3D = Plane.CreateXY(AxisZ, 1, 1)

  ' The main model that holds both front and back side
  Private model As ModelVisual3D = New ModelVisual3D()
  
  ' Front and back Visuals, these hold the content in their .Visual property
  Private frontVisual3D As Viewport2DVisual3D
  Private backVisual3D As Viewport2DVisual3D

  ' The Front and Back content
  Private frontElement As UIElement
  Private backElement As UIElement

  ' 3D view port
  Private viewPort As Viewport3D
  
  ' The container holding the model 
  Private contentContainer As ModelVisual3D
  
  ' Rotation, translation and scale of the contentContainer
  Private rotation As AxisAngleRotation3D = New AxisAngleRotation3D(FlipPanel.AxisY, 0)
  Private translation As TranslateTransform3D = New TranslateTransform3D()
  Private scale As ScaleTransform3D = New ScaleTransform3D(1, 1, 1)
  
  ...
  
 End Class

The FlipPanel I've implemented for this article is simply a class extending Panel and exposing two properties of type UIElement called Front and Back. Using those properties any content can be added to the panel's front and back like this (where f is the XML namespace pointing to the FlipPanel implementation);

<f:FlipPanel>
  <f:FlipPanel.Front>
    <Border Background="Red" BorderBrush="Black" BorderThickness="2">
      <Button Content="Front" VerticalAlignment="Center" Click="Button_Click"/>
    </Border>
  </f:FlipPanel.Front>
  <f:FlipPanel.Back>
    <Border Background="Green" BorderBrush="Black" BorderThickness="2">
      <Button Content="Back" VerticalAlignment="Center" Click="Button_Click"/>
    </Border>
  </f:FlipPanel.Back>
</f:FlipPanel>  

The content inside the Front and Back properties can obviously consist of more (or less) XAML or a user control. Just about anything, really.

In order for the FlipPanel to be able to host this is need some predefined children to set up the 3D scene. Essentially the structure needed for this looks something like this:

<Grid>
  <Viewport3D>
    <Viewport3D.Camera>
      <PerspectiveCamera FieldOfView="90" Position="0,0,0.5" LookDirection="0,0,-1"/>
   </Viewport3D.Camera>
   <ModelVisual3D>
     <ModelVisual3D.Content>
       <AmbientLight Color="#808080"/>
     </ModelVisual3D.Content>
   </ModelVisual3D>
   <ModelVisual3D>
     <ModelVisual3D.Content>
       <DirectionalLight Color="White" Direction="0,0,-1"/>
     </ModelVisual3D.Content>
   </ModelVisual3D x:Name="container"/> 
 </Viewport3D>
</Grid>  

But since this is a panel implementation I've done it all in code instead in a method called InitializeComponent, the code equivalent of the above XAML looks something like this:

Sub InitializeComponent()
  viewPort = New Viewport3D()
  viewPort.Camera = New PerspectiveCamera With
  {
    .FieldOfView = 90,
    .Position = New Point3D(0, 0, 0.5),
    .LookDirection = New Point3D(0, 0, -1)
  }
  viewPort.Children.Add(New ModelVisual3D With {.Content = New AmbientLight(Colors.DarkGray)})
  viewPort.Children.Add(New ModelVisual3D With {.Content = New DirectionalLight(Colors.White, New Vector3D(0, 0, -1))})

  Dim transform As Transform3DGroup = New Transform3DGroup()
  transform.Children.Add(New RotateTransform3D(rotation))
  transform.Children.Add(translation)
  transform.Children.Add(scale)

  contentContainer = New ModelVisual3D With {.Transform = transform}
  viewPort.Children.Add(contentContainer)
  Children.Add(viewPort)

  contentContainer.Children.Add(model)
End Sub

Note that at this point a translation, scale and rotation is added to a Transform3DGroup that is then set as the transform for the contentContainer. This is because the contentContainer is what will hold the two models for the front and the back and by rotating the entire parent model both the front and the back can be rotated using a single transform.

The translation part is required because I want the panel to "back up" or move further away from the screen so that it never has any part that is technically in front of the screen during rotation. More on this later.

The scale part is required because the model that hosts the actual content needs to be resized to fit the parent container. The FlipPanel achieves this by letting the model be 1 unit wide, and scaling the height to the ratio (aspect ratio) between the height and width of the FlipPanel itself. In order to get a model that is 1 unit wide to always fill the screen on the horizontal simply position the camera 0.5 units away from the model and set the field of view to 90 degrees.

The FlipPanel uses ArrangeOverride and MeasureOverride to let the content think it is hosted on a surface the size of the control rather than the size of the model. This is important as not doing this yields UI's that look stretched or compressed.

338671/cameraposition.png

Position and field of view of camera to have a quad of width 1.0 fully fill the viewport.

Dependency properties

To keep it simple, only three properties are exposed on the FlipPanel and those are;

  • FrontVisible
  • SpinTime
  • SpinAxis
Obviously properties for setting the front and back content exists as well, but they're just standard properties, not dependency properties.
In code, these three properties are defined like this:
  Public Shared ReadOnly FrontVisibleProperty As DependencyProperty = DependencyProperty.Register("FrontVisible", GetType(Boolean), GetType(FlipPanel), New PropertyMetadata(False, AddressOf OnFrontVisibleChanged))
  Public Shared ReadOnly SpinTimeProperty As DependencyProperty = DependencyProperty.Register("SpinTime", GetType(Double), GetType(FlipPanel), New PropertyMetadata(1.0))
  Public Shared ReadOnly SpinAxisProperty As DependencyProperty = DependencyProperty.Register("SpinAxis", GetType(Orientation), GetType(FlipPanel), New PropertyMetadata(Orientation.Vertical, AddressOf OnSpinAxisChanged))

FrontVisible

This property determines which side should be the one facing the user, it's just a boolean and whenever it changes value the method OnFrontVisibleChanged is called and that method delegates to a method called Spin which is the one responsible for rotating the planes so that the one that should be facing the user actually is facing the user.

  Private Shared Sub OnFrontVisibleChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
    CType(d, FlipPanel).Spin()
  End Sub

SpinTime

This property is a double and it holds the number of seconds a spin from front to back should take. It only sets the value, there's no method called when this is changed as the value is only relevant when a new rotation is started, and at that point the Spin method reads the SpinTime in order to setup the Durations for the spin animations correctly.

SpinAxis

This property holds a value of type Orientation (i.e. Vertical or Horizontal) that is the axis the planes should spin around. Initially one might this that this property need not call a method when it's value is changed as with the SpinTime property the value is only relevant at animation start but that's not the case.
The way the content must added the front and back side of the planes differs depending on the axis of rotation. If rotation takes place around the Y-axis, both contents need to be oriented the same way, but if the axis of rotation is the X-axis then the back content must be oriented upsidedown compared to the front content for it to appear right side up after the rotation.
(That explanation didn't come out as clear as I'd like, but it makes sense if you think about it).

Because of this the models has to be setup again whenever this value changes and that is done in SetupModel which is called like this;

  Private Shared Sub OnSpinAxisChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
    CType(d, FlipPanel).SetupModel()
  End Sub

Content layout

In order to get the content to display in a non-streched way, it's important to let the UIElements that make up the content for the front and back side think they're laid out on area the size of the control. This is because when the content of a 3D control is added as a texture it will always fit, but the aspect ratio might be skewed.

To fix this the FlipPanel hooks into both stages of the WPF layout pass, measure and arrange, by overriding the corresponding methods;

  Protected Overrides Function MeasureOverride(ByVal availableSize As Size) As Size
    viewPort.Measure(availableSize)
    If Not frontElement Is Nothing Then
      frontElement.Measure(availableSize)
    End If

    If Not backElement Is Nothing Then
      backElement.Measure(availableSize)
    End If

    Return availableSize
  End Function

  Protected Overrides Function ArrangeOverride(ByVal finalSize As Size) As Size
    Dim r As Rect = New Rect(finalSize)
    viewPort.Arrange(r)

    If Not frontElement Is Nothing Then
      frontElement.Arrange(r)
    End If

    If Not backElement Is Nothing Then
      backElement.Arrange(r)
    End If

    Return finalSize
  End Function

In addition to doing the layout passes, the 3D models need to be adjusted so that they always exactly fill the viewport, this is done by hooking in to the SizeChanged event and adjusting the width and height of the models.
In reality the models themselves never change, it's simply a scale transform that is applied to the container holding the model.
Since the width of the model is always 1.0, just the height part of the scale needs to be changed to represent the fraction of the width that the height makes up;

 
  Private Sub HandlesSizeChanged(ByVal sender As Object, ByVal args As SizeChangedEventArgs) Handles Me.SizeChanged
    If args.NewSize.Width > 0 Then
      scale.ScaleY = args.NewSize.Height / args.NewSize.Width
    End If
  End Sub

Where scale in the snippet above is the scale transform applied to the content in SetupModel.

Setting up the model

Setting up the models is a matter of creating two Viewport2DVisual3D with the front and back content and then adding those to the main model.
This is straightforward enough and the only thing to note about the method is that the rotation for the back content has to take the SpinAxis dependency property into account, as discussed earlier.
Also, as new Viewport2DVisual3Ds are created, the current content has to be stored in temporary variables so that they can be restored onto the new Viewport2DVisual3D.

  Public Sub SetupModel()
    Dim front As Visual = Nothing
    Dim back As Visual = Nothing

    ' Store temporary and unhook current
    If Not frontVisual3D Is Nothing Then
      front = frontVisual3D.Visual
     frontVisual3D.Visual = Nothing
    End If

    If Not backVisual3D Is Nothing Then
      back = backVisual3D.Visual
      backVisual3D.Visual = Nothing
    End If

    ' Set up the rotation for the back, this depends on the axis it's going to spin about
    Dim backRotation As AxisAngleRotation3D = New AxisAngleRotation3D(IIf(SpinAxis = Orientation.Vertical, AxisY, AxisX), 180)

    ' Set up the front and back Viewport2DVisual3D, adding the backRotation to the transform of the back
    ' Note that they share the same mesh and material
    frontVisual3D = New Viewport2DVisual3D With {.Geometry = mesh, .Material = visualHostMaterial}
    backVisual3D = New Viewport2DVisual3D With {.Geometry = mesh, .Material = visualHostMaterial, .Transform = New RotateTransform3D(backRotation)}
    rotation.Axis = IIf(SpinAxis = Orientation.Vertical, AxisY, AxisX)

    ' Set the visuals if not null
    If Not front Is Nothing Then ' Double negatives, must be a better way of doing this...
      frontVisual3D.Visual = front
    End If

    If Not back Is Nothing Then
      backVisual3D.Visual = back
    End If

    ' Clear the current, and add the newly created
    model.Children.Clear()
    model.Children.Add(frontVisual3D)
    model.Children.Add(backVisual3D)

    ' Force layout pass
    InvalidateMeasure()
  End Sub

Front and Back

The front and back UIElements are set through two aptly named properties Front and Back.

These properties are responsible for unhooking the current content before setting the new one on to the Viewport2DVisual3D hosts created in method SetupModel.
The implementations looks like this;

  Public Property Front() As UIElement
    Get
      Return frontElement
    End Get
    Set(ByVal value As UIElement)
      frontVisual3D.Visual = Nothing
      frontVisual3D.Visual = value
      frontElement = value
    End Set
  End Property

  Public Property Back() As UIElement
    Get
      Return backElement
    End Get
    Set(ByVal value As UIElement)
      backVisual3D.Visual = Nothing
      backVisual3D.Visual = value
      backElement = value
    End Set
  End Property

These are then the properties that the user of this control is supposed to use to set the content, possibly in XAML like this;

<f:FlipPanel x:Name="flipper" f:FlipPanel.FrontVisible="False">
  <f:FlipPanel.Front>
    <Border Background="Red" BorderBrush="Black" BorderThickness="2">
      <Button Content="Front" VerticalAlignment="Center" Click="Button_Click"/>
    </Border>
  </f:FlipPanel.Front>
  <f:FlipPanel.Back>
    <Border Background="Green" BorderBrush="Black" BorderThickness="2">
      <Button Content="Back" VerticalAlignment="Center" Click="Button_Click"/>
    </Border>
  </f:FlipPanel.Back>
</f:FlipPanel>

Like a record, baby

Last step of the FlipPanel is to get the content to spin around, right round, baby. Like a record, baby. Round, round.
When everything has been setup using the methods I've described above, the matter of actually spinning this whenever the FrontVisible dependency property changes value becomes really, really simple.

As all the transforms are already attached to the container it's now a matter of just creating two animations;

  • Rotation
  • Translation
Both are needed to get a good looking rotation from front to back.

Rotation

This is the simpler of the to animations, it is an DoubleAnimation animating the rotation member either to 180.0 or 0.0 degrees depending on which side is currently showing. The Duration of the animation is dictated by the SpinTime dependency property.

Setting this up is easy;

  Dim rotationAnimation As DoubleAnimation = New DoubleAnimation(IIf(FrontVisible, 180, 0), New Duration(TimeSpan.FromSeconds(SpinTime)))
  rotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, rotationAnimation)

Translation

The translation part of the animation is required to prevent any part of the model ending up "behind" the camera.
This is achieved by backing up the model by such a distant that the near corner of the model remains at Z-coordinate 0.0.

The distance to find is distance d (in the diagram below) for any angle a such that the point highlighted by the orange overlay always lines up with the position of the virtual screen, or in our case Z-coordinate 0.0. This distance starts at 0.0 then increases to 0.5 before it drops back to 0.0.

338671/distance.png
A Z-translation distance d for any angle a is required to keep the panel in front of the screen.

This might initally sound like quite complicated to figure out, as the distance changes non-linear over time, that is to say the model needs to first back away slowly, then speed up, then again slow down they way it backs up. And that's just half of it, they all of that has to be repeated in the other direction when it moves back towards the camera.

Luckily, while this is slightly complicated to show in maths, it just works out in code as the distance needed to back up is essentially sine of the rotation so far.
And this makes it easy because WPF already provides SineEase, which applied with proper values gives us exactly the behaviour we want.

  Dim translationAnimation As DoubleAnimationUsingKeyFrames = New DoubleAnimationUsingKeyFrames()
  translationAnimation.KeyFrames.Add(New EasingDoubleKeyFrame(-0.5, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(SpinTime / 2.0)), New SineEase()))
  translationAnimation.KeyFrames.Add(New EasingDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(SpinTime)), New SineEase With {.EasingMode = EasingMode.EaseIn}))
  translation.BeginAnimation(TranslateTransform3D.OffsetZProperty, translationAnimation)

First key frame is moving back, second keyframe is moving front again, back into position where the model fills the screen.

Using it

Using the FlipPanel is easy, just add it, or several of it, like any other Panel to your WPF control, set the Front and Back properties and make sure you hook up a way to trigger the spinning. Simple.

Some people might say that Front and Back should have been dependecy properties but I disagree with that approach. For the content to change during runtime I rather have the content of Front and Back to hold a ContentPresenter and that's where the bindings should be. The FlipPanel is, like any Panel about layout, not content. It just happens to layout content in more dimensions.

Points of Interest

Flawed

A flaw in the design of this control is that it inherits from Panel, I did this because it's so convinient but since this particular Panel can only have two children, the hosts for Front and Back, it is illegal to add any children to the Panel without using these properties.
This problem is easily fixed by making sure that when the visual children of the FlipPanel changes, the child affected is either front or back;

  Protected Overrides Sub OnVisualChildrenChanged(ByVal visualAdded As DependencyObject, ByVal visualRemoved As DependencyObject)
    If Not Object.ReferenceEquals(visualAdded, frontVisual3D) And Object.ReferenceEquals(visualAdded, backVisual3D) Then
      Throw New InvalidOperationException("Add children using the Front and Back properties")
    End If
    MyBase.OnVisualChildrenChanged(visualAdded, visualRemoved)
  End Sub

And while this works fine it's still a big flaw I think because the implied contract of Panel has been violated. There is not a clean is-a relationship between Panel and FlipPanel. And that's bad design.

Screen real estate

I really think this sort of control is useful when it comes to conserving screen real estate and I try to show that in the C# example apps, also available on YouTube over here.

History

  • 2012-03-01; First version
  • 2012-03-14; Fixed bug in the stock demo app. 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Fredrik Bornander
Software Developer (Senior)
Sweden Sweden
Article videos
Oakmead Apps Android Games
 
21 Feb 2014: Best VB.NET Article of January 2014 - Second Prize
18 Oct 2013: Best VB.NET article of September 2013
23 Jun 2012: Best C++ article of May 2012
20 Apr 2012: Best VB.NET article of March 2012
22 Feb 2010: Best overall article of January 2010
22 Feb 2010: Best C# article of January 2010
Follow on   Google+   LinkedIn

Comments and Discussions

 
QuestionContent gets blurry PinmemberMarkus Mauch26-Nov-13 3:56 
AnswerRe: Content gets blurry PinprofessionalFredrik Bornander26-Nov-13 4:00 
GeneralRe: Content gets blurry PinmemberMarkus Mauch26-Nov-13 5:25 
AnswerRe: Content gets blurry PinmemberMember 787723223-May-14 4:27 
GeneralThanks a lot PinmemberJohnny J.6-Aug-12 12:02 
GeneralRe: Thanks a lot PinmemberFredrik Bornander7-Aug-12 0:01 
GeneralRe: Thanks a lot PinmemberJohnny J.12-Aug-12 11:58 
GeneralRe: Thanks a lot PinmemberFredrik Bornander14-Aug-12 20:47 
GeneralMy vote of 5 Pinmembernjdnjdnjdnjdnjd25-Jul-12 1:40 
GeneralRe: My vote of 5 PinmemberFredrik Bornander6-Aug-12 23:59 
QuestionCool PinmemberCIDev9-Apr-12 6:31 
AnswerRe: Cool PinmemberFredrik Bornander9-Apr-12 6:35 
General5 PinmemberRavi Bhavnani10-Mar-12 17:27 
GeneralRe: 5 PinmemberFredrik Bornander10-Mar-12 22:51 
BugI fand a bug [modified] PinmemberSpringLo6-Mar-12 21:17 
GeneralRe: I fand a bug PinmemberFredrik Bornander6-Mar-12 22:11 
AnswerRe: I fand a bug PinmemberFredrik Bornander14-Mar-12 21:50 
QuestionExcellent stuff Fredrik PinprotectorPete O'Hanlon6-Mar-12 2:41 
AnswerRe: Excellent stuff Fredrik PinmemberFredrik Bornander6-Mar-12 3:20 
GeneralMy vote of 5 PinmemberSledgeHammer012-Mar-12 10:45 
GeneralRe: My vote of 5 PinmemberFredrik Bornander2-Mar-12 10:56 
QuestionGreat Job Pinmemberlouislong2-Mar-12 6:01 
AnswerRe: Great Job PinmemberFredrik Bornander5-Mar-12 1:07 
QuestionNeato PinmvpSacha Barber2-Mar-12 0:20 
AnswerRe: Neato PinmemberFredrik Bornander2-Mar-12 10:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140926.1 | Last Updated 14 Mar 2012
Article Copyright 2012 by Fredrik Bornander
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid