Chapter 10. Items Controls

Besides content controls, the other major category of WPF controls is items controls, which can contain an unbounded collection of items rather than just a single piece of content. All items controls derive from the abstract ItemsControl class, which, like ContentControl, is a direct subclass of Control.

ItemsControl stores its content in an Items property (of type ItemCollection). Each item can be an arbitrary object that by default gets rendered just as it would inside a content control. In other words, any UIElement is rendered as expected, and (ignoring data templates) any other type is rendered as a TextBlock containing the string returned by its ToString method.

The ListBox control used in earlier chapters is an items control. Whereas those chapters always added ListBoxItems to the Items collection, the following example adds arbitrary objects to Items:

<ListBox
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:sys="clr-namespace:System;assembly=mscorlib">
  <Button>Button</Button>
  <Expander Header="Expander"/>
  <sys:DateTime>1/1/2016</sys:DateTime>
  <sys:DateTime>1/2/2016</sys:DateTime>
  <sys:DateTime>1/3/2016</sys:DateTime>
</ListBox>

(This snippet uses sys:DateTime instead of x:DateTime so it works as both loose XAML and compiled XAML.)

The child elements are implicitly added to the Items collection because Items is a content property. This ListBox is shown in Figure 10.1. The two UIElements (Button and Expander) are rendered normally and are fully interactive. The three DateTime objects are rendered according to their ToString method.

Image

FIGURE 10.1 A ListBox containing arbitrary objects.

As mentioned in Chapter 2, “XAML Demystified,” the Items property is read-only. This means that you can add objects to the initially empty collection or remove objects, but you can’t point Items to an entirely different collection. ItemsControl has a separate property—ItemsSource—that supports filling its items with an existing arbitrary collection. The use of ItemsSource is examined further in Chapter 13, “Data Binding.”


Tip

To keep things simple, examples in this chapter fill items controls with visual elements. However, the preferred approach is to give items controls nonvisual items (for example, custom business objects) and use data templates to define how each item gets rendered. Chapter 13 discusses data templates in depth.


Common Functionality

Besides Items and ItemsSource, ItemsControl has a few additional interesting properties, including the following:

Image HasItems—A read-only Boolean property that makes it easy to act on the control’s empty state from declarative XAML. From C#, you can either use this property or simply check the value of Items.Count.

Image IsGrouping—Another read-only Boolean property that tells if the control’s items are divided into top-level groups. This grouping is done directly within the ItemsCollection class, which contains several properties for managing and naming groups of items. You’ll learn more about grouping in Chapter 13.

Image AlternationCount and AlternationIndex—This pair of properties makes it easy to vary the style of items based on their index. For example, an AlternationCount of 2 can be used to give even-indexed items one style and odd-indexed items another style. Chapter 14, “Styles, Templates, Skins, and Themes,” shows an example of using these properties.

Image DisplayMemberPath—A string property that can be set to the name of a property on each item (or a more complicated expression) that changes how each object is rendered.

Image ItemsPanel—A property that can be used to customize how the control’s items are arranged without replacing the entire control template.

The next two sections provide further explanation of the last two properties in this list.

DisplayMemberPath

Figure 10.2 demonstrates what happens when DisplayMemberPath is applied to the preceding ListBox, as follows:

<ListBox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:sys="clr-namespace:System;assembly=mscorlib" DisplayMemberPath="DayOfWeek">
  <Button>Button</Button>
  <Expander Header="Expander"/>
  <sys:DateTime>1/1/2016</sys:DateTime>
  <sys:DateTime>1/2/2016</sys:DateTime>
  <sys:DateTime>1/3/2016</sys:DateTime>
</ListBox>

Image

FIGURE 10.2 The ListBox from Figure 10.1 with DisplayMemberPath set to DayOfWeek.

Setting DisplayMemberPath to DayOfWeek tells WPF to render the value of each item’s DayOfWeek property rather than each item itself. That is why the three DateTime objects render as Friday, Saturday, and Sunday in Figure 10.2. (This is the ToString-based rendering of each DayOfWeek enumeration value returned by the DayOfWeek property.) Because Button and Expander don’t have a DayOfWeek property, they are rendered as empty TextBlocks.

ItemsPanel

Like all other WPF controls, the essence of items controls is not their visual appearance but their storage of multiple items and, in many cases, the ways in which their items are logically selected. Although all WPF controls can be visually altered by applying a new control template, items controls have a shortcut for replacing just the piece of the control template responsible for arranging its items. This mini-template, called an items panel, enables you to swap out the panel used to arrange items while leaving everything else about the control intact.

You can use any of the panels discussed in Chapter 5, “Layout with Panels” (or any Panel-derived custom panel) as an items panel. For example, a ListBox stacks its items vertically by default, but the following XAML replaces this arrangement with a WrapPanel, as done with Photo Gallery in Chapter 7, “Structuring and Deploying an Application”:

<ListBox>
<ListBox.ItemsPanel>
  <ItemsPanelTemplate>
    <WrapPanel/>
  </ItemsPanelTemplate>
</ListBox.ItemsPanel>
  ...
</ListBox>

The translation of this XAML to procedural code is not straightforward, but here’s how you can accomplish the same task in C#:

FrameworkElementFactory panelFactory = new
  FrameworkElementFactory(typeof(WrapPanel));
myListBox.ItemsPanel = new ItemsPanelTemplate(panelFactory);

Here’s an example with a custom FanCanvas that will be implemented in Chapter 21, “Layout with Custom Panels”:

<ListBox>
<ListBox.ItemsPanel>
  <ItemsPanelTemplate>
    <custom:FanCanvas/>
  </ItemsPanelTemplate>
</ListBox.ItemsPanel>
  ...
</ListBox>

Figure 10.3 shows the result of applying this to Photo Gallery (and wrapping the ListBox in a Viewbox) and selecting one item. The ListBox retains all its behaviors with item selection despite the custom inner layout.

Image

FIGURE 10.3 ListBox with a custom FanCanvas used as its ItemsPanel isn’t appropriate for Photo Gallery, but the results are definitely interesting.


Image FAQ: How can I make ListBox arrange its items horizontally instead of vertically?

By default, ListBox uses a panel called VirtualizingStackPanel to arrange its items vertically. The following code replaces it with a new VirtualizingStackPanel that explicitly sets its Orientation to Horizontal:

<ListBox>
<ListBox.ItemsPanel>
  <ItemsPanelTemplate>
    <VirtualizingStackPanel Orientation="Horizontal"/>
  </ItemsPanelTemplate>
</ListBox.ItemsPanel>
  ...
</ListBox>



Tip

Many items controls use VirtualizingStackPanel as their default ItemsPanel to get good performance. As of WPF 4.0, this panel supports a mode that improves scrolling performance even further, but you need to turn it on explicitly. To do so, you set the VirtualizingStackPanel.VirtualizationMode attached property to Recycling. When this is done, the panel reuses (“recycles”) the containers that hold each onscreen item rather than constructing a new container for each item.


If you look at the default control template for an items control such as ListBox, you can see an ItemsPresenter, which does the work of picking up the appropriate ItemsPanel:

<ControlTemplate TargetType="{x:Type ListBox}">
  <Border ...>
    <ScrollViewer Padding="{TemplateBinding Padding}" Focusable="false">
      <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
    </ScrollViewer>
  </Border>
<ControlTemplate.Triggers>
  ...
</ControlTemplate.Triggers>
</ControlTemplate>

The presence of ScrollViewer in the default control template explains where the default scrolling behavior comes from. You can control an items control’s scrolling behavior with various ScrollViewer attached properties.

Controlling Scrolling Behavior

Using ListBox as an example, the following properties have the following values by default:

Image ScrollViewer.HorizontalScrollBarVisibilityAuto

Image ScrollViewer.VerticalScrollBarVisibilityAuto

Image ScrollViewer.CanContentScrolltrue

Image ScrollViewer.IsDeferredScrollingEnabledfalse

When CanContentScroll is true, scrolling is done in item-by-item chunks. When it is false, the pixel-by-pixel scrolling is smooth but doesn’t do anything to ensure that the first item is “snapped” to the edge.

When IsDeferredScrollingEnabled is false, scrolling happens in real-time while the scrollbar thumb is dragged. When it is true, the ScrollViewer’s contents do not update until the scrollbar thumb is released. When an items control is using a virtualizing panel and it contains a large number of complex items, setting IsDeferredScrollingEnabled to true can result in a significant performance improvement by avoiding the rendering of intermediate states. Applications such as Microsoft Outlook scroll through long lists in this fashion.

Here is an example of a ListBox that sets all four of these ScrollViewer attached properties to affect the ScrollViewer’s behavior in its default control template:

<ListBox
  ScrollViewer.HorizontalScrollBarVisibility="Disabled"
  ScrollViewer.VerticalScrollBarVisibility="Disabled"
  ScrollViewer.CanContentScroll="False"
  ScrollViewer.IsDeferredScrollingEnabled="True"

  ...
</ListBox>

ListBox is not the only items control, of course. Items controls can be divided into three main groups, as discussed in the following sections: selectors, menus, and others.

Selectors

Selectors are items controls whose items can be indexed and, most importantly, selected. The abstract Selector class, which derives from ItemsControl, adds a few properties to handle selection. For example, the following are three similar properties for getting or setting the current selection:

Image SelectedIndex—A zero-based integer that indicates what item is selected or -1 if nothing is selected. Items are numbered in the order in which they are added to the collection.

Image SelectedItem—The actual item instance that is currently selected.

Image SelectedValue—The value of the currently selected item. By default this value is the item itself, making SelectedValue identical to SelectedItem. You can set SelectedValuePath, however, to choose an arbitrary property or expression that should represent each item’s value. (SelectedValuePath works just like DisplayMemberPath.)

All three properties are read/write, so you can use them to change the current selection as well as retrieve it.

Selector also supports two attached properties that can be applied to individual items:

Image IsSelected—A Boolean that can be used to select or unselect an item (or to retrieve its current selection state)

Image IsSelectionActive—A read-only Boolean that tells whether the selection has focus

Selector also defines an event—SelectionChanged—that makes it possible to listen for changes to the current selection. Chapter 6, “Input Events: Keyboard, Mouse, Stylus, and Touch,” uses this with a ListBox when demonstrating attached events.

WPF 4.5 ships six Selector-derived controls, described in the following sections:

Image ComboBox

Image ListBox

Image ListView

Image TabControl

Image DataGrid

Image Ribbon

ComboBox

The ComboBox control, shown in Figure 10.4, enables users to select one item from a list. ComboBox is a popular control because it doesn’t occupy much space. It displays only the current selection in a selection box, with the rest of the list shown on demand in a drop-down. The drop-down can be opened and closed by clicking the button or by pressing Alt+up arrow, Alt+down arrow, or F4.

Image

FIGURE 10.4 The WPF ComboBox, with its drop-down showing.

ComboBox defines two events—DropDownOpened and DropDownClosed—and a property—IsDropDownOpen—that enable you to act on the drop-down being opened or closed. For example, you can delay the filling of ComboBox items until the drop-down is opened by handling the DropDownOpened event. Note that IsDropDownOpen is a read/write property, so you can set it directly to change the state of the drop-down.

