You should create a ? UserControlor custom
Controlthat hosts a
ListBoxto display the sections and the buttons to navigate between them. You then animate the
ScrollViewer` to navigate to the selected section.
This makes the implementation dynamic, means you don't have to add new animations etc when adding new sections.
- create the abstract base class or interface e.g.
SectionItem
. Its the template for all section items (data models) and contains common properties and logic.
- Each section (e.g., News, DLC, Mods) implements this base class/interface and is added to a common collection e.g.
Sections
in the view model.
- Create a
UserControl
or custom Control
SectionsView
. SectionsView
hosts the navigation buttons and will display the individual sections or SectionItem
items. When a button is pressed, the animated navigation to the section is executed.
- This
SectionView
exposes a ItemsSource
proerty which binds to the view model's Sections
collection.
- Create a
DataTemplate
for each SectionItem
. This template defines the appearance of the actual section. Those templates are added to the ResourceDictionary
of the SectionView
.
- To animate the
ScrollViewer
of the ListBox
the SectionsView
must implement a DependencyProperty
e.g. NavigationOffset
. This is necessary, because ScrollViewer
only offers a method to modify its offsets.
Create the section items
Each item must extend the base class SectionItem
:
SectionItem.cs
public abstract class SectionItem : INotifyPropertyChanged
{
public SectionItem(Section id)
{
this.id = id;
}
private Section id;
public Section Id
{
get => this.id;
set
{
this.id = value;
OnPropertyChanged();
}
}
private string title;
public string Title
{
get => this.title;
set
{
this.title = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Implement the actual section models
class DlcSection : SectionItem
{
public DlcSection(Section id) : base(id)
{
}
}
class SettingsSection : SectionItem
{
public SettingsSection(Section id) : base(id)
{
}
}
class NewsSection : SectionItem
{
public NewsSection(Section id) : base(id)
{
}
}
The enum
which is used as section ID of SectionItem
and CommandParameter
Section.cs
public enum Section
{
None = 0,
Dlc,
Settings,
News
}
Implement the SectionsView
The SectionsView
extends UserControl
(or Control
) and encapsulates the display of the SectionItem
items and their navigation. To trigger navigation it exposes a routed comnmand NavigateToSectionRoutedCommand
:
SectionsView.xaml.cs
public partial class SectionsView : UserControl
{
#region Routed commands
public static readonly RoutedUICommand NavigateToSectionRoutedCommand = new RoutedUICommand(
"Navigates to section by section ID which is an enum value of the enumeration 'Section'.",
nameof(SectionsView.NavigateToSectionRoutedCommand),
typeof(SectionsView));
#endregion Routed commands
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
"ItemsSource",
typeof(IEnumerable),
typeof(SectionsView),
new PropertyMetadata(default(IEnumerable)));
public IEnumerable ItemsSource
{
get => (IEnumerable) GetValue(SectionsView.ItemsSourceProperty);
set => SetValue(SectionsView.ItemsSourceProperty, value);
}
public static readonly DependencyProperty NavigationOffsetProperty = DependencyProperty.Register(
"NavigationOffset",
typeof(double),
typeof(SectionsView),
new PropertyMetadata(default(double), SectionNavigator.OnNavigationOffsetChanged));
public double NavigationOffset
{
get => (double) GetValue(SectionsView.NavigationOffsetProperty);
set => SetValue(SectionsView.NavigationOffsetProperty, value);
}
private ScrollViewer Navigator { get; set; }
public SectionsView()
{
InitializeComponent();
this.Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (TryFindVisualChildElement(this.SectionItemsView, out ScrollViewer scrollViewer))
{
this.Navigator = scrollViewer;
}
}
private static void OnNavigationOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as SectionsView).Navigator.ScrollToVerticalOffset((double) e.NewValue);
}
private void NavigateToSection_OnExecuted(object sender, ExecutedRoutedEventArgs e)
{
SectionItem targetSection = this.SectionItemsView.Items
.Cast<SectionItem>()
.FirstOrDefault(section => section.Id == (Section) e.Parameter);
if (targetSection == null)
{
return;
}
double verticalOffset = 0;
if (this.Navigator.CanContentScroll)
{
verticalOffset = this.SectionItemsView.Items.IndexOf(targetSection);
}
else
{
var sectionContainer =
this.SectionItemsView.ItemContainerGenerator.ContainerFromItem(targetSection) as UIElement;
Point absoluteContainerPosition = sectionContainer.TransformToAncestor(this.Navigator).Transform(new Point());
verticalOffset = this.Navigator.VerticalOffset + absoluteContainerPosition.Y;
}
var navigationAnimation = this.Resources["NavigationAnimation"] as DoubleAnimation;
navigationAnimation.From = this.Navigator.VerticalOffset;
navigationAnimation.To = verticalOffset;
BeginAnimation(SectionNavigator.NavigationOffsetProperty, navigationAnimation);
}
private void NavigateToSection_OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = e.Parameter is Section;
}
private bool TryFindVisualChildElement<TChild>(DependencyObject parent, out TChild resultElement)
where TChild : DependencyObject
{
resultElement = null;
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is Popup popup)
{
childElement = popup.Child;
}
if (childElement is TChild)
{
resultElement = childElement as TChild;
return true;
}
if (TryFindVisualChildElement(childElement, out resultElement))
{
return true;
}
}
return false;
}
}
SectionsView.xaml
<UserControl x:Class="SectionsView">
<UserControl.Resources>
<!-- Animation can be changed, but name must remain the same -->
<DoubleAnimation x:Key="NavigationAnimation" Storyboard.TargetName="Root" Storyboard.TargetProperty="NavigationOffset"
Duration="0:0:0.3">
<DoubleAnimation.EasingFunction>
<PowerEase EasingMode="EaseIn" Power="5" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!-- DataTemplates for different section items -->
<DataTemplate DataType="{x:Type local:DlcSection}">
<Grid Height="200" Background="Green">
<TextBlock Text="{Binding Title}" FontSize="18" />
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type local:SettingsSection}">
<Grid Height="200" Background="OrangeRed">
<TextBlock Text="{Binding Title}" FontSize="18" />
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:NewsSection}">
<Grid Height="200" Background="Yellow">
<TextBlock Text="{Binding Title}" FontSize="18" />
</Grid>
</DataTemplate>
</UserControl.Resources>
<UserControl.CommandBindings>
<CommandBinding Command="{x:Static local:SectionNavigator.NavigateToSectionRoutedCommand}"
Executed="NavigateToSection_OnExecuted" CanExecute="NavigateToSection_OnCanExecute" />
</UserControl.CommandBindings>
<Grid>
<StackPanel>
<Button Content="Navigate" Command="{x:Static local:SectionNavigator.NavigateToSectionRoutedCommand}"
CommandParameter="{x:Static local:Section.News}" />
<Button Content="Navigate" Command="{x:Static local:SectionNavigator.NavigateToSectionRoutedCommand}"
CommandParameter="{x:Static local:Section.Dlc}" />
<Button Content="Navigate" Command="{x:Static local:SectionNavigator.NavigateToSectionRoutedCommand}"
CommandParameter="{x:Static local:Section.Settings}" />
<!-- ScrollViewer.CanContentScroll is set to False to enable smooth scrolling for large (high) items -->
<ListBox x:Name="SectionItemsView"
Height="250"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:SectionNavigator}, Path=Sections}"
ScrollViewer.CanContentScroll="False" />
</StackPanel>
</Grid>
</UserControl>
Usage
ViewModel.cs
class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<SectionItem> Sections { get; set; }
public ViewModel()
{
this.Sections = new ObservableCollection<SectionItem>
{
new NewsSection(Section.News) {Title = "News"},
new DlcSection(Section.Dlc) {Title = "DLC"},
new SettingsSection(Section.Settings) {Title = "Settings"}
};
}
}
MainWindow.xaml
<Window>
<Window.Resources>
<ViewModel />
</Window.Resources>
<SectionsView ItemsSource="{Binding Sections}" />
</Window>