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

WPFSpark: 3 of n: FluidWrapPanel

, 19 Jan 2012
Rate this:
Please Sign up or sign in to vote.
A rearrangeable WrapPanel

Introduction

This is the third article in the WPFSpark series. WPFSpark is an Open Source project in CodePlex containing a library of user controls providing rich user experience.

The previous articles in the WPFSpark series can be accessed here:

  1. WPFSpark: 1 of n: SprocketControl
  2. WPFSpark: 2 of n: ToggleSwitch

In this article, I describe in detail the third control in this library which I have developed. It is called the FluidWrapPanel control and derives from Canvas and provides a rich user experience. It derives from Panel and provides the functionality of a WrapPanel with an added advantage - the child elements of the panel can be easily rearranged by simple drag and drop.

Inspiration

FluidWrapPanel is inspired from the cool and elegant iOS UI. Recently, I got a chance to play around with the 4th generation iPod Touch. I was fascinated by the feature provided in the Home Screen which allows you to rearrange the App icons. I wondered if this functionality could be done in WPF.

I wish to thank Roger Peters whose blog provided details about a similar implementation done in Silverlight. It proved very helpful indeed.

Update - WPFSparkv1.1

Thanks to valuable suggestions from the WPF guru, Sacha Barber, I have rewritten the core logic of FluidWrapPanel class from scratch to make it more robust and usable in various scenarios. These changes are breaking changes, meaning if you are using the latest WPFSpark library (v1.1) then your code, using the old FluidWrapPanel, will not compile unless you update it. The interface IFluidDrag has been removed. Child elements no longer need to implement the IFluidDrag interface to participate in the drag and drop interaction. Instead I have added a new Behavior called FluidMouseDragBehavior which would facilitate the child element with drag and drop interaction (more details in the following section). These changes in the FluidWrapPanel code have resulted in a faster, optimized code.

FluidWrapPanel Demystified

FluidWrapPanel is composed of three major components:

  • FluidMouseDragBehavior
  • FluidLayoutManager
  • FluidWrapPanel

FluidMouseDragBehavior

FluidMouseDragBehavior derives from System.Windows.Interactivity.Behavior<T> which is the base class for providing attachable state and commands to an object. This behavior replaces the IFluidDrag interface, thus making it easier for child elements to subscribe to mouse events (down/move/up). This behavior has a Dependency Property called DragButton of type System.Windows.Input.MouseButton which the user can set to indicate which mouse button events must be subscribed to.

namespace System.Windows.Input
{
    // Summary:
    //     Defines values that specify the buttons on a mouse device.
    public enum MouseButton
    {
        // Summary:
        //     The left mouse button.
        Left = 0,
        //
        // Summary:
        //     The middle mouse button.
        Middle = 1,
        //
        // Summary:
        //     The right mouse button.
        Right = 2,
        //
        // Summary:
        //     The first extended mouse button.
        XButton1 = 3,
        //
        // Summary:
        //     The second extended mouse button.
        XButton2 = 4,
    }
}

The default value of DragButton is System.Windows.Input.MouseButton.Left.

Here is the code for FluidMouseDragBehavior:

public class FluidMouseDragBehavior : Behavior<UIElement>
{
    #region Fields

    FluidWrapPanel parentFWPanel = null;
    ListBoxItem parentLBItem = null;

    #endregion

    #region Dependency Properties

    #region DragButton

    /// <summary>
    /// DragButton Dependency Property
    /// </summary>
    public static readonly DependencyProperty DragButtonProperty =
        DependencyProperty.Register("DragButton", typeof(MouseButton), 
        typeof(FluidMouseDragBehavior),
            new FrameworkPropertyMetadata(MouseButton.Left));

    /// <summary>
    /// Gets or sets the DragButton property. This dependency property 
    /// indicates which Mouse button should participate in the drag interaction.
    /// </summary>
    public MouseButton DragButton
    {
        get { return (MouseButton)GetValue(DragButtonProperty); }
        set { SetValue(DragButtonProperty, value); }
    }

    #endregion

    #endregion

    #region Overrides

    /// <summary>
    /// 
    /// </summary>
    protected override void OnAttached()
    {
        // Subscribe to the Loaded event
        (this.AssociatedObject as FrameworkElement).Loaded += 
            new RoutedEventHandler(OnAssociatedObjectLoaded);
    }

    void OnAssociatedObjectLoaded(object sender, RoutedEventArgs e)
    {
        // Get the parent FluidWrapPanel and check if the AssociatedObject is
        // hosted inside a ListBoxItem (this scenario will occur if the FluidWrapPanel
        // is the ItemsPanel for a ListBox).
        GetParentPanel();

        // Subscribe to the Mouse down/move/up events
        if (parentLBItem != null)
        {
            parentLBItem.PreviewMouseDown += 
            new MouseButtonEventHandler(OnPreviewMouseDown);
            parentLBItem.PreviewMouseMove += new MouseEventHandler(OnPreviewMouseMove);
            parentLBItem.PreviewMouseUp += new MouseButtonEventHandler(OnPreviewMouseUp);
        }
        else
        {
            this.AssociatedObject.PreviewMouseDown += 
            new MouseButtonEventHandler(OnPreviewMouseDown);
            this.AssociatedObject.PreviewMouseMove += 
            new MouseEventHandler(OnPreviewMouseMove);
            this.AssociatedObject.PreviewMouseUp += 
            new MouseButtonEventHandler(OnPreviewMouseUp);
        }
    }

