WPF双向绑定到属性的属性以替换父属性

3
我有一个使用DependencyProperties(或INotifyPropertyChanged)的ViewModel,其中包含一个非常简单的复合类型属性,例如System.Windows.Point。这个简单的复合类型不使用DependencyProperties或INotifyPropertyChanged,并且打算保持这种状态(我无法控制它)。
现在我想创建双向数据绑定到Point的X和Y属性,但是当其中一个属性被更改时,我希望整个Point类被替换,而不仅仅是更新成员。
以下是示例代码:
<Window ...>
    <StackPanel>
        <TextBox Text="{Binding TestPoint.X, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}, StringFormat=\{0:F\}}"/>
        <TextBox Text="{Binding TestPoint.Y, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}, StringFormat=\{0:F\}}"/>
        <!-- make following label update based on textbox changes above -->
        <Label Content="{Binding TestPoint, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}"/>
    </StackPanel>
</Window>

代码后台:

public partial class MainWindow : Window
{
    public Point TestPoint
    {
        get { return (Point)GetValue(TestPointProperty); }
        set { SetValue(TestPointProperty, value); }
    }
    public static readonly DependencyProperty TestPointProperty = DependencyProperty.Register("TestPoint", typeof(Point), typeof(MainWindow), new PropertyMetadata(new Point(1.0, 1.0)));
}

我想的是直接将两个文本框绑定到TestPoint属性,并使用IValueConverter过滤出特定成员,但是在ConvertBack方法中会有问题,因为修改X值时不再存在Y值。
我感觉这一定有一个非常简单的解决方案,但我还没有想到。
编辑:上面的代码只是一个简化的示例,实际应用比这更复杂。复合类型有大约7个成员,并且在整个应用程序中通常被广泛使用,因此将其拆分为单个成员并不合适。此外,我希望依靠依赖属性的OnChanged事件来调用其他更新,因此我真的需要替换整个类。

你可以将另一个值保存在转换器内部,你试过这样吗? - WPFUser
你是指作为转换器的成员吗?我没有考虑过这个。我想我需要为每个字段拥有单独的转换器实例,对吗?此外,这样做安全吗?在执行ConvertBack时,存储的Y值是否保证仍然有效? - markyxl
当绑定到XY属性时,请注意潜在的内存泄漏。由于Point不是INotifyPropertyChanged,也没有DependencyProperties,因此绑定将使用PropertyDescriptor,如果绑定不是OneTime,则可能会导致泄漏:https://dev59.com/3WMl5IYBdhLWcg3wRlLo - wkl
是的,对于每个字段(技术上来说是每个x和y字段),您需要拥有单独的转换器实例。是的,当执行ConvertBack时存储的值将是有效的。 - WPFUser
3个回答

7
为什么不使用访问器?
public partial class MainWindow : Window
{
    public Point TestPoint
    {
        get { return (Point)GetValue(TestPointProperty); }
        set { SetValue(TestPointProperty, value); }
    }

    public double TestPointX
    {
        get { return this.TestPoint.X; }
        set
        { 
            SetValue(TestPointProperty, new Point(value, this.TestPointY);
        }
    }

    public double TestPointY
    {
        get { return this.TestPoint.Y; }
        set
        { 
            SetValue(TestPointProperty, new Point(this.TestPointX, value);
        }
    }

    public static readonly DependencyProperty TestPointProperty = DependencyProperty.Register("TestPoint", typeof(Point), typeof(MainWindow), new PropertyMetadata(new Point(1.0, 1.0)));
}

在您的XAML代码中:

<Window ...>
<StackPanel>
        <TextBox Text="{Binding TestPointX, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}, StringFormat=\{0:F\}}"/>
        <TextBox Text="{Binding TestPointY, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}, StringFormat=\{0:F\}}"/>
        <Label Content="{Binding TestPoint, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}"/>
    </StackPanel>
</Window>

这是一个针对简单问题有点冗长的解决方案,但我认为这将是最安全和易读的解决方案,谢谢! - markyxl

0
我认为在这种情况下,引入两个DependencyProperties会更容易,一个用于X值,另一个用于Y值,并简单地绑定它们。在每个DependencyProperty的PropertyMetadata中注册一个方法,以便在每次值更改时可以替换您的Point对象(如果您仍然需要它)。
您还可以使用适当的单向IMultiValueConverter将标签绑定到两个属性的MultiBinding。

抱歉,我无法完全分离这些值。我已将解释添加到原问题底部。 - markyxl

0

正如我在评论中所说,您可以尝试这样做。当我们将转换器添加到特定控件的资源中时,相同的实例将用于子控件。

  <Grid>
    <Grid.Resources />
    <StackPanel>
        <StackPanel>
            <StackPanel.Resources>
                <local:PointConverter x:Key="PointConverter" />
            </StackPanel.Resources>
            <TextBox Text="{Binding TestPoint, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}, Converter={StaticResource PointConverter}, ConverterParameter=x}" />
            <TextBox Text="{Binding TestPoint, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}, Converter={StaticResource PointConverter}, ConverterParameter=y}" />
            <!--  make following label update based on textbox changes above  -->
            <Label Content="{Binding TestPoint, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" />
        </StackPanel>
        <StackPanel>
            <StackPanel.Resources>
                <local:PointConverter x:Key="PointConverter" />
            </StackPanel.Resources>
            <TextBox Text="{Binding TestPoint2, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}, Converter={StaticResource PointConverter}, ConverterParameter=x}" />
            <TextBox Text="{Binding TestPoint2, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}, Converter={StaticResource PointConverter}, ConverterParameter=y}" />
            <!--  make following label update based on textbox changes above  -->
            <Label Content="{Binding TestPoint2, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" />
        </StackPanel>
    </StackPanel>
</Grid>

以及代码后台

    public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }


    public Point TestPoint
    {
        get
        {
            return (Point)GetValue(TestPointProperty);
        }
        set
        {
            SetValue(TestPointProperty, value);
        }
    }

    // Using a DependencyProperty as the backing store for TestPoint.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty TestPointProperty =
        DependencyProperty.Register("TestPoint", typeof(Point), typeof(MainWindow), new PropertyMetadata(new Point(1.0, 1.0)));
     public Point TestPoint2
    {
        get
        {
            return (Point)GetValue(TestPoint2Property);
        }
        set
        {
            SetValue(TestPoint2Property, value);
        }
    }

    // Using a DependencyProperty as the backing store for TestPoint.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty TestPoint2Property =
        DependencyProperty.Register("TestPoint2", typeof(Point), typeof(MainWindow), new PropertyMetadata(new Point(1.0, 1.0)));


}

public class PointConverter : IValueConverter
{
    double knownX = 0.0;
    double knownY = 0.0;
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
         if (parameter.ToString() == "x")
        {
            knownX = ((Point)value).X;
            return ((Point)value).X;
        }
        else
        {
            knownY = ((Point)value).Y;
            return ((Point)value).Y;
        }

    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {

         Point p = new Point();
        if (parameter.ToString() == "x")
        {
            p.Y = knownY;
            p.X = double.Parse(value.ToString());
        }
        else
        {
            p.X = knownX;
            p.Y = double.Parse(value.ToString());
        }
        return p;
    }
}

注意:我还没有添加任何空值检查。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接