How-To: UserControls in ItemsControl mit Überschriften trennen

Eine der wichtigsten Gründe Prism einzusetzen ist die Möglichkeit die GUI in Regionen aufzuteilen. Jede dieser Regionen wird als Content-, Items- o.ä. Control repräsentiert und kann dann zur Laufzeit mit 1 (ContentControl) bis n (Items– oder TabControl) UserControls bestückt werden. Auf diese Weise wird die eigentliche Benutzeroberfläche lose gekoppelt und kann ohne größeren Aufwand an neue Anforderungen angepasst werden.

Ein immer wiederkehrendes Problem stellt für mich dabei die Nutzung des ItemsControl dar. Jenes zeigt die einzelnen Views in Form einer Liste an, was unter Umständen etwas langweilig und verwirrend wirken kann. Aus diesem Grund bietet es sich an, jene Views durch Überschriften oder Abgrenzungen voneinander zu trennen, aber wie kriegt man das hin? Kann man es evtl. auch ohne Prism verwenden? Und vor allem wie schafft man es mit möglichst wenig Aufwand?

Erst einmal völlig statisch…

Bevor wir uns Prism zu wenden, noch einmal kurz beschrieben was wir eigentlich tun wollen. Das grundlegende Ziel ist zunächst das Anzeigen mehrerer Controls in einer Liste. Dies kann wie folgt geschehen, wobei der Einfachheit halber Rechtecke statt UserControls verwendet wurden.

Der Vorteil eines ItemsControls gegenüber z.B. einer ListBox ist hierbei vor allem, dass keine Selektierung statt findet  sobald eines der enthalten UserControls den Fokus erhält, denn für den Anwender darf nicht ersichtlich sein, dass es sich tatsächlich um verschiedene Bestandteile handelt.

<ItemsControl>
   <Rectanlge Height="20" Fill="Red" />
   <Rectanlge Height="20" Fill="Blue" />
   <Rectanlge Height="20" Fill="Green" />
</ItemsControl>

Wie man im obigen Bild schon sieht, ist das etwas verwirrend da alle Controls dicht an dicht aufgereiht sind und somit die logische Gruppierung fehlt. Dies wird um so schlimmer je mehr Inhalt sie haben. Also wäre es doch besser sie untereinander abzugrenzen.

Dazu kann man entweder vor jedem Control einen entsprechend gestylten TextBlock einfügen oder wie folgt das HeaderedContentControl nutzen. Letzteres hat  den Vorteil, dass man die Einträge im Xaml besser unterscheiden und vor allem leichter stylen kann.

<ItemsControl>
   <HeaderedContentControl Header="Red" Style="{StaticResource ItemHeaderStyle}">
     <Rectanlge Height="20" Fill="Red" />
   </HeaderedContentControl>

   <HeaderedContentControl Header="Blue" Style="{StaticResource ItemHeaderStyle}">
     <Rectanlge Height="20" Fill="Blue" />
   </HeaderedContentControl>

   <HeaderedContentControl Header="Green" Style="{StaticResource ItemHeaderStyle}">
      <Rectanlge Height="20" Fill="Green" />
   </HeaderedContentControl>
</ItemsControl>

…dann etwas dynamisch…

Im dynamischen Fall werden die anzuzeigenden Controls nicht mehr fest angegeben, sondern aus einer Datenquelle bezogen. Hier ist die Abgrenzung über einen allgemeinen Style noch wichtiger als im statischen Fall, denn in diesem hat man wesentlich geringeren Einfluss darauf wie die geladenen Controls intern aufgebaut sind.

<ItemsControl ItemsSource="{Binding ControlSource}" />

Nun hat man zunächst aber ein weiteres Problem. Denn man muss irgend wie die Überschrift ermitteln die zuvor noch statisch angegeben werden konnten. Hier bietet sich die Nutzung eines Interfaces an, dass genau jene Property vorschreibt auf die der Header sich dann bindet.

public interface IHeaderedView
{
   string Header { get; }
}

Als nächstes müssen wir nun noch das Aussehen unseres Headers festlegen. Diesmal verwende ich kein HeaderedContentControl sondern überschreibe das ControlTemplate des ContentControls eines jeden Items. Sieht man sich den Code genauer an, merkt man sehr schnell, dass ich jetzt genau das tue, was ich im statischen Fall zu vermeiden versucht habe.

<Style x:Key="ItemHeaderStyle">
   <Setter Property="Control.Template">
     <Setter.Value>
         <ControlTemplate TargetType="{x:Type ContentControl}">
            <StackPanel>
               <TextBlock Text="{Binding Path=Header,
                    RelativeSource={RelativeSource TemplatedParent}}" />
               <ContentPresenter />
            </StackPanel>
         </ControlTemplate>
      </Setter.Value>
   </Setter>
</Style>

Der ContentPresenter spiegelt hierbei das tatsächlich hinzugefügte Element wieder, welches von einem StackPanel umgeben und dessen Header von einem TextBlock dargestellt wird. Der Text des TextBlocks ergibt sich nun aus der von IHeaderedView vorgeschriebenen Property. Damit diese Bindung funktioniert muss das Interface jedoch im CodeBehind der View umgesetzt werden.

<ItemsControl ItemsSource="{Binding ControlSource}"
              ItemContainerStyle="{StaticResource ItemHeaderStyle}"/>

Damit es aber nun wirklich wie gewünscht aussieht, muss der Style noch verwendet werden. Hierzu ist dieser dem ItemContainerStyle des ItemsControl zuzuweisen.

…zuletzt dann auch mit Prism.

Tja, das war eigentlich schon alles. Um Prism hier ins Spiel zu bringen muss man nun eigentlich nur das Binding auf die Liste der Controls entfernen und das ItemControl als Region bekannt machen. Wie so etwas aussehen kann, zeigt der folgende Code und das entsprechende Bild.

<ItemsControl Prism:RegionManager.RegionName="DiagramRegion"
              ItemContainerStyle="{StaticResource ItemHeaderStyle}"/>

Wen es interessiert: Als Ausgangspunkt für den Style im Bild wurde das Shiny Red Theme verwendet.

Weitere Infos

WPF Content Model in der MSDN

– sehr gute Beschreibung zu Items Containern & Co.