Customizing the Selection Box

ComboBox supports a mode in which the user can type arbitrary text into the selection box. If the text matches one of the existing items, that item automatically becomes selected. Otherwise, no item gets selected, but the custom text gets stored in ComboBox’s Text property so you can act on it appropriately. This mode can be controlled with two poorly named properties, IsEditable and IsReadOnly, which are both false by default. In addition, a StaysOpenOnEdit property can be set to true to keep the drop-down open if the user clicks on the selection box (matching the behavior of drop-downs in Microsoft Office as opposed to normal Win32 drop-downs).


Image FAQ: What’s the difference between ComboBox’s IsEditable and IsReadOnly properties?

Setting IsEditable to true turns ComboBox’s selection box into a text box. IsReadOnly controls whether that text box can be edited, just like TextBox’s IsReadOnly property. This means that IsReadOnly is meaningless unless IsEditable is true, and IsEditable being true doesn’t necessarily mean that the selection text can be edited. Table 10.1 sums up the behavior of ComboBox based on the values of these two properties.

Image

TABLE 10.1 The Behavior for All Combinations of IsEditable and IsReadOnly


When the selection box is a text box, the selected item can be displayed only as a simple string. This isn’t a problem when items in the ComboBox are strings (or content controls containing strings), but when they are more complicated items, you must tell ComboBox what to use as the string representation for its items.

Listing 10.1 contains XAML for a ComboBox with complex items. Each item displays a PowerPoint design in a way that makes the ComboBox look like a Microsoft Office–style gallery, showing a preview and description for each item. A typical gallery in Office restricts the selection box to simple text, however, rather than keeping the full richness of the selected item. Figure 10.5 shows the rendered result of Listing 10.1, as well as what happens by default when this ComboBox is marked with IsEditable set to true.

Image

FIGURE 10.5 By default, setting IsEditable to true causes ToString-based rendering in the selection box.

LISTING 10.1 A ComboBox with Complex Items, Such as a Microsoft Office Gallery


<ComboBox>
  <!-- Item #1 -->
  <StackPanel Orientation="Horizontal" Margin="5">
    <Image Source="CurtainCall.bmp"/>
    <StackPanel Width="200">
      <TextBlock Margin="5,0" FontSize="14" FontWeight="Bold"
        VerticalAlignment="center">Curtain Call</TextBlock>
      <TextBlock Margin="5" VerticalAlignment="center" TextWrapping="Wrap">
        Whimsical, with a red curtain background that represents a stage.
      </TextBlock>
    </StackPanel>
  </StackPanel>
  <!-- Item #2 -->
  <StackPanel Orientation="Horizontal" Margin="5">
    <Image Source="Fireworks.bmp"/>
    <StackPanel Width="200">
      <TextBlock Margin="5,0" FontSize="14" FontWeight="Bold"
        VerticalAlignment="center">Fireworks</TextBlock>
      <TextBlock Margin="5" VerticalAlignment="center" TextWrapping="Wrap">
        Sleek, with a black sky containing fireworks. When you need to
        celebrate PowerPoint-style, this design is for you!
      </TextBlock>
    </StackPanel>
  </StackPanel>
  ...more items...
</ComboBox>


Obviously, displaying the type name of "System.Windows.Controls.StackPanel" in the selection box is not acceptable, so that’s where the TextSearch class comes in. TextSearch defines two attached properties that provide control over the text that gets displayed in an editable selection box.

A TextSearch.TextPath property can be attached to a ComboBox to designate the property (or subproperty) of each item to use as the selection box text. This works just like the DisplayMemberPath and SelectedValuePath properties; the only difference between these three properties is how the final value is used.

For each item in Listing 10.1, the obvious text to use in the selection box is the content of the first TextBlock because it contains the title (such as "Curtain Call" or "Fireworks"). Because the TextBlock is nested within two StackPanels, the desired property path involves referencing the inner StackPanel (the second child of each item) before referencing the TextBlock (the first child of each inner StackPanel). Therefore, the TextPath attached property can be applied to Listing 10.1 as follows:

<ComboBox IsEditable="True" TextSearch.TextPath="Children[1].Children[0].Text">
  ...
</ComboBox>

This is a bit fragile, however, because the property path will stop working if the structure of the items is changed. It also doesn’t handle heterogeneous items; any item that doesn’t match the structure of TextPath is displayed as an empty string in the selection box.

TextSearch’s other attached property, Text, is more flexible but must be applied to individual items in the ComboBox. You can set Text to the literal text you want to be displayed in the selection box for each item. It could be applied to Listing 10.1 as follows:

<ComboBox IsEditable="True">
  <!-- Item #1 -->
  <StackPanel TextSearch.Text="Curtain Call" Orientation="Horizontal" Margin="5">
    ...
  </StackPanel>
  <!-- Item #2 -->
  <StackPanel TextSearch.Text="Fireworks" Orientation="Horizontal" Margin="5">
    ...
  </StackPanel>
  ...more items...
</ComboBox>

You can use TextSearch.TextPath on the ComboBox and TextSearch.Text on individual items simultaneously. In this case, TextPath provides the default selection box representation, and Text overrides this representation for any marked items.

Figure 10.6 shows the result of using either TextSearch.TextPath or TextSearch.Text as in the preceding snippets.

Image

FIGURE 10.6 A proper-looking Office-style gallery, thanks to the use of TextSearch attached properties.


Tip

You can disable TextSearch by setting ItemsControl’s IsTextSearchEnabled property to false. ItemsControl’s IsTextSearchCaseSensitive property (which is false by default) controls whether the case of typing must match the case of the text.



Image FAQ: When the SelectionChanged event gets raised, how do I get the new selection?

The SelectionChanged event is designed to handle controls that allow multiple selections, so it can be a little confusing for a single-selection selector such as ComboBox. The SelectionChangedEventArgs type passed to event handlers has two properties of type IList: AddedItems and RemovedItems. AddedItems contains the new selection, and RemovedItems contains the previous selection. You can retrieve a new single selection as follows:

void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
  if (e.AddedItems.Count > 0)
    object newSelection = e.AddedItems[0];
}

And, like this code, you should never assume that there’s a selected item! Besides the fact that ComboBox’s selection can be cleared programmatically, it can get cleared by the user when IsEditable is true and IsReadOnly is false. In this case, if the user changes the selection box text to something that doesn’t match any item, the SelectionChanged event is raised with an empty AddedItems collection.


ComboBoxItem

ComboBox implicitly wraps each of its items in a ComboBoxItem object. (You can see this from code if you traverse up the visual tree from any of the items.) But you can explicitly wrap any item in a ComboBoxItem, which happens to be a content control. You can apply this to each item in Listing 10.1 as follows:

<!-- Item #1 -->
<ComboBoxItem TextSearch.Text="Curtain Call">
  <StackPanel Orientation="Horizontal" Margin="5">
    ...
  </StackPanel>
</ComboBoxItem>
<!-- Item #2 -->
<ComboBoxItem TextSearch.Text="Fireworks">
  <StackPanel Orientation="Horizontal" Margin="5">
    ...
  </StackPanel>
</ComboBoxItem>
...more items...

Notice that if you’re using the TextSearch.Text attached property, you need to move it to the ComboBoxItem element now that StackPanel is not the outermost element for each item. Similarly, the TextSearch.TextPath value used earlier needs to be changed to Content.Children[1].Children[0].Text.


Image FAQ: Why should I bother wrapping items in a ComboBoxItem?

ComboBoxItem exposes some useful properties—IsSelected and IsHighlighted—and useful events—Selected and Unselected. Using ComboBoxItem also avoids a quirky behavior with showing content controls in the selection box (when IsEditable is false): If an item in a ComboBox is a content control, the entire control doesn’t get displayed in the selection box. Instead, the inner content is extracted and shown. By using ComboBoxItem as the outermost content control, the inner content is now the entire control that you probably wanted to be displayed in the first place.

Because ComboBoxItem is a content control, it is also handy for adding simple strings to a ComboBox (rather than using something like TextBlock or Label). Here’s an example:

<ComboBox>
  <ComboBoxItem>Item 1</ComboBoxItem>
  <ComboBoxItem>Item 2</ComboBoxItem>
</ComboBox>


ListBox

The familiar ListBox control is similar to ComboBox, except that all items are displayed directly within the control’s bounds (or you can scroll to view additional items if they don’t all fit). Figure 10.7 shows a ListBox that contains the same items used in Listing 10.1.

Image

FIGURE 10.7 The WPF ListBox.

Probably the most important feature of ListBox is that it can support multiple simultaneous selections. This is controllable via the SelectionMode property, which accepts three values (from a SelectionMode enumeration):

Image Single—Only one item can be selected at a time, just like with ComboBox. This is the default value.

Image Multiple—Any number of items can be selected simultaneously. Clicking an unselected item adds it to ListBox’s SelectedItems collection, and clicking a selected item removes it from the collection.

Image Extended—Any number of items can be selected simultaneously, but the behavior is optimized for the single selection case. To select multiple items in this mode, you must hold down Shift (for contiguous items) or Ctrl (for noncontiguous items) while clicking. This matches the behavior of the Win32 ListBox control.

Just as ComboBox has its companion ComboBoxItem class, ListBox has a ListBoxItem class, as seen in earlier chapters. In fact, ComboBoxItem derives from ListBoxItem, which defines the IsSelected property and Selected and Unselected events.


Tip

The TextSearch technique shown with ComboBox in the preceding section is important for ListBox, too. For example, if the items in Figure 10.7 are marked with the appropriate TextSearch.Text values, then typing F while the ListBox has focus makes the selection jump to the Fireworks item. Without the use of TextSearch, pressing S would cause the items to get focus because that’s the first letter in System.Windows.Controls.StackPanel. (And that would be a weird user experience!)



Image FAQ: How can I get ListBox to scroll smoothly?

By default, ListBox scrolls on an item-by-item basis. Because the scrolling is based on each item’s height, it can look quite choppy if you have large items. If you want smooth scrolling, so each scrolling action shifts the items by a small number of pixels regardless of their heights, the easiest solution is to set the ScrollViewer.CanContentScroll attached property to false on the ListBox control, as shown previously in this chapter.

Be aware, however, that by making this change, you lose ListBox’s virtualization functionality. Virtualization refers to the optimization of creating child elements only when they become visible on the screen. Virtualization is possible only when using data binding to fill the control’s items, so setting CanContentScroll to false can negatively impact the performance of data-bound scenarios only.



Image FAQ: How can I sort items in a ListBox (or any other ItemsControl)?

Sorting can be done via a mechanism on the ItemsCollection object, so it applies equally to all ItemsControls. ItemsCollection has a SortDescriptions property that can hold any number of System.ComponentModel.SortDescription instances. Each SortDescription describes which property of the items should be used for sorting and whether the sort is in ascending or descending order. For example, the following code sorts a bunch of ContentControl items based on their Content property:

// Clear any existing sorting first
myItemsControl.Items.SortDescriptions.Clear();
// Sort by the Content property
myItemsControl.Items.SortDescriptions.Add(
  new SortDescription("Content", ListSortDirection.Ascending));



