WPF ComboBox绑定行为

3
我有以下的XAML标记:
<TextBox x:Name="MyTextBox" Text="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox x:Name="MyComboBox" ItemsSource="{Binding Products}" DisplayMemberPath="ProductName"
    SelectedValue="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber}"
    SelectedValuePath="ProductNumber" />

我的视图DataContext绑定到一个包含名为SelectedCustomer的公共属性的视图模型。客户对象包含类型为Product的FavouriteProduct属性,而Product对象包含公共属性ProductNumber和ProductName。
我要实现的行为是使ComboBox的SelectedItem更新TextBox的文本,反之亦然。ComboBox到TextBox的更新效果很好。选择ComboBox中的任何产品都会将该产品的产品编号更新到TextBox中。然而,当我尝试从另一个方向进行时,我得到了一些奇怪的行为。它只适用于所选项目之前的项目。我将尝试解释:
考虑以下产品清单([产品编号],[产品名称]):
  1. 芬达
  2. 百事可乐
  3. 可口可乐
  4. 雪碧
现在假设所选客户的最喜欢的产品是可口可乐(必须是开发人员)。因此,当窗口打开时,文本框读取3,组合框读取可口可乐。很好。现在让我们将文本框中的产品编号更改为2。组合框会更新其值为百事可乐。现在尝试将文本框中的产品编号更改为高于可口可乐(3)的任何数字。不是很好。选择4(雪碧)或5(水)会使组合框恢复到可口可乐。因此,行为似乎是从源中打开窗口宽度下方的任何项都不起作用。将其设置为1(芬达),其他所有内容都无法使用。将其设置为5(水),则所有内容均可使用。这可能与ComboBox的某些初始化有关吗?潜在的错误?有没有人看到过这种行为,很好奇。

更新:

阅读了Mike Brown的回答后,我为SelectedProduct和SelectedProductNumber创建了属性。我遇到的问题是,一旦您从ComboBox中选择了某个内容,就会陷入无限循环,其中属性会不断更新彼此。我是否未正确实现OnPropertyChanged处理程序或是否有什么我错过的东西?这是来自我的ViewModel的代码片段:

private int _SelectedProductNumber = -1;
        public int SelectedProductNumber
        {
            get
            {
                if (_SelectedProductNumber == -1 && SelectedCustomer.Product != null)
                    _SelectedProductNumber = SelectedCustomer.Product.ProductNumber;
                return _SelectedProductNumber;
            }
            set
            {
                _SelectedProductNumber = value;
                OnPropertyChanged("SelectedProductNumber");
                _SelectedProduct = ProductList.FirstOrDefault(s => s.ProductNumber == value);
            }
        }

        private Product _SelectedProduct;
        public Product SelectedProduct
        {
            get
            {
                if (_SelectedProduct == null)
                    _SelectedProduct = SelectedCustomer.Product;
                return _SelectedProduct;
            }
            set
            {
                _SelectedProduct = value;
                OnPropertyChanged("SelectedProduct");
                _SelectedProductNumber = value.ProductNumber;
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string property)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(property));
        }

更新2

我现在稍微改变了实现方式,通过更新SelectedCustomer.FavouriteProduct的两个属性,然后在读取它们的值时使用它。现在这个方法可以工作了,但我不确定这是否是“正确的方法”。

private int _SelectedProductNumber = 0;
public int SelectedProductNumber
{
    get
    {
        if (SelectedCustomer.Product != null)
            _SelectedProductNumber = SelectedCustomer.Product.ProductNumber;
        return _SelectedProductNumber;
    }
    set
    {
        _SelectedProductNumber = value;
        SelectedCustomer.FavouriteProduct = ProductList.FirstOrDefault(s => s.ProductNumber == value);
        OnPropertyChanged("SelectedProductNumber");
        OnPropertyChanged("SelectedProduct");
    }
}

