Udostępnij za pośrednictwem


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

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 this part, we are going to look at how we do the docking element.

Take a peek at the finished sample running, here.

The positioning and docking logic takes place in a host control, called DragDockPanelHost. This is a panel control, that derives Canvas, that positions the panels in a grid and then moves them around when the user is dragging DragDockPanel.

[But first, a small digression...]

Why derive Canvas and not Panel I hear you cry??? Dave Relyea posted a while back on why he doesn’t like Canvas. Whilst he makes some good points, I feel he missed a trick when it comes to considering Canvas for the type of layout we do here.

Deriving Panel gives you 2 methods - Measure and Arrange - tell the layout system how big you want to be, then layout your children. Simple for when you want your children to go to a specific place every time the layout updates. However, with our layout system, we are more complicated. We have panels that can be dragged, shuffled around, maximised etc. etc.

To do all of this by deriving Panel seemed difficult and cumbersome, however, when starting with Canvas, you have a superbly basic layout system, using absolute positioning and giving you animatable positioning properties. I didn’t need to worry about layout updated events at all, but just consume the custom events my panels raised.

Simple. I like Canvas for this kind of purpose. Please challenge me if you feel differently!

[Digression ends.]

OK. So first off, we create out class and derive from Canvas...

    public class DragDockPanelHost : Canvas

    {

        #region Private members

        // A local store of the number of rows

        private int rows = 1;

        // A local store of the number of columns

        private int columns = 1;

        // The panel currently being dragged

        private DragDockPanel draggingPanel = null;

        #endregion

        #region Constructors

        /// <summary>

        /// Constructor

        /// </summary>

        public DragDockPanelHost()

        {

            this.Loaded +=

                new RoutedEventHandler(DragDockPanelHost_Loaded);

            this.SizeChanged +=

                new SizeChangedEventHandler(DragDockPanelHost_SizeChanged);

        }

        #endregion

    }

We also have some private variables that store the number of rows and columns (so we don’t have to recalculate every time) as well as the currently dragging panel. In the constructor, we hook up the Loaded events and SizeChanged events.

When we get the Loaded event, we do the work to calculate how may rows and columns we require (based on how many children the panel has), place each panel in a grid and row, and hook up some events from the panel.

(NOTE - we only do this in the Loaded event, meaning panels added later won’t be ‘hooked up’. Please check out the limitations section at the end of the post for more details on this.)

Firstly, we work out the number of rows. When placing the panels in a grid like layout, we try and lay them out in a square, giving preference to width over height (as most resolutions are 4:3 or wider). Let me give you an example...

If we had 6 panels, we would have 2 rows with 3 panel in each row, rather than 3 rows with 2 panels in each. If we had 8 panels, we would have 2 rows with 4 panels in each. If we had 9 panels, we would have 3 rows, with 3 panels in each.

To work out the rows, we take the square root of the number of children, and round it down.

Once we know how many rows we have, we can work out how many columns are required...

    // Calculate the number of rows and columns required

    this.rows =

        (int)Math.Floor(Math.Sqrt((double)this.Children.Count));

    this.columns =

        (int)Math.Ceiling((double)this.Children.Count / (double)rows);

We then loop through the rows and columns, assigning each panel a row and column, and hooking up the panels events...

    int child = 0;

    // Loop through the rows and columns and assign to children

    for (int r = 0; r < this.rows; r++)

    {

        for (int c = 0; c < this.columns; c++)

        {

            DragDockPanel panel = this.Children[child] as DragDockPanel;

            // Set starting row and column

            Grid.SetRow(panel, r);

            Grid.SetColumn(panel, c);

            // Hook up panel events

            panel.DragStarted +=

                new DragEventHander(dragDockPanel_DragStarted);

            panel.DragFinished +=

                new DragEventHander(dragDockPanel_DragFinished);

            panel.DragMoved +=

                new DragEventHander(dragDockPanel_DragMoved);

            child++;

            // if we are on the last child, break out of the loop

            if (child == this.Children.Count)

                break;

        }

        // if we are on the last child, break out of the loop

        if (child == this.Children.Count)

            break;

    }

You will see that we actually use the Grid.Row and Grid.Column attached properties to record the row and column, even though the panels aren’t actually in a Grid control. We couldn’t think of a reason why this would be bad!