Image FAQ: How do I get the items in my ItemsControl to have automation IDs, as seen in tools such as UI Spy?

The easiest way to give any FrameworkElement an automation ID is to set its Name property, as that is used by default for automation purposes. However, if you want to give an element an ID that is different from its name, simply set the AutomationProperties.AutomationID attached property (from the System.Windows.Automation namespace) to the desired string.


ListView

The ListView control, which derives from ListBox, looks and acts just like a ListBox, except that it uses the Extended SelectionMode by default. But ListView also adds a property called View that enables you to customize the view in a richer way than choosing a custom ItemsPanel.

The View property is of type ViewBase, an abstract class. WPF ships with one concrete subclass, GridView. Its default experience is much like File Explorer’s Details view. (In fact, in beta versions of WPF, GridView was even called DetailsView.)

Figure 10.8 displays a simple ListView created from the following XAML, which assumes that the sys prefix corresponds to the System .NET namespace in mscorlib.dll:

<ListView>
<ListView.View>
  <GridView>
    <GridViewColumn Header="Date"/>
    <GridViewColumn Header="Day of Week"
      DisplayMemberBinding="{Binding DayOfWeek}"/>
    <GridViewColumn Header="Year" DisplayMemberBinding="{Binding Year}"/>
  </GridView>
</ListView.View>
  <sys:DateTime>1/1/2018</sys:DateTime>
  <sys:DateTime>1/2/2018</sys:DateTime>
  <sys:DateTime>1/3/2018</sys:DateTime>
</ListView>

Image

FIGURE 10.8 The WPF ListView, using GridView.

GridView has a Columns content property that holds a collection of GridViewColumn objects, as well as other properties to control the behavior of the column headers. WPF defines a ListViewItem element that derives from ListBoxItem. In this case, the DateTime objects are implicitly wrapped in ListViewItems because they are not used explicitly.

ListView’s items are specified as a simple list, as with ListBox, so the key to displaying different data in each column is the DisplayMemberBinding property of GridViewColumn. The idea is that ListView contains a complex object for each row, and the value for every column is a property or subproperty of each object. Unlike ItemsControl’s DisplayMemberPath property, however, DisplayMemberBinding requires the use of data binding techniques described in Chapter 13.

What’s nice about GridView is that it automatically supports some of the advanced features of File Explorer’s Details view:

Image You can reorder columns by dragging and dropping them.

Image You can resize columns by dragging the column separators.

Image You can cause columns to automatically resize to “just fit” their content by double-clicking their separators.

GridView doesn’t, however, support automatic sorting by clicking on a column header, which is an unfortunate gap in functionality. The code to sort items when a header is clicked is not complicated (you simply use the SortDescriptions property mentioned in the previous section), but you also have to manually create the little arrow in the header that typically indicates which column is being used for sorting and whether it’s an ascending or descending sort. Basically, ListView with GridView is a poor-man’s DataGrid. But because a real DataGrid control was introduced in WPF 4.0, the usefulness of the GridView control is diminished.

TabControl

The next selector, TabControl, is useful for switching between multiple pages of content. Figure 10.9 shows what a basic TabControl looks like. Tabs in a TabControl are typically placed on the top, but with TabControl’s TabStripPlacment property (of type Dock), you can also set their placement to Left, Right, or Bottom.

Image

FIGURE 10.9 The WPF TabControl.

TabControl is pretty easy to use. You simply add items, and each item is placed on a separate tab. Here’s an example:

<TabControl>
  <TextBlock>Content for Tab 1.</TextBlock>
  <TextBlock>Content for Tab 2.</TextBlock>
  <TextBlock>Content for Tab 3.</TextBlock>
</TabControl>

Much like ComboBox with ComboBoxItem, ListBox with ListBoxItem, and so on, TabControl implicitly wraps each item in its companion TabItem type. It’s unlikely that you’d add non-TabItem children directly to TabControl, however, because without an explicit TabItem there’s no way to label the corresponding tab. For example, the following XAML is the source for Figure 10.9:

<TabControl>
  <TabItem Header="Tab 1">Content for Tab 1.</TabItem>
  <TabItem Header="Tab 2">Content for Tab 2.</TabItem>
  <TabItem Header="Tab 3">Content for Tab 3.</TabItem>
</TabControl>

TabItem is a headered content control, so Header can be any arbitrary object, just like with GroupBox or Expander.

Unlike with the other selectors, with TabItem, the first item is selected by default. However, you can programmatically unselect all tabs by setting SelectedItem to null or SelectedIndex to -1.

DataGrid

DataGrid is a versatile control for displaying multicolumn rows of data that can be sorted, edited, and much more. It is optimized for easy hook-up to an in-memory database table (such as System.Data.DataTable in ADO.NET). Wizards in Visual Studio and technologies such as LINQ to SQL make this connection especially easy.

Listing 10.2 shows a DataGrid that directly contains a XAML-instantiated collection of two instances of the following custom Record type:

public class Record
{
  public string FirstName   { get; set; }
  public string LastName    { get; set; }
  public Uri Website        { get; set; }
  public bool IsBillionaire { get; set; }
  public Gender Gender      { get; set; }
}

where the Gender enumeration is defined as follows:

public enum Gender
{
  Male,
  Female
}

The five columns of data shown in Figure 10.10 (one for each property on the Record object) are defined in the Columns collection.

Image

FIGURE 10.10 The WPF DataGrid, as constructed in Listing 10.2.

LISTING 10.2 A DataGrid with Inline Data and a Variety of Column Types


<DataGrid IsReadOnly="True"
  xmlns:local="clr-namespace:Listing10_2"
  xmlns:sys="clr-namespace:System;assembly=mscorlib">

  <!-- Support for showing all genders in the DataGridComboBoxColumn: -->
  <DataGrid.Resources>
    <ObjectDataProvider x:Key="genderEnum" MethodName="GetValues"
      ObjectType="{x:Type sys:Enum}">
    <ObjectDataProvider.MethodParameters>
      <x:Type Type="local:Gender"/>
    </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
  </DataGrid.Resources>

  <!-- The columns: -->
  <DataGrid.Columns>
    <DataGridTextColumn Header="First Name" Binding="{Binding FirstName}"/>
    <DataGridTextColumn Header="Last Name" Binding="{Binding LastName}"/>
    <DataGridHyperlinkColumn Header="Website" Binding="{Binding Website}"/>
    <DataGridCheckBoxColumn Header="Billionaire?"
      Binding="{Binding IsBillionaire}"/>
    <DataGridComboBoxColumn Header="Gender" SelectedItemBinding="{Binding Gender}"
      ItemsSource="{Binding Source={StaticResource genderEnum}}"/>
  </DataGrid.Columns>

  <!-- The data: -->
  <local:Record FirstName="Adam" LastName="Nathan"
    Website="http://twitter.com/adamnathan" Gender="Male"/>
  <local:Record FirstName="Bill" LastName="Gates"
    Website="http://twitter.com/billgates" IsBillionaire="True" Gender="Male"/>

</DataGrid>


The DataGrid automatically supports reordering, resizing, and sorting the columns, but any or all of this functionality can be disabled by setting any of the following properties to false: CanUserReorderColumns, CanUserResizeColumns, CanUserResizeRows, and CanUserSortColumns. The grid lines and headers can be easily disabled via the GridLinesVisibility and HeadersVisibility properties.

Listing 10.2 highlights the main column types supported by DataGrid:

Image DataGridTextColumn—Perfect for strings, this column type displays a TextBlock for its normal display and a TextBox when the value is being edited.

Image DataGridHyperlinkColumn—Turns what would be plain text into a clickable hyperlink. However, note that there is no default behavior associated with clicking that link (such as opening a web browser). You must explicitly handle such actions.

Image DataGridCheckBoxColumn—Perfect for Boolean values, this column type displays a CheckBox to represent a true (checked) or false (unchecked) value.

Image DataGridComboBoxColumn—Perfect for enumerations, this column type displays a TextBlock for its normal display and a ComboBox filled with possible values when the value is being edited.

WPF has one more built-in column type:

Image DataGridTemplateColumn—Enables an arbitrary template to be set for a value’s normal display as well as its editing display. This is done by setting its CellTemplate and CellEditingTemplate properties.

Auto-Generated Columns

When DataGrid’s items are set via ItemsSource, it attempts to automatically generate appropriate columns. When this happens, DataGridTextColumn is automatically used for strings, DataGridHyperlinkColumn is automatically used for URIs, DataGridCheckBoxColumn is automatically used for Booleans, and DataGridComboBoxColumn is automatically used for enumerations (with an appropriate items source hooked up automatically).

Therefore, the following empty DataGrid:

<DataGrid Name="dataGrid" />

produces almost exactly the same result as Figure 10.10 when its ItemsSource is set as follows in code-behind:

dataGrid.ItemsSource = new Record[]
{
  new Record { FirstName="Adam", LastName="Nathan", Website=
    new Uri("http://twitter.com/adamnathan"), Gender=Gender.Male },
  new Record { FirstName="Bill", LastName="Gates", Website=
    new Uri("http://twitter.com/billgates"), Gender=Gender.Male,
    IsBillionaire=true }
};

The only visual difference is the labels used in the headers, which now match the corresponding property names. Figure 10.11 shows the result.

Image

FIGURE 10.11 The WPF DataGrid, with autogenerated columns that use Record’s property names as the header text.

Besides being much simpler to construct, the DataGrid in Figure 10.11 automatically supports editing of the fields in each item, unlike when the items were placed directly in DataGrid’s Items collection. Cells in the first three columns automatically turn into editable TextBoxes when clicked, the CheckBoxes are clickable, and cells in the Gender column automatically turn into a ComboBox with the appropriate values when clicked. Keyboard gestures such as pressing the spacebar or F2 can also be used on the cell that has keyboard focus. All edits, when committed, are reflected in the underlying ItemsSource collection. (Unfortunately, checking the IsBillionaire box next to my name did not cause any change to be reflected in my bank account. Perhaps this sample has a bug.)

If a DataGrid already has explicit columns defined, any autogenerated columns are placed after them. You can customize or remove individual autogenerated columns by handling the AutoGeneratingColumn event, which is raised once for each column. When all the columns have been generated, a single AutoGeneratedColumns event is raised. To disable autogenerated columns altogether, simply set DataGrid’s AutoGenerateColumns property to false.

Selecting Rows and/or Cells

DataGrid supports multiple selection modes controlled by two properties—SelectionMode and SelectionUnit. SelectionMode can be set to Single for single-item selection or Extended for multiple-item selection (the default behavior). The definition of “item” depends on the value of SelectionUnit. It can be set to any of the following:

Image Cell—Only individual cells can be selected.

Image FullRow—Only full rows can be selected. This is the default.

Image CellOrRowHeader—Either can be selected. (To select a full row, click a row header.)

When multiselection is enabled, the Shift key can be held down to select multiple contiguous items or the Ctrl key can be held down to select multiple noncontiguous items.

When rows are selected, the Selected event is raised and the SelectedItems property contains the items. For the DataGrid in Listing 10.2, these items would be the Record instances. When individual cells are selected, the SelectedCellChanged event is raised and the SelectedCells property contains a list of DataGridCellInfo structures that contain information about the relevant columns and data. Instances of DataGridRow and DataGridCell involved in the selection also raise their own Selected event and have an IsSelected property set to true.

