WPF无法绑定ComboBox的SelectedItem属性

9

我已经广泛搜索了,但是我找不到任何解决方案来解决我的问题。

在我的项目中,我有几个ComboBox,并且正在寻找自动完成的解决方案,然后我找到了一个很好的解决方案,并将其应用于我的项目,我还将解决方案的样式应用于我的所有ComboBox。

之后,SelectedItem停止工作了,有人可以帮忙吗?

我的ComboBox:

<ComboBox Name="CbOwnerType" Grid.Column="1" Grid.Row="2" ItemsSource="{Binding Path=OwnerTypes, Mode=OneWay}" SelectedItem="{Binding Owner.OwnerType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValuePath="Id" DisplayMemberPath="Name" Margin="5,0,10,0" />

我的风格:

<Style TargetType="{x:Type ComboBox}">
    <Setter Property="FocusVisualStyle" Value="{x:Null}" />
    <Setter Property="Foreground" Value="Black" />
    <Setter Property="FontWeight" Value="ExtraBold" />
    <Setter Property="IsEditable" Value="False"/>
    <Setter Property="IsSynchronizedWithCurrentItem" Value="False" />
    <Setter Property="StaysOpenOnEdit" Value="True" />
    <Setter Property="SnapsToDevicePixels" Value="True"/>
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
    <Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ComboBox}">
                <Grid>
                    <ToggleButton Name="ToggleButton" Template="{StaticResource ComboBoxToggleButton}" Grid.Column="2" Focusable="True" IsChecked="{Binding Path=IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" ClickMode="Press" BorderThickness="0" />
                    <ContentPresenter Name="ContentSite" IsHitTestVisible="False" Content="{TemplateBinding SelectionBoxItem}" ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" Margin="5,0,20,0" VerticalAlignment="Center" HorizontalAlignment="Left" />
                    <TextBox x:Name="PART_EditableTextBox" Style="{x:Null}" Template="{StaticResource ComboBoxTextBox}" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="3,3,23,3" Focusable="True" Background="Transparent" Visibility="Hidden" IsReadOnly="{TemplateBinding IsReadOnly}" />
                    <Popup Name="Popup" Placement="Bottom" IsOpen="{TemplateBinding IsDropDownOpen}" AllowsTransparency="True" Focusable="False" PopupAnimation="Slide">
                        <Themes:SystemDropShadowChrome Margin="4,6,4,6" CornerRadius="4">
                            <Grid Name="DropDown" SnapsToDevicePixels="True" MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}">
                                <Border x:Name="DropDownBorder" Background="{StaticResource WindowBackgroundBrush}" BorderThickness="1" BorderBrush="{StaticResource SolidBorderBrush}" />
                                <ScrollViewer Margin="4,6,4,6" SnapsToDevicePixels="True">
                                    <ItemsPresenter />
                                </ScrollViewer>
                            </Grid>
                        </Themes:SystemDropShadowChrome>
                    </Popup>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="HasItems" Value="false">
                        <Setter TargetName="DropDownBorder" Property="MinHeight" Value="95"/>
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Foreground" Value="{StaticResource DisabledForegroundBrush}"/>
                    </Trigger>
                    <Trigger Property="IsGrouping" Value="true">
                        <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
                    </Trigger>
                    <Trigger SourceName="Popup" Property="Popup.AllowsTransparency" Value="true">
                        <Setter TargetName="DropDownBorder" Property="CornerRadius" Value="4"/>
                        <Setter TargetName="DropDownBorder" Property="Margin" Value="0,2,0,0"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

更新

我的ToggleButton

<ControlTemplate x:Key="ComboBoxToggleButton" TargetType="{x:Type ToggleButton}" >
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition Width="20" />
        </Grid.ColumnDefinitions>
        <Border x:Name="Border" Grid.ColumnSpan="2" BorderBrush="{StaticResource LabPetsStandardColor}" BorderThickness="1" CornerRadius="5" />
        <Border Grid.Column="0" Margin="1" Background="Transparent" BorderBrush="{StaticResource NormalBorderBrush}" BorderThickness="0" CornerRadius="5,0,0,5" />
        <Path x:Name="Arrow" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center" Data="M 0 0 L 4 4 L 8 0 Z">
            <Path.Fill>
                <SolidColorBrush Color="Black" />
            </Path.Fill>
        </Path>
    </Grid>
    <ControlTemplate.Triggers>
        <Trigger Property="ToggleButton.IsMouseOver" Value="true">
            <Setter TargetName="Border" Property="Background" Value="{StaticResource LabPetsStandardColor}" />
        </Trigger>
        <Trigger Property="ToggleButton.IsChecked" Value="true">
            <Setter TargetName="Border" Property="Background" Value="{StaticResource LabPetsPressedStandardColor}" />
        </Trigger>
        <Trigger Property="IsEnabled" Value="False">
            <Setter TargetName="Border" Property="Background" Value="{StaticResource DisabledBackgroundBrush}" />
            <Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource DisabledBorderBrush}" />
            <Setter Property="Foreground" Value="{StaticResource DisabledForegroundBrush}"/>
            <Setter TargetName="Arrow" Property="Fill" Value="{StaticResource DisabledForegroundBrush}" />
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

