Silverlight provides several layout panels which can be used to layout almost any simple user interface. These layout panels are the Grid, Canvas and StackPanel. In more sophisticated user interfaces, however, these basic layout panels may not be enough. In this chapter, you will learn how to create a custom layout panel that organizes its children in a 3D stack. We will create a test application for this panel which will allow you to manipulate various properties of the panel in order to achieve different visual effects including one similar to what is seen in Windows Vista Ultimate “Flip 3D” feature (Alt+Tab while various applications or folders are open to see the “Flip 3D” feature in action) .
To begin, we need to create a new project Silverlight Class Library project in visual studio and name it CustomPanels. Now, add a class to the project and name it Stack3DPanel. Stack3DPanel needs to derive from Panel in order to inherit some of the properties and methods common to all layout panels. The most important of these is the ‘Children’ property which contains all the visual elements to be arranged by the panel. Next, we will add the dependency properties needed to manipulate the way the panel will layout its children.
Define Dependency Properties
Whenever creating properties on custom Silverlight classes, you should consider making those properties dependency properties. Although there are numerous reasons to create them as dependency properties, the most compelling reasons are arguably that dependency properties can support animations, styling and data binding. The dependency properties we will create for the Stack3DPanel are the DepthOffset, HorizontalOffset, VerticalOffset, SkewYAngle and Blur properties.
Before creating the dependency properties, we need to create a static event handler which will execute whenever any of the dependency properties’ values change. Whenever a dependency property value changes, we want to rearrange the children in the panel accordingly. To do so, we simply call the InvalidateArrange method available to us because we derived from the Panel class. Notice we cannot directly access the panel’s InvalidateArrange method by calling this.InvalidateArrange() because this method is a static method. However, the DependencyObject parameter contains a reference to the panel. We can therefore access it as a Stack3DPanel to get to the InvalidateArrange method.
The DepthOffset property specifies the depth between items within the panel. This correlates to the projected location of the item relative to the z axis. The greater the DepthOffsett, the greater the distance between objects in the panel. Consequently, the items in the panel will appear to get closer to the user as the DepthOffset increases.
Note, the Stack3DPanel arranges its children in a Last In First Out (LIFO) fashion. In other words, the first item inserted into the panel is placed at the bottom of the stack and all successive items will be placed on top of that. To add the DepthOffset property, we need to add a public static dependency property and a public property to access it. This pattern will be repeated for all the dependency properties we will create for this panel.
In order for the Silverlight property system to use the dependency property, we must follow the naming conventions it defines. Dependency properties themselves have unique basic names (DepthOffset). When creating the identifiers for those properties, the basic name is combined with the suffix Property (DepthOffsetProperty). When defining the property’s identifier, we register the dependency property by calling the static Register method of the DependencyProperty class. We will provide 4 parameters for this method. The first is the basic name of the property (i.e. DepthOffset). The second is the data type of that property. The third parameter identifies the type of the class registering the dependency property which in this case is Stack3DPanel. The final parameter is of type PropertyMetaData. This last parameter ultimately identifies what event handler to execute whenever the value of that dependency property changes. In this implementation, we want the same method to be executed whenever any of our dependency properties change, so instead of defining a new PropertyMetaData object within the Register method call (which is typically done), we will define a private variable of type PropertyMetaData which will point to the OnLayoutPropertyChanged event handler.
After we have created the property identifier, we can create the property which simply sets/gets the appropriate dependency property value from the Silverlight property system by calling get/set value for the appropriate property identifier.
public double DepthOffset
For the following dependency properties, we follow this same pattern; define the identifier by registering and then the property with a getter and setter for which access the registered dependency property.
The HorizontalOffset property specifies the amount of horizontal space between items within the panel.
First, we register the dependency property.
And then we define the HorizontalOffset property.
The VerticalOffset property specifies the amount of vertical space between items within the panel.
First, we register the dependency property.
And then we define the VerticalOffset property.
The SkewYAngle property specifies the angle by which all the items within the panel are skewed along the y axis to create various visual effects.
First, we register the dependency property.
And then we define the SkewYAngle property.
Lastly, Blur property specifies if a blur effect is applied to the items within the panel giving the illusion of loss of focus as the individual items are further away from the user.
Firs, we register the dependency property.
And then we define the Blur property.
Now it’s time to work on the heart of the Stack3DPanel. Whenever creating custom layout panels, the two most important methods that you must override are MeasureOverride and ArrangeOverride. We’ll start by implementing the MeasureOverride method.
MeasureOverride method first calls the Measure method for each child element. This call is necessary for the child element to calculate its DesiredSize property, which is later used in the ArrangeOverride method. The method then simply returns the availableSize it originally provided.
As its name suggests, the ArrangeOverride method is responsible for placing the children of the panel in their appropriate locations. In essence, this method contains all the logic which causes the panel to arrange its children in a 3D stack formation. While iterating through all its children, the ArrangeOverride method first center aligns each child element both vertically and horizontally. Second, a BlurEffect is applied to the children if the Blur property is set to true and if the current child element is not the first in the list. We do not want the first child element blurred because it appears in front of all the other elements. The appropriate visual effect is for those that are further back to appear blurred while the ones in front appear more focused. Then, the elements are placed on the panel. This is done by calling the child.Arrange method. Initially, all the children are placed in the same location. A series of projections and a transforms are then applied to the children according to their relative position to achieve the appropriate visual effect. First, a SkewTransform is applied with the AngleY set to the value in the SkewYAngle property as a RenderTransform. Finally, a PlaneProjection is applied to each child element. Where LocalOffsetZ, LocalOffsetX and LocalOffset Y are set to i*DepthOffset, i*HorizontalOffset and i*VerticalOffset respectively. At the conclusion of the method, the finalSize value passed in as a parameter is returned. This is typically done in most custom panel implementations. Note: when the panel is rendered, the last child element will appear in the front of the stack (closest to the user) and the first child element will appear in the back (furthest from the user).
One feature we will add to this custom panel is the ability to interact with it. We want to allow users to cycle through the items in the stack panel by popping the element on top off the stack and pushing it to the bottom of the stack allowing users to easily flip through the children in the panel. To do this, we must implement the Push, Pop, Cycle and CycleBack methods.
The panel we are creating is called a 3DStackPanel because it lays out its children in a 3 dimensional stack formation. The way we will implement that adding and removing of items from this panel, however, will more closely resemble that of a queue. Because we are calling it a 3DStackPanel, we will use name these operations Push and Pop as is typical of any stack. Note, in a traditional queue implementation, elements are added in the back of the queue and removed from the front. These operations are known as enqueue and dequeue respectively. In essence, we will implement an enqueue method and name it push and a dequeue method and name it pop.
We start with the simplest method to implement – Push. The Push method takes in an UIElement as an input parameter. This is the element that is to be inserted into the stack. We simply insert it in the front of the Children collection. Because of how the ArrangeOverride method was implemented, inserting the element in the front of the Children collection will make it appear in the back of the stack once it is rendered to the screen.
The Pop operation will simply remove the last child element (which is rendered at the front of the stack) from the stack and returns it. We need to check for an empty Children collection. If there are no children, the method simply returns null;
Now that Push and Pop have been implemented, we can add two other methods that combine these operations to allow us to flip through the stack. The cycle method will take the element appearing in the front and place it in the back. This is done by calling the Pop method and then the Push method with that same element. After we have rearranged the Children collection by calling the Push and Pop methods, we call the InvalidateArrange method to rearrange the elements on the screen.
The CycleBack method will do the opposite of the Cycle method. It removes the first element in the Children collection, which appears in the back of the stack, and sets it as the last in the Children collection, which in turn places it in the front of the stack when rendered. Again, once the Children are rearranged, we call InvalidateArrange to arrange the elements on the screen.
The last piece of functionality we will add is mousewheel interactivity. We will allow users to cycle back and forth through the elements in the panel by scrolling back and forth on the mouse wheel. To do this, we need to add a MouseWheel event handler. We then simply call the Cycle and CycleBack methods inside the event handler depending on which way the mouse wheel is rolled.
We have now finished implementing our custom Stack3DPanel. Now, let’s create a sample application to use this panel to arrange images in the 3D stack.
Create Sample Application
Add a new Silverlight Application project to the Visual Studio Solution and name it PictureStack. This will add two projects to the solution, the silverlight project (PictureStack) and a web project (PictureStack.Web). Paste the following XAML into MainPage.xaml located in the silverlight project to layout the user interface. In this user interface, we are placing an instance of the Stack3DPanel and using data binding to manipulate all the dependency properties we defined for it.
Now add the following code to MainPage.xaml.cs to implement the application logic. All we are doing is calling the Cycle method when the cycle button is clicked and using an OpenFileDialog to add images to the Stack3DPanel.
Now, run the application and experiment by changing the values of the slider to come up with different visual effects. I encourage you to also experiment with this new panel by placing images and other user interface elements such as buttons, textboxes, and other layout panels to create more sophisticated user interfaces. An interesting use of this custom panel would be to use to layout various screens of an application wizard.