How to make my custom UserControl handle a two-way Binding when it is inside a Template Setter inside a DataTrigger?

|▌冷眼眸甩不掉的悲伤 提交于 2019-12-11 16:42:54

问题


  • If I put the TimeSpanPicker directly in the UserControl element, it works.
  • If I put a DateTimePicker (from the Extended WPF Toolkit) instead of my TimeSpanPicker, it works in both ways.
  • (This situation is what I wish to use, it is in the code below) If I put the TimeSpanPicker in a Template Setter inside a DataTrigger inside Style.Triggers inside UserControl.Style, the Binding stops working.

The binding that does not work in any way (although it is set to TwoWay) is this:

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

The TimeSpan property is a dependency property, and the CurrentValue property is directly inside an object that implements INotifyPropertyChanged for CurrentValue too. I also tried to use RelativeSource of the Binding to TemplatedParent and it does not work in my situation.

All the code necessary to reproduce the problem is below, excepting most of the wpf-timespanpicker assembly (I left here just the pieces that are relevant).

Steps to reproduce:

  1. Test the code as it is now.

1.1. Run the program.

1.2. Click on the Apply TimeSpan button.

1.3. The TimeSpanPicker appears at the top of the window displaying 0 seconds, although the TextBox below shows 00:10:00.

1.4. Change the value shown by the TimeSpanPicker by actioning on it like the end user.

1.5. The TextBox still displays 00:10:00.

  1. Change the code.

2.1. Put this instead of the Style attribute in UserControl1.xaml:

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

2.2. Repeat steps 1.2.-1.5. and see that the value in the TextBox is updated to reflect either the initial value of Model.CurrentValue (00:10:00) or the value set by the end user in the UI.

Binding diagnostics output

From what I see in this output, I think that the DataContext is wrong, it is set directly to the templated parent, not to its DataContext.

If I set the path of the Binding to DataContext.CurrentValue it still does not work, maybe because the DataContext is not set explicitly, it is inherited from the parent Control.

What is the most correct way to set up this Binding?

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:

NOTE: in this class I only set and get the TimeSpan property using the standard .NET property wrapper. I do not set up any Bindings in this class.

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

I wish that the Binding presented at the start of the question works, but it does not update neither the source, nor the target.


回答1:


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>

I do not verification this, but I think TargetType Must be settting in ControlTemplate.And BindingSource need to be explicit.




回答2:


Previously, the c-tor of the TimeSpanPicker was this (after renaming TimeSpanProperty to 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);
}

The OnValueChanged static event handler, set when the property was registered, was never called.

I commented out the Value = TimeSpan.FromSeconds(0); line and everything works well now. It was an useless line because the default value was already set in the registration of the ValueProperty dependency property. I still do not understand how repairing this makes the two-way Binding work perfectly. I think that it is possible that the default value was sent to the UI (in the Binding) and the property always compared that value with the value set directly inside the c-tor.



来源:https://stackoverflow.com/questions/55630467/how-to-make-my-custom-usercontrol-handle-a-two-way-binding-when-it-is-inside-a-t

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!