#1,191 – Custom Element with a Single Child Element

Below is an example of a simple custom element that derives from FrameworkElement and includes a single child element (UIElement), set using the Child dependency property.

Here’s the full code for the custom element.  We do the following:

  • Override both Measure and Layout
  • Override VisualChildrenCount and GetVisualChild to respond indicating that we have a single child
  • Override Render to render the graphical portion of the control
  • Use AddVisualChildAddLogicalChild to indicate that the child element belongs to the parent element
    public class MyElement : FrameworkElement
    {
        private static FrameworkPropertyMetadata childMetadata =
                   new FrameworkPropertyMetadata(null,
                            FrameworkPropertyMetadataOptions.AffectsParentArrange,
                            new PropertyChangedCallback(OnChildChanged));

        public static void OnChildChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            MyElement thisElement = obj as MyElement;
            if (thisElement == null)
                throw new Exception("Child property must be attached to MyElement");

            // Remove old one
            Visual oldChild = e.OldValue as Visual;
            if (oldChild != null)
            {
                thisElement.RemoveVisualChild(oldChild);
                thisElement.RemoveLogicalChild(oldChild);
            }

            // Attach new one
            Visual newChild = e.NewValue as Visual;
            if (newChild != null)
            {
                thisElement.AddVisualChild(newChild);
                thisElement.AddLogicalChild(newChild);
            }
        }

        public static readonly DependencyProperty ChildProperty =
            DependencyProperty.RegisterAttached("Child", typeof(UIElement),
                typeof(MyElement), childMetadata);

        public static void SetChild(DependencyObject depObj, UIElement value)
        {
            depObj.SetValue(ChildProperty, value);
        }

        protected override int VisualChildrenCount
        {
            get
            {
                UIElement childElement = (UIElement)GetValue(ChildProperty);
                return childElement != null ? 1 : 0;
            }
        }

        protected override Visual GetVisualChild(int index)
        {
            // (ignoring index)
            return (UIElement)GetValue(ChildProperty);
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            UIElement childElement = (UIElement)GetValue(ChildProperty);
            if (childElement != null)
                childElement.Measure(availableSize);

            // "X" and child both use all of the available space
            return availableSize;
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            UIElement childElement = (UIElement)GetValue(ChildProperty);
            if (childElement != null)
                childElement.Arrange(new Rect(new Point(0.0, 0.0), finalSize));

            return finalSize;
        }

        // Render a big "X"
        protected override void OnRender(DrawingContext dc)
        {
            dc.DrawLine(new Pen(Brushes.Blue, 2.0),
                new Point(0.0, 0.0),
                new Point(ActualWidth, ActualHeight));
            dc.DrawLine(new Pen(Brushes.Green, 2.0),
                new Point(ActualWidth, 0.0),
                new Point(0.0, ActualHeight));
        }
    }

We can now use this element as follows:

    <Grid>
        <loc:MyElement>
            <loc:MyElement.Child>
                <Label Content="I'm the child" HorizontalAlignment="Center"/>
            </loc:MyElement.Child>
        </loc:MyElement>
    </Grid>

When rendered, it looks like this:

1191-001

#1,190 – VisualChildrenCount and GetVisualChild

FrameworkElement provides the VisualChildrenCount property and the GetVisualChild method, typically used during layout to walk through the child elements of the element participating in layout.  When you create a custom control that derives from FrameworkElement, you typically override both members, providing values that make sense for your control.

Below, we use these members to get a report of the visual children of a custom StackPanel.  (Because neither member is public, we override StackPanel to allow access).

XAML is:

    <StackPanel>
        <loc:MyStackPanel x:Name="aStackPanel">
            <Label Content="Hi there"/>
            <Button Content="Click me"/>
        </loc:MyStackPanel>

        <Button Content="Visual Children" Click="Button_Click"/>
    </StackPanel>

Code-behind is:

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            StringBuilder sbMsg = new StringBuilder();

            sbMsg.Append("Children of MyStackPanel:\n");
            int childCount = aStackPanel.MyVisualChildrenCount;
            for (int i = 0; i < childCount; i++)
            {
                Visual child = aStackPanel.MyGetVisualChild(i);
                sbMsg.AppendFormat(" - {0}\n", child.GetType().ToString());
            }

            MessageBox.Show(sbMsg.ToString());
        }

Results at run-time:
1190-001

#1,189 – MeasureOverride Input and Output

The MeasureOverride method in FrameworkElement takes as input a Size and then returns a Size.  These parameters are intended to be used as follows:

  • Input Size – during layout, this is how much room is available for the element to be rendered
  • Output Size – this is how much room the element says that it needs