    /// <summary>
    /// Get the parent FluidWrapPanel and check if the AssociatedObject is
    /// hosted inside a ListBoxItem (this scenario will occur if the FluidWrapPanel
    /// is the ItemsPanel for a ListBox).
    /// </summary>
    private void GetParentPanel()
    {
        FrameworkElement ancestor = this.AssociatedObject as FrameworkElement;

        while (ancestor != null)
        {
            if (ancestor is ListBoxItem)
            {
                parentLBItem = ancestor as ListBoxItem;
            }

            if (ancestor is FluidWrapPanel)
            {
                parentFWPanel = ancestor as FluidWrapPanel;
                // No need to go further up
                return;
            }

            // Find the visual ancestor of the current item
            ancestor = VisualTreeHelper.GetParent(ancestor) as FrameworkElement;
        }
    }

    protected override void OnDetaching()
    {
        (this.AssociatedObject as FrameworkElement).Loaded -= OnAssociatedObjectLoaded;
        if (parentLBItem != null)
        {
            parentLBItem.PreviewMouseDown -= OnPreviewMouseDown;
            parentLBItem.PreviewMouseMove -= OnPreviewMouseMove;
            parentLBItem.PreviewMouseUp -= OnPreviewMouseUp;
        }
        else
        {
            this.AssociatedObject.PreviewMouseDown -= OnPreviewMouseDown;
            this.AssociatedObject.PreviewMouseMove -= OnPreviewMouseMove;
            this.AssociatedObject.PreviewMouseUp -= OnPreviewMouseUp;
        }
    }

    #endregion

    #region Event Handlers

    void OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
        if (e.ChangedButton == DragButton)
        {
            Point position = parentLBItem != null ? 
        e.GetPosition(parentLBItem) : e.GetPosition(this.AssociatedObject);

            FrameworkElement fElem = this.AssociatedObject as FrameworkElement;
            if ((fElem != null) && (parentFWPanel != null))
            {
                if (parentLBItem != null)
                    parentFWPanel.BeginFluidDrag(parentLBItem, position);
                else
                    parentFWPanel.BeginFluidDrag(this.AssociatedObject, position);
            }
        }
    }

    void OnPreviewMouseMove(object sender, MouseEventArgs e)
    {
        bool isDragging = false;

        switch (DragButton)
        {
            case MouseButton.Left:
                if (e.LeftButton == MouseButtonState.Pressed)
                {
                    isDragging = true;
                }
                break;
            case MouseButton.Middle:
                if (e.MiddleButton == MouseButtonState.Pressed)
                {
                    isDragging = true;
                }
                break;
            case MouseButton.Right:
                if (e.RightButton == MouseButtonState.Pressed)
                {
                    isDragging = true;
                }
                break;
            case MouseButton.XButton1:
                if (e.XButton1 == MouseButtonState.Pressed)
                {
                    isDragging = true;
                }
                break;
            case MouseButton.XButton2:
                if (e.XButton2 == MouseButtonState.Pressed)
                {
                    isDragging = true;
                }
                break;
            default:
                break;
        }

        if (isDragging)
        {
            Point position = parentLBItem != null ? 
        e.GetPosition(parentLBItem) : e.GetPosition(this.AssociatedObject);

            FrameworkElement fElem = this.AssociatedObject as FrameworkElement;
            if ((fElem != null) && (parentFWPanel != null))
            {
                Point positionInParent = e.GetPosition(parentFWPanel);
                if (parentLBItem != null)
                    parentFWPanel.FluidDrag(parentLBItem, position, positionInParent);
                else
                    parentFWPanel.FluidDrag(this.AssociatedObject, 
                    position, positionInParent);
            }
        }
    }

    void OnPreviewMouseUp(object sender, MouseButtonEventArgs e)
    {
        if (e.ChangedButton == DragButton)
        {
            Point position = parentLBItem != null ? 
        e.GetPosition(parentLBItem) : e.GetPosition(this.AssociatedObject);

            FrameworkElement fElem = this.AssociatedObject as FrameworkElement;
            if ((fElem != null) && (parentFWPanel != null))
            {
                Point positionInParent = e.GetPosition(parentFWPanel);
                if (parentLBItem != null)
                    parentFWPanel.EndFluidDrag(parentLBItem, position, positionInParent);
                else
                    parentFWPanel.EndFluidDrag
            (this.AssociatedObject, position, positionInParent);
            }
        }
    }

    #endregion
}

If the FluidWrapPanel is used as an ItemsPanel in a ListBox, then the ListBoxItems are added to the panel's Children collection. So this scenario is handled in this behavior when the child (to which this behavior is attached) is loaded. To achieve this, the VisualTreeHelper class is utilized.

Fluid Layout Manager

The FluidLayoutManager acts as an assistant to the FluidWrapPanel providing various helper methods like calculating the initial position of a child control before the FluidWrapPanel is displayed, getting the position of a child in the FluidWrapPanel during user interaction, calculation the layout transform of the child, and creating the Storyboard for animating the child controls.

internal sealed class FluidLayoutManager
{
  #region Fields

    private Size panelSize;
    private Size cellSize;
    private Orientation panelOrientation;
    private Int32 cellsPerLine;

    #endregion

    #region APIs