There are 3 layout methods in DragDockPanelHost - UpdatePanelLayout, AnimatePanelSizes, AnimatePanelLayout. The first sets the child panels size and positions without animation, the other two animate the sizes and positions respectively, using AnimateSize and AnimatePosition (from the AnimatedContentControl base class).

Let’s look at UpdatePanelLayout...

    private void UpdatePanelLayout()

    {

        // Layout children as per rows and columns

        foreach (UIElement child in this.Children)

        {

            DragDockPanel panel = (DragDockPanel)child;

            Canvas.SetLeft(

                panel,

                (Grid.GetColumn(panel) *

                    (this.ActualWidth / (double)this.columns))

                );

           

            Canvas.SetTop(

                panel,

                (Grid.GetRow(panel) *

     (this.ActualHeight / (double)this.rows))

                );

            panel.Width =

                (this.ActualWidth / (double)this.columns) -

                panel.Margin.Left - panel.Margin.Right;

           

            panel.Height =

                (this.ActualHeight / (double)this.rows) -

                panel.Margin.Top - panel.Margin.Bottom;

        }

    }

In this method, we loop through the children in the host, setting the position and size of each panel. To set the position, we get the column and row the panel sits in, and multiply by the size of the host over the number of columns and rows.

For the size, we set the width and height to be the size of the host over the number of columns or rows and subtracting any margin the panel has.

The UpdatePanelLayout method is called every time the host changes size...

    void DragDockPanelHost_SizeChanged(

        object sender, SizeChangedEventArgs e)

    {

        this.UpdatePanelLayout();

    }

So, we now have our grid layout, and as the host resizes, the panels will stay in position.

Next, let’s deal with the dragging / docking.

In the loaded event, we hooked up 3 events for each of the panels - DragStarted, DragFinished and DragMoved. These are the 3 events that tell the host when a panel is being dragged about and when it has been dropped.

The handlers for DragStarted and DragFinished are very simple...

    void dragDockPanel_DragStarted(object sender, DragEventArgs args)

    {

        DragDockPanel panel = sender as DragDockPanel;

        // Keep reference to dragging panel

        this.draggingPanel = panel;

    }

In DragStarted we just keep a reference to the panel that is being dragged.

    void dragDockPanel_DragFinished(object sender, DragEventArgs args)

    {

        // Set dragging panel back to null

        this.draggingPanel = null;

        // Update the layout (to reset all panel positions)

        this.UpdatePanelLayout();

    }

In DragFinished, we clear the reference the dragging panel and call UpdatePanelLayout to reset all of the panels to their current position and size.

The smart stuff happens in the DragMoved event. This handler is called every time the mouse moves when a panel is being dragged. It works out the position of the mouse (which row and column it’s in), whether there is a panel in that row and column (that is not the panel being dragged), and if so, slides that panel into the available space. Lets look at the handler...

    void dragDockPanel_DragMoved(object sender, DragEventArgs args)

    {

        ...

    }

First thing worth noting is the argument type - DragEventArgs. These event arguments contain the source mouse event arguments for getting the position. We use these arguments to work out which row and column we are in (using the same logic we used to position the panels in UpdatePanelLayout)...

    Point mousePosInHost =

        args.MouseEventArgs.GetPosition(this);

   

    int currentRow =

        (int)Math.Floor(mousePosInHost.Y /

        (this.ActualHeight / (double)this.rows));

    int currentColumn =

        (int)Math.Floor(mousePosInHost.X /

        (this.ActualWidth / (double)this.columns));

Once we know what column and row the mouse is in, we can loop through the children and work out which panel is in that row and column. If it’s not the panel being dragged, we store it.

    // Stores the panel we will swap with

    DragDockPanel swapPanel = null;

    // Loop through children to see if there is a panel to swap with

    foreach (UIElement child in this.Children)

    {

        DragDockPanel panel = child as DragDockPanel;

        // If the panel is not the dragging panel and is in the current row

        // or current column... mark it as the panel to swap with

        if (panel != this.draggingPanel &&

            Grid.GetColumn(panel) == currentColumn &&

            Grid.GetRow(panel) == currentRow)

        {

            swapPanel = panel;

            break;

        }

    }

