Silverlight 2 Samples: Dragging, docking, expanding panels (Part 1)

UPDATE: Get the latest Dragging, docking, expanding panel code from Blacklight, our new CodePlex project! 

NOTE: Part 2 and Part 3 now posted, with a demo of the final sample here .  

A control that we used in a number of places in the MSCUI Patient Journey Demonstrator was the dragging, docking, expanding panel control. When designing the dashboards in the application, we wanted to create an experience that was customisable, fun to use and allowed the user to focus in on the important information when they needed it.

The shots below show panels in action...

The controls provide the following...

· Automatic grid layout of panels

· Picking up and moving panels around and changing their position in the grid (dragging and docking)

· ‘Maximising’ a panel causing the panel to take up much more space, whilst the others stack up on the right hand side of the application

As we used the panels in a number of places, building them for re-use was a big priority too. In this article, I will show how to build a similar customisable layout system.

There are 2 parts to the DragDockPanel system. The DragDockPanel itself (the thing you pick up and drag around) and the container for multiple DragDockPanel’s - I call it DragDockPanelHost. We will look at DragDockPanel today, and have it working just in a regular Canvas, and the host container in Part 2.

As a panel needs to contain something, it made sense for DragDockPanel to extend ContentControl. However, I also want DragDockPanel to have to easy methods to animate size and position, so I actually inherit AnimatedContentControl (a custom control that generates size and position animations and methods - see Silverlight 2 Samples: Animating controls).

As we are extending AnimatedContentControl, we also need to create a template in XAML for the control. The great thing about templates is that they allow a designer / developer to define the building blocks and visuals for a control. The only thing we need to ask of them is that they include a couple of specifically named elements, so that we can implement the functionality. For DragDockPanel, there are 3 things a designer / developer need to include in the template to get the full functionality of the panel...

· A ContentPresenter control - as we are extending ContentControl (via AnimatedContentControl) we need a ContentPresenter in the template to be the site of the panel’s content

· An element named “PART_GripBar” - this is the portion of the panel that allows the user to pick up the panel and drag it around

· A ToggleButton control (which could also be a CheckBox or RadioButton) named “PART_MaximizeToggle” - this is the button that when clicked, the panel will either maximize or collapse

When writing the code behind for the control, the developer has the choice to implement none, some or all of the above. There would obviously be missing functionally, however, if they don’t include, say, the grip bar - they would not be able to drag the panel around.

Below shows a style with an outline template (included in the attached sample) with the above parts included...

<Style TargetType="local:DragDockPanel">

    <Setter Property="Template">

      <Setter.Value>

        <ControlTemplate TargetType="local:DragDockPanel">

          <Grid>

           

            <!-- Border with white background -->

            <Border CornerRadius="3,3,3,3"

                    Background="#ffffffff"

                    BorderBrush="#ff999999"

                    BorderThickness="1"

                    Padding="2">

              <Grid>

                <!-- Content presenter for hosting the content -->

                <ContentPresenter />

                <!--

                  Element named PART_GripBar for

                  handling the dragging of the panel

                  -->

                <Border x:Name="PART_GripBar"

                        Background="#7fffffff"

                        VerticalAlignment="Top"

                        Height="30" Cursor="Hand">

                  ...

                </Border>

                <!-- ToggleButton for maximizing function -->

                <ToggleButton x:Name="PART_MaximizeToggle"

                              VerticalAlignment="Top"

                              HorizontalAlignment="Right"

                              Margin="0,5,5,0" Width="20" Height="20"

                              Cursor="Hand"

                              />

              </Grid>

            </Border>

          </Grid>

        </ControlTemplate>

      </Setter.Value>

    </Setter>

  </Style>

You will find the above style in generic.xaml in the attached project. Placing the style in generic.xaml means that this will be the default style for DragDockPanel. Using the XAML shown below, we can now put a panel on the page, with a media element inside...

<local:DragDockPanel>

<MediaElement Source="..." />

</local:DragDockPanel>

The result...

You can see that the panel contains a video, with a grip bar and maximize button across the top of the panel. Great. Lets get it moving around the screen...

First things first, we need to get hold of the elements in the template so we can hook up the mouse events. When creating a custom control, you can override a method - public override void OnApplyTemplate() - which is called as soon as the template is applied to your control. You can then use this.GetTemplateChild("PART_ElementName") to get your elements out, and hook up events. I have mentioned before that we needed to have specially named elements in the default template in generic.xaml - this is why. Our override in DragDockPanel looks like this...