    /// <summary>
    /// Calculates the initial location of the child in the FluidWrapPanel
    /// when the child is added.
    /// </summary>
    /// <param name="index">Index of the child in the FluidWrapPanel</param>
    /// <returns></returns>
    internal Point GetInitialLocationOfChild(int index)
    {
        Point result = new Point();

        int row, column;

        GetCellFromIndex(index, out row, out column);

        int maxRows = (Int32)Math.Floor(panelSize.Height / cellSize.Height);
        int maxCols = (Int32)Math.Floor(panelSize.Width / cellSize.Width);

        bool isLeft = true;
        bool isTop = true;
        bool isCenterHeight = false;
        bool isCenterWidth = false;

        int halfRows = 0;
        int halfCols = 0;

        halfRows = (int)((double)maxRows / (double)2);

        // Even number of rows
        if ((maxRows % 2) == 0)
        {
            isTop = row < halfRows;
        }
        // Odd number of rows
        else
        {
            if (row == halfRows)
            {
                isCenterHeight = true;
                isTop = false;
            }
            else
            {
                isTop = row < halfRows;
            }
        }

        halfCols = (int)((double)maxCols / (double)2);

        // Even number of columns
        if ((maxCols % 2) == 0)
        {
            isLeft = column < halfCols;
        }
        // Odd number of columns
        else
        {
            if (column == halfCols)
            {
                isCenterWidth = true;
                isLeft = false;
            }
            else
            {
                isLeft = column < halfCols;
            }
        }

        if (isCenterHeight && isCenterWidth)
        {
            double posX = (halfCols) * cellSize.Width;
            double posY = (halfRows + 2) * cellSize.Height;

            return new Point(posX, posY);
        }

        if (isCenterHeight)
        {
            if (isLeft)
            {
                double posX = ((halfCols - column) + 1) * cellSize.Width;
                double posY = (halfRows) * cellSize.Height;

                result = new Point(-posX, posY);
            }
            else
            {
                double posX = ((column - halfCols) + 1) * cellSize.Width;
                double posY = (halfRows) * cellSize.Height;

                result = new Point(panelSize.Width + posX, posY);
            }

            return result;
        }

        if (isCenterWidth)
        {
            if (isTop)
            {
                double posX = (halfCols) * cellSize.Width;
                double posY = ((halfRows - row) + 1) * cellSize.Height;

                result = new Point(posX, -posY);
            }
            else
            {
                double posX = (halfCols) * cellSize.Width;
                double posY = ((row - halfRows) + 1) * cellSize.Height;

                result = new Point(posX, panelSize.Height + posY);
            }

            return result;
        }

        if (isTop)
        {
            if (isLeft)
            {
                double posX = ((halfCols - column) + 1) * cellSize.Width;
                double posY = ((halfRows - row) + 1) * cellSize.Height;

                result = new Point(-posX, -posY);
            }
            else
            {
                double posX = ((column - halfCols) + 1) * cellSize.Width;
                double posY = ((halfRows - row) + 1) * cellSize.Height;

                result = new Point(posX + panelSize.Width, -posY);
            }
        }
        else
        {
            if (isLeft)
            {
                double posX = ((halfCols - column) + 1) * cellSize.Width;
                double posY = ((row - halfRows) + 1) * cellSize.Height;

                result = new Point(-posX, panelSize.Height + posY);
            }
            else
            {
                double posX = ((column - halfCols) + 1) * cellSize.Width;
                double posY = ((row - halfRows) + 1) * cellSize.Height;

                result = new Point(posX + panelSize.Width, panelSize.Height + posY);
            }
        }

        return result;
    }

    /// <summary>
    /// Initializes the FluidLayoutManager
    /// </summary>
    /// <param name="panelWidth">Width of the FluidWrapPanel</param>
    /// <param name="panelHeight">Height of the FluidWrapPanel</param>
    /// <param name="cellWidth">Width of each child in the FluidWrapPanel</param>
    /// <param name="cellHeight">Height of each child in the FluidWrapPanel</param>
    /// <param name="orientation">Orientation of the panel - Horizontal or Vertical</param>
    internal void Initialize(double panelWidth, double panelHeight, 
        double cellWidth, double cellHeight, Orientation orientation)
    {
        if (panelWidth <= 0.0d)
            panelWidth = cellWidth;
        if (panelHeight <= 0.0d)
            panelHeight = cellHeight;
        if ((cellWidth <= 0.0d) || (cellHeight <= 0.0d))
        {
            cellsPerLine = 0;
            return;
        }

        if ((panelSize.Width != panelWidth) ||
            (panelSize.Height != panelHeight) ||
            (cellSize.Width != cellWidth) ||
            (cellSize.Height != cellHeight))
        {
            panelSize = new Size(panelWidth, panelHeight);
            cellSize = new Size(cellWidth, cellHeight);
            panelOrientation = orientation;

            // Calculate the number of cells that can be fit in a line
            CalculateCellsPerLine();
        }
    }

    /// <summary>
    /// Provides the index of the child (in the FluidWrapPanel's children) 
    /// from the given row and column
    /// </summary>
    /// <param name="row">Row</param>
    /// <param name="column">Column</param>
    /// <returns>Index</returns>
    internal int GetIndexFromCell(int row, int column)
    {
        int result = -1;

        if ((row >= 0) && (column >= 0))
        {
            switch (panelOrientation)
            {
                case Orientation.Horizontal:
                    result = (cellsPerLine * row) + column;
                    break;
                case Orientation.Vertical:
                    result = (cellsPerLine * column) + row;
                    break;
                default:
                    break;
            }
        }

        return result;
    }

