WPF - ListBox, ItemsPanelTemplate and a WrapPanel

|

The ListBox is a very versatile control in WPF, when you want to have some kind of list selection, regardless of the actual item layout, you'll probably opt for a ListBox with a custom ItemsTemplate. Due to this versatility I've used the ListBox control quite a bit in my WPF applications, and a common task I find myself doing is hacking replacing the ItemsPanelTemplate.

This is the panel that houses and arranges all the child items that make up the list. For example, you might want a horizontal list rather than the default vertical list. This a simple task, as shown below:

<ListBox>
    <ListBox.ItemsPanel>
       
<ItemsPanelTemplate>
            <VirtualizingStackPanel
                Orientation="Horizontal"
                IsItemsHost="True" />
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.Items>
        <TextBlock Text="One" />
        <TextBlock Text="Two" />
        <TextBlock Text="Three" />
        <TextBlock Text="Four" />
    </ListBox.Items>
</ListBox>

The VirtualizingStackPanel is the default panel for the ListBox, so I'm using the same control here, but changing the value of the Orientation property to Horizontal. Job done.

Another common panel "trick" I like to do is to swap out the VirtualizingStackPanel for a WrapPanel. This gives me item selection in a WrapPanel, similar to the Windows Explorer experience when you're looking at your files in list mode. At first, this might appear to be a simple task:

<ItemsPanelTemplate>
    <WrapPanel IsItemsHost="True" />
</ItemsPanelTemplate>

Lovely, however, things start getting icky when you want the items to actually wrap - which kind of the point of using a WrapPanel in the first place!! Grrrr! Below is a screen-shot showing the problem.

The items are clearly not wrapping. This is because the Width property of the panel is set to Auto, the default value, meaning infinite width. The other issue is that further up the chain in the visual tree is a ScrollContentPresenter. The implications of that are if a width value is not explicitly set for the contained control, the WrapPanel in this case, it will be allowed to be as wide as it needs to be to display all of its content. The ScrollContentPresenter will then show a scroll bar if the width is larger than the viewable area. Drat and double drat!

One approach could be to remove the ScrollContentPresenter from the visual tree, but that involves replacing the ListBox's control template, which is a lot of hard work just to have the items wrap in the panel.

So what we really want to do is stop the ScrollContentPresenter from thinking that the WrapPanel is bigger than it is, which will stop the scrollbars AND ensure that the WrapPanel has a finite width, making it wrap. We can achieve both goals with a "simple" binding expression in the ItemsPanelTemplate defined for our WrapPanel:

<ItemsPanelTemplate x:Key="VideoItemsPanelTemplate">
    <WrapPanel
         IsItemsHost="True"
         Width="{Binding
                  Path=ActualWidth,
                  RelativeSource={RelativeSource
                      Mode=FindAncestor,
                      AncestorType=
                         {x:Type ScrollContentPresenter}}}"
/>
</ItemsPanelTemplate>

Now, I know that this is pretty funky looking syntax; especially if you've not done too much data binding. But quite simply, all I'm doing here is setting the Width of the WrapPanel to the value of the ActualWidth property on the ScrollContentPresenter, and I find the ScrollContentPresenter by walking up the visual tree.

See, simple(?)

On question you might have is why not just use the RelativeSource of TemplatedParent? The answer, to your great question, is that the parent of the WrapPanel is an ItemsPresenter not the ScrollContentPresenter,  as shown below with an image taken from XAMLPad; which suffers from the same problem as the WrapPanel, in that it can have infinite width and will cause the scroll bars to appear.

I'm sure that there are other solutions to the WrapPanel issue described in this post and if you have one I would love to hear from you.

22 comments:

John "Z-Bo" Zabroski said...

I had the same problem and when I searched Google, you were one of only 7 search results TOTAL! Whew, thankfully I found your workaround.

I think this might be a bug, though. What I determined is weird: if your listbox items are binding to a data source that has a GroupDescription set, then the WrapPanel behaves as expected. WTF?

Chinese said...

Hi, Thanks to your article, Could you tell me how to change ListBox.ItemsPanel from code way?
I tried the following code but failed:

FrameworkElementFactory itemsPanelTemplateFactory = new FrameworkElementFactory(typeof(WrapPanel));
ItemsPanelTemplate itemsPanelTemplate = new ItemsPanelTemplate();
itemsPanelTemplate .VisualTree = itemsPanelTemplateFactory;
ListBox.ItemsPanel = itemsPanelTemplate;

