引言
面包屑控件是在单窗体应用中常用的一种控件,用来指示当前页面的路由信息,以及提供导航功能。本文将完整地呈现一个面包屑控件的诞生过程。
一、面包屑控件的结构
面包屑控件由一个主体Breadcrumb和内部的BreadcrumbItem集合组成,这种关系自然而然让人联想到ItemsControl。以下是具体的控件实现:
Breadcrumb控件
Breadcrumb 控件继承自 ItemsControl,并实现了 ICommandSource 接口。它的主要功能是管理面包屑项并处理导航事件。
/// <summary>
/// Represents a breadcrumb control that displays a hierarchical navigation path.
/// </summary>
public class Breadcrumb : ItemsControl, ICommandSource
{
#region 指定子项容器
/// <inheritdoc/>
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new BreadcrumbItem();
}
/// <inheritdoc/>
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<BreadcrumbItem>(item, out recycleKey);
}
#endregion
#region 样式化属性
/// <summary>
/// Gets or sets the separator used between breadcrumb items.
/// </summary>
public static readonly StyledProperty<object> SeparatorProperty =
AvaloniaProperty.Register<Breadcrumb, object>(nameof(Separator), "/");
/// <summary>
/// Gets or sets the separator used between breadcrumb items.
/// </summary>
public object Separator
{
get => GetValue(SeparatorProperty);
set => SetValue(SeparatorProperty, value);
}
#endregion
#region 路由事件
/// <summary>
/// Defines the <see cref="NavigateTo"/> event.
/// </summary>
public static readonly RoutedEvent<CustomRoutedEventArgs<string>> NavigateToEvent =
RoutedEvent.Register<Breadcrumb, CustomRoutedEventArgs<string>>(nameof(NavigateTo), RoutingStrategies.Bubble);
/// <summary>
/// Occurs when navigation is requested.
/// </summary>
public event EventHandler<CustomRoutedEventArgs<string>> NavigateTo
{
add => AddHandler(NavigateToEvent, value);
remove => RemoveHandler(NavigateToEvent, value);
}
#endregion
#region 命令实现
private bool _commandCanExecute = true;
/// <summary>
/// Gets or sets the command to be executed when a breadcrumb item is clicked.
/// </summary>
public ICommand Command
{
get { return GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
/// <summary>
/// Defines the <see cref="Command"/> property.
/// </summary>
public static readonly StyledProperty<ICommand> CommandProperty = AvaloniaProperty.Register<Breadcrumb, ICommand>(nameof(Command));
/// <summary>
/// Gets or sets the parameter to be passed to the <see cref="Command"/>.
/// </summary>
public object? CommandParameter
{
get { return GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
/// <summary>
/// Defines the <see cref="CommandParameter"/> property.
/// </summary>
public static readonly StyledProperty<object?> CommandParameterProperty = AvaloniaProperty.Register<Breadcrumb, object?>(nameof(CommandParameter));
/// <summary>
/// Handles the CanExecuteChanged event of the command.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">An EventArgs that contains the event data.</param>
public void CanExecuteChanged(object sender, EventArgs e)
{
(var command, var parameter) = (Command, CommandParameter);
CanExecuteChanged(command, parameter);
}
/// <summary>
/// Updates the command execution state.
/// </summary>
/// <param name="command">The command to check.</param>
/// <param name="parameter">The command parameter.</param>
private void CanExecuteChanged(ICommand? command, object? parameter)
{
if (!((ILogical)this).IsAttachedToLogicalTree)
{
return;
}
var canExecute = command is null || command.CanExecute(parameter);
if (canExecute != _commandCanExecute)
{
_commandCanExecute = canExecute;
UpdateIsEffectivelyEnabled();
}
}
#endregion
#region 构造函数
/// <summary>
/// Initializes a new instance of the <see cref="Breadcrumb"/> class.
/// </summary>
public Breadcrumb()
{
this.AddHandler(BreadcrumbItem.NavigateToEvent, (s, e) =>
{
var arg = new CustomRoutedEventArgs<string>(NavigateToEvent)
{
Data = e.Data
};
RaiseEvent(arg);
(var command, var parameter) = (Command, CommandParameter ?? e.Data);
if (!arg.Handled && command is not null && command.CanExecute(parameter))
{
command.Execute(parameter);
arg.Handled = true;
}
});
}
#endregion
}
BreadcrumbItem 控件
BreadcrumbItem 控件表示单个面包屑项,继承自 HeaderedContentControl。它包含一个 To 属性,用于指定导航目标,并定义了 NavigateTo 事件。
/// <summary>
/// Represents an individual item within a <see cref="Breadcrumb"/> control.
/// </summary>
public class BreadcrumbItem : HeaderedContentControl, ICommandSource
{
#region 样式化属性
/// <summary>
/// Defines the <see cref="To"/> property.
/// </summary>
public static readonly StyledProperty<string> ToProperty =
AvaloniaProperty.Register<BreadcrumbItem, string>(nameof(To));
/// <summary>
/// Gets or sets the target location or identifier for this breadcrumb item.
/// </summary>
public string To
{
get => GetValue(ToProperty);
set => SetValue(ToProperty, value);
}
#endregion
#region 路由事件
/// <summary>
/// Defines the NavigateToEvent.
/// </summary>
public static readonly RoutedEvent<CustomRoutedEventArgs<string>> NavigateToEvent =
RoutedEvent.Register<BreadcrumbItem, CustomRoutedEventArgs<string>>(nameof(NavigateTo), RoutingStrategies.Bubble);
/// <summary>
/// Event handler for the NavigateTo event.
/// </summary>
public event EventHandler<CustomRoutedEventArgs<string>> NavigateTo
{
add => AddHandler(NavigateToEvent, value);
remove => RemoveHandler(NavigateToEvent, value);
}
#endregion
#region 命令实现
private bool _commandCanExecute = true;
/// <summary>
/// Gets or Sets the <see cref="Command"/>
/// </summary>
public ICommand Command
{
get { return GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
/// <summary>
/// Defines the <see cref="Command"/> property.
/// </summary>
public static readonly StyledProperty<ICommand> CommandProperty = AvaloniaProperty.Register<BreadcrumbItem, ICommand>(nameof(Command));
/// <summary>
/// Gets or Sets the <see cref="CommandParameter"/>
/// </summary>
public object? CommandParameter
{
get { return GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
/// <summary>
/// Gets or sets a parameter to be passed to the <see cref="Command"/>
/// </summary>
public static readonly StyledProperty<object?> CommandParameterProperty = AvaloniaProperty.Register<BreadcrumbItem, object?>(nameof(CommandParameter));
/// <inheritdoc/>
public void CanExecuteChanged(object sender, EventArgs e)
{
(var command, var parameter) = (Command, CommandParameter);
CanExecuteChanged(command, parameter);
}
private void CanExecuteChanged(ICommand? command, object? parameter)
{
if (!((ILogical)this).IsAttachedToLogicalTree)
{
return;
}
var canExecute = command is null || command.CanExecute(parameter);
if (canExecute != _commandCanExecute)
{
_commandCanExecute = canExecute;
UpdateIsEffectivelyEnabled();
}
}
#endregion
#region 默认实现
/// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var breadcrumb = this.FindAncestorOfType<Breadcrumb>();
if (breadcrumb != null && breadcrumb.DisplayMemberBinding != null)
{
this[!ContentProperty] = breadcrumb.DisplayMemberBinding;
}
var button = e.NameScope.Find("PART_NavButton") as Button;
if (button != null)
{
button.Click += NavButton_Click;
}
}
private void NavButton_Click(object? sender, RoutedEventArgs e)
{
var arg = new CustomRoutedEventArgs<string>(NavigateToEvent)
{
Data = To
};
RaiseEvent(arg);
(var command, var parameter) = (Command, CommandParameter ?? arg.Data);
if (!arg.Handled && command is not null && command.CanExecute(parameter))
{
command.Execute(parameter);
arg.Handled = true;
}
}
#endregion
}
以上代码中用到了带自定义参数的RoutedEventArgs对象,实现如下:
/// <summary>
/// 通用的带自定义参数的路由事件参数对象
/// </summary>
/// <typeparam name="T"></typeparam>
public class CustomRoutedEventArgs<T> : RoutedEventArgs
{
/// <summary>
/// 事件参数
/// </summary>
public T Data { get; set; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="routedEvent">路由事件</param>
/// <param name="source">事件发起者</param>
public CustomRoutedEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source)
{
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="routedEvent">路由事件</param>
public CustomRoutedEventArgs(RoutedEvent routedEvent) : base(routedEvent)
{
}
}
二、面包屑控件的模板及样式
在Avalonia中,使用ControlTheme定义控件的模板,在Styles中定义控件的样式。
控件模板
<ControlTheme x:Key="{x:Type local:Breadcrumb}" TargetType="local:Breadcrumb">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ItemsPresenter Margin="{TemplateBinding Padding}" ItemsPanel="{TemplateBinding ItemsPanel}"/>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type local:BreadcrumbItem}" TargetType="local:BreadcrumbItem">
<Setter Property="Padding" Value="5,0"/>
<Setter Property="Foreground" Value="{Binding RelativeSource={RelativeSource AncestorType=local:Breadcrumb}, Path=Foreground}"/>
<Setter Property="Header" Value="{Binding RelativeSource={RelativeSource AncestorType=local:Breadcrumb}, Path=Separator}"/>
<Setter Property="Template">
<ControlTemplate>
<StackPanel Orientation="Horizontal">
<ContentPresenter x:Name="PART_Header"
Content="{TemplateBinding Header}"
Margin="5,0"
VerticalAlignment="Center"
IsVisible="{TemplateBinding Header,Converter={x:Static ObjectConverters.IsNotNull}}"/>
<ContentPresenter x:Name="PART_Content"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
IsVisible="{TemplateBinding To,Converter={x:Static StringConverters.IsNullOrEmpty}}"
VerticalAlignment="Center"/>
<Button x:Name="PART_NavButton"
Classes="text"
Foreground="{TemplateBinding Foreground}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
IsVisible="{TemplateBinding To,Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
VerticalAlignment="Center"/>
</StackPanel>
</ControlTemplate>
</Setter>
</ControlTheme>
控件样式
<!-- 第一个元素不显示分隔符 -->
<Style Selector="local|BreadcrumbItem:nth-child(1) /template/ ContentPresenter#PART_Header">
<Setter Property="IsVisible" Value="False"/>
</Style>
<!--文本样式的按钮-->
<Style Selector="Button.text">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentPresenter x:Name="content"
Content="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}"/>
</Border>
</ControlTemplate>
</Setter>
<!-- PointerOver state -->
<Style Selector="^:pointerover">
<Setter Property="Opacity" Value="0.6"/>
</Style>
<!-- Pressed state -->
<Style Selector="^:pressed">
<Setter Property="Opacity" Value="0.8"/>
</Style>
<!-- Disabled state -->
<Style Selector="^:disabled">
<Setter Property="Opacity" Value="0.5"/>
</Style>
</Style>
总结
通过以上的实现,就创建好了一个具备导航功能的面包屑控件,且支持数据绑定,可以用于各种场景。