Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Customizing ListBox for a Smooth Animated Navigation Bar in WPF

5.00/5 (6 votes)
10 Jan 2024CPOL13 min read 3.9K  
Control featuring design and animations specialized for mobile, but can be elegantly and structurally implemented using ListBox and Animation technologies available in WPF.
The navigation control in WPF, MagicBar, exemplifies a structural and design approach for multi-screen connections through menu configurations, leveraging animations and ListBox technology. This comprehensive guide details the intricacies of its implementation, promoting WPF's flexibility across various platforms while emphasizing design, animation, and code quality improvements.

Image 1

Introduction

WPF applications traditionally prefer a programmatic approach that connects multiple screens through menu configurations and presents them in a unified manner. This technique, often referred to as the menu or Navigation, is one of the core implementations in WPF. It also has a direct correlation with the architecture (design) of the project, so paying more attention to its implementation can positively impact the quality of the project.

This control features a design and animations specialized for mobile, but it can be elegantly and structurally implemented using ListBox and Animation technologies available in WPF. Additionally, it can be similarly implemented in Cross-Platform environments such as AvaloniaUI, Uno, OpenSilver, MAUI, which allows this project to be researched and applied across various platforms.

The goal is also to widely promote the flexibility and excellence of WPF implementation and share the technology. Through this project, we hope you will deeply experience the charm of WPF.

Learning through Tutorial Videos and CodeProject Article

This control comes with a tutorial video, approximately 30 minutes long, available in English and Chinese audio with Korean subtitles. Creating a tutorial video requires more time and effort than one might think, and while it's challenging, your encouragement and support are becoming a great motivation.

You can learn more through the following platforms:

Additionally, check out other tutorial videos like ThemeSwitch, Lol-PlayButton, etc.

Design and Structural Philosophy

Image 2

This control style is one used widely through web or mobile navigation configurations. Therefore, it's commonly seen implemented using IOS, Android, or HTML/CSS technologies. Implementing it with CSS/HTML and JavaScript allows for relatively easy construction of structure and animation functions. In contrast, WPF, through XAML, might feel comparatively complex in terms of design, event, and animation implementation. Thus, the key to this control's implementation is to make the most of WPF's characteristics and provide a high-level implementation method that lets users feel the structural strengths of WPF.

A lot of focus has been put into the quality of the Source code through Refactoring. The project minimizes/optimizes hierarchical XAML structures and emphasizes enhancing code quality through interaction between XAML and Behind code using CustomControl. The control isn't just about providing basic functionality; it's about conveying technical inspiration and encouraging diverse applications through its structural philosophy.

Project Overview

MagicBar.cs

MagicBar, the core control of this project, is a CustomControl inheriting from ListBox control. In most development scenarios, UserControl is the usual choice, but for functions involving complex features, animations, and repetitive elements like in this case, it's more effective to divide and implement them as smaller Control (CustomControl) units.

If you're not familiar with CustomControl, please read the following:

Quote:

The CustomControl approach itself is technically challenging and conceptually different from traditional desktop methods like Windows Forms, making it somewhat difficult to approach easily. Additionally, finding reference materials for guidance is challenging. However, this is an important process to elevate your WPF technical skills. We encourage you to open-mindedly take on the challenge of CustomControl implementation with this opportunity.

Generic.xaml

CustomControl is characterized by its separation and management of the XAML Design area. Therefore, it doesn't provide direct interaction between the XAML area and the control (Class). Interaction between these two areas is supported through other indirect methods. The first method involves exploring the Template area through the OnApplyTemplate timing. The second method extends binding through DependencyProperty declarations.

This structural feature allows for a perfect separation of design and code, enhancing code reusability and extensibility, and understanding the traditional structure of WPF in depth. All controls used in WPF follow this same method. To verify this, you can directly explore the open-source dotnet/WPF repository available on GitHub.

XAML Configuration

Introduction to Geometry

Geometry is one of the design elements provided in WPF, used for vector-based designs. Traditionally, development methods preferred bitmap images like PNG or JPEG, but there's a growing preference for vector-based designs in recent times. This change can be attributed to improvements in computer performance, developments in monitor resolutions, and shifts in design trends. Hence, the role of Geometry elements is significant in this control. The process of implementing the Circle in the latter part is explained in more detail.

Separation of Animation Elements and ItemsPresenter