Anonymous said...

Hi Paul, I'm looking for some guidance on using this type of binding in Silverlight 2. Specifically I have a wrappanel class which inherits from Panel with some code to handle the layout. However on setting the binding of the wrappanel Width property in XAML to its ScrollPresenter, I recieve an error and exception with "Unknown attribute Width on element WrapPanel" it seems the XAML on runtime cant see the inherited width Property. Is this a bug or restriction in Silverlight? Or am I missing something?

More specifically I'm using Michael Sync's Wrappanel whihc can be found here: http://michaelsync.net/2008/07/23/tall-skinny-data-columns-using-improved-wrappanel-for-silverlightmatt-perdeck.

Thanks in advance.
Maria

Paul said...

Hi Maria, on the whole you're going to struggle with this kind of binding trick in SL2, the binding engine is not yet up to it, the relative source concept cannot be used.

However, on your specific issue of the Width property inheritance I have not come across that issue in Silverlight 2, I've been working specifically with beta 2.

Maybe if you could point me to location where I could download the code to take look at? (I use Box.NET) or you could mail it to me: "paul at compilewith dot net".

I'm not around for a week or so now, but I'll happily take a look once I'm back on-line.

I hope this helps.

-PJ

Pete Magsig said...

I'm trying to do the exact same thing with Michael Sync's WrapPanel in Silverlight Beta 2, but I've got past the Binding problem mentioned by Maria.

If you set ScrollViewer.HorizontalScrollBarVisibility="Disabled" in the ListBox, the WrapPanel will get the proper width constraint. A lot simpler.

However, once I got that working (I can tell by setting a breakpoint in the WrapPanel code and watching the available size Width go from Infinity to the real number), I found a really odd behavior: The ListBox behaves as if there is no WrapPanel there at all and reverts to its original layout, i.e. one item per row.

Thinking that maybe the ScrollViewer.HorizontalScrollBarVisibility="Disabled" had something to do with this, I got rid of this setting and just set the Width of the WrapPanel by hand to a fixed number, in my case 250. Again, the ListBox reverted to its original layout.

Very puzzling.

Pete Magsig said...

Never mind. I don't think this is a problem with the ListBox. I forgot to set up my ListBoxItem Templates, and the WrapPanel was using the default.

My suggestion of disabling the ScrollViewer.HorizontalScrollBarVisibility appears to work, though.

Jon said...

Just to repeat the clean solution from Peter in case others missed it like I did:

If you set ScrollViewer.HorizontalScrollBarVisibility="Disabled" in the ListBox, the WrapPanel will get the proper width constraint. A lot simpler.

Scotty said...

Thanks so much for this blog post. It turned out to be the perfect solution for what I needed.

Gishu said...

Just what I needed for the view of my Bowling Scorer. Thanks.

Paul said...

@Gishu glad you found it useful. Thanks for reading.

-PJ

n said...

Thanks. It solves my prb right now. But still i have some confusion. Will ask later if i have touse it

Anonymous said...

You don't have to use IsItemsHost in this case.

AuntKK said...

Thanks. This is really a big help.

Anonymous said...

Great! Both Wrapping and dynamic width. Awesome!

Stewart Walker said...

Mate, thanks this post is a life saver!!!

Have been trying to figure this out for ages and had a similar thought but couldn't figure out how to implement it.

Thanks you, Thank you, Thank you!!!!

Paul said...

Hey Stewart, you're welcome. I'm glad you found it useful. -PJ

sayvaz said...

this is sweet! thanks,

Dale Barnard said...

Thanks for solving the WrapPanel-not-wrapping problem and posting it!

Eric said...

Great explanation !
Crystal clear !

You post is on my very short list of great and clean article.

Thanks a lot!

Malte said...

Thanks Paul (and Pete & Jon), very helpful!

Pravesh Singh said...

Really this is good one. Only to the point nothing else. You made it very simple and understandable which make more user friendly. Some good articles too, I've found during searching time over internet which also explained very well about WrapPanel in WPF. Those post URL:
http://www.c-sharpcorner.com/uploadfile/mahesh/wrappanel-in-wpf/
and
WrapPanel in WPF

Anonymous said...

finaly.... 1 hour left to try to make it simple.
You save me the rest of the day :)
Thanks Paul