This article is compatible with the latest version of Silverlight.
Introduction
In our serial article about custom controls in Silverlight we will create a control that inherits from ItemsControl. We thought it would be interesting to show you how to replace the default StackPanel with Grid and let items arrange consecutively like in StackPanel instead of using the Row and Column attached properties. Another thing we added is a container for each item. We used ListBox as a model for our control, the Reflector was very helpful for that. To achieve extended functionality we implement Dependency Properties. One of the advantages is that Visual Studio Designer is displaying our control correctly.
Download full source code
Overview
What we do is:
- inherit from ItemsControl
- in the default template of our control we change the ItemsPanel to Grid
- override some ItemsControl methods to support custom container
- add custom logic to arrange the Grid and the items in it
The first two steps are pretty self-explanatory. To see the basics of templating and how templating combines with the VisualStateManager see the following links.
Creating a Silverlight Custom Control - The Basics
Custom ContentControl using VisualStateManager
Inheriting from ItemsControl
To be able to use a container for the objects in the Grid we need to override some protected methods from ItemsControl. This will allow the use of a single template for all objects we put into the grid.
The first addition to the ItemsControl that we inherit is a dictionary where we keep the items and their corresponding containers:
private
Dictionary<
object
, ItemContainer> _objectToItemContainer;
and two helper methods
private
ItemContainer GetItemContainerForObject(
object
value )
{
ItemContainer item = value
as
ItemContainer;
if
( item ==
null
)
{
this
.ObjectToItemContainer.TryGetValue( value,
out
item );
}
return
item;
}
private
IDictionary<
object
, ItemContainer> ObjectToItemContainer
{
get
{
if
(
this
._objectToItemContainer ==
null
)
{
this
._objectToItemContainer =
new
Dictionary<
object
, ItemContainer>();
}
return
this
._objectToItemContainer;
}
}
The table below explains more about the methods we override.
Name |
Description from MSDN |
GetContainerForItemOverride |
Creates or identifies the element that is used to display the given item. |
Implementation |
Returns an instance of the container. |
protected override DependencyObject GetContainerForItemOverride()
{
ItemContainer item = new ItemContainer();
if ( this.ItemContainerStyle != null )
{
item.Style = this.ItemContainerStyle;
}
return item;
}
|
Name |
Description from MSDN |
IsItemItsOwnContainerOverride |
Determines if the specified item is (or is eligible to be) its own container. |
Implementation |
Returns true if the item is of the type of the container. |
protected override bool IsItemItsOwnContainerOverride(object item)
{
return (item is ItemContainer);
}
|
Name |
Description from MSDN |
PrepareContainerForItemOverride |
Prepares the specified element to display the specified item. |
Implementation |
Applies ItemTemplate (which is DataTemplate) to the item. Sets the content of the container to be the item. Applies Style to the container. Mainains the index of the last added item. |
protected override void PrepareContainerForItemOverride( DependencyObject element, object item )
{
base.PrepareContainerForItemOverride( element, item );
ItemContainer item2 = element as ItemContainer;
bool flag = true;
if ( item2 != item )
{
if ( base.ItemTemplate != null )
{
item2.ContentTemplate = base.ItemTemplate;
}
else if ( !string.IsNullOrEmpty( base.DisplayMemberPath ) )
{
Binding binding = new Binding( base.DisplayMemberPath );
item2.SetBinding( ContentControl.ContentProperty, binding );
flag = false;
}
if ( flag )
{
item2.Content = item;
}
// Addition to the original ListBox function that we use
this.ArrangeItem( item2 );
this.ObjectToItemContainer[ item ] = item2;
}
if ( ( this.ItemContainerStyle != null ) && ( item2.Style == null ) )
{
item2.Style = this.ItemContainerStyle;
}
}
|
Name |
Description from MSDN |
ClearContainerForItemOverride |
When overridden in a derived class, undoes the effects of the PrepareContainerForItemOverride method. |
Implementation |
Removes the item if it is not self container. Corrects the index of the last item. |
protected override void ClearContainerForItemOverride( DependencyObject element, object item )
{
base.ClearContainerForItemOverride( element, item );
ItemContainer item2 = element as ItemContainer;
if (item == null)
{
item = (item2.Content == null) ? item2 : item2.Content;
}
if (item2 != item)
{
this.ObjectToItemContainer.Remove(item);
GoToPreviousIndex();
}
}
|
We've taken the implementations of these functions from ListBox and modified them to suit our purposes. This way we have similar functionality. The main difference with the ListBox functions is that we add the ArrangeItem method to put the item in the correct column and row of the Grid.
As we can see from the names of the functions ItemsControl assumes the inheritor class will have a container. In our example the class we use as a container is ItemsContainer which is simply an empty ContentControl inheritor. We did that in case we want a default style for the container. To be able to apply custom style to the container we expose ItemContainerStyle property. This is a dependency property and when it's changed we iterate through our dictionary to change the style of each container:
internal
virtual
void
OnItemContainerStyleChanged( Style oldItemContainerStyle, Style newItemContainerStyle )
{
if
( oldItemContainerStyle != newItemContainerStyle )
foreach
(
object
obj2
in
base
.Items )
{
ItemContainer ItemContainerForObject =
this
.GetItemContainerForObject( obj2 );
if
( ( ItemContainerForObject !=
null
) && ( ( ItemContainerForObject.Style ==
null
) ||
( oldItemContainerStyle == ItemContainerForObject.Style ) ) )
{
if
( ItemContainerForObject.Style !=
null
)
{
throw
new
NotSupportedException(
null
);
}
ItemContainerForObject.Style = newItemContainerStyle;
}
}
}
So far we exploited code from the ListBox control in Silverlight.
Logic of arrangement
Our control has uniform cells. This eliminates the need of Row and ColumnDefinitions from the user. We have two properties - Rows and Columns instead. When they are changed the control adds the appropriate number of definitions to the grid. However we had troubles getting reference to the Grid of our control. To use grid we define a default style for our control. The easiest way to get access to certain control is to give it a name in the Template setter. However, ItemsPanel is a property outside of the Template property. So GetTemplateChild that we usually use for getting reference to a control in a template can't find the control defined in ItemsPanelTemplate. Because of that we get reference to the ItemsPanel element in the Template setter and then search its children to find a Grid.
<
Style
TargetType
=
"c:UniformGrid"
>
<
Setter
Property
=
"Template"
>
<
Setter.Value
>
<
ControlTemplate
>
<
Grid
Background
=
"{TemplateBinding Background}"
>
<
ItemsPresenter
x:Name
=
"ItemsPresenter"
></
ItemsPresenter
>
</
Grid
>
</
ControlTemplate
>
</
Setter.Value
>
</
Setter
>
<
Setter
Property
=
"ItemsPanel"
>
<
Setter.Value
>
<
ItemsPanelTemplate
>
<
Grid
/>
</
ItemsPanelTemplate
>
</
Setter.Value
>
</
Setter
>
</
Style
>
This approach had problems too. Applying the ItemsPanelTemplate seems to be the last thing that's done during the initialization of the control. That's our conclusion since the children of "ItemsPresenter" would be 0 even after the Loaded event or when OnApplyTemplate occurs. Our workaround is to hook to the first LayoutUpdate event which works fine.
public
override
void
OnApplyTemplate()
{
base
.OnApplyTemplate();
ItemsPresenter = (ItemsPresenter)GetTemplateChild(
"ItemsPresenter"
);
// 0 children
ItemsPresenter.LayoutUpdated +=
new
EventHandler(ItemsPresenter_LayoutUpdated);
}
void
ItemsPresenter_LayoutUpdated(
object
sender, EventArgs e)
{
ItemsPresenter.LayoutUpdated -=
new
EventHandler(ItemsPresenter_LayoutUpdated);
ItemsPresenter = ( ItemsPresenter )GetTemplateChild(
"ItemsPresenter"
);
UpdateMeasure();
}
Now we add the columns and rows for the user in UpdateMeasure().
DependencyObject target = VisualTreeHelper.GetChild(ItemsPresenter, i);
if
(target
is
Grid)
{
g = target
as
Grid;
g.RowDefinitions.Clear();
for
(
int
r = 0; r < Rows; r++)
g.RowDefinitions.Add(
new
RowDefinition());
g.ColumnDefinitions.Clear();
for
(
int
c = 0; c < Columns; c++)
g.ColumnDefinitions.Add(
new
ColumnDefinition());
}
The other arrangement method iterates the items and updates their position in the grid. We keep the index of the last added item in _last*Index private members.
protected
void
ArrangePanel()
{
_lastColumnIndex = 0;
_lastRowIndex = 0;
foreach
(
object
obj
in
base
.Items)
{
ItemContainer ItemContainerForObject =
this
.GetItemContainerForObject(obj);
if
(ItemContainerForObject !=
null
)
ArrangeItem(ItemContainerForObject);
}
}
protected
void
ArrangeItem(ItemContainer item)
{
item.SetValue(Grid.ColumnProperty, _lastColumnIndex);
item.SetValue(Grid.RowProperty, _lastRowIndex);
GoToNextIndex();
}
ArrangeItem() calls GoToNextItem to prepare the index for the next item.
protected
void
GoToNextIndex()
{
if
(ChildrenFlow == Orientation.Horizontal)
{
if
(_lastRowIndex >= Rows && AutoFill ==
true
)
Rows++;
if
(_lastColumnIndex < Columns - 1)
_lastColumnIndex++;
else
{
_lastColumnIndex = 0;
_lastRowIndex++;
}
}
This illustrates the logic of arrangement when items are added horizontally. Simply move to the end column and then continue from the beginning of the next row. The logic for returning back the index when removing items is analogical but in the opposite direction. We've added two more properties. AutoFill when it's true allows the number of rows/columns to change dynamically when the control is full and we add more items. The other property ChildrenFlow says whether to fill the Grid row by row or column by column.
Conclusion
In the end we should point that there is a different approach to create such UniformGrid as we like to call it. Instead of inheriting from ItemsControl and arranging the items in the Grid you could inherit from panel to create your own grid and override ArrangeOverride and MeasureOverride and then make it ItemsPanel of the ItemsControl.
The custom ItemsControl that we made offers functionality similar to that of a ListBox. You may bind it to data objects in ObservableCollection or you can define element directly in the XAML where you use it. We didn't include selection though it is probably good feature. We wanted to concentrate on consistency in this example to make truly usable control. Await our future article where we would probably use this control and have some selection included.
References
The Layout System
http://msdn.microsoft.com/en-us/library/ms745058.aspx
Custom Radial Panel Sample
http://msdn.microsoft.com/en-us/library/ms771363.aspx
ItemsControl class
http://msdn.microsoft.com/en-us/library/system.windows.controls.itemscontrol(VS.95).aspx