    /// <summary>
    /// Provides the index of the child (in the FluidWrapPanel's children) 
    /// from the given point
    /// </summary>
    /// <param name="p"></param>
    /// <returns></returns>
    internal int GetIndexFromPoint(Point p)
    {
        int result = -1;
        if ((p.X > 0.00D) &&
            (p.X < panelSize.Width) &&
            (p.Y > 0.00D) &&
            (p.Y < panelSize.Height))
        {
            int row;
            int column;

            GetCellFromPoint(p, out row, out column);
            result = GetIndexFromCell(row, column);
        }

        return result;
    }

    /// <summary>
    /// Provides the row and column of the child based on its index in the 
    /// FluidWrapPanel.Children
    /// </summary>
    /// <param name="index">Index</param>
    /// <param name="row">Row</param>
    /// <param name="column">Column</param>
    internal void GetCellFromIndex(int index, out int row, out int column)
    {
        row = column = -1;

        if (index >= 0)
        {
            switch (panelOrientation)
            {
                case Orientation.Horizontal:
                    row = (int)(index / (double)cellsPerLine);
                    column = (int)(index % (double)cellsPerLine);
                    break;
                case Orientation.Vertical:
                    column = (int)(index / (double)cellsPerLine);
                    row = (int)(index % (double)cellsPerLine);
                    break;
                default:
                    break;
            }
        }
    }

    /// <summary>
    /// Provides the row and column of the child based on its 
    /// location in the FluidWrapPanel
    /// </summary>
    /// <param name="p">Location of the child in the parentFWPanel</param>
    /// <param name="row">Row</param>
    /// <param name="column">Column</param>
    internal void GetCellFromPoint(Point p, out int row, out int column)
    {
        row = column = -1;

        if ((p.X < 0.00D) ||
            (p.X > panelSize.Width) ||
            (p.Y < 0.00D) ||
            (p.Y > panelSize.Height))
        {
            return;
        }

        row = (int)(p.Y / cellSize.Height);
        column = (int)(p.X / cellSize.Width);
    }

    /// <summary>
    /// Provides the location of the child in the FluidWrapPanel 
    /// based on the given row and column
    /// </summary>
    /// <param name="row">Row</param>
    /// <param name="column">Column</param>
    /// <returns>Location of the child in the panel</returns>
    internal Point GetPointFromCell(int row, int column)
    {
        Point result = new Point();

        if ((row >= 0) && (column >= 0))
        {
            result = new Point(cellSize.Width * column, cellSize.Height * row);
        }

        return result;
    }

    /// <summary>
    /// Provides the location of the child in the FluidWrapPanel 
    /// based on the given row and column
    /// </summary>
    /// <param name="index">Index</param>
    /// <returns>Location of the child in the panel</returns>
    internal Point GetPointFromIndex(int index)
    {
        Point result = new Point();

        if (index >= 0)
        {
            int row;
            int column;

            GetCellFromIndex(index, out row, out column);
            result = GetPointFromCell(row, column);
        }

        return result;
    }

    /// <summary>
    /// Creates a TransformGroup based on the given Translation, Scale and Rotation
    /// </summary>
    /// <param name="transX">Translation in the X-axis</param>
    /// <param name="transY">Translation in the Y-axis</param>
    /// <param name="scaleX">Scale factor in the X-axis</param>
    /// <param name="scaleY">Scale factor in the Y-axis</param>
    /// <param name="rotAngle">Rotation</param>
    /// <returns>TransformGroup</returns>
    internal TransformGroup CreateTransform(double transX, double transY, 
            double scaleX, double scaleY, double rotAngle = 0.0D)
    {
        TranslateTransform translation = new TranslateTransform();
        translation.X = transX;
        translation.Y = transY;

        ScaleTransform scale = new ScaleTransform();
        scale.ScaleX = scaleX;
        scale.ScaleY = scaleY;

        //RotateTransform rotation = new RotateTransform();
        //rotation.Angle = rotAngle;

        TransformGroup transform = new TransformGroup();
        // THE ORDER OF TRANSFORM IS IMPORTANT
        // First, scale, then rotate and finally translate
        transform.Children.Add(scale);
        //transform.Children.Add(rotation);
        transform.Children.Add(translation);

        return transform;
    }