Even if multiple cells or rows are selected, there is at most one cell that has focus at any time. You can get or set that cell with the CurrentCell property. In addition, the CurrentColumn property reveals the column containing CurrentCell, and CurrentItem contains the data item corresponding to CurrentCell’s row.

A lot of the support for bulk selection and selection transactions comes from the base MultiSelector class, which derives from Selector and was introduced in WPF 3.5. Other WPF controls support multiple selections, but DataGrid is the only one that derives from MultiSelector.

Additional Customizations

DataGrid supports a number of customizations easily, such as its interaction with the clipboard, virtualization, the ability to add extra details to rows, and the ability to “freeze” columns.

Clipboard Interaction

The data that gets copied to the clipboard from a DataGrid (such as when pressing Ctrl+C on a selection) can be customized with the ClipboardCopyMode property. It can be set to the following values:

Image ExcludeHeader—Column headers are not included in the copied text. This is the default.

Image IncludeHeader—Column headers are included in the copied text.

Image None—Nothing can be copied to the clipboard.

Virtualization

By default, DataGrid’s rows are virtualized (UIElements are not created for rows offscreen, and the underlying data might even be fetched lazily, depending on the data source), but its columns are not. You can alter this behavior by setting EnableRowVirtualization to false or EnableColumnVirtualization to true. EnableColumnVirtualization is not true by default because it can slow down the frame rate when doing horizontal scrolling.

Extra Row Details

DataGrid supports showing extended details on rows by setting the RowDetailsTemplate property. Here’s an example:

<DataGrid ...>
<DataGrid.RowDetailsTemplate>
  <DataTemplate>
    <TextBlock Margin="10" FontWeight="Bold">Details go here.</TextBlock>
  </DataTemplate>
</DataGrid.RowDetailsTemplate>
  ...
</DataGrid>

Ordinarily, the elements inside RowDetailsTemplate would use data binding to customize the contents for the current row, but this example uses a simple TextBlock. Figure 10.12 shows the result when selecting a row.

Image

FIGURE 10.12 Showing details on a selected row in a DataGrid.

By default, details are shown only for the selected row(s), but this behavior can be changed with the RowDetailsVisibilityMode property. It can be set to one of the following values:

Image VisibleWhenSelected—The row details are shown for only selected rows. This is the default value.

Image Visible—The row details are shown for every row.

Image Collapsed—The row details are not shown for any row.

Column Freezing

DataGrid supports “freezing” any number of columns, meaning that they never scroll out of view. This is a lot like freezing columns in Microsoft Excel. There are several limitations to this support: They can only be the leftmost columns, and frozen columns cannot be reordered among unfrozen columns (and vice versa).

To freeze one or more columns, you simply set the FrozenColumnCount property to a value other than its default value of 0. Figure 10.13 shows the DataGrid from Listing 10.2 but with FrozenColumnCount set to 2. The columns after the first two have been scrolled, which is why you can’t see the header text for the third column.

Image

FIGURE 10.13 The DataGrid from Listing 10.2 with FrozenColumnCount="2".

Editing, Adding, and Removing Data

We’ve already seen that editing the data in individual items works automatically with DataGrid’s ItemsSource. If the ItemsSource collection supports adding and removing items, then DataGrid automatically supports adding and removing items as well. With the previous example, wrapping the array in a List<Record> (so the static array is only used to initialize the dynamic list) is enough to enable this functionality:

dataGrid.ItemsSource = new List<Record>(
  new Record[]
  {
    new Record { FirstName="Adam", LastName="Nathan", Website=
      new Uri("http://twitter.com/adamnathan"), Gender=Gender.Male },
    new Record { FirstName="Bill", LastName="Gates", Website=
      new Uri("http://twitter.com/billgates"), Gender=Gender.Male,
      IsBillionaire=true }
  }
);


Image FAQ: Can I freeze rows in a DataGrid?

No, there is no built-in support for that. The only other things that can be automatically frozen are row details. When AreRowDetailsFrozen is true, any row details that are shown do not scroll horizontally.


This gives the DataGrid an extra blank row at the bottom, so a new entry can be added at any time. DataGrid defines methods and commands for the common actions of beginning an edit (bound to F2), cancelling an edit (bound to Esc), committing an edit (bound to Enter), and deleting a row (bound to Delete).

IsReadOnly can be set to true to prevent editing, and CanUserAddRows/CanUserDeleteRows can be set to false to prevent adding and deleting. Listing 10.2 sets IsReadOnly to true to avoid exceptions, as the inline collection of Record objects does not support editing. Although editing (and switching a cell to editing mode) happens automatically, several events are raised during the process to customize the behavior: PreparingCellForEdit, BeginningEdit, CellEditEnding/RowEditEnding, and InitializeNewItem.


Warning: CanUserAddRows and CanUserDeleteRows can be automatically changed to false!

Depending on the values of other properties, CanUserAddRows and CanUserDeleteRows can become false even if you explicitly set them to true! For example, if DataGrid’s IsReadOnly or IsEnabled properties are set to false, these two previously mentioned properties become false. But even more subtly, if the data source doesn’t support adding and removing—ultimately revealed by IEditableCollectionView’s CanAddNew and CanRemove properties—then the two properties also become false. See Chapter 13 for more information about collection views such as IEditableCollectionView.


Ribbon

The Ribbon, new to WPF 4.5, acts like the ribbon user interface featured in Microsoft Office programs and other desktop programs such as Paint and WordPad. Although you can use arbitrary elements for items in a Ribbon (as it is a Selector), it only works as expected when you use a specific hierarchy of types described in this section:

Image Ribbon should contain one or more RibbonTabs

Image Each RibbonTab (another ItemsControl) should contain one or more RibbonGroups

Image Each RibbonGroup (yet another ItemsControl) should contain “ribbon versions” of otherwise-familiar controls: RibbonButton, RibbonCheckBox, and so on

Figure 10.14 shows the following Ribbon that conforms to these rules:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Class="Chapter10.MainWindow" Title="MainWindow">
  <Grid>
    <Ribbon Title="My Ribbon">
      <RibbonTab Header="Home">
        <RibbonGroup Header="Clipboard" SmallImageSource="Clipboard.png">
          <RibbonSplitButton LargeImageSource="Paste.png" Label="Paste">
            <RibbonMenuItem Header="Paste _Special..." />
            <RibbonMenuItem Header="Set Def_ault Paste..." />
          </RibbonSplitButton>
          <RibbonButton SmallImageSource="Cut.png" Label="Cut" />
          <RibbonButton SmallImageSource="Copy.png" Label="Copy" />
          <RibbonButton SmallImageSource="FormatPainter.png"
                        Label="Format Painter" />
        </RibbonGroup>
      </RibbonTab>
    </Ribbon>
  </Grid>
</Window>

Image

FIGURE 10.14 A one-tab, one-group Ribbon mimics part of the Word user interface.


Tip

To use Ribbon, you must add a reference to System.Windows.Controls.Ribbon.dll, which is not referenced by default in Visual Studio projects.


RibbonTab is the ribbon analog of TabItem (not TabControl). It has a Header property that populates its corresponding tab inside its host Ribbon, an IsSelected property, and—unlike most WPF controls—reveals the horizontal coordinates of its tab with TabHeaderLeft and TabHeaderRight properties of type double. Because it is meant to contain a collection of RibbonGroups, it is a headered items control (deriving from HeaderedItemsControl) rather than a headered content control like TabItem.

RibbonGroup is also a headered items control. Its header is rendered at the bottom (“Clipboard” in Figure 10.14), and its items are arranged in interesting ways that match common layouts used in Microsoft Office and other ribbon-enabled programs.

Although the style of Ribbon and its controls match Office 2010 rather than Office 2013, that can be remedied with the techniques from Chapter 14.

You can set Ribbon’s HelpPaneContent to arbitrary content to make it render in the top corner opposite the application menu, as follows:

<Ribbon Title="My Ribbon">
  <Ribbon.HelpPaneContent>
    <RibbonButton SmallImageSource="Help.png"/>
  </Ribbon.HelpPaneContent>
  ...
</Ribbon>

The result is shown in Figure 10.15. You can use any elements here, although ribbon controls like RibbonButton look best.

Image

FIGURE 10.15 HelpPaneContent is rendered in the upper-right area of the Ribbon.

Ribbon can be “minimized” so the tabs look more like menus and the content underneath only appears (in a popup) when a tab is clicked. The user can do this by double-clicking a tab or right-clicking the Ribbon then clicking Minimize the Ribbon, or you can do it programmatically with the IsMinimized property. A separate IsDropDownOpen property tells you if one of the tabs is currently opened when in this minimized state, or you can set it yourself to force the selected tab to be open or closed. In addition, a Ribbon can be “collapsed,” which makes it disappear completely, by setting IsCollapsed to true. (This is no different than setting its Visibility to Collapsed.) A Ribbon automatically collapses when its parent becomes narrow or short enough. Ribbon’s Collapsed and Expanded events can be used to track such changes.

Ribbon works in conjunction with many types in order to provide the features one would expect when mimicking Office, examined in the following sections:

Image Ribbon Controls

Image Resizing Behavior

Image Key Tips

Image Application Menu

Image Quick Access Toolbar

Image Contextual Tabs

Image ScreenTips

Image Galleries

By now you might be wondering why Ribbon has a goofy-looking bar above it that contains a title. That area is there to contain elements used by the Quick Access Toolbar and contextual tabs features (covered later). In Office applications, these appear in the window’s chrome instead. Fortunately, you can easily enable the same integration into your own window’s chrome—and get rid of the extra bar—by hosting your Ribbon inside a RibbonWindow instead of a Window or NavigationWindow. (When you do that, Ribbon’s Title property is used for the window title, but only if RibbonWindow’s own Title property is null.) Figure 10.16 shows the result of changing the base class in the code-behind file:

public partial class MainWindow : RibbonWindow
{
  ...
}

Image

FIGURE 10.16 RibbonWindow enables the Ribbon to integrate into the window’s chrome, resulting in a user interface that wastes less space and matches Microsoft Office.

And making this change to the XAML file:

<RibbonWindow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Class="Chapter10.MainWindow" Title="MainWindow">
  <Grid>
    <Ribbon Title="My Ribbon">
      <RibbonTab Header="Home">
        <RibbonGroup Header="Clipboard" SmallImageSource="Clipboard.png">
          <RibbonSplitButton LargeImageSource="Paste.png" Label="Paste">
            <RibbonMenuItem Header="Paste _Special..." />
            <RibbonMenuItem Header="Set Def_ault Paste..." />
          </RibbonSplitButton>
          <RibbonButton SmallImageSource="Cut.png" Label="Cut" />
          <RibbonButton SmallImageSource="Copy.png" Label="Copy" />
          <RibbonButton SmallImageSource="FormatPainter.png"
                        Label="Format Painter" />
        </RibbonGroup>
      </RibbonTab>
    </Ribbon>
  </Grid>
</RibbonWindow>

