具有自定义样式的 WPF Inherited Datepicker 不允许将子项标记为

WPF Inherited Datepicker with custom style won't allow children to be tabbed to

我想创建一个自定义 DatePicker,我用 MaskedTextBox(来自 WPFToolkit)代替了 DatePickerTextBox。无论出于何种原因,我都无法切换到控件中的 MaskedTextBox。相反,当项目被选中时 "highlights/focuses" 整个控件和再次被选中时将转到下一个可用控件。

我希望能够位于自定义控件之前的某个控件处,当切换到该控件时会将焦点放在自定义控件内的 MaskedTextBox 上。

此控件取决于存在的 MahApps 和 WPFToolkit。

Public Partial Class CustomMaskedDatePicker
Inherits DatePicker

Public Shared ReadOnly MaskedSelectedDateProperty As DependencyProperty = DependencyProperty.Register("MaskedSelectedDate", GetType(String), GetType(CustomMaskedDatePicker), New FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, AddressOf OnMaskedSelectedDateChanged))

Shared Sub New()
    DefaultStyleKeyProperty.OverrideMetadata(GetType(CustomMaskedDatePicker), New FrameworkPropertyMetadata(GetType(CustomMaskedDatePicker)))
End Sub

Private Shared Sub OnMaskedSelectedDateChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
    Dim tempDate As Date
    Dim selectedDate As Date? = DirectCast(d, CustomMaskedDatePicker).SelectedDate
    Dim tempDateString As String = e.NewValue

    If tempDateString = "" AndAlso IsNothing(DirectCast(d, CustomMaskedDatePicker).SelectedDate) Then Exit Sub

    If tempDateString = "" Then
        DirectCast(d, CustomMaskedDatePicker).SelectedDate = Nothing
        Exit Sub
    End If

    If tempDateString.Contains("_") Then
        DirectCast(d, CustomMaskedDatePicker).MaskedSelectedDate = If(IsNothing(selectedDate), "", String.Format("{0}{1}{2}", CDate(selectedDate).Month.ToString.PadLeft(2, "0"), CDate(selectedDate).Day.ToString.PadLeft(2, "0"), CDate(selectedDate).Year.ToString.PadLeft(4, "0")))
        Exit Sub
    End If

    If Not tempDateString.Contains("/") Then tempDateString = String.Format("{0}/{1}/{2}", tempDateString.Substring(0, 2), tempDateString.Substring(2, 2), tempDateString.Substring(4, 4))

    If Not Date.TryParse(tempDateString, tempDate) Then
        DirectCast(d, CustomMaskedDatePicker).MaskedSelectedDate = If(IsNothing(selectedDate), "", String.Format("{0}{1}{2}", CDate(selectedDate).Month.ToString.PadLeft(2, "0"), CDate(selectedDate).Day.ToString.PadLeft(2, "0"), CDate(selectedDate).Year.ToString.PadLeft(4, "0")))
        Exit Sub
    End If

    If IsNothing(selectedDate) OrElse selectedDate <> tempDate Then DirectCast(d, CustomMaskedDatePicker).SelectedDate = tempDate
End Sub

Public Property MaskedSelectedDate As String
    Get
        Return GetValue(MaskedSelectedDateProperty).ToString
    End Get
    Set
        SetValue(MaskedSelectedDateProperty, value.Replace("/", ""))
    End Set
End Property

Friend Const ElementMaskedTextBox As String = "PART_MaskedTextBox"

Protected Overrides Sub OnSelectedDateChanged(e As SelectionChangedEventArgs)
    MyBase.OnSelectedDateChanged(e)

    Dim dt As Date = CDate(e.AddedItems(0)).ToShortDateString()
    MaskedSelectedDate = String.Format("{0}{1}{2}", dt.Month.ToString.PadLeft(2, "0"), dt.Day.ToString.PadLeft(2, "0"), dt.Year.ToString.PadLeft(4, "0"))
End Sub 
End Class

XAML:

<ResourceDictionary
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:App"
         xmlns:controls="http://metro.mahapps.com/winfx/xaml/controls"
         xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
         mc:Ignorable="d">

<ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
    <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
    <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>