MagicBar inherits from the ListBox control and uniquely uses the ItemsPresenter element provided through the ItemsControl feature. However, interaction between child elements within the ItemsPresenter is not possible, implying that continuing Animation actions among child items is also unfeasible.

The behavior of ListBoxItem is determined by the type of Panel specified through the ItemsPanelTemplate in the ItemsPresenter element. Therefore, the choice of Panel layout significantly affects the behavior of ListBoxItem. In the case of StackPanel, the order of the added child elements in the Children collection determines their position. For Grid, placement is determined by Row/Column settings.

Thus, linking Animation actions between child elements structurally is not possible.

Quote:

However, there are exceptions. In the case of Canvas, interaction through Animation is possible using the concept of coordinates, but it requires complex calculations and precise implementation for all controls. Yet, better implementation methods exist, so Canvas control content is omitted in this context.

ListBox ControlTemplate Hierarchy

Usually, in implementing ListBox control, greater emphasis is placed on the child element ListBoxItem. However, for this control, the key feature - the Circle structure - needs to be positioned outside the area of the ItemsPresenter element. Therefore, forming a complex Template in the ListBox control is crucial.

The hierarchy of the ControlTemplate is as follows:

Quote:

The following is a simplified representation for clarity and differs from the actual Source code content. The Circle part can easily be found in the text as "PART_Circle".

XAML
<ControlTemplate TargetType="{x:Type ListBox}">
     <Grid>         
        <Circle/>         
        <ItemsPresenter/>     
     </Grid> 
</ControlTemplate>

As seen above, the key is to position the ItemsPresenter and Circle at the same hierarchical level. This arrangement allows the Circle element's Animation range to appear as if freely moving across the ItemsPresenter's child elements. Moreover, it's essential to place the ItemsPresenter element in front of the Circle so that the ListBoxItem element's icons and text do not visually cover the Circle.

Having discussed the theory, let's now delve into the actual source code for a detailed comparison.

Quote:

  The area with x:Name="PART_Circle" corresponds to the Circle.

XAML
<Style TargetType="{x:Type local:MagicBar}">
<Setter Property="ItemContainerStyle" Value="{StaticResource MagicBarItem}"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="UseLayoutRounding" Value="True"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Width" Value="440"/>
<Setter Property="Height" Value="120"/>
<Setter Property="Template">
    <Setter.Value>
	<ControlTemplate TargetType="{x:Type local:MagicBar}">
	    <Grid Background="{TemplateBinding Background}">
		<Grid.Clip>
		    <RectangleGeometry Rect="0 0 440 120"/>
		</Grid.Clip>
		<Border Style="{StaticResource Bar}"/>
		<Canvas Margin="20 0 20 0">
		    <Grid x:Name="PART_Circle" Style="{StaticResource Circle}">
			<Path Style="{StaticResource Arc}"/>
			<Ellipse Fill="#222222"/>
			<Ellipse Fill="CadetBlue" Margin="6"/>
		    </Grid>
		</Canvas>
		<ItemsPresenter Margin="20 40 20 0"/>
	    </Grid>
	</ControlTemplate>
    </Setter.Value>
</Setter>
<Setter Property="ItemsPanel">
    <Setter.Value>
	<ItemsPanelTemplate>
	    <UniformGrid Columns="5"/>
	</ItemsPanelTemplate>
    </Setter.Value>
</Setter>
</Style>

ListBoxItem Template Configuration

Unlike the ListBox control's Template, the configuration of the ListBoxItem is relatively simple. Also, since it's unrelated to the Circle Animation element, it comprises only the menu item's icon and text.

XAML
<Style TargetType="{x:Type ListBoxItem}" x:Key="MagicBarItem">
    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <Grid Background="{TemplateBinding Background}">
                    <james:JamesIcon x:Name="icon" Style="{StaticResource Icon}"/>
                    <TextBlock x:Name="name" Style="{StaticResource Name}"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

In addition, Animation that changes the position and color of the icon and text is included. As previously mentioned, no special functionality needs to be implemented in this ListBoxItem element.

Quote:

JamesIcon is a control provided through the Jamesnet.Wpf library available via NuGet, offering various icons. To substitute it, you can either use the Path control for direct Geometry design implementation or use images with a transparent (Transparent) background.

JamesIcon Style

JamesIcon internally includes a Path control and provides various DependencyProperty attributes to allow flexible design definitions from the outside. Key properties include Icon, Width, Height, Fill, etc.

