• Если я помещаю TimeSpan Picker непосредственно в элемент управления пользователя, он работает.
  • Если я поставлю DateTimePicker (из Extended WPF Toolkit) вместо моего TimeSpanPicker, он будет работать обоими способами.
  • (Это ситуация, которую я хочу использовать, она приведена в приведенном ниже коде). Если я помещу TimeSpanPicker в Setter шаблона внутри DataTrigger внутри Style.Triggers внутри UserControl.Style, привязка перестает работать.

Привязка, которая никак не работает (хотя она установлена на TwoWay):

    TimeSpan="{Binding Path=CurrentValue,
        Mode=TwoWay,
        RelativeSource={RelativeSource Mode=TemplatedParent},
        UpdateSourceTrigger=PropertyChanged}"

Свойство TimeSpan является свойством зависимости, а свойство CurrentValue находится непосредственно внутри объекта, который также реализует INotifyPropertyChanged для CurrentValue. Я также попытался использовать RelativeSource объекта Binding to TemplatedParent, и он не работает в моей ситуации.

Весь код, необходимый для воспроизведения проблемы, приведен ниже, за исключением большей части сборки wpf-timespanpicker (я оставил здесь только те части, которые имеют отношение к делу).

Действия по воспроизведению:

  1. Протестируйте код, как сейчас.

1.1 . Запустите программу.

1.2 . Нажмите на кнопку «Применить интервал времени».

1.3 . TimeSpanPicker отображается в верхней части окна и отображает 0 секунд, хотя в текстовом поле ниже показано 00:10:00.

1.4 . Измените значение, отображаемое TimeSpanPicker, действуя как конечный пользователь.

1,5 . TextBox по-прежнему отображает 00:10:00.

screenshot 1

  1. Измените код.

2.1 . Поместите это вместо атрибута Style в UserControl1.xaml:

<w:TimeSpanPicker
    HorizontalAlignment="Center"
    VerticalAlignment="Center"
    MinHeight="50" MinWidth="70"
    TimeSpan="{Binding Path=CurrentValue,
        Mode=TwoWay,
        UpdateSourceTrigger=PropertyChanged}"/>

2.2 . Повторите шаги 1.2.-1.5. и убедитесь, что значение в TextBox обновлено, чтобы отразить либо начальное значение Model.CurrentValue (00:10:00), либо значение, установленное конечным пользователем в пользовательском интерфейсе.

screenshot 2

Диагностика выходных данных

Из того, что я вижу в этом выводе, я думаю, что DataContext неверен, он установлен непосредственно для шаблонного родителя, а не для его DataContext.

Если я задаю путь привязки для DataContext.CurrentValue, он все равно не работает, возможно, из-за того, что DataContext не установлен явно, он наследуется от родительского элемента управления.

Как правильно настроить привязку?

System.Windows.Data Warning: 56 : Created BindingExpression (hash=4620049) for Binding (hash=22799085)
System.Windows.Data Warning: 58 :   Path: 'CurrentValue'
System.Windows.Data Warning: 62 : BindingExpression (hash=4620049): Attach to wpf_timespanpicker.TimeSpanPicker.TimeSpan (hash=34786562)
System.Windows.Data Warning: 67 : BindingExpression (hash=4620049): Resolving source 
System.Windows.Data Warning: 70 : BindingExpression (hash=4620049): Found data context element: <null> (OK)
System.Windows.Data Warning: 72 :   RelativeSource.TemplatedParent found UserControl1 (hash=31201899)
System.Windows.Data Warning: 78 : BindingExpression (hash=4620049): Activate with root item UserControl1 (hash=31201899)
'cs-wpf-test-7.exe' (CLR v4.0.30319: cs-wpf-test-7.exe): Loaded 'C:\Windows\Microsoft.Net\assembly\GAC_MSIL\PresentationFramework-SystemCore\v4.0_4.0.0.0__b77a5c561934e089\PresentationFramework-SystemCore.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
System.Windows.Data Warning: 108 : BindingExpression (hash=4620049):   At level 0 - for UserControl1.CurrentValue found accessor <null>
System.Windows.Data Error: 40 : BindingExpression path error: 'CurrentValue' property not found on 'object' ''UserControl1' (Name='')'. BindingExpression:Path=CurrentValue; DataItem='UserControl1' (Name=''); target element is 'TimeSpanPicker' (Name=''); target property is 'TimeSpan' (type 'TimeSpan')
System.Windows.Data Warning: 80 : BindingExpression (hash=4620049): TransferValue - got raw value {DependencyProperty.UnsetValue}
System.Windows.Data Warning: 88 : BindingExpression (hash=4620049): TransferValue - using fallback/default value TimeSpan (hash=0)
System.Windows.Data Warning: 89 : BindingExpression (hash=4620049): TransferValue - using final value TimeSpan (hash=0)

UserControl1.xaml :