<Style TargetType="{x:Type local:CustomMaskedDatePicker}">
    <Setter Property="Foreground" Value="{DynamicResource TextBrush}" />
    <Setter Property="Background" Value="{DynamicResource ControlBackgroundBrush}" />
    <Setter Property="BorderBrush" Value="{DynamicResource TextBoxBorderBrush}" />
    <Setter Property="BorderThickness" Value="1" />
    <Setter Property="CalendarStyle" Value="{DynamicResource MetroCalendar}" />
    <Setter Property="controls:ControlsHelper.FocusBorderBrush" Value="{DynamicResource TextBoxFocusBorderBrush}" />
    <Setter Property="controls:ControlsHelper.MouseOverBorderBrush" Value="{DynamicResource TextBoxMouseOverBorderBrush}" />
    <Setter Property="controls:TextBoxHelper.IsMonitoring" Value="True" />
    <Setter Property="FontFamily" Value="{DynamicResource ContentFontFamily}" />
    <Setter Property="FontSize" Value="{DynamicResource ContentFontSize}" />
    <Setter Property="IsTodayHighlighted" Value="True" />
    <Setter Property="MinHeight" Value="26" />
    <Setter Property="Padding" Value="0" />
    <Setter Property="SelectedDateFormat" Value="Short" />
    <Setter Property="SnapsToDevicePixels" Value="True" />
    <Setter Property="Validation.ErrorTemplate" Value="{DynamicResource ValidationErrorTemplate}" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:CustomMaskedDatePicker}">
                <Grid x:Name="PART_Root">
                    <Border x:Name="Base" 
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                    <Grid x:Name="PART_InnerGrid" Margin="-5,0,0,0">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto"/>
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>

                        <Button x:Name="PART_Button" 
                                Grid.Column="1"
                                Height="Auto"
                                Style="{DynamicResource ChromelessButtonStyle}"
                                Foreground="{TemplateBinding Foreground}"
                                IsTabStop="False"
                                HorizontalAlignment="Right"
                                Margin="0,2,2,2">
                            <ContentControl Style="{DynamicResource {x:Type ContentControl}}"
                                            Content="M34,52H31V38.5C29.66,39.9 28.16,40.78 26.34,41.45V37.76C27.3,37.45 28.34,36.86 29.46,36C30.59,35.15 31.36,34.15 31.78,33H34V52M45,52V48H37V45L45,33H48V45H50V48H48V52H45M45,45V38.26L40.26,45H45M18,57V23H23V20A2,2 0 0,1 25,18H29C30.11,18 31,18.9 31,20V23H45V20A2,2 0 0,1 47,18H51C52.11,18 53,18.9 53,20V23H58V57H18M21,54H55V31H21V54M48.5,20A1.5,1.5 0 0,0 47,21.5V24.5A1.5,1.5 0 0,0 48.5,26H49.5C50.34,26 51,25.33 51,24.5V21.5A1.5,1.5 0 0,0 49.5,20H48.5M26.5,20A1.5,1.5 0 0,0 25,21.5V24.5A1.5,1.5 0 0,0 26.5,26H27.5A1.5,1.5 0 0,0 29,24.5V21.5A1.5,1.5 0 0,0 27.5,20H26.5Z"
                                            Padding="0"
                                            Width="21"
                                            Height="16">
                                <ContentControl.Template>
                                    <ControlTemplate TargetType="{x:Type ContentControl}">
                                        <Viewbox Margin="{TemplateBinding Padding}">
                                            <Path Fill="{TemplateBinding Foreground}"
                                                Stretch="Uniform"
                                                Data="{Binding Content, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay, Converter={local:xConNullToUnsetValueConverter}}"
                                                SnapsToDevicePixels="True"
                                                UseLayoutRounding="False" />
                                        </Viewbox>
                                    </ControlTemplate>
                                </ContentControl.Template>
                            </ContentControl>
                        </Button>

                        <DatePickerTextBox Grid.Column="0" x:Name="PART_TextBox" Visibility="Hidden" IsTabStop="False"/>
                        <xctk:MaskedTextBox x:Name="PART_MaskedTextBox" Style="{DynamicResource {x:Type TextBox}}"
                                            Grid.Column="0"
                                            HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                                            VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
                                            Foreground="{TemplateBinding Foreground}"
                                            Background="Transparent"
                                            FocusVisualStyle ="{x:Null}"
                                            FontFamily ="{DynamicResource ContentFontFamily}"
                                            FontSize="{DynamicResource ContentFontSize}"
                                            ScrollViewer.PanningMode="VerticalFirst"
                                            Stylus.IsFlicksEnabled="False"
                                            CaretBrush="{DynamicResource BlackBrush}"
                                            ContextMenu="{DynamicResource TextBoxMetroContextMenu}"
                                            Focusable="{TemplateBinding Focusable}"
                                            Mask="00/00/0000"
                                            Text="{Binding MaskedSelectedDate, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
                                            BorderThickness="0"
                                            IsTabStop="True"/>
                    </Grid>
                    <Popup x:Name="PART_Popup"
                               AllowsTransparency="True"
                               Placement="Bottom"
                               PlacementTarget="{Binding ElementName=PART_Root}"
                               StaysOpen="False"/>
                    <Border x:Name="DisabledVisualElement"
                            Background="{DynamicResource ControlsDisabledBrush}"
                            BorderBrush="{DynamicResource ControlsDisabledBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Opacity="0"
                            IsHitTestVisible="False"
                            SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />

                </Grid>

                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter TargetName="Base" Property="BorderBrush" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(controls:ControlsHelper.MouseOverBorderBrush)}" />
                    </Trigger>
                    <Trigger Property="IsFocused" Value="True" SourceName="PART_MaskedTextBox">
                        <Setter TargetName="Base" Property="BorderBrush" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(controls:ControlsHelper.FocusBorderBrush)}" />
                    </Trigger>
                    <Trigger Property="IsKeyboardFocusWithin" Value="True">
                        <Setter TargetName="Base" Property="BorderBrush" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(controls:ControlsHelper.FocusBorderBrush)}" />
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter TargetName="DisabledVisualElement" Property="Opacity" Value="0.6" />
                    </Trigger>
                    <Trigger SourceName="PART_Button" Property="IsMouseOver" Value="True">
                        <Setter TargetName="PART_Button" Property="Background" Value="{DynamicResource GrayBrush8}" />
                        <Setter TargetName="PART_Button" Property="Foreground" Value="{DynamicResource AccentColorBrush}" />
                    </Trigger>
                    <Trigger SourceName="PART_Button" Property="IsPressed" Value="True">
                        <Setter TargetName="PART_Button" Property="Background" Value="{DynamicResource BlackBrush}" />
                        <Setter TargetName="PART_Button" Property="Foreground" Value="{DynamicResource WhiteBrush}" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