Quote:

Vector-based Geometry icons offer consistent designs, which is one way to enhance the quality of the control. Therefore, it's worth examining these differences closely.

XAML
<Style TargetType="{x:Type james:JamesIcon}" x:Key="Icon">
    <Setter Property="Icon" Value="{TemplateBinding Tag}"/>
    <Setter Property="Width" Value="40"/>
    <Setter Property="Height" Value="40"/>
    <Setter Property="Fill" Value="#44333333"/>
</Style>

RelativeSource Binding

Since the JamesIcon style is separated from the Template, it's impossible to use TemplateBinding Tag binding as shown below:

XAML
// Binding method that's not possible</code>
<Setter Property="Icon" Value="{TemplateBinding Tag}"/>

Therefore, RelativeSource binding is used to search for the ListBoxItem, the parent element, and bind its Tag property, as shown below:

XAML
<... Value="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem}, Path=Tag}"/>

Using RelativeSource binding, the original TemplateBinding of the icon defined within the ListBoxItem area can be individually moved to the JamesIcon area. This approach allows each component (JamesIcon) to have its own definition and style, making the code more modular, easier to maintain, and reusable. Separating bindings and styles into their respective areas clarifies the overall code structure, making it easier to understand and modify. Additionally, this separation provides greater flexibility, allowing individual components' styles and behaviors to be adjusted without affecting other components.

2. Microsoft Blend: Geometry Design

Image 3

Microsoft Blend, the successor to Expression Blend, continues to hold its name despite a reduction in certain features. This program can be added during the installation process of Visual Studio. If you can't find this program, it's possible to add it via the Visual Studio Installer.

Although Microsoft Blend shares most features with Visual Studio, it includes some additional features specialized in design. Among them are functions related to Geometry, partially resembling features found in Adobe's Illustrator.

Using Microsoft Blend in WPF development isn't essential, nor is it exclusively for designers. Instead, it serves as a valuable tool for developers to create professional and attractive design elements without extensive design training.

Quote:

However, most of the design features provided by Microsoft Blend can be more powerfully utilized in environments like Figma and Illustrator, so there's no pressing need to learn it. But some features related to Geometry are easy to use without separate training, and thus worth examining closely.

Analysis of Circle (🔵) Design

The Circle in the MagicBar control is a crucial point of this project, visually functioning as the menu changes. It includes smooth Animation, adding a contemporary and trendy design element.

The Circle element doesn't necessarily have to be implemented based on Geometry. Using an image could be a simpler method. However, in terms of quality, Geometry designs are becoming more popular as they can handle resolution changes due to size variations more delicately.

Quote:

As shown in the image below, a characteristic of Geometry is that you can resize it as much as you want without losing clarity.

Image 4

If you look closely at the Circle design, you'll see that it creates a sense of space by overlapping a black circle and a green circle. Additionally, rounding the lines on both sides makes it blend naturally into the MagicBar area. This not only looks visually smooth but also appears more elegant when animated. However, implementing this arc can be challenging and is often abandoned during practical implementation.

But this is where Microsoft Blend becomes useful in easily creating these special shapes.

Drawing Method

The design process involves drawing a large circle with a convex arc at the bottom, then adding smaller circles of the same height on both sides of the large circle. By adjusting the diameter of the large circle, ensure that the large and small circles intersect perfectly.

Image 5

Next, use the merge function to cut the unnecessary parts of the large circle and the subtract function to remove unwanted parts of the small circle, leaving only the arc shape at the intersection. Finally, add a rectangle and remove unnecessary parts to create a unique and natural arc shape.

Image 6

This method of implementing design elements not only demonstrates how to use Microsoft Blend for complex graphics but also provides a new perspective on thinking and solving design problems. This approach makes the circle not only aesthetically appealing but also technically innovative, enhancing quality.

3. Animation: ListBoxItem

The animation behavior in the ListBoxItem area, which includes icons and text, is relatively simple. It features moving components upwards and adjusting opacity transparency when IsSelected is set to true.

Quote:

Please carefully observe the animation path and effects through the image below:

Image 7

As shown in the image above, the animation is triggered each time the IsSelected value of the ListBox control changes. Additionally, since the movement of the icon and text doesn't go beyond the ListBoxItem area, it's preferable to implement a static Storyboard element directly within XAML.

Quote:

This can be controlled using a Trigger or VisualStateManager module. For this control, a simple Trigger module approach is utilized for handling just the IsSelected action.

Storyboard

For the ListBoxItem area's animation behavior, it's necessary to prepare scenarios for both when IsSelected is true and when it's false.

XAML
<Storyboard x:Key="Selected">
	<james:ThickItem Mode="CubicEaseInOut" TargetName="icon" 
	Duration="0:0:0.5" Property="Margin" To="0 -80 0 0"/>
	<james:ThickItem Mode="CubicEaseInOut" TargetName="name" 
	Duration="0:0:0.5" Property="Margin" To="0 45 0 0"/>
	<james:ColorItem Mode="CubicEaseInOut" TargetName="icon" 
	Duration="0:0:0.5" Property="Fill.Color" To="#333333"/>
	<james:ColorItem Mode="CubicEaseInOut" TargetName="name" 
	Duration="0:0:0.5" Property="Foreground.Color" To="#333333"/>
</Storyboard>

<Storyboard x:Key="UnSelected">
	<james:ThickItem Mode="CubicEaseInOut" TargetName="icon" 
	Duration="0:0:0.5" Property="Margin" To="0 0 0 0"/>
	<james:ThickItem Mode="CubicEaseInOut" TargetName="name" 
	Duration="0:0:0.5" Property="Margin" To="0 60 0 0"/>
	<james:ColorItem Mode="CubicEaseInOut" TargetName="icon" 
	Duration="0:0:0.5" Property="Fill.Color" To="#44333333"/>
	<james:ColorItem Mode="CubicEaseInOut" TargetName="name" 
	Duration="0:0:0.5" Property="Foreground.Color" To="#00000000"/>
</Storyboard>

The key here is specifying the movement path in 'Selected' and the return path in 'UnSelected'.

Trigger

Finally, the implementation of animation in the ListBoxItem area concludes by declaring BeginStoryboard using Trigger to activate the respective (Selected/UnSelected) Storyboards.

Quote:

Unlike typical Trigger property changes, animations require a return scenario as well.

XAML
<ControlTemplate.Triggers>
    <Trigger Property="IsSelected" Value="True">
        <Trigger.EnterActions>
            <BeginStoryboard Storyboard="{StaticResource Selected}"/>
        </Trigger.EnterActions>
        <Trigger.ExitActions>
            <BeginStoryboard Storyboard="{StaticResource UnSelected}"/>
        </Trigger.ExitActions>
    </Trigger>
</ControlTemplate.Triggers>

The method of configuring animation in the ListBoxItem area is relatively simple. However, implementing the movement of the Circle component, which is introduced next, requires more complex calculations for dynamic behavior.

4. Movement of the Circle Component

Now it's time to implement the animation for the movement of the Circle component. Below is a video showing the dynamic movement of the Circle.

Image 8

The movement of the Circle component must be precisely calculated based on the clicked position, so it can't be implemented in XAML and needs to be handled dynamically in C# code. Therefore, a method for connecting XAML and Code Behind is required.

OnApplyTemplate

This method is used to retrieve the Circle area inside the MagicBar control. It's called internally at the connection point between the control and the template. Hence, it's implemented in the MagicBar class via override.

Then, the 'PART_Circle' named circle element is searched using the GetTemplateChild method. This Grid will be the target element for displaying the animation effect during interaction.

C#
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    Grid grid = (Grid)GetTemplateChild("PART_Circle");

    InitStoryboard(grid);
}

InitStoryboard

This method initializes the animation. Instances of ValueItem (_vi) and Storyboard (_sb) are created first. The animation effect set in ValueItem is QuinticEaseInOut, which slows down at the start and end of the animation, making it look smooth and natural.

The movement path for the Circle is specified as Canvas.LeftProperty, meaning it changes the horizontal position of the target element. The duration of the animation is set to 0.5 seconds. Finally, the animation target is set as the Circle component (Grid), and the defined animation is added to the storyboard.

C#
private void InitStoryboard(Grid circle)
{
    _vi = new();
    _sb = new();

    _vi.Mode = EasingFunctionBaseMode.QuinticEaseInOut;
    _vi.Property = new PropertyPath(Canvas.LeftProperty);
    _vi.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 500));

    Storyboard.SetTarget(_vi, circle);
    Storyboard.SetTargetProperty(_vi, _vi.Property);

    _sb.Children.Add(_vi);
}