public override void OnApplyTemplate()

        {

            base.OnApplyTemplate();

            FrameworkElement gripBar =

                this.GetTemplateChild("PART_GripBar") as FrameworkElement;

            if (gripBar != null)

            {

                gripBar.MouseLeftButtonDown +=

                    new MouseButtonEventHandler(gripBar_MouseLeftButtonDown);

                gripBar.MouseMove +=

                   new MouseEventHandler(gripBar_MouseMove);

                gripBar.MouseLeftButtonUp +=

                    new MouseButtonEventHandler(gripBar_MouseLeftButtonUp);

            }

        }

Here, we are pulling out the grip bar into a private member, and hooking up some events.

For the grip bar, I hook up the MouseLeftButtonDown, MouseMove, MouseLeftButtonUp. I need these to move the panel about when the grip bar is dragged.

The MouseLeftButtonDown simply checks to see if dragging is enable on this panel (another private member), we then bring the panel to the front (setting the ZIndex), capture the mouse, store the current position, flag that we are dragging, and then raise an event to say the panel has started to be dragged.

void gripBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)

        {

            if (this.draggingEnabled)

            {

                // Bring the panel to the front

                Canvas.SetZIndex(this, currentZIndex++);

                // Capture the mouse

                ((FrameworkElement)sender).CaptureMouse();

                // Store the start position

                this.lastDragPosition = e.GetPosition(sender as UIElement);

                // Set dragging to true

                this.isDragging = true;

                // Fire the drag started event

                if (this.DragStarted != null)

                    this.DragStarted(this, new DragEventArgs(0, 0, e));

            }

           

        } 

 

Our MouseMove firstly checks to see if we are dragging (i.e. if the mouse is down), we then use the last position variable to calculate the panels new position. We then set Canvas.Left and Canvas.Top which moves the panel within a Canvas.

Finally, we fire an event to say the panel has moved, and store the last position again.

void gripBar_MouseMove(object sender, MouseEventArgs e)

        {

            if (this.isDragging)

            {

                Point position = e.GetPosition(sender as UIElement);

                // Move the panel

                Canvas.SetLeft(

                    this,

                    Canvas.GetLeft(this) + position.X - this.lastDragPosition.X

                    );

               

                Canvas.SetTop(

                    this,

                    Canvas.GetTop(this) + position.Y - this.lastDragPosition.Y

                    );

 

                // Fire the drag moved event

                if (this.DragMoved != null)

                    this.DragMoved(

                        this,

                        new DragEventArgs(

                            position.X - this.lastDragPosition.X,

                            position.Y - this.lastDragPosition.Y, e));

               

                // Update the last mouse position

                this.lastDragPosition = e.GetPosition(sender as UIElement);

               

            }

        }

In MouseLeftButtonUp we simple release mouse capture, set the dragging flag to false, and raise an event to say the dragging has finished.

void gripBar_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)

        {

            if (this.draggingEnabled)

            {

                // Capture the mouse

                ((FrameworkElement)sender).ReleaseMouseCapture();

                // Set dragging to true

                this.isDragging = false;

                Point position = e.GetPosition(sender as UIElement);

                // Fire the drag finished event

                if (this.DragFinished != null)

                    this.DragFinished(

                        this,

                        new DragEventArgs(

                            position.X - this.lastDragPosition.X,

                            position.Y - this.lastDragPosition.Y, e));

            }

        }

And that’s it! We can now drag our panel around a canvas, bringing it to the front with each mouse down action. On the project page, you can use the panels in a canvas like so...

<Canvas>

            <local:DragDockPanel Width="400" Height="300">

                <MediaElement Source="..." />

            </local:DragDockPanel>

            <local:DragDockPanel  Width="400" Height="300">

                <MediaElement Source="..." />

            </local:DragDockPanel>

            <local:DragDockPanel  Width="400" Height="300">

                <MediaElement Source="..." />

            </local:DragDockPanel>

            <local:DragDockPanel  Width="400" Height="300" >

                <MediaElement Source="..." />

            </local:DragDockPanel>

            <local:DragDockPanel  Width="400" Height="300">

                <MediaElement Source="..." />

            </local:DragDockPanel>

            <local:DragDockPanel  Width="400" Height="300" >

                <MediaElement Source="..." />

            </local:DragDockPanel>

        </Canvas>

The result should look like this (after you drag the panels about a bit!)

Take a look at the sample running here. 

In part 2 we will look at how to arrange the panels automatically into a grid, move and shuffle the panels around, maximise / minimise panels and also how to deal with any amount of panels on the screen.

Source code is available from www.codeplex.com/blacklight.

Enjoy J