The resulting chrome has several frustrating flaws. The icon rendering is inexplicably bad, the text is rendered differently, isn’t centered, and gets ellipsized too late when the window shrinks, plus the title and the text and icon get slightly truncated when the window is maximized on Windows 8 or later. Still, this is perhaps a small price to pay to get rid of the extra bar and integrate the Quick Access Toolbar into the chrome. Besides, when a Ribbon is hosted in a RibbonWindow (which you can determine programmatically with Ribbon’s IsHostedInRibbonWindow property), you can remove the icon by setting Ribbon’s WindowIconVisibility property to Collapsed. (You could also set it to Hidden, but then it still occupies space before the window title.)

Ribbon Controls

The System.Windows.Controls.Ribbon namespace contains ribbon variations of many familiar controls, such as RibbonButton, RibbonCheckBox, RibbonRadioButton, RibbonTextBox, RibbonToggleButton, RibbonComboBox, and so on. There are also ribbon variations of controls introduced later in the chapter, such as RibbonSeparator instead of Separator, and some unique controls such as RibbonSplitButton (half-Button, half-Menu) and RibbonTwoLineText, a label that can evenly split its text into two lines and even show an inline shape.

So why are so many built-in controls redefined? There are two reasons. One is to fit the expected styling and layout of the Ribbon without requiring the developer to do anything special. The other is to enable tight integration with Ribbon features such as key tips and the Quick Access Toolbar, covered later. Figure 10.17 shows several of the ribbon controls from the following XAML:

<RibbonWindow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="Chapter10.MainWindow">
  <Grid>
    <Ribbon Title="My Ribbon">
      <RibbonTab Header="Controls">
        <RibbonGroup Header="Buttons">
          <RibbonButton Label="RibbonButton" SmallImageSource="Star.png"/>
          <RibbonRadioButton Label="RibbonRadioButton"
                             SmallImageSource="Star.png" IsChecked="True"/>
          <RibbonToggleButton Label="RibbonToggleButton"
                              SmallImageSource="Star.png" IsChecked="True"/>
          <RibbonSplitButton Label="RibbonSplitButton"
                             SmallImageSource="Star.png"/>
          <RibbonCheckBox Label="RibbonCheckBox" SmallImageSource="Star.png"
                          IsChecked="True"/>
        </RibbonGroup>
        <RibbonGroup Header="Other">
          <RibbonTextBox Label="RibbonTextBox" SmallImageSource="Star.png"
                         Text="the text"/>
          <RibbonComboBox Label="RibbonComboBox" SmallImageSource="Star.png"/>
          <RibbonTwoLineText Text="Line One Line Two" HasTwoLines="True"
                             PathFill="Red" PathStroke="Blue">
            <RibbonTwoLineText.PathData>
              <RectangleGeometry Rect="0,-10,20,20"/>
            </RibbonTwoLineText.PathData>
          </RibbonTwoLineText>
        </RibbonGroup>
      </RibbonTab>
    </Ribbon>
  </Grid>
</RibbonWindow>

Image

FIGURE 10.17 An example of several ribbon controls.

Almost all of the ribbon controls have consistent Label, SmallImageSource, and LargeImageSource properties. SmallImageSource is meant to be set to a 16x16 pixel-image and LargeImageSource is meant to be set to a 32x32-pixel image, although any images you provide will be scaled if the size isn’t correct. Setting LargeImageSource on most controls make it render in a larger form. For example, adding LargeImageSource to the RibbonButton in Figure 10.17 as follows produces the result in Figure 10.18:

<RibbonButton Label="RibbonButton" SmallImageSource="Star.png"
              LargeImageSource="Star.png"/>

Image

FIGURE 10.18 RibbonButton with LargeImageSource set to the same image used for SmallImageSource.

In some cases, the images are only used in specialized situations. For example, the use for SmallImageSource on RibbonGroup will be seen later.

Ribbon controls have several properties that are shortcuts for altering their style, such as MouseOverBackground and MouseOverBorderBrush. Ribbon itself has several such properties that impact all ribbon controls within:

Image CheckedBackground and CheckedBorderBrush

Image FocusedBackground and FocusedBorderBrush

Image MouseOverBackground and MouseOverBorderBrush

Image PressedBackground and PressedBorderBrush

Figure 10.19 shows the impact of setting a few of these properties, as well as changing the Ribbon’s Background.

Image

FIGURE 10.19 Figure 10.17 with CheckedBackground=Yellow, MouseOverBackground=Lime, and Background=Pink.


Warning: The Content property isn’t used on ribbon controls!

Although many controls, such as RibbonButton, ultimately derive from ContentControl, they use Label and SmallImageSource/LargeImageSource for their content. Setting the Content property does nothing. Although this is confusing at first, it’s nice to have such consistency among all the Ribbon controls.


The ribbon controls don’t actually share a common base class. RibbonButton derives from Button, RibbonCheckBox derives from CheckBox, and so on. So the common properties are duplicated on each class.

Resizing Behavior

Ribbon contains rich functionality to dynamically adjust its content as the width of the host Window changes. The idea is to show as much content as possible at all times, so the appearance of controls inside RibbonGroups can be “reduced” when space is limited rather than being truncated. Controls support three sizes:

Image Large: LargeImageSource and Label are rendered

Image Medium: SmallImageSource and Label are rendered

Image Small: SmallImageSource is rendered, but without a Label

Controls are reduced from right to left (or left to right for the relevant cultures), although the exact algorithm is a little complex. As the width of a Window starts to decrease, each RibbonGroup, from right to left, takes a turn reducing its appearance by one “step.” If the Window width keeps decreasing after the left-most RibbonGroup has reduced itself, the turns start over from right to left.

Each time a RibbonGroup reduces itself, it does so in a right-to-left manner. For a RibbonGroup with three columns of controls, the right-most column will be reduced first, followed by the middle column (but only after all other RibbonGroups have been reduced once), followed by the left column (only after all RibbonGroups have been reduced twice). If a RibbonGroup needs to reduce but it can’t reduce its controls any further, it shrinks into a RibbonMenuButton whose image is set to the RibbonGroup’s SmallImageSource (not seen otherwise), and its Label is set to the RibbonGroup’s Label. In its dropdown menu, the content becomes the fully-expanded RibbonGroup. (RibbonGroup has an IsDropDownOpen property for detecting this condition.) Figure 10.20 demonstrates this by taking the XAML from Figure 10.16 and pasting the Clipboard RibbonGroup three more times, to force these conditions.

Image

FIGURE 10.20 Due to limited width, two RibbonGroups are fully reduced, and two RibbonGroups have their right-most RibbonButtons reduced from medium to small.


Tip

If you’re testing Ribbon resizing, make sure it has enough content so you can see the impact. Recall that the Ribbon collapses entirely once the host Window gets narrow enough!


By default, Ribbon’s resizing behaves reasonably and consistent with user expectations, but you can customize the behavior in a number of ways. You can do this for an entire RibbonTab, on individual RibbonGroups inside each tab, or on individual ribbon controls.

Customizing RibbonTab Resizing

RibbonTab has a GroupSizeReductionOrder property that can be set to a collection of RibbonGroup names. (In XAML, this can be a comma-delimited string.) These names are the values of their Name properties; not their Labels. The order of the names represents the order that the groups are reduced. Strangely, this does not impact the final reduction of RibbonGroups into their RibbonMenuButton form. It only impacts all the reduction up until that point.

The following XAML reverses the order of the initial RibbonGroup reductions to be left-to-right regardless of culture, as shown in Figure 10.21:

<RibbonWindow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="Chapter10.MainWindow" Title="MainWindow">
  <Grid>
    <Ribbon Title="My Ribbon">
      <RibbonTab Header="Home"
        GroupSizeReductionOrder="Clipboard1, Clipboard2, Clipboard3, Clipboard4">
        <RibbonGroup Name="Clipboard1"
                     Header="Clipboard" SmallImageSource="Clipboard.png">
          ...
        </RibbonGroup>
        <RibbonGroup Name="Clipboard2"
                     Header="Clipboard" SmallImageSource="Clipboard.png">
          ...
        </RibbonGroup>
        <RibbonGroup Name="Clipboard3"
                     Header="Clipboard" SmallImageSource="Clipboard.png">
          ...
        </RibbonGroup>
        <RibbonGroup Name="Clipboard4"
                     Header="Clipboard" SmallImageSource="Clipboard.png">
          ...
        </RibbonGroup>
      </RibbonTab>
    </Ribbon>
  </Grid>
</RibbonWindow>

Image

FIGURE 10.21 The left-most RibbonGroup is reduced first due to the custom GroupSizeReductionOrder specified on the RibbonTab.

Customizing RibbonGroup Resizing

Whereas RibbonTab enables you to customize the order in which RibbonGroups take turns reducing, RibbonGroup enables you to customize what happens when it takes a turn, as well as how many turns it takes to collapse into a RibbonMenuButton.

This customization is done with RibbonGroup’s GroupSizeDefinitions property, which can be set to a collection of RibbonGroupSizeDefinition objects, one per turn. Each RibbonGroupSizeDefinition contains a set of RibbonControlSizeDefinition objects, one per control. Each RibbonControlSizeDefinition has two properties that define the size of the corresponding control: ImageSize and IsLabelVisible. Here is how the combinations of possible values change the size of the control:

Image ImageSize=Large and IsLabelVisible=True: The “large” size described earlier

Image ImageSize=Large and IsLabelVisible=False: The large image with no label

Image ImageSize=Small and IsLabelVisible=True: The “medium” size described earlier

Image ImageSize=Small and IsLabelVisible=False: The “small” size described earlier

Image ImageSize=Collapsed and IsLabelVisible=True: The label with no image

Image ImageSize=Collapsed and IsLabelVisible=False: Not rendered at all

Here is an example of how to set GroupSizeDefinitions on one of the Clipboard RibbonGroups:

<RibbonGroup Header="Clipboard" SmallImageSource="Clipboard.png">
  <RibbonGroup.GroupSizeDefinitions>
    <!-- The fully-expanded sizes -->
    <RibbonGroupSizeDefinition>
      <!-- Paste -->
      <RibbonControlSizeDefinition ImageSize="Large" IsLabelVisible="True" />
      <!-- Cut -->
      <RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="True" />
      <!-- Copy -->
      <RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="True" />
      <!-- Format Painter -->
      <RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="True" />
    </RibbonGroupSizeDefinition>

    <!-- One level of reduction -->
    <RibbonGroupSizeDefinition>
      <!-- Paste -->
      <RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="True" />
      <!-- Cut -->
      <RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="True" />
      <!-- Copy -->
      <RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="True" />
      <!-- Format Painter -->
      <RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="False" />
    </RibbonGroupSizeDefinition>

    <!-- The final reduction -->
    <RibbonGroupSizeDefinition IsCollapsed="True" />
  </RibbonGroup.GroupSizeDefinitions>
  <RibbonSplitButton LargeImageSource="Paste.png" Label="Paste">
    <RibbonMenuItem Header="Paste _Special..." />
    <RibbonMenuItem Header="Set Def_ault Paste..." />
  </RibbonSplitButton>
  <RibbonButton SmallImageSource="Cut.png" Label="Cut" />
  <RibbonButton SmallImageSource="Copy.png" Label="Copy" />
  <RibbonButton SmallImageSource="FormatPainter.png" Label="Format Painter" />