<UserControl xmlns:wpf_timespanpicker="clr-namespace:wpf_timespanpicker;assembly=wpf-timespanpicker"  x:Class="cs_wpf_test_7.UserControl1"
             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:xwpf="clr-namespace:Xceed.Wpf.Toolkit;assembly=Xceed.Wpf.Toolkit"
             xmlns:local="clr-namespace:cs_wpf_test_7"
             xmlns:w="clr-namespace:wpf_timespanpicker;assembly=wpf-timespanpicker"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <local:MyValueConverter x:Key="MyConv"/>

        <ControlTemplate x:Key="x">
            <w:TimeSpanPicker
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                MinHeight="50" MinWidth="70"
                TimeSpan="{Binding Path=CurrentValue,
                    Mode=TwoWay,
                    RelativeSource={RelativeSource Mode=TemplatedParent},
                    UpdateSourceTrigger=PropertyChanged}"/>
        </ControlTemplate>

        <ControlTemplate x:Key="y">
            <xwpf:DateTimePicker
                Value="{Binding Path=CurrentValue,
                    Mode=TwoWay,
                    UpdateSourceTrigger=PropertyChanged}"/>
        </ControlTemplate>
    </UserControl.Resources>

    <UserControl.Style>
        <Style TargetType="{x:Type local:UserControl1}">
            <Style.Triggers>
                <DataTrigger Binding="{Binding Path=CurrentValue, Mode=OneWay, Converter={StaticResource MyConv}}"
                                            Value="TimeSpan">
                    <Setter Property="Template" Value="{StaticResource x}"/>
                </DataTrigger>

                <DataTrigger Binding="{Binding Path=CurrentValue, Mode=OneWay, Converter={StaticResource MyConv}}"
                                            Value="DateTime">
                    <Setter Property="Template" Value="{StaticResource y}"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </UserControl.Style>
</UserControl>

MyValueConverter.cs

public class MyValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value == null ? "null" : value.GetType().Name;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Класс модели

public class Model : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    internal object _CurrentValue = null;
    public object CurrentValue
    {
        get
        {
            return _CurrentValue;
        }
        set
        {
            if (_CurrentValue != value)
            {
                _CurrentValue = value;
                PropertyChanged?.Invoke(this,
                    new PropertyChangedEventArgs(
                        "CurrentValue"));
            }
        }
    }
}

MainWindow.xaml

<Window x:Class="cs_wpf_test_7.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:cs_wpf_test_7"
        mc:Ignorable="d"
        Title="MainWindow" Height="187" Width="254"
        Loaded="Window_Loaded">
    <StackPanel>
        <local:UserControl1>
        </local:UserControl1>

        <TextBox Text="{Binding Path=CurrentValue,
            Mode=OneWay,
            UpdateSourceTrigger=PropertyChanged}"></TextBox>

        <Button Name="MyApplyTimeSpanButton"
                Click="MyApplyTimeSpanButton_Click">
            Apply TimeSpan
        </Button>
        <Button Name="MyApplyDateTimeButton"
                Click="MyApplyDateTimeButton_Click">
            Apply DateTime
        </Button>
    </StackPanel>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    Model m = new Model();

    public MainWindow()
    {
        InitializeComponent();
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        DataContext = m;
    }

    private void MyApplyTimeSpanButton_Click(object sender, RoutedEventArgs e)
    {
        m.CurrentValue = TimeSpan.FromMinutes(10);
    }

    private void MyApplyDateTimeButton_Click(object sender, RoutedEventArgs e)
    {
        m.CurrentValue = DateTime.Now;
    }
}

TimeSpanPicker.xaml :

<UserControl x:Class="wpf_timespanpicker.TimeSpanPicker"
             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:wpf_timespanpicker"
             mc:Ignorable="d"
             d:DesignHeight="170" d:DesignWidth="365"

             KeyboardNavigation.TabNavigation="Continue"
             IsTabStop="True"
             Focusable="True"

             GotKeyboardFocus="UserControl_GotKeyboardFocus"
             LostKeyboardFocus="UserControl_LostKeyboardFocus"
             KeyDown="UserControl_KeyDown"
             PreviewKeyDown="UserControl_PreviewKeyDown"
             PreviewMouseDown="UserControl_PreviewMouseDown"
             MouseDown="UserControl_MouseDown"
             MouseLeave="UserControl_MouseLeave"
             PreviewMouseUp="UserControl_PreviewMouseUp"
             GotFocus="UserControl_GotFocus"
             LostFocus="UserControl_LostFocus"
             IsEnabledChanged="UserControl_IsEnabledChanged"
             Loaded="UserControl_Loaded"
             MouseWheel="UserControl_MouseWheel">
    <Canvas SizeChanged="Canvas_SizeChanged">
        <local:ArrowButton x:Name="hPlusBtn" State="True"/>
        <local:TwoDigitsDisplay x:Name="tdd1" MouseUp="Tdd1_MouseUp"/>
        <local:ArrowButton x:Name="hMinusBtn" State="False"/>
        <local:ColonDisplay x:Name="tbc1"/>
        <local:ArrowButton x:Name="mPlusBtn" State="True"/>
        <local:TwoDigitsDisplay x:Name="tdd2" MouseUp="Tdd2_MouseUp"/>
        <local:ArrowButton x:Name="mMinusBtn" State="False"/>
        <local:ColonDisplay x:Name="tbc2"/>
        <local:ArrowButton x:Name="sPlusBtn" State="True"/>
        <local:TwoDigitsDisplay x:Name="tdd3" MouseUp="Tdd3_MouseUp"/>
        <local:ArrowButton x:Name="sMinusBtn" State="False"/>
    </Canvas>
