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

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

In Part 1, we looked at how we construct a Dragging, docking, expanding panel, and added the ‘dragging’ functionality by placing the panel in a Canvas. In Part 2 we looked at the host panel that controls the grid layout and the docking functionality. In this part, we are going to look at how we add the maximising functionality.

 

We will start with some additional code we need to add the DragDockPanel. The toggle button for toggling a panel’s maximised state is already in the template (see Part 1). We now need to get the toggle button out, and hook up the checked and unchecked events...

    ToggleButton maximizeToggle =

        this.GetTemplateChild("PART_MaximizeToggle") as ToggleButton;

    if (maximizeToggle != null)

    {

        maximizeToggle.Checked +=

            new RoutedEventHandler(maximizeToggle_Checked);

        maximizeToggle.Unchecked +=

            new RoutedEventHandler(maximizeToggle_Unchecked);

    }

Next, we add a private variable that will be used as a flag to store whether a control was minimised by the toggle button, or minimised programmatically...

    private bool ignoreUnCheckedEvent = false;

We now add a public member to get / set whether the panel is maximised or not.

    private bool isMaximized = false;

    public bool IsMaximized

    {

        get { return this.isMaximized; }

        set

        {

            this.isMaximized = value;

            ToggleButton maximizeToggle =

                this.GetTemplateChild("PART_MaximizeToggle")

                as ToggleButton;

            if (maximizeToggle != null)

            {

                this.ignoreUnCheckedEvent = true;

                maximizeToggle.IsChecked = this.isMaximized;

            }

        }

    }

In the setter for IsMaximized, we store the value locally, and also update the toggle’s button checked state. This is where we use ignoreUnCheckedEvent. When we programmatically set the checked state of a toggle button, the Checked / UnChecked event is also raised from the button, so here we set a flag to say that we have programmatically set the property so we can ignore the next event that is raised.

Let’s take a look at the event handler for the toggle button’s checked event...

    void maximizeToggle_Checked(object sender, RoutedEventArgs e)

    {

        // Bring the panel to the front

        Canvas.SetZIndex(this, currentZIndex++);

        this.ignoreUnCheckedEvent = false;

        // Fire the panel maximized event

        if (this.Maximized != null)

            this.Maximized(this, e);

    }

The handler firstly brings the panel to the front, sets the flag to false again, and finally fires the panel’s own Maximized event for the host to pick up and respond too.

The unchecked handler is similar...

    void maximizeToggle_Unchecked(object sender, RoutedEventArgs e)

    {

        if (!this.ignoreUnCheckedEvent)

        {

            this.IsMaximized = false;

            // Fire the panel minimized event

            if (this.Minimized != null)

                this.Minimized(this, e);

        }

        else

        {

            this.ignoreUnCheckedEvent = false;

        }

    }

Here, we check the flag, and if we don’t ignore the event, we set IsMaximized to false, and raise the panel’s own Minimized event. Otherwise we just reset the flag to false.

That’s it for DragDockPanel. Let’s take a look at DragDockPanelHost where we handle the layout rearranging...

Firstly, we add a private member to store the maximised panel...

    private DragDockPanel maximizedPanel = null;

When a panel is maximised, the other panels move to a stack on the right hand side. We have a public property that allows you to set how wide you wish to make that stack...

    private double minimizedColumnWidth = 250.0;

    public double MinimizedColumnWidth

    {

        get { return this.minimizedColumnWidth; }

        set { this.minimizedColumnWidth = value; }

    }

In Part 2, in the DragDockPanelHost Loaded event handler, we hooked up the events necessary to handle the panel dragging. In the same place, we can add handlers to the panels’ minimised and maximised events...

    panel.Maximized +=

        new EventHandler(dragDockPanel_Maximized);

    panel.Minimized +=

        new EventHandler(dragDockPanel_Minimized);

The 2 handlers are quite simple - that bulk of the work takes place in our layout methods. The maximised handler looks like this...

    void dragDockPanel_Maximized(object sender, EventArgs e)

    {

        DragDockPanel maximizedPanel =

            sender as DragDockPanel;

        // Store max'ed panel

        this.maximizedPanel = maximizedPanel;

        // Loop through children to disable dragging

        foreach (UIElement child in this.Children)

        {

            DragDockPanel panel =

                child as DragDockPanel;

            panel.DraggingEnabled = false;

            if (panel != this.maximizedPanel)

                panel.IsMaximized = false;

        }

        // Update sizes and layout

        this.AnimatePanelSizes();

        this.AnimatePanelLayout();

    }

When a panel is maximised, we keep a reference to the maximised panel, loop through all of the children disabling dragging, and setting all of the other panels IsMaximized property to false, and finally, updating the layout with the animating methods.