OnSelectionChanged

The scenario for moving the Circle component is now implemented. In the MagicBar class, the OnSelectionChanged event method is implemented to handle the 'PART_Circle' (Grid) element and to execute (Begin) the storyboard.

Quote:

The MagicBar control, being a CustomControl derived from ListBox, has the advantage of flexibly implementing override features.

C#
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
    base.OnSelectionChanged(e);

    _vi.To = SelectedIndex * 80;
    _sb.Begin();
}

In this method, the logic to dynamically calculate and change the To value based on the SelectedIndex is implemented every time the selected menu changes.

5. Conclusion: Examining the Complete Source Code of the CustomControl

Finally, it's time to take a look at the complete structure of the XAML/Csharp code for the MagicBar control. This is an opportunity to see how elegantly and succinctly the control is implemented within the CustomControl structure.

Generic.xaml

Despite the implementation of various features, you can observe the maximally streamlined structure of XAML. Notably, the ControlTemplate structure included in the MagicBar simplifies complex layer hierarchies for easy viewing. Additionally, even small elements like Storyboard, Geometry, TextBlock, and JamesIcon are organized in a regular and systematic manner.

XAML
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:james="https://jamesnet.dev/xaml/presentation"
    xmlns:local="clr-namespace:NavigationBar">

    <Storyboard x:Key="Selected">
        <james:ThickItem Mode="CubicEaseInOut" TargetName="icon" 
        Duration="0:0:0.5" Property="Margin" To="0 -80 0 0"/>
        <james:ThickItem Mode="CubicEaseInOut" TargetName="name" 
        Duration="0:0:0.5" Property="Margin" To="0 45 0 0"/>
        <james:ColorItem Mode="CubicEaseInOut" TargetName="icon" 
        Duration="0:0:0.5" Property="Fill.Color" To="#333333"/>
        <james:ColorItem Mode="CubicEaseInOut" TargetName="name" 
        Duration="0:0:0.5" Property="Foreground.Color" To="#333333"/>
    </Storyboard>

    <Storyboard x:Key="UnSelected">
        <james:ThickItem Mode="CubicEaseInOut" TargetName="icon" 
        Duration="0:0:0.5" Property="Margin" To="0 0 0 0"/>
        <james:ThickItem Mode="CubicEaseInOut" TargetName="name" 
        Duration="0:0:0.5" Property="Margin" To="0 60 0 0"/>
        <james:ColorItem Mode="CubicEaseInOut" TargetName="icon" 
        Duration="0:0:0.5" Property="Fill.Color" To="#44333333"/>
        <james:ColorItem Mode="CubicEaseInOut" TargetName="name" 
        Duration="0:0:0.5" Property="Foreground.Color" To="#00000000"/>
    </Storyboard>
    
    <Style TargetType="{x:Type james:JamesIcon}" x:Key="Icon">
        <Setter Property="Icon" 
        Value="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem},
                Path=Tag}"/>
        <Setter Property="Width" Value="40"/>
        <Setter Property="Height" Value="40"/>
        <Setter Property="Fill" Value="#44333333"/>
    </Style>

    <Style TargetType="{x:Type TextBlock}" x:Key="Name">
        <Setter Property="Text" 
        Value="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem},
                Path=Content}"/>
        <Setter Property="HorizontalAlignment" Value="Center"/>
        <Setter Property="FontWeight" Value="Bold"/>
        <Setter Property="FontSize" Value="14"/>
        <Setter Property="Foreground" Value="#00000000"/>
        <Setter Property="Margin" Value="0 60 0 0"/>
    </Style>
    
    <Style TargetType="{x:Type ListBoxItem}" x:Key="MagicBarItem">
        <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
        <Setter Property="Background" Value="Transparent"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ListBoxItem}">
                    <Grid Background="{TemplateBinding Background}">
                        <james:JamesIcon x:Name="icon" Style="{StaticResource Icon}"/>
                        <TextBlock x:Name="name" Style="{StaticResource Name}"/>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsSelected" Value="True">
                            <Trigger.EnterActions>
                                <BeginStoryboard Storyboard="{StaticResource Selected}"/>
                            </Trigger.EnterActions>
                            <Trigger.ExitActions>
                                <BeginStoryboard Storyboard="{StaticResource UnSelected}"/>
                            </Trigger.ExitActions>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    
    <Geometry x:Key="ArcData">
        M0,0 L100,0 C95.167503,0 91.135628,3.4278221 90.203163,7.9846497 L90.152122,
        8.2704506 89.963921,9.1416779 C85.813438,27.384438 69.496498,41 50,41 30.5035,
        41 14.186564,27.384438 10.036079,9.1416779 L9.8478823,8.2704926 9.7968359,
        7.9846497 C8.8643732,3.4278221 4.8324914,0 0,0 z
    </Geometry>

    <Style TargetType="{x:Type Path}" x:Key="Arc">
        <Setter Property="Data" Value="{StaticResource ArcData}"/>
        <Setter Property="Width" Value="100"/>
        <Setter Property="Height" Value="100"/>
        <Setter Property="Fill" Value="#222222"/>
        <Setter Property="Margin" Value="-10 40 -10 -1"/>
    </Style>
    
    <Style TargetType="{x:Type Border}" x:Key="Bar">
        <Setter Property="Background" Value="#DDDDDD"/>
        <Setter Property="Margin" Value="0 40 0 0"/>
        <Setter Property="CornerRadius" Value="10"/>
    </Style>

    <Style TargetType="{x:Type Grid}" x:Key="Circle">
        <Setter Property="Width" Value="80"/>
        <Setter Property="Height" Value="80"/>
        <Setter Property="Canvas.Left" Value="-100"/>
    </Style>
    
    <Style TargetType="{x:Type local:MagicBar}">
        <Setter Property="ItemContainerStyle" Value="{StaticResource MagicBarItem}"/>
        <Setter Property="SnapsToDevicePixels" Value="True"/>
        <Setter Property="UseLayoutRounding" Value="True"/>
        <Setter Property="Background" Value="Transparent"/>
        <Setter Property="Width" Value="440"/>
        <Setter Property="Height" Value="120"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MagicBar}">
                    <Grid Background="{TemplateBinding Background}">
                        <Grid.Clip>
                            <RectangleGeometry Rect="0 0 440 120"/>
                        </Grid.Clip>
                        <Border Style="{StaticResource Bar}"/>
                        <Canvas Margin="20 0 20 0">
                            <Grid x:Name="PART_Circle" Style="{StaticResource Circle}">
                                <Path Style="{StaticResource Arc}"/>
                                <Ellipse Fill="#222222"/>
                                <Ellipse Fill="CadetBlue" Margin="6"/>
                            </Grid>
                        </Canvas>
                        <ItemsPresenter Margin="20 40 20 0"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <UniformGrid Columns="5"/>
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

