Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF isn't binding ComboBox SelectedItem

I already search widely, but I can't find any solution to my case.

I have several ComboBox's at my project and I was searching for an AutoComplete solution, then I found a good one and applied in my project, I applied solution's style as well to all ComboBox in my project.

After that, SelectedItem stopped working, some one can help me ?

My 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" />

My style:

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

UPDATE

My 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>

My TextBox

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

Some, please, can help me ?

UPDATE 2

Found a hack, not the perfect solution, but kind of works...

If I insert property SelectedValue and the value Owner.OwnerTypeId, it works like a charm... But, it's right this?

My Combobox now:

<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" />

This is a solution, not that I like it, but it's a solution... Some one can answer why SelectedItem isn't working as it should ?

Obs.: When I change the selection, SelectedItem works, just doesn't work when I load my view.

UPDATE 3

Ok, it's worked like I said, but the problem is that WPF is hitting 4 times at my ViewModel, so I changed my ComboBox a little:

<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" />

So, now, WPF just search the OwnerTypeId and when I change an item, WPF just hits 2 times.

UPDATE 4

Ok, another strange finding... In another ComboBox, with the same properties, except SelectedValue, it's working perfect... I can't understand what is happening.

UPDATE 5

Sorry about that, I forgot to post my models.

Model Owner:

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

Model OwnerType:

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>();
    }
}
like image 767
BetaSystems - Rodrigo Duarte Avatar asked Dec 13 '13 22:12

BetaSystems - Rodrigo Duarte


3 Answers

Your SelectedItem binding isn't working because WPF compares the SelectedItem to the items in the ItemsSource by the .Equals() method, which by default compares by reference. And the instance in memory containing your SelectedItem is not the same instance in memory as one of the items in your ItemsSource

There are 3 ways to handle this.

  • First, as you've already discovered you can bind SelectedValue to a value type property on your item, and set SelectedValuePath.

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

    This is usually the solution I go with because it's often easiest

  • Second, you could ensure your SelectedItem is set to the same reference in memory as one of the ItemsSource items. Depending on the application design, this isn't a bad choice either.

    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;
            }
        }
    }
    
  • And last of all, you could override the .Equals() method on the OwnerType object so it considers the two values equal if the Id properties are the same.

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

    I usually try to avoid this method unless I know I'll always want to compare this object's equality by the Id property only, but sometimes this is the best way to go.

    Also, its good practice to override .GetHashCode() whenever you overwrite .Equals().

As a side note, you usually don't want to be binding both SelectedItem and SelectedValue. They are two different ways of setting the same thing, and you can get unexpected results by binding both of them.

like image 64
Rachel Avatar answered Nov 08 '22 05:11

Rachel


You have to do a Template binding of selectedValue of comobox. Because you have overriden the control template and placed your own TextBlock to display the selected value. Now when the value is selected from the UI control Template takes care of displaying it. but it is not setting the value to the SelectedValue or 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}"/> 

Hope this helps you

like image 36
Kumareshan Avatar answered Nov 08 '22 06:11

Kumareshan


Let's look at the bindings, and break down what they say:

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

So this means:

  1. The items in the list are retrieved from the property "OwnerTypes".
  2. The selected item will be bound to the property "OwnerType" inside the instance of the property "Owner".

Since there is no binding applied to SelectedValue, the SelectedValuePath will be completely ignored. Note the definition of SelectedValuePath:

Gets or sets the path that is used to get the SelectedValue from the SelectedItem.

So let's omit #3 for now. Working with #1 and #2, let's assume that the property "OwnerTypes" is of type List<OwnerTypeDef>. This implies that the type OwnerTypeDef is the same type as the property "OwnerType" inside "Owner" property. With this setup --

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

-- the binding works for me. The property "OwnerType" inside "Owner" gets updated when I change the selection in the UI.

Edit

Let's look at the other scenario, where you use SelectedValuePath and SelectedValue. In this case, we are binding the selected value to "Owner.OwnerTypeId" (an integer). We'll use SelectedValuePath=Id, which tells the framework to look for the "Id" property in the selected item (which is OwnerDef.Id, an int). Therefore, we'll have to add a matching int property in the OwnerDef class, call it "OwnerTypeId". XAML in this case would be:

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

With this setup the binding is updating the "OwnerTypeId" correctly.

Edit #2

It's also possible use both SelectedItem and SelectedValue, so that both the "OwnerType" and "OwnerTypeID" properties get updated. Make sure to use TwoWay bindings (in Update #3 above they are the default OneWay):

<ComboBox ItemsSource="{Binding OwnerTypes}" 
          SelectedValuePath="Id" SelectedValue="{Binding Owner.OwnerTypeId,Mode=TwoWay}"
          SelectedItem="{Binding Owner.OwnerType,Mode=TwoWay}"
          DisplayMemberPath="Name"
          />

This setup updates both the "OwnerTypeId" and the "OwnerType" properties when I select an item in the combo box.

like image 42
McGarnagle Avatar answered Nov 08 '22 05:11

McGarnagle