4.1. Using the WrapPanel
Let's consider using the WrapPanel in a user interface. One straightforward option is to use it in similar fashion to a StackPanel:
<wrappanellib:WrapPanel Orientation="Horizontal">
<TextBlock Text="Child 1"/>
<TextBlock Text="Child 2"/>
<TextBlock Text="Child 3"/>
<TextBlock Text="Child 4"/>
<TextBlock Text="Child 5"/>
<TextBlock Text="Child 6"/>
<TextBlock Text="Child 7"/>
<TextBlock Text="Child 8"/>
<Button Content="Child 9" Width="60" Height="30"/>
<Button Content="Child 10" Width="70" Height="30"/>
<Button Content="Child 11" Width="60" Height="30"/>
<RadioButton Content="Child 12" Width="90" Height="30"/>
<RadioButton Content="Child 13" Width="60" Height="30"/>
<Button Content="Child 14" Width="80" Height="30"/>
<Button Content="Child 15" Width="60" Height="30"/>
</wrappanellib:WrapPanel>
This code shows the standard XAML usage pattern where all children, a mixed bag of controls in this case, are listed within the WrapPanel
declaration, much in the fashion of any other layout container. You
should be able to cut and paste this code in your own sample
application and use it as is. Just remember to reference the assembly
from the sample code, and create a namespace mapping (you have mapped
the wrappanellib namespace here).
One of the interesting usages of a panel is to assist in the layout process of an ItemsControl, which is the primary base control for visually representing a collection of many items. ItemsControl exposes a property named ItemsPanel of type ItemsPanelTemplate that can be defined in terms of any type that extends Panel. A better control is the ListBox, which extends the ItemsControl and, by virtue of that, uses a panel internally for layout. Let's see how to replace the layout panel for a ListBox and control some of the panel's properties as well.
Listing 2 shows the XAML for a page with a ListBox in it, with its default panel replaced with your own WrapPanel from this recipe.
Listing 2. XAML for a ListBox using the WrapPanel for layout
<UserControl x:Class="Recipe5_9.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Recipe5_9" xmlns:wrappanellib= "clr-namespace:Recipe5_9;assembly=Recipe5_9.WrapPanel" Width="585" Height="440"> <UserControl.Resources>
<local:ImagesCollection x:Key="dsImages" />
<DataTemplate x:Key="dtImageItem"> <Grid Background="#007A7575" Margin="10,10,10,10" >
<Rectangle Fill="#FF7A7575" Stroke="#FF000000" RadiusX="5" RadiusY="5"/> <Image Margin="10,10,10,10" Width="50" Height="50" VerticalAlignment="Center" HorizontalAlignment="Center" Source="{Binding ImageFromResource}"/>
</Grid> </DataTemplate>
<Style TargetType="ListBox" x:Key="STYLE_WrapPanelListBox"> <Setter Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <wrappanellib:WrapPanel Orientation="{Binding CurrentOrientation}" Width="600" Height="600"/> </ItemsPanelTemplate> </Setter.Value>
</Setter> </Style>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White"> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <ListBox x:Name="lbxWrapPanelTest" Grid.Row="0" ItemTemplate="{StaticResource dtImageItem}" ItemsSource="{StaticResource dsImages}" Style="{StaticResource STYLE_WrapPanelListBox}"> </ListBox> <StackPanel Orientation="Horizontal" Grid.Row="1"> <RadioButton Content="Horizontal Arrangement" Margin="0,0,20,0" GroupName="OrientationChoice" x:Name="rbtnHorizontal" Checked="rbtnHorizontal_Checked" IsChecked="True"/> <RadioButton Content="Vertical Arrangement" Margin="0,0,0,0" GroupName="OrientationChoice" x:Name="rbtnVertical" Checked="rbtnVertical_Checked"/> </StackPanel> </Grid>
</UserControl>
|
The first thing to note is the ItemsPanelTemplate definition for the ListBox. The internal implementation of the ListBox in the framework uses a StackPanel as the panel, but you redefine it to use your own WrapPanel and set it as the value of the ItemsPanel property in a style targeting a ListBox. You then apply the style to the ListBox lbxWrapPanelTest. We will come back to this definition in a moment.
Also notice that lbxWrapPanelTest gets its data from a data source named dsImages pointing to a collection named ImagesCollection. The ItemTemplate is set to a data template dtImageItem that displays some images contained in dsImages; each image is encapsulated in a type named CustomImageSource.
Listing 3 shows the code for CustomImageSource and ImagesCollection.
Listing 3. Code for CustomImageSource and ImagesCollection types
using System.Windows.Media.Imaging; using System.Reflection; using System.Collections.Generic;
namespace Recipe5_9 { public class CustomImageSource
{ public string ImageName { get; set; } private BitmapImage _bitmapImage; public BitmapImage ImageFromResource { get { if (_bitmapImage == null) { _bitmapImage = new BitmapImage(); _bitmapImage.SetSource( this.GetType().Assembly.GetManifestResourceStream(ImageName)); }
return _bitmapImage; } } } public class ImagesCollection : List<CustomImageSource> { public ImagesCollection() { Assembly thisAssembly = this.GetType().Assembly; List<string> ImageNames = new List<string>(thisAssembly.GetManifestResourceNames());
foreach (string Name in ImageNames) { if (Name.Contains(".png")) this.Add(new CustomImageSource { ImageName = Name }); } } } }
|
The images used in this sample are embedded as resources in the project assembly. ImagesCollection uses GetManifestResourceNames()
to get a collection of the string names of all the embedded resources.
It then iterates over the collection of resource names and uses GetManifestResourceStream() to acquire each resource as a stream. It creates a new CustomImageSource for each one ending with the .png extension indicating an image resource, and the CustomImageSource type constructor loads the image.
Let's take another look at that ItemsPanelTemplate definition. Once the ItemsPanel property is set on the ListBoxItem, the panel instance that is created internally by the ListBox
is not made available to your application code in any way. However,
there may be a need to access properties on the underlying panel from
application code. An example could be the need to change your WrapPanel's Orientation property to influence the ListBox's layout. However, since the panel is not directly exposed, you need to take a slightly indirect approach to this.
Inside the ItemsPanelTemplate declaration, the WrapPanel has full access to the DataContext of its parent ListBox lbxWrapPanelTest.
This gives you a way to bind a property exposed by the panel to
application data, as long as that data is made available through the ListBox's DataContext. As shown in Listing 1, you bind the WrapPanel.Orientation property to the CurrentOrientation property of some data item. Further, you have two RadioButtons on the page with Checked event handlers defined in the codebehind. Listing 4 shows the codebehind for the page.
Listing 4 . Codebehind for the MainPage hosting the ListBox
using System.Windows.Controls; using System.ComponentModel;
namespace Recipe5_9 { public partial class MainPage : UserControl { ListBoxPanelOrientation CurrentLbxOrientation = new ListBoxPanelOrientation { CurrentOrientation = Orientation.Horizontal }; public MainPage() { InitializeComponent(); lbxWrapPanelTest.DataContext = CurrentLbxOrientation; }
private void rbtnHorizontal_Checked(object sender, System.Windows.RoutedEventArgs e) { CurrentLbxOrientation.CurrentOrientation = Orientation.Horizontal; }
private void rbtnVertical_Checked(object sender, System.Windows.RoutedEventArgs e) { CurrentLbxOrientation.CurrentOrientation = Orientation.Vertical; } } public class ListBoxPanelOrientation : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged;
private Orientation _Current; public Orientation CurrentOrientation { get { return _Current; }
set { _Current = value; if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("CurrentOrientation")); } } } }
|
The ListBoxPanelOrientation type exposes the CurrentOrientation property enabled with property change notification. You construct and initialize an instance of ListBoxPanelOrientation, and set it to the ListBox's DataContext. This causes the internal WrapPanel instance to adopt this orientation through the binding discussed earlier. In the Checked event handlers of the RadioButtons, you change the CurrentOrientation value, which causes the ListBox to change its orientation dynamically, again because of the property change notification flowing back to the WrapPanel through the binding.
Figure 1 shows the ListBox and the contained WrapPanel in action.