Finally, if we found a panel to swap with, we swap the row and column for it with the dragging panel’s row and column and animate the all the panels to their new positions...

    // If there is a panel to swap with

    if (swapPanel != null)

    {

        // Store the new row and column

        int draggingPanelNewColumn = Grid.GetColumn(swapPanel);

        int draggingPanelNewRow = Grid.GetRow(swapPanel);

        // Update the swapping panel row and column

        Grid.SetColumn(swapPanel, Grid.GetColumn(this.draggingPanel));

        Grid.SetRow(swapPanel, Grid.GetRow(this.draggingPanel));

        // Update the dragging panel row and column

        Grid.SetColumn(this.draggingPanel, draggingPanelNewColumn);

        Grid.SetRow(this.draggingPanel, draggingPanelNewRow);

      // Animate the layout to the new positions

        this.AnimatePanelLayout();

    }

The AnimatePanelLayout method is almost the same as UpdatePanelLayout, only, it just updates the panels positions, and uses AnimatePosition rather than setting directly...

    private void AnimatePanelLayout()

    {

        // Loop through children and size to row and columns

        foreach (UIElement child in this.Children)

        {

            DragDockPanel panel = (DragDockPanel)child;

            if (panel != this.draggingPanel)

            {

                panel.AnimatePosition(

                    (Grid.GetColumn(panel) *

                    (this.ActualWidth / (double)this.columns)),

                    (Grid.GetRow(panel)

                    * (this.ActualHeight / (double)this.rows))

                    );

            }

        }

    }

And there we have it - dragging, docking panels. We are now ready to use this on our page. In part one, where we previously had 6 panels in a canvas, we can replace the canvas with our DragDockPanelHost...

  <local:DragDockPanelHost Margin="50">

    <local:DragDockPanel Margin="10">

      <MediaElement Source="..." />

    </local:DragDockPanel>

    <local:DragDockPanel Margin="10">

      <MediaElement Source="..." />

    </local:DragDockPanel>

    <local:DragDockPanel Margin="10">

      <MediaElement Source="..." />

    </local:DragDockPanel>

    <local:DragDockPanel Margin="10" >

      <MediaElement Source="..." />

    </local:DragDockPanel>

    <local:DragDockPanel Margin="10">

      <MediaElement Source="..." />

    </local:DragDockPanel>

    <local:DragDockPanel Margin="10" >

      <MediaElement Source="..." />

    </local:DragDockPanel>

   

  </local:DragDockPanelHost>

The result should look like this...

Experiment with resizing your browser, dragging the panels around, and, adding more panels to the UI!

I mentioned earlier on that there is a limitation with this implementation of DragDockPanelHost... we can’t add new panels dynamically at run-time. This could be worked around by creating an Add method that will place the child in the visual tree, add an addition row / column is required, hook up the events and update the layout, however, if a consumer of the control attempted to add a child using the panels Children.Add(...) then this would go ignored. I had thought about designing this control as an ItemsControl, but wanted to keep this example simple, and focus on the layout, however, and ItemsControl would be a good solution, allowing you to be notified when a child is added the collection.

I would be more than interested to hear people’s ideas about how this could be done cleanly!

I hope you have enjoyed this post - watch out for Part 3 which will deal with Maximising panels and creating new templates J

As always, source code is at www.codeplex.com/blacklight.

Martin

