Refactoring Focus Panel: ItemsControl

So while the FocusPanel we built in "Using our animation framework, a focus panel " it had a lot of room for improvement. The biggest to change are, the bad syntax required to add items to the panel in xaml and the inability to bind data to the collection. Previously, anything we wanted to add to the panel had to be declared within a FocusPanelItem which was the control that was animated and had the focus button". I forgot to include this in the previous post but the xaml for the example you can see at " Focus Panel example" is as follows.

<cx:FocusPanel x:Name="Items">
    <cx:FocusPanelItem Content="Example Text"/>
    <cx:FocusPanelItem>
        <cx:FocusPanelItem.Content>
            <Ellipse Fill="#FF5885A4" />
        </cx:FocusPanelItem.Content>
    </cx:FocusPanelItem>
    <cx:FocusPanelItem>
        <cx:FocusPanelItem.Content>
            <TextBox Text="Examplte TextBox" />
        </cx:FocusPanelItem.Content>
    </cx:FocusPanelItem>
    <cx:FocusPanelItem>
        <cx:FocusPanelItem.Content>
            <Button Content="Example Button"/>
        </cx:FocusPanelItem.Content>
    </cx:FocusPanelItem>
</cx:FocusPanel>

Pretty verbose huh? So how can we change this?. Well a lot of the functionality we're going to be needing comes hand in hand with the ItemsControl. The ItemsControl is very very useful all by itself, used directly you define two templates. The ItemPanelTemplate defines the layout to display the items. This could be a StackPanel, Grid, Canvas or any other custom panel control you have. The ItemTemplate allows you to define a DataTemplate to display the item you're binding. Between these two template we can define a lot of what we want, but we need to go a step further.

Rather than inheriting FocusPanel from Canvas we'll need to inherit from ItemsControl. But we still need a Canvas to do our layout (as we animate the Canvas.Left and Canvas.Top attached properties for our position animation). So the first thing we'll do is define the DefaultStyleKey in the constructor and set up a default ItemPanelTemplate in Generic.xaml.

public FocusPanel()
{
    DefaultStyleKey = typeof(FocusPanel);
    SizeChanged += OnSizeChanged;
 
    FocusPanelItems = new List<FocusPanelItem>();
}
<Style TargetType="cx:FocusPanel">
    <Setter Property="ItemsPanel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <Canvas/>
            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
</Style>

ItemsControl also has the concept of a container. Each item bound is inserted into a container if necessary. If the item isn't a UIElement then it'll create a very simple container (a ContentPresenter) and use that to display the non-UIElement object. What we need to do is change a little of this behaviour.

ItemsControl doesn't internally maintain a list of items so we'll need to manually maintain this one. We also won't need to do anything on the Loaded event so we can remove the handler for that. We can also remove the code we attach to the OnFocused event in LoadItems as we'll be doing that elsewhere.

private int Columns { get; set; }
private int Rows { get; set; }
private IList<FocusPanelItem> FocusPanelItems { get; set; }
private FocusPanelItem FocussedPanel { get; set; }
 
 
public FocusPanel()
{
    DefaultStyleKey = typeof(FocusPanel);
    SizeChanged += OnSizeChanged;
 
    FocusPanelItems = new List<FocusPanelItem>();
}

Now we need to tell the ItemsControl to create a FocusPanelItem for every item (unless the item is already a FocusPanelItem). We override IsItemItsOwnContainerOverride (I'm not sure why they chose to append Override to the name but ok).

private int Columns { get; set; }
private int Rows { get; set; }
private IList<FocusPanelItem> FocusPanelItems { get; set; }
private FocusPanelItem FocussedPanel { get; set; }
 
 
public FocusPanel()
{
    DefaultStyleKey = typeof(FocusPanel);
    SizeChanged += OnSizeChanged;
 
    FocusPanelItems = new List<FocusPanelItem>();
}

We then override GetContainerForItemOverride which will create a new FocusPanelItem for the the currently bound item.

protected override DependencyObject GetContainerForItemOverride()
{
    return new FocusPanelItem();
}

Overriding PrepareContainerForItemOverride we want to keep the base behavior but also add our item to our internal collection, attach the OnFocussed event and then update our panels.

protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
    base.PrepareContainerForItemOverride(element, item);
 
    var panelItem = (FocusPanelItem)element;
 
    panelItem.Focused += OnItemFocused;
 
    FocusPanelItems.Add(panelItem);
 
    LoadItems();
    UpdateItems(false);
}

And when an is removed we override ClearContainerForItemOverride which will remove our item from the collection, detach from the OnFocussed event and reupdate our panels.

protected override void ClearContainerForItemOverride(DependencyObject element, object item)
{
    var panel = (FocusPanelItem)element;
    panel.Focused -= OnItemFocused;
 
    FocusPanelItems.Remove(panel);
 
    if(FocussedPanel == panel)
        FocussedPanel = null;
 
    LoadItems();
    UpdateItems(false);
}

Overall our example hasn't changed much but we can now either bind to the panels ItemSource (though code or in xaml) or use xaml to add children to the panel.

private void OnLoaded(object sender, RoutedEventArgs e)
{
    Items.ItemsSource = new string[] { "Item One", "Item Two", "Item Three" };
}
<cx:FocusPanel x:Name="Items">
    <Ellipse Fill="#FF5885A4" />
    <TextBox Text="Examplte TextBox" />
    <Button Content="Example Button"/>
</cx:FocusPanel>

You can download the code for this at "FocusPanel example code"