private Product _SelectedProduct;
public Product SelectedProduct
{
    get
    {
        if (SelectedCustomer.Product != null)
            _SelectedProduct = SelectedCustomer.Product;
        return _SelectedProduct;
    }
    set
    {
        _SelectedProduct = value;
        SelectedCustomer.FavouriteProduct = value;
        OnPropertyChanged("SelectedProduct");
        OnPropertyChanged("SelectedProductNumber");
    }
}

我的错误,我已经用正确的代码更新了我的回答。 - Michael Brown
为确保在属性实际未更改时不触发更改事件,请使用 if (_selectedProduct != value) { /*...*/ } 代码块。 - Brett Ryan
你的新解决方案是完全有效的(事实上,它避免了重复信息,这非常好)。然而,对我来说奇怪的是你仍然收到堆栈溢出的错误。我将代码放入VS中,它在我的电脑上运行得很好。不确定你那边出了什么问题。你对selectedcustomer.favoriteproduct的更改是否也会促使其他两个属性的更新?无论如何,我已经使用“在我的机器上可用”的版本更新了我的示例代码。 - Michael Brown
3个回答

1

您的目标不是很清晰,因此我已经编写了以下内容来支持我所看到的任何选项。

要将两个元素绑定到一个项目并保持同步,您可以在下面所示的组合框上设置IsSynchronizedWithCurrentItem="True"

<TextBox x:Name="MyTextBox" Text="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox x:Name="MyComboBox" ItemsSource="{Binding Products}" DisplayMemberPath="ProductName"
    SelectedValue="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber}"
    IsSynchronizedWithCurrentItem="True"
    SelectedValuePath="ProductNumber" />

这意味着当前窗口中绑定到相同背景对象的所有内容将保持同步,不会出现您看到的奇怪行为。

这段引用来自较长的MSDN文章,描述了其效果:

IsSynchronizedWithCurrentItem属性很重要,因为当选择更改时,这就是窗口所关心的“当前项目”发生变化的地方。这告诉WPF引擎,此对象将用于更改当前项目。如果没有此属性,则DataContext中的当前项目不会更改,因此您的文本框将假定它仍在列表中的第一项上。

然后按照其他答案建议的设置Mode=TwoWay,只能确保在更新文本框时更新底层对象,在更新对象时更新文本框。

这使得文本框编辑所选项目的文本,而不是选择具有匹配文本的组合列表中的项目(这可能是您试图实现的替代想法?)

为了实现同步选择效果,值得在组合框上设置IsEditable="True",以允许用户键入项目并放弃文本框。或者,如果您需要两个框,请使用第二个组合框替换文本框,并设置IsSynchronizedWithCurrentItem="True"IsEditable="True",然后进行样式设置,使其看起来像文本框。

这看起来非常有前途,但不幸的是,我仍然得到相同的行为。当我从ComboBox中选择某些内容时,TextBox会更新,但反过来就不能正常工作。我认为我可能完全错了。 - Christian Hagelid
你是想让文本框中的文本更改选择,还是更改/编辑所选项目的文本?我已经更新了答案,提供了两种解决方案。 - John
我想给用户提供两种选择产品的选项。一种是通过ComboBox选择产品,另一种是通过键入产品编号进行选择。我可以使ComboBox可编辑,但这将不允许我输入产品编号。我还没有研究过使用带有IsSynchronizedWithCurrentItem选项的单独ComboBox。 - Christian Hagelid

1
你想要做的是在你的ViewModel上公开当前选定产品和当前选定产品编号的不同属性。当选定的产品发生变化时,更新产品编号,反之亦然。因此,你的ViewModel应该看起来像这样:
public class MyViewModel:INotifyPropertyChanged
{
    private Product _SelectedProduct;
    public Product SelectedProduct
    {
        get { return _SelectedProduct; }
        set
        {
            _SelectedProduct = value;
            PropertyChanged(this, new PropertyChangedEventArgs("SelectedProduct"));
            _SelectedProductID = _SelectedProduct.ID;
            PropertyChanged(this, new PropertyChangedEventArgs("SelectedProductID"));
        }
    }
    private int _SelectedProductID;
    public int SelectedProductID
    {
        get { return _SelectedProductID; }
        set
        {
            _SelectedProductID = value;
            PropertyChanged(this, new PropertyChangedEventArgs("SelectedProductID")); 
            _SelectedProduct = _AvailableProducts.FirstOrDefault(p => p.ID == value); 
            PropertyChanged(this,new PropertyChangedEventArgs("SelectedProduct"));
        }
    }  
    private IEnumerable<Product> _AvailableProducts = GetAvailableProducts();