    /// <summary>
    /// Creates the storyboard for animating a child from its 
    /// old location to the new location.
    /// The Translation and Scale properties are animated.
    /// </summary>
    /// <param name="child">UIElement for which the storyboard has to be created</param>
    /// <param name="newLocation">New location of the UIElement</param>
    /// <param name="period">Duration of animation</param>
    /// <param name="easing">Easing function</param>
    /// <returns>Storyboard</returns>
    internal Storyboard CreateTransition(UIElement element, 
    Point newLocation, TimeSpan period, EasingFunctionBase easing)
    {
        Duration duration = new Duration(period);

        // Animate X
        DoubleAnimation translateAnimationX = new DoubleAnimation();
        translateAnimationX.To = newLocation.X;
        translateAnimationX.Duration = duration;
        if (easing != null)
            translateAnimationX.EasingFunction = easing;

        Storyboard.SetTarget(translateAnimationX, element);
        Storyboard.SetTargetProperty(translateAnimationX,
            new PropertyPath("(UIElement.RenderTransform).
        (TransformGroup.Children)[1].(TranslateTransform.X)"));

        // Animate Y
        DoubleAnimation translateAnimationY = new DoubleAnimation();
        translateAnimationY.To = newLocation.Y;
        translateAnimationY.Duration = duration;
        if (easing != null)
            translateAnimationY.EasingFunction = easing;

        Storyboard.SetTarget(translateAnimationY, element);
        Storyboard.SetTargetProperty(translateAnimationY,
            new PropertyPath("(UIElement.RenderTransform).
        (TransformGroup.Children)[1].(TranslateTransform.Y)"));

        // Animate ScaleX
        DoubleAnimation scaleAnimationX = new DoubleAnimation();
        scaleAnimationX.To = 1.0D;
        scaleAnimationX.Duration = duration;
        if (easing != null)
            scaleAnimationX.EasingFunction = easing;

        Storyboard.SetTarget(scaleAnimationX, element);
        Storyboard.SetTargetProperty(scaleAnimationX,
            new PropertyPath("(UIElement.RenderTransform).
        (TransformGroup.Children)[0].(ScaleTransform.ScaleX)"));

        // Animate ScaleY
        DoubleAnimation scaleAnimationY = new DoubleAnimation();
        scaleAnimationY.To = 1.0D;
        scaleAnimationY.Duration = duration;
        if (easing != null)
            scaleAnimationY.EasingFunction = easing;

        Storyboard.SetTarget(scaleAnimationY, element);
        Storyboard.SetTargetProperty(scaleAnimationY,
            new PropertyPath("(UIElement.RenderTransform).
        (TransformGroup.Children)[0].(ScaleTransform.ScaleY)"));

        Storyboard sb = new Storyboard();
        sb.Duration = duration;
        sb.Children.Add(translateAnimationX);
        sb.Children.Add(translateAnimationY);
        sb.Children.Add(scaleAnimationX);
        sb.Children.Add(scaleAnimationY);

        return sb;
    }

    /// <summary>
    /// Gets the total size taken up by the children after the Arrange Layout Phase
    /// </summary>
    /// <param name="childrenCount">Number of children</param>
    /// <param name="finalSize">Available size provided by the FluidWrapPanel</param>
    /// <returns>Total size</returns>
    internal Size GetArrangedSize(int childrenCount, Size finalSize)
    {
        if ((cellsPerLine == 0.0) || (childrenCount == 0))
            return finalSize;

        int numLines = (Int32)(childrenCount / (double)cellsPerLine);
        int modLines = childrenCount % cellsPerLine;
        if (modLines > 0)
            numLines++;

        if (panelOrientation == Orientation.Horizontal)
        {
            return new Size(cellsPerLine * cellSize.Width, numLines * cellSize.Height);
        }

        return new Size(numLines * cellSize.Width, cellsPerLine * cellSize.Height);
    }

    #endregion

    #region Helpers

    /// <summary>
    /// Calculates the number of child items that can be accommodated in a single line
    /// </summary>
    private void CalculateCellsPerLine()
    {
        double count = (panelOrientation == Orientation.Horizontal) ? 
                    panelSize.Width / cellSize.Width :
                                               panelSize.Height / cellSize.Height;
        cellsPerLine = (Int32)Math.Floor(count);
        if ((1.0D + cellsPerLine - count) < Double.Epsilon)
            cellsPerLine++;
    }

    #endregion        
}

FluidWrapPanel

FluidWrapPanel is the main component which uses the LayoutManager to provide the desired functionality. It derives from Panel.

Before a FluidWrapPanel is displayed for the first time, its children are arranged around it (outside the panel). It calculates the location of each child using the FluidLayoutManager's GetInitialLocationOfChild method. Just after the FluidWrapPanel is loaded, the children are transitioned to their respective location within the panel using animation. FluidLayoutManager's CreateTransition method provides the Storyboard for the animation.

Even though FluidWrapPanel inherits the Children property (of type UIElementCollection) of Panel, it maintains another collection of its own called fluidElements. This collection is used when the children are rearranged by user interaction.

public class FluidWrapPanel : Panel
{
    #region Constants

    private const double NORMAL_SCALE = 1.0d;
    private const double DRAG_SCALE_DEFAULT = 1.3d;
    private const double NORMAL_OPACITY = 1.0d;
    private const double DRAG_OPACITY_DEFAULT = 0.6d;
    private const double OPACITY_MIN = 0.1d;
    private const Int32 Z_INDEX_NORMAL = 0;
    private const Int32 Z_INDEX_INTERMEDIATE = 1;
    private const Int32 Z_INDEX_DRAG = 10;
    private static TimeSpan DEFAULT_ANIMATION_TIME_WITHOUT_EASING = 
                    TimeSpan.FromMilliseconds(200);
    private static TimeSpan DEFAULT_ANIMATION_TIME_WITH_EASING = 
                    TimeSpan.FromMilliseconds(400);
    private static TimeSpan FIRST_TIME_ANIMATION_DURATION = TimeSpan.FromMilliseconds(320);

    #endregion

    #region Fields

    Point dragStartPoint = new Point();
    UIElement dragElement = null;
    UIElement lastDragElement = null;
    List<UIElement> fluidElements = null;
    FluidLayoutManager layoutManager = null;
    bool isInitializeArrangeRequired = false;

    #endregion

    #region Dependency Properties

    ...

    #endregion

    #region Overrides

    /// <summary>
    /// Override for the Measure Layout Phase
    /// </summary>
    /// <param name="availableSize">Available Size</param>
    /// <returns>Size required by the panel</returns>
    protected override Size MeasureOverride(Size availableSize)
    {
        Size availableItemSize = new Size
        (Double.PositiveInfinity, Double.PositiveInfinity);
        double rowWidth = 0.0;
        double maxRowHeight = 0.0;
        double colHeight = 0.0;
        double maxColWidth = 0.0;
        double totalColumnWidth = 0.0;
        double totalRowHeight = 0.0;

        // Iterate through all the UIElements in the Children collection
        for (int i = 0; i < InternalChildren.Count; i++)
        {
            UIElement child = InternalChildren[i];
            if (child != null)
            {
                // Ask the child how much size it needs
                child.Measure(availableItemSize);
                // Check if the child is already added to the fluidElements collection
                if (!fluidElements.Contains(child))
                {
                    AddChildToFluidElements(child);
                }

                if (this.Orientation == Orientation.Horizontal)
                {
                    // Will the child fit in the current row?
                    if (rowWidth + child.DesiredSize.Width > availableSize.Width)
                    {
                        // Wrap to next row
                        totalRowHeight += maxRowHeight;

                        // Is the current row width greater than the previous row widths
                        if (rowWidth > totalColumnWidth)
                            totalColumnWidth = rowWidth;

                        rowWidth = 0.0;
                        maxRowHeight = 0.0;
                    }

                    rowWidth += child.DesiredSize.Width;
                    if (child.DesiredSize.Height > maxRowHeight)
                        maxRowHeight = child.DesiredSize.Height;
                }
                else // Vertical Orientation
                {
                    // Will the child fit in the current column?
                    if (colHeight + child.DesiredSize.Height > availableSize.Height)
                    {
                        // Wrap to next column
                        totalColumnWidth += maxColWidth;

                        // Is the current column height greater 
                        // than the previous column heights
                        if (colHeight > totalRowHeight)
                            totalRowHeight = colHeight;

                        colHeight = 0.0;
                        maxColWidth = 0.0;
                    }

                    colHeight += child.DesiredSize.Height;
                    if (child.DesiredSize.Width > maxColWidth)
                        maxColWidth = child.DesiredSize.Width;
                }
            }
        }

        if (this.Orientation == Orientation.Horizontal)
        {
            // Add the height of the last row
            totalRowHeight += maxRowHeight;
            // If there is only one row, take its width as the total width
            if (totalColumnWidth == 0.0)
            {
                totalColumnWidth = rowWidth;
            }
        }
        else
        {
            // Add the width of the last column
            totalColumnWidth += maxColWidth;
            // If there is only one column, take its height as the total height
            if (totalRowHeight == 0.0)
            {
                totalRowHeight = colHeight;
            }
        }

        Size resultSize = new Size(totalColumnWidth, totalRowHeight);

        return resultSize;
    }

    /// <summary>
    /// Override for the Arrange Layout Phase
    /// </summary>
    /// <param name="finalSize">Available size provided by the FluidWrapPanel</param>
    /// <returns>Size taken up by the Panel</returns>
    protected override Size ArrangeOverride(Size finalSize)
    {
        if (layoutManager == null)
            layoutManager = new FluidLayoutManager();

        // Initialize the LayoutManager
        layoutManager.Initialize(finalSize.Width, finalSize.Height, 
                ItemWidth, ItemHeight, Orientation);

        bool isEasingRequired = !isInitializeArrangeRequired;

        // If the children are newly added, then set their initial 
        // location before the panel loads
        if ((isInitializeArrangeRequired) && (this.Children.Count > 0))
        {
            InitializeArrange();
            isInitializeArrangeRequired = false;
        }

        // Update the Layout
        UpdateFluidLayout(isEasingRequired);

        // Return the size taken up by the Panel's Children
        return layoutManager.GetArrangedSize(fluidElements.Count, finalSize);
    }

    #endregion

    #region Construction / Initialization

    /// <summary>
    /// Ctor
    /// </summary>
    public FluidWrapPanel()
    {
        fluidElements = new List<UIElement>();
        layoutManager = new FluidLayoutManager();
        isInitializeArrangeRequired = true;
    }

    #endregion

    #region Helpers

    /// <summary>
    /// Adds the child to the fluidElements collection and 
    /// initializes its RenderTransform.
    /// </summary>
    /// <param name="child">UIElement</param>
    private void AddChildToFluidElements(UIElement child)
    {
        // Add the child to the fluidElements collection
        fluidElements.Add(child);
        // Initialize its RenderTransform
        child.RenderTransform = layoutManager.CreateTransform
        (-ItemWidth, -ItemHeight, NORMAL_SCALE, NORMAL_SCALE);
    }

    /// <summary>
    /// Intializes the arrangement of the children
    /// </summary>
    private void InitializeArrange()
    {
        foreach (UIElement child in fluidElements)
        {
            // Get the child's index in the fluidElements
            int index = fluidElements.IndexOf(child);

            // Get the initial location of the child
            Point pos = layoutManager.GetInitialLocationOfChild(index);

            // Initialize the appropriate Render Transform for the child
            child.RenderTransform = layoutManager.CreateTransform
                (pos.X, pos.Y, NORMAL_SCALE, NORMAL_SCALE);
        }
    }

    /// <summary>
    /// Iterates through all the fluid elements and animate their
    /// movement to their new location.
    /// </summary>
    private void UpdateFluidLayout(bool showEasing = true)
    {
        // Iterate through all the fluid elements and animate their
        // movement to their new location.
        for (int index = 0; index < fluidElements.Count; index++)
        {
            UIElement element = fluidElements[index];
            if (element == null)
                continue;

            // If an child is currently being dragged, then no need to animate it
            if (dragElement != null && index == fluidElements.IndexOf(dragElement))
                continue;

            element.Arrange(new Rect(0, 0, element.DesiredSize.Width,
                  element.DesiredSize.Height));

            // Get the cell position of the current index
            Point pos = layoutManager.GetPointFromIndex(index);

            Storyboard transition;
            // Is the child being animated the same as the child which was last dragged?
            if (element == lastDragElement)
            {
                if (!showEasing)
                {
                    // Create the Storyboard for the transition
                    transition = layoutManager.CreateTransition
            (element, pos, FIRST_TIME_ANIMATION_DURATION, null);
                }
                else
                {
                    // Is easing function specified for the animation?
                    TimeSpan duration = (DragEasing != null) ? 
            DEFAULT_ANIMATION_TIME_WITH_EASING : 
            DEFAULT_ANIMATION_TIME_WITHOUT_EASING;
                    // Create the Storyboard for the transition
                    transition = layoutManager.CreateTransition
                (element, pos, duration, DragEasing);
                }

                // When the user releases the drag child, it's Z-Index is set to 1 so that 
                // during the animation it does not go below other elements.
                // After the animation has completed set its Z-Index to 0
                transition.Completed += (s, e) =>
                {
                    if (lastDragElement != null)
                    {
                        lastDragElement.SetValue(Canvas.ZIndexProperty, 0);
                        lastDragElement = null;
                    }
                };
            }
            else // It is a non-dragElement
            {
                if (!showEasing)
                {
                    // Create the Storyboard for the transition
                    transition = layoutManager.CreateTransition
            (element, pos, FIRST_TIME_ANIMATION_DURATION, null);
                }
                else
                {
                    // Is easing function specified for the animation?
                    TimeSpan duration = (ElementEasing != null) ? 
            DEFAULT_ANIMATION_TIME_WITH_EASING : 
            DEFAULT_ANIMATION_TIME_WITHOUT_EASING;
                    // Create the Storyboard for the transition
                    transition = layoutManager.CreateTransition
            (element, pos, duration, ElementEasing);
                }
            }

            // Start the animation
            transition.Begin();
        }
    }

    /// <summary>
    /// Moves the dragElement to the new Index
    /// </summary>
    /// <param name="newIndex">Index of the new location</param>
    /// <returns>True-if dragElement was moved otherwise False</returns>
    private bool UpdateDragElementIndex(int newIndex)
    {
        // Check if the dragElement is being moved to its current place
        // If yes, then no need to proceed further. (Improves efficiency!)
        int dragCellIndex = fluidElements.IndexOf(dragElement);
        if (dragCellIndex == newIndex)
            return false;

        fluidElements.RemoveAt(dragCellIndex);
        fluidElements.Insert(newIndex, dragElement);

        return true;
    }

    /// <summary>
    /// Removes all the children from the FluidWrapPanel
    /// </summary>
    private void ClearItemsSource()
    {
        fluidElements.Clear();
        Children.Clear();
    }

    #endregion

    #region FluidDrag Event Handlers

    /// <summary>
    /// Handler for the event when the user starts dragging the dragElement.
    /// </summary>
    /// <param name="child">UIElement being dragged</param>
    /// <param name="position">Position in the child where the user clicked</param>
    internal void BeginFluidDrag(UIElement child, Point position)
    {
        if ((child == null) || (!IsComposing))
            return;

        // Call the event handler core on the Dispatcher. (Improves efficiency!)
        Dispatcher.BeginInvoke(new Action(() =>
        {
            child.Opacity = DragOpacity;
            child.SetValue(Canvas.ZIndexProperty, Z_INDEX_DRAG);
            // Capture further mouse events
            child.CaptureMouse();
            dragElement = child;
            lastDragElement = null;

            // Since we are scaling the dragElement by DragScale, 
            // the clickPoint also shifts
            dragStartPoint = new Point(position.X * DragScale, position.Y * DragScale);
        }));
    }

    /// <summary>
    /// Handler for the event when the user drags the dragElement.
    /// </summary>
    /// <param name="child">UIElement being dragged</param>
    /// <param name="position">Position where the user clicked 
    /// w.r.t. the UIElement being dragged</param>
    /// <param name="positionInParent">Position where the user clicked 
    /// w.r.t. the FluidWrapPanel (the parentFWPanel of the UIElement being dragged</param>
    internal void FluidDrag(UIElement child, Point position, Point positionInParent)
    {
        if ((child == null) || (!IsComposing))
            return;

        // Call the event handler core on the Dispatcher. (Improves efficiency!)
        Dispatcher.BeginInvoke(new Action(() =>
        {
            if ((dragElement != null) && (layoutManager != null))
            {
                dragElement.RenderTransform = 
        layoutManager.CreateTransform(positionInParent.X - dragStartPoint.X,
                                                positionInParent.Y - dragStartPoint.Y,
                                                DragScale,
                                                DragScale);

                // Get the index in the fluidElements list corresponding 
                // to the current mouse location
                Point currentPt = positionInParent;
                int index = layoutManager.GetIndexFromPoint(currentPt);

                // If no valid cell index is obtained, add the child to the end of the 
                // fluidElements list.
                if ((index == -1) || (index >= fluidElements.Count))
                {
                    index = fluidElements.Count - 1;
                }

                // If the dragElement is moved to a new location, then only
                // call the updation of the layout.
                if (UpdateDragElementIndex(index))
                {
                    UpdateFluidLayout();
                }
            }
        }));
    }

    /// <summary>
    /// Handler for the event when the user stops dragging the dragElement and releases it.
    /// </summary>
    /// <param name="child">UIElement being dragged</param>
    /// <param name="position">Position where the user clicked w.r.t. 
    /// the UIElement being dragged</param>
    /// <param name="positionInParent">Position where the user clicked 
    /// w.r.t. the FluidWrapPanel (the parentFWPanel of the UIElement being dragged</param>
    internal void EndFluidDrag(UIElement child, Point position, Point positionInParent)
    {
        if ((child == null) || (!IsComposing))
            return;

        // Call the event handler core on the Dispatcher. (Improves efficiency!)
        Dispatcher.BeginInvoke(new Action(() =>
        {
            if ((dragElement != null) && (layoutManager != null))
            {
                dragElement.RenderTransform = 
        layoutManager.CreateTransform(positionInParent.X - dragStartPoint.X,
                                                positionInParent.Y - dragStartPoint.Y,
                                                DragScale,
                                                DragScale);

                child.Opacity = NORMAL_OPACITY;
                // Z-Index is set to 1 so that during the animation it does 
                // not go below other elements.
                child.SetValue(Canvas.ZIndexProperty, Z_INDEX_INTERMEDIATE);
                // Release the mouse capture
                child.ReleaseMouseCapture();

                // Reference used to set the Z-Index to 0 during the UpdateFluidLayout
                lastDragElement = dragElement;

                dragElement = null;
            }

            UpdateFluidLayout();
        }));
    }

    #endregion
}

FluidWrapPanel Properties

Dependency Property Type Description Default Value
DragEasing EasingFunction Gets or sets the Easing function to be used, to animate the element, when the user stops dragging and releases the element. null
DragOpacity Double Gets or sets the Opacity of the element when it is being dragged by the user. Range: 0.1D - 1.0D inclusive. 0.6D
DragScale Double Gets or sets the Scale Factor of the element when it is being dragged by the user. 1.3D
ElementEasing EasingFunction Gets or sets the Easing function to be used, to animate the elements in the FluidWrapPanel, when they are rearranged. null
IsEditable Boolean Flag to indicate whether the children in the FluidWrapPanel can be rearranged or not. False
ItemHeight Double Gets or sets the Height to be allotted for each child in the FluidWrapPanel. 0
ItemsSource IEnumerable Bindable property to which a collection can be bound. null
ItemWidth Double Gets or sets the Width to be allotted for each child in the FluidWrapPanel. 0
Orientation System.Windows.Controls.Orientation Gets or sets the different orientations the FluidWrapPanel can have. Possible values are Horizontal and Vertical. Horizontal

EndPoint

To participate in the Fluid Drag interactions, the child can add the FluidMouseDragBehavior either through XAML or through code. For this purpose, reference to System.Windows.Interactivity must be added.

Adding behavior through XAML

<UserControl x:Class="WPFSparkClient.ImageIcon"
             ...
             xmlns:i="clr-namespace:System.Windows.Interactivity;
            assembly=System.Windows.Interactivity"
             xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
             xmlns:wpfspark="clr-namespace:WPFSpark;assembly=WPFSpark">
    <i:Interaction.Behaviors>
        <wpfspark:FluidMouseDragBehavior DragButton="Left">
            </wpfspark:FluidMouseDragBehavior>
    </i:Interaction.Behaviors>
    <Grid>
        ...
    </Grid>
</UserControl>

Adding behavior through code

using System.Windows.Interactivity;

public class AppButton : Button
{
    public AppButton() : base()
    {
        var behaviors = Interaction.GetBehaviors(this);
        behaviors.Add(new FluidMouseDragBehavior { DragButton = MouseButton.Right });
    }
}

FluidWrapPanel references the assemblies Microsoft.Expression.Interactions and System.Windows.Interactivity. If you have Expression Blend installed on your machine, then these DLLs will be available in the GAC. Else you can just install the Expression Blend SDK (available here) which will provide you with the required DLLs.

History

  • January 19, 2012: WPFSpark v1.1 released
  • December 21, 2011: WPFSpark v1.0 released
  • August 23, 2011: WPFSpark v0.7 released

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Share

About the Author

Ratish Philip
Software Developer NA
India India
Ratish Philip is a software developer with 8 years of experience. He loves programming in C#, WPF & Silverlight.
 
He is currently exploring the depths of Windows 8 programming.
 
Creating enriched user experiences is what appeals to him the most.
 
Occasionally expresses his creativity through pencil sketching too!
 
Ratish's personal blog: wpfspark.wordpress.com

Comments and Discussions

 
QuestionItemHeight Pinmemberdamylen2-Jan-14 7:04 
AnswerRe: ItemHeight PinmemberRatish Philip6-Jan-14 2:52 
QuestionMultiple FluidWrapPanels in a Stack Panel? [modified] PinmemberJoshua McKearin6-Aug-13 7:46 
AnswerRe: Multiple FluidWrapPanels in a Stack Panel? PinmemberRatish Philip8-Aug-13 2:27 
GeneralMy vote of 5 PinmemberiJam_j6-Apr-13 14:24 
GeneralAgain Awesome!!!! Pinmemberraananv18-Sep-12 0:49 
GeneralRe: Again Awesome!!!! PinmemberRatish Philip18-Oct-12 17:23 
QuestionCannot click an item inside FluidWrapPanel with behavior of "DragButton=Left" Pinmemberericis28-Feb-12 11:58 
AnswerRe: Cannot click an item inside FluidWrapPanel with behavior of "DragButton=Left" PinmemberRatish Philip4-Mar-12 21:19 
GeneralMy vote of 5 PinmemberWESTSEYI3-Feb-12 23:58 

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
Web04 | 2.8.140921.1 | Last Updated 19 Jan 2012
Article Copyright 2011 by Ratish Philip
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid