Как заставить мой пользовательский элемент управления обрабатывать двустороннюю привязку, когда он находится внутри установщика шаблонов внутри DataTrigger?

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

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

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

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

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

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

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

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

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

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

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

1.5. В текстовом поле по-прежнему отображается 00:10:00.

скриншот 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), либо значение, установленное конечным пользователем в пользовательском интерфейсе.

скриншот 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"));
}

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


person silviubogan    schedule 11.04.2019    source источник
comment
Непонятно, что такое DataContext элемента управления. Почему CurrentValue свойство зависимости? Разбейте свой пример на MCVE.   -  person mm8    schedule 11.04.2019
comment
@ mm8 Я обновил свой вопрос. Спасибо вам за помощь.   -  person silviubogan    schedule 12.04.2019


Ответы (2)


пытаться:

    <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 должен быть явным.

person ZJ.Wang    schedule 12.04.2019

Раньше 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. Я до сих пор не понимаю, как исправление этого делает двустороннюю привязку безупречной. Я думаю, что возможно, что значение по умолчанию было отправлено в пользовательский интерфейс (в привязке), и свойство всегда сравнивало это значение со значением, установленным непосредственно внутри c-tor.

person silviubogan    schedule 16.04.2019