WPF UserControl:使多个链接的依赖项属性保持同步,而不会导致递归循环 堆栈溢出

Posted

技术标签:

【中文标题】WPF UserControl:使多个链接的依赖项属性保持同步,而不会导致递归循环 堆栈溢出【英文标题】:WPF UserControl : Keep multiple linked dependency properties in sync without recursive loop causing Stack Overflow 【发布时间】:2021-04-06 02:31:30 【问题描述】:

我正在尝试为具有多个属性的不可变对象创建一个简单的用户控件。

不可变对象及其属性应该通过用户控件的依赖属性暴露出来。 当一个依赖属性改变时,其他的也需要改变。

我认为解释我的问题的最好方法是用一个小例子:

假设我有一个不可变类 Person。 Person 类有两个属性:FirstName 和 LastName。

人物类:

    public class Person
    
        private readonly string _firstName;
        private readonly string _lastName;
        public string FirstName => _firstName;
        public string LastName => _lastName;

        public Person(string firstName, string lastName)
        
            _firstName = firstName;
            _lastName = lastName;
        
    

我想创建一个包含 2 个文本框的视图:一个用于名字,一个用于最后一个。我还希望这个视图有一个依赖属性,允许绑定到由这两个文本框的输入创建的 Person 对象。

当我更改名字时,这应该会产生一个新的人。 当我更改姓氏时,这应该会产生一个新的人。 当我更改 Person 时,FirstName 和 LastName 也需要更改。

使用示例:

        <StackPanel Orientation="Vertical">
            <local:PersonView Person="Binding PersonA, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged"></local:PersonView>
            <local:PersonView Person="Binding PersonB, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged"></local:PersonView>
            <local:PersonView Person="Binding PersonC, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged"></local:PersonView>
        </StackPanel>

用户控制:

<UserControl x:Class="AutoMuse.GlobalSettings.PersonView"
             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:AutoMuse.GlobalSettings"
             mc:Ignorable="d"
             d:DataContext="d:DesignInstance Type=local:PersonView, IsDesignTimeCreatable=True"
             d:DesignWidth="300">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <StackPanel Orientation="Vertical"
                    Grid.Column="0"
                    Grid.Row="0">
            <Label>First Name</Label>
            <TextBox Text="Binding FirstName, Mode=TwoWay, UpdateSourceTrigger=LostFocus, RelativeSource=RelativeSource AncestorType=UserControl"></TextBox>
        </StackPanel>
        <StackPanel Orientation="Vertical"
                    Grid.Column="1"
                    Grid.Row="0">
            <Label>Last Name</Label>
            <TextBox Text="Binding LastName, Mode=TwoWay, UpdateSourceTrigger=LostFocus, RelativeSource=RelativeSource AncestorType=UserControl"></TextBox>
        </StackPanel>
        <StackPanel Orientation="Horizontal"
                    Grid.Column="0"
                    Grid.ColumnSpan="2"
                    Grid.Row="1">
            <TextBlock>Welcome: </TextBlock>
            <TextBlock Text="Binding Person, Mode=OneWay, UpdateSourceTrigger=PropertyChanged, TargetNullValue=-, FallbackValue=-, RelativeSource=RelativeSource AncestorType=UserControl"></TextBlock>
        </StackPanel>
    </Grid>
</UserControl>

用户控制代码:

    /// <summary>
    /// Interaction logic for PersonView.xaml
    /// </summary>
    public partial class PersonView : UserControl
    
        public string FirstName
        
            get  return (string)GetValue(FirstNameProperty); 
            set  SetValue(FirstNameProperty, value); 
        

        // Using a DependencyProperty as the backing store for FirstName.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty FirstNameProperty =
            DependencyProperty.Register("FirstName", typeof(string), typeof(PersonView), new PropertyMetadata(OnFirstNameChanged));

        public string LastName
        
            get  return (string)GetValue(LastNameProperty); 
            set  SetValue(LastNameProperty, value); 
        

        // Using a DependencyProperty as the backing store for LastName.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LastNameProperty =
            DependencyProperty.Register("LastName", typeof(string), typeof(PersonView), new PropertyMetadata(OnLastNameChanged));

        public Person Person
        
            get  return (Person)GetValue(PersonProperty); 
            set  SetValue(PersonProperty, value); 
        

        // Using a DependencyProperty as the backing store for FullName.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PersonProperty =
            DependencyProperty.Register("Person", typeof(string), typeof(PersonView), new PropertyMetadata(OnPersonChanged));


        private static void OnLastNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        
            if (d is PersonView personView)
                personView.OnLastNameChanged((string)e.OldValue, (string)e.NewValue);
        
        private static void OnFirstNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        
            if (d is PersonView personView)
                personView.OnFirstNameChanged((string)e.OldValue, (string)e.NewValue);
        
        private static void OnPersonChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        
            if (d is PersonView personView)
                personView.OnPersonChanged((Person)e.OldValue, (Person)e.NewValue);
        

        private void OnFirstNameChanged(string oldValue, string newValue) 
         
            Person = new Person(newValue, Person?.LastName); 
        
        private void OnLastNameChanged(string oldValue, string newValue) 
         
            Person = new Person(Person?.FirstName, newValue); 
        

        private void OnPersonChanged(Person oldValue, Person newValue)
        
            FirstName = newValue?.FirstName;
            LastName = newValue?.LastName;
        

        public PersonView()
        
            InitializeComponent();
        
    

主要关注这些方法:

        private void OnFirstNameChanged(string oldValue, string newValue) 
         
            Person = new Person(newValue, Person?.LastName); 
        
        private void OnLastNameChanged(string oldValue, string newValue) 
         
            Person = new Person(Person?.FirstName, newValue); 
        

        private void OnPersonChanged(Person oldValue, Person newValue)
        
            FirstName = newValue?.FirstName;
            LastName = newValue?.LastName;
        

不幸的是,在设置 Person 时,这将设置 FirstName,这将设置 Person,这将设置 FirstName,等等...... 您可以看到这会导致循环,从而导致堆栈溢出。

我能想到一些变通方法:

添加布尔字段以防止递归调用(例如:_handlingOnPersonChanged)然后检查。 使用 Equals 函数仅设置不等式的新值。

但是我不认为这些是解决这个问题的正确方法。

在没有这种递归循环的情况下保持多个依赖属性同步的好方法是什么?

提前致谢!

编辑 1

似乎该示例运行正常。可能是因为在后台比较了值,并且仅在值实际更改时才调用值更改回调。在我的实际应用程序中,FirstName 和 LastName 等依赖属性不是字符串,而是非原始自定义类型。它们同时具有 Equals 方法和 Equality 运算符重载,但似乎没有调用 Equals 方法,也没有调用 '=='/'!='。这使得无论新旧值是否相等,每次都会调用值更改回调。这再次导致递归循环:PersonChanged > FirstNameChanged > PersonChanged > FirstNameChanged > ...

正如 ASh 所指出的:不需要自己调用 OnPropertyChanged(...),因为这是在后台处理的,所以这些都被删除了。然而,删除它们并没有解决问题。

【问题讨论】:

【参考方案1】:

不要为任何属性调用 OnPropertyChanged。如果有任何值变化,将由属性变化回调处理。

private void OnPersonChanged(Person oldValue, Person newValue)

    if (FirstName != newValue?.FirstName)
        FirstName = newValue?.FirstName;
    if (LastName != newValue?.LastName)
        LastName = newValue?.LastName;

【讨论】:

嗨,ASh,感谢您的回答,我根据您的参考编辑了我的问题。不幸的是,关于这个问题,它没有任何区别。 @CédricMoers,如果您在分配前检查相等性,它会改善情况吗? (我认为 DP 在内部完成) 它确实在内部比较新旧字符串以及可能的其他类型。因此在示例中,它由依赖项处理。该方法“看到”新值等于旧值,并得出结论认为没有理由进行更改,因此避免了递归循环。但是当使用非原始类型时,由于某种原因,它不使用 Equals 方法,也不使用任何 Equality 重载。所以它基本上进入了一个递归循环,因为没有什么可以阻止它。另请参阅我自己的答案,我也在其中手动检查是否相等。【参考方案2】:

这是原始问题中提到的一种可能的解决方案(使用 Equals 函数仅设置不等式的新值)。 我想知道是否有一个不需要 Equality 方法的简单解决方案? 实现相等方法并不难,但出于好奇仍然想知道。


我为字符串创建了一个包装器,以查看如果 FirstName/LastName 的属性类型不是模拟我的实际应用程序的原始类型会发生什么。在这种情况下,它是一个简单的 StringWrapper:

    public class StringWrapper : IEquatable<StringWrapper>
    
        public string Value  get; set; 
        public override string ToString() => Value;
        public override bool Equals(object obj) => Equals(obj as StringWrapper);
        public bool Equals(StringWrapper other) => other != null && Value == other.Value;
        public override int GetHashCode() => -1937169414 + EqualityComparer<string>.Default.GetHashCode(Value);

        public StringWrapper(string str)
        
            Value = str;
        

        public static bool operator ==(StringWrapper left, StringWrapper right)
        
            return EqualityComparer<StringWrapper>.Default.Equals(left, right);
        

        public static bool operator !=(StringWrapper left, StringWrapper right)
        
            return !(left == right);
        
    


就我所见,Equals 方法,而不是 == 或 != 运算符是 not 自动调用的(我可能错了)。但是手动添加它并在设置之前检查是否相等可以解决问题,如下所示。


        private void OnFirstNameChanged(StringWrapper oldValue, StringWrapper newValue) 
         
            if (newValue != oldValue)
                Person = new Person(newValue?.Value, Person?.LastName?.Value); 
        
        private void OnLastNameChanged(StringWrapper oldValue, StringWrapper newValue) 
        
            if (newValue != oldValue)
                Person = new Person(Person?.FirstName?.Value, newValue?.Value); 
        

        private void OnPersonChanged(Person oldValue, Person newValue)
        
            if (newValue != oldValue)
            
                FirstName = newValue?.FirstName;
                LastName = newValue?.LastName;
            
        

【讨论】:

以上是关于WPF UserControl:使多个链接的依赖项属性保持同步,而不会导致递归循环 堆栈溢出的主要内容,如果未能解决你的问题,请参考以下文章

WPF ----在UserControl的xaml里绑定依赖属性

如何使 WPF UserControl 中的一个 DependencyProperty 更改另一个 [重复]

WPF TwoWay绑定在多个UserControl中

带有Image属性的WPF UserControl

数据绑定到 WPF 中的 UserControl

“UserControl”类型不支持直接内容