通过覆盖代码中的 OnGotFocus 和 OnGotKeyboardFocus 解决了这个问题。

Public Partial Class CustomMaskedDatePicker
    Inherits DatePicker

    Public Shared ReadOnly MaskedSelectedDateProperty As DependencyProperty = DependencyProperty.Register("MaskedSelectedDate", GetType(String), GetType(CustomMaskedDatePicker), New FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, AddressOf OnMaskedSelectedDateChanged))
    Friend Const ElementMaskedTextBox As String = "PART_MaskedTextBox"

    Public Property MaskedSelectedDate As String
        Get
            Return GetValue(MaskedSelectedDateProperty).ToString
        End Get
        Set
            SetValue(MaskedSelectedDateProperty, value.Replace("/", ""))
        End Set
    End Property

    Shared Sub New()
        DefaultStyleKeyProperty.OverrideMetadata(GetType(CustomMaskedDatePicker), New FrameworkPropertyMetadata(GetType(CustomMaskedDatePicker)))
    End Sub

    Protected Overrides Sub OnGotFocus(e As RoutedEventArgs)
        Dim mskTxtBx As Object = Template.FindName(ElementMaskedTextBox, Me)
        If IsNothing(mskTxtBx) Then
            MyBase.OnGotFocus(e)
        Else 
            DirectCast(mskTxtBx, MaskedTextBox).Focus
        End If
    End Sub

    Protected Overrides Sub OnGotKeyboardFocus(e As KeyboardFocusChangedEventArgs)
        Dim mskTxtBx As Object = Template.FindName(ElementMaskedTextBox, Me)
        If IsNothing(mskTxtBx) Then
            MyBase.OnGotFocus(e)
        Else 
            DirectCast(mskTxtBx, MaskedTextBox).Focus
        End If
    End Sub

    Private Shared Sub OnMaskedSelectedDateChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
        Dim tempDate As Date
        Dim selectedDate As Date? = DirectCast(d, CustomMaskedDatePicker).SelectedDate
        Dim tempDateString As String = e.NewValue

        If tempDateString = "" AndAlso IsNothing(DirectCast(d, CustomMaskedDatePicker).SelectedDate) Then Exit Sub

        If tempDateString = "" Then
            DirectCast(d, CustomMaskedDatePicker).SelectedDate = Nothing
            Exit Sub
        End If

        If tempDateString.Contains("_") Then
            DirectCast(d, CustomMaskedDatePicker).MaskedSelectedDate = If(IsNothing(selectedDate), "", String.Format("{0}{1}{2}", CDate(selectedDate).Month.ToString.PadLeft(2, "0"), CDate(selectedDate).Day.ToString.PadLeft(2, "0"), CDate(selectedDate).Year.ToString.PadLeft(4, "0")))
            Exit Sub
        End If

        If Not tempDateString.Contains("/") Then tempDateString = String.Format("{0}/{1}/{2}", tempDateString.Substring(0, 2), tempDateString.Substring(2, 2), tempDateString.Substring(4, 4))

        If Not Date.TryParse(tempDateString, tempDate) Then
            DirectCast(d, CustomMaskedDatePicker).MaskedSelectedDate = If(IsNothing(selectedDate), "", String.Format("{0}{1}{2}", CDate(selectedDate).Month.ToString.PadLeft(2, "0"), CDate(selectedDate).Day.ToString.PadLeft(2, "0"), CDate(selectedDate).Year.ToString.PadLeft(4, "0")))
            Exit Sub
        End If

        If IsNothing(selectedDate) OrElse selectedDate <> tempDate Then DirectCast(d, CustomMaskedDatePicker).SelectedDate = tempDate
    End Sub

    Protected Overrides Sub OnSelectedDateChanged(e As SelectionChangedEventArgs)
        MyBase.OnSelectedDateChanged(e)

        Dim dt As Date = CDate(e.AddedItems(0)).ToShortDateString()
        MaskedSelectedDate = String.Format("{0}{1}{2}", dt.Month.ToString.PadLeft(2, "0"), dt.Day.ToString.PadLeft(2, "0"), dt.Year.ToString.PadLeft(4, "0"))
    End Sub
End Class