    private static IEnumerable<Product> GetAvailableProducts()
    {
        return new List<Product>
                   {
                       new Product{ID=1, ProductName = "Coke"},
                       new Product{ID = 2, ProductName="Sprite"},
                       new Product{ID = 3, ProductName = "Vault"},
                       new Product{ID=4, ProductName = "Barq's"}
                   };
    }

    public IEnumerable<Product> AvailableProducts
    {
        get { return _AvailableProducts; }
    }  
    private Customer _SelectedCustomer; 
    public Customer SelectedCustomer
    {
        get { return _SelectedCustomer; } 
        set
        {
            _SelectedCustomer = value; 
            PropertyChanged(this, new PropertyChangedEventArgs("SelectedCustomer")); 
            SelectedProduct = value.FavoriteProduct;
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
}

所以现在你的XAML绑定到了适当的属性,viewModel负责同步。

<TextBox 
  x:Name="MyTextBox" 
  Text="{Binding Path=SelectedProductID, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox 
  x:Name="MyComboBox" 
  ItemsSource="{Binding AvailableProducts}" 
  DisplayMemberPath="ProductName" 
  SelectedItem="{Binding SelectedProduct}" />

别忘了实现INotifyPropertyChanged的其余部分和GetAvailableProducts函数。此外可能会有一些错误。我在这里手动输入而不是使用VS,但你应该能够理解大致意思。


我想我可能需要做这样的事情。我已经添加了相应的属性,但是在ProductId和Product属性之间存在问题。只要你更改它们中的任何一个,就会陷入无限循环,并最终得到StackOverflow异常 ;) 不过这可能是因为我实现处理程序的方式(我对WPF还比较新)。我将在问题中更新代码。 - Christian Hagelid
抱歉,那是我犯的低级错误...与其更新相反的属性,不如只更新后备字段并调用属性更改...我会使用正确的实现更新代码。 - Michael Brown
1
好的,完成了...现在应该可以正常工作了...他们应该为导致堆栈溢出的答案添加一个特殊徽章 ;) - Michael Brown
哎呀,我用这段代码仍然得到相同的错误。只要我更新SelectedProductNumber从SelectedProduct属性(或反之亦然),我就会陷入无限循环中。现在我已经稍微改变了解决方案,通过从两个属性更新SelectedCustomer.FavouriteProduct字段(请参见更新2)。 - Christian Hagelid

0
尝试使用以下代码: SelectedItem="{Binding Path=YourPath, Mode=TwoWay"} 而不是设置SelectedValue和SelectedValuePath。
也许SelectedValue也可以工作,但别忘了加上Mode=TwoWay,因为这不是默认值。
一个好的方法是使用主细节模式 - 将主视图(如组合框)绑定到数据源集合,将细节视图(如文本框)绑定到源集合中的选定项,并使用绑定转换器来读取/写入相应的属性。
以下是一个示例:http://blogs.microsoft.co.il/blogs/tomershamam/archive/2008/03/28/63397.aspx 请注意,主绑定的格式为{Binding}或{Binding SourceCollection},而详细信息绑定的格式为{Binding}或{Binding SourceCollection}。
要使此功能正常工作,您需要使用一个保留所选项目的对象来包装集合。WPF内置了这样一个对象:ObjectDataProvider。

Example: http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/068977c9-95a8-4b4a-9d38-b0cc36d06446


我不知道如何使用SelectedItem来根据ProductNumber设置所选的产品。 - Christian Hagelid
修改了答案以解决问题。 - Danny Varod

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