如何在我的自定义UserControl处理DataTrigger内的模板设置器时处理双向绑定?

问题描述 投票:0回答:2
  • 如果我将TimeSpan Picker直接放在User Control元素中,它就可以工作。
  • 如果我放置一个DateTimePicker(来自Extended WPF Toolkit)而不是我的TimeSpanPicker,它可以两种方式工作。
  • (这种情况是我想要使用的,它位于下面的代码中)如果我将TimeSpanPicker放在UserControl.Style中Style.Triggers内的DataTrigger内的模板设置器中,Binding将停止工作。

不以任何方式工作的绑定(虽然设置为TwoWay)是这样的:

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

TimeSpan属性是依赖属性,CurrentValue属性直接位于为CurrentValue实现INotifyPropertyChanged的对象内。我也尝试使用绑定到TemplatedParent的RelativeSource,它在我的情况下不起作用。

重现问题所需的所有代码都在下面,除了大多数wpf-timespanpicker程序集(我在这里只留下相关的部分)。

重现步骤:

  1. 像现在一样测试代码。

1.1。运行程序。

1.2。单击Apply TimeSpan按钮。

1.3。 TimeSpanPicker出现在显示0秒的窗口顶部,尽管下面的TextBox显示为00:10:00。

1.4。通过像最终用户一样对其进行操作来更改TimeSpanPicker显示的值。

1.5。 TextBox仍然显示00:10:00。

screenshot 1

  1. 更改代码。

2.1。将其替换为UserControl1.xaml中的Style属性:

<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)的初始值或UI中最终用户设置的值。

screenshot 2

Binding diagnostics output

从我在这个输出中看到的,我认为DataContext是错误的,它直接设置为模板化父级,而不是其DataContext。

如果我将Binding的路径设置为DataContext.CurrentValue,它仍然不起作用,可能是因为没有显式设置DataContext,它是从父Control继承的。

设置此绑定的最正确方法是什么?

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();
    }
}

The Model class

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>

A part of TimeSpanPicker.xaml.cs:

注意:在这个类中,我只使用标准.NET属性包装器设置并获取TimeSpan属性。我没有在这个类中设置任何Bindings。

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"));
}

我希望在问题开头提出的Binding有效,但它既不会更新源,也不会更新目标。

wpf data-binding controltemplate datatrigger two-way-binding
2个回答
0
投票

尝试:

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

我不验证这个,但我认为Target Type必须在ControlTemplate中设置.BindingSource需要是显式的。


0
投票

以前,TimeSpanPicker的c-tor就是这个(在将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依赖项属性的注册中设置了默认值。我仍然不明白如何修复这使得双向绑定工作完美。我认为有可能将默认值发送到UI(在Binding中),并且属性始终将该值与直接在c-tor内设置的值进行比较。

© www.soinside.com 2019 - 2024. All rights reserved.