The minimised handler is very similar...

    void dragDockPanel_Minimized(object sender, EventArgs e)

    {

        // Set max'ed panel to null

        this.maximizedPanel = null;

        // Loop through children to disable dragging

        foreach (UIElement child in this.Children)

        {

            DragDockPanel panel =

                child as DragDockPanel;

            panel.DraggingEnabled = true;

        }

        // Update sizes and layout

        this.AnimatePanelSizes();

        this.AnimatePanelLayout();

    }

We clear the reference to the maximised panel, and then loop through all of the children, re-enabling dragging. Finally, we update the layout using the animated methods.

Let’s take a look at the animated size and layout methods, firstly, AnimatePanelLayout.

private void AnimatePanelLayout()

    {

        // If we are not in max'ed panel mode...

        if (this.maximizedPanel == null)

        {

            // Place panels in a grid...

        }

        else // If a panel is maximized...

        {

            // Layout the children with one panel maxmisised.

        }

     }

 

Firstly, we check to see if there is a maximised panel - if not, we use the code we already have to put them into a grid, if so, then we lay them out, with one maximised.

Something we need to consider here is that the panels may have been shuffled around, so they are no longer displayed in the same order that the host has them in the visual tree. So firstly, we need to get the current order of the panels, going left to right across rows, and down each row.

Dictionary<int, DragDockPanel> orderedPanels =

   new Dictionary<int, DragDockPanel>();

            // Loop through children to order them according to their

            // current row and column...

            foreach (UIElement child in this.Children)

            {

                DragDockPanel panel = (DragDockPanel)child;

                orderedPanels.Add(

                    (Grid.GetRow(panel) * this.columns) +

                    Grid.GetColumn(panel),

                    panel);

            }

 

Here we create a dictionary that stores an index of a panel, and the panel itself. The index is calculated using the panel’s row and column properties. This gives us the real position of the panel (in terms of a stacking order). Now we have the panels in order, we can loop through and position them.

// Set initial top of minimized panels to 0

            double currentTop = 0.0;

            // For each of the panels (as ordered in the grid)

            for (int i = 0; i < orderedPanels.Count; i++)

            {

                // If the current panel is not the maximized panel

                if (orderedPanels[i] != this.maximizedPanel)

                {

                    // Animate the size

                    orderedPanels[i].AnimatePosition(

                        this.ActualWidth -

     this.minimizedColumnWidth,

                        currentTop

                        );

                    // Increment current top

                    currentTop +=

                        this.ActualHeight /

                        (double)(this.Children.Count - 1);

                }

                else // If the current panel is the maxmized panel

                {

                    // Animate it to 0,0

                    orderedPanels[i].AnimatePosition(0, 0);

                }

 

The simple loop goes through each panel. If it’s not the maximised panel, it stacks it on the right hand side, and keeps track of the position in a variable currentTop. If the panel is the maximised panel, then we move it to 0,0.

That’s all that’s required to position the panels when one is maximised. Let’s take a quick look at the panel sizing.

    private void AnimatePanelSizes()

    {

        // If there is not a maxmized panel...

        if (this.maximizedPanel == null)

       {

            // Size to grid cells

            ...

        }

        else // If there is a maximized panel

        {

            // Size for a maximised panel.

           

        }

    }

You will see that first we check if there is a maximised panel - if not, we use the code we had before to size the elements for a grid. Otherwise we loop through children, giving the appropriate sizes.

            foreach (UIElement child in this.Children)

            {

                DragDockPanel panel =

                    (DragDockPanel)child;

                // Set the size of the non

                // maximized children

                if (panel != this.maximizedPanel)

                {

                    panel.AnimateSize(

                        minimizedColumnWidth -

                        panel.Margin.Left -

                        panel.Margin.Right,

                        (this.ActualHeight /

                        (double)(this.Children.Count - 1)) -

                        panel.Margin.Top - panel.Margin.Bottom

                        );

                }

                else // Set the size of the maximized child

                {

                    panel.AnimateSize(

                        this.ActualWidth -

                        this.minimizedColumnWidth -

                        panel.Margin.Left - panel.Margin.Right,

                        this.ActualHeight -

                        panel.Margin.Top - panel.Margin.Bottom

                        );

                }

            }

 

As we loop through the children, we check to see if the current panel is the maximised one - if it isn’t then we size the panel to fit on the stack on the right hand side, if it is, then we size the panel to fill the rest of the space. Cool.

The same logic is added to the UpdatePanelLayout method, but we won’t go through that here.

I have decided to do templating of the panels in a video, showing off Blend 2.5. That will be the final part of the Drag-Dock-Expanding panel sample. We will move onto something new after that!

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

Check out a running sample here!

Martin