For example, below we have a custom element that draws a big “X” using all of the available space.  (When rendering, it uses ActualWidth and ActualHeight).  Because it will use as much room as it is given, MeasureOverrides return value is all of the available space.

    public class MyElement : FrameworkElement
    {
        protected override Size MeasureOverride(Size availableSize)
        {
            Size sizeIWillNeed = availableSize;

            return sizeIWillNeed;
        }

        protected override void OnRender(DrawingContext dc)
        {
            dc.DrawLine(new Pen(Brushes.Blue, 2.0),
                new Point(0.0, 0.0),
                new Point(ActualWidth, ActualHeight));
            dc.DrawLine(new Pen(Brushes.Green, 2.0),
                new Point(ActualWidth, 0.0),
                new Point(0.0, ActualHeight));
        }
    }

#1,188 – Using an ItemContainerStyle to Animate Items in an ItemsControl

One common use when defining a new ItemContainerStyle for an ItemsControl is to do something special with either the currently selected item or the item that the mouse is currently over.

We can set property values for the currently selected item or the item that the mouse is over, using triggers and property setters.  You can also use a storyboard to animate a transition to a new property value.

Below, we do two things to animate the item that the mouse is currently over.  (Both are done in the Style used for the ItemContainerStyle).

  • Add a Setter for the LayoutTransform, containing a ScaleTransform
  • Trigger on IsMouseOver, setting up a storyboard to animate the ScaleX and ScaleY properties of the ScaleTransform
        <Style x:Key="FocusVisual">
            <Setter Property="Control.Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Rectangle Margin="2" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <SolidColorBrush x:Key="Item.MouseOver.Background" Color="#1F26A0DA"/>
        <SolidColorBrush x:Key="Item.MouseOver.Border" Color="#a826A0Da"/>
        <SolidColorBrush x:Key="Item.SelectedInactive.Background" Color="#3DDADADA"/>
        <SolidColorBrush x:Key="Item.SelectedInactive.Border" Color="#FFDADADA"/>
        <SolidColorBrush x:Key="Item.SelectedActive.Background" Color="#3D26A0DA"/>
        <SolidColorBrush x:Key="Item.SelectedActive.Border" Color="#FF26A0DA"/>

        <Style x:Key="lbDefaultItemContainerStyle" TargetType="{x:Type ListBoxItem}">
            <Setter Property="SnapsToDevicePixels" Value="True"/>
            <Setter Property="Padding" Value="4,1"/>
            <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="BorderBrush" Value="Transparent"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}"/>
            <Setter Property="LayoutTransform">
                <Setter.Value>
                    <ScaleTransform ScaleX="1" ScaleY="1"/>
                </Setter.Value>
            </Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBoxItem}">
                        <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
                            <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Trigger.EnterActions>
                                    <BeginStoryboard>
                                        <Storyboard>
                                            <DoubleAnimation To="1.1" Duration="0:0:0.3"
                                                             Storyboard.TargetProperty="LayoutTransform.(ScaleTransform.ScaleX)"/>
                                            <DoubleAnimation To="1.1" Duration="0:0:0.3"
                                                             Storyboard.TargetProperty="LayoutTransform.(ScaleTransform.ScaleY)"/>
                                        </Storyboard>
                                    </BeginStoryboard>
                                </Trigger.EnterActions>
                                <Trigger.ExitActions>
                                    <BeginStoryboard>
                                        <Storyboard>
                                            <DoubleAnimation To="1" Duration="0:0:0.3"
                                                             Storyboard.TargetProperty="LayoutTransform.(ScaleTransform.ScaleX)"/>
                                            <DoubleAnimation To="1" Duration="0:0:0.3"
                                                             Storyboard.TargetProperty="LayoutTransform.(ScaleTransform.ScaleY)"/>
                                        </Storyboard>
                                    </BeginStoryboard>
                                </Trigger.ExitActions>
                                <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.MouseOver.Background}"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.MouseOver.Border}"/>
                            </Trigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="Selector.IsSelectionActive" Value="False"/>
                                    <Condition Property="IsSelected" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedInactive.Background}"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.SelectedInactive.Border}"/>
                            </MultiTrigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="Selector.IsSelectionActive" Value="True"/>
                                    <Condition Property="IsSelected" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedActive.Background}"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.SelectedActive.Border}"/>
                            </MultiTrigger>
                            <Trigger Property="IsEnabled" Value="False">
                                <Setter Property="TextElement.Foreground" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

 

#1,187 – Using an ItemContainerStyle to Change Items in an ItemsControl

One common use when defining a new ItemContainerStyle for an ItemsControl is to do something special with either the currently selected item or the item that the mouse is currently over.  The default template used as an ItemContainerStyle for a ListBox, for example, changes the background and border for both the selected item and the item that the mouse is currently over.