</RibbonGroup>

The first RibbonGroupSizeDefinition defines the fully-expanded state of the RibbonGroup. Each RibbonControlSizeDefinition is applied to the RibbonGroup’s items in order. Extra RibbonControlSizeDefinitions are ignored, and if you don’t specify enough, the default behavior is applied to the remaining controls.

The second RibbonGroupSizeDefinition defines what happens to the controls when the RibbonGroup is reduced once, as shown in Figure 10.22. Notice that the Paste control has no image because the RibbonSplitButton doesn’t have a SmallImageSource specified. It could be set to the same Paste.png image, although the 32x32 image would be scaled down to 16x16, so the best approach is to assign a unique image to both properties so the control looks ideal in every state.

Image

FIGURE 10.22 A custom reduced state specified with a RibbonGroupSizeDefinition.

The last RibbonGroupSizeDefinition applies the final reduction because IsCollapsed is set to true. When this is done, the RibbonGroupSizeDefinition must not contain any RibbonControlSizeDefinitions. If it does, the IsCollapsed setting is ignored.

Customizing Ribbon Control Resizing

Most ribbon controls have a ControlSizeDefinition property that can be set to a single RibbonControlSizeDefinition instance just like the ones used in the preceding example. This is simply a way to specify the initial size of each control directly on the control rather than on its containing RibbonGroup. However, as the following XAML shows, this feature only works if the containing RibbonGroup has the first item in its GroupSizeDefinitions collection set to an “empty” RibbonGroupSizeDefinition:

<RibbonGroup Header="Clipboard" SmallImageSource="Clipboard.png">

  <!-- This must be here to make the control-specific settings work: -->
  <RibbonGroup.GroupSizeDefinitions>
    <RibbonGroupSizeDefinition/>
  </RibbonGroup.GroupSizeDefinitions>

  <RibbonSplitButton LargeImageSource="Paste.png" Label="Paste">
    <!-- Override the default behavior: -->
    <RibbonSplitButton.ControlSizeDefinition>
      <RibbonControlSizeDefinition ImageSize="Collapsed" IsLabelVisible="False"/>
    </RibbonSplitButton.ControlSizeDefinition>
    <RibbonMenuItem Header="Paste _Special..." />
    <RibbonMenuItem Header="Set Def_ault Paste..." />
  </RibbonSplitButton>
  <RibbonButton SmallImageSource="Cut.png" Label="Cut">
    <!-- Override the default behavior: -->
    <RibbonButton.ControlSizeDefinition>
      <RibbonControlSizeDefinition ImageSize="Large" IsLabelVisible="False"/>
    </RibbonButton.ControlSizeDefinition>
  </RibbonButton>
  <RibbonButton SmallImageSource="Copy.png" Label="Copy" />
  <RibbonButton SmallImageSource="FormatPainter.png" Label="Format Painter" />
</RibbonGroup>

In addition, you can lump together multiple ribbon controls by wrapping them in a RibbonControlGroup, an ItemsControl. RibbonControlGroup exists solely for the purpose of resizing multiple controls simultaneously. If a RibbonControlGroup’s ControlSizeDefinition property is set, it applies to all items inside. Even if you don’t set any explicit sizes, items in a RibbonControlGroup naturally stay the same size as each other. This is demonstrated by the following XAML, rendered in Figure 10.23:

<RibbonGroup Header="Clipboard" SmallImageSource="Clipboard.png">
  <RibbonControlGroup>
    <RibbonSplitButton LargeImageSource="Paste.png" Label="Paste">
      <RibbonMenuItem Header="Paste _Special..." />
      <RibbonMenuItem Header="Set Def_ault Paste..." />
    </RibbonSplitButton>
    <RibbonButton SmallImageSource="Cut.png" Label="Cut" />
    <RibbonButton SmallImageSource="Copy.png" Label="Copy" />
    <RibbonButton SmallImageSource="FormatPainter.png" Label="Format Painter" />
  </RibbonControlGroup>
</RibbonGroup>

Image

FIGURE 10.23 All controls in a RibbonControlGroup remain the same size as each other.

Key Tips

Key tips are the ribbon approach to shortcut keys. You can assign a ribbon control’s KeyTip property to one or more characters, and Ribbon will support it as a keyboard shortcut. The first time the user presses the Alt key, tooltips appear that reveal the first level of key tips: those for the RibbonTabs and any items in the Quick Access Toolbar. If the user presses the key sequence for a specific RibbonTab, then the tooltips are shown for items within that RibbonTab, at which point the user can type the shortcut for a specific control. This either invokes the control, such as clicking a RibbonButton, or gives the control focus, as is the case with a RibbonTextBox.

The following XAML produces the results shown in Figure 10.24 through Figure 10.26:

<RibbonWindow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="Chapter10.MainWindow">
  <Grid>
    <Ribbon Title="My Ribbon">
      <RibbonTab Header="Home" KeyTip="H">
        <RibbonGroup Header="Clipboard" SmallImageSource="Clipboard.png">
          <RibbonSplitButton LargeImageSource="Paste.png" Label="Paste"
                             KeyTip="V">
            <RibbonMenuItem Header="Paste _Special..." />
            <RibbonMenuItem Header="Set Def_ault Paste..." />
          </RibbonSplitButton>
          <RibbonButton SmallImageSource="Cut.png" Label="Cut" KeyTip="X"/>
          <RibbonButton SmallImageSource="Copy.png" Label="Copy" KeyTip="C"/>
          <RibbonButton SmallImageSource="FormatPainter.png"
                        Label="Format Painter" KeyTip="FP"/>
        </RibbonGroup>
      </RibbonTab>
    </Ribbon>
  </Grid>
</RibbonWindow>

Image

FIGURE 10.24 After pressing the Alt key, the “H” key tip is shown for the Home tab.

Image

FIGURE 10.25 After pressing Alt+H, the key tips within the Home tab are shown.

Image

FIGURE 10.26 After pressing Alt+V, the key tips within the Paste RibbonSplitButton are shown.

Notice in Figure 10.26, key tips are shown for items that don’t actually have their KeyTip properties set. When no KeyTip is set, the standard keyboard shortcut mechanism (that shows an underlined letter) is used. Both of these RibbonMenuItems happen to use this mechanism by including an underscore in their text.


Tip

If you have an element in a Ribbon that isn’t a ribbon control, you can still apply a key tip to it by marking it with the KeyTipService.KeyTip attached property. This class exposes advanced ways to interact with key tips, including a KeyTipStyle attached property and ActivatingKeyTip, KeyTipAccessed, and PreviewKeyTipAccessed attached events.


Application Menu

RibbonApplicationMenu is a subclass of RibbonMenuButton that can be used to populate the special first “tab” of the Ribbon. It is an ItemsControl meant to be filled with a list of RibbonApplicationMenuItems or RibbonApplicationSplitMenuItems. It also exposes two properties that can be set to arbitrary content around the list of items: AuxiliaryPaneContent and FooterPaneContent. You associate it with the Ribbon by setting it as the value of its ApplicationMenu property. The following XAML demonstrates this, producing the result in Figure 10.27:

<RibbonWindow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="Chapter10.MainWindow">
  <Grid>
    <Ribbon Title="My Ribbon">
      <Ribbon.ApplicationMenu>
        <RibbonApplicationMenu SmallImageSource="Star.png">
          <RibbonApplicationMenu.AuxiliaryPaneContent>
            <Button>Auxiliary</Button>
          </RibbonApplicationMenu.AuxiliaryPaneContent>
          <RibbonApplicationMenu.FooterPaneContent>
            <Button>Footer</Button>
          </RibbonApplicationMenu.FooterPaneContent>
          <RibbonApplicationMenuItem Header="Simple Menu Item"/>
          <RibbonApplicationSplitMenuItem Header="Split Menu Item">
            <RibbonApplicationMenuItem Header="one"/>
            <RibbonApplicationMenuItem Header="two"/>
            <RibbonApplicationMenuItem Header="three"/>
          </RibbonApplicationSplitMenuItem>
          <RibbonApplicationMenuItem Header="Menu Item with Items">
            <RibbonApplicationMenuItem Header="one"/>
            <RibbonApplicationMenuItem Header="two"/>
            <RibbonApplicationMenuItem Header="three"/>
          </RibbonApplicationMenuItem>
        </RibbonApplicationMenu>
      </Ribbon.ApplicationMenu>
      <RibbonTab Header="Home" KeyTip="H">
        ...
      </RibbonTab>
    </Ribbon>
  </Grid>
</RibbonWindow>

Image

FIGURE 10.27 The custom RibbonApplicationMenu controls the behavior of the special “first tab.”

Quick Access Toolbar

Setting Ribbon’s QuickAccessToolBar property to an instance of the RibbonQuickAccessToolBar control enables you to place arbitrary elements in RibbonWindow’s chrome. RibbonQuickAccessToolBar is an ItemsControl, so it can be filled as follows to produce the result in Figure 10.28:

<RibbonWindow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="Chapter10.MainWindow">
  <Grid>
    <Ribbon Name="Ribbon" Title="My Ribbon">
      <Ribbon.QuickAccessToolBar>
        <RibbonQuickAccessToolBar>
          <RibbonButton QuickAccessToolBarId="cut" SmallImageSource="Cut.png"
                        Label="Cut" KeyTip="X"/>
          <RibbonButton QuickAccessToolBarId="copy" SmallImageSource="Copy.png"
                        Label="Copy" KeyTip="C"/>
        </RibbonQuickAccessToolBar>
      </Ribbon.QuickAccessToolBar>
      <RibbonTab Header="Home" KeyTip="H">
        <RibbonGroup Header="Clipboard" SmallImageSource="Clipboard.png">
          <RibbonSplitButton LargeImageSource="Paste.png" Label="Paste"
                             KeyTip="V">
            <RibbonMenuItem Header="Paste _Special..." />
            <RibbonMenuItem Header="Set Def_ault Paste..." />
          </RibbonSplitButton>
          <RibbonButton QuickAccessToolBarId="cut" SmallImageSource="Cut.png"
                        Label="Cut" KeyTip="X"/>
          <RibbonButton QuickAccessToolBarId="copy" SmallImageSource="Copy.png"
                        Label="Copy" KeyTip="C"/>
          <RibbonButton SmallImageSource="FormatPainter.png"
                        Label="Format Painter" KeyTip="FP"/>
        </RibbonGroup>
      </RibbonTab>
    </Ribbon>
  </Grid>
</RibbonWindow>

Image

FIGURE 10.28 The Quick Access Toolbar shows Cut and Copy buttons in the window chrome.

The pattern for using RibbonQuickAccessToolBar is not just to duplicate ribbon controls inside it, but to give each control and its corresponding “original” control a matching string value for the QuickAccessToolBarId property. That’s because Ribbon provides a context menu for adding/removing items from the Quick Access Toolbar. Without these IDs, it doesn’t know whether a particular control is logically “in” the Quick Access Toolbar. In Figure 10.29, you can see that the option to add the Cut button to the Quick Access Toolbar is disabled. This is thanks to the QuickAccessToolBarId of cut assigned to both controls.

Image

FIGURE 10.29 QuickAccessToolBarIds keep the state of the first context menu item correct.


Tip

The built-in “Add to Quick Access Toolbar” menu item is only enabled for controls with an assigned QuickAccessToolBarId (and not already in the Quick Access Toolbar).


By setting Ribbon’s ShowQuickAccessToolBarOnTop property to false, you can move the Quick Access Toolbar to a region that appears below the Ribbon, just like the “Show Below the Ribbon” option in Microsoft Office or the “Show Quick Access Toolbar below the Ribbon” option shown in Figure 10.29.

Items added and removed by the user work just fine with items that you manually add to the Quick Access Toolbar. The key is to assign every relevant ribbon control a QuickAccessToolBarId.


Tip

You likely want to persist the state of the Quick Access Toolbar so you can restore on the next application launch. To do this, you only need the relevant QuickAccessToolBarIds. In your code that initially populates the RibbonQuickAccessToolBar, you cannot add the same control instance to a RibbonGroup and the QuickAccessToolBar, because an exception will be thrown due to attempting to give the same element multiple logical parents. Instead, you must construct a copy of the control and give it the matching QuickAccessToolBarId.

For the copy of ribbon controls inside the Quick Access Toolbar, their read-only IsInQuickAccessToolBar property is true. Note the property is always false for the original controls that stay in the Ribbon, even though it is logically “in” the Quick Access Toolbar.


If you want to customize the appearance of an item in the Quick Access Toolbar, you can assign a control’s QuickAccessToolBarControlSizeDefinition property to an instance of a RibbonControlSizeDefinition object discussed earlier. If you want the user to have the ability to add an entire RibbonGroup to the Quick Access Toolbar, this can be done simply by assigning its QuickAccessToolBarId. The result of this is shown in Figure 10.30.

Image

FIGURE 10.30 A whole RibbonGroup can be added to the Quick Access Toolbar.

You can set RibbonQuickAccessToolBar’s CustomizeMenuButton to an instance of a RibbonMenuButton to plug in an arbitrary customization user interface. Without this feature, you could simply add a RibbonMenuButton to RibbonQuickAccessToolBar’s items, but the user would be able to remove it and it wouldn’t stay on the right if the user added more items to the Quick Access Toolbar.

Contextual Tabs

A contextual tab is a RibbonTab that appears or disappears based on the current context. Such tabs get a special visual treatment that signify this temporary nature. Turning any RibbonTab into a contextual tab is easy. You must set its ContextualTabGroupHeader property to a string, and then add a RibbonContextualTabGroup object with a matching Header value to Ribbon’s ContextualTabGroups collection. This is done as follows, producing the result in Figure 10.31:

<RibbonWindow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="Chapter10.MainWindow">
  <Grid>
    <Ribbon Name="Ribbon" Title="My Ribbon">
      <Ribbon.ContextualTabGroups>
        <RibbonContextualTabGroup Header="This Tab Isn't Always Shown"
                                  Visibility="Visible" Background="Red" />
      </Ribbon.ContextualTabGroups>
      <RibbonTab Header="Home" KeyTip="H"
                 ContextualTabGroupHeader="This Tab Isn't Always Shown">
        ...
      </RibbonTab>
    </Ribbon>
  </Grid>
</RibbonWindow>

Image

FIGURE 10.31 The Home tab has been turned into a contextual tab.

You can control the appearance of the contextual tab embellishment with RibbonContextualTabGroup’s Background property. To show and hide the contextual tab based on context, you should have code-behind toggle the value of RibbonContextualTabGroup’s Visibility property at the appropriate times. When RibbonContextualTabGroup’s Visibility is set to Collapsed, or if a RibbonTab’s ContextualTabGroupHeader value doesn’t match any headers in the Ribbon’s collection, the RibbonTab is removed.


Tip

You can add multiple tabs to the same RibbonContextualTabGroup by assigning all of them the same, matching value for ContextualTabGroupHeader.


ScreenTips

Ribbon controls have the usual ToolTip property (inherited from FrameworkElement), and you can set that in the usual manner. However, by setting it to an instance of a RibbonToolTip control, you can take advantage of a predefined template just by setting a few simple properties. This looks like the kind of ScreenTip mentioned in the preceding chapter. Here’s an example, rendered in Figure 10.32:

<RibbonButton SmallImageSource="FormatPainter.png" Label="Format Painter"
              KeyTip="FP">
  <RibbonButton.ToolTip>
    <RibbonToolTip
        Title="Format Painter (Ctrl+Shift+C)"
        Description="Copy formatting from one place and apply it to another."
        ImageSource="FormatPainter.png"
        FooterTitle="Press F1 for more help."
        FooterDescription="This will open your web browser."
        FooterImageSource="Help.png" />
  </RibbonButton.ToolTip>
</RibbonButton>

Image

FIGURE 10.32 A RibbonToolTip provides a fancy ScreenTip with minimal effort.

One quirk about using RibbonToolTip is that the ScreenTip appears along the edge of the host window rather than near the relevant control. If you find this to be unacceptable, you’ll have to use a plain ToolTip and style it appropriately.


Tip

You can produce the same result as setting ToolTip to a RibbonToolTip by instead setting the equivalent six properties that appear directly on ribbon controls. For example, this produces the same result seen in Figure 10.32:

<RibbonButton SmallImageSource="FormatPainter.png" Label="Format Painter"
  KeyTip="FP"
  ToolTipTitle="Format Painter (Ctrl+Shift+C)"
  ToolTipDescription="Copy formatting from one place and apply it to another."
  ToolTipImageSource="FormatPainter.png"
  ToolTipFooterTitle="Press F1 for more help."
  ToolTipFooterDescription="This will open your web browser."
  ToolTipFooterImageSource="Help.png" />

When you set these properties, you must not set the ToolTip property.


Galleries

Although “gallery” normally refers to a visual style of list, as with the ComboBox examples earlier in this chapter, in the context of the Ribbon, a gallery is nothing more than a categorized list.

RibbonGallery is an ItemsControl that can be hosted inside many controls such as a RibbonSplitButton or a RibbonComboBox. It is meant to provide a categorized view of items rather than a flat list. Therefore, it is designed to contain RibbonGalleryCategory objects, which are ItemsControls designed to contain RibbonGalleryItems.

The following addition to the Ribbon example produces the result in Figure 10.33:

<RibbonWindow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="Chapter10.MainWindow">
  <Grid>
    <Ribbon Name="Ribbon" Title="My Ribbon">
      <RibbonTab Header="Home" KeyTip="H">
        <RibbonGroup Header="Clipboard" SmallImageSource="Clipboard.png">
          ...
        </RibbonGroup>
        <RibbonGroup Header="Font">
          <RibbonComboBox>
            <RibbonGallery>
              <RibbonGalleryCategory Header="Theme Fonts">
                <RibbonGalleryItem FontFamily="Cambria" Content="Cambria"/>
                <RibbonGalleryItem FontFamily="Calibri" Content="Calibri"/>
              </RibbonGalleryCategory>
              <RibbonGalleryCategory Header="Recently Used Fonts">
                <RibbonGalleryItem FontFamily="Tw Cen MT Condensed"
                                      Content="Tw Cen MT Condensed"/>
                <RibbonGalleryItem FontFamily="Courier New"
                                      Content="Courier New"/>
              </RibbonGalleryCategory>
              <RibbonGalleryCategory Header="All Fonts">
                <RibbonGalleryItem FontFamily="Arial" Content="Arial"/>
                <RibbonGalleryItem FontFamily="Arial Black"
                                      Content="Arial Black"/>
                <RibbonGalleryItem FontFamily="Arial Narrow"
                                      Content="Arial Narrow"/>
                <RibbonGalleryItem FontFamily="Arial Rounded MT Bold"
                                      Content="Arial Rounded MT Bold"/>
              </RibbonGalleryCategory>
            </RibbonGallery>
          </RibbonComboBox>
        </RibbonGroup>
      </RibbonTab>
    </Ribbon>
  </Grid>
</RibbonWindow>

Image

FIGURE 10.33 A RibbonGallery displays a categorized list of items.

You can customize the layout of a RibbonGallery without resorting to a custom items panel by setting a few of its properties, such as MinColumnCount, MaxColumnCount, and ColumnsStretchToFill. Figure 10.34 shows the result of marking the preceding RibbonGallery with MinColumnCount="2".

Image

FIGURE 10.34 A two-column RibbonGallery is enabled by setting MinColumnCount to 2.

Menus

WPF has both of the familiar menu controls built-in—Menu and ContextMenu. Unlike in Win32-based technologies, WPF menus are not special-cased over other controls to have distinct prominence or limitations. They are just another set of items controls, designed for the hierarchical display of items in a series of cascading popups.

Menu

Menu simply stacks its items horizontally, with the characteristic gray bar (by default) as its background. The only public API that Menu adds to its ItemsControl base class is the IsMainMenu property. When true (which it is by default), the Menu gets focus when the user presses the Alt or F10 key, matching user expectations for Win32 menus.

As with any other items control, Menu’s items can be anything, but it’s expected that you’ll use MenuItem and Separator objects. Figure 10.35 displays a typical menu created from the XAML in Listing 10.3.

Image

FIGURE 10.35 The WPF Menu.

LISTING 10.3 A Typical Menu, with MenuItem and Separator Children


<Menu>
  <MenuItem Header="_File">
    <MenuItem Header="_New..."/>
    <MenuItem Header="_Open..."/>
    <Separator/>
    <MenuItem Header="Sen_d To">
      <MenuItem Header="Mail Recipient"/>
      <MenuItem Header="My Documents"/>
    </MenuItem>
  </MenuItem>
  <MenuItem Header="_Edit">
  ...
  </MenuItem>
  <MenuItem Header="_View">
  ...
  </MenuItem>
</Menu>


MenuItem is a headered items control (derived from HeaderedItemsControl), just like RibbonTab and RibbonGroup. For MenuItem, Header is actually the main object (typically text, as in Figure 10.35). The Items, if any, are the child elements that get displayed as a submenu. Like Button and Label, MenuItem supports access keys by using the underscore prefix.

Separator is a simple control that, when placed in a MenuItem, gets rendered as the horizontal line shown in Figure 10.35. Separator is also designed for two other items controls discussed later in this chapter: ToolBar and StatusBar. (You can use it in a Ribbon as well, but in that case you should use the derived RibbonSeparator instead.)

Although Menu is a simple control, MenuItem contains many properties for customizing its behavior. Some of the interesting ones are as follows:

Image Icon—Enables you to add an arbitrary object to be placed alongside the Header. The Icon object gets rendered just like Header, although typically a small image or drawing is used.

Image IsCheckable—Enables you to make a MenuItem act like a CheckBox control.

Image InputGestureText—Enables you to label an item with an associated gesture (most commonly a keyboard shortcut such as Ctrl+O).

MenuItem also defines five events: Checked, Unchecked, SubmenuOpened, SubmenuClosed, and Click. Although handling a Click event is a common way to attach behavior to a MenuItem, you can alternatively assign a command to MenuItem’s Command property.