</UserControl>

Часть TimeSpanPicker.xaml.cs:

ПРИМЕЧАНИЕ: в этом классе я только устанавливаю и получаю свойство TimeSpan, используя стандартную оболочку свойства .NET. Я не устанавливаю никаких Привязок в этом классе.

public static readonly DependencyProperty TimeSpanProperty =
    DependencyProperty.Register("TimeSpan", typeof(TimeSpan), typeof(TimeSpanPicker),
        new PropertyMetadata(TimeSpan.Zero, OnTimeSpanChanged, TimeSpanCoerceCallback));
private static void OnTimeSpanChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    (d as TimeSpanPicker).OnTimeSpanChanged();
}
private static object TimeSpanCoerceCallback(DependencyObject d, object baseValue)
{
    return ((TimeSpan)baseValue).Subtract(
        TimeSpan.FromMilliseconds(((TimeSpan)baseValue).Milliseconds));
}
public TimeSpan TimeSpan
{
    get
    {
        return (TimeSpan)GetValue(TimeSpanProperty);
    }
    set
    {
        SetValue(TimeSpanProperty, value);
    }
}
private void OnTimeSpanChanged()
{
    ApplyTimeSpanToVisual(TimeSpan);
    TimeSpanValueChanged?.Invoke(this, EventArgs.Empty);
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("TimeSpan"));
}

Я бы хотел, чтобы привязка, представленная в начале вопроса, работала, но она не обновляет ни источник, ни цель.

0
silviubogan 11 Апр 2019 в 13:36

2 ответа

Лучший ответ

Ранее c-tor для TimeSpanPicker был таким (после переименования TimeSpanProperty в ValueProperty):

public TimeSpanPicker()
{
    InitializeComponent();

    hPlusBtn.MyButton.Click += HPlusBtn_Click;
    hMinusBtn.MyButton.Click += HMinusBtn_Click;

    mPlusBtn.MyButton.Click += MPlusBtn_Click;
    mMinusBtn.MyButton.Click += MMinusBtn_Click;

    sPlusBtn.MyButton.Click += SPlusBtn_Click;
    sMinusBtn.MyButton.Click += SMinusBtn_Click;

    LongPressTimer.Tick += LongPressTimer_Tick;

    Value = TimeSpan.FromSeconds(0);
    ApplyValueToVisual(Value);
}

Обработчик статических событий OnValueChanged, установленный при регистрации свойства, никогда не вызывался.

Я закомментировал строку Value = TimeSpan.FromSeconds(0);, и теперь все работает хорошо. Это была бесполезная строка, потому что значение по умолчанию уже было установлено при регистрации свойства зависимостей ValueProperty. Я до сих пор не понимаю, как это исправляет двустороннюю привязку. Я думаю, что возможно, что значение по умолчанию было отправлено в пользовательский интерфейс (в Binding), и свойство всегда сравнивало это значение со значением, установленным непосредственно внутри c-tor.

0
silviubogan 16 Апр 2019 в 09:48

Пытаться:

    <ControlTemplate x:Key="x" TargetType={x:Type local:ClockValueScreen}>
        <wpf:TimeSpanPicker
            HorizontalAlignment="Stretch"
            VerticalAlignment="Stretch"
            HorizontalContentAlignment="Stretch"
            VerticalContentAlignment="Stretch"
            Margin="0,0,7,0"
            Loaded="MyTimeSpanPicker_Loaded"
            TimeSpan="{Binding Path=CurrentValue,RelativeSource={RelativeSource Mode=TemplatedParent}, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,diag:PresentationTraceSources.TraceLevel=High}"/>
    </ControlTemplate>
    <ControlTemplate x:Key="y" TargetType={x:Type local:ClockValueScreen}>
        <Viewbox>
            <xwpf:DateTimePicker
                Value="{Binding Path=CurrentValue,RelativeSource={RelativeSource Mode=TemplatedParent}, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
                Loaded="DateTimePicker_Loaded"/>
        </Viewbox>
    </ControlTemplate>

Я не проверяю это, но я думаю, что TargetType должен быть задан в ControlTemplate. И BindingSource должен быть явным.

0
ZJ.Wang 12 Апр 2019 в 05:43