我的文本框

<Style x:Key="ComboBoxTextBox" TargetType="{x:Type TextBox}">
    <Setter Property="OverridesDefaultStyle" Value="True" />
    <Setter Property="AllowDrop" Value="True" />
    <Setter Property="MinWidth" Value="0" />
    <Setter Property="MinHeight" Value="0" />
    <Setter Property="FocusVisualStyle" Value="{x:Null}" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBox}">
                <ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" Background="#00FFFFFF" Name="PART_ContentHost" Focusable="False" VerticalAlignment="Center" VerticalContentAlignment="Center" Margin="0">
                    <ScrollViewer.Style>
                        <Style TargetType="ScrollViewer">
                            <Setter Property="OverridesDefaultStyle" Value="True" />
                        </Style>
                    </ScrollViewer.Style>
                </ScrollViewer>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

有人能帮我吗?

更新2

找到了一个方法,虽然不是完美的解决方案,但基本上可以用...

如果我插入属性SelectedValue和值Owner.OwnerTypeId,它就可以完美运行...但这样正确吗?

我的组合框现在:

<ComboBox Name="CbOwnerType" Grid.Column="1" Grid.Row="2" ItemsSource="{Binding Path=OwnerTypes, Mode=OneWay}" SelectedItem="{Binding Owner.OwnerType}" SelectedValue="{Binding Owner.OwnerTypeId}" SelectedValuePath="Id" DisplayMemberPath="Name" Margin="5,0,10,0" />

这是一个解决方案,虽然我不喜欢它,但这是一个解决方案... 有人能回答为什么 SelectedItem 没有按照预期工作吗?
注意:当我更改选择时,SelectedItem 起作用,只是在加载视图时不起作用。
更新3
好的,它像我说的那样工作了,但问题是WPF在我的ViewModel中进行了4次访问,因此我稍微修改了我的 ComboBox
<ComboBox Name="CbOwnerType" Grid.Column="1" Grid.Row="2" ItemsSource="{Binding Path=OwnerTypes, Mode=OneWay}" SelectedItem="{Binding Owner.OwnerType}" SelectedValue="{Binding Owner.OwnerTypeId, Mode=OneTime}" SelectedValuePath="Id" DisplayMemberPath="Name" Margin="5,0,10,0" />

所以,现在WPF只是查找OwnerTypeId,当我更改一个项目时,WPF只会命中两次。

更新4

好的,又发现了一个奇怪的事情......在另一个ComboBox中,除了SelectedValue之外,具有相同的属性,它完美地工作......我无法理解发生了什么。

更新5

抱歉,我忘记发布我的模型了。

所有者模型:

public class Owner
{
    public int Id { get; set; }
    public int OwnerTypeId { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string FormatedPhone
    {
        get
        {
            if (this.Phone == null)
                return string.Empty;

            switch (this.Phone.Length)
            {
                case 11:
                    return Regex.Replace(this.Phone, @"(\d{2})(\d{4})(\d{4})", "($1) $2-$3");
                case 12:
                    return Regex.Replace(this.Phone, @"(\d{2})(\d{5})(\d{4})", "($1) $2-$3");
                default:
                    return this.Phone;
            }
        }
    }
    public string Phone { get; set; }
    public string CellPhone { get; set; }
    public string FormatedCellPhone
    {
        get
        {
            if (this.CellPhone == null)
                return string.Empty;

            switch (this.CellPhone.Length)
            {
                case 11:
                    return Regex.Replace(this.Phone, @"(\d{2})(\d{4})(\d{4})", "($1) $2-$3");
                case 12:
                    return Regex.Replace(this.Phone, @"(\d{2})(\d{5})(\d{4})", "($1) $2-$3");
                default:
                    return this.CellPhone;
            }
        }
    }
    public string Email { get; set; }
    public virtual OwnerType OwnerType { get; set; }
    public virtual ICollection<Animal> Animals { get; set; }

    public Owner()
    {
        this.OwnerType = new OwnerType();
        this.Animals = new List<Animal>();

        this.ErrorList = new StringBuilder();
    }

模型所有者类型:

public class OwnerType
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Owner> Owners { get; set; }

    public OwnerType()
    {
        this.Owners = new List<Owner>();
    }
}

您不能同时定义SelectedItem和SelectedValue的绑定,否则组合框将无法正常工作。组合框存在一个错误,会导致其中一个属性的值丢失。 - Akash Kava
@McGarnagle,模型已添加。 - BetaSystems - Rodrigo Duarte
1
谢谢 - 我应该假设你已经把所有的INotifyPropertyChanged都放在那里了,只是为了简洁起见而省略了吗? - McGarnagle
1
还有一个笔误:我认为你必须使用Style="{StaticResource ComboBoxTextBox}"而不是Style="{x:Null}" Template="{StaticResource ComboBoxTextBox}" - McGarnagle
1
“SelectedItem”是否与“ItemsSource”中的某个项目具有相同的内存引用?最近我回答了类似的问题,所以当我看到你的问题时,这是我首先想到的事情。 - Rachel
显示剩余9条评论
3个回答

15

您的SelectedItem绑定不起作用,因为WPF通过.Equals()方法将SelectedItemItemsSource中的项进行比较,默认情况下按引用比较。而包含您的SelectedItem的内存实例与ItemsSource中的某个项的内存实例不同。

有三种处理方法:

  • 首先,正如您已经发现的,可以将SelectedValue绑定到项上的值类型属性,并设置SelectedValuePath

    <ComboBox ItemsSource="{Binding Path=OwnerTypes}" 
              SelectedValue="{Binding Owner.OwnerTypeId}" 
              SelectedValuePath="Id" DisplayMemberPath="Name"
              />
    

    通常我会选择这个解决方案,因为它通常是最简单的。

  • 其次,您可以确保您的SelectedItemItemsSource项目中的一个具有相同的内存引用。根据应用程序设计,这也不是一个坏选择。

  • public class Owner()
    {
        public int OwnerTypeId { get; set; }
    
        public OwnerType OwnerType
        {
            get 
            { 
                return StaticClass.OwnerTypes
                    .FirstOrDefault(p => p.Id == this.OwnerTypeId);
            }
    
            set
            {
                if (value != null)
                    OwnerTypeId = value.Id;
            }
        }
    }
    
  • 最后,您可以覆盖 OwnerType 对象上的 .Equals() 方法,使其在两个值的 Id 属性相同时将它们视为相等。

  • public override bool Equals(object obj) 
    { 
        if (obj == null || !(obj is OwnerType)) 
            return false; 
    
        return ((OwnerType)obj).Id == this.Id); 
    }
    

    除非我知道我将始终仅通过Id属性比较该对象的相等性,否则我通常会避免使用此方法,但有时这是最好的方法。

    此外,当您重写.Equals()时,最好覆盖.GetHashCode()

顺便提一下,通常不要同时绑定SelectedItemSelectedValue。它们是设置同一内容的两种不同方法,通过同时绑定它们可能会得到意想不到的结果。


谢谢您的回答,但我不确定WPF的工作方式,因为在整个项目中,我使用不同的内存实例中的相同方式“ItemsSource”和“SelectedItem”...随着我更新问题,在项目的其他部分,“SelectedItem”可以工作,只是在这个部分不能... - BetaSystems - Rodrigo Duarte
除非我看到你已经工作的其他代码,否则我无法确定,但最常见的情况是如果您绑定到值类型而不是引用类型,或者您的 SelectedItem 被设置为 ItemsSource 集合中的现有项而不是创建新项。我确定这是 WPF 的工作方式,因为我过去也遇到过这个问题 :) - Rachel
将 IEquatable 添加到数据中对我很有帮助。 - David Ehnis

1

让我们来看一下绑定,并分解它们的含义:

<ComboBox ItemsSource="{Binding Path=OwnerTypes, Mode=OneWay}" 
          SelectedItem="{Binding Owner.OwnerType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
          SelectedValuePath="Id" DisplayMemberPath="Name"
          />

这意味着:
  1. 列表中的项目是从属性“OwnerTypes”检索的。
  2. 所选项目将绑定到“Owner”实例内部的属性“OwnerType”。
由于SelectedValue没有应用绑定,因此将完全忽略SelectedValuePath。请注意SelectedValuePath定义

获取或设置用于从SelectedItem获取SelectedValue的路径。

因此,现在让我们省略#3。在处理#1和#2时,假设属性“OwnerTypes”的类型为List<OwnerTypeDef>。这意味着类型OwnerTypeDef与“Owner”属性内部的属性“OwnerType”类型相同。通过这种设置--
public class OwnerDef : INotifyPropertyChanged   // TODO implement INotifyPropertyChanged 
{
    public OwnerTypeDef OwnerType
    {
        get { return _ownerType; }
        set
        {
            if (_ownerType == value)
                return;
            _ownerType = value;
            RaisePropertyChanged();
        }
    }
    private OwnerTypeDef _ownerType;
}