Below is an example where we change the FontWeight for the currently selected item (to SemiBold) and make the item larger, using a transform.  This method uses a trigger to change several properties.

        <Style x:Key="FocusVisual">
            <Setter Property="Control.Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Rectangle Margin="2" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <SolidColorBrush x:Key="Item.MouseOver.Background" Color="#1F26A0DA"/>
        <SolidColorBrush x:Key="Item.MouseOver.Border" Color="#a826A0Da"/>
        <SolidColorBrush x:Key="Item.SelectedInactive.Background" Color="#3DDADADA"/>
        <SolidColorBrush x:Key="Item.SelectedInactive.Border" Color="#FFDADADA"/>
        <SolidColorBrush x:Key="Item.SelectedActive.Background" Color="#3D26A0DA"/>
        <SolidColorBrush x:Key="Item.SelectedActive.Border" Color="#FF26A0DA"/>
        
        <Style x:Key="lbDefaultItemContainerStyle" TargetType="{x:Type ListBoxItem}">
            <Setter Property="SnapsToDevicePixels" Value="True"/>
            <Setter Property="Padding" Value="4,1"/>
            <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="BorderBrush" Value="Transparent"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBoxItem}">
                        <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
                            <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsMouseOver" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.MouseOver.Background}"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.MouseOver.Border}"/>
                                <Setter Property="LayoutTransform">
                                    <Setter.Value>
                                        <ScaleTransform ScaleX="1.5" ScaleY="1.5"/>
                                    </Setter.Value>
                                </Setter>
                                <Setter Property="FontWeight" Value="SemiBold"/>
                            </MultiTrigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="Selector.IsSelectionActive" Value="False"/>
                                    <Condition Property="IsSelected" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedInactive.Background}"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.SelectedInactive.Border}"/>
                            </MultiTrigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="Selector.IsSelectionActive" Value="True"/>
                                    <Condition Property="IsSelected" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedActive.Background}"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.SelectedActive.Border}"/>
                            </MultiTrigger>
                            <Trigger Property="IsEnabled" Value="False">
                                <Setter Property="TextElement.Foreground" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

The resulting behavior looks like this:
1187-001

#1,186 – Default ItemContainerStyle for a ListBox

The default style for the ItemContainerStyle of a ListBox (version 4.5.1 of the .NET Framework) is shown below.  The template is included within a style that includes some other default property values.

This style is applied to each ListBoxItem rendered in the ListBox.  The structure of each ListBoxItem is a Border surrounding a ContentPresenter.  More importantly, the style shows the default triggers that control how items are rendered when you move the mouse over an item or select an item.

        <Style x:Key="lbDefaultItemContainerStyle" TargetType="{x:Type ListBoxItem}">
            <Setter Property="SnapsToDevicePixels" Value="True"/>
            <Setter Property="Padding" Value="4,1"/>
            <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="BorderBrush" Value="Transparent"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBoxItem}">
                        <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
                            <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsMouseOver" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.MouseOver.Background}"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.MouseOver.Border}"/>
                            </MultiTrigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="Selector.IsSelectionActive" Value="False"/>
                                    <Condition Property="IsSelected" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedInactive.Background}"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.SelectedInactive.Border}"/>
                            </MultiTrigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="Selector.IsSelectionActive" Value="True"/>
                                    <Condition Property="IsSelected" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedActive.Background}"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.SelectedActive.Border}"/>
                            </MultiTrigger>
                            <Trigger Property="IsEnabled" Value="False">
                                <Setter Property="TextElement.Foreground" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

The style references some other predefined resources, shown below:

        <Style x:Key="FocusVisual">
            <Setter Property="Control.Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Rectangle Margin="2" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <SolidColorBrush x:Key="Item.MouseOver.Background" Color="#1F26A0DA"/>
        <SolidColorBrush x:Key="Item.MouseOver.Border" Color="#a826A0Da"/>
        <SolidColorBrush x:Key="Item.SelectedInactive.Background" Color="#3DDADADA"/>
        <SolidColorBrush x:Key="Item.SelectedInactive.Border" Color="#FFDADADA"/>
        <SolidColorBrush x:Key="Item.SelectedActive.Background" Color="#3D26A0DA"/>
        <SolidColorBrush x:Key="Item.SelectedActive.Border" Color="#FF26A0DA"/>

You would use the main style as follows:

        <ListBox Margin="5" ItemsSource="{Binding MovieList}"
                 ItemContainerStyle="{DynamicResource lbDefaultItemContainerStyle}" />

#1,185 – ItemsControl Customization Summary

You can can customize an ItemsControl, or any of the controls that derive from it (ComboBox, DataGrid, ListBoxTabControl, TreeView et al), in a number of ways.  The various customization mechanisms are summarized below.

  • Set Style to apply a set of property values for the main control
  • Set Template to change the control template of the control.
    • This changing the highest level of the control (e.g. for a ListBox, a Border wrapping a ScrollViewer, in turn wrapping an ItemsPresenter)
  • Set ItemsPanel to change the panel used to lay out the individual items
    • E.g. User horizontally oriented StackPanel to lay out ListBox items horizontally, rather than vertically
  • Set ItemContainerStyle to set properties that apply to the container for each item
    • E.g. Properties that apply to each ListBoxItem within a ListBox
  • Set ItemTemplate to change the data template used to render each item in the list
    • E.g. Create a custom layout for each item in a ListBox

 

Follow

Get every new post delivered to your Inbox.

Join 375 other followers