MagicBar.cs

The process of locating the disjointed ControlTemplate elements through OnApplyTemplate is a very important and fundamental task, akin to a symbol of WPF. Finding the designated PART_Circle object (Grid) and dynamically composing and activating the Circle's movement (Move) animation whenever the menu changes serves to vividly demonstrate the vitality and dynamic capabilities of WPF.

C#
using Jamesnet.Wpf.Animation;
using Jamesnet.Wpf.Controls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace NavigationBar
{
    public class MagicBar : ListBox
    {
        private ValueItem _vi;
        private Storyboard _sb;

        static MagicBar()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MagicBar), 
                   new FrameworkPropertyMetadata(typeof(MagicBar)));
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            Grid grid = (Grid)GetTemplateChild("PART_Circle");

            InitStoryboard(grid);
        }

        private void InitStoryboard(Grid circle)
        {
            _vi = new();
            _sb = new();

            _vi.Mode = EasingFunctionBaseMode.QuinticEaseInOut;
            _vi.Property = new PropertyPath(Canvas.LeftProperty);
            _vi.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 500));

            Storyboard.SetTarget(_vi, circle);
            Storyboard.SetTargetProperty(_vi, _vi.Property);

            _sb.Children.Add(_vi);
        }

        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            base.OnSelectionChanged(e);

            _vi.To = SelectedIndex * 80;
            _sb.Begin();
        }
    }
}

As such, by implementing features that would normally be handled through UserControl in a CustomControl approach at the control level, we can achieve more sophisticated and efficient modularization.

With this, I conclude the explanation of the main features. Detailed information about this control is freely available through the GitHub source code. Additionally, in-depth tutorials are provided in both English and Chinese on YouTube and Bilibili, respectively. I look forward to seeing the diverse research and application of this control in XAML-based platforms.

History

  • 10th January, 2024: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)