public class OwnerTypeDef : INotifyPropertyChanged
{
    public string Name
    {
        get { return _name; }
        set
        {
            if (_name == value)
                return;
            _name = value;
            RaisePropertyChanged();
        }
    }
    private string _name;

    public int Id
    {
        get { return _id; }
        set
        {
            if (_id == value)
                return;
            _id = value;
            RaisePropertyChanged();
        }
    }
    private int _id;
}

public class ViewModel : INotifyPropertyChanged
{
    public List<OwnerTypeDef> OwnerTypes
    {
        get { return _ownerTypes; }
        set { _ownerTypes = value; }
    }
    private List<OwnerTypeDef> _ownerTypes = new List<OwnerTypeDef>
        {
            new OwnerTypeDef { Name = "foo", Id = 1, },
            new OwnerTypeDef { Name = "bar", Id = 2, },
            new OwnerTypeDef { Name = "baz", Id = 3, },
        };

    public OwnerDef Owner
    {
        get { return _owner; }
        set
        {
            if (_owner == value)
                return;
            _owner = value;
            RaisePropertyChanged();
        }
    }
    private OwnerDef _owner = new OwnerDef();
}

-- 绑定对我有效。当我在UI中更改选择时,“Owner”内部的“OwnerType”属性会得到更新。 编辑 让我们看看另一种情况,您使用SelectedValuePathSelectedValue。 在这种情况下,我们将选定的绑定到“Owner.OwnerTypeId”(一个整数)。 我们将使用SelectedValuePath=Id,它告诉框架在所选项目中查找“Id”属性(这是OwnerDef.Id,一个int)。 因此,我们必须在OwnerDef类中添加一个匹配的int属性,称之为“OwnerTypeId”。 在这种情况下,XAML将是:
<ComboBox ItemsSource="{Binding Path=OwnerTypes, Mode=OneWay}" 
          SelectedValue="{Binding Owner.OwnerTypeId, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
          SelectedValuePath="Id" DisplayMemberPath="Name"
          />

使用这个设置,绑定可以正确更新“OwnerTypeId”。
编辑#2
还可以同时使用SelectedItem和SelectedValue,以便更新“OwnerType”和“OwnerTypeID”属性。确保使用双向绑定(在上面的更新#3中它们是默认的单向绑定):
<ComboBox ItemsSource="{Binding OwnerTypes}" 
          SelectedValuePath="Id" SelectedValue="{Binding Owner.OwnerTypeId,Mode=TwoWay}"
          SelectedItem="{Binding Owner.OwnerType,Mode=TwoWay}"
          DisplayMemberPath="Name"
          />

这个设置会在我选择下拉框中的项目时更新“OwnerTypeId”和“OwnerType”属性。

正如我在第四次更新中所说:`更新4好的,又发现了一个奇怪的问题...在另一个ComboBox中,除了SelectedValue之外,具有相同属性,它完美地工作...我无法理解发生了什么。`所以,我理解你的答案,但如果可能的话,我想继续那种方式工作。 - BetaSystems - Rodrigo Duarte
这种方法可以工作,但不是正确的方法,因为当我提交到数据库时,我需要分配或加载OwnerType,而使用SelectedValue时,在提交之前我没有设置任何值。 - BetaSystems - Rodrigo Duarte
@BetaSystems-RodrigoDuarte 但在更新#3中,SelectedValue绑定是OneTime,所以当然不会被更新。 您只需要使用SelectedValue或SelectedItem之一,而不是两者都使用。 只需从Owner setter(可能最简单)设置OwnerTypeId,或反之亦然。 - McGarnagle
@BetaSystems-RodrigoDuarte 我尝试使用SelectedValueSelectedItem两种方式,似乎都可以工作 - 请参见上面的第2次编辑。您的问题可能只是因为您的第3次更新使用了单向绑定吗? - McGarnagle
这样做可以使其在“OneWay”绑定中工作,但我怀疑为什么没有“SelectedValue”也不能工作?因为所有其他ComboBox都只使用“SelectedItem”。 - BetaSystems - Rodrigo Duarte

1
你需要对下拉框的selectedValue进行模板绑定。因为你覆盖了控件模板并且放置了自己的TextBlock以显示所选的值。现在当值从UI中被选中时,模板会处理它的显示,但它没有将值设置为SelectedValue或SelectedItem。
<TextBox x:Name="PART_EditableTextBox" Style="{x:Null}" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="3,3,23,3" Focusable="True" Background="Transparent" Visibility="Hidden" IsReadOnly="{TemplateBinding IsReadOnly}" Text="{TemplateBinding SelectedValue}"/> 

希望这可以帮助你


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