Как заставить мой пользовательский UserControl обрабатывать двухстороннюю привязку, когда она находится внутри шаблона Setter внутри DataTrigger? - PullRequest
0 голосов
/ 11 апреля 2019
  • Если я помещаю TimeSpanPicker непосредственно в элемент UserControl, он работает.
  • Если я помещаю DateTimePicker (из расширенного инструментария WPF) вместо моего 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.Нажмите на кнопку Apply TimeSpan.

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

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

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

screenshot 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:

ПРИМЕЧАНИЕ: в этом классе я только устанавливаю и получаю свойство TimeSpany используя стандартную оболочку свойств .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"));
}

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

Ответы [ 2 ]

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

0 голосов
/ 12 апреля 2019

try:

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

...