你应该创建一个 ? UserControlor customControlthat hosts aListBoxto display the sections and the buttons to navigate between them. You then animate theScrollViewer`导航到选定的部分。
这使得实现动态化,意味着您在添加新部分时不必添加新动画等。
- 创建抽象基类或接口,例如
SectionItem。它是所有部分项(数据模型)的模板,包含通用属性和逻辑。
- 每个部分(例如,新闻、DLC、Mods)都实现了这个基类/接口,并被添加到一个公共集合中,例如
Sections 在视图模型中。
- 创建
UserControl 或自定义Control SectionsView。 SectionsView 承载导航按钮并将显示各个部分或SectionItem 项目。按下按钮时,将执行到该部分的动画导航。
- 这个
SectionView 公开了一个绑定到视图模型的Sections 集合的ItemsSource 属性。
- 为每个
SectionItem 创建一个DataTemplate。该模板定义了实际部分的外观。这些模板被添加到SectionView 的ResourceDictionary。
- 要为
ListBox 的ScrollViewer 设置动画,SectionsView 必须实现DependencyProperty,例如NavigationOffset。这是必要的,因为ScrollViewer 只提供了一种修改其偏移量的方法。
创建部分项目
每个项目都必须扩展基类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));
}
}
实现实际截面模型
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)
{
}
}
enum 用作SectionItem 和CommandParameter 的部分ID
Section.cs
public enum Section
{
None = 0,
Dlc,
Settings,
News
}
实现SectionsView
SectionsView 扩展了UserControl(或Control)并封装了SectionItem 项目的显示及其导航。为了触发导航,它公开了一个路由命令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>
用法
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>