Comments

  • Anonymous
    August 18, 2008
    PingBack from http://housesfunnywallpaper.cn/?p=1214

  • Anonymous
    August 18, 2008
    The comment has been removed

  • Anonymous
    August 18, 2008
    Thanks Martin for sharing your thoughts and ideas on dragging/docking of the controls through this post. The ideas are really nice and cool. Hoping to see your post on Maximizing the controls soon. -Raju

  • Anonymous
    August 18, 2008
    Thanks Martin!  On the subject of using a Canvas, I think it's similar to the "GoTo" argument.  Having said that, while you may need the Canvas to promote easy dragging, I have implemented a similar shifting object application using Stack Panels or Grids(no animation).  I think it really depends on how you're used to approaching the problem.  In reality, EVERYTHING is based off of absolute positioning!  The various panels simply give us the apperance of it not being there for simplicity of design. One other issue.  I tried adding a simple button to test adding another object, but couldn't control the size.  Is there a way to have DragDockPanel's of different sizes? Thanks.

  • Anonymous
    August 18, 2008
    Please give some insight about how to build a day dairy of a doctor you implemented in demonstrator

  • Anonymous
    August 18, 2008
    Hi all, part 3 should be here by the end of the week! samcov - DragDockPanelHost, as it currently is, overrides any set size of the panels to fill the grid cell. Really, in a fuller implementation, the host should look at the panels alignment properties and specified sizes and display them as the user has specified. I havent done that here as I really wanted to focus on the dragging and docking functionality. One way to set a panels size, however, is to set the MaxWidth and MaxHeight rather than the width and height. This will align the panel to the top left of the cell, but keep the specified size. If you extend the host to cope with these scenarios and dont mind sharing, let me know and I can post an update here. Prejesh - I hope to cover at some point how I go about building expanding / collapsing lists (as we see in the Dr's schdeule), and I am sure I can produce a simliar example. Probably wont be for a few weeks however, as there are some other topics I intent to write about first - e.g. drag & drop, customising list boxes etc. Thanks for all the feedback, Martin

  • Anonymous
    August 19, 2008
    Martin, 4 more points... First, I would share anything I do with this.  Secondly, I just had the thought that what you've really created is a pseudo WrapPanel(the host). Third, the MaxWidth works fine, but as I re-thought what I really wanted, it's a bit more complicated.  The goal was to have a "Portal" type layout, a 3 column situation where the middle column is allowed to be wider than the two outer columns.  There are also other layout combinations, but I'll have to think about what I need to accomplish, i.e. not something I'm asking you to implement, just getting it off my chest. Fourth, I tried to place a host inside a host, and it crashed.  I mention this just as an FYI because I can see the design probably isn't geared to do this type of nesting.

  • Anonymous
    August 22, 2008
    Hi Martin I want to create dockable panel, is this possible by using your solution. It means like visual studio editor you can move explorer, Properties or output window and dock them any four side. Plz help me how can i achieve this.

  • Anonymous
    August 25, 2008
    tried to open and build your solutions in VS08.. the page just keeps loading and nothing appears...

  • Anonymous
    August 26, 2008
    Hi brandon, Do you definately have the silverlight tools installed? Goto www.silverlight.net, click getting started at the top, and follow the steps. Let me know if you still have issues. Martin

  • Anonymous
    August 26, 2008
    Hi Shan, If you are looking to build something like visual studio's docking system, this sample probably isnt the best place to start, as it is quite grid centric. It sounds like you are after something more like the WPF DockPanel control. A quick search found this - a controls project that contains a DockPanel... http://www.codeplex.com/SLExtensions I would say that it wouldnt be too hard to build something yourself, particularly if you know how many columns / rows you need, you can use Silverlight's Grid control and place your sub panels in different rows and columns. You can then do some drag and drop by updating your sub panels TranslateTransform... Let me know how you get on! It would be great to see an IDE framework in Silverlight! Martin

  • Anonymous
    August 28, 2008
    Hi Martin, Appreciate for the great samples :) I'm trying to learn your sample. I placed it into my application; but whenever I start the page, all panels are stack at top of each other and it's mazimized! once I click on it, then they get rearranged same as sample. I can't figure out why it does that...however, I have a master page with a container; and I call this page into the container of the master page. I'm suspices that when I load the page, containers are getting mixed up. I'm looking forward to seeing the third part of the sample for maximizing the panels. Do you know when you will post the next sample? great job...thanks a lot Marjan

  • Anonymous
    August 28, 2008
    I was able to fix that problem. The UpdatePanelLayout Event was getting called ahead of Loaded Event....I just placed the UpdatePanelLayout() at the end of the Loaded Event...and it works.... Now....I'm hoping to get the expanding panels sample.... Thanks again for this great samples :) Marjan

  • Anonymous
    August 29, 2008
    In Part 1 , we looked at how we construct a Dragging, docking, expanding panel, and added the ‘dragging’

  • Anonymous
    August 29, 2008
    Thanks Martin.... I'm trying to customize the panels and I would like to place them in tabcontrol...I tried, but getting error at initialization time.... It looks like I'm getting limited with the design! Any Idea? I want to put all the panels into the tabcontrol and still being dockable and expandable....then on each panel having a button and if user click on it....it shows the result in the new tab of the tabcontrol.... Thanks in advance Marjan

  • Anonymous
    August 29, 2008
    Hey there marjantehrani, A collegue of mine had trouble with tabs before too. I think this is because the tab control uses the Visibility property on the content to show / hide it, and when visibility is used, it seems to play about with the layout - e.g. the loaded event will get called for the first time when visibility is set to visible, but we wont get the size changed event, and the size is normally 0 when the loaded event is run. If you could send me a quick sample of your issue (martin.grayson@microsoft.com) I could come up with a workaround. My collegue was playing around with calling UpdatePanelLayout in the loaded event, as you tried, but if the control has no size, then we will need to come up with something else. Let me know, and if you can, please send over a sample. Thanks, Martin

  • Anonymous
    September 02, 2008
    Hi Martin, Thanks for your update....It's great... I have emailed you the sample code for the tabcontrol problem... and yes, you are right, the problem is happening in the DragDockPanelHost.cs file in updatePanelLayout...the this.ActualWidth is 0.0 thanks again Marjan Tehrani

  • Anonymous
    September 02, 2008
    Koen Zwikstra with Silverlight Spy, John Papa on UserControl from Popup Control, Shawn Wildermuth on

  • Anonymous
    September 17, 2008
    Hey marjantehrani, Got your mail. I am taking a look and will get back to you very soon! :)

  • Anonymous
    September 24, 2008
    Thanks Martin, I was able to have datagrid on the panel....and the trick was having the datagrid in the grid layout....if I was placing it in the canvas or stackpanel then it wasn't working properly....such as datagrid scrolling.... I'm trying to add panels dynamically instead of loading them at the loading the page.... The other change I made was having one panel maximized and the others minimized at the very begining when the page is getting loaded....but then this will affect all the pages using the panels.... Thanks again Marjan

  • Anonymous
    September 29, 2008
    The comment has been removed

  • Anonymous
    November 24, 2008
    I want Control Content on Zoom Windows(Maximize) and Minimize

  • Anonymous
    November 24, 2008
    How About .. I Can Control Content Zoom Windows (Maximize) and Minimize Windows

  • Anonymous
    November 25, 2008
    Hi Jacky, A couple of approaches here...

  1. (The easiest) Your content could listen to its SizeChanged event, and change whats visible when it gets to a certain size.
  2. You could inherit DragDropPanel with different panel for each type of content, listen to the events (which should fire as soon as the button is clicked) and change your content. You wont get the minimised event on every panel, only the one where the button is clicked. You will probably need to code around this.
  3. You could add methods to the base class called MaximiseContent / MinimiseContent which each inherited panel overrides. And my favourite...
  4. Have an interface - IDragDockPanelContent - that has 2 methods - MaximiseContent / MinimiseContent - all of your content user controls can implement this, and then your DragDockPanel can call the methods at the appropriate time on its content. Just some ideas, good luck! Martin
  • Anonymous
    May 15, 2009
    Hi, Great tutorial thanks for the effort. I am trying to get the same layout as the Doctor Landing Page of the Primary Care CUI Showcase i.e One panel on the left that spans 2 rows then another 4 or 6 panels to the right each one a sinlge row and column. Where do I set the row span on a dock panel? Cheers, Kevin.

  • Anonymous
    May 15, 2009
    to to extend on the above question, I am using VS2008 and Blend 2 and usign WPF controls not Silverlight

  • Anonymous
    July 07, 2009
    I am using blacklight controls in my project with changes to the colors.  I need to have 6 DragDockPanel out of which first column should have 2, second column with 1 and third column with 3 DragDockPanel panels in the DragDockPanelHost.  Please let me know how i can do this. I don't think the above article would help me, Please let me know your thoughts.

  • Anonymous
    July 08, 2009
    I want to be able to click inside of a DragDockPanel and have it automatically become the active panel with a move and a resize thus emulating clicking the expand button. Is there anyway to do this?

  • Anonymous
    February 13, 2011
    Hi, Using Silverlight 4 and Visual Studio 2010, I am developing a user control with child panels within it. Here are the following requirements: 1.slide feature(primary)-I need to hide such a panel and display only when the user hovers the mouse. 2.dock feature(secondary)-I need to always display such a panel. 3.hide feature(secondary)-I need to hide such a panel for the rest of the session. I am hoping to develop controls like Docking panels in visual studio to hold the Solution Explorer,Class Explorer,Server Explorer, the Controls Toolbox, etc. Please show me the approach to be taken/the controls to be used and the constraints if any with this option. I would to prefer to use the primary Silverlight/.NET framework to any third party controls. Kindly provide me a code sample as well for me to use. thanks Codrenalin(codrenalin@live.com).