Warning: Setting InputGestureText doesn’t give a MenuItem its keyboard shortcut!

In a confusing departure from systems such as Windows Forms and key tips inside Ribbon, setting MenuItem’s InputGestureText to a string such as "Ctrl+O" doesn’t automatically invoke the item when Ctrl+O is pressed! Instead, the string just serves as documentation.

To give a MenuItem a keyboard shortcut, you should hook it up to a command via its Command property. If the command has an associated input gesture, MenuItem’s InputGestureText property is automatically set to the correct string, so the shortcut is displayed without any explicit action.



Tip

When assigning MenuItem’s Command property to an instance of RoutedUICommand, its Header is automatically set to the command’s Text property. You can override this behavior by explicitly setting Header.



Image FAQ: How can I make Menu arrange its items vertically instead of horizontally?

Because Menu is just another items control, you can use the same ItemsPanel trick shown earlier for ListBox but replace the default panel with a StackPanel:

<Menu>
<Menu.ItemsPanel>
  <ItemsPanelTemplate>
    <StackPanel/>
  </ItemsPanelTemplate>
</Menu.ItemsPanel>
  ...
</Menu>

The default orientation for StackPanel is vertical, so you don’t need to set the Orientation property in this case. Figure 10.36 shows the result.

Image

FIGURE 10.36 A vertical Menu.

If you want the entire menu to be rotated to the vertical position (with sideways text, like what happens in older Microsoft Office programs when you drag and dock menus to the left or right edge of the window), you should instead use a RotateTransform.


ContextMenu

ContextMenu works just like Menu; it’s a simple container designed to hold MenuItems and Separators. You can’t embed ContextMenu directly in an element tree, however. You must attach it to a control via an appropriate property, such as the ContextMenu property defined on FrameworkElement and FrameworkContentElement. When a user right-clicks the element (or presses Shift+F10), the context menu is displayed.

Figure 10.37 displays a context menu applied to a ListBox as follows, using exactly the same MenuItems from Listing 10.3:

<ListBox>
<ListBox.ContextMenu>
  <ContextMenu>
    ...The three MenuItems from Listing 10.3...
  </ContextMenu>
</ListBox.ContextMenu>
  ...
</ListBox>

Image

FIGURE 10.37 The WPF ContextMenu.

Besides the expected IsOpen property and Opened/Closed events, ContextMenu defines many properties for customizing the placement of the menu. By default, the menu appears with its upper-left corner directly under the mouse pointer. But you can change its Placement to something other than MousePoint (for example, Absolute) and/or set its HorizontalOffset and VerticalOffset to adjust this behavior.

Just as ToolTip has a companion ToolTipService static class for controlling properties from the ToolTip’s target, ContextMenu has a ContextMenuService static class for the same purpose. It contains several attached properties that correspond to many of the properties defined directly on ContextMenu.


Image FAQ: How do I get a context menu to appear when I right-click on a disabled element?

Just like ToolTipService, ContextMenuService contains a ShowOnDisabled attached property for this purpose. You can use it as follows:

<ListBox ContextMenuService.ShowOnDisabled="True">
<ListBox.ContextMenu>
  ...
</ListBox.ContextMenu>
  ...
</ListBox>


Other Items Controls

The remaining items controls—TreeView, ToolBar and StatusBar—are neither selectors nor menus but can still contain an unbounded number of arbitrary objects.

TreeView

TreeView is a popular control for displaying hierarchical data with nodes that can be expanded and collapsed, as shown in Figure 10.38.

Image

FIGURE 10.38 The WPF TreeView control.

TreeView, like Menu, is a very simple control. It can contain any items, and it stacks them vertically. But TreeView is pretty pointless unless you fill it with TreeViewItems.

TreeViewItem, just like MenuItem, is a headered items control. TreeViewItem’s Header property contains the current item, and its Items collection contains subitems (which, again, are expected to be TreeViewItems).

The TreeView in Figure 10.38 can be created with the following XAML:

<TreeView>
  <TreeViewItem Header="Desktop">
    <TreeViewItem Header="Computer">
      ...
    </TreeViewItem>
    <TreeViewItem Header="Recycle Bin">
      ...
    </TreeViewItem>
    <TreeViewItem Header="Control Panel">
      <TreeViewItem Header="Programs"/>
      <TreeViewItem Header="Security"/>
      <TreeViewItem Header="User Accounts"/>
    </TreeViewItem>
    <TreeViewItem Header="Network">
      ...
    </TreeViewItem>
  </TreeViewItem>
</TreeView>

TreeViewItem contains handy IsExpanded and IsSelected properties, as well as four events covering all four states from these properties: Expanded, Collapsed, Selected, and Unselected. TreeViewItem also supports rich keyboard navigation, with the plus (+) and minus (-) keys expanding and collapsing an item, and the arrow keys, Page Up, Page Down, Home, and End keys enabling several ways to move focus from one item to another.


Tip

TreeView supports virtualization, but you have to turn it on explicitly by setting the VirtualizingStackPanel.IsVirtualizing attached property to true on the TreeView. Doing so can save large amounts of memory and can significantly improve the performance of scrolling when there are lots of items.



Warning: Always use TreeViewItem to explicitly wrap items in a TreeView!

It might be tempting to use simple TextBlocks as leaf nodes, but when you do so, you can run into a subtle property value inheritance trap that can make the text in such TextBlocks seem to disappear. By default, selecting a parent node changes its Foreground to white, and if TextBlocks are direct logical children, their text turns white as well. (Although the implicit TreeViewItem is the visual parent for each TextBlock, the logical parent takes precedence for inheritance.) Against the default white background, such text cannot be seen. If you make TreeViewItem the explicit (logical) parent of each TextBlock, however, the undesirable inheritance no longer occurs.


ToolBar

The ToolBar control is typically used to group together many small buttons (or other controls) as an enhancement to a traditional menu system. Figure 10.39 displays a ToolBar created from the following XAML:

<ToolBar RenderOptions.BitmapScalingMode="NearestNeighbor">
  <Button><Image Source="copy.gif"/></Button>
  <Separator/>
  <ToggleButton><Image Source="bold.gif"/></ToggleButton>
  <ToggleButton><Image Source="italic.gif"/></ToggleButton>
  <ToggleButton><Image Source="underline.gif"/></ToggleButton>
  <Separator/>
  <ToggleButton><Image Source="left.gif"/></ToggleButton>
  <ToggleButton><Image Source="right.gif"/></ToggleButton>
  <ToggleButton><Image Source="justify.gif"/></ToggleButton>
  <Separator/>
  <Label>Zoom</Label>
  <ComboBox>
    ...
  </ComboBox>
  <Separator/>
  <Button><Image Source="superscript.gif"/></Button>
  <Button><Image Source="subscript.gif"/></Button>
  ...
</ToolBar>

Image

FIGURE 10.39 The WPF ToolBar.

Notice that the Button and ComboBox controls used in the ToolBar look different than they normally do. In addition, Separator now gets rendered as a vertical line instead of the horizontal line seen when it is placed inside a Menu. ToolBar overrides the default styles of its items so that they automatically get the look that most people expect from a ToolBar.

ToolBars can be placed anywhere in an element tree, but they are typically placed inside a FrameworkElement called ToolBarTray. ToolBarTray holds a collection of ToolBars (in its content property called ToolBars) and, unless its IsLocked property is set to true, it enables users to drag and reposition the ToolBars. (ToolBarTray also defines an IsLocked attached property that can be placed on individual ToolBars.) ToolBarTray has an Orientation property that can be set to Vertical to make all its ToolBars arrange its items vertically.

If a ToolBar contains more items than it can fit within its bounds, the extra items move to an overflow area. This overflow area is a popup that can be accessed by clicking the little arrow at the end of the control, as shown in Figure 10.40. By default, the last item is the first to move to the overflow area, but you can control the overflow behavior of individual items with ToolBar’s OverflowMode attached property. You can use this property to mark an item to overflow AsNeeded (the default), Always, or Never.

Image

FIGURE 10.40 ToolBar has an overflow area for items that don’t fit.


Tip

You can create a Visual Studio–style customizable ToolBar by setting ToolBar.OverflowMode to Never on each item, then adding a Menu with the header "_Add or Remove Buttons" and ToolBar.OverflowMode set to Always (so it always remains in the overflow area). You can then add MenuItems to this Menu that users can check/uncheck to add/remove the corresponding item to/from the ToolBar.



Tip

Whenever elements contain small, iconic images, it’s a good idea to set the RenderOptions.BitmapScalingMode attached property to NearestNeighbor. This makes such images look much crisper than their default rendering. The ToolBar in this section takes advantage of this property.


StatusBar

StatusBar behaves just like Menu, but it stacks its items horizontally, as shown in Figure 10.41. It’s typically used along the bottom of a Window to display status information.

Image

FIGURE 10.41 The WPF StatusBar.

The StatusBar in Figure 10.41 can be created with the following XAML:

<StatusBar RenderOptions.BitmapScalingMode="NearestNeighbor">
  <Label>27 Items</Label>
  <Separator/>
  <Label>Zoom</Label>
  <ComboBox>
    ...
  </ComboBox>
  <Separator/>
  <Button><Image Source="justify.gif"/></Button>
</StatusBar>

By default, StatusBar gives Separator a control template that renders it as a vertical line, just like when it is within a ToolBar. Items in a StatusBar (other than Separator) are implicitly wrapped in a StatusBarItem, but you can also do this wrapping explicitly. This way, you can customize their position with the layout-related attached properties discussed in Chapter 5.


Image FAQ: How can I get items in a StatusBar to grow proportionally?

It’s common to want StatusBar panes to remain proportionately sized. For example, perhaps you want a left pane that occupies 25% of the StatusBar’s width and a right pane that occupies 75% of the width. You can make this happen by overriding StatusBar’s ItemsPanel with a Grid and configuring the Grid’s columns as follows:

<StatusBar>
  <StatusBar.ItemsPanel>
    <ItemsPanelTemplate>
      <Grid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="3*"/>
      </Grid.ColumnDefinitions>
      </Grid>
    </ItemsPanelTemplate>
  </StatusBar.ItemsPanel>
  <StatusBarItem Grid.Column="0">...</StatusBarItem>
  <Separator     Grid.Column="1"/>
  <StatusBarItem Grid.Column="2">...</StatusBarItem>
</StatusBar>

Note that items inside the StatusBar need to be explicitly marked with Grid.Column (which is meaningful only when Grid is the ItemsPanel) to avoid all being placed in column zero. Also, be aware that such layout properties work only for children of type StatusBarItem or Separator. That’s because other elements (such as the Label, ComboBox, and Button in the previous StatusBar snippet) would get implicitly wrapped with a StatusBarItem that would be missing the necessary attached properties. Therefore, you must wrap any such elements explicitly in a StatusBarItem.


Summary

Items controls are vital to understand for just about any WPF development. It’s hard to imagine a WPF application not using content controls and items controls. But unlike content controls, there’s a lot to learn about items controls! A recurring theme throughout this chapter is the importance of data binding if you’re working with a sizable or dynamic list of items. However, there are a few more areas of WPF to cover before we get to data binding in depth. The next chapter covers images, text, and other controls.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset