Click here to Skip to main content
Click here to Skip to main content

WrapPanel for Silverlight 2.0

By , 6 Mar 2008
 

Introduction

One of the most exciting features of Silverlight 2.0 (beta 1) is the ability to create custom panels - just like WPF! Silverlight 2.0 (beta 1) comes with three panels: Canvas, Grid, and StackPanel. Many people will miss other popular panels already included with WPF. This article shows how to build a simple WrapPanel using the extensibility features available with Silverlight 2.0.

Background

A custom panel in Silverlight (and WPF) contains two important parts: Measure and Arrange. This is a two-pass recursive system. The first round measures the size of all children in the panel. In a recursive manner, all children in turn measure the size of their children, and so on. In the next round, the children of the panel are arranged using whatever algorithm you like.

For a complete description of building a custom panel, see this MSDN article.

Using the code

The first important aspect of the custom WrapPanel is to inherit from Panel. Panel is an abstract class from which all panels must derive. The Orientation dependency property determines whether WrapPanel flows vertically or horizontally.

public class WrapPanel : Panel
{

  public Orientation Orientation
  { 
    get { return (Orientation)GetValue(OrientationProperty); }
    set { SetValue(OrientationProperty, value); }
   }

   public static readonly DependencyProperty OrientationProperty =
     DependencyProperty.Register("Orientation", 
     typeof(Orientation), typeof(WrapPanel), null);

 public WrapPanel()
 {
   // default orientation
   Orientation = Orientation.Horizontal;
 }

Next, we must override the Measure function. The input parameter 'availableableSize' to MeasureOverride tells the panel how much size it has to work with. This size is given to the panel by its parent. The most important aspect in measuring each child is to indicate to each child their allowed size. In this simple example, we are just saying to each child that they can have the whole space of the panel. The result of the measuring yields a 'DesiredSize' for each child - this desired size will be used when arranging the children.

protected override Size MeasureOverride(Size availableSize)
{
  foreach (UIElement child in Children)
  {
    child.Measure(new Size(availableSize.Width, availableSize.Height)); 
  }

  return base.MeasureOverride(availableSize);
 }

Finally, we must override the Arrange method. Here, we will position each child in the panel. In our case, we position items either horizontally or vertically. In the horizontal case, we arrange the children (left to right) until the right edge of the panel is reached; then, we move to the next line and continue laying out the children (left to right).

protected override Size ArrangeOverride(Size finalSize)
{
  Point point = new Point(0,0);
  int i = 0;

  if (Orientation == Orientation.Horizontal)
  {
    double largestHeight = 0.0;

    foreach (UIElement child in Children)
    {
      child.Arrange(new Rect(point, new Point(point.X + 
        child.DesiredSize.Width, point.Y + child.DesiredSize.Height)));

      if (child.DesiredSize.Height > largestHeight)
         largestHeight = child.DesiredSize.Height;

      point.X = point.X + child.DesiredSize.Width;

      if ((i + 1) < Children.Count)
      {
        if ((point.X + Children[i + 1].DesiredSize.Width) > finalSize.Width)
        {
           point.X = 0;
           point.Y = point.Y + largestHeight;
           largestHeight = 0.0;
         }
       }
       i++;
     }
    }
    else
    {
       double largestWidth = 0.0;

       foreach (UIElement child in Children)
       {
         child.Arrange(new Rect(point, new Point(point.X + 
           child.DesiredSize.Width, point.Y + child.DesiredSize.Height)));

         if (child.DesiredSize.Width > largestWidth)
           largestWidth = child.DesiredSize.Width;

          point.Y = point.Y + child.DesiredSize.Height;

          if ((i + 1) < Children.Count)
          {
            if ((point.Y + Children[i + 1].DesiredSize.Height) > finalSize.Height)
            {
              point.Y = 0;
              point.X = point.X + largestWidth;
              largestWidth = 0.0;
             }
           }

           i++;
          }
        }

    return base.ArrangeOverride(finalSize);
   }
 }
}

Points of interest

Note, the WrapPanel provided here functions mostly like the WPF WrapPanel. I noticed some minor differences, but have not had time to fix. Feel free to send along your suggested changes.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

lneir
Software Developer (Senior) Skype
United States United States
Member
I work in the Bay Area primarily developing software on the Windows platform using C++, .NET/C#, WPF, and Silverlight.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5memberlewax002 Feb '12 - 8:36 
This is just what I needed, thanks! (After the modification to work in a ScrollViewer anyways)
GeneralGreat silverlight controlmemberMichał Zalewski31 Oct '09 - 15:25 
Thank you for this control. It's great especially for newbie in silverlight like me Smile | :)
Great work!
GeneralSilverlightContribmemberrob_houweling26 Oct '08 - 0:11 
Hi Ineir,
 
Great work on the WrapPanel. I am working on the SilverlightContrib project. This is an open source Silverlight control library (http://www.silverlightcontrib.org). Like Member 2952139 I would like to use your WrapPanel as a base for a WrapPanel control in the SilverlightContrib project.
Can I get your permission to use it?
 
Thanks,
 
Rob.
GeneralRe: SilverlightContribmemberlneir26 Oct '08 - 5:53 
That would be fine. Good luck.
QuestionCan I use this code?memberMember 295213923 Sep '08 - 3:47 
Hi lneir,
 
I use your WrapPanel in my LinkLabel control - http://www.silverlightshow.net/items/Silverlight-LinkLabel-control.aspx.
I'm planning to publish my control to CodePlex. I've read the license of this article and as far as I understand there is no problem to use your control for my purpose. Anyway, I thought it is best if I for your permission. So, do you mind if I use it?
 
Thank you.
AnswerRe: Can I use this code?memberlneir23 Sep '08 - 6:00 
Feel free to use as you like. If you have some good additions, fixes, etc., let us know.
 
Lynn
Questionmodified to behave as animating panelmemberMember 28546833 Jul '08 - 18:22 
hi, I have modified the code to make this work like an animating wrap panel and intend to blog about it at my blog site (http://blogs.infosys.com/microsoft and publish the modified code. Hope you don't have issues with it?
AnswerRe: modified to behave as animating panelmemberlneir4 Jul '08 - 7:33 
Totally cool...thanks for doing this.
AnswerRe: modified to behave as animating panelmemberRay Guan17 Nov '10 - 22:59 
detail web address, please, thanks
Today is a gift, that's why we call it present

GeneralThanks!memberSeth Webster13 Jun '08 - 16:23 
Appreciate it!
GeneralWrappanel inside another panelmemberMember 380211822 May '08 - 7:33 
As the previous poster mentioned, this method doesn't work when inside another layout panel. I was able to get it working (somewhat) by doing the following
 
protected override Size MeasureOverride(Size availableSize)
        {
            foreach (UIElement child in Children)
            {
                child.Measure(new Size(availableSize.Width, availableSize.Height));
            }
 
            // find a limiting width
            double maxWidth = double.MaxValue;
            FrameworkElement parent = this.Parent as FrameworkElement;
            while (parent != null)
            {
                if (!Double.IsNaN(parent.Width))
                {
                    if (parent.Width < maxWidth)
                    {
                        maxWidth = parent.Width;
                    }
                }
                parent = parent.Parent as FrameworkElement;
            }
 
            // start laying out - 
            int rowNum = 1;
            double remainingWidth = maxWidth;
            foreach (UIElement child in Children)
            {
                if (remainingWidth - child.DesiredSize.Width < 0)
                {
                    rowNum += 1;
                    remainingWidth = maxWidth;
                }
                remainingWidth -= child.DesiredSize.Width;
            }
 
            return new Size(maxWidth, rowNum * this.Children[0].DesiredSize.Height);
        }
 
I'm sure you could get it working after playing with it a bit.
GeneralRe: Wrappanel inside another panelmembervRud7 Jun '08 - 10:01 
This didn't work for me, but I was able to modify it slightly to give the desired result for me:
 
protected override Size MeasureOverride(Size availableSize)
        {
            foreach (UIElement child in Children)
            {
                child.Measure(new Size(availableSize.Width, availableSize.Height));
            }
 
            if (Children.Count == 0)
            {
                return base.MeasureOverride(availableSize);
            }
            else
            {
                // find a limiting width
                double maxWidth = double.MaxValue;
                FrameworkElement parent = this.Parent as FrameworkElement;
                maxWidth = parent.ActualWidth;
 
                // start laying out - 
                int rowNum = 1;
                double remainingWidth = maxWidth;
                foreach (UIElement child in Children)
                {
                    if (remainingWidth - child.DesiredSize.Width < 0)
                    {
                        rowNum += 1;
                        remainingWidth = maxWidth;
                    }
                    remainingWidth -= child.DesiredSize.Width;
                }
 
                return new Size(maxWidth, rowNum * this.Children[0].DesiredSize.Height);
            }
        }
 
I am filling the WrapPanel dynamically, and has it inside a ScrollViewer. Maybe thats why it didn't work at first.
GeneralRe: Wrappanel inside another panelmemberchris rothery14 Aug '08 - 2:32 
Thanks for the wrapPanel, just what I was looking for. Thanks also vRud for your amendment.
 
I've made a couple of tweaks to it as my maxWidth was remaining infinite (i.e. no parents had fixed width) and also the full width was being claimed meaning that I couldn't center my panel contents horizontally. I did have an issue with all of the rows being assumed to be the same height but I changed my app rather than sorting this out. Shouldn't be to difficult to fix though, keeping a running total of height so far as ArrangeOverride does.
 
Here's my alteration:
 
protected override Size MeasureOverride(Size availableSize)
        {
            foreach (UIElement child in Children)
            {
                child.Measure(new Size(availableSize.Width, availableSize.Height));
            }
            // find a limiting width            
            double maxWidth = double.MaxValue;
            FrameworkElement parent = this.Parent as FrameworkElement;
            while (parent != null)
            {
                if (!Double.IsNaN(parent.Width))
                {
                    if (parent.Width < maxWidth)
                    {
                        maxWidth = parent.Width;
                    }
                }
                parent = parent.Parent as FrameworkElement;
            }
 
            if (maxWidth == double.MaxValue && !double.IsInfinity(availableSize.Width))
            {
                maxWidth = availableSize.Width;
            }
 
            // start laying out -          
            int rowNum = 1;
            double remainingWidth = maxWidth;
 
           // keep track of smallest remaining width on change of row as that will tell us how 
            // wide to report the panel contents are
            double minRemainingWidth = maxWidth;
            foreach (UIElement child in Children)
            {
                if (remainingWidth - child.DesiredSize.Width < 0)
                {
                    if (remainingWidth < minRemainingWidth)
                    {
                        minRemainingWidth = remainingWidth;
                    }
                    rowNum += 1; 
                    remainingWidth = maxWidth;                    
                }
                remainingWidth -= child.DesiredSize.Width;
            }
 
            // check the rem width on this last line
            if (remainingWidth < minRemainingWidth)
            {
                minRemainingWidth = remainingWidth;
            }
 
            maxWidth -= minRemainingWidth;
 
            if (this.Children == null || this.Children.Count == 0)
            {
                return new Size(maxWidth, 10);
            }
 
            return new Size(maxWidth, rowNum * this.Children[0].DesiredSize.Height);
        }
 
Enjoy!
 
Chris
GeneralDoesn't work inside ScrollViewermemberraulgspan27 Mar '08 - 15:45 
This code isn't quite right... put it inside a ScrollViewer and you'll see what I mean. You need to do the same type of "how many rows/cols do I have" in MeasureOverride() that you do in ArrangeOverride(). The reason for this is that the WrapPanel's parent might need to know its exact dimensions (such as when the parent is a ScrollViewer), and it gets this from MeasureOverride().
 
Thanks,
Russ.
http://www.russellgreenspan.com
GeneralRe: Doesn't work inside ScrollViewermemberlneir27 Mar '08 - 16:05 
Yes, it probably does not work inside a ScrollViewer. I did not take that into consideration when I designed. Thanks for info...I'll look at incorporating the changes ... or feel free to send along changes.
 
Thanks,
Lynn
QuestionRe: Doesn't work inside ScrollViewermemberHarlequin66622 Sep '08 - 5:14 
Does anyone have the suggested changes for it to work in a scrollviewer?
AnswerRe: Doesn't work inside ScrollViewermemberHarlequin66622 Sep '08 - 6:19 
This inside the MeasureOverride seems to give it's parent(like a ScrollViewer) it's proper size:
Size infiniteSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
double curX = 0, curY = 0, curLineHeight = 0;
 
foreach (UIElement child in Children)
{
  child.Measure(infiniteSize);
 
  if (curX + child.DesiredSize.Width > availableSize.Width)
  { //Wrap to next line
    curY += curLineHeight;
    curX = 0;
    curLineHeight = 0;
  }
 
  curX += child.DesiredSize.Width;
  if (child.DesiredSize.Height > curLineHeight)
  {
    curLineHeight = child.DesiredSize.Height;
  }
}
 
curY += curLineHeight;
 
Size resultSize = new Size();
resultSize.Width = double.IsPositiveInfinity(availableSize.Width) ? curX : availableSize.Width;
resultSize.Height = double.IsPositiveInfinity(availableSize.Height) ? curY : availableSize.Height;
 
return resultSize;

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 7 Mar 2008
Article Copyright 2008 by lneir
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid