C# WPF UserControl:保持多个链接的依赖项属性同步,避免递归循环导致堆栈溢出

C# WPF UserControl:保持多个链接的依赖项属性同步,避免递归循环导致堆栈溢出,c#,wpf,mvvm,data-binding,user-controls,C#,Wpf,Mvvm,Data Binding,User Controls,我正在尝试为具有多个属性的不可变对象创建一个简单的用户控件 不可变对象及其属性应通过用户控件的依赖项属性公开。 当一个依赖属性更改时,其他属性也需要更改 我认为用一个小例子来解释我的问题是最好的: 假设我有一个不可变的类Person。 Person类有两个属性:FirstName和LastName 人员类别: public class Person { private readonly string _firstName; private read

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

不可变对象及其属性应通过用户控件的依赖项属性公开。 当一个依赖属性更改时,其他属性也需要更改

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

假设我有一个不可变的类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;
        }
    }
我想创建一个包含两个文本框的视图:一个用于名字,一个用于最后一个。我还希望这个视图有一个依赖属性,允许绑定到通过输入这两个文本框创建的Person对象

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

用法示例:

        <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>
不幸的是,在设置Person时,这将设置FirstName,这将设置Person,这将设置FirstName,依此类推。。。 您可以看到,这会导致循环,从而导致堆栈溢出

我可以想出一些解决办法:

添加布尔字段以防止递归调用,例如:_handlingOnPersonChanged,然后检查此项。 使用Equals函数仅设置不等式的新值。 然而,我不认为这些是解决这个问题的正确方法

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

提前谢谢

编辑1

看来这个例子确实正确。可能是因为在后台比较了值,只有在值实际发生更改时才调用值更改回调。在我的实际应用程序中,依赖项属性(如FirstName和LastName)不是字符串,而是非基本自定义类型。它们都有Equals方法和Equality操作符重载,但似乎Equals方法和'=='/'!='都没有重载我们称之为。这使得无论新旧值是否相等,每次都调用值更改回调。这再次导致递归循环:PersonChanged>FirstNameChanged>PersonChanged>FirstNameChanged>


正如ASh所指出的:调用PropertyChanged。。。我自己是不需要的,因为这是在引擎盖下处理,所以这些是删除。但是,删除它们并不能解决问题。

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

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

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

我为字符串制作了一个包装器,以查看如果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);
        }
    }



嗨,阿什,谢谢你的回答,我用你的参考资料编辑了我的问题。不幸的是,关于这个问题,它没有任何区别。@CédricMoers,如果你在分配前检查平等性,它会改善情况吗?我认为DP会在内部进行比较。它会在内部对字符串和其他类型进行新旧比较。因此,在本例中,它由依赖项prop处理。该方法“看到”新值等于旧值,并得出结论认为没有更改的理由,因此避免了递归循环。但是当使用非原语类型时,出于某种原因,它不使用Equals方法,也不使用任何相等重载。因此,它基本上进入了一个递归循环,因为没有任何东西可以阻止它。请看我自己的答案,我也手动检查是否相等。
        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;
        }

private void OnPersonChanged(Person oldValue, Person newValue)
{
    if (FirstName != newValue?.FirstName)
        FirstName = newValue?.FirstName;
    if (LastName != newValue?.LastName)
        LastName = newValue?.LastName;